From 4ae827ada491d1add33c32c72453f0660fdb306f Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:17:14 +0100 Subject: [PATCH 01/22] filter expr impl --- .../src/core/entities/properties/prop/mod.rs | 1 - raphtory-graphql/src/model/graph/filtering.rs | 76 +- raphtory/src/db/api/state/ops/filter.rs | 10 +- raphtory/src/db/api/state/ops/node.rs | 1 + .../db/graph/views/filter/model/attribute.rs | 1231 +++++++++++++++++ .../graph/views/filter/model/edge_filter.rs | 4 +- .../src/db/graph/views/filter/model/filter.rs | 47 +- .../views/filter/model/filter_operator.rs | 197 ++- .../graph/views/filter/model/filter_value.rs | 50 + .../src/db/graph/views/filter/model/mod.rs | 9 +- .../views/filter/model/node_filter/mod.rs | 53 +- .../filter/model/node_filter/validate.rs | 34 +- .../views/filter/model/property_filter/mod.rs | 12 +- .../src/python/filter/node_filter_builders.rs | 129 +- raphtory/src/search/query_builder.rs | 16 +- 15 files changed, 1628 insertions(+), 242 deletions(-) create mode 100644 raphtory/src/db/graph/views/filter/model/attribute.rs create mode 100644 raphtory/src/db/graph/views/filter/model/filter_value.rs 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/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index aa7d7796e7..73b191a567 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, ), 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/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/graph/views/filter/model/attribute.rs b/raphtory/src/db/graph/views/filter/model/attribute.rs new file mode 100644 index 0000000000..d995601505 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/attribute.rs @@ -0,0 +1,1231 @@ +use crate::{ + db::{ + api::{ + properties::PropertiesOps, + state::ops::{Const, Degree, Name, NodeOp, Type}, + view::{internal::GraphView, NodeViewOps}, + }, + graph::views::filter::{ + model::{ + edge_filter::CompositeEdgeFilter, + filter_operator::{BinaryOp, SetOp, UnaryOp}, + ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, + TryAsCompositeFilter, + }, + node_filtered_graph::NodeFilteredGraph, + }, + }, + errors::GraphError, + prelude::GraphViewOps, +}; +use raphtory_api::core::{ + entities::{properties::prop::Prop, VID}, + Direction, +}; +use raphtory_storage::graph::graph::GraphStorage; +use std::{collections::HashSet, hash::Hash, sync::Arc}; +use strsim::levenshtein; + +// ───────────────────────────────────────────────────────────────────────────── +// Comparable — type-driven dispatch for BinOpNodeOp +// ───────────────────────────────────────────────────────────────────────────── + +/// Comparison trait used by `BinOpNodeOp` to evaluate a `BinaryOp` against two values. +/// +/// Implemented for `usize`, `String`, `Prop`, and `Option`. +/// The `Option` impl handles `None` symmetrically: `(None, None)` is equal, +/// one `None` is unequal, and ordering ops return `false` when either side is `None`. +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, + _ => false, + } + } +} + +impl Comparable for String { + fn binary_cmp(op: &BinaryOp, left: &String, right: &String) -> bool { + // Coerce to &str to avoid ambiguity with NodeExprFilterOps methods of the same name. + let (l, r): (&str, &str) = (left, right); + 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, + BinaryOp::StartsWith => l.starts_with(r), + BinaryOp::EndsWith => l.ends_with(r), + BinaryOp::Contains => l.contains(r), + BinaryOp::NotContains => !l.contains(r), + BinaryOp::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 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), + _ => false, + } + } +} + +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), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Unwrap — constrains Output = Option +// ───────────────────────────────────────────────────────────────────────────── + +pub trait Unwrap { + type Inner; + fn is_some(&self) -> bool; + fn is_none(&self) -> bool; + fn unwrap_inner(self) -> Option; +} + +impl Unwrap for Option { + type Inner = T; + fn is_some(&self) -> bool { + Option::is_some(self) + } + fn is_none(&self) -> bool { + Option::is_none(self) + } + fn unwrap_inner(self) -> Option { + self + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Attribute — user-defined value extractor +// ───────────────────────────────────────────────────────────────────────────── + +/// A typed attribute that can be extracted from a graph entity. +/// +/// `E` is the entity identifier type (e.g. `VID` for nodes). +/// User-defined attributes implement this trait and can be wrapped in +/// `AttrNodeExpr` to use them as `NodeExpr`. +// pub trait Attribute: Clone + Send + Sync + 'static { +// type Output: PartialEq + PartialOrd + Eq + Hash + Clone + Send + Sync; +// +// fn extract<'graph, G: GraphViewOps<'graph>>( +// &self, +// graph: &G, +// entity: E, +// ) -> Option; +// } + +// ───────────────────────────────────────────────────────────────────────────── +// NodeExpr — typed node expression with associated Output type +// ───────────────────────────────────────────────────────────────────────────── + +/// A typed expression that produces a value per node. +/// +/// `Output` carries nullability directly: `Option` for properties that +/// may be absent, `Option` for name/type, `Option` for degree. +/// +/// 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") +/// ``` +/// +/// Wrap a user-defined `Attribute` in `AttrNodeExpr` to use it here. +pub trait NodeExpr: Clone + Send + Sync + 'static { + type Output: Comparable + 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>; +} + +// ───────────────────────────────────────────────────────────────────────────── +// OptionWrapOp — adapts NodeOp to NodeOp> +// ───────────────────────────────────────────────────────────────────────────── + +/// Wraps an inner `NodeOp` and returns `Some(inner.apply(...))`. +/// +/// Used by `DegreeExpr` and `Name` to produce `Option`-wrapped outputs from +/// the existing `Degree` and `Name` ops in `db/api/state/ops/node.rs`, +/// without reimplementing their logic. +#[derive(Clone)] +pub(crate) struct OptionWrapOp(O); + +impl NodeOp for OptionWrapOp +where + O::Output: Clone + Send + Sync + 'static, +{ + type Output = Option; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + Some(self.0.apply(storage, node)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// NodeTypeStringOp — maps Type's Option to Option +// ───────────────────────────────────────────────────────────────────────────── + +/// Evaluates `Type` from `node.rs` and converts `ArcStr` to `String`. +/// +/// `Type: NodeOp>` — this op converts to `Option` +/// without reimplementing the type-id lookup logic. +#[derive(Clone)] +pub(crate) struct NodeTypeStringOp; + +impl NodeOp for NodeTypeStringOp { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + Type.apply(storage, node).map(|a| a.to_string()) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// NodePropOp — property/metadata lookup, prop_id resolved at creation time +// ───────────────────────────────────────────────────────────────────────────── + +/// Evaluates a named property or metadata field. +/// +/// The property name is resolved to a column ID once in `create_node_op`; per-node +/// evaluation is O(1). +#[derive(Clone)] +pub(crate) struct NodePropOp { + // split into prop_op and metadata_op + graph: G, + prop_id: usize, + is_metadata: bool, +} + +impl NodeOp for NodePropOp { + type Output = Option; + + fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { + let n = self.graph.node(node)?; + if self.is_metadata { + n.metadata().get_by_id(self.prop_id) + } else { + n.properties().get_by_id(self.prop_id) + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// AttributeNodeOp — bridges Attribute to NodeOp> +// ───────────────────────────────────────────────────────────────────────────── + +// #[derive(Clone)] +// pub(crate) struct AttributeNodeOp { +// attribute: A, +// graph: G, +// } + +// impl, G: GraphView> NodeOp for AttributeNodeOp { +// type Output = Option; +// +// fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { +// self.attribute.extract(&self.graph, node) +// } +// } + +// ───────────────────────────────────────────────────────────────────────────── +// AttrNodeExpr — wraps Attribute as a NodeExpr +// ───────────────────────────────────────────────────────────────────────────── + +/// Wraps a user-defined `Attribute` so it can be used as a `NodeExpr`. +/// +/// User-defined attributes are NOT serializable (`try_as_spec` returns `None`), +/// so `BinOpNodeFilter` built from them cannot be stored in the permissions store. +/// +/// Usage: +/// ```rust,ignore +/// AttrNodeExpr(MyDegreeAttr).gt(2usize) +/// AttrNodeExpr(MyDegreeAttr).gt(AttrNodeExpr(MyHalfDegreeAttr)) +/// ``` +// #[derive(Clone)] +// pub struct AttrNodeExpr(pub A); +// +// impl> NodeExpr for AttrNodeExpr +// where +// A::Output: Comparable + Clone + Send + Sync + 'static, +// Option: Comparable, +// { +// type Output = Option; +// +// fn create_node_op<'g, G: GraphView + 'g>( +// &self, +// graph: G, +// ) -> Result> + 'g>, GraphError> { +// Ok(Arc::new(AttributeNodeOp { attribute: self.0.clone(), graph })) +// } +// } + +// ───────────────────────────────────────────────────────────────────────────── +// Concrete expression structs +// ───────────────────────────────────────────────────────────────────────────── + +/// Degree expression (total / in / out). +/// +/// Delegates to `Degree` from `db/api/state/ops/node.rs` — no reimplementation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DegreeExpr { + Total, + In, + Out, +} + +impl NodeExpr for DegreeExpr { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let dir = match self { + DegreeExpr::Total => Direction::BOTH, + DegreeExpr::In => Direction::IN, + DegreeExpr::Out => Direction::OUT, + }; + Ok(Arc::new(OptionWrapOp(Degree { dir, view: graph }))) + } +} + +/// Current (latest) value of a named property. +/// +/// The property name is resolved to a column ID once at `create_node_op` time. +#[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, + is_metadata: false, + })) + } +} + +/// Static metadata field. +/// +/// The metadata name is resolved to a column ID once at `create_node_op` time. +#[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(NodePropOp { + graph, + prop_id, + is_metadata: true, + })) + } +} + +/// `Type` from `db/api/state/ops/node.rs` used as a node expression. +/// +/// `Type: NodeOp>` — this impl converts to `Option` +/// via `NodeTypeStringOp` without reimplementing the type-id lookup. +impl NodeExpr for Type { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(NodeTypeStringOp)) + } +} + +/// `Name` from `db/api/state/ops/node.rs` used as a node expression. +/// +/// Wraps the existing `Name` op via `OptionWrapOp` so it fits the +/// `NodeExpr>` interface without reimplementation. +impl NodeExpr for Name { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(OptionWrapOp(Name))) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// NodeExpr impls for constant value types +// +// 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 = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(*self)))) + } +} + +impl NodeExpr for String { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.clone())))) + } +} + +impl NodeExpr for &'static str { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.to_string())))) + } +} + +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())))) + } +} + +// TODO: try IntoProp +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`, etc.) can be passed directly; +/// `ConstExpr` is only needed for custom attribute output types. +#[derive(Clone)] +pub struct ConstExpr(pub T) +where + Option: Comparable; + +impl NodeExpr for ConstExpr +where + Option: Comparable, +{ + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.0.clone())))) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// BinOpNodeOp<'g, T> — compares two NodeOp using BinaryOp +// ───────────────────────────────────────────────────────────────────────────── + +/// Execution op for `BinOpNodeFilter`. +/// +/// Holds two compiled `NodeOp` (type-erased via `Arc`) +/// and applies `T::binary_cmp`. The `'g` lifetime bounds both ops to the graph +/// view they were compiled against. +#[derive(Clone)] +pub struct BinOpNodeOp<'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 BinOpNodeOp<'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) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// UnaryNodeOp<'g, T> — evaluates is_some / is_none +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub struct UnaryNodeOp<'g, T: Unwrap + Clone + Send + Sync + 'static> +where + T::Inner: Clone + Send + Sync + 'static, +{ + inner: Arc + 'g>, + op: UnaryOp, +} + +impl<'g, T: Unwrap + Clone + Send + Sync + 'static> NodeOp for UnaryNodeOp<'g, T> +where + T::Inner: Clone + Send + Sync + 'static, +{ + 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(), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// SetNodeOp<'g, T> — evaluates is_in / is_not_in +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub struct SetNodeOp<'g, T: Unwrap + Clone + Send + Sync + 'static> +where + T::Inner: Eq + Hash + Clone + Send + Sync + 'static, +{ + inner: Arc + 'g>, + op: SetOp, + values: Arc>, +} + +impl<'g, T: Unwrap + Clone + Send + Sync + 'static> NodeOp for SetNodeOp<'g, T> +where + T::Inner: Eq + Hash + Clone + Send + Sync + 'static, +{ + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + let v = self.inner.apply(storage, node).unwrap_inner(); + 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), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// BinOpNodeFilter — binary expression filter (no PhantomData) +// ───────────────────────────────────────────────────────────────────────────── + +/// 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. No `PhantomData` required +/// because the output type is encoded as an associated type of `L`. +/// +/// Created by `NodeExprFilterOps`: +/// ```rust,ignore +/// DegreeExpr::Total.gt(2usize) +/// DegreeExpr::Out.gt(DegreeExpr::In) +/// NodeFilter::property("age").gt(30i64) +/// NodeFilter::name().eq("Alice") +/// ``` +pub struct BinOpNodeFilter +where + L: NodeExpr, + R: NodeExpr, +{ + pub left: L, + pub op: BinaryOp, + pub right: R, +} + +impl BinOpNodeFilter +where + L: NodeExpr, + R: NodeExpr, +{ + pub fn new(left: L, op: BinaryOp, right: R) -> Self { + Self { left, op, right } + } +} + +impl Clone for BinOpNodeFilter +where + L: NodeExpr, + R: NodeExpr, +{ + fn clone(&self) -> Self { + Self { + left: self.left.clone(), + op: self.op, + right: self.right.clone(), + } + } +} + +impl ComposableFilter for BinOpNodeFilter +where + L: NodeExpr, + R: NodeExpr, +{ +} + +impl CreateFilter for BinOpNodeFilter +where + L: NodeExpr, + R: NodeExpr, + L::Output: Comparable, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodeFilteredGraph>; + + type NodeFilter<'graph, G: GraphView + 'graph> = BinOpNodeOp<'graph, L::Output>; + + 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)?; + Ok(BinOpNodeOp { + left, + right, + op: self.op, + }) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } +} + +impl TryAsCompositeFilter for BinOpNodeFilter +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()` and `.is_none()` on any `NodeExpr` whose `Output` +/// implements `Unwrap` (i.e., is an `Option`). +pub struct UnaryNodeFilter +where + E::Output: Unwrap, +{ + pub expr: E, + pub op: UnaryOp, +} + +impl Clone for UnaryNodeFilter +where + E::Output: Unwrap, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + op: self.op, + } + } +} + +impl ComposableFilter for UnaryNodeFilter where E::Output: Unwrap {} + +impl CreateFilter for UnaryNodeFilter +where + E::Output: Unwrap + Clone + Send + Sync + 'static, + ::Inner: Clone + Send + Sync + 'static, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodeFilteredGraph>; + + type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, E::Output>; + + 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 }) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } +} + +impl TryAsCompositeFilter for UnaryNodeFilter +where + E::Output: Unwrap + Clone + Send + Sync + 'static, + ::Inner: 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 the inner value of an `Option`-valued +/// expression is contained in (or absent from) a fixed set. +/// +/// Created by `.is_in(values)` and `.is_not_in(values)`. +#[derive(Clone)] +pub struct SetNodeFilter +where + E::Output: Unwrap, + ::Inner: Eq + Hash + Clone, +{ + pub expr: E, + pub op: SetOp, + pub values: Arc::Inner>>, +} + +// impl Clone for SetNodeFilter +// where +// E::Output: Unwrap, +// ::Inner: Eq + Hash + Clone, +// { +// fn clone(&self) -> Self { +// Self { expr: self.expr.clone(), op: self.op, values: self.values.clone() } +// } +// } + +impl ComposableFilter for SetNodeFilter +where + E::Output: Unwrap, + ::Inner: Eq + Hash + Clone, +{ +} + +impl CreateFilter for SetNodeFilter +where + E::Output: Unwrap + Clone + Send + Sync + 'static, + ::Inner: Eq + Hash + Clone + Send + Sync + 'static, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodeFilteredGraph>; + + type NodeFilter<'graph, G: GraphView + 'graph> = SetNodeOp<'graph, E::Output>; + + 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, + }) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } +} + +impl TryAsCompositeFilter for SetNodeFilter +where + E::Output: Unwrap + Clone + Send + Sync + 'static, + ::Inner: 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) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// NodeExprFilterOps — comparison and set operators on NodeExpr +// ───────────────────────────────────────────────────────────────────────────── + +/// Comparison, string, set, and presence operators on any `NodeExpr`. +/// +/// `gt(rhs)` accepts any `R: NodeExpr`: +/// ```rust,ignore +/// DegreeExpr::Total.gt(2usize) +/// DegreeExpr::Out.gt(DegreeExpr::In) +/// NodeFilter::property("age").gt(30i64) +/// AttrNodeExpr(MyAttr).is_in([2usize, 3usize]) +/// ``` +pub trait NodeExprFilterOps: NodeExpr + Sized { + fn gt>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::Gt, rhs) + } + + fn ge>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::Ge, rhs) + } + + fn lt>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::Lt, rhs) + } + + fn le>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::Le, rhs) + } + + fn eq>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::Eq, rhs) + } + + fn ne>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::Ne, rhs) + } + + fn starts_with>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::StartsWith, rhs) + } + + fn ends_with>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::EndsWith, rhs) + } + + fn contains>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::Contains, rhs) + } + + fn not_contains>(self, rhs: R) -> BinOpNodeFilter { + BinOpNodeFilter::new(self, BinaryOp::NotContains, rhs) + } + + fn fuzzy_search>( + self, + rhs: R, + levenshtein_distance: usize, + prefix_match: bool, + ) -> BinOpNodeFilter { + BinOpNodeFilter::new( + self, + BinaryOp::FuzzySearch { + levenshtein_distance, + prefix_match, + }, + rhs, + ) + } + + fn is_some(self) -> UnaryNodeFilter + where + Self::Output: Unwrap, + { + UnaryNodeFilter { + expr: self, + op: UnaryOp::IsSome, + } + } + + fn is_none(self) -> UnaryNodeFilter + where + Self::Output: Unwrap, + { + UnaryNodeFilter { + expr: self, + op: UnaryOp::IsNone, + } + } + + fn is_in(self, values: I) -> SetNodeFilter + where + Self::Output: Unwrap, + ::Inner: Eq + Hash + Clone, + I: IntoIterator::Inner>, + { + let set: HashSet<_> = values.into_iter().collect(); + SetNodeFilter { + expr: self, + op: SetOp::IsIn, + values: Arc::new(set), + } + } + + fn is_not_in(self, values: I) -> SetNodeFilter + where + Self::Output: Unwrap, + ::Inner: Eq + Hash + Clone, + I: IntoIterator::Inner>, + { + let set: HashSet<_> = values.into_iter().collect(); + SetNodeFilter { + expr: self, + op: SetOp::IsNotIn, + values: Arc::new(set), + } + } +} + +impl NodeExprFilterOps for E {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}; + + // ── user-defined attributes (via Attribute — not serializable) ────── + + #[derive(Clone)] + struct DegreeAttr; + + impl Attribute for DegreeAttr { + type Output = usize; + + fn extract<'graph, G: GraphViewOps<'graph>>( + &self, + graph: &G, + entity: VID, + ) -> Option { + graph.node(entity).map(|n| n.degree()) + } + } + + #[derive(Clone)] + struct HalfDegreeAttr; + + impl Attribute for HalfDegreeAttr { + type Output = usize; + + fn extract<'graph, G: GraphViewOps<'graph>>( + &self, + graph: &G, + entity: VID, + ) -> Option { + graph.node(entity).map(|n| n.degree() / 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 + } + + // ── NodeExprFilterOps comparison tests ─────────────────────────────────── + + #[test] + fn degree_ge_2_keeps_high_degree_nodes() { + let g = build_test_graph(); + assert_eq!( + filtered_names(AttrNodeExpr(DegreeAttr).ge(2usize), g), + vec!["a", "b", "c"] + ); + } + + #[test] + fn degree_eq_1_keeps_no_nodes() { + let g = build_test_graph(); + assert!(filtered_names(AttrNodeExpr(DegreeAttr).eq(1usize), g).is_empty()); + } + + #[test] + fn degree_le_2_keeps_all_nodes() { + let g = build_test_graph(); + assert_eq!( + filtered_names(AttrNodeExpr(DegreeAttr).le(2usize), g), + vec!["a", "b", "c"] + ); + } + + #[test] + fn degree_gt_2_keeps_no_nodes() { + let g = build_test_graph(); + assert!(filtered_names(AttrNodeExpr(DegreeAttr).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(AttrNodeExpr(DegreeAttr).ne(2usize), g).is_empty()); + } + + // ── unified gt: constant and expression RHS use the SAME method ────────── + + #[test] + fn degree_gt_half_degree_unified_method() { + let g = build_test_graph(); + assert_eq!( + filtered_names(AttrNodeExpr(DegreeAttr).gt(AttrNodeExpr(HalfDegreeAttr)), g), + vec!["a", "b", "c"] + ); + } + + #[test] + fn degree_eq_half_degree_keeps_no_nodes_when_unequal() { + let g = build_test_graph(); + assert!( + filtered_names(AttrNodeExpr(DegreeAttr).eq(AttrNodeExpr(HalfDegreeAttr)), g).is_empty() + ); + } + + // ── set / unary ops via NodeExprFilterOps ──────────────────────────────── + + #[test] + fn degree_is_some_keeps_all_nodes() { + let g = build_test_graph(); + assert_eq!( + filtered_names(AttrNodeExpr(DegreeAttr).is_some(), g), + vec!["a", "b", "c"] + ); + } + + #[test] + fn degree_is_none_keeps_no_nodes() { + let g = build_test_graph(); + assert!(filtered_names(AttrNodeExpr(DegreeAttr).is_none(), g).is_empty()); + } + + #[test] + fn degree_is_in_set() { + let g = build_test_graph(); + assert_eq!( + filtered_names(AttrNodeExpr(DegreeAttr).is_in([2usize]), g), + vec!["a", "b", "c"] + ); + } + + #[test] + fn degree_is_not_in_set_excludes_matching_nodes() { + let g = build_test_graph(); + assert!(filtered_names(AttrNodeExpr(DegreeAttr).is_not_in([2usize]), g).is_empty()); + } + + // ── built-in DegreeExpr ────────────────────────────────────────────────── + + #[test] + fn builtin_degree_ge_2() { + let g = build_test_graph(); + assert_eq!( + filtered_names(DegreeExpr::Total.ge(2usize), g), + vec!["a", "b", "c"] + ); + } + + // ── ConstExpr still works for custom output types ───────────────────────── + + #[test] + fn const_expr_still_works() { + let filter = BinOpNodeFilter::new(ConstExpr(2usize), BinaryOp::Eq, ConstExpr(2usize)); + let g = build_test_graph(); + assert_eq!(filtered_names(filter, g), vec!["a", "b", "c"]); + } +} 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..7c5cbc403d 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -162,12 +162,12 @@ impl EdgeEndpointWrapper { #[inline] pub fn name(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::name(), self.endpoint) + EdgeEndpointWrapper::new(NodeNameFilterBuilder, self.endpoint) } #[inline] pub fn node_type(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::node_type(), self.endpoint) + EdgeEndpointWrapper::new(NodeTypeFilterBuilder, self.endpoint) } } 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..c63159b81b 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,92 @@ 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}; use std::{collections::HashSet, fmt, fmt::Display, ops::Deref}; use strsim::levenshtein; +// ───────────────────────────────────────────────────────────────────────────── +// Focused operator enums for the NodeExpr expression system +// ───────────────────────────────────────────────────────────────────────────── + +/// Binary comparison / string operators used by `BinOpNodeFilter`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOp { + Eq, + Ne, + Lt, + Le, + Gt, + Ge, + StartsWith, + EndsWith, + Contains, + NotContains, + FuzzySearch { + levenshtein_distance: usize, + prefix_match: bool, + }, +} + +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, ">="), + BinaryOp::StartsWith => write!(f, "STARTS_WITH"), + BinaryOp::EndsWith => write!(f, "ENDS_WITH"), + BinaryOp::Contains => write!(f, "CONTAINS"), + BinaryOp::NotContains => write!(f, "NOT_CONTAINS"), + BinaryOp::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 +207,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 +222,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 +272,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 +281,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 +310,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 +318,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 +337,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 +356,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 +369,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 +379,71 @@ impl FilterOperator { }, } } + + /// Compare two optional values symmetrically. + /// + /// Used by `BinOpNodeFilter` 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/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 967f6794e0..a60e1fe9df 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -29,12 +29,17 @@ pub use crate::{ graph::views::{ filter::{ model::{ + attribute::{ + AttrNodeExpr, Attribute, BinOpNodeFilter, Comparable, ConstExpr, + DegreeExpr, Metadata, NodeExpr, NodeExprFilterOps, Property, SetNodeFilter, + UnaryNodeFilter, Unwrap, + }, edge_filter::{EdgeEndpointWrapper, EdgeFilter}, exploded_edge_filter::{ CompositeExplodedEdgeFilter, ExplodedEdgeEndpointWrapper, ExplodedEdgeFilter, }, - filter_operator::FilterOperator, + filter_operator::{BinaryOp, FilterOperator, SetOp, UnaryOp}, node_filter::{NodeFilter, NodeNameFilter, NodeTypeFilter}, not_filter::NotFilter, or_filter::OrFilter, @@ -56,10 +61,12 @@ use raphtory_api::core::{ use std::{ops::Deref, sync::Arc}; pub mod and_filter; +pub mod attribute; 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; 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..6950768e47 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, + Name, NodeOp, Type, TypeId, }, NodeStateValue, TypedNodeState, }, @@ -15,15 +15,13 @@ use crate::{ }, graph::views::filter::{ model::{ + attribute::{DegreeExpr, Metadata, Property}, edge_filter::CompositeEdgeFilter, filter::Filter, is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, - node_filter::{ - builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, - validate::validate, - }, + node_filter::{builders::NodeIdFilterBuilder, validate::validate}, node_state_filter::NodeStateBoolColOp, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, snapshot_filter::{SnapshotAt, SnapshotLatest}, @@ -61,14 +59,21 @@ impl NodeFilter { NodeIdFilterBuilder } + /// 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 + pub fn name() -> Name { + Name } + /// Selects the node type field for filtering. + /// + /// Returns `Type` which implements `NodeExprFilterOps`. #[inline] - pub fn node_type() -> NodeTypeFilterBuilder { - NodeTypeFilterBuilder + pub fn node_type() -> Type { + Type } /// Build a filter from a boolean column inside a TypedNodeState. @@ -82,6 +87,36 @@ impl NodeFilter { { state.bool_col_filter(col) } + + /// Total degree expression — serializable, supports `.gt(n)`, `.lt(n)`, etc. + #[inline] + pub fn degree() -> DegreeExpr { + DegreeExpr::Total + } + + /// In-degree expression — serializable. + #[inline] + pub fn in_degree() -> DegreeExpr { + DegreeExpr::In + } + + /// Out-degree expression — serializable. + #[inline] + pub fn out_degree() -> DegreeExpr { + DegreeExpr::Out + } + + /// Current (latest) value of a named property — serializable. + #[inline] + pub fn property(name: impl Into) -> Property { + Property::new(name) + } + + /// Static metadata field — serializable. + #[inline] + pub fn metadata(name: impl Into) -> Metadata { + Metadata::new(name) + } } impl Wrap for NodeFilter { 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/property_filter/mod.rs b/raphtory/src/db/graph/views/filter/model/property_filter/mod.rs index ce064a1048..3bf3fe8a3b 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,7 +42,7 @@ 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; @@ -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, diff --git a/raphtory/src/python/filter/node_filter_builders.rs b/raphtory/src/python/filter/node_filter_builders.rs index 01613b34b0..d646080bd6 100644 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ b/raphtory/src/python/filter/node_filter_builders.rs @@ -1,13 +1,17 @@ use crate::{ - db::graph::views::filter::model::{ - node_filter::{ - builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, - ops::{NodeFilterOps, NodeIdFilterOps}, - NodeFilter, + db::{ + api::state::ops::{Name, Type}, + graph::views::filter::model::{ + attribute::NodeExprFilterOps, + node_filter::{ + builders::NodeIdFilterBuilder, + ops::{NodeFilterOps, NodeIdFilterOps}, + 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::{ @@ -217,7 +221,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 +232,53 @@ 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))) + PyFilterExpr(Arc::new($expr.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))) + PyFilterExpr(Arc::new($expr.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.is_not_in(vals))) } - /// 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 +288,8 @@ 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); +impl_node_text_filter_builder!(PyNodeTypeFilterBuilder, Type); /// Constructs node filter expressions. /// @@ -391,7 +318,7 @@ impl PyNodeFilter { /// filter.NodeNameFilterBuilder: #[staticmethod] fn name() -> PyNodeNameFilterBuilder { - PyNodeNameFilterBuilder(Arc::new(NodeFilter::name())) + PyNodeNameFilterBuilder } /// Selects the node type field for filtering. @@ -400,7 +327,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/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() From 4460d1972108f613c8554f86a3741a1651678067 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:53:00 +0100 Subject: [PATCH 02/22] split NodePropOp into NodePropOp and NodeMetaOp, remove is_metadata flag --- .../db/graph/views/filter/model/attribute.rs | 228 ++++-------------- .../src/db/graph/views/filter/model/mod.rs | 5 +- .../views/filter/model/node_filter/mod.rs | 14 +- 3 files changed, 60 insertions(+), 187 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/attribute.rs b/raphtory/src/db/graph/views/filter/model/attribute.rs index d995601505..faa9e082b7 100644 --- a/raphtory/src/db/graph/views/filter/model/attribute.rs +++ b/raphtory/src/db/graph/views/filter/model/attribute.rs @@ -137,25 +137,6 @@ impl Unwrap for Option { } } -// ───────────────────────────────────────────────────────────────────────────── -// Attribute — user-defined value extractor -// ───────────────────────────────────────────────────────────────────────────── - -/// A typed attribute that can be extracted from a graph entity. -/// -/// `E` is the entity identifier type (e.g. `VID` for nodes). -/// User-defined attributes implement this trait and can be wrapped in -/// `AttrNodeExpr` to use them as `NodeExpr`. -// pub trait Attribute: Clone + Send + Sync + 'static { -// type Output: PartialEq + PartialOrd + Eq + Hash + Clone + Send + Sync; -// -// fn extract<'graph, G: GraphViewOps<'graph>>( -// &self, -// graph: &G, -// entity: E, -// ) -> Option; -// } - // ───────────────────────────────────────────────────────────────────────────── // NodeExpr — typed node expression with associated Output type // ───────────────────────────────────────────────────────────────────────────── @@ -176,7 +157,6 @@ impl Unwrap for Option { /// NodeFilter::name().eq("Alice") /// ``` /// -/// Wrap a user-defined `Attribute` in `AttrNodeExpr` to use it here. pub trait NodeExpr: Clone + Send + Sync + 'static { type Output: Comparable + Clone + Send + Sync + 'static; @@ -232,97 +212,48 @@ impl NodeOp for NodeTypeStringOp { } // ───────────────────────────────────────────────────────────────────────────── -// NodePropOp — property/metadata lookup, prop_id resolved at creation time +// NodePropOp / NodeMetaOp — prop_id resolved at creation time // ───────────────────────────────────────────────────────────────────────────── -/// Evaluates a named property or metadata field. -/// -/// The property name is resolved to a column ID once in `create_node_op`; per-node -/// evaluation is O(1). +/// Evaluates a temporal property by pre-resolved column ID. #[derive(Clone)] pub(crate) struct NodePropOp { - // split into prop_op and metadata_op graph: G, prop_id: usize, - is_metadata: bool, } impl NodeOp for NodePropOp { type Output = Option; fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { - let n = self.graph.node(node)?; - if self.is_metadata { - n.metadata().get_by_id(self.prop_id) - } else { - n.properties().get_by_id(self.prop_id) - } + self.graph.node(node)?.properties().get_by_id(self.prop_id) } } -// ───────────────────────────────────────────────────────────────────────────── -// AttributeNodeOp — bridges Attribute to NodeOp> -// ───────────────────────────────────────────────────────────────────────────── - -// #[derive(Clone)] -// pub(crate) struct AttributeNodeOp { -// attribute: A, -// graph: G, -// } - -// impl, G: GraphView> NodeOp for AttributeNodeOp { -// type Output = Option; -// -// fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { -// self.attribute.extract(&self.graph, node) -// } -// } +/// Evaluates a metadata (static) field by pre-resolved column ID. +#[derive(Clone)] +pub(crate) struct NodeMetaOp { + graph: G, + prop_id: usize, +} -// ───────────────────────────────────────────────────────────────────────────── -// AttrNodeExpr — wraps Attribute as a NodeExpr -// ───────────────────────────────────────────────────────────────────────────── +impl NodeOp for NodeMetaOp { + type Output = Option; -/// Wraps a user-defined `Attribute` so it can be used as a `NodeExpr`. -/// -/// User-defined attributes are NOT serializable (`try_as_spec` returns `None`), -/// so `BinOpNodeFilter` built from them cannot be stored in the permissions store. -/// -/// Usage: -/// ```rust,ignore -/// AttrNodeExpr(MyDegreeAttr).gt(2usize) -/// AttrNodeExpr(MyDegreeAttr).gt(AttrNodeExpr(MyHalfDegreeAttr)) -/// ``` -// #[derive(Clone)] -// pub struct AttrNodeExpr(pub A); -// -// impl> NodeExpr for AttrNodeExpr -// where -// A::Output: Comparable + Clone + Send + Sync + 'static, -// Option: Comparable, -// { -// type Output = Option; -// -// fn create_node_op<'g, G: GraphView + 'g>( -// &self, -// graph: G, -// ) -> Result> + 'g>, GraphError> { -// Ok(Arc::new(AttributeNodeOp { attribute: self.0.clone(), graph })) -// } -// } + fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { + self.graph.node(node)?.metadata().get_by_id(self.prop_id) + } +} // ───────────────────────────────────────────────────────────────────────────── // Concrete expression structs // ───────────────────────────────────────────────────────────────────────────── -/// Degree expression (total / in / out). +/// Wraps a `Direction` so it can be used as a `NodeExpr` for degree filtering. /// -/// Delegates to `Degree` from `db/api/state/ops/node.rs` — no reimplementation. +/// Delegates to `Degree` from `db/api/state/ops/node.rs`. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum DegreeExpr { - Total, - In, - Out, -} +pub struct DegreeExpr(pub Direction); impl NodeExpr for DegreeExpr { type Output = Option; @@ -331,12 +262,10 @@ impl NodeExpr for DegreeExpr { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let dir = match self { - DegreeExpr::Total => Direction::BOTH, - DegreeExpr::In => Direction::IN, - DegreeExpr::Out => Direction::OUT, - }; - Ok(Arc::new(OptionWrapOp(Degree { dir, view: graph }))) + Ok(Arc::new(OptionWrapOp(Degree { + dir: self.0, + view: graph, + }))) } } @@ -365,11 +294,7 @@ impl NodeExpr for Property { .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, - is_metadata: false, - })) + Ok(Arc::new(NodePropOp { graph, prop_id })) } } @@ -398,11 +323,7 @@ impl NodeExpr for Metadata { .node_meta() .get_prop_id_and_type(&self.name, true) .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; - Ok(Arc::new(NodePropOp { - graph, - prop_id, - is_metadata: true, - })) + Ok(Arc::new(NodeMetaOp { graph, prop_id })) } } @@ -489,7 +410,6 @@ impl NodeExpr for Prop { } } -// TODO: try IntoProp macro_rules! impl_node_expr_for_numeric { ($prim:ty, $variant:ident) => { impl NodeExpr for $prim { @@ -636,8 +556,8 @@ where /// /// Created by `NodeExprFilterOps`: /// ```rust,ignore -/// DegreeExpr::Total.gt(2usize) -/// DegreeExpr::Out.gt(DegreeExpr::In) +/// DegreeExpr(Direction::BOTH).gt(2usize) +/// DegreeExpr(Direction::OUT).gt(DegreeExpr(Direction::IN)) /// NodeFilter::property("age").gt(30i64) /// NodeFilter::name().eq("Alice") /// ``` @@ -946,8 +866,8 @@ where /// /// `gt(rhs)` accepts any `R: NodeExpr`: /// ```rust,ignore -/// DegreeExpr::Total.gt(2usize) -/// DegreeExpr::Out.gt(DegreeExpr::In) +/// DegreeExpr(Direction::BOTH).gt(2usize) +/// DegreeExpr(Direction::OUT).gt(DegreeExpr(Direction::IN)) /// NodeFilter::property("age").gt(30i64) /// AttrNodeExpr(MyAttr).is_in([2usize, 3usize]) /// ``` @@ -1064,38 +984,8 @@ mod tests { use super::*; use crate::prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}; - // ── user-defined attributes (via Attribute — not serializable) ────── - - #[derive(Clone)] - struct DegreeAttr; - - impl Attribute for DegreeAttr { - type Output = usize; - - fn extract<'graph, G: GraphViewOps<'graph>>( - &self, - graph: &G, - entity: VID, - ) -> Option { - graph.node(entity).map(|n| n.degree()) - } - } - - #[derive(Clone)] - struct HalfDegreeAttr; - - impl Attribute for HalfDegreeAttr { - type Output = usize; - - fn extract<'graph, G: GraphViewOps<'graph>>( - &self, - graph: &G, - entity: VID, - ) -> Option { - graph.node(entity).map(|n| n.degree() / 2) - } - } - + // 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(); @@ -1120,13 +1010,13 @@ mod tests { names } - // ── NodeExprFilterOps comparison tests ─────────────────────────────────── + // ── DegreeExpr comparison operators ────────────────────────────────────── #[test] - fn degree_ge_2_keeps_high_degree_nodes() { + fn degree_ge_2_keeps_all_nodes() { let g = build_test_graph(); assert_eq!( - filtered_names(AttrNodeExpr(DegreeAttr).ge(2usize), g), + filtered_names(DegreeExpr(Direction::BOTH).ge(2usize), g), vec!["a", "b", "c"] ); } @@ -1134,14 +1024,14 @@ mod tests { #[test] fn degree_eq_1_keeps_no_nodes() { let g = build_test_graph(); - assert!(filtered_names(AttrNodeExpr(DegreeAttr).eq(1usize), g).is_empty()); + assert!(filtered_names(DegreeExpr(Direction::BOTH).eq(1usize), g).is_empty()); } #[test] fn degree_le_2_keeps_all_nodes() { let g = build_test_graph(); assert_eq!( - filtered_names(AttrNodeExpr(DegreeAttr).le(2usize), g), + filtered_names(DegreeExpr(Direction::BOTH).le(2usize), g), vec!["a", "b", "c"] ); } @@ -1149,41 +1039,34 @@ mod tests { #[test] fn degree_gt_2_keeps_no_nodes() { let g = build_test_graph(); - assert!(filtered_names(AttrNodeExpr(DegreeAttr).gt(2usize), g).is_empty()); + assert!(filtered_names(DegreeExpr(Direction::BOTH).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(AttrNodeExpr(DegreeAttr).ne(2usize), g).is_empty()); + assert!(filtered_names(DegreeExpr(Direction::BOTH).ne(2usize), g).is_empty()); } - // ── unified gt: constant and expression RHS use the SAME method ────────── + // ── expression-vs-expression: RHS can be another NodeExpr ──────────────── #[test] - fn degree_gt_half_degree_unified_method() { + 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(AttrNodeExpr(DegreeAttr).gt(AttrNodeExpr(HalfDegreeAttr)), g), - vec!["a", "b", "c"] - ); - } - - #[test] - fn degree_eq_half_degree_keeps_no_nodes_when_unequal() { - let g = build_test_graph(); - assert!( - filtered_names(AttrNodeExpr(DegreeAttr).eq(AttrNodeExpr(HalfDegreeAttr)), g).is_empty() + filtered_names(DegreeExpr(Direction::BOTH).gt(DegreeExpr(Direction::IN)), g), + vec!["a", "b"] ); } - // ── set / unary ops via NodeExprFilterOps ──────────────────────────────── + // ── unary ops ──────────────────────────────────────────────────────────── #[test] fn degree_is_some_keeps_all_nodes() { let g = build_test_graph(); assert_eq!( - filtered_names(AttrNodeExpr(DegreeAttr).is_some(), g), + filtered_names(DegreeExpr(Direction::BOTH).is_some(), g), vec!["a", "b", "c"] ); } @@ -1191,14 +1074,16 @@ mod tests { #[test] fn degree_is_none_keeps_no_nodes() { let g = build_test_graph(); - assert!(filtered_names(AttrNodeExpr(DegreeAttr).is_none(), g).is_empty()); + assert!(filtered_names(DegreeExpr(Direction::BOTH).is_none(), g).is_empty()); } + // ── set ops ────────────────────────────────────────────────────────────── + #[test] fn degree_is_in_set() { let g = build_test_graph(); assert_eq!( - filtered_names(AttrNodeExpr(DegreeAttr).is_in([2usize]), g), + filtered_names(DegreeExpr(Direction::BOTH).is_in([2usize]), g), vec!["a", "b", "c"] ); } @@ -1206,24 +1091,13 @@ mod tests { #[test] fn degree_is_not_in_set_excludes_matching_nodes() { let g = build_test_graph(); - assert!(filtered_names(AttrNodeExpr(DegreeAttr).is_not_in([2usize]), g).is_empty()); - } - - // ── built-in DegreeExpr ────────────────────────────────────────────────── - - #[test] - fn builtin_degree_ge_2() { - let g = build_test_graph(); - assert_eq!( - filtered_names(DegreeExpr::Total.ge(2usize), g), - vec!["a", "b", "c"] - ); + assert!(filtered_names(DegreeExpr(Direction::BOTH).is_not_in([2usize]), g).is_empty()); } - // ── ConstExpr still works for custom output types ───────────────────────── + // ── ConstExpr for custom output types ──────────────────────────────────── #[test] - fn const_expr_still_works() { + fn const_expr_works() { let filter = BinOpNodeFilter::new(ConstExpr(2usize), BinaryOp::Eq, ConstExpr(2usize)); let g = build_test_graph(); assert_eq!(filtered_names(filter, g), vec!["a", "b", "c"]); diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index a60e1fe9df..ab7379c51b 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -30,9 +30,8 @@ pub use crate::{ filter::{ model::{ attribute::{ - AttrNodeExpr, Attribute, BinOpNodeFilter, Comparable, ConstExpr, - DegreeExpr, Metadata, NodeExpr, NodeExprFilterOps, Property, SetNodeFilter, - UnaryNodeFilter, Unwrap, + BinOpNodeFilter, Comparable, ConstExpr, DegreeExpr, Metadata, NodeExpr, + NodeExprFilterOps, Property, SetNodeFilter, UnaryNodeFilter, Unwrap, }, edge_filter::{EdgeEndpointWrapper, EdgeFilter}, exploded_edge_filter::{ diff --git a/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs b/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs index 6950768e47..2ec87ca9fe 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 @@ -37,7 +37,7 @@ use crate::{ errors::GraphError, prelude::{GraphViewOps, PropertyFilter}, }; -use raphtory_api::core::storage::timeindex::EventTime; +use raphtory_api::core::{storage::timeindex::EventTime, Direction}; use std::{fmt, fmt::Display, sync::Arc}; pub mod builders; @@ -88,22 +88,22 @@ impl NodeFilter { state.bool_col_filter(col) } - /// Total degree expression — serializable, supports `.gt(n)`, `.lt(n)`, etc. + /// Total degree expression — supports `.gt(n)`, `.lt(n)`, etc. #[inline] pub fn degree() -> DegreeExpr { - DegreeExpr::Total + DegreeExpr(Direction::BOTH) } - /// In-degree expression — serializable. + /// In-degree expression. #[inline] pub fn in_degree() -> DegreeExpr { - DegreeExpr::In + DegreeExpr(Direction::IN) } - /// Out-degree expression — serializable. + /// Out-degree expression. #[inline] pub fn out_degree() -> DegreeExpr { - DegreeExpr::Out + DegreeExpr(Direction::OUT) } /// Current (latest) value of a named property — serializable. From 9d4691c9930f82d8164c41d36ba10bda9cba34d1 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:56:55 +0100 Subject: [PATCH 03/22] remove NodeTypeStringOp, use ArcStr directly for node type comparisons --- .../db/graph/views/filter/model/attribute.rs | 96 +++++++++---------- .../src/python/filter/node_filter_builders.rs | 56 ++++++++++- 2 files changed, 101 insertions(+), 51 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/attribute.rs b/raphtory/src/db/graph/views/filter/model/attribute.rs index faa9e082b7..e98b34cfde 100644 --- a/raphtory/src/db/graph/views/filter/model/attribute.rs +++ b/raphtory/src/db/graph/views/filter/model/attribute.rs @@ -20,6 +20,7 @@ use crate::{ }; use raphtory_api::core::{ entities::{properties::prop::Prop, VID}, + storage::arc_str::ArcStr, Direction, }; use raphtory_storage::graph::graph::GraphStorage; @@ -53,35 +54,41 @@ impl Comparable for usize { } } -impl Comparable for String { - fn binary_cmp(op: &BinaryOp, left: &String, right: &String) -> bool { - // Coerce to &str to avoid ambiguity with NodeExprFilterOps methods of the same name. - let (l, r): (&str, &str) = (left, right); - 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, - BinaryOp::StartsWith => l.starts_with(r), - BinaryOp::EndsWith => l.ends_with(r), - BinaryOp::Contains => l.contains(r), - BinaryOp::NotContains => !l.contains(r), - BinaryOp::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 +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, + BinaryOp::StartsWith => l.starts_with(r), + BinaryOp::EndsWith => l.ends_with(r), + BinaryOp::Contains => l.contains(r), + BinaryOp::NotContains => !l.contains(r), + BinaryOp::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_comparable_str!(String); +impl_comparable_str!(ArcStr); + impl Comparable for Prop { fn binary_cmp(op: &BinaryOp, left: &Prop, right: &Prop) -> bool { use std::cmp::Ordering::*; @@ -192,25 +199,6 @@ where } } -// ───────────────────────────────────────────────────────────────────────────── -// NodeTypeStringOp — maps Type's Option to Option -// ───────────────────────────────────────────────────────────────────────────── - -/// Evaluates `Type` from `node.rs` and converts `ArcStr` to `String`. -/// -/// `Type: NodeOp>` — this op converts to `Option` -/// without reimplementing the type-id lookup logic. -#[derive(Clone)] -pub(crate) struct NodeTypeStringOp; - -impl NodeOp for NodeTypeStringOp { - type Output = Option; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - Type.apply(storage, node).map(|a| a.to_string()) - } -} - // ───────────────────────────────────────────────────────────────────────────── // NodePropOp / NodeMetaOp — prop_id resolved at creation time // ───────────────────────────────────────────────────────────────────────────── @@ -329,16 +317,15 @@ impl NodeExpr for Metadata { /// `Type` from `db/api/state/ops/node.rs` used as a node expression. /// -/// `Type: NodeOp>` — this impl converts to `Option` -/// via `NodeTypeStringOp` without reimplementing the type-id lookup. +/// `Type: NodeOp>` — used directly, no conversion. impl NodeExpr for Type { - type Output = Option; + type Output = Option; fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(NodeTypeStringOp)) + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Type)) } } @@ -388,6 +375,17 @@ impl NodeExpr for String { } } +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())))) + } +} + impl NodeExpr for &'static str { type Output = Option; diff --git a/raphtory/src/python/filter/node_filter_builders.rs b/raphtory/src/python/filter/node_filter_builders.rs index d646080bd6..c8cfddc393 100644 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ b/raphtory/src/python/filter/node_filter_builders.rs @@ -25,7 +25,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. @@ -289,7 +292,56 @@ macro_rules! impl_node_text_filter_builder { } impl_node_text_filter_builder!(PyNodeNameFilterBuilder, Name); -impl_node_text_filter_builder!(PyNodeTypeFilterBuilder, Type); + +#[pymethods] +impl PyNodeTypeFilterBuilder { + fn __eq__(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(Type.eq(ArcStr::from(value)))) + } + + fn __ne__(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(Type.ne(ArcStr::from(value)))) + } + + 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))) + } + + fn starts_with(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(Type.starts_with(ArcStr::from(value)))) + } + + fn ends_with(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(Type.ends_with(ArcStr::from(value)))) + } + + fn contains(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(Type.contains(ArcStr::from(value)))) + } + + fn not_contains(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(Type.not_contains(ArcStr::from(value)))) + } + + fn fuzzy_search( + &self, + value: String, + levenshtein_distance: usize, + prefix_match: bool, + ) -> PyFilterExpr { + PyFilterExpr(Arc::new(Type.fuzzy_search( + ArcStr::from(value), + levenshtein_distance, + prefix_match, + ))) + } +} /// Constructs node filter expressions. /// From 793b0e618644934e59a50788d498dbbc7ed70c6d Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 5 Jun 2026 05:52:59 +0100 Subject: [PATCH 04/22] ref --- .../views/filter/model/filter_operator.rs | 93 ++++- .../src/db/graph/views/filter/model/mod.rs | 12 +- .../model/{attribute.rs => node_expr.rs} | 374 +++++------------- .../views/filter/model/node_filter/mod.rs | 2 +- .../src/python/filter/edge_filter_builders.rs | 51 ++- .../src/python/filter/node_filter_builders.rs | 27 +- 6 files changed, 264 insertions(+), 295 deletions(-) rename raphtory/src/db/graph/views/filter/model/{attribute.rs => node_expr.rs} (70%) 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 c63159b81b..ddbf4cd553 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,101 @@ use crate::db::graph::views::filter::model::{ filter::FieldFilterValue, filter_value::FilterValue, property_filter::PropertyFilterValue, }; -use raphtory_api::core::entities::{properties::prop::Prop, GidRef, GID}; +use raphtory_api::core::{ + entities::{properties::prop::Prop, GidRef, GID}, + storage::arc_str::ArcStr, +}; use std::{collections::HashSet, fmt, fmt::Display, ops::Deref}; use strsim::levenshtein; +// ───────────────────────────────────────────────────────────────────────────── +// Comparable — type-driven value comparison for BinOpNodeOp +// ───────────────────────────────────────────────────────────────────────────── + +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, + _ => false, + } + } +} + +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, + BinaryOp::StartsWith => l.starts_with(r), + BinaryOp::EndsWith => l.ends_with(r), + BinaryOp::Contains => l.contains(r), + BinaryOp::NotContains => !l.contains(r), + BinaryOp::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_comparable_str!(String); +impl_comparable_str!(ArcStr); + +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), + _ => false, + } + } +} + +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), + } + } +} + // ───────────────────────────────────────────────────────────────────────────── // Focused operator enums for the NodeExpr expression system // ───────────────────────────────────────────────────────────────────────────── diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index ab7379c51b..b1bc781531 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -29,16 +29,16 @@ pub use crate::{ graph::views::{ filter::{ model::{ - attribute::{ - BinOpNodeFilter, Comparable, ConstExpr, DegreeExpr, Metadata, NodeExpr, - NodeExprFilterOps, Property, SetNodeFilter, UnaryNodeFilter, Unwrap, - }, edge_filter::{EdgeEndpointWrapper, EdgeFilter}, exploded_edge_filter::{ CompositeExplodedEdgeFilter, ExplodedEdgeEndpointWrapper, ExplodedEdgeFilter, }, - filter_operator::{BinaryOp, FilterOperator, SetOp, UnaryOp}, + filter_operator::{BinaryOp, Comparable, FilterOperator, SetOp, UnaryOp}, + node_expr::{ + BinOpNodeFilter, ConstExpr, DegreeExpr, Metadata, NodeExpr, + NodeExprFilterOps, Property, SetNodeFilter, UnaryNodeFilter, + }, node_filter::{NodeFilter, NodeNameFilter, NodeTypeFilter}, not_filter::NotFilter, or_filter::OrFilter, @@ -60,7 +60,6 @@ use raphtory_api::core::{ use std::{ops::Deref, sync::Arc}; pub mod and_filter; -pub mod attribute; pub mod edge_filter; pub mod exploded_edge_filter; pub mod filter; @@ -74,6 +73,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; diff --git a/raphtory/src/db/graph/views/filter/model/attribute.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs similarity index 70% rename from raphtory/src/db/graph/views/filter/model/attribute.rs rename to raphtory/src/db/graph/views/filter/model/node_expr.rs index e98b34cfde..0ecb15eae0 100644 --- a/raphtory/src/db/graph/views/filter/model/attribute.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr.rs @@ -8,7 +8,7 @@ use crate::{ graph::views::filter::{ model::{ edge_filter::CompositeEdgeFilter, - filter_operator::{BinaryOp, SetOp, UnaryOp}, + filter_operator::{BinaryOp, Comparable, SetOp, UnaryOp}, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, TryAsCompositeFilter, }, @@ -24,125 +24,7 @@ use raphtory_api::core::{ Direction, }; use raphtory_storage::graph::graph::GraphStorage; -use std::{collections::HashSet, hash::Hash, sync::Arc}; -use strsim::levenshtein; - -// ───────────────────────────────────────────────────────────────────────────── -// Comparable — type-driven dispatch for BinOpNodeOp -// ───────────────────────────────────────────────────────────────────────────── - -/// Comparison trait used by `BinOpNodeOp` to evaluate a `BinaryOp` against two values. -/// -/// Implemented for `usize`, `String`, `Prop`, and `Option`. -/// The `Option` impl handles `None` symmetrically: `(None, None)` is equal, -/// one `None` is unequal, and ordering ops return `false` when either side is `None`. -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, - _ => false, - } - } -} - -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, - BinaryOp::StartsWith => l.starts_with(r), - BinaryOp::EndsWith => l.ends_with(r), - BinaryOp::Contains => l.contains(r), - BinaryOp::NotContains => !l.contains(r), - BinaryOp::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_comparable_str!(String); -impl_comparable_str!(ArcStr); - -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), - _ => false, - } - } -} - -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), - } - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// Unwrap — constrains Output = Option -// ───────────────────────────────────────────────────────────────────────────── - -pub trait Unwrap { - type Inner; - fn is_some(&self) -> bool; - fn is_none(&self) -> bool; - fn unwrap_inner(self) -> Option; -} - -impl Unwrap for Option { - type Inner = T; - fn is_some(&self) -> bool { - Option::is_some(self) - } - fn is_none(&self) -> bool { - Option::is_none(self) - } - fn unwrap_inner(self) -> Option { - self - } -} +use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; // ───────────────────────────────────────────────────────────────────────────── // NodeExpr — typed node expression with associated Output type @@ -150,8 +32,9 @@ impl Unwrap for Option { /// A typed expression that produces a value per node. /// -/// `Output` carries nullability directly: `Option` for properties that -/// may be absent, `Option` for name/type, `Option` for degree. +/// `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. @@ -176,29 +59,6 @@ pub trait NodeExpr: Clone + Send + Sync + 'static { ) -> Result + 'g>, GraphError>; } -// ───────────────────────────────────────────────────────────────────────────── -// OptionWrapOp — adapts NodeOp to NodeOp> -// ───────────────────────────────────────────────────────────────────────────── - -/// Wraps an inner `NodeOp` and returns `Some(inner.apply(...))`. -/// -/// Used by `DegreeExpr` and `Name` to produce `Option`-wrapped outputs from -/// the existing `Degree` and `Name` ops in `db/api/state/ops/node.rs`, -/// without reimplementing their logic. -#[derive(Clone)] -pub(crate) struct OptionWrapOp(O); - -impl NodeOp for OptionWrapOp -where - O::Output: Clone + Send + Sync + 'static, -{ - type Output = Option; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - Some(self.0.apply(storage, node)) - } -} - // ───────────────────────────────────────────────────────────────────────────── // NodePropOp / NodeMetaOp — prop_id resolved at creation time // ───────────────────────────────────────────────────────────────────────────── @@ -244,16 +104,16 @@ impl NodeOp for NodeMetaOp { pub struct DegreeExpr(pub Direction); impl NodeExpr for DegreeExpr { - type Output = Option; + type Output = usize; fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(OptionWrapOp(Degree { + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Degree { dir: self.0, view: graph, - }))) + })) } } @@ -330,17 +190,14 @@ impl NodeExpr for Type { } /// `Name` from `db/api/state/ops/node.rs` used as a node expression. -/// -/// Wraps the existing `Name` op via `OptionWrapOp` so it fits the -/// `NodeExpr>` interface without reimplementation. impl NodeExpr for Name { - type Output = Option; + type Output = String; fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(OptionWrapOp(Name))) + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Name)) } } @@ -354,24 +211,24 @@ impl NodeExpr for Name { // ───────────────────────────────────────────────────────────────────────────── impl NodeExpr for usize { - type Output = Option; + type Output = usize; fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(*self)))) + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(*self))) } } impl NodeExpr for String { - type Output = Option; + type Output = String; fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(self.clone())))) + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(self.clone()))) } } @@ -387,13 +244,13 @@ impl NodeExpr for ArcStr { } impl NodeExpr for &'static str { - type Output = Option; + type Output = String; fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(self.to_string())))) + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(self.to_string()))) } } @@ -487,18 +344,12 @@ impl<'g, T: Comparable + Clone + Send + Sync + 'static> NodeOp for BinOpNodeOp<' // ───────────────────────────────────────────────────────────────────────────── #[derive(Clone)] -pub struct UnaryNodeOp<'g, T: Unwrap + Clone + Send + Sync + 'static> -where - T::Inner: Clone + Send + Sync + 'static, -{ - inner: Arc + 'g>, +pub struct UnaryNodeOp<'g, I: Clone + Send + Sync + 'static> { + inner: Arc> + 'g>, op: UnaryOp, } -impl<'g, T: Unwrap + Clone + Send + Sync + 'static> NodeOp for UnaryNodeOp<'g, T> -where - T::Inner: Clone + Send + Sync + 'static, -{ +impl<'g, I: Clone + Send + Sync + 'static> NodeOp for UnaryNodeOp<'g, I> { type Output = bool; fn apply(&self, storage: &GraphStorage, node: VID) -> bool { @@ -515,23 +366,17 @@ where // ───────────────────────────────────────────────────────────────────────────── #[derive(Clone)] -pub struct SetNodeOp<'g, T: Unwrap + Clone + Send + Sync + 'static> -where - T::Inner: Eq + Hash + Clone + Send + Sync + 'static, -{ - inner: Arc + 'g>, +pub struct SetNodeOp<'g, I: Eq + Hash + Clone + Send + Sync + 'static> { + inner: Arc> + 'g>, op: SetOp, - values: Arc>, + values: Arc>, } -impl<'g, T: Unwrap + Clone + Send + Sync + 'static> NodeOp for SetNodeOp<'g, T> -where - T::Inner: Eq + Hash + Clone + Send + Sync + 'static, -{ +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).unwrap_inner(); + 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 @@ -672,39 +517,47 @@ where /// A node filter that tests the presence of an `Option`-valued expression. /// -/// Created by `.is_some()` and `.is_none()` on any `NodeExpr` whose `Output` -/// implements `Unwrap` (i.e., is an `Option`). -pub struct UnaryNodeFilter +/// Created by `.is_some()` and `.is_none()` on any `NodeExpr>`. +pub struct UnaryNodeFilter where - E::Output: Unwrap, + E: NodeExpr>, + I: Clone + Send + Sync + 'static, { pub expr: E, pub op: UnaryOp, + _phantom: PhantomData, } -impl Clone for UnaryNodeFilter +impl Clone for UnaryNodeFilter where - E::Output: Unwrap, + 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::Output: Unwrap {} +impl ComposableFilter for UnaryNodeFilter +where + E: NodeExpr>, + I: Clone + Send + Sync + 'static, +{ +} -impl CreateFilter for UnaryNodeFilter +impl CreateFilter for UnaryNodeFilter where - E::Output: Unwrap + Clone + Send + Sync + 'static, - ::Inner: Clone + Send + Sync + 'static, + E: NodeExpr>, + I: Clone + Send + Sync + 'static, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - NodeFilteredGraph>; + NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, E::Output>; + type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, I>; type FilteredGraph<'graph, G> = G @@ -736,10 +589,10 @@ where } } -impl TryAsCompositeFilter for UnaryNodeFilter +impl TryAsCompositeFilter for UnaryNodeFilter where - E::Output: Unwrap + Clone + Send + Sync + 'static, - ::Inner: Clone + Send + Sync + 'static, + E: NodeExpr>, + I: Clone + Send + Sync + 'static, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -764,43 +617,48 @@ where /// expression is contained in (or absent from) a fixed set. /// /// Created by `.is_in(values)` and `.is_not_in(values)`. -#[derive(Clone)] -pub struct SetNodeFilter +pub struct SetNodeFilter where - E::Output: Unwrap, - ::Inner: Eq + Hash + Clone, + E: NodeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, { pub expr: E, pub op: SetOp, - pub values: Arc::Inner>>, + pub values: Arc>, + _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 Clone for SetNodeFilter -// where -// E::Output: Unwrap, -// ::Inner: Eq + Hash + Clone, -// { -// fn clone(&self) -> Self { -// Self { expr: self.expr.clone(), op: self.op, values: self.values.clone() } -// } -// } - -impl ComposableFilter for SetNodeFilter +impl ComposableFilter for SetNodeFilter where - E::Output: Unwrap, - ::Inner: Eq + Hash + Clone, + E: NodeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, { } -impl CreateFilter for SetNodeFilter +impl CreateFilter for SetNodeFilter where - E::Output: Unwrap + Clone + Send + Sync + 'static, - ::Inner: Eq + Hash + Clone + Send + Sync + 'static, + E: NodeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - NodeFilteredGraph>; + NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph> = SetNodeOp<'graph, E::Output>; + type NodeFilter<'graph, G: GraphView + 'graph> = SetNodeOp<'graph, I>; type FilteredGraph<'graph, G> = G @@ -836,10 +694,10 @@ where } } -impl TryAsCompositeFilter for SetNodeFilter +impl TryAsCompositeFilter for SetNodeFilter where - E::Output: Unwrap + Clone + Send + Sync + 'static, - ::Inner: Eq + Hash + Clone + Send + Sync + 'static, + E: NodeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -867,7 +725,7 @@ where /// DegreeExpr(Direction::BOTH).gt(2usize) /// DegreeExpr(Direction::OUT).gt(DegreeExpr(Direction::IN)) /// NodeFilter::property("age").gt(30i64) -/// AttrNodeExpr(MyAttr).is_in([2usize, 3usize]) +/// DegreeExpr(Direction::BOTH).is_in([2usize, 3usize]) /// ``` pub trait NodeExprFilterOps: NodeExpr + Sized { fn gt>(self, rhs: R) -> BinOpNodeFilter { @@ -926,51 +784,57 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { ) } - fn is_some(self) -> UnaryNodeFilter + fn is_some(self) -> UnaryNodeFilter where - Self::Output: Unwrap, + Self: NodeExpr>, + Inner: Clone + Send + Sync + 'static, { UnaryNodeFilter { expr: self, op: UnaryOp::IsSome, + _phantom: PhantomData, } } - fn is_none(self) -> UnaryNodeFilter + fn is_none(self) -> UnaryNodeFilter where - Self::Output: Unwrap, + Self: NodeExpr>, + Inner: Clone + Send + Sync + 'static, { UnaryNodeFilter { expr: self, op: UnaryOp::IsNone, + _phantom: PhantomData, } } - fn is_in(self, values: I) -> SetNodeFilter + fn is_in(self, values: Iter) -> SetNodeFilter where - Self::Output: Unwrap, - ::Inner: Eq + Hash + Clone, - I: IntoIterator::Inner>, + 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: I) -> SetNodeFilter + fn is_not_in(self, values: Iter) -> SetNodeFilter where - Self::Output: Unwrap, - ::Inner: Eq + Hash + Clone, - I: IntoIterator::Inner>, + 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, } } } @@ -1058,40 +922,6 @@ mod tests { ); } - // ── unary ops ──────────────────────────────────────────────────────────── - - #[test] - fn degree_is_some_keeps_all_nodes() { - let g = build_test_graph(); - assert_eq!( - filtered_names(DegreeExpr(Direction::BOTH).is_some(), g), - vec!["a", "b", "c"] - ); - } - - #[test] - fn degree_is_none_keeps_no_nodes() { - let g = build_test_graph(); - assert!(filtered_names(DegreeExpr(Direction::BOTH).is_none(), g).is_empty()); - } - - // ── set ops ────────────────────────────────────────────────────────────── - - #[test] - fn degree_is_in_set() { - let g = build_test_graph(); - assert_eq!( - filtered_names(DegreeExpr(Direction::BOTH).is_in([2usize]), g), - vec!["a", "b", "c"] - ); - } - - #[test] - fn degree_is_not_in_set_excludes_matching_nodes() { - let g = build_test_graph(); - assert!(filtered_names(DegreeExpr(Direction::BOTH).is_not_in([2usize]), g).is_empty()); - } - // ── ConstExpr for custom output types ──────────────────────────────────── #[test] 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 2ec87ca9fe..45ca949c4d 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 @@ -15,12 +15,12 @@ use crate::{ }, graph::views::filter::{ model::{ - attribute::{DegreeExpr, Metadata, Property}, edge_filter::CompositeEdgeFilter, filter::Filter, is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, + node_expr::{DegreeExpr, Metadata, Property}, node_filter::{builders::NodeIdFilterBuilder, validate::validate}, node_state_filter::NodeStateBoolColOp, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, diff --git a/raphtory/src/python/filter/edge_filter_builders.rs b/raphtory/src/python/filter/edge_filter_builders.rs index e4184e33e6..cfd92a3f7a 100644 --- a/raphtory/src/python/filter/edge_filter_builders.rs +++ b/raphtory/src/python/filter/edge_filter_builders.rs @@ -9,7 +9,6 @@ use crate::{ property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, - impl_node_text_filter_builder, python::{ filter::{ filter_expr::PyFilterExpr, @@ -220,8 +219,54 @@ pub struct PyEdgeEndpointNameFilterBuilder(pub EdgeEndpointWrapper); -impl_node_text_filter_builder!(PyEdgeEndpointNameFilterBuilder); -impl_node_text_filter_builder!(PyEdgeEndpointTypeFilterBuilder); +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_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 c8cfddc393..35315829c8 100644 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ b/raphtory/src/python/filter/node_filter_builders.rs @@ -2,9 +2,9 @@ use crate::{ db::{ api::state::ops::{Name, Type}, graph::views::filter::model::{ - attribute::NodeExprFilterOps, + node_expr::NodeExprFilterOps, node_filter::{ - builders::NodeIdFilterBuilder, + builders::{NodeIdFilterBuilder, NodeNameFilterBuilder}, ops::{NodeFilterOps, NodeIdFilterOps}, NodeFilter, }, @@ -249,16 +249,6 @@ macro_rules! impl_node_text_filter_builder { PyFilterExpr(Arc::new($expr.ne(value))) } - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new($expr.is_in(vals))) - } - - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new($expr.is_not_in(vals))) - } - fn starts_with(&self, value: String) -> PyFilterExpr { PyFilterExpr(Arc::new($expr.starts_with(value))) } @@ -293,6 +283,19 @@ macro_rules! impl_node_text_filter_builder { 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(NodeNameFilterBuilder.is_in(vals))) + } + + fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { + let vals: Vec = values.into_iter().collect(); + PyFilterExpr(Arc::new(NodeNameFilterBuilder.is_not_in(vals))) + } +} + #[pymethods] impl PyNodeTypeFilterBuilder { fn __eq__(&self, value: String) -> PyFilterExpr { From c3c3da7f92c40653161af4007d1db3c3698122bf Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 5 Jun 2026 06:09:13 +0100 Subject: [PATCH 05/22] split CompositeNodeFilter::Node into typed Id/Name/Type variants, removing string dispatch --- .../views/filter/model/node_filter/mod.rs | 44 ++++++++++--------- raphtory/src/search/node_filter_executor.rs | 37 ++++++++-------- 2 files changed, 42 insertions(+), 39 deletions(-) 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 45ca949c4d..5e622061d4 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 @@ -215,7 +215,7 @@ impl CreateFilter for NodeIdFilter { 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 { @@ -281,7 +281,7 @@ impl CreateFilter for NodeNameFilter { 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 { @@ -364,7 +364,7 @@ impl CreateFilter for NodeTypeFilter { 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 { @@ -380,7 +380,9 @@ 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>), @@ -403,7 +405,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), @@ -436,14 +440,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); @@ -485,14 +488,13 @@ impl CreateFilter for CompositeNodeFilter { 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::Id(i) => Ok(Arc::new(NodeIdFilter(i).filter_graph_view(graph)?)), + CompositeNodeFilter::Name(i) => { + Ok(Arc::new(NodeNameFilter(i).filter_graph_view(graph)?)) + } + CompositeNodeFilter::Type(i) => { + Ok(Arc::new(NodeTypeFilter(i).filter_graph_view(graph)?)) + } 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)?)), 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) From 9889d5f6bbec38e1d5a9317f7fed41f87ee138b7 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:13:50 +0100 Subject: [PATCH 06/22] replace NodeIdFilterBuilder/InternalNodeIdFilterBuilder/NodeIdFilterOps with direct methods on Id accepting T: Into --- .../graph/views/filter/model/edge_filter.rs | 84 ++++++++++++++--- .../filter/model/exploded_edge_filter.rs | 11 +-- .../views/filter/model/filter_operator.rs | 18 ++++ .../graph/views/filter/model/latest_filter.rs | 10 +-- .../views/filter/model/layered_filter.rs | 10 +-- .../db/graph/views/filter/model/node_expr.rs | 16 +++- .../filter/model/node_filter/builders.rs | 28 ------ .../views/filter/model/node_filter/mod.rs | 82 +++++++++++++++-- .../views/filter/model/node_filter/ops.rs | 90 +------------------ .../views/filter/model/windowed_filter.rs | 10 +-- .../src/python/filter/edge_filter_builders.rs | 47 +++++----- .../src/python/filter/node_filter_builders.rs | 12 +-- 12 files changed, 215 insertions(+), 203 deletions(-) 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 7c5cbc403d..608abefd83 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, NotANodeFilter}, view::{internal::GraphView, BoxableGraphView}, }, graph::views::filter::{ @@ -16,10 +16,9 @@ use crate::{ layered_filter::Layered, node_filter::{ builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder, + InternalNodeFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder, }, - CompositeNodeFilter, NodeFilter, + CompositeNodeFilter, NodeFilter, NodeIdFilter, }, property_filter::{ builders::{ @@ -39,7 +38,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. @@ -156,7 +155,7 @@ impl EdgeEndpointWrapper { impl EdgeEndpointWrapper { #[inline] - pub fn id(&self) -> EdgeEndpointWrapper { + pub fn id(&self) -> EdgeEndpointWrapper { EdgeEndpointWrapper::new(NodeFilter::id(), self.endpoint) } @@ -171,6 +170,73 @@ impl EdgeEndpointWrapper { } } +impl EdgeEndpointWrapper { + pub fn eq(self, value: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.eq(value)) + } + + pub fn ne(self, value: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.ne(value)) + } + + pub fn lt(self, value: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.lt(value)) + } + + pub fn le(self, value: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.le(value)) + } + + pub fn gt(self, value: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.gt(value)) + } + + pub fn ge(self, value: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.ge(value)) + } + + pub fn starts_with(self, s: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.starts_with(s)) + } + + pub fn ends_with(self, s: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.ends_with(s)) + } + + pub fn contains(self, s: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.contains(s)) + } + + pub fn not_contains(self, s: impl Into) -> EdgeEndpointWrapper { + self.map(|id| id.not_contains(s)) + } + + pub fn fuzzy_search( + self, + s: impl Into, + levenshtein_distance: usize, + prefix_match: bool, + ) -> EdgeEndpointWrapper { + self.map(|id| id.fuzzy_search(s, levenshtein_distance, prefix_match)) + } + + pub fn is_in(self, values: I) -> EdgeEndpointWrapper + where + I: IntoIterator, + T: Into, + { + self.map(|id| id.is_in(values)) + } + + pub fn is_not_in(self, values: I) -> EdgeEndpointWrapper + where + I: IntoIterator, + T: Into, + { + self.map(|id| id.is_not_in(values)) + } +} + impl Wrap for EdgeEndpointWrapper { type Wrapped = EdgeEndpointWrapper; @@ -184,12 +250,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 { 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..7fa21be41b 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,8 +15,7 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_filter::{ - builders::{InternalNodeFilterBuilder, InternalNodeIdFilterBuilder}, - CompositeNodeFilter, NodeFilter, + builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, }, property_filter::{ builders::{ @@ -152,14 +151,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; 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 ddbf4cd553..28cddaa87a 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -86,6 +86,24 @@ impl Comparable for Prop { } } +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, + _ => false, + }, + (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) { 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..b596edc9ab 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -10,9 +10,7 @@ use crate::{ is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, + node_filter::builders::InternalNodeFilterBuilder, property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, windowed_filter::Windowed, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, @@ -64,12 +62,6 @@ impl InternalNodeFilterBuilder for Latest { } } -impl InternalNodeIdFilterBuilder for Latest { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - impl InternalPropertyFilterBuilder for Latest { type Filter = Latest; type ExprBuilder = Latest; 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..85e1ac32a0 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -10,9 +10,7 @@ use crate::{ is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, + node_filter::builders::InternalNodeFilterBuilder, property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, EdgeViewFilterOps, InternalPropertyFilterBuilder, @@ -77,12 +75,6 @@ impl InternalNodeFilterBuilder for Layered { } } -impl InternalNodeIdFilterBuilder for Layered { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - impl InternalPropertyFilterBuilder for Layered { type Filter = Layered; type ExprBuilder = Layered; diff --git a/raphtory/src/db/graph/views/filter/model/node_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs index 0ecb15eae0..44c82a8aec 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr.rs @@ -2,7 +2,7 @@ use crate::{ db::{ api::{ properties::PropertiesOps, - state::ops::{Const, Degree, Name, NodeOp, Type}, + state::ops::{Const, Degree, Id, Name, NodeOp, Type}, view::{internal::GraphView, NodeViewOps}, }, graph::views::filter::{ @@ -19,7 +19,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::core::{ - entities::{properties::prop::Prop, VID}, + entities::{properties::prop::Prop, GID, VID}, storage::arc_str::ArcStr, Direction, }; @@ -201,6 +201,18 @@ impl NodeExpr for Name { } } +/// `Id` from `db/api/state/ops/node.rs` used as a node expression. +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)) + } +} + // ───────────────────────────────────────────────────────────────────────────── // NodeExpr impls for constant value types // 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..17a7c17cd3 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 @@ -5,16 +5,6 @@ use crate::db::graph::views::filter::model::{ }; 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() - } -} - pub trait InternalNodeFilterBuilder: Send + Sync + Wrap { type FilterType: From; fn field_name(&self) -> &'static str; @@ -28,24 +18,6 @@ impl InternalNodeFilterBuilder for Arc { } } -#[derive(Clone, Debug)] -pub struct NodeIdFilterBuilder; - -impl Wrap for NodeIdFilterBuilder { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - -impl InternalNodeIdFilterBuilder for NodeIdFilterBuilder { - #[inline] - fn field_name(&self) -> &'static str { - "node_id" - } -} - #[derive(Clone, Debug)] pub struct NodeNameFilterBuilder; 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 5e622061d4..0175bd7586 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, }, - Name, NodeOp, Type, TypeId, + Id, Name, NodeOp, Type, TypeId, }, NodeStateValue, TypedNodeState, }, @@ -21,7 +21,7 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_expr::{DegreeExpr, Metadata, Property}, - node_filter::{builders::NodeIdFilterBuilder, validate::validate}, + node_filter::validate::validate, node_state_filter::NodeStateBoolColOp, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, snapshot_filter::{SnapshotAt, SnapshotLatest}, @@ -37,7 +37,7 @@ use crate::{ errors::GraphError, prelude::{GraphViewOps, PropertyFilter}, }; -use raphtory_api::core::{storage::timeindex::EventTime, Direction}; +use raphtory_api::core::{entities::GID, storage::timeindex::EventTime, Direction}; use std::{fmt, fmt::Display, sync::Arc}; pub mod builders; @@ -55,8 +55,8 @@ impl From for EntityMarker { impl NodeFilter { #[inline] - pub fn id() -> NodeIdFilterBuilder { - NodeIdFilterBuilder + pub fn id() -> Id { + Id } /// Selects the node name field for filtering. @@ -229,6 +229,78 @@ impl TryAsCompositeFilter for NodeIdFilter { } } +impl Id { + pub fn eq(self, value: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::eq_id("node_id", value)) + } + + pub fn ne(self, value: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::ne_id("node_id", value)) + } + + pub fn lt(self, value: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::lt("node_id", value)) + } + + pub fn le(self, value: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::le("node_id", value)) + } + + pub fn gt(self, value: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::gt("node_id", value)) + } + + pub fn ge(self, value: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::ge("node_id", value)) + } + + pub fn starts_with(self, s: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::starts_with("node_id", s)) + } + + pub fn ends_with(self, s: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::ends_with("node_id", s)) + } + + pub fn contains(self, s: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::contains("node_id", s)) + } + + pub fn not_contains(self, s: impl Into) -> NodeIdFilter { + NodeIdFilter(Filter::not_contains("node_id", s)) + } + + pub fn fuzzy_search( + self, + s: impl Into, + levenshtein_distance: usize, + prefix_match: bool, + ) -> NodeIdFilter { + NodeIdFilter(Filter::fuzzy_search( + "node_id", + s, + levenshtein_distance, + prefix_match, + )) + } + + pub fn is_in(self, values: I) -> NodeIdFilter + where + I: IntoIterator, + T: Into, + { + NodeIdFilter(Filter::is_in_id("node_id", values)) + } + + pub fn is_not_in(self, values: I) -> NodeIdFilter + where + I: IntoIterator, + T: Into, + { + NodeIdFilter(Filter::is_not_in_id("node_id", values)) + } +} + #[derive(Debug, Clone)] pub struct NodeNameFilter(pub Filter); 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/windowed_filter.rs b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs index 41f284e5fc..89a02dcf0e 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -10,9 +10,7 @@ use crate::{ is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, + node_filter::builders::InternalNodeFilterBuilder, property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, EdgeViewFilterOps, InternalPropertyFilterBuilder, @@ -90,12 +88,6 @@ impl InternalNodeFilterBuilder for Windowed { } } -impl InternalNodeIdFilterBuilder for Windowed { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - impl InternalPropertyFilterBuilder for Windowed { type Filter = Windowed; type ExprBuilder = Windowed; diff --git a/raphtory/src/python/filter/edge_filter_builders.rs b/raphtory/src/python/filter/edge_filter_builders.rs index cfd92a3f7a..b3ca1392f6 100644 --- a/raphtory/src/python/filter/edge_filter_builders.rs +++ b/raphtory/src/python/filter/edge_filter_builders.rs @@ -1,13 +1,16 @@ 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, + graph::views::filter::model::{ + edge_filter::{EdgeEndpointWrapper, EdgeFilter}, + node_filter::{ + builders::{NodeNameFilterBuilder, NodeTypeFilterBuilder}, + ops::NodeFilterOps, + NodeFilter, + }, + property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, + EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, - property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, - EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, python::{ filter::{ @@ -34,7 +37,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 { @@ -46,7 +49,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. @@ -57,7 +60,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). @@ -68,7 +71,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. @@ -79,7 +82,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). @@ -90,7 +93,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. @@ -101,7 +104,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. @@ -112,7 +115,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. @@ -123,7 +126,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. @@ -134,7 +137,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. @@ -145,7 +148,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. @@ -156,7 +159,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. @@ -167,7 +170,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. @@ -187,7 +190,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, diff --git a/raphtory/src/python/filter/node_filter_builders.rs b/raphtory/src/python/filter/node_filter_builders.rs index 35315829c8..57f19eec14 100644 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ b/raphtory/src/python/filter/node_filter_builders.rs @@ -1,13 +1,9 @@ use crate::{ db::{ - api::state::ops::{Name, Type}, + api::state::ops::{Id, Name, Type}, graph::views::filter::model::{ node_expr::NodeExprFilterOps, - node_filter::{ - builders::{NodeIdFilterBuilder, NodeNameFilterBuilder}, - ops::{NodeFilterOps, NodeIdFilterOps}, - NodeFilter, - }, + node_filter::{builders::NodeNameFilterBuilder, ops::NodeFilterOps, NodeFilter}, node_state_filter::NodeStateBoolColOp, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, @@ -42,7 +38,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 { @@ -364,7 +360,7 @@ impl PyNodeFilter { /// filter.NodeIdFilterBuilder: #[staticmethod] fn id() -> PyNodeIdFilterBuilder { - PyNodeIdFilterBuilder(Arc::new(NodeFilter::id())) + PyNodeIdFilterBuilder(NodeFilter::id()) } /// Selects the node name field for filtering. From 454e3d189c7bd93230a01e3882857013eced5ab8 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:46:52 +0100 Subject: [PATCH 07/22] implement InternalNodeFilterBuilder on Name and Type directly, replacing NodeNameFilterBuilder/NodeTypeFilterBuilder --- .../graph/views/filter/model/edge_filter.rs | 16 +++---- .../graph/views/filter/model/latest_filter.rs | 1 + .../filter/model/node_filter/builders.rs | 26 +++++------ .../src/python/filter/edge_filter_builders.rs | 12 ++--- .../src/python/filter/node_filter_builders.rs | 46 ++----------------- raphtory/src/search/searcher.rs | 10 ++-- 6 files changed, 33 insertions(+), 78 deletions(-) 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 608abefd83..e7bba93982 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::{Id, NotANodeFilter}, + state::ops::{Id, Name, NotANodeFilter, Type}, view::{internal::GraphView, BoxableGraphView}, }, graph::views::filter::{ @@ -15,10 +15,8 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_filter::{ - builders::{ - InternalNodeFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder, - }, - CompositeNodeFilter, NodeFilter, NodeIdFilter, + builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, + NodeIdFilter, }, property_filter::{ builders::{ @@ -160,13 +158,13 @@ impl EdgeEndpointWrapper { } #[inline] - pub fn name(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeNameFilterBuilder, self.endpoint) + pub fn name(&self) -> EdgeEndpointWrapper { + EdgeEndpointWrapper::new(NodeFilter::name(), self.endpoint) } #[inline] - pub fn node_type(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeTypeFilterBuilder, self.endpoint) + pub fn node_type(&self) -> EdgeEndpointWrapper { + EdgeEndpointWrapper::new(NodeFilter::node_type(), self.endpoint) } } 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 b596edc9ab..1187fb65cb 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -57,6 +57,7 @@ impl InternalViewWrapOps for Latest { impl InternalNodeFilterBuilder for Latest { type FilterType = T::FilterType; + fn field_name(&self) -> &'static str { self.inner.field_name() } 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 17a7c17cd3..d593573936 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,7 +1,10 @@ -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}; @@ -18,10 +21,7 @@ impl InternalNodeFilterBuilder for Arc { } } -#[derive(Clone, Debug)] -pub struct NodeNameFilterBuilder; - -impl Wrap for NodeNameFilterBuilder { +impl Wrap for Name { type Wrapped = T; fn wrap(&self, value: T) -> Self::Wrapped { @@ -29,7 +29,7 @@ impl Wrap for NodeNameFilterBuilder { } } -impl InternalNodeFilterBuilder for NodeNameFilterBuilder { +impl InternalNodeFilterBuilder for Name { type FilterType = NodeNameFilter; fn field_name(&self) -> &'static str { @@ -37,10 +37,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 { @@ -48,8 +45,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/python/filter/edge_filter_builders.rs b/raphtory/src/python/filter/edge_filter_builders.rs index b3ca1392f6..bc0239e404 100644 --- a/raphtory/src/python/filter/edge_filter_builders.rs +++ b/raphtory/src/python/filter/edge_filter_builders.rs @@ -1,13 +1,9 @@ use crate::{ db::{ - api::state::ops::Id, + api::state::ops::{Id, Name, Type}, graph::views::filter::model::{ edge_filter::{EdgeEndpointWrapper, EdgeFilter}, - node_filter::{ - builders::{NodeNameFilterBuilder, NodeTypeFilterBuilder}, - ops::NodeFilterOps, - NodeFilter, - }, + node_filter::{ops::NodeFilterOps, NodeFilter}, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, @@ -208,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. /// @@ -220,7 +216,7 @@ pub struct PyEdgeEndpointNameFilterBuilder(pub EdgeEndpointWrapper); +pub struct PyEdgeEndpointTypeFilterBuilder(pub EdgeEndpointWrapper); macro_rules! impl_edge_text_filter_builder { ($py_ty:ident) => { diff --git a/raphtory/src/python/filter/node_filter_builders.rs b/raphtory/src/python/filter/node_filter_builders.rs index 57f19eec14..17908e106e 100644 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ b/raphtory/src/python/filter/node_filter_builders.rs @@ -2,8 +2,7 @@ use crate::{ db::{ api::state::ops::{Id, Name, Type}, graph::views::filter::model::{ - node_expr::NodeExprFilterOps, - node_filter::{builders::NodeNameFilterBuilder, ops::NodeFilterOps, NodeFilter}, + node_filter::{ops::NodeFilterOps, NodeFilter}, node_state_filter::NodeStateBoolColOp, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, @@ -283,25 +282,19 @@ impl_node_text_filter_builder!(PyNodeNameFilterBuilder, Name); impl PyNodeNameFilterBuilder { fn is_in(&self, values: FromIterable) -> PyFilterExpr { let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new(NodeNameFilterBuilder.is_in(vals))) + 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(NodeNameFilterBuilder.is_not_in(vals))) + PyFilterExpr(Arc::new(Name.is_not_in(vals))) } } +impl_node_text_filter_builder!(PyNodeTypeFilterBuilder, Type); + #[pymethods] impl PyNodeTypeFilterBuilder { - fn __eq__(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(Type.eq(ArcStr::from(value)))) - } - - fn __ne__(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(Type.ne(ArcStr::from(value)))) - } - fn is_in(&self, values: FromIterable) -> PyFilterExpr { let vals: Vec = values.into_iter().map(ArcStr::from).collect(); PyFilterExpr(Arc::new(Type.is_in(vals))) @@ -311,35 +304,6 @@ impl PyNodeTypeFilterBuilder { let vals: Vec = values.into_iter().map(ArcStr::from).collect(); PyFilterExpr(Arc::new(Type.is_not_in(vals))) } - - fn starts_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(Type.starts_with(ArcStr::from(value)))) - } - - fn ends_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(Type.ends_with(ArcStr::from(value)))) - } - - fn contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(Type.contains(ArcStr::from(value)))) - } - - fn not_contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(Type.not_contains(ArcStr::from(value)))) - } - - fn fuzzy_search( - &self, - value: String, - levenshtein_distance: usize, - prefix_match: bool, - ) -> PyFilterExpr { - PyFilterExpr(Arc::new(Type.fuzzy_search( - ArcStr::from(value), - levenshtein_distance, - prefix_match, - ))) - } } /// Constructs node filter expressions. 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}, From 142699fdccc50d4650abae9b5cb5f3359e49f10d Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:47:30 +0100 Subject: [PATCH 08/22] implement InternalNodeFilterBuilder on Name and Type directly, replacing NodeNameFilterBuilder/NodeTypeFilterBuilder --- raphtory-graphql/src/model/graph/filtering.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 73b191a567..0b870b4d95 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -1382,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()); From af438c10f4c4caa9b2ffc45857e2901c2d5fc4fb Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:52:16 +0100 Subject: [PATCH 09/22] add comment --- .../src/db/graph/views/filter/model/node_filter/builders.rs | 2 ++ 1 file changed, 2 insertions(+) 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 d593573936..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 @@ -8,6 +8,8 @@ use crate::db::{ }; use std::{ops::Deref, sync::Arc}; +// 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; From 18eaec79313bbfccadb5b424d82e232f3c86504b Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Fri, 5 Jun 2026 12:05:27 +0200 Subject: [PATCH 10/22] some experiments --- raphtory-graphql/schema.graphql | 1 - .../graph/views/filter/model/edge_filter.rs | 68 ----------- .../views/filter/model/filter_operator.rs | 1 + .../db/graph/views/filter/model/node_expr.rs | 106 +++++++++++++++--- .../views/filter/model/node_filter/mod.rs | 72 ------------ 5 files changed, 91 insertions(+), 157 deletions(-) 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/src/db/graph/views/filter/model/edge_filter.rs b/raphtory/src/db/graph/views/filter/model/edge_filter.rs index e7bba93982..efb64f8695 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -16,7 +16,6 @@ use crate::{ layered_filter::Layered, node_filter::{ builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, - NodeIdFilter, }, property_filter::{ builders::{ @@ -168,73 +167,6 @@ impl EdgeEndpointWrapper { } } -impl EdgeEndpointWrapper { - pub fn eq(self, value: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.eq(value)) - } - - pub fn ne(self, value: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.ne(value)) - } - - pub fn lt(self, value: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.lt(value)) - } - - pub fn le(self, value: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.le(value)) - } - - pub fn gt(self, value: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.gt(value)) - } - - pub fn ge(self, value: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.ge(value)) - } - - pub fn starts_with(self, s: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.starts_with(s)) - } - - pub fn ends_with(self, s: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.ends_with(s)) - } - - pub fn contains(self, s: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.contains(s)) - } - - pub fn not_contains(self, s: impl Into) -> EdgeEndpointWrapper { - self.map(|id| id.not_contains(s)) - } - - pub fn fuzzy_search( - self, - s: impl Into, - levenshtein_distance: usize, - prefix_match: bool, - ) -> EdgeEndpointWrapper { - self.map(|id| id.fuzzy_search(s, levenshtein_distance, prefix_match)) - } - - pub fn is_in(self, values: I) -> EdgeEndpointWrapper - where - I: IntoIterator, - T: Into, - { - self.map(|id| id.is_in(values)) - } - - pub fn is_not_in(self, values: I) -> EdgeEndpointWrapper - where - I: IntoIterator, - T: Into, - { - self.map(|id| id.is_not_in(values)) - } -} - impl Wrap for EdgeEndpointWrapper { type Wrapped = EdgeEndpointWrapper; 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 28cddaa87a..8b2a862664 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -64,6 +64,7 @@ macro_rules! impl_comparable_str { 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 { diff --git a/raphtory/src/db/graph/views/filter/model/node_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs index 44c82a8aec..64013018c3 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr.rs @@ -3,7 +3,10 @@ use crate::{ api::{ properties::PropertiesOps, state::ops::{Const, Degree, Id, Name, NodeOp, Type}, - view::{internal::GraphView, NodeViewOps}, + view::{ + internal::{GraphView, NodeList}, + NodeViewOps, + }, }, graph::views::filter::{ model::{ @@ -25,7 +28,6 @@ use raphtory_api::core::{ }; use raphtory_storage::graph::graph::GraphStorage; use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; - // ───────────────────────────────────────────────────────────────────────────── // NodeExpr — typed node expression with associated Output type // ───────────────────────────────────────────────────────────────────────────── @@ -256,13 +258,13 @@ impl NodeExpr for ArcStr { } impl NodeExpr for &'static str { - type Output = String; + type Output = &'static str; fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result + 'g>, GraphError> { - Ok(Arc::new(Const(self.to_string()))) + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(*self))) } } @@ -277,6 +279,17 @@ impl NodeExpr for Prop { } } +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()))) + } +} + macro_rules! impl_node_expr_for_numeric { ($prim:ty, $variant:ident) => { impl NodeExpr for $prim { @@ -292,6 +305,43 @@ macro_rules! impl_node_expr_for_numeric { }; } +#[derive(Debug, Clone, Copy)] +struct AsProp(E); + +#[derive(Debug, Clone, Copy)] +struct AsPropOp(Op); + +impl>> NodeOp for AsPropOp { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { + self.0.apply(storage, node).into() + } + + fn domain(&self, storage: &GraphStorage) -> NodeList { + self.0.domain(storage) + } + + fn const_value_in_domain(&self) -> Option { + self.0.const_value_in_domain().map(|v| v.into()) + } + + fn const_value(&self) -> Option { + self.0.const_value().map(|v| v.into()) + } +} + +impl>> NodeExpr for AsProp { + type Output = Prop; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(AsPropOp(self.0.create_node_op(graph)?))) + } +} + impl_node_expr_for_numeric!(i32, I32); impl_node_expr_for_numeric!(i64, I64); impl_node_expr_for_numeric!(u32, U32); @@ -307,21 +357,16 @@ impl_node_expr_for_numeric!(u16, U16); /// Built-in types (`usize`, `String`, `Prop`, etc.) can be passed directly; /// `ConstExpr` is only needed for custom attribute output types. #[derive(Clone)] -pub struct ConstExpr(pub T) -where - Option: Comparable; +pub struct ConstExpr(pub T); -impl NodeExpr for ConstExpr -where - Option: Comparable, -{ - type Output = Option; +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(Some(self.0.clone())))) + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(self.0.clone()))) } } @@ -377,6 +422,8 @@ impl<'g, I: Clone + Send + Sync + 'static> NodeOp for UnaryNodeOp<'g, I> { // SetNodeOp<'g, T> — evaluates is_in / is_not_in // ───────────────────────────────────────────────────────────────────────────── +// is_some_and, is_none_or + #[derive(Clone)] pub struct SetNodeOp<'g, I: Eq + Hash + Clone + Send + Sync + 'static> { inner: Arc> + 'g>, @@ -856,7 +903,12 @@ impl NodeExprFilterOps for E {} #[cfg(test)] mod tests { use super::*; - use crate::prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}; + use crate::{ + db::api::{state::ops::filter::NO_FILTER, view::filter_ops::NodeSelect}, + prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, + }; + use crate::db::graph::views::filter::model::{PropertyFilterFactory, ViewWrapOps}; + use crate::prelude::NodeFilter; // Test graph: a→b, a→c, b→c // All nodes have total degree 2; in-degrees: a=0, b=1, c=2 @@ -942,4 +994,26 @@ mod tests { 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]) + } + + #[test] + fn test_window_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(); + + g.add_edge(2, 1, 6, NO_PROPS, None).unwrap(); + + let filter = NodeFilter.window(1,3).property("test"); + + } } 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 0175bd7586..4f503f992b 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 @@ -229,78 +229,6 @@ impl TryAsCompositeFilter for NodeIdFilter { } } -impl Id { - pub fn eq(self, value: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::eq_id("node_id", value)) - } - - pub fn ne(self, value: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::ne_id("node_id", value)) - } - - pub fn lt(self, value: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::lt("node_id", value)) - } - - pub fn le(self, value: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::le("node_id", value)) - } - - pub fn gt(self, value: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::gt("node_id", value)) - } - - pub fn ge(self, value: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::ge("node_id", value)) - } - - pub fn starts_with(self, s: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::starts_with("node_id", s)) - } - - pub fn ends_with(self, s: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::ends_with("node_id", s)) - } - - pub fn contains(self, s: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::contains("node_id", s)) - } - - pub fn not_contains(self, s: impl Into) -> NodeIdFilter { - NodeIdFilter(Filter::not_contains("node_id", s)) - } - - pub fn fuzzy_search( - self, - s: impl Into, - levenshtein_distance: usize, - prefix_match: bool, - ) -> NodeIdFilter { - NodeIdFilter(Filter::fuzzy_search( - "node_id", - s, - levenshtein_distance, - prefix_match, - )) - } - - pub fn is_in(self, values: I) -> NodeIdFilter - where - I: IntoIterator, - T: Into, - { - NodeIdFilter(Filter::is_in_id("node_id", values)) - } - - pub fn is_not_in(self, values: I) -> NodeIdFilter - where - I: IntoIterator, - T: Into, - { - NodeIdFilter(Filter::is_not_in_id("node_id", values)) - } -} - #[derive(Debug, Clone)] pub struct NodeNameFilter(pub Filter); From 4659ba20a1fd25344879ffe2f40dd832bbffef86 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:16:11 +0100 Subject: [PATCH 11/22] add temporal property NodeExpr with any/all quantifiers and aggregators (windowed + layered) --- raphtory-graphql/schema.graphql | 1 - .../src/db/graph/views/filter/model/mod.rs | 12 +- .../db/graph/views/filter/model/node_expr.rs | 970 +++++++++++++++++- .../views/filter/model/node_filter/mod.rs | 24 +- .../views/filter/model/property_filter/mod.rs | 2 +- 5 files changed, 1000 insertions(+), 9 deletions(-) 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/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index b1bc781531..7c8623e1ce 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -36,10 +36,16 @@ pub use crate::{ }, filter_operator::{BinaryOp, Comparable, FilterOperator, SetOp, UnaryOp}, node_expr::{ - BinOpNodeFilter, ConstExpr, DegreeExpr, Metadata, NodeExpr, - NodeExprFilterOps, Property, SetNodeFilter, UnaryNodeFilter, + AllMode, AnyMode, AvgExpr, BinOpNodeFilter, ConstExpr, DegreeExpr, + FirstExpr, LastExpr, LenExpr, MaxExpr, Metadata, MinExpr, NoWrap, NodeExpr, + NodeExprContextBuilder, NodeExprFilterOps, Property, + QuantifiedContextBuilder, QuantifiedNodeFilter, QuantifierMode, + SetNodeFilter, SumExpr, TemporalExprOps, TemporalPropContext, + TemporalPropertyExpr, UnaryNodeFilter, + }, + node_filter::{ + NodeFilter, NodeNameFilter, NodeTypeFilter, TemporalNodeExprBuilderOps, }, - node_filter::{NodeFilter, NodeNameFilter, NodeTypeFilter}, not_filter::NotFilter, or_filter::OrFilter, }, diff --git a/raphtory/src/db/graph/views/filter/model/node_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs index 44c82a8aec..dda50da091 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr.rs @@ -9,8 +9,9 @@ use crate::{ model::{ edge_filter::CompositeEdgeFilter, filter_operator::{BinaryOp, Comparable, SetOp, UnaryOp}, + property_filter::{evaluate::aggregate_values, Op}, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, - TryAsCompositeFilter, + TryAsCompositeFilter, Wrap, }, node_filtered_graph::NodeFilteredGraph, }, @@ -48,7 +49,7 @@ use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; /// ``` /// pub trait NodeExpr: Clone + Send + Sync + 'static { - type Output: Comparable + Clone + Send + Sync + 'static; + type Output: Clone + Send + Sync + 'static; /// Compile the expression against a specific graph view. /// @@ -853,10 +854,641 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { impl NodeExprFilterOps for E {} +// ───────────────────────────────────────────────────────────────────────────── +// Sealed trait for QuantifierMode +// ───────────────────────────────────────────────────────────────────────────── + +mod sealed { + pub trait Sealed {} +} + +// ───────────────────────────────────────────────────────────────────────────── +// QuantifierMode — AnyMode / AllMode +// ───────────────────────────────────────────────────────────────────────────── + +pub trait QuantifierMode: sealed::Sealed + Clone + Copy + Send + Sync + 'static { + const IS_ANY: bool; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AnyMode; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AllMode; + +impl sealed::Sealed for AnyMode {} +impl sealed::Sealed for AllMode {} +impl QuantifierMode for AnyMode { + const IS_ANY: bool = true; +} +impl QuantifierMode for AllMode { + const IS_ANY: bool = false; +} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalNodePropOp — returns all temporal values for a property +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct TemporalNodePropOp { + graph: G, + prop_id: usize, +} + +impl NodeOp for TemporalNodePropOp { + type Output = Vec; + + fn apply(&self, _storage: &GraphStorage, node: VID) -> 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() + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalPropertyExpr — NodeExpr> +// ───────────────────────────────────────────────────────────────────────────── + +/// All temporal values of a named property over the current view window. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TemporalPropertyExpr { + pub name: String, +} + +impl TemporalPropertyExpr { + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } + } +} + +impl NodeExpr for TemporalPropertyExpr { + type Output = Vec; + + 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(TemporalNodePropOp { graph, prop_id })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Aggregator NodeOps — compile-time resolved against a concrete graph view +// ───────────────────────────────────────────────────────────────────────────── + +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 = self.inner.apply(storage, node); + ($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() }); + +// ───────────────────────────────────────────────────────────────────────────── +// Aggregator Exprs — NodeExpr wrappers producing a single scalar +// ───────────────────────────────────────────────────────────────────────────── + +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); + +// ───────────────────────────────────────────────────────────────────────────── +// QuantifiedNodeOp — applies any/all quantification over a temporal sequence +// ───────────────────────────────────────────────────────────────────────────── + +pub struct QuantifiedNodeOp<'g> { + inner: Arc> + 'g>, + rhs: Arc> + 'g>, + op: BinaryOp, + is_any: bool, +} + +impl<'g> Clone for QuantifiedNodeOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + rhs: self.rhs.clone(), + op: self.op, + is_any: self.is_any, + } + } +} + +impl<'g> NodeOp for QuantifiedNodeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + let vals = self.inner.apply(storage, node); + let Some(rhs) = self.rhs.apply(storage, node) else { + return false; + }; + if self.is_any { + vals.iter().any(|v| Prop::binary_cmp(&self.op, v, &rhs)) + } else { + !vals.is_empty() && vals.iter().all(|v| Prop::binary_cmp(&self.op, v, &rhs)) + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// QuantifiedNodeFilter — leaf filter wrapping a quantified comparison +// ───────────────────────────────────────────────────────────────────────────── + +pub struct QuantifiedNodeFilter +where + E: NodeExpr>, + Q: QuantifierMode, + R: NodeExpr>, +{ + pub expr: E, + pub rhs: R, + pub op: BinaryOp, + _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>, + Q: QuantifierMode, + R: NodeExpr>, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodeFilteredGraph>; + + type NodeFilter<'graph, G: GraphView + 'graph> = QuantifiedNodeOp<'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 = self.expr.create_node_op(graph.clone())?; + let rhs = self.rhs.create_node_op(graph)?; + Ok(QuantifiedNodeOp { + inner, + rhs, + op: self.op, + is_any: Q::IS_ANY, + }) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } +} + +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) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Context builders — carry wrap context through the builder chain +// ───────────────────────────────────────────────────────────────────────────── + +/// Builder returned from `.any()` / `.all()` on a temporal expression. +/// +/// Carries the wrapper context `W` (identity for `NodeFilter`, `Windowed` for windowed filters). +/// Call `.eq(rhs)`, `.gt(rhs)` etc. to produce the final filter wrapped in `W`. +pub struct QuantifiedContextBuilder +where + W: Wrap + Clone, + E: NodeExpr>, + Q: QuantifierMode, +{ + pub(crate) wrap_ctx: W, + pub(crate) expr: E, + pub(crate) _q: PhantomData, +} + +impl QuantifiedContextBuilder +where + W: Wrap + Clone, + E: NodeExpr>, + Q: QuantifierMode, +{ + fn finish>>( + self, + op: BinaryOp, + rhs: R, + ) -> W::Wrapped> { + self.wrap_ctx + .wrap(QuantifiedNodeFilter::new(self.expr, op, rhs)) + } + + pub fn eq>>( + self, + rhs: R, + ) -> W::Wrapped> { + self.finish(BinaryOp::Eq, rhs) + } + + pub fn ne>>( + self, + rhs: R, + ) -> W::Wrapped> { + self.finish(BinaryOp::Ne, rhs) + } + + pub fn gt>>( + self, + rhs: R, + ) -> W::Wrapped> { + self.finish(BinaryOp::Gt, rhs) + } + + pub fn ge>>( + self, + rhs: R, + ) -> W::Wrapped> { + self.finish(BinaryOp::Ge, rhs) + } + + pub fn lt>>( + self, + rhs: R, + ) -> W::Wrapped> { + self.finish(BinaryOp::Lt, rhs) + } + + pub fn le>>( + self, + rhs: R, + ) -> W::Wrapped> { + self.finish(BinaryOp::Le, rhs) + } +} + +/// Builder returned from aggregators (`.sum()`, `.avg()` etc.) on a temporal expression. +/// +/// Carries the wrapper context `W` and the aggregator expression `E`. +/// Call `.eq(rhs)`, `.gt(rhs)` etc. to produce the final filter wrapped in `W`. +pub struct NodeExprContextBuilder +where + W: Wrap + Clone, + E: NodeExpr, +{ + pub(crate) wrap_ctx: W, + pub(crate) expr: E, +} + +impl NodeExprContextBuilder +where + W: Wrap + Clone, + E: NodeExpr, +{ + fn finish>( + self, + op: BinaryOp, + rhs: R, + ) -> W::Wrapped> { + self.wrap_ctx.wrap(BinOpNodeFilter::new(self.expr, op, rhs)) + } + + pub fn eq>(self, rhs: R) -> W::Wrapped> { + self.finish(BinaryOp::Eq, rhs) + } + + pub fn ne>(self, rhs: R) -> W::Wrapped> { + self.finish(BinaryOp::Ne, rhs) + } + + pub fn gt>(self, rhs: R) -> W::Wrapped> { + self.finish(BinaryOp::Gt, rhs) + } + + pub fn ge>(self, rhs: R) -> W::Wrapped> { + self.finish(BinaryOp::Ge, rhs) + } + + pub fn lt>(self, rhs: R) -> W::Wrapped> { + self.finish(BinaryOp::Lt, rhs) + } + + pub fn le>(self, rhs: R) -> W::Wrapped> { + self.finish(BinaryOp::Le, rhs) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalPropContext — entry point returned from `.temporal_property(name)` +// ───────────────────────────────────────────────────────────────────────────── + +/// Builder returned from `.temporal_property(name)`. +/// +/// `W` carries the wrapping context so that windowed temporal filters are correctly +/// produced when called on a `Windowed`. +/// +/// Usage: +/// ```rust,ignore +/// NodeFilter::temporal_property("score").any().gt(10i64) +/// NodeFilter.window(0, 100).temporal_property("score").any().gt(10i64) +/// NodeFilter::temporal_property("price").sum().gt(100i64) +/// ``` +pub struct TemporalPropContext { + wrap_ctx: W, + expr: TemporalPropertyExpr, +} + +impl TemporalPropContext { + pub(crate) fn new(wrap_ctx: W, name: impl Into) -> Self { + Self { + wrap_ctx, + expr: TemporalPropertyExpr::new(name), + } + } + + pub fn any(self) -> QuantifiedContextBuilder { + QuantifiedContextBuilder { + wrap_ctx: self.wrap_ctx, + expr: self.expr, + _q: PhantomData, + } + } + + pub fn all(self) -> QuantifiedContextBuilder { + QuantifiedContextBuilder { + wrap_ctx: self.wrap_ctx, + expr: self.expr, + _q: PhantomData, + } + } + + pub fn sum(self) -> NodeExprContextBuilder> { + NodeExprContextBuilder { + wrap_ctx: self.wrap_ctx, + expr: SumExpr(self.expr), + } + } + + pub fn avg(self) -> NodeExprContextBuilder> { + NodeExprContextBuilder { + wrap_ctx: self.wrap_ctx, + expr: AvgExpr(self.expr), + } + } + + pub fn min(self) -> NodeExprContextBuilder> { + NodeExprContextBuilder { + wrap_ctx: self.wrap_ctx, + expr: MinExpr(self.expr), + } + } + + pub fn max(self) -> NodeExprContextBuilder> { + NodeExprContextBuilder { + wrap_ctx: self.wrap_ctx, + expr: MaxExpr(self.expr), + } + } + + pub fn first(self) -> NodeExprContextBuilder> { + NodeExprContextBuilder { + wrap_ctx: self.wrap_ctx, + expr: FirstExpr(self.expr), + } + } + + pub fn last(self) -> NodeExprContextBuilder> { + NodeExprContextBuilder { + wrap_ctx: self.wrap_ctx, + expr: LastExpr(self.expr), + } + } + + pub fn len(self) -> NodeExprContextBuilder> { + NodeExprContextBuilder { + wrap_ctx: self.wrap_ctx, + expr: LenExpr(self.expr), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalExprOps — blanket trait for E: NodeExpr> +// ───────────────────────────────────────────────────────────────────────────── + +/// Quantifier and aggregator operators for temporal property sequences. +/// +/// Available on any `NodeExpr>` (e.g. `TemporalPropertyExpr`). +pub trait TemporalExprOps: NodeExpr> + Sized { + fn any(self) -> QuantifiedContextBuilder { + QuantifiedContextBuilder { + wrap_ctx: NoWrap, + expr: self, + _q: PhantomData, + } + } + + fn all(self) -> QuantifiedContextBuilder { + QuantifiedContextBuilder { + wrap_ctx: NoWrap, + 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 {} + +/// Identity wrapper — used by `TemporalExprOps` blanket to avoid wrapping. +#[derive(Debug, Clone, Copy)] +pub struct NoWrap; + +impl Wrap for NoWrap { + type Wrapped = T; + + fn wrap(&self, value: T) -> T { + value + } +} + #[cfg(test)] mod tests { use super::*; - use crate::prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}; + use crate::{ + db::graph::views::filter::model::{ + node_filter::{NodeFilter, TemporalNodeExprBuilderOps}, + ViewWrapOps, + }, + prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, + }; + use raphtory_api::core::entities::properties::prop::IntoProp; // Test graph: a→b, a→c, b→c // All nodes have total degree 2; in-degrees: a=0, b=1, c=2 @@ -942,4 +1574,336 @@ mod tests { let g = build_test_graph(); assert_eq!(filtered_names(filter, g), vec!["a", "b", "c"]); } + + // ── 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::temporal_property("score").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 filter using the full two-step pipeline (filter_graph_view → create_filter). + /// Required for windowed filters where filter_graph_view applies the window. + fn windowed_filtered_names(filter: F, g: Graph) -> Vec + where + F: CreateFilter + Clone, + for<'graph> F::EntityFiltered<'graph, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, + { + let fg = filter.filter_graph_view(g).unwrap(); + let mut names: Vec = filter + .create_filter(fg) + .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) + .temporal_property("score") + .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) + .temporal_property("score") + .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 + } + + /// Run the full filter_graph_view → create_filter pipeline for a layered filter. + /// Identical in structure to `windowed_filtered_names`; factored separately for clarity. + fn layered_filtered_names(filter: F, g: Graph) -> Vec + where + F: CreateFilter + Clone, + for<'graph> F::EntityFiltered<'graph, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, + { + let fg = filter.filter_graph_view(g).unwrap(); + let mut names: Vec = filter + .create_filter(fg) + .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") + .temporal_property("score") + .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") + .temporal_property("score") + .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") + .temporal_property("score") + .sum() + .gt(10i64); + assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); + } } 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 0175bd7586..5ce2068f92 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 @@ -20,7 +20,7 @@ use crate::{ is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, - node_expr::{DegreeExpr, Metadata, Property}, + node_expr::{DegreeExpr, Metadata, Property, TemporalPropContext}, node_filter::validate::validate, node_state_filter::NodeStateBoolColOp, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, @@ -117,8 +117,30 @@ impl NodeFilter { pub fn metadata(name: impl Into) -> Metadata { Metadata::new(name) } + + /// Full temporal history of a named property as a sequence of `Prop` values. + /// + /// Values are scoped to the current view window (all time if no window applied). + /// Chain with `.any()`, `.all()`, `.sum()`, `.avg()`, `.min()`, `.max()`, + /// `.first()`, `.last()`, or `.len()` to produce a filter or scalar expression. + #[inline] + pub fn temporal_property(name: impl Into) -> TemporalPropContext { + TemporalPropContext::new(NodeFilter, name) + } } +/// Extension trait that adds `.temporal_property(name)` to any wrapper type. +/// +/// Implemented for all `W: Wrap + Clone` so that `NodeFilter`, `Windowed`, +/// `Latest`, etc. all support the same entry point. +pub trait TemporalNodeExprBuilderOps: Wrap + Clone + Sized { + fn temporal_property(self, name: impl Into) -> TemporalPropContext { + TemporalPropContext::new(self, name) + } +} + +impl TemporalNodeExprBuilderOps for T {} + impl Wrap for NodeFilter { type Wrapped = T; 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 3bf3fe8a3b..f00f82d6ad 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 @@ -45,7 +45,7 @@ use raphtory_storage::graph::{ use std::{fmt, fmt::Display, sync::Arc}; pub mod builders; -mod evaluate; +pub(crate) mod evaluate; pub mod ops; mod validate; From 16e63046ae6c20bd4eef897e386459de372a4232 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:25:16 +0100 Subject: [PATCH 12/22] split QuantifiedNodeOp into AnyNodeOp/AllNodeOp, fix post-merge build breaks --- .../db/graph/views/filter/model/node_expr.rs | 107 ++++++++++++++---- 1 file changed, 82 insertions(+), 25 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/node_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs index 25c430fc5c..86aae97904 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr.rs @@ -1073,28 +1073,26 @@ impl_agg_expr!(LastExpr, LastNodeOp, Option); impl_agg_expr!(LenExpr, LenNodeOp, usize); // ───────────────────────────────────────────────────────────────────────────── -// QuantifiedNodeOp — applies any/all quantification over a temporal sequence +// AnyNodeOp / AllNodeOp — quantified comparison over a temporal sequence // ───────────────────────────────────────────────────────────────────────────── -pub struct QuantifiedNodeOp<'g> { +pub struct AnyNodeOp<'g> { inner: Arc> + 'g>, rhs: Arc> + 'g>, op: BinaryOp, - is_any: bool, } -impl<'g> Clone for QuantifiedNodeOp<'g> { +impl<'g> Clone for AnyNodeOp<'g> { fn clone(&self) -> Self { Self { inner: self.inner.clone(), rhs: self.rhs.clone(), op: self.op, - is_any: self.is_any, } } } -impl<'g> NodeOp for QuantifiedNodeOp<'g> { +impl<'g> NodeOp for AnyNodeOp<'g> { type Output = bool; fn apply(&self, storage: &GraphStorage, node: VID) -> bool { @@ -1102,14 +1100,38 @@ impl<'g> NodeOp for QuantifiedNodeOp<'g> { let Some(rhs) = self.rhs.apply(storage, node) else { return false; }; - if self.is_any { - vals.iter().any(|v| Prop::binary_cmp(&self.op, v, &rhs)) - } else { - !vals.is_empty() && vals.iter().all(|v| Prop::binary_cmp(&self.op, v, &rhs)) + vals.iter().any(|v| Prop::binary_cmp(&self.op, v, &rhs)) + } +} + +pub struct AllNodeOp<'g> { + inner: Arc> + 'g>, + rhs: Arc> + 'g>, + op: BinaryOp, +} + +impl<'g> Clone for AllNodeOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + rhs: self.rhs.clone(), + op: self.op, } } } +impl<'g> NodeOp for AllNodeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + let vals = self.inner.apply(storage, node); + let Some(rhs) = self.rhs.apply(storage, node) else { + return false; + }; + !vals.is_empty() && vals.iter().all(|v| Prop::binary_cmp(&self.op, v, &rhs)) + } +} + // ───────────────────────────────────────────────────────────────────────────── // QuantifiedNodeFilter — leaf filter wrapping a quantified comparison // ───────────────────────────────────────────────────────────────────────────── @@ -1166,17 +1188,53 @@ where { } -impl CreateFilter for QuantifiedNodeFilter +impl CreateFilter for QuantifiedNodeFilter where E: NodeExpr>, - Q: QuantifierMode, R: NodeExpr>, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - NodeFilteredGraph>; + 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>; - type NodeFilter<'graph, G: GraphView + 'graph> = QuantifiedNodeOp<'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> { + Ok(AnyNodeOp { + inner: self.expr.create_node_op(graph.clone())?, + rhs: self.rhs.create_node_op(graph)?, + op: self.op, + }) + } + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } +} + +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 @@ -1195,13 +1253,10 @@ where self, graph: G, ) -> Result, GraphError> { - let inner = self.expr.create_node_op(graph.clone())?; - let rhs = self.rhs.create_node_op(graph)?; - Ok(QuantifiedNodeOp { - inner, - rhs, + Ok(AllNodeOp { + inner: self.expr.create_node_op(graph.clone())?, + rhs: self.rhs.create_node_op(graph)?, op: self.op, - is_any: Q::IS_ANY, }) } @@ -1527,14 +1582,16 @@ impl Wrap for NoWrap { mod tests { use super::*; use crate::{ - db::graph::views::filter::model::{ - node_filter::{NodeFilter, TemporalNodeExprBuilderOps}, - ViewWrapOps, + db::{ + api::view::filter_ops::NodeSelect, + graph::views::filter::model::{ + node_filter::{NodeFilter, TemporalNodeExprBuilderOps}, + ViewWrapOps, + }, }, prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, }; use raphtory_api::core::entities::properties::prop::IntoProp; - use crate::db::api::view::filter_ops::NodeSelect; // Test graph: a→b, a→c, b→c // All nodes have total degree 2; in-degrees: a=0, b=1, c=2 From b09096a3a7290e0009256044bdc61bf55510f845 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Mon, 8 Jun 2026 15:31:56 +0200 Subject: [PATCH 13/22] start reworking some bits --- Cargo.lock | 8 +- raphtory/src/db/graph/views/filter/mod.rs | 22 - .../filter/model/is_active_node_filter.rs | 25 +- .../src/db/graph/views/filter/model/mod.rs | 448 ++++++------------ .../db/graph/views/filter/model/node_expr.rs | 135 ++++-- .../views/filter/model/node_filter/mod.rs | 66 ++- 6 files changed, 274 insertions(+), 430 deletions(-) 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/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/is_active_node_filter.rs b/raphtory/src/db/graph/views/filter/model/is_active_node_filter.rs index 994d3d2521..af4f6b8578 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 { + 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 @@ -47,6 +49,7 @@ impl CreateFilter for IsActiveNode { self, graph: G, ) -> Result, GraphError> { + let graph = self.view_expr.create_view(graph.clone())?; let op = self.create_node_filter(graph.clone())?; Ok(NodeFilteredGraph::new(graph, op)) } @@ -55,21 +58,15 @@ impl CreateFilter for IsActiveNode { self, graph: G, ) -> Result, GraphError> { - let op: Map, bool> = HistoryOp::new(graph).map(|h| !h.is_empty()); + let op: Map, bool> = + HistoryOp::new(self.view_expr.create_view(graph)?).map(|h| !h.is_empty()); Ok(op) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } -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)) } diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 7c8623e1ce..79b062649c 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, @@ -41,11 +17,9 @@ pub use crate::{ NodeExprContextBuilder, NodeExprFilterOps, Property, QuantifiedContextBuilder, QuantifiedNodeFilter, QuantifierMode, SetNodeFilter, SumExpr, TemporalExprOps, TemporalPropContext, - TemporalPropertyExpr, UnaryNodeFilter, - }, - node_filter::{ - NodeFilter, NodeNameFilter, NodeTypeFilter, TemporalNodeExprBuilderOps, + UnaryNodeFilter, }, + node_filter::NodeFilter, not_filter::NotFilter, or_filter::OrFilter, }, @@ -57,9 +31,40 @@ pub use crate::{ errors::GraphError, prelude::{GraphViewOps, TimeOps}, }; +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, + node_expr::{NodeMetaOp, NodePropOp}, + property_filter::{ + builders::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, }; @@ -121,13 +126,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 { @@ -179,22 +177,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, @@ -205,11 +187,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 @@ -229,13 +206,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 { @@ -265,131 +235,6 @@ 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; - - fn dyn_into_expr_builder( - &self, - builder: PropertyExprBuilderInput, - ) -> Arc; -} - -impl DynPropertyFilterBuilder for T { - fn dyn_property_ref(&self) -> PropertyRef { - self.property_ref() - } - - fn dyn_ops(&self) -> &[Op] { - self.ops() - } - - fn dyn_entity(&self) -> EntityMarker { - self.entity().into() - } - - fn dyn_filter(&self, filter: PropertyFilterInput) -> Arc { - Arc::new(self.filter(filter)) - } - - fn dyn_into_expr_builder( - &self, - builder: PropertyExprBuilderInput, - ) -> Arc { - Arc::new(self.with_expr_builder(builder)) - } -} - -impl InternalPropertyFilterBuilder for Arc { - type Filter = Arc; - type ExprBuilder = Arc; - type Marker = EntityMarker; - - fn property_ref(&self) -> PropertyRef { - self.deref().dyn_property_ref() - } - - fn ops(&self) -> &[Op] { - self.deref().dyn_ops() - } - - fn entity(&self) -> Self::Marker { - self.deref().dyn_entity() - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.deref().dyn_filter(filter) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.deref().dyn_into_expr_builder(builder) - } -} - -impl InternalPropertyFilterBuilder for Arc { - type Filter = Arc; - type ExprBuilder = Arc; - type Marker = EntityMarker; - - fn property_ref(&self) -> PropertyRef { - self.deref().dyn_property_ref() - } - - fn ops(&self) -> &[Op] { - self.deref().dyn_ops() - } - - fn entity(&self) -> Self::Marker { - self.deref().dyn_entity() - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.deref().dyn_filter(filter) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.deref().dyn_into_expr_builder(builder) - } -} - -impl InternalPropertyFilterBuilder for Arc { - type Filter = T::Filter; - type ExprBuilder = T::ExprBuilder; - type Marker = T::Marker; - - fn property_ref(&self) -> PropertyRef { - self.deref().property_ref() - } - - fn ops(&self) -> &[Op] { - self.deref().ops() - } - - fn entity(&self) -> Self::Marker { - self.deref().entity() - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.deref().filter(filter) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.deref().with_expr_builder(builder) - } } #[derive(Copy, Clone)] @@ -399,92 +244,90 @@ pub enum EntityMarker { 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; +#[derive(Clone)] +pub struct PropertyExpr { + view_expr: E, + name: String, } -impl DynPropertyFilterFactory for T { - fn dyn_entity(&self) -> EntityMarker { - self.entity().into() - } +impl NodeExpr for PropertyExpr { + type Output = Option; - fn dyn_property_builder(&self, property: String) -> Arc { - Arc::new(self.property_builder(property)) + 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_metadata_builder(&self, property: String) -> Arc { - Arc::new(self.metadata_builder(property)) - } +#[derive(Clone)] +pub struct MetadataExpr { + view_expr: E, + name: String, } -impl InternalPropertyFilterFactory for Arc { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; +impl NodeExpr for MetadataExpr { + type Output = Option; - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() + 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, true) + .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; + let graph = self.view_expr.create_view(graph)?; + Ok(Arc::new(NodeMetaOp { graph, prop_id })) } +} - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) - } +pub trait PropertyFilterFactory: Sized { + fn property(&self, name: impl Into) -> PropertyExpr; - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) - } + fn metadata(&self, name: impl Into) -> MetadataExpr; } -pub trait PropertyFilterFactory: InternalPropertyFilterFactory { - fn property(&self, name: impl Into) -> Self::PropertyBuilder { - self.property_builder(name.into()) +impl PropertyFilterFactory for T { + fn property(&self, name: impl Into) -> PropertyExpr { + PropertyExpr { + view_expr: self.clone(), + name, + } } - fn metadata(&self, name: impl Into) -> Self::MetadataBuilder { - self.metadata_builder(name.into()) + fn metadata(&self, name: impl Into) -> MetadataExpr { + MetadataExpr { + view_expr: self.clone(), + name, + } } } -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 DynPropertyFilterFactory { + fn property(&self, name: String) -> PropertyExpr>; } -pub trait DynTemporalPropertyFilterBuilder: DynPropertyFilterBuilder { - fn dyn_temporal(&self) -> Arc; +pub struct TemporalPropertyExpr { + view_expr: E, + name: String, } -impl DynTemporalPropertyFilterBuilder for T { - fn dyn_temporal(&self) -> Arc { - Arc::new(self.temporal()) +impl PropertyExpr { + pub fn temporal(&self) -> TemporalPropertyExpr { + TemporalPropertyExpr { + view_expr: self.view_expr.clone(), + name: self.name.clone(), + } } } -impl TemporalPropertyFilterFactory for Arc {} - pub trait TryAsCompositeFilter: Send + Sync { fn try_as_composite_node_filter(&self) -> Result; @@ -611,32 +454,71 @@ 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 { + 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 { + 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)) } +} + +impl CreateView for NodeFilter { + type View<'graph, G: GraphView + 'graph> = G; - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) + 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) } } +pub trait ViewWrapPropOps: InternalViewWrapOps + PropertyFilterFactory + Sized {} + +impl ViewWrapPropOps for T where T: InternalViewWrapOps + PropertyFilterFactory + Sized {} + +pub trait DynInternalViewWrapPropOps: DynInternalViewWrapOps + DynPropertyFilterFactory {} + +impl DynInternalViewWrapPropOps for T where T: DynInternalViewWrapOps + DynPropertyFilterFactory {} + impl InternalViewWrapOps for Arc { type Window = Arc; @@ -750,24 +632,6 @@ impl NodeViewFilterOps for DynNodeViewProps { } } -impl InternalPropertyFilterFactory for DynNodeViewProps { - 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 type DynEdgeViewProps = Arc; impl InternalViewWrapOps for DynEdgeViewProps { @@ -801,21 +665,3 @@ impl EdgeViewFilterOps for DynEdgeViewProps { self.deref().dyn_is_self_loop() } } - -impl InternalPropertyFilterFactory for DynEdgeViewProps { - 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) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/node_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs index 86aae97904..cf3345afdb 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr.rs @@ -14,7 +14,7 @@ use crate::{ filter_operator::{BinaryOp, Comparable, SetOp, UnaryOp}, property_filter::{evaluate::aggregate_values, Op}, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, - TryAsCompositeFilter, Wrap, + CreateView, InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, node_filtered_graph::NodeFilteredGraph, }, @@ -24,7 +24,7 @@ use crate::{ }; use raphtory_api::core::{ entities::{properties::prop::Prop, GID, VID}, - storage::arc_str::ArcStr, + storage::{arc_str::ArcStr, timeindex::EventTime}, Direction, }; use raphtory_storage::graph::graph::GraphStorage; @@ -69,8 +69,8 @@ pub trait NodeExpr: Clone + Send + Sync + 'static { /// Evaluates a temporal property by pre-resolved column ID. #[derive(Clone)] pub(crate) struct NodePropOp { - graph: G, - prop_id: usize, + pub(crate) graph: G, + pub(crate) prop_id: usize, } impl NodeOp for NodePropOp { @@ -84,8 +84,8 @@ impl NodeOp for NodePropOp { /// Evaluates a metadata (static) field by pre-resolved column ID. #[derive(Clone)] pub(crate) struct NodeMetaOp { - graph: G, - prop_id: usize, + pub(crate) graph: G, + pub(crate) prop_id: usize, } impl NodeOp for NodeMetaOp { @@ -104,9 +104,12 @@ impl NodeOp for NodeMetaOp { /// /// Delegates to `Degree` from `db/api/state/ops/node.rs`. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct DegreeExpr(pub Direction); +pub struct DegreeExpr { + pub dir: Direction, + pub view_expr: E, +} -impl NodeExpr for DegreeExpr { +impl NodeExpr for DegreeExpr { type Output = usize; fn create_node_op<'g, G: GraphView + 'g>( @@ -114,8 +117,8 @@ impl NodeExpr for DegreeExpr { graph: G, ) -> Result + 'g>, GraphError> { Ok(Arc::new(Degree { - dir: self.0, - view: graph, + dir: self.dir, + view: self.view_expr.create_view(graph), })) } } @@ -472,6 +475,17 @@ where pub right: R, } +// [0, 1, 2, 3] < Const(2) => [true, true, false, false] +// [[0, 1], [0, 1, 2, 3]] < 2 => [[true, true], [true, true, false, false]] +// ([[0, 1], [0, 1, 2, 3]] < 2).any() => [true, true] +// ([[0, 1], [0, 1, 2, 3]] < 2).all() => [true, false] +// ([[0, 1], [0, 1, 2, 3]] < 2).any().all() => true +// ([[0, 1], [0, 1, 2, 3]] < 2).all().all() => false +// ([[0, 1], [0, 1, 2, 3]] < 2).all().any() => true + +// AnyExpr> +// NodeFilter.property("boolean_list_property").any() + impl BinOpNodeFilter where L: NodeExpr, @@ -540,13 +554,6 @@ where op: self.op, }) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl TryAsCompositeFilter for BinOpNodeFilter @@ -638,13 +645,6 @@ where let inner = self.expr.create_node_op(graph)?; Ok(UnaryNodeOp { inner, op: self.op }) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl TryAsCompositeFilter for UnaryNodeFilter @@ -743,13 +743,6 @@ where values: self.values, }) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl TryAsCompositeFilter for SetNodeFilter @@ -944,7 +937,7 @@ impl NodeOp for TemporalNodePropOp { type Output = Vec; fn apply(&self, _storage: &GraphStorage, node: VID) -> Vec { - self.graph + (&&self.graph) .node(node) .and_then(|n| { n.properties() @@ -1110,6 +1103,10 @@ pub struct AllNodeOp<'g> { op: BinaryOp, } +pub struct AllNodeOp2<'g> { + inner: Arc> + 'g>, +} + impl<'g> Clone for AllNodeOp<'g> { fn clone(&self) -> Self { Self { @@ -1219,13 +1216,6 @@ where op: self.op, }) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl CreateFilter for QuantifiedNodeFilter @@ -1259,13 +1249,6 @@ where op: self.op, }) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl TryAsCompositeFilter for QuantifiedNodeFilter @@ -1625,7 +1608,14 @@ mod tests { fn degree_ge_2_keeps_all_nodes() { let g = build_test_graph(); assert_eq!( - filtered_names(DegreeExpr(Direction::BOTH).ge(2usize), g), + filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .ge(2usize), + g + ), vec!["a", "b", "c"] ); } @@ -1633,14 +1623,29 @@ mod tests { #[test] fn degree_eq_1_keeps_no_nodes() { let g = build_test_graph(); - assert!(filtered_names(DegreeExpr(Direction::BOTH).eq(1usize), g).is_empty()); + 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(Direction::BOTH).le(2usize), g), + filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .le(2usize), + g + ), vec!["a", "b", "c"] ); } @@ -1648,13 +1653,29 @@ mod tests { #[test] fn degree_gt_2_keeps_no_nodes() { let g = build_test_graph(); - assert!(filtered_names(DegreeExpr(Direction::BOTH).gt(2usize), g).is_empty()); + 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(Direction::BOTH).ne(2usize), g).is_empty()); + assert!(filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .ne(2usize), + g + ) + .is_empty()); } // ── expression-vs-expression: RHS can be another NodeExpr ──────────────── @@ -1664,7 +1685,17 @@ mod tests { // 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(Direction::BOTH).gt(DegreeExpr(Direction::IN)), g), + filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .gt(DegreeExpr { + dir: Direction::IN, + view_expr: NodeFilter + }), + g + ), vec!["a", "b"] ); } 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 3f234114ae..3536155318 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 @@ -27,8 +27,8 @@ use crate::{ snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, AndFilter, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - EntityMarker, InternalPropertyFilterFactory, InternalViewWrapOps, - NodeViewFilterOps, NotFilter, OrFilter, TryAsCompositeFilter, Wrap, + EntityMarker, InternalViewWrapOps, NodeViewFilterOps, NotFilter, OrFilter, + PropertyFilterFactory, TryAsCompositeFilter, Wrap, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -53,9 +53,9 @@ impl From for EntityMarker { } } -impl NodeFilter { +pub trait NodeFilterFactory: PropertyFilterFactory { #[inline] - pub fn id() -> Id { + fn id(&self) -> Id { Id } @@ -64,7 +64,7 @@ impl NodeFilter { /// Returns `Name` which implements `NodeExprFilterOps` — use `.eq("Alice")`, /// `.contains("ali")`, `.is_in([…])`, etc. directly on the returned value. #[inline] - pub fn name() -> Name { + fn name(&self) -> Name { Name } @@ -72,12 +72,13 @@ impl NodeFilter { /// /// Returns `Type` which implements `NodeExprFilterOps`. #[inline] - pub fn node_type() -> Type { + 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 @@ -89,23 +90,36 @@ impl NodeFilter { } /// Total degree expression — supports `.gt(n)`, `.lt(n)`, etc. - #[inline] - pub fn degree() -> DegreeExpr { - DegreeExpr(Direction::BOTH) + fn degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::BOTH, + view_expr: self.clone(), + } } /// In-degree expression. - #[inline] - pub fn in_degree() -> DegreeExpr { - DegreeExpr(Direction::IN) + fn in_degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::IN, + view_expr: self.clone(), + } } /// Out-degree expression. #[inline] - pub fn out_degree() -> DegreeExpr { - DegreeExpr(Direction::OUT) + fn out_degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::OUT, + view_expr: self.clone(), + } + } + + fn is_active(&self) -> IsActiveNode { + IsActiveNode } +} +impl NodeFilter { /// Current (latest) value of a named property — serializable. #[inline] pub fn property(name: impl Into) -> Property { @@ -157,30 +171,8 @@ impl InternalViewWrapOps for NodeFilter { } } -impl InternalPropertyFilterFactory for NodeFilter { - type Entity = NodeFilter; - type PropertyBuilder = PropertyFilterBuilder; - type MetadataBuilder = MetadataFilterBuilder; - - fn entity(&self) -> Self::Entity { - NodeFilter - } - - 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 NodeViewFilterOps for NodeFilter { type Output = T; - - fn is_active(&self) -> Self::Output { - IsActiveNode - } } #[derive(Debug, Clone)] From 9a1381f9342410271b24ee9ad4e71032f7d8553e Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 9 Jun 2026 10:06:58 +0200 Subject: [PATCH 14/22] break more things --- raphtory/src/db/api/state/ops/mod.rs | 27 ++++++- .../filter/model/is_active_node_filter.rs | 8 +- .../db/graph/views/filter/model/node_expr.rs | 10 +-- .../views/filter/model/node_filter/mod.rs | 73 ++----------------- 4 files changed, 39 insertions(+), 79 deletions(-) diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 83afa1851e..5c045633d3 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -3,9 +3,12 @@ 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::BinOpNodeOp, BinaryOp, Comparable}, }; pub use history::*; pub use node::*; @@ -15,6 +18,7 @@ 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; @@ -41,6 +45,23 @@ pub trait NodeOp: Send + Sync { { Map { op: self, map } } + + + /// Override if binary comparison can be optimised + fn bin_cmp( + &self, + op: BinaryOp, + rhs: Arc>, + ) -> Arc> + where + Self::Output: Comparable, + { + Arc::new(BinOpNodeOp { + left: Arc::new(self.clone()), + right: rhs, + op, + }) + } } pub trait IntoArrowNodeOp: NodeOp + Sized { 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 af4f6b8578..415e048c58 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 @@ -17,7 +17,7 @@ use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IsActiveNode { - view_expr: E, + pub(crate) view_expr: E, } impl fmt::Display for IsActiveNode { @@ -66,9 +66,11 @@ impl CreateFilter 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/node_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs index cf3345afdb..07ef647b57 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr.rs @@ -524,9 +524,9 @@ where L::Output: Comparable, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - NodeFilteredGraph>; + NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph> = BinOpNodeOp<'graph, L::Output>; + type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -548,11 +548,7 @@ where ) -> Result, GraphError> { let left = self.left.create_node_op(graph.clone())?; let right = self.right.create_node_op(graph)?; - Ok(BinOpNodeOp { - left, - right, - op: self.op, - }) + Ok(left.bin_cmp(self.op, right)) } } 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 3536155318..7cf5ecc81b 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 @@ -114,11 +114,15 @@ pub trait NodeFilterFactory: PropertyFilterFactory { } } - fn is_active(&self) -> IsActiveNode { - IsActiveNode + fn is_active(&self) -> IsActiveNode { + IsActiveNode { + view_expr: self.clone(), + } } } +impl NodeFilterFactory for NodeFilter {} + impl NodeFilter { /// Current (latest) value of a named property — serializable. #[inline] @@ -171,10 +175,6 @@ impl InternalViewWrapOps for NodeFilter { } } -impl NodeViewFilterOps for NodeFilter { - type Output = T; -} - #[derive(Debug, Clone)] pub struct NodeIdFilter(pub Filter); @@ -218,13 +218,6 @@ 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 { @@ -284,13 +277,6 @@ 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 { @@ -367,13 +353,6 @@ 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 { @@ -403,7 +382,7 @@ pub enum CompositeNodeFilter { SnapshotAt(Box>), SnapshotLatest(Box>), Layered(Box>), - IsActiveNode(IsActiveNode), + IsActiveNode(Box), And(Box, Box), Or(Box, Box), Not(Box), @@ -496,44 +475,6 @@ impl CreateFilter for CompositeNodeFilter { } } } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - match self.clone() { - CompositeNodeFilter::Id(i) => Ok(Arc::new(NodeIdFilter(i).filter_graph_view(graph)?)), - CompositeNodeFilter::Name(i) => { - Ok(Arc::new(NodeNameFilter(i).filter_graph_view(graph)?)) - } - CompositeNodeFilter::Type(i) => { - Ok(Arc::new(NodeTypeFilter(i).filter_graph_view(graph)?)) - } - 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 { From e5ff97d08447ffb9dabff460b3da4173fd8110ea Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:21:04 +0100 Subject: [PATCH 15/22] make things --- raphtory/src/db/api/state/ops/mod.rs | 1 + raphtory/src/db/api/view/filter_ops.rs | 15 +- .../db/graph/views/filter/model/and_filter.rs | 33 +--- .../graph/views/filter/model/edge_filter.rs | 136 +------------ .../filter/model/exploded_edge_filter.rs | 139 +------------ .../graph/views/filter/model/graph_filter.rs | 6 - .../filter/model/is_active_edge_filter.rs | 6 - .../filter/model/is_active_node_filter.rs | 9 +- .../views/filter/model/is_deleted_filter.rs | 6 - .../views/filter/model/is_self_loop_filter.rs | 6 - .../views/filter/model/is_valid_filter.rs | 6 - .../graph/views/filter/model/latest_filter.rs | 105 +--------- .../views/filter/model/layered_filter.rs | 107 +--------- .../src/db/graph/views/filter/model/mod.rs | 94 +++++++-- .../db/graph/views/filter/model/node_expr.rs | 179 ++++++++--------- .../views/filter/model/node_filter/mod.rs | 32 ++- .../db/graph/views/filter/model/not_filter.rs | 20 +- .../db/graph/views/filter/model/or_filter.rs | 34 +--- .../filter/model/property_filter/builders.rs | 22 ++- .../views/filter/model/property_filter/mod.rs | 21 -- .../views/filter/model/property_filter/ops.rs | 5 +- .../views/filter/model/snapshot_filter.rs | 183 +----------------- .../views/filter/model/windowed_filter.rs | 112 ++--------- 23 files changed, 265 insertions(+), 1012 deletions(-) diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 5c045633d3..0e5a51960d 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -54,6 +54,7 @@ pub trait NodeOp: Send + Sync { rhs: Arc>, ) -> Arc> where + Self: Clone + 'static, Self::Output: Comparable, { Arc::new(BinOpNodeOp { diff --git a/raphtory/src/db/api/view/filter_ops.rs b/raphtory/src/db/api/view/filter_ops.rs index 76bb5acb4a..781d657bca 100644 --- a/raphtory/src/db/api/view/filter_ops.rs +++ b/raphtory/src/db/api/view/filter_ops.rs @@ -11,11 +11,10 @@ pub trait Filter<'graph>: InternalFilter<'graph> { &self, filter: F, ) -> Result< - Self::Filtered>>, + Self::Filtered>, GraphError, > { - let fg = filter.filter_graph_view(self.base_graph().clone())?; - Ok(self.apply_filter(filter.create_filter(fg)?)) + Ok(self.apply_filter(filter.create_filter(self.base_graph().clone())?)) } } @@ -24,11 +23,10 @@ pub trait NodeSelect<'graph>: InternalNodeSelect<'graph> { &self, filter: F, ) -> Result< - Self::IterFiltered>>, + Self::IterFiltered>, GraphError, > { - let fg = filter.filter_graph_view(self.iter_graph().clone())?; - Ok(self.apply_iter_filter(filter.create_node_filter(fg)?)) + Ok(self.apply_iter_filter(filter.create_node_filter(self.iter_graph().clone())?)) } } @@ -37,11 +35,10 @@ pub trait EdgeSelect<'graph>: InternalEdgeSelect<'graph> { &self, filter: F, ) -> Result< - Self::IterFiltered>>, + Self::IterFiltered>, GraphError, > { - let fg = filter.filter_graph_view(self.iter_graph().clone())?; - Ok(self.apply_iter_filter(filter.create_filter(fg)?)) + Ok(self.apply_iter_filter(filter.create_filter(self.iter_graph().clone())?)) } } 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 efb64f8695..b4a1152532 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -17,17 +17,11 @@ use crate::{ node_filter::{ builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, }, - property_filter::{ - builders::{ - MetadataFilterBuilder, PropertyExprBuilderInput, PropertyFilterBuilder, - }, - Op, PropertyFilter, PropertyFilterInput, PropertyRef, - }, + 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, }, @@ -76,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; @@ -153,17 +129,17 @@ impl EdgeEndpointWrapper { impl EdgeEndpointWrapper { #[inline] pub fn id(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::id(), self.endpoint) + EdgeEndpointWrapper::new(Id, self.endpoint) } #[inline] pub fn name(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::name(), self.endpoint) + EdgeEndpointWrapper::new(Name, self.endpoint) } #[inline] pub fn node_type(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::node_type(), self.endpoint) + EdgeEndpointWrapper::new(Type, self.endpoint) } } @@ -187,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> @@ -250,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>; @@ -269,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 { @@ -425,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 7fa21be41b..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 @@ -17,17 +17,11 @@ use crate::{ node_filter::{ builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, }, - property_filter::{ - builders::{ - MetadataFilterBuilder, PropertyExprBuilderInput, PropertyFilterBuilder, - }, - Op, PropertyFilter, PropertyFilterInput, PropertyRef, - }, + 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, }, @@ -75,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; @@ -159,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> @@ -228,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>; @@ -254,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 @@ -411,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/graph_filter.rs b/raphtory/src/db/graph/views/filter/model/graph_filter.rs index 417fd1baa8..121e49e7b7 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -65,12 +65,6 @@ impl CreateFilter for GraphFilter { 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..15b099a9a4 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 @@ -59,12 +59,6 @@ impl CreateFilter for IsActiveEdge { 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 415e048c58..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 @@ -26,7 +26,7 @@ impl fmt::Display for IsActiveNode { } } -impl CreateFilter for IsActiveNode { +impl CreateFilter for IsActiveNode { type EntityFiltered<'graph, G> = NodeFilteredGraph> where @@ -34,7 +34,7 @@ impl CreateFilter for IsActiveNode { G: GraphViewOps<'graph>; type NodeFilter<'graph, G> - = Map, bool> + = Map>, bool> where Self: 'graph, G: GraphView + 'graph; @@ -49,7 +49,6 @@ impl CreateFilter for IsActiveNode { self, graph: G, ) -> Result, GraphError> { - let graph = self.view_expr.create_view(graph.clone())?; let op = self.create_node_filter(graph.clone())?; Ok(NodeFilteredGraph::new(graph, op)) } @@ -58,9 +57,7 @@ impl CreateFilter for IsActiveNode { self, graph: G, ) -> Result, GraphError> { - let op: Map, bool> = - HistoryOp::new(self.view_expr.create_view(graph)?).map(|h| !h.is_empty()); - Ok(op) + Ok(HistoryOp::new(self.view_expr.create_view(graph)?).map(|h| !h.is_empty())) } } 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..b9ff655e27 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 @@ -59,12 +59,6 @@ impl CreateFilter for IsDeletedEdge { 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..97783efef2 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 @@ -59,12 +59,6 @@ impl CreateFilter for IsSelfLoopEdge { 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..64d54e72e5 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 @@ -59,12 +59,6 @@ impl CreateFilter for IsValidEdge { 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 1187fb65cb..a8b34e4ba8 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -5,18 +5,10 @@ use crate::{ 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, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, InternalPropertyFilterBuilder, - InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, - PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -55,40 +47,6 @@ impl InternalViewWrapOps for Latest { } } -impl InternalNodeFilterBuilder for Latest { - type FilterType = T::FilterType; - - 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( @@ -123,7 +81,7 @@ impl CreateFilter for Latest G: GraphView + TimeOps<'graph> + Clone + 'graph; type FilteredGraph<'graph, G> - = WindowedGraph> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -147,13 +105,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 {} @@ -164,51 +115,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 85e1ac32a0..f87cb2a11a 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -5,17 +5,9 @@ use crate::{ 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, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, InternalPropertyFilterBuilder, - InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, - PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -67,40 +59,6 @@ impl InternalViewWrapOps for Layered { } } -impl InternalNodeFilterBuilder for Layered { - type FilterType = T::FilterType; - - 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()?; @@ -135,7 +93,7 @@ impl CreateFilter for Layered - = LayeredGraph> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -159,15 +117,6 @@ impl CreateFilter for Layered( - &self, - graph: G, - ) -> Result, GraphError> { - self.inner - .filter_graph_view(graph)? - .layers(self.layer.clone()) - } } impl ComposableFilter for Layered {} @@ -179,51 +128,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 79b062649c..309221c8a6 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -250,7 +250,7 @@ pub struct PropertyExpr { name: String, } -impl NodeExpr for PropertyExpr { +impl NodeExpr for PropertyExpr { type Output = Option; fn create_node_op<'g, G: GraphView + 'g>( @@ -272,7 +272,7 @@ pub struct MetadataExpr { name: String, } -impl NodeExpr for MetadataExpr { +impl NodeExpr for MetadataExpr { type Output = Option; fn create_node_op<'g, G: GraphView + 'g>( @@ -298,14 +298,14 @@ impl PropertyFilterFactory for T { fn property(&self, name: impl Into) -> PropertyExpr { PropertyExpr { view_expr: self.clone(), - name, + name: name.into(), } } fn metadata(&self, name: impl Into) -> MetadataExpr { MetadataExpr { view_expr: self.clone(), - name, + name: name.into(), } } } @@ -314,6 +314,15 @@ pub trait DynPropertyFilterFactory { fn property(&self, name: String) -> PropertyExpr>; } +impl DynPropertyFilterFactory for T { + fn property(&self, name: String) -> PropertyExpr> { + PropertyExpr { + view_expr: Arc::new(self.clone()) as Arc, + name, + } + } +} + pub struct TemporalPropertyExpr { view_expr: E, name: String, @@ -507,7 +516,7 @@ impl CreateView for Layered { &self, view: G, ) -> Result, GraphError> { - view.layers(self.layer) + view.layers(self.layer.clone()) } } @@ -515,9 +524,9 @@ pub trait ViewWrapPropOps: InternalViewWrapOps + PropertyFilterFactory + Sized { impl ViewWrapPropOps for T where T: InternalViewWrapOps + PropertyFilterFactory + Sized {} -pub trait DynInternalViewWrapPropOps: DynInternalViewWrapOps + DynPropertyFilterFactory {} +pub trait DynInternalViewWrapPropOps: DynInternalViewWrapOps + DynPropertyFilterFactory + DynCreateView {} -impl DynInternalViewWrapPropOps for T where T: DynInternalViewWrapOps + DynPropertyFilterFactory {} +impl DynInternalViewWrapPropOps for T where T: DynInternalViewWrapOps + DynPropertyFilterFactory + DynCreateView {} impl InternalViewWrapOps for Arc { type Window = Arc; @@ -531,6 +540,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 {} @@ -557,14 +577,14 @@ 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()) } @@ -582,7 +602,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; @@ -592,7 +612,7 @@ 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()) } @@ -612,6 +632,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; @@ -627,13 +658,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 DynNodeViewFilterOps for Windowed { + fn dyn_is_active(&self) -> Arc { + Arc::new(IsActiveNode { + view_expr: self.clone(), + }) + } +} + pub type DynEdgeViewProps = Arc; +impl CreateView for DynEdgeViewProps { + 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 DynEdgeViewProps { type Window = DynEdgeViewProps; @@ -665,3 +715,21 @@ impl EdgeViewFilterOps for DynEdgeViewProps { self.deref().dyn_is_self_loop() } } + +impl DynEdgeViewFilterOps for Windowed { + fn dyn_is_active(&self) -> Arc { + Arc::new(Windowed::new(self.start, self.end, IsActiveEdge)) + } + + fn dyn_is_valid(&self) -> Arc { + Arc::new(Windowed::new(self.start, self.end, IsValidEdge)) + } + + fn dyn_is_deleted(&self) -> Arc { + Arc::new(Windowed::new(self.start, self.end, IsDeletedEdge)) + } + + 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.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs index 07ef647b57..adb236ec37 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr.rs @@ -12,6 +12,7 @@ use crate::{ model::{ edge_filter::CompositeEdgeFilter, filter_operator::{BinaryOp, Comparable, SetOp, UnaryOp}, + node_filter::NodeFilter, property_filter::{evaluate::aggregate_values, Op}, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, CreateView, InternalViewWrapOps, TryAsCompositeFilter, Wrap, @@ -109,7 +110,7 @@ pub struct DegreeExpr { pub view_expr: E, } -impl NodeExpr for DegreeExpr { +impl NodeExpr for DegreeExpr { type Output = usize; fn create_node_op<'g, G: GraphView + 'g>( @@ -118,7 +119,7 @@ impl NodeExpr for DegreeExpr { ) -> Result + 'g>, GraphError> { Ok(Arc::new(Degree { dir: self.dir, - view: self.view_expr.create_view(graph), + view: self.view_expr.create_view(graph)?, })) } } @@ -548,7 +549,7 @@ where ) -> Result, GraphError> { let left = self.left.create_node_op(graph.clone())?; let right = self.right.create_node_op(graph)?; - Ok(left.bin_cmp(self.op, right)) + Ok(Arc::new(BinOpNodeOp { left, right, op: self.op })) } } @@ -950,18 +951,22 @@ impl NodeOp for TemporalNodePropOp { // ───────────────────────────────────────────────────────────────────────────── /// All temporal values of a named property over the current view window. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TemporalPropertyExpr { +#[derive(Clone)] +pub struct TemporalPropertyExpr { + pub view_expr: E, pub name: String, } -impl TemporalPropertyExpr { +impl TemporalPropertyExpr { pub fn new(name: impl Into) -> Self { - Self { name: name.into() } + Self { + view_expr: NodeFilter, + name: name.into(), + } } } -impl NodeExpr for TemporalPropertyExpr { +impl NodeExpr for TemporalPropertyExpr { type Output = Vec; fn create_node_op<'g, G: GraphView + 'g>( @@ -972,6 +977,7 @@ impl NodeExpr for TemporalPropertyExpr { .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 })) } } @@ -1274,22 +1280,18 @@ where /// Builder returned from `.any()` / `.all()` on a temporal expression. /// -/// Carries the wrapper context `W` (identity for `NodeFilter`, `Windowed` for windowed filters). -/// Call `.eq(rhs)`, `.gt(rhs)` etc. to produce the final filter wrapped in `W`. -pub struct QuantifiedContextBuilder +/// Call `.eq(rhs)`, `.gt(rhs)` etc. to produce the final `QuantifiedNodeFilter`. +pub struct QuantifiedContextBuilder where - W: Wrap + Clone, E: NodeExpr>, Q: QuantifierMode, { - pub(crate) wrap_ctx: W, pub(crate) expr: E, pub(crate) _q: PhantomData, } -impl QuantifiedContextBuilder +impl QuantifiedContextBuilder where - W: Wrap + Clone, E: NodeExpr>, Q: QuantifierMode, { @@ -1297,101 +1299,90 @@ where self, op: BinaryOp, rhs: R, - ) -> W::Wrapped> { - self.wrap_ctx - .wrap(QuantifiedNodeFilter::new(self.expr, op, rhs)) + ) -> QuantifiedNodeFilter { + QuantifiedNodeFilter::new(self.expr, op, rhs) } pub fn eq>>( self, rhs: R, - ) -> W::Wrapped> { + ) -> QuantifiedNodeFilter { self.finish(BinaryOp::Eq, rhs) } pub fn ne>>( self, rhs: R, - ) -> W::Wrapped> { + ) -> QuantifiedNodeFilter { self.finish(BinaryOp::Ne, rhs) } pub fn gt>>( self, rhs: R, - ) -> W::Wrapped> { + ) -> QuantifiedNodeFilter { self.finish(BinaryOp::Gt, rhs) } pub fn ge>>( self, rhs: R, - ) -> W::Wrapped> { + ) -> QuantifiedNodeFilter { self.finish(BinaryOp::Ge, rhs) } pub fn lt>>( self, rhs: R, - ) -> W::Wrapped> { + ) -> QuantifiedNodeFilter { self.finish(BinaryOp::Lt, rhs) } pub fn le>>( self, rhs: R, - ) -> W::Wrapped> { + ) -> QuantifiedNodeFilter { self.finish(BinaryOp::Le, rhs) } } /// Builder returned from aggregators (`.sum()`, `.avg()` etc.) on a temporal expression. /// -/// Carries the wrapper context `W` and the aggregator expression `E`. -/// Call `.eq(rhs)`, `.gt(rhs)` etc. to produce the final filter wrapped in `W`. -pub struct NodeExprContextBuilder -where - W: Wrap + Clone, - E: NodeExpr, -{ - pub(crate) wrap_ctx: W, +/// Call `.eq(rhs)`, `.gt(rhs)` etc. to produce the final `BinOpNodeFilter`. +pub struct NodeExprContextBuilder { pub(crate) expr: E, } -impl NodeExprContextBuilder -where - W: Wrap + Clone, - E: NodeExpr, -{ +impl NodeExprContextBuilder { fn finish>( self, op: BinaryOp, rhs: R, - ) -> W::Wrapped> { - self.wrap_ctx.wrap(BinOpNodeFilter::new(self.expr, op, rhs)) + ) -> BinOpNodeFilter { + BinOpNodeFilter::new(self.expr, op, rhs) } - pub fn eq>(self, rhs: R) -> W::Wrapped> { + pub fn eq>(self, rhs: R) -> BinOpNodeFilter { self.finish(BinaryOp::Eq, rhs) } - pub fn ne>(self, rhs: R) -> W::Wrapped> { + pub fn ne>(self, rhs: R) -> BinOpNodeFilter { self.finish(BinaryOp::Ne, rhs) } - pub fn gt>(self, rhs: R) -> W::Wrapped> { + pub fn gt>(self, rhs: R) -> BinOpNodeFilter { self.finish(BinaryOp::Gt, rhs) } - pub fn ge>(self, rhs: R) -> W::Wrapped> { + pub fn ge>(self, rhs: R) -> BinOpNodeFilter { self.finish(BinaryOp::Ge, rhs) } - pub fn lt>(self, rhs: R) -> W::Wrapped> { + pub fn lt>(self, rhs: R) -> BinOpNodeFilter { self.finish(BinaryOp::Lt, rhs) } - pub fn le>(self, rhs: R) -> W::Wrapped> { + pub fn le>(self, rhs: R) -> BinOpNodeFilter { self.finish(BinaryOp::Le, rhs) } } @@ -1402,8 +1393,8 @@ where /// Builder returned from `.temporal_property(name)`. /// -/// `W` carries the wrapping context so that windowed temporal filters are correctly -/// produced when called on a `Windowed`. +/// `E` is the view expression (e.g. `NodeFilter`, `Windowed`, `Layered`) +/// that scopes which temporal property values are visible. /// /// Usage: /// ```rust,ignore @@ -1411,81 +1402,79 @@ where /// NodeFilter.window(0, 100).temporal_property("score").any().gt(10i64) /// NodeFilter::temporal_property("price").sum().gt(100i64) /// ``` -pub struct TemporalPropContext { - wrap_ctx: W, - expr: TemporalPropertyExpr, +pub struct TemporalPropContext { + view_expr: E, + name: String, } -impl TemporalPropContext { - pub(crate) fn new(wrap_ctx: W, name: impl Into) -> Self { +impl TemporalPropContext { + pub(crate) fn new(view_expr: E, name: impl Into) -> Self { Self { - wrap_ctx, - expr: TemporalPropertyExpr::new(name), + view_expr, + name: name.into(), } } - pub fn any(self) -> QuantifiedContextBuilder { + fn make_expr(self) -> TemporalPropertyExpr { + TemporalPropertyExpr { + view_expr: self.view_expr, + name: self.name, + } + } + + pub fn any(self) -> QuantifiedContextBuilder, AnyMode> { QuantifiedContextBuilder { - wrap_ctx: self.wrap_ctx, - expr: self.expr, + expr: self.make_expr(), _q: PhantomData, } } - pub fn all(self) -> QuantifiedContextBuilder { + pub fn all(self) -> QuantifiedContextBuilder, AllMode> { QuantifiedContextBuilder { - wrap_ctx: self.wrap_ctx, - expr: self.expr, + expr: self.make_expr(), _q: PhantomData, } } - pub fn sum(self) -> NodeExprContextBuilder> { + pub fn sum(self) -> NodeExprContextBuilder>> { NodeExprContextBuilder { - wrap_ctx: self.wrap_ctx, - expr: SumExpr(self.expr), + expr: SumExpr(self.make_expr()), } } - pub fn avg(self) -> NodeExprContextBuilder> { + pub fn avg(self) -> NodeExprContextBuilder>> { NodeExprContextBuilder { - wrap_ctx: self.wrap_ctx, - expr: AvgExpr(self.expr), + expr: AvgExpr(self.make_expr()), } } - pub fn min(self) -> NodeExprContextBuilder> { + pub fn min(self) -> NodeExprContextBuilder>> { NodeExprContextBuilder { - wrap_ctx: self.wrap_ctx, - expr: MinExpr(self.expr), + expr: MinExpr(self.make_expr()), } } - pub fn max(self) -> NodeExprContextBuilder> { + pub fn max(self) -> NodeExprContextBuilder>> { NodeExprContextBuilder { - wrap_ctx: self.wrap_ctx, - expr: MaxExpr(self.expr), + expr: MaxExpr(self.make_expr()), } } - pub fn first(self) -> NodeExprContextBuilder> { + pub fn first(self) -> NodeExprContextBuilder>> { NodeExprContextBuilder { - wrap_ctx: self.wrap_ctx, - expr: FirstExpr(self.expr), + expr: FirstExpr(self.make_expr()), } } - pub fn last(self) -> NodeExprContextBuilder> { + pub fn last(self) -> NodeExprContextBuilder>> { NodeExprContextBuilder { - wrap_ctx: self.wrap_ctx, - expr: LastExpr(self.expr), + expr: LastExpr(self.make_expr()), } } - pub fn len(self) -> NodeExprContextBuilder> { + pub fn len(self) -> NodeExprContextBuilder>> { NodeExprContextBuilder { - wrap_ctx: self.wrap_ctx, - expr: LenExpr(self.expr), + expr: LenExpr(self.make_expr()), } } } @@ -1498,17 +1487,15 @@ impl TemporalPropContext { /// /// Available on any `NodeExpr>` (e.g. `TemporalPropertyExpr`). pub trait TemporalExprOps: NodeExpr> + Sized { - fn any(self) -> QuantifiedContextBuilder { + fn any(self) -> QuantifiedContextBuilder { QuantifiedContextBuilder { - wrap_ctx: NoWrap, expr: self, _q: PhantomData, } } - fn all(self) -> QuantifiedContextBuilder { + fn all(self) -> QuantifiedContextBuilder { QuantifiedContextBuilder { - wrap_ctx: NoWrap, expr: self, _q: PhantomData, } @@ -1882,17 +1869,14 @@ mod tests { // ── Windowed temporal filter ────────────────────────────────────────────── - /// Apply a filter using the full two-step pipeline (filter_graph_view → create_filter). - /// Required for windowed filters where filter_graph_view applies the window. + /// Apply a windowed temporal filter directly (view is embedded in the expression). fn windowed_filtered_names(filter: F, g: Graph) -> Vec where - F: CreateFilter + Clone, - for<'graph> F::EntityFiltered<'graph, F::FilteredGraph<'graph, Graph>>: - GraphViewOps<'graph>, + F: CreateFilter, + for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, { - let fg = filter.filter_graph_view(g).unwrap(); let mut names: Vec = filter - .create_filter(fg) + .create_filter(g) .unwrap() .nodes() .iter() @@ -1988,17 +1972,14 @@ mod tests { g } - /// Run the full filter_graph_view → create_filter pipeline for a layered filter. - /// Identical in structure to `windowed_filtered_names`; factored separately for clarity. + /// Apply a layered temporal filter directly (view is embedded in the expression). fn layered_filtered_names(filter: F, g: Graph) -> Vec where - F: CreateFilter + Clone, - for<'graph> F::EntityFiltered<'graph, F::FilteredGraph<'graph, Graph>>: - GraphViewOps<'graph>, + F: CreateFilter, + for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, { - let fg = filter.filter_graph_view(g).unwrap(); let mut names: Vec = filter - .create_filter(fg) + .create_filter(g) .unwrap() .nodes() .iter() 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 7cf5ecc81b..a07ac8336c 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 @@ -27,8 +27,8 @@ use crate::{ snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, AndFilter, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - EntityMarker, InternalViewWrapOps, NodeViewFilterOps, NotFilter, OrFilter, - PropertyFilterFactory, TryAsCompositeFilter, Wrap, + CreateView, EntityMarker, InternalViewWrapOps, NodeViewFilterOps, NotFilter, + OrFilter, PropertyFilterFactory, TryAsCompositeFilter, Wrap, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -53,7 +53,7 @@ impl From for EntityMarker { } } -pub trait NodeFilterFactory: PropertyFilterFactory { +pub trait NodeFilterFactory: PropertyFilterFactory + Clone { #[inline] fn id(&self) -> Id { Id @@ -123,6 +123,22 @@ pub trait NodeFilterFactory: PropertyFilterFactory { 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) + } +} + impl NodeFilter { /// Current (latest) value of a named property — serializable. #[inline] @@ -147,17 +163,17 @@ impl NodeFilter { } } -/// Extension trait that adds `.temporal_property(name)` to any wrapper type. +/// Extension trait that adds `.temporal_property(name)` to any view expression type. /// -/// Implemented for all `W: Wrap + Clone` so that `NodeFilter`, `Windowed`, -/// `Latest`, etc. all support the same entry point. -pub trait TemporalNodeExprBuilderOps: Wrap + Clone + Sized { +/// Implemented for all `T: CreateView + Clone` so that `NodeFilter`, `Windowed`, +/// `Layered`, etc. all support the same entry point. +pub trait TemporalNodeExprBuilderOps: CreateView + Clone + Send + Sync + Sized + 'static { fn temporal_property(self, name: impl Into) -> TemporalPropContext { TemporalPropContext::new(self, name) } } -impl TemporalNodeExprBuilderOps for T {} +impl TemporalNodeExprBuilderOps for T {} impl Wrap for NodeFilter { type Wrapped = T; 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 f00f82d6ad..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 @@ -406,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 { @@ -440,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 { @@ -472,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..cb1c79ec1a 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,17 @@ use crate::{ db::{ - api::view::{internal::GraphView, time::TimeOps}, + api::view::internal::GraphView, 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, + ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, - window_graph::WindowedGraph, }, }, errors::GraphError, @@ -58,32 +50,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 +89,7 @@ impl CreateFilter for SnapshotA G: GraphView + 'graph; type FilteredGraph<'graph, G> - = WindowedGraph> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -147,13 +113,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 +127,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 +153,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 +185,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 +211,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 +221,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 89a02dcf0e..f5cdd71ac9 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -5,17 +5,9 @@ use crate::{ 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, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, InternalPropertyFilterBuilder, - InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, - PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, CreateView, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -80,40 +72,6 @@ impl InternalViewWrapOps for Windowed { } } -impl InternalNodeFilterBuilder for Windowed { - type FilterType = T::FilterType; - - 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()?; @@ -148,7 +106,7 @@ impl CreateFilter for Windowed< G: GraphView + 'graph; type FilteredGraph<'graph, G> - = WindowedGraph> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -172,16 +130,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 {} @@ -194,50 +142,14 @@ impl Wrap for Windowed { } } -impl InternalPropertyFilterFactory for Windowed { - type Entity = T::Entity; - type PropertyBuilder = Windowed; - type MetadataBuilder = Windowed; +impl CreateView for Windowed { + type View<'graph, G: GraphView + 'graph> = WindowedGraph>; - 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 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())) } } From fd45acf76f437a5edbac6c6caf342f3f65a44424 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 9 Jun 2026 15:31:50 +0200 Subject: [PATCH 16/22] start introducing prop_type for handling validation --- raphtory/src/db/api/state/ops/mod.rs | 4 + .../src/db/graph/views/filter/model/mod.rs | 43 ++++-- .../db/graph/views/filter/model/node_expr.rs | 139 ++++++++++-------- 3 files changed, 105 insertions(+), 81 deletions(-) diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 0e5a51960d..863ad567af 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -17,11 +17,15 @@ use raphtory_api::core::entities::VID; use raphtory_storage::graph::graph::GraphStorage; use serde::{Deserialize, Serialize}; use std::{fmt::Debug, marker::PhantomData, ops::Deref, sync::Arc}; +use raphtory_api::core::entities::properties::prop::PropType; // 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 + fn prop_type(&self) -> PropType; + /// The domain of validity for this node op fn domain(&self, _storage: &GraphStorage) -> NodeList { NodeList::All diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 309221c8a6..bb8af01708 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -34,6 +34,7 @@ pub use crate::{ use crate::{ db::{ api::{ + properties::TemporalPropertyView, state::{ ops::{filter::NO_FILTER, Const}, NodeOp, @@ -50,7 +51,8 @@ use crate::{ is_valid_filter::IsValidEdge, latest_filter::Latest, layered_filter::Layered, - node_expr::{NodeMetaOp, NodePropOp}, + node_expr::{NodeMetaOp, NodePropOp, TemporalPropertyExpr}, + node_filter::NodeFilterFactory, property_filter::{ builders::PropertyExprBuilderInput, Op, PropertyFilterInput, PropertyRef, }, @@ -250,7 +252,9 @@ pub struct PropertyExpr { name: String, } -impl NodeExpr for PropertyExpr { +impl NodeExpr + for PropertyExpr +{ type Output = Option; fn create_node_op<'g, G: GraphView + 'g>( @@ -272,7 +276,9 @@ pub struct MetadataExpr { name: String, } -impl NodeExpr for MetadataExpr { +impl NodeExpr + for MetadataExpr +{ type Output = Option; fn create_node_op<'g, G: GraphView + 'g>( @@ -314,7 +320,7 @@ pub trait DynPropertyFilterFactory { fn property(&self, name: String) -> PropertyExpr>; } -impl DynPropertyFilterFactory for T { +impl DynPropertyFilterFactory for T { fn property(&self, name: String) -> PropertyExpr> { PropertyExpr { view_expr: Arc::new(self.clone()) as Arc, @@ -323,13 +329,8 @@ impl DynPropertyFilterFactory for } } -pub struct TemporalPropertyExpr { - view_expr: E, - name: String, -} - -impl PropertyExpr { - pub fn temporal(&self) -> TemporalPropertyExpr { +impl PropertyExpr { + pub fn temporal(&self) -> TemporalPropertyExpr { TemporalPropertyExpr { view_expr: self.view_expr.clone(), name: self.name.clone(), @@ -463,7 +464,7 @@ pub trait ViewWrapOps: InternalViewWrapOps + Sized { impl ViewWrapOps for T {} -pub trait CreateView: Clone { +pub trait CreateView: Clone + Send + Sync + 'static { type View<'graph, G: GraphView + 'graph>: GraphView + 'graph; fn create_view<'graph, G: GraphView + 'graph>( &self, @@ -524,9 +525,15 @@ pub trait ViewWrapPropOps: InternalViewWrapOps + PropertyFilterFactory + Sized { impl ViewWrapPropOps for T where T: InternalViewWrapOps + PropertyFilterFactory + Sized {} -pub trait DynInternalViewWrapPropOps: DynInternalViewWrapOps + DynPropertyFilterFactory + DynCreateView {} +pub trait DynInternalViewWrapPropOps: + DynInternalViewWrapOps + DynPropertyFilterFactory + DynCreateView +{ +} -impl DynInternalViewWrapPropOps for T where T: DynInternalViewWrapOps + DynPropertyFilterFactory + DynCreateView {} +impl DynInternalViewWrapPropOps for T where + T: DynInternalViewWrapOps + DynPropertyFilterFactory + DynCreateView +{ +} impl InternalViewWrapOps for Arc { type Window = Arc; @@ -584,7 +591,9 @@ pub trait DynNodeViewFilterOps: DynInternalViewWrapPropOps + TryAsCompositeFilte 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()) } @@ -612,7 +621,9 @@ pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps + TryAsCompositeFilte 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()) } diff --git a/raphtory/src/db/graph/views/filter/model/node_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs index adb236ec37..8e406ebc11 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr.rs @@ -24,12 +24,17 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::core::{ - entities::{properties::prop::Prop, GID, VID}, + entities::{ + properties::prop::{Prop, PropType}, + GID, VID, + }, storage::{arc_str::ArcStr, timeindex::EventTime}, Direction, }; use raphtory_storage::graph::graph::GraphStorage; use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; +use raphtory_api::core::entities::GidType; +use raphtory_storage::core_ops::CoreGraphOps; // ───────────────────────────────────────────────────────────────────────────── // NodeExpr — typed node expression with associated Output type // ───────────────────────────────────────────────────────────────────────────── @@ -52,7 +57,7 @@ use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; /// ``` /// pub trait NodeExpr: Clone + Send + Sync + 'static { - type Output: Clone + Send + Sync + 'static; + type Output: Clone + Send + Sync + Into + 'static; /// Compile the expression against a specific graph view. /// @@ -61,6 +66,11 @@ pub trait NodeExpr: Clone + Send + Sync + 'static { &self, graph: G, ) -> Result + 'g>, GraphError>; + + /// A priory known type (for early validation where possible) + fn prop_type(&self) -> PropType { + PropType::Empty + } } // ───────────────────────────────────────────────────────────────────────────── @@ -80,6 +90,10 @@ impl NodeOp for NodePropOp { 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() + } } /// Evaluates a metadata (static) field by pre-resolved column ID. @@ -95,6 +109,10 @@ impl NodeOp for NodeMetaOp { 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() + } } // ───────────────────────────────────────────────────────────────────────────── @@ -194,6 +212,10 @@ impl NodeExpr for Type { ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Type)) } + + fn prop_type(&self) -> PropType { + PropType::Str + } } /// `Name` from `db/api/state/ops/node.rs` used as a node expression. @@ -206,6 +228,10 @@ impl NodeExpr for Name { ) -> Result + 'g>, GraphError> { Ok(Arc::new(Name)) } + + fn prop_type(&self) -> PropType { + PropType::Str + } } /// `Id` from `db/api/state/ops/node.rs` used as a node expression. @@ -238,6 +264,10 @@ impl NodeExpr for usize { ) -> Result + 'g>, GraphError> { Ok(Arc::new(Const(*self))) } + + fn prop_type(&self) -> PropType { + PropType::U64 + } } impl NodeExpr for String { @@ -249,6 +279,10 @@ impl NodeExpr for String { ) -> Result + 'g>, GraphError> { Ok(Arc::new(Const(self.clone()))) } + + fn prop_type(&self) -> PropType { + PropType::Str + } } impl NodeExpr for ArcStr { @@ -260,6 +294,10 @@ impl NodeExpr for ArcStr { ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Const(Some(self.clone())))) } + + fn prop_type(&self) -> PropType { + PropType::Str + } } impl NodeExpr for &'static str { @@ -271,6 +309,10 @@ impl NodeExpr for &'static str { ) -> Result + 'g>, GraphError> { Ok(Arc::new(Const(*self))) } + + fn prop_type(&self) -> PropType { + PropType::Str + } } impl NodeExpr for Prop { @@ -282,6 +324,10 @@ impl NodeExpr for Prop { ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Const(Some(self.clone())))) } + + fn prop_type(&self) -> PropType { + self.dtype() + } } impl NodeExpr for GID { @@ -310,43 +356,6 @@ macro_rules! impl_node_expr_for_numeric { }; } -#[derive(Debug, Clone, Copy)] -struct AsProp(E); - -#[derive(Debug, Clone, Copy)] -struct AsPropOp(Op); - -impl>> NodeOp for AsPropOp { - type Output = Prop; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { - self.0.apply(storage, node).into() - } - - fn domain(&self, storage: &GraphStorage) -> NodeList { - self.0.domain(storage) - } - - fn const_value_in_domain(&self) -> Option { - self.0.const_value_in_domain().map(|v| v.into()) - } - - fn const_value(&self) -> Option { - self.0.const_value().map(|v| v.into()) - } -} - -impl>> NodeExpr for AsProp { - type Output = Prop; - - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result + 'g>, GraphError> { - Ok(Arc::new(AsPropOp(self.0.create_node_op(graph)?))) - } -} - impl_node_expr_for_numeric!(i32, I32); impl_node_expr_for_numeric!(i64, I64); impl_node_expr_for_numeric!(u32, U32); @@ -399,6 +408,10 @@ impl<'g, T: Comparable + Clone + Send + Sync + 'static> NodeOp for BinOpNodeOp<' let rv = self.right.apply(storage, node); T::binary_cmp(&self.op, &lv, &rv) } + + fn prop_type(&self) -> PropType { + PropType::Bool + } } // ───────────────────────────────────────────────────────────────────────────── @@ -421,8 +434,18 @@ impl<'g, I: Clone + Send + Sync + 'static> NodeOp for UnaryNodeOp<'g, I> { UnaryOp::IsNone => v.is_none(), } } + + fn prop_type(&self) -> PropType { + PropType::Bool + } } +// graph.nodes.select(NodeFilter.property("bool_prop") || NodeFilter.degree() > 10) -> should work +// graph.nodes.select(NodeFilter.property("str_prop") || NodeFilter.degree() > 10) -> should fail when you construct the filter +// NodeFilter.degree() || ... should fail immediately when you try to construct the expression or ideally at compile-time + + + // ───────────────────────────────────────────────────────────────────────────── // SetNodeOp<'g, T> — evaluates is_in / is_not_in // ───────────────────────────────────────────────────────────────────────────── @@ -527,7 +550,7 @@ where type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; + type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -549,7 +572,11 @@ where ) -> Result, GraphError> { let left = self.left.create_node_op(graph.clone())?; let right = self.right.create_node_op(graph)?; - Ok(Arc::new(BinOpNodeOp { left, right, op: self.op })) + Ok(Arc::new(BinOpNodeOp { + left, + right, + op: self.op, + })) } } @@ -1303,45 +1330,27 @@ where QuantifiedNodeFilter::new(self.expr, op, rhs) } - pub fn eq>>( - self, - rhs: R, - ) -> QuantifiedNodeFilter { + pub fn eq>>(self, rhs: R) -> QuantifiedNodeFilter { self.finish(BinaryOp::Eq, rhs) } - pub fn ne>>( - self, - rhs: R, - ) -> QuantifiedNodeFilter { + pub fn ne>>(self, rhs: R) -> QuantifiedNodeFilter { self.finish(BinaryOp::Ne, rhs) } - pub fn gt>>( - self, - rhs: R, - ) -> QuantifiedNodeFilter { + pub fn gt>>(self, rhs: R) -> QuantifiedNodeFilter { self.finish(BinaryOp::Gt, rhs) } - pub fn ge>>( - self, - rhs: R, - ) -> QuantifiedNodeFilter { + pub fn ge>>(self, rhs: R) -> QuantifiedNodeFilter { self.finish(BinaryOp::Ge, rhs) } - pub fn lt>>( - self, - rhs: R, - ) -> QuantifiedNodeFilter { + pub fn lt>>(self, rhs: R) -> QuantifiedNodeFilter { self.finish(BinaryOp::Lt, rhs) } - pub fn le>>( - self, - rhs: R, - ) -> QuantifiedNodeFilter { + pub fn le>>(self, rhs: R) -> QuantifiedNodeFilter { self.finish(BinaryOp::Le, rhs) } } From fedf488031083dddd4169d66f1b11d66995b68f9 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:38:19 +0100 Subject: [PATCH 17/22] reorganise node_expr into exprs/ops/filters, drop NoWrap, document the full filter-building pipeline on each type --- raphtory/src/db/api/state/ops/mod.rs | 21 +- raphtory/src/db/api/view/filter_ops.rs | 15 +- .../views/filter/model/filter_operator.rs | 128 +- .../graph/views/filter/model/graph_filter.rs | 1 - .../filter/model/is_active_edge_filter.rs | 1 - .../views/filter/model/is_deleted_filter.rs | 1 - .../views/filter/model/is_self_loop_filter.rs | 1 - .../views/filter/model/is_valid_filter.rs | 1 - .../graph/views/filter/model/latest_filter.rs | 6 +- .../views/filter/model/layered_filter.rs | 5 +- .../src/db/graph/views/filter/model/mod.rs | 15 +- .../db/graph/views/filter/model/node_expr.rs | 2039 ----------------- .../views/filter/model/node_expr/exprs.rs | 483 ++++ .../views/filter/model/node_expr/filters.rs | 1138 +++++++++ .../graph/views/filter/model/node_expr/mod.rs | 55 + .../graph/views/filter/model/node_expr/ops.rs | 443 ++++ .../views/filter/model/node_expr/tests.rs | 518 +++++ .../views/filter/model/snapshot_filter.rs | 16 +- .../views/filter/model/windowed_filter.rs | 7 +- 19 files changed, 2768 insertions(+), 2126 deletions(-) delete mode 100644 raphtory/src/db/graph/views/filter/model/node_expr.rs create mode 100644 raphtory/src/db/graph/views/filter/model/node_expr/exprs.rs create mode 100644 raphtory/src/db/graph/views/filter/model/node_expr/filters.rs create mode 100644 raphtory/src/db/graph/views/filter/model/node_expr/mod.rs create mode 100644 raphtory/src/db/graph/views/filter/model/node_expr/ops.rs create mode 100644 raphtory/src/db/graph/views/filter/model/node_expr/tests.rs diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 863ad567af..361dbf2abb 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -8,23 +8,25 @@ use crate::db::{ state::ops::filter::{AndOp, NotOp, OrOp}, view::internal::NodeList, }, - graph::views::filter::model::{node_expr::BinOpNodeOp, BinaryOp, Comparable}, + 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}; -use raphtory_api::core::entities::properties::prop::PropType; // 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 - fn prop_type(&self) -> PropType; + /// 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 { @@ -50,8 +52,7 @@ pub trait NodeOp: Send + Sync { Map { op: self, map } } - - /// Override if binary comparison can be optimised + /// Override if binary comparison can be optimized fn bin_cmp( &self, op: BinaryOp, @@ -61,7 +62,7 @@ pub trait NodeOp: Send + Sync { Self: Clone + 'static, Self::Output: Comparable, { - Arc::new(BinOpNodeOp { + Arc::new(BinaryCmpNodeOp { left: Arc::new(self.clone()), right: rhs, op, @@ -194,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/view/filter_ops.rs b/raphtory/src/db/api/view/filter_ops.rs index 781d657bca..4bacd8d468 100644 --- a/raphtory/src/db/api/view/filter_ops.rs +++ b/raphtory/src/db/api/view/filter_ops.rs @@ -10,10 +10,7 @@ pub trait Filter<'graph>: InternalFilter<'graph> { fn filter( &self, filter: F, - ) -> Result< - Self::Filtered>, - GraphError, - > { + ) -> Result>, GraphError> { Ok(self.apply_filter(filter.create_filter(self.base_graph().clone())?)) } } @@ -22,10 +19,7 @@ pub trait NodeSelect<'graph>: InternalNodeSelect<'graph> { fn select( &self, filter: F, - ) -> Result< - Self::IterFiltered>, - GraphError, - > { + ) -> Result>, GraphError> { Ok(self.apply_iter_filter(filter.create_node_filter(self.iter_graph().clone())?)) } } @@ -34,10 +28,7 @@ pub trait EdgeSelect<'graph>: InternalEdgeSelect<'graph> { fn select( &self, filter: F, - ) -> Result< - Self::IterFiltered>, - GraphError, - > { + ) -> Result>, GraphError> { Ok(self.apply_iter_filter(filter.create_filter(self.iter_graph().clone())?)) } } 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 8b2a862664..0f32aace4c 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -9,7 +9,7 @@ use std::{collections::HashSet, fmt, fmt::Display, ops::Deref}; use strsim::levenshtein; // ───────────────────────────────────────────────────────────────────────────── -// Comparable — type-driven value comparison for BinOpNodeOp +// Comparable — type-driven ordering/equality comparison for BinaryCmpNodeOp // ───────────────────────────────────────────────────────────────────────────── pub trait Comparable: Clone + Send + Sync + 'static { @@ -25,7 +25,6 @@ impl Comparable for usize { BinaryOp::Le => left <= right, BinaryOp::Gt => left > right, BinaryOp::Ge => left >= right, - _ => false, } } } @@ -42,20 +41,6 @@ macro_rules! impl_comparable_str { BinaryOp::Le => l <= r, BinaryOp::Gt => l > r, BinaryOp::Ge => l >= r, - BinaryOp::StartsWith => l.starts_with(r), - BinaryOp::EndsWith => l.ends_with(r), - BinaryOp::Contains => l.contains(r), - BinaryOp::NotContains => !l.contains(r), - BinaryOp::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 - } } } } @@ -82,7 +67,6 @@ impl Comparable for Prop { .map(|o| o == Greater) .unwrap_or(false), BinaryOp::Ge => left.partial_cmp(right).map(|o| o != Less).unwrap_or(false), - _ => false, } } } @@ -97,7 +81,6 @@ impl Comparable for GID { BinaryOp::Le => l <= r, BinaryOp::Gt => l > r, BinaryOp::Ge => l >= r, - _ => false, }, (GID::Str(l), GID::Str(r)) => String::binary_cmp(op, l, r), _ => matches!(op, BinaryOp::Ne), @@ -115,11 +98,76 @@ impl Comparable for Option { } } +// ───────────────────────────────────────────────────────────────────────────── +// 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 // ───────────────────────────────────────────────────────────────────────────── -/// Binary comparison / string operators used by `BinOpNodeFilter`. +/// Ordering and equality operators used by `BinaryCmpNodeFilter`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BinaryOp { Eq, @@ -128,6 +176,24 @@ pub enum BinaryOp { 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, @@ -138,25 +204,17 @@ pub enum BinaryOp { }, } -impl Display for BinaryOp { +impl Display for StringOp { 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, ">="), - BinaryOp::StartsWith => write!(f, "STARTS_WITH"), - BinaryOp::EndsWith => write!(f, "ENDS_WITH"), - BinaryOp::Contains => write!(f, "CONTAINS"), - BinaryOp::NotContains => write!(f, "NOT_CONTAINS"), - BinaryOp::FuzzySearch { + 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) - } + } => write!(f, "FUZZY_SEARCH({},{})", levenshtein_distance, prefix_match), } } } @@ -492,7 +550,7 @@ impl FilterOperator { /// Compare two optional values symmetrically. /// - /// Used by `BinOpNodeFilter` where both sides are expressions that may return `None`. + /// 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 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 121e49e7b7..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,7 +64,6 @@ impl CreateFilter for GraphFilter { ) -> Result, GraphError> { Ok(NodeExistsOp::new(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 15b099a9a4..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,7 +58,6 @@ impl CreateFilter for IsActiveEdge { ) -> Result, GraphError> { Ok(NodeExistsOp::new(IsActiveGraph::new(graph))) } - } impl ComposableFilter for IsActiveEdge {} 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 b9ff655e27..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,7 +58,6 @@ impl CreateFilter for IsDeletedEdge { ) -> Result, GraphError> { Ok(NodeExistsOp::new(IsDeletedGraph::new(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 97783efef2..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,7 +58,6 @@ impl CreateFilter for IsSelfLoopEdge { ) -> Result, GraphError> { Ok(NodeExistsOp::new(IsSelfLoopGraph::new(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 64d54e72e5..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,7 +58,6 @@ impl CreateFilter for IsValidEdge { ) -> Result, GraphError> { Ok(NodeExistsOp::new(ValidGraph::new(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 a8b34e4ba8..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,10 +4,8 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - windowed_filter::Windowed, - ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, InternalViewWrapOps, + edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, 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 f87cb2a11a..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,9 +4,8 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, InternalViewWrapOps, + edge_filter::CompositeEdgeFilter, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index bb8af01708..68befed4c8 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -10,14 +10,17 @@ pub use crate::{ CompositeExplodedEdgeFilter, ExplodedEdgeEndpointWrapper, ExplodedEdgeFilter, }, - filter_operator::{BinaryOp, Comparable, FilterOperator, SetOp, UnaryOp}, + filter_operator::{ + BinaryOp, Comparable, FilterOperator, SetOp, StringComparable, StringOp, + UnaryOp, + }, node_expr::{ - AllMode, AnyMode, AvgExpr, BinOpNodeFilter, ConstExpr, DegreeExpr, - FirstExpr, LastExpr, LenExpr, MaxExpr, Metadata, MinExpr, NoWrap, NodeExpr, + AllMode, AnyMode, AvgExpr, BinaryCmpNodeFilter, ConstExpr, DegreeExpr, + FirstExpr, LastExpr, LenExpr, MaxExpr, Metadata, MinExpr, NodeExpr, NodeExprContextBuilder, NodeExprFilterOps, Property, QuantifiedContextBuilder, QuantifiedNodeFilter, QuantifierMode, - SetNodeFilter, SumExpr, TemporalExprOps, TemporalPropContext, - UnaryNodeFilter, + SetNodeFilter, StringNodeFilter, SumExpr, TemporalExprOps, + TemporalPropContext, UnaryNodeFilter, }, node_filter::NodeFilter, not_filter::NotFilter, @@ -472,7 +475,7 @@ pub trait CreateView: Clone + Send + Sync + 'static { ) -> Result, GraphError>; } -pub trait DynCreateView { +pub trait DynCreateView: Send + Sync { fn dyn_create_view<'graph>( &self, view: Arc, diff --git a/raphtory/src/db/graph/views/filter/model/node_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr.rs deleted file mode 100644 index 8e406ebc11..0000000000 --- a/raphtory/src/db/graph/views/filter/model/node_expr.rs +++ /dev/null @@ -1,2039 +0,0 @@ -use crate::{ - db::{ - api::{ - properties::PropertiesOps, - state::ops::{Const, Degree, Id, Name, NodeOp, Type}, - view::{ - internal::{GraphView, NodeList}, - NodeViewOps, - }, - }, - graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - filter_operator::{BinaryOp, Comparable, SetOp, UnaryOp}, - node_filter::NodeFilter, - property_filter::{evaluate::aggregate_values, Op}, - ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, - CreateView, InternalViewWrapOps, TryAsCompositeFilter, Wrap, - }, - node_filtered_graph::NodeFilteredGraph, - }, - }, - errors::GraphError, - prelude::GraphViewOps, -}; -use raphtory_api::core::{ - entities::{ - properties::prop::{Prop, PropType}, - GID, VID, - }, - storage::{arc_str::ArcStr, timeindex::EventTime}, - Direction, -}; -use raphtory_storage::graph::graph::GraphStorage; -use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; -use raphtory_api::core::entities::GidType; -use raphtory_storage::core_ops::CoreGraphOps; -// ───────────────────────────────────────────────────────────────────────────── -// 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 + Into + '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 - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// NodePropOp / NodeMetaOp — prop_id resolved at creation time -// ───────────────────────────────────────────────────────────────────────────── - -/// Evaluates a temporal property by pre-resolved column ID. -#[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() - } -} - -/// Evaluates a metadata (static) field by pre-resolved column ID. -#[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() - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// Concrete expression structs -// ───────────────────────────────────────────────────────────────────────────── - -/// Wraps a `Direction` so it can be used as a `NodeExpr` for degree filtering. -/// -/// Delegates to `Degree` from `db/api/state/ops/node.rs`. -#[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. -/// -/// The property name is resolved to a column ID once at `create_node_op` time. -#[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 metadata field. -/// -/// The metadata name is resolved to a column ID once at `create_node_op` time. -#[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 })) - } -} - -/// `Type` from `db/api/state/ops/node.rs` used as a node expression. -/// -/// `Type: NodeOp>` — used directly, no conversion. -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 - } -} - -/// `Name` from `db/api/state/ops/node.rs` used as a node expression. -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 - } -} - -/// `Id` from `db/api/state/ops/node.rs` used as a node expression. -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)) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// NodeExpr impls for constant value types -// -// 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() - } -} - -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()))) - } -} - -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`, etc.) can be passed directly; -/// `ConstExpr` is only needed for custom attribute output types. -#[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()))) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// BinOpNodeOp<'g, T> — compares two NodeOp using BinaryOp -// ───────────────────────────────────────────────────────────────────────────── - -/// Execution op for `BinOpNodeFilter`. -/// -/// Holds two compiled `NodeOp` (type-erased via `Arc`) -/// and applies `T::binary_cmp`. The `'g` lifetime bounds both ops to the graph -/// view they were compiled against. -#[derive(Clone)] -pub struct BinOpNodeOp<'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 BinOpNodeOp<'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 - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// UnaryNodeOp<'g, T> — evaluates is_some / is_none -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub struct UnaryNodeOp<'g, I: Clone + Send + Sync + 'static> { - inner: Arc> + 'g>, - 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 - } -} - -// graph.nodes.select(NodeFilter.property("bool_prop") || NodeFilter.degree() > 10) -> should work -// graph.nodes.select(NodeFilter.property("str_prop") || NodeFilter.degree() > 10) -> should fail when you construct the filter -// NodeFilter.degree() || ... should fail immediately when you try to construct the expression or ideally at compile-time - - - -// ───────────────────────────────────────────────────────────────────────────── -// SetNodeOp<'g, T> — evaluates is_in / is_not_in -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub struct SetNodeOp<'g, I: Eq + Hash + Clone + Send + Sync + 'static> { - inner: Arc> + 'g>, - op: SetOp, - 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), - } - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// BinOpNodeFilter — binary expression filter (no PhantomData) -// ───────────────────────────────────────────────────────────────────────────── - -/// 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. No `PhantomData` required -/// because the output type is encoded as an associated type of `L`. -/// -/// Created by `NodeExprFilterOps`: -/// ```rust,ignore -/// DegreeExpr(Direction::BOTH).gt(2usize) -/// DegreeExpr(Direction::OUT).gt(DegreeExpr(Direction::IN)) -/// NodeFilter::property("age").gt(30i64) -/// NodeFilter::name().eq("Alice") -/// ``` -pub struct BinOpNodeFilter -where - L: NodeExpr, - R: NodeExpr, -{ - pub left: L, - pub op: BinaryOp, - pub right: R, -} - -// [0, 1, 2, 3] < Const(2) => [true, true, false, false] -// [[0, 1], [0, 1, 2, 3]] < 2 => [[true, true], [true, true, false, false]] -// ([[0, 1], [0, 1, 2, 3]] < 2).any() => [true, true] -// ([[0, 1], [0, 1, 2, 3]] < 2).all() => [true, false] -// ([[0, 1], [0, 1, 2, 3]] < 2).any().all() => true -// ([[0, 1], [0, 1, 2, 3]] < 2).all().all() => false -// ([[0, 1], [0, 1, 2, 3]] < 2).all().any() => true - -// AnyExpr> -// NodeFilter.property("boolean_list_property").any() - -impl BinOpNodeFilter -where - L: NodeExpr, - R: NodeExpr, -{ - pub fn new(left: L, op: BinaryOp, right: R) -> Self { - Self { left, op, right } - } -} - -impl Clone for BinOpNodeFilter -where - L: NodeExpr, - R: NodeExpr, -{ - fn clone(&self) -> Self { - Self { - left: self.left.clone(), - op: self.op, - right: self.right.clone(), - } - } -} - -impl ComposableFilter for BinOpNodeFilter -where - L: NodeExpr, - R: NodeExpr, -{ -} - -impl CreateFilter for BinOpNodeFilter -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)?; - Ok(Arc::new(BinOpNodeOp { - left, - right, - op: self.op, - })) - } -} - -impl TryAsCompositeFilter for BinOpNodeFilter -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()` and `.is_none()` on any `NodeExpr>`. -pub struct UnaryNodeFilter -where - E: NodeExpr>, - I: Clone + Send + Sync + 'static, -{ - pub expr: E, - pub op: UnaryOp, - _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 the inner value of an `Option`-valued -/// expression is contained in (or absent from) a fixed set. -/// -/// Created by `.is_in(values)` and `.is_not_in(values)`. -pub struct SetNodeFilter -where - E: NodeExpr>, - I: Eq + Hash + Clone + Send + Sync + 'static, -{ - pub expr: E, - pub op: SetOp, - pub values: Arc>, - _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) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// NodeExprFilterOps — comparison and set operators on NodeExpr -// ───────────────────────────────────────────────────────────────────────────── - -/// Comparison, string, set, and presence operators on any `NodeExpr`. -/// -/// `gt(rhs)` accepts any `R: 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) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::Gt, rhs) - } - - fn ge>(self, rhs: R) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::Ge, rhs) - } - - fn lt>(self, rhs: R) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::Lt, rhs) - } - - fn le>(self, rhs: R) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::Le, rhs) - } - - fn eq>(self, rhs: R) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::Eq, rhs) - } - - fn ne>(self, rhs: R) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::Ne, rhs) - } - - fn starts_with>(self, rhs: R) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::StartsWith, rhs) - } - - fn ends_with>(self, rhs: R) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::EndsWith, rhs) - } - - fn contains>(self, rhs: R) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::Contains, rhs) - } - - fn not_contains>(self, rhs: R) -> BinOpNodeFilter { - BinOpNodeFilter::new(self, BinaryOp::NotContains, rhs) - } - - fn fuzzy_search>( - self, - rhs: R, - levenshtein_distance: usize, - prefix_match: bool, - ) -> BinOpNodeFilter { - BinOpNodeFilter::new( - self, - BinaryOp::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_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 {} - -// ───────────────────────────────────────────────────────────────────────────── -// Sealed trait for QuantifierMode -// ───────────────────────────────────────────────────────────────────────────── - -mod sealed { - pub trait Sealed {} -} - -// ───────────────────────────────────────────────────────────────────────────── -// QuantifierMode — AnyMode / AllMode -// ───────────────────────────────────────────────────────────────────────────── - -pub trait QuantifierMode: sealed::Sealed + Clone + Copy + Send + Sync + 'static { - const IS_ANY: bool; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AnyMode; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AllMode; - -impl sealed::Sealed for AnyMode {} -impl sealed::Sealed for AllMode {} -impl QuantifierMode for AnyMode { - const IS_ANY: bool = true; -} -impl QuantifierMode for AllMode { - const IS_ANY: bool = false; -} - -// ───────────────────────────────────────────────────────────────────────────── -// TemporalNodePropOp — returns all temporal values for a property -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct TemporalNodePropOp { - graph: G, - prop_id: usize, -} - -impl NodeOp for TemporalNodePropOp { - type Output = Vec; - - fn apply(&self, _storage: &GraphStorage, node: VID) -> 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() - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// TemporalPropertyExpr — NodeExpr> -// ───────────────────────────────────────────────────────────────────────────── - -/// All temporal values of a named property over the current view window. -#[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 = Vec; - - 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 NodeOps — compile-time resolved against a concrete graph view -// ───────────────────────────────────────────────────────────────────────────── - -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 = self.inner.apply(storage, node); - ($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() }); - -// ───────────────────────────────────────────────────────────────────────────── -// Aggregator Exprs — NodeExpr wrappers producing a single scalar -// ───────────────────────────────────────────────────────────────────────────── - -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); - -// ───────────────────────────────────────────────────────────────────────────── -// AnyNodeOp / AllNodeOp — quantified comparison over a temporal sequence -// ───────────────────────────────────────────────────────────────────────────── - -pub struct AnyNodeOp<'g> { - inner: Arc> + 'g>, - rhs: Arc> + 'g>, - op: BinaryOp, -} - -impl<'g> Clone for AnyNodeOp<'g> { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - rhs: self.rhs.clone(), - op: self.op, - } - } -} - -impl<'g> NodeOp for AnyNodeOp<'g> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, node: VID) -> bool { - let vals = self.inner.apply(storage, node); - let Some(rhs) = self.rhs.apply(storage, node) else { - return false; - }; - vals.iter().any(|v| Prop::binary_cmp(&self.op, v, &rhs)) - } -} - -pub struct AllNodeOp<'g> { - inner: Arc> + 'g>, - rhs: Arc> + 'g>, - op: BinaryOp, -} - -pub struct AllNodeOp2<'g> { - inner: Arc> + 'g>, -} - -impl<'g> Clone for AllNodeOp<'g> { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - rhs: self.rhs.clone(), - op: self.op, - } - } -} - -impl<'g> NodeOp for AllNodeOp<'g> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, node: VID) -> bool { - let vals = self.inner.apply(storage, node); - let Some(rhs) = self.rhs.apply(storage, node) else { - return false; - }; - !vals.is_empty() && vals.iter().all(|v| Prop::binary_cmp(&self.op, v, &rhs)) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// QuantifiedNodeFilter — leaf filter wrapping a quantified comparison -// ───────────────────────────────────────────────────────────────────────────── - -pub struct QuantifiedNodeFilter -where - E: NodeExpr>, - Q: QuantifierMode, - R: NodeExpr>, -{ - pub expr: E, - pub rhs: R, - pub op: BinaryOp, - _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> { - Ok(AnyNodeOp { - inner: self.expr.create_node_op(graph.clone())?, - rhs: self.rhs.create_node_op(graph)?, - op: self.op, - }) - } -} - -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> { - Ok(AllNodeOp { - inner: self.expr.create_node_op(graph.clone())?, - rhs: self.rhs.create_node_op(graph)?, - op: self.op, - }) - } -} - -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) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// Context builders — carry wrap context through the builder chain -// ───────────────────────────────────────────────────────────────────────────── - -/// Builder returned from `.any()` / `.all()` on a temporal expression. -/// -/// Call `.eq(rhs)`, `.gt(rhs)` etc. to produce the final `QuantifiedNodeFilter`. -pub struct QuantifiedContextBuilder -where - E: NodeExpr>, - Q: QuantifierMode, -{ - pub(crate) expr: E, - pub(crate) _q: PhantomData, -} - -impl QuantifiedContextBuilder -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) - } -} - -/// Builder returned from aggregators (`.sum()`, `.avg()` etc.) on a temporal expression. -/// -/// Call `.eq(rhs)`, `.gt(rhs)` etc. to produce the final `BinOpNodeFilter`. -pub struct NodeExprContextBuilder { - pub(crate) expr: E, -} - -impl NodeExprContextBuilder { - fn finish>( - self, - op: BinaryOp, - rhs: R, - ) -> BinOpNodeFilter { - BinOpNodeFilter::new(self.expr, op, rhs) - } - - pub fn eq>(self, rhs: R) -> BinOpNodeFilter { - self.finish(BinaryOp::Eq, rhs) - } - - pub fn ne>(self, rhs: R) -> BinOpNodeFilter { - self.finish(BinaryOp::Ne, rhs) - } - - pub fn gt>(self, rhs: R) -> BinOpNodeFilter { - self.finish(BinaryOp::Gt, rhs) - } - - pub fn ge>(self, rhs: R) -> BinOpNodeFilter { - self.finish(BinaryOp::Ge, rhs) - } - - pub fn lt>(self, rhs: R) -> BinOpNodeFilter { - self.finish(BinaryOp::Lt, rhs) - } - - pub fn le>(self, rhs: R) -> BinOpNodeFilter { - self.finish(BinaryOp::Le, rhs) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// TemporalPropContext — entry point returned from `.temporal_property(name)` -// ───────────────────────────────────────────────────────────────────────────── - -/// Builder returned from `.temporal_property(name)`. -/// -/// `E` is the view expression (e.g. `NodeFilter`, `Windowed`, `Layered`) -/// that scopes which temporal property values are visible. -/// -/// Usage: -/// ```rust,ignore -/// NodeFilter::temporal_property("score").any().gt(10i64) -/// NodeFilter.window(0, 100).temporal_property("score").any().gt(10i64) -/// NodeFilter::temporal_property("price").sum().gt(100i64) -/// ``` -pub struct TemporalPropContext { - view_expr: E, - name: String, -} - -impl TemporalPropContext { - 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) -> QuantifiedContextBuilder, AnyMode> { - QuantifiedContextBuilder { - expr: self.make_expr(), - _q: PhantomData, - } - } - - pub fn all(self) -> QuantifiedContextBuilder, AllMode> { - QuantifiedContextBuilder { - expr: self.make_expr(), - _q: PhantomData, - } - } - - pub fn sum(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { - expr: SumExpr(self.make_expr()), - } - } - - pub fn avg(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { - expr: AvgExpr(self.make_expr()), - } - } - - pub fn min(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { - expr: MinExpr(self.make_expr()), - } - } - - pub fn max(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { - expr: MaxExpr(self.make_expr()), - } - } - - pub fn first(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { - expr: FirstExpr(self.make_expr()), - } - } - - pub fn last(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { - expr: LastExpr(self.make_expr()), - } - } - - pub fn len(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { - expr: LenExpr(self.make_expr()), - } - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// TemporalExprOps — blanket trait for E: NodeExpr> -// ───────────────────────────────────────────────────────────────────────────── - -/// Quantifier and aggregator operators for temporal property sequences. -/// -/// Available on any `NodeExpr>` (e.g. `TemporalPropertyExpr`). -pub trait TemporalExprOps: NodeExpr> + Sized { - fn any(self) -> QuantifiedContextBuilder { - QuantifiedContextBuilder { - expr: self, - _q: PhantomData, - } - } - - fn all(self) -> QuantifiedContextBuilder { - QuantifiedContextBuilder { - 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 {} - -/// Identity wrapper — used by `TemporalExprOps` blanket to avoid wrapping. -#[derive(Debug, Clone, Copy)] -pub struct NoWrap; - -impl Wrap for NoWrap { - type Wrapped = T; - - fn wrap(&self, value: T) -> T { - value - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - db::{ - api::view::filter_ops::NodeSelect, - graph::views::filter::model::{ - node_filter::{NodeFilter, TemporalNodeExprBuilderOps}, - ViewWrapOps, - }, - }, - prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, - }; - use raphtory_api::core::entities::properties::prop::IntoProp; - - // 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 = BinOpNodeFilter::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::temporal_property("score").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) - .temporal_property("score") - .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) - .temporal_property("score") - .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") - .temporal_property("score") - .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") - .temporal_property("score") - .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") - .temporal_property("score") - .sum() - .gt(10i64); - assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); - } -} 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..de88471023 --- /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 builder chain started +/// by `NodeFilter::temporal_property(name)`: +/// +/// ```rust,ignore +/// // NodeFilter::temporal_property("score") returns TemporalPropContext, 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. They are not constructed directly — +// TemporalPropContext / TemporalExprOps methods return NodeExprContextBuilder>: +// +// .temporal_property("v").sum() → NodeExprContextBuilder>> +// .temporal_property("v").len() → NodeExprContextBuilder>> +// +// Calling .gt() / .eq() etc. on the builder 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..15e321d410 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs @@ -0,0 +1,1138 @@ +//! 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 +/// [`QuantifiedContextBuilder`] 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 `QuantifiedContextBuilder::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) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Context builders — carry expression through the builder chain +// ───────────────────────────────────────────────────────────────────────────── + +/// Intermediate builder returned by [`TemporalPropContext::any`] / [`TemporalPropContext::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() // → QuantifiedContextBuilder, AnyMode> +/// .gt(10i64) // → QuantifiedNodeFilter, AnyMode, i64> +/// ``` +pub struct QuantifiedContextBuilder +where + E: NodeExpr, + Q: QuantifierMode, +{ + pub(crate) expr: E, + pub(crate) _q: PhantomData, +} + +impl QuantifiedContextBuilder +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) + } +} + +/// Intermediate builder returned by [`TemporalPropContext::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() // → NodeExprContextBuilder>> +/// .gt(100i64) // → BinaryCmpNodeFilter>, i64> +/// ``` +pub struct NodeExprContextBuilder { + pub(crate) expr: E, +} + +impl NodeExprContextBuilder { + 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) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalPropContext — 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 on this builder creates the next step in the chain: +/// ```rust,ignore +/// NodeFilter::temporal_property("score") // → TemporalPropContext +/// .any() // → QuantifiedContextBuilder, AnyMode> +/// .gt(10i64) // → QuantifiedNodeFilter<.., AnyMode, i64> +/// +/// NodeFilter::temporal_property("price") // → TemporalPropContext +/// .sum() // → NodeExprContextBuilder>> +/// .gt(100i64) // → BinaryCmpNodeFilter, i64> +/// +/// NodeFilter.window(0, 100) +/// .temporal_property("score") // → TemporalPropContext> +/// .any().gt(10i64) +/// ``` +pub struct TemporalPropContext { + pub(crate) view_expr: E, + pub(crate) name: String, +} + +impl TemporalPropContext { + 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) -> QuantifiedContextBuilder, AnyMode> { + QuantifiedContextBuilder { + expr: self.make_expr(), + _q: PhantomData, + } + } + + pub fn all(self) -> QuantifiedContextBuilder, AllMode> { + QuantifiedContextBuilder { + expr: self.make_expr(), + _q: PhantomData, + } + } + + pub fn sum(self) -> NodeExprContextBuilder>> { + NodeExprContextBuilder { + expr: SumExpr(self.make_expr()), + } + } + + pub fn avg(self) -> NodeExprContextBuilder>> { + NodeExprContextBuilder { + expr: AvgExpr(self.make_expr()), + } + } + + pub fn min(self) -> NodeExprContextBuilder>> { + NodeExprContextBuilder { + expr: MinExpr(self.make_expr()), + } + } + + pub fn max(self) -> NodeExprContextBuilder>> { + NodeExprContextBuilder { + expr: MaxExpr(self.make_expr()), + } + } + + pub fn first(self) -> NodeExprContextBuilder>> { + NodeExprContextBuilder { + expr: FirstExpr(self.make_expr()), + } + } + + pub fn last(self) -> NodeExprContextBuilder>> { + NodeExprContextBuilder { + expr: LastExpr(self.make_expr()), + } + } + + pub fn len(self) -> NodeExprContextBuilder>> { + NodeExprContextBuilder { + 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_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) -> QuantifiedContextBuilder { + QuantifiedContextBuilder { + expr: self, + _q: PhantomData, + } + } + + fn all(self) -> QuantifiedContextBuilder { + QuantifiedContextBuilder { + 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..848593eb3d --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs @@ -0,0 +1,518 @@ +use super::*; +use crate::{ + db::{ + api::{state::ops::Id, view::filter_ops::NodeSelect}, + graph::views::filter::{ + model::{ + filter_operator::BinaryOp, + node_filter::{NodeFilter, TemporalNodeExprBuilderOps}, + 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::temporal_property("score").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) + .temporal_property("score") + .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) + .temporal_property("score") + .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") + .temporal_property("score") + .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") + .temporal_property("score") + .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") + .temporal_property("score") + .sum() + .gt(10i64); + assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); +} + +// ── 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/snapshot_filter.rs b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs index cb1c79ec1a..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,17 +1,13 @@ use crate::{ db::{ api::view::internal::GraphView, - graph::views::{ - filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - windowed_filter::Windowed, - ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, - }, - CreateFilter, + graph::views::filter::{ + model::{ + edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, + CreateFilter, }, }, errors::GraphError, 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 f5cdd71ac9..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,10 +4,9 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateView, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, + InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, }, From d2cbd57cbf40825deff1b7effa4f8c1b0ab8f178 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:52:04 +0100 Subject: [PATCH 18/22] add is_true() / is_false() convenience filters for boolean properties --- .../views/filter/model/node_expr/filters.rs | 14 +++++++ .../views/filter/model/node_expr/tests.rs | 39 +++++++++++++++++++ 2 files changed, 53 insertions(+) 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 index 15e321d410..b5a58d7017 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs @@ -1051,6 +1051,20 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { } } + 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>, 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 index 848593eb3d..17ad2149cd 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs @@ -490,6 +490,45 @@ fn layered_temporal_sum_is_layer_scoped() { 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] From 66ef4d1b406ebf115de1b615005e74f790a1e859 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:33:50 +0100 Subject: [PATCH 19/22] =?UTF-8?q?rename=20TemporalPropContext/QuantifiedCo?= =?UTF-8?q?ntextBuilder/NodeExprContextBuilder=20=E2=86=92=20TemporalProp/?= =?UTF-8?q?Quantified/Aggregated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/db/graph/views/filter/model/mod.rs | 11 ++- .../views/filter/model/node_expr/exprs.rs | 14 ++-- .../views/filter/model/node_expr/filters.rs | 84 +++++++++---------- .../views/filter/model/node_filter/mod.rs | 10 +-- 4 files changed, 59 insertions(+), 60 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 68befed4c8..998968585a 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -15,12 +15,11 @@ pub use crate::{ UnaryOp, }, node_expr::{ - AllMode, AnyMode, AvgExpr, BinaryCmpNodeFilter, ConstExpr, DegreeExpr, - FirstExpr, LastExpr, LenExpr, MaxExpr, Metadata, MinExpr, NodeExpr, - NodeExprContextBuilder, NodeExprFilterOps, Property, - QuantifiedContextBuilder, QuantifiedNodeFilter, QuantifierMode, - SetNodeFilter, StringNodeFilter, SumExpr, TemporalExprOps, - TemporalPropContext, UnaryNodeFilter, + 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, 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 index de88471023..ed2c6bf514 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/exprs.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/exprs.rs @@ -396,11 +396,11 @@ impl NodeExpr for Metadata { /// /// Produces `Prop::List` of every recorded value within the view. /// -/// Not constructed directly — created internally by the builder chain started +/// Not constructed directly — created internally by the fluent chain started /// by `NodeFilter::temporal_property(name)`: /// /// ```rust,ignore -/// // NodeFilter::temporal_property("score") returns TemporalPropContext, not this type. +/// // 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> @@ -440,13 +440,13 @@ impl NodeExpr for TemporalPropert // Aggregator Exprs — NodeExpr wrappers producing a single scalar // // Each wraps a NodeExpr (typically TemporalPropertyExpr) and reduces -// the Prop::List it produces to a scalar. They are not constructed directly — -// TemporalPropContext / TemporalExprOps methods return NodeExprContextBuilder>: +// the Prop::List it produces to a scalar. Not constructed directly — +// TemporalProp / TemporalExprOps methods return Aggregated>: // -// .temporal_property("v").sum() → NodeExprContextBuilder>> -// .temporal_property("v").len() → NodeExprContextBuilder>> +// .temporal_property("v").sum() → Aggregated>> +// .temporal_property("v").len() → Aggregated>> // -// Calling .gt() / .eq() etc. on the builder then produces: +// Calling .gt() / .eq() etc. on Aggregated then produces: // BinaryCmpNodeFilter>, RHS> // ───────────────────────────────────────────────────────────────────────────── 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 index b5a58d7017..880deebf7a 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs @@ -80,7 +80,7 @@ mod sealed { // ───────────────────────────────────────────────────────────────────────────── /// Sealed marker trait used as a type parameter on [`QuantifiedNodeFilter`] and -/// [`QuantifiedContextBuilder`] to distinguish `any` vs `all` semantics at compile time. +/// [`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 {} @@ -587,7 +587,7 @@ where /// 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 `QuantifiedContextBuilder::gt/eq/…`: +/// Not constructed directly — returned by `Quantified::gt/eq/…`: /// ```rust,ignore /// // NodeFilter::temporal_property("score").any().gt(10i64) /// // → QuantifiedNodeFilter, AnyMode, i64> @@ -739,18 +739,18 @@ where } // ───────────────────────────────────────────────────────────────────────────── -// Context builders — carry expression through the builder chain +// Quantified / Aggregated / TemporalProp — intermediate types in the fluent chain // ───────────────────────────────────────────────────────────────────────────── -/// Intermediate builder returned by [`TemporalPropContext::any`] / [`TemporalPropContext::all`]. +/// 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() // → QuantifiedContextBuilder, AnyMode> +/// NodeFilter::temporal_property("score").any() // → Quantified, AnyMode> /// .gt(10i64) // → QuantifiedNodeFilter, AnyMode, i64> /// ``` -pub struct QuantifiedContextBuilder +pub struct Quantified where E: NodeExpr, Q: QuantifierMode, @@ -759,7 +759,7 @@ where pub(crate) _q: PhantomData, } -impl QuantifiedContextBuilder +impl Quantified where E: NodeExpr, Q: QuantifierMode, @@ -797,19 +797,19 @@ where } } -/// Intermediate builder returned by [`TemporalPropContext::sum`], `.avg()`, `.min()` etc. +/// 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() // → NodeExprContextBuilder>> +/// NodeFilter::temporal_property("price").sum() // → Aggregated>> /// .gt(100i64) // → BinaryCmpNodeFilter>, i64> /// ``` -pub struct NodeExprContextBuilder { +pub struct Aggregated { pub(crate) expr: E, } -impl NodeExprContextBuilder { +impl Aggregated { fn finish>( self, op: BinaryOp, @@ -844,7 +844,7 @@ impl NodeExprContextBuilder { } // ───────────────────────────────────────────────────────────────────────────── -// TemporalPropContext — entry point returned from `.temporal_property(name)` +// TemporalProp — entry point returned from `.temporal_property(name)` // ───────────────────────────────────────────────────────────────────────────── /// Entry point returned by `NodeFilter::temporal_property(name)`. @@ -852,26 +852,26 @@ impl NodeExprContextBuilder { /// `E` is the view expression (e.g. `NodeFilter`, `Windowed`, `Layered`) /// that scopes which temporal property values are visible. /// -/// Calling a method on this builder creates the next step in the chain: +/// Calling a method produces the next step in the chain: /// ```rust,ignore -/// NodeFilter::temporal_property("score") // → TemporalPropContext -/// .any() // → QuantifiedContextBuilder, AnyMode> +/// NodeFilter::temporal_property("score") // → TemporalProp +/// .any() // → Quantified, AnyMode> /// .gt(10i64) // → QuantifiedNodeFilter<.., AnyMode, i64> /// -/// NodeFilter::temporal_property("price") // → TemporalPropContext -/// .sum() // → NodeExprContextBuilder>> +/// NodeFilter::temporal_property("price") // → TemporalProp +/// .sum() // → Aggregated>> /// .gt(100i64) // → BinaryCmpNodeFilter, i64> /// /// NodeFilter.window(0, 100) -/// .temporal_property("score") // → TemporalPropContext> +/// .temporal_property("score") // → TemporalProp> /// .any().gt(10i64) /// ``` -pub struct TemporalPropContext { +pub struct TemporalProp { pub(crate) view_expr: E, pub(crate) name: String, } -impl TemporalPropContext { +impl TemporalProp { pub(crate) fn new(view_expr: E, name: impl Into) -> Self { Self { view_expr, @@ -886,58 +886,58 @@ impl TemporalPropContext { } } - pub fn any(self) -> QuantifiedContextBuilder, AnyMode> { - QuantifiedContextBuilder { + pub fn any(self) -> Quantified, AnyMode> { + Quantified { expr: self.make_expr(), _q: PhantomData, } } - pub fn all(self) -> QuantifiedContextBuilder, AllMode> { - QuantifiedContextBuilder { + pub fn all(self) -> Quantified, AllMode> { + Quantified { expr: self.make_expr(), _q: PhantomData, } } - pub fn sum(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { + pub fn sum(self) -> Aggregated>> { + Aggregated { expr: SumExpr(self.make_expr()), } } - pub fn avg(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { + pub fn avg(self) -> Aggregated>> { + Aggregated { expr: AvgExpr(self.make_expr()), } } - pub fn min(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { + pub fn min(self) -> Aggregated>> { + Aggregated { expr: MinExpr(self.make_expr()), } } - pub fn max(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { + pub fn max(self) -> Aggregated>> { + Aggregated { expr: MaxExpr(self.make_expr()), } } - pub fn first(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { + pub fn first(self) -> Aggregated>> { + Aggregated { expr: FirstExpr(self.make_expr()), } } - pub fn last(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { + pub fn last(self) -> Aggregated>> { + Aggregated { expr: LastExpr(self.make_expr()), } } - pub fn len(self) -> NodeExprContextBuilder>> { - NodeExprContextBuilder { + pub fn len(self) -> Aggregated>> { + Aggregated { expr: LenExpr(self.make_expr()), } } @@ -1106,15 +1106,15 @@ impl NodeExprFilterOps for E {} /// /// Available on any `NodeExpr` that returns a `Prop::List` (e.g. [`TemporalPropertyExpr`]). pub trait TemporalExprOps: NodeExpr + Sized { - fn any(self) -> QuantifiedContextBuilder { - QuantifiedContextBuilder { + fn any(self) -> Quantified { + Quantified { expr: self, _q: PhantomData, } } - fn all(self) -> QuantifiedContextBuilder { - QuantifiedContextBuilder { + fn all(self) -> Quantified { + Quantified { expr: self, _q: PhantomData, } 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 a07ac8336c..c90177a7f7 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 @@ -20,7 +20,7 @@ use crate::{ is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, - node_expr::{DegreeExpr, Metadata, Property, TemporalPropContext}, + node_expr::{DegreeExpr, Metadata, Property, TemporalProp}, node_filter::validate::validate, node_state_filter::NodeStateBoolColOp, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, @@ -158,8 +158,8 @@ impl NodeFilter { /// Chain with `.any()`, `.all()`, `.sum()`, `.avg()`, `.min()`, `.max()`, /// `.first()`, `.last()`, or `.len()` to produce a filter or scalar expression. #[inline] - pub fn temporal_property(name: impl Into) -> TemporalPropContext { - TemporalPropContext::new(NodeFilter, name) + pub fn temporal_property(name: impl Into) -> TemporalProp { + TemporalProp::new(NodeFilter, name) } } @@ -168,8 +168,8 @@ impl NodeFilter { /// Implemented for all `T: CreateView + Clone` so that `NodeFilter`, `Windowed`, /// `Layered`, etc. all support the same entry point. pub trait TemporalNodeExprBuilderOps: CreateView + Clone + Send + Sync + Sized + 'static { - fn temporal_property(self, name: impl Into) -> TemporalPropContext { - TemporalPropContext::new(self, name) + fn temporal_property(self, name: impl Into) -> TemporalProp { + TemporalProp::new(self, name) } } From d74efe28b34fe870947db9e2a4fdb3d92ca9892c Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:09:50 +0100 Subject: [PATCH 20/22] add NodeFilter::name/id/node_type associated fns and export NodeExprFilterOps from prelude --- .../views/filter/model/node_filter/mod.rs | 18 ++++++++++++++++++ raphtory/src/lib.rs | 2 ++ 2 files changed, 20 insertions(+) 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 c90177a7f7..b5a103591c 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 @@ -140,6 +140,24 @@ impl TryAsCompositeFilter for NodeFilter { } impl NodeFilter { + /// Node name expression — `NodeExpr` — use `.eq("Alice")` etc. + #[inline] + pub fn name() -> Name { + Name + } + + /// Node GID expression — `NodeExpr`. + #[inline] + pub fn id() -> Id { + Id + } + + /// Node type expression — `NodeExpr>`. + #[inline] + pub fn node_type() -> Type { + Type + } + /// Current (latest) value of a named property — serializable. #[inline] pub fn property(name: impl Into) -> Property { diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 980816865f..d14e785ef4 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -156,6 +156,8 @@ pub mod prelude { pub use crate::db::graph::views::filter::model::{node_filter::NodeFilter, EdgeFilter}; + pub use crate::db::graph::views::filter::model::node_expr::NodeExprFilterOps; + pub use storage::{persist::config::ConfigOps, Config}; #[cfg(feature = "io")] From 4c02e777265706d6c60c4da973defabc7ccf1e87 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:30:25 +0100 Subject: [PATCH 21/22] implement InternalPropertyFilterBuilder for PropertyExpr/MetadataExpr to restore dot-syntax property filter ops --- raphtory-tests/tests/node_property_filter.rs | 5 +- .../src/db/graph/views/filter/model/mod.rs | 68 ++++++++++++++++++- .../views/filter/model/node_filter/mod.rs | 18 ----- raphtory/src/lib.rs | 2 - 4 files changed, 70 insertions(+), 23 deletions(-) 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/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 998968585a..536a9bd7e9 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -56,7 +56,11 @@ use crate::{ node_expr::{NodeMetaOp, NodePropOp, TemporalPropertyExpr}, node_filter::NodeFilterFactory, property_filter::{ - builders::PropertyExprBuilderInput, Op, PropertyFilterInput, PropertyRef, + builders::{ + InternalPropertyFilterBuilder, PropertyExprBuilder, + PropertyExprBuilderInput, + }, + Op, PropertyFilterInput, PropertyRef, }, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, @@ -331,6 +335,68 @@ impl DynPropertyFilterFactory for T { } } +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 { + PropertyRef::Property(self.name.clone()) + } + + fn ops(&self) -> &[Op] { + &[] + } + + fn entity(&self) -> Self::Marker { + self.view_expr.clone() + } + + fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { + filter.with_entity(self.entity()) + } + + fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { + builder.with_entity(self.entity()) + } +} + +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 { + PropertyRef::Metadata(self.name.clone()) + } + + fn ops(&self) -> &[Op] { + &[] + } + + fn entity(&self) -> Self::Marker { + self.view_expr.clone() + } + + fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { + filter.with_entity(self.entity()) + } + + fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { + builder.with_entity(self.entity()) + } +} + impl PropertyExpr { pub fn temporal(&self) -> TemporalPropertyExpr { TemporalPropertyExpr { 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 b5a103591c..c90177a7f7 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 @@ -140,24 +140,6 @@ impl TryAsCompositeFilter for NodeFilter { } impl NodeFilter { - /// Node name expression — `NodeExpr` — use `.eq("Alice")` etc. - #[inline] - pub fn name() -> Name { - Name - } - - /// Node GID expression — `NodeExpr`. - #[inline] - pub fn id() -> Id { - Id - } - - /// Node type expression — `NodeExpr>`. - #[inline] - pub fn node_type() -> Type { - Type - } - /// Current (latest) value of a named property — serializable. #[inline] pub fn property(name: impl Into) -> Property { diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index d14e785ef4..980816865f 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -156,8 +156,6 @@ pub mod prelude { pub use crate::db::graph::views::filter::model::{node_filter::NodeFilter, EdgeFilter}; - pub use crate::db::graph::views::filter::model::node_expr::NodeExprFilterOps; - pub use storage::{persist::config::ConfigOps, Config}; #[cfg(feature = "io")] From 54747e588300a41ac53ef1219e3f70d143ebffec Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:45:04 +0100 Subject: [PATCH 22/22] =?UTF-8?q?remove=20TemporalNodeExprBuilderOps=20and?= =?UTF-8?q?=20temporal=5Fproperty=20shortcut=20=E2=80=94=20use=20.property?= =?UTF-8?q?().temporal()=20instead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/db/graph/views/filter/model/mod.rs | 11 ++++----- .../views/filter/model/node_expr/tests.rs | 20 +++++++++------- .../views/filter/model/node_filter/mod.rs | 24 +------------------ 3 files changed, 17 insertions(+), 38 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 536a9bd7e9..19f6e3bb71 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -53,7 +53,7 @@ use crate::{ is_valid_filter::IsValidEdge, latest_filter::Latest, layered_filter::Layered, - node_expr::{NodeMetaOp, NodePropOp, TemporalPropertyExpr}, + node_expr::{NodeMetaOp, NodePropOp}, node_filter::NodeFilterFactory, property_filter::{ builders::{ @@ -397,12 +397,9 @@ where } } -impl PropertyExpr { - pub fn temporal(&self) -> TemporalPropertyExpr { - TemporalPropertyExpr { - view_expr: self.view_expr.clone(), - name: self.name.clone(), - } +impl PropertyExpr { + pub fn temporal(&self) -> TemporalProp { + TemporalProp::new(self.view_expr.clone(), self.name.clone()) } } 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 index 17ad2149cd..498e5286e8 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs @@ -4,8 +4,7 @@ use crate::{ api::{state::ops::Id, view::filter_ops::NodeSelect}, graph::views::filter::{ model::{ - filter_operator::BinaryOp, - node_filter::{NodeFilter, TemporalNodeExprBuilderOps}, + filter_operator::BinaryOp, node_filter::NodeFilter, PropertyFilterFactory, ViewWrapOps, }, CreateFilter, @@ -315,7 +314,7 @@ fn temporal_len_ge_2() { #[test] fn node_filter_temporal_property_entry_point() { let g = build_temporal_graph(); - let filter = NodeFilter::temporal_property("score").any().eq(5i64); + let filter = NodeFilter.property("score").temporal().any().eq(5i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -356,7 +355,8 @@ fn windowed_temporal_any_restricts_to_window() { let g = build_temporal_graph(); let filter = NodeFilter .window(1, 2) - .temporal_property("score") + .property("score") + .temporal() .any() .eq(5i64); // window [1,2) shows t=1 only → alice has score=1, not 5 @@ -369,7 +369,8 @@ fn windowed_temporal_any_matches_in_window() { let g = build_temporal_graph(); let filter = NodeFilter .window(2, 3) - .temporal_property("score") + .property("score") + .temporal() .any() .eq(5i64); assert_eq!(windowed_filtered_names(filter, g), vec!["alice"]); @@ -458,7 +459,8 @@ fn layered_temporal_any_restricts_to_layer_a_updates() { let g = build_layered_temporal_graph(); let filter = NodeFilter .layer("layer_a") - .temporal_property("score") + .property("score") + .temporal() .any() .eq(5i64); assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); @@ -471,7 +473,8 @@ fn layered_temporal_any_restricts_to_layer_b_updates() { let g = build_layered_temporal_graph(); let filter = NodeFilter .layer("layer_b") - .temporal_property("score") + .property("score") + .temporal() .any() .gt(2i64); assert_eq!(layered_filtered_names(filter, g), vec!["bob"]); @@ -484,7 +487,8 @@ fn layered_temporal_sum_is_layer_scoped() { let g = build_layered_temporal_graph(); let filter = NodeFilter .layer("layer_a") - .temporal_property("score") + .property("score") + .temporal() .sum() .gt(10i64); assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); 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 c90177a7f7..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 @@ -20,7 +20,7 @@ use crate::{ is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, - node_expr::{DegreeExpr, Metadata, Property, TemporalProp}, + node_expr::{DegreeExpr, Metadata, Property}, node_filter::validate::validate, node_state_filter::NodeStateBoolColOp, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, @@ -151,30 +151,8 @@ impl NodeFilter { pub fn metadata(name: impl Into) -> Metadata { Metadata::new(name) } - - /// Full temporal history of a named property as a sequence of `Prop` values. - /// - /// Values are scoped to the current view window (all time if no window applied). - /// Chain with `.any()`, `.all()`, `.sum()`, `.avg()`, `.min()`, `.max()`, - /// `.first()`, `.last()`, or `.len()` to produce a filter or scalar expression. - #[inline] - pub fn temporal_property(name: impl Into) -> TemporalProp { - TemporalProp::new(NodeFilter, name) - } } -/// Extension trait that adds `.temporal_property(name)` to any view expression type. -/// -/// Implemented for all `T: CreateView + Clone` so that `NodeFilter`, `Windowed`, -/// `Layered`, etc. all support the same entry point. -pub trait TemporalNodeExprBuilderOps: CreateView + Clone + Send + Sync + Sized + 'static { - fn temporal_property(self, name: impl Into) -> TemporalProp { - TemporalProp::new(self, name) - } -} - -impl TemporalNodeExprBuilderOps for T {} - impl Wrap for NodeFilter { type Wrapped = T;