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 001/155] 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 002/155] 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 003/155] 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 004/155] 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 005/155] 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 006/155] 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 007/155] 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 008/155] 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 009/155] 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 010/155] 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 011/155] 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 012/155] 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 013/155] 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 014/155] 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 015/155] 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 016/155] 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 017/155] 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 018/155] 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 019/155] =?UTF-8?q?rename=20TemporalPropContext/Quantified?= =?UTF-8?q?ContextBuilder/NodeExprContextBuilder=20=E2=86=92=20TemporalPro?= =?UTF-8?q?p/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 020/155] 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 021/155] 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 022/155] =?UTF-8?q?remove=20TemporalNodeExprBuilderOps=20a?= =?UTF-8?q?nd=20temporal=5Fproperty=20shortcut=20=E2=80=94=20use=20.proper?= =?UTF-8?q?ty().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; From ac0f1f6a2d1ac2162fc0c17b4efb3134be60a763 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 15 Jun 2026 08:59:50 +0100 Subject: [PATCH 023/155] impl edge sides of things --- raphtory-tests/tests/edge_expr_filter.rs | 673 +++++++++++++ raphtory-tests/tests/test_filters.rs | 122 +-- .../views/filter/edge_expr_filtered_graph.rs | 131 +++ raphtory/src/db/graph/views/filter/mod.rs | 1 + .../views/filter/model/edge_expr/exprs.rs | 274 ++++++ .../views/filter/model/edge_expr/filters.rs | 894 ++++++++++++++++++ .../graph/views/filter/model/edge_expr/mod.rs | 533 +++++++++++ .../graph/views/filter/model/edge_expr/ops.rs | 566 +++++++++++ .../graph/views/filter/model/edge_filter.rs | 16 +- .../graph/views/filter/model/latest_filter.rs | 15 +- .../src/db/graph/views/filter/model/mod.rs | 124 ++- .../views/filter/model/node_expr/exprs.rs | 159 +++- .../views/filter/model/node_expr/filters.rs | 739 ++++++++++++--- .../graph/views/filter/model/node_expr/mod.rs | 1 + .../graph/views/filter/model/node_expr/ops.rs | 230 ++++- .../views/filter/model/node_filter/mod.rs | 7 +- .../views/filter/model/snapshot_filter.rs | 24 +- raphtory/src/lib.rs | 9 +- 18 files changed, 4268 insertions(+), 250 deletions(-) create mode 100644 raphtory-tests/tests/edge_expr_filter.rs create mode 100644 raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs create mode 100644 raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs create mode 100644 raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs create mode 100644 raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs create mode 100644 raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs new file mode 100644 index 0000000000..ce81282aa6 --- /dev/null +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -0,0 +1,673 @@ +use raphtory::{ + db::{ + api::view::Filter, + graph::views::filter::model::{ComposableFilter, EdgeFilter, PropertyFilterFactory}, + }, + prelude::*, +}; +use raphtory::prelude::EdgeExprFilterOps; + +fn sorted_edges(g: impl GraphViewOps<'static>) -> Vec { + let mut edges: Vec = g + .edges() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + edges.sort(); + edges +} + +#[test] +fn test_edge_temporal_len_gt() { + let g = Graph::new(); + // A->B gets 3 temporal updates for "score" + g.add_edge(1, "A", "B", [("score", Prop::I64(10))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(20))], None).unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(30))], None).unwrap(); + // C->D gets 1 temporal update for "score" + g.add_edge(1, "C", "D", [("score", Prop::I64(10))], None).unwrap(); + // E->F gets no "score" update (zero temporal values) + g.add_edge(1, "E", "F", [("other", Prop::I64(1))], None).unwrap(); + + let filter = EdgeFilter.property("score").temporal().len().gt(1usize); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_temporal_len_eq() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(10))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(20))], None).unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(30))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(10))], None).unwrap(); + + // exactly 1 temporal update + let filter = EdgeFilter.property("score").temporal().len().eq(1usize); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["C->D"]); +} + +#[test] +fn test_edge_temporal_len_lt() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::I64(2))], None).unwrap(); + + // fewer than 3 updates + let filter = EdgeFilter.property("score").temporal().len().lt(3usize); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["C->D"]); +} + +#[test] +fn test_edge_temporal_len_le() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::I64(2))], None).unwrap(); + + // at most 2 updates + let filter = EdgeFilter.property("score").temporal().len().le(2usize); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["C->D"]); +} + +#[test] +fn test_edge_temporal_len_ge() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); + + // at least 2 updates + let filter = EdgeFilter.property("score").temporal().len().ge(2usize); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_temporal_len_ne() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); + + // not exactly 1 update + let filter = EdgeFilter.property("score").temporal().len().ne(1usize); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_temporal_len_combined_with_and() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::I64(2))], None).unwrap(); + g.add_edge(3, "C", "D", [("score", Prop::I64(3))], None).unwrap(); + + // both have >= 2 updates; only A->B has exactly 2 + let filter = EdgeFilter + .property("score") + .temporal() + .len() + .ge(2usize) + .and(EdgeFilter.property("score").temporal().len().le(2usize)); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// String ops via EdgeExprFilterOps (generic) and EdgeAggregated convenience +// ───────────────────────────────────────────────────────────────────────────── + +fn band_graph() -> Graph { + let g = Graph::new(); + g.add_edge(1, "Jimi", "John", [("band", Prop::str("Pink Floyd"))], None).unwrap(); + g.add_edge(1, "John", "David", [("band", Prop::str("Led Zeppelin"))], None).unwrap(); + g.add_edge(1, "David", "Robert", [("band", Prop::str("Deep Purple"))], None).unwrap(); + g +} + +#[test] +fn test_edge_property_contains_via_expr_filter_ops() { + let g = band_graph(); + // generic form: PropertyExpr.contains(Prop::Str(...)) + let filter = EdgeFilter + .property("band") + .contains(Prop::str("Floyd")); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["Jimi->John"]); +} + +#[test] +fn test_edge_property_not_contains_via_expr_filter_ops() { + let g = band_graph(); + let filter = EdgeFilter + .property("band") + .not_contains(Prop::str("Floyd")); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["David->Robert", "John->David"]); +} + +#[test] +fn test_edge_property_starts_with_via_expr_filter_ops() { + let g = band_graph(); + let filter = EdgeFilter + .property("band") + .starts_with(Prop::str("Pink")); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["Jimi->John"]); +} + +#[test] +fn test_edge_property_ends_with_via_expr_filter_ops() { + let g = band_graph(); + let filter = EdgeFilter + .property("band") + .ends_with(Prop::str("Zeppelin")); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["John->David"]); +} + +#[test] +fn test_edge_property_fuzzy_search_via_expr_filter_ops() { + let g = Graph::new(); + // Use short values so whole-string Levenshtein is meaningful: + // "Floyd" vs "Floid" = 1 substitution (y→i) + // "Zeppelin" is much farther away + g.add_edge(1, "A", "B", [("tag", Prop::str("Floyd"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("Zeppelin"))], None).unwrap(); + + let filter = EdgeFilter + .property("tag") + .fuzzy_search(Prop::str("Floid"), 1, false); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_aggregated_last_contains_str_convenience() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + // last temporal value of "tag": A->B = "metal", C->D = "jazz" + let filter = EdgeFilter.property("tag").temporal().last().contains("etal"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_aggregated_first_starts_with_str_convenience() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + // first temporal value: A->B = "rock", C->D = "jazz" + let filter = EdgeFilter.property("tag").temporal().first().starts_with("ro"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Set ops — PropValueSetEdgeFilter (linear scan, Option) and +// SetEdgeFilter (HashSet, Option) +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn test_edge_property_is_in_prop_values() { + // Path A: EdgePropertyExprOps::is_in — PropValueSetEdgeFilter + let g = band_graph(); + let filter = EdgeFilter.property("band").is_in([ + Prop::str("Pink Floyd"), + Prop::str("Deep Purple"), + ]); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["David->Robert", "Jimi->John"]); +} + +#[test] +fn test_edge_property_is_not_in_prop_values() { + let g = band_graph(); + let filter = EdgeFilter.property("band").is_not_in([ + Prop::str("Pink Floyd"), + Prop::str("Deep Purple"), + ]); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["John->David"]); +} + +#[test] +fn test_edge_aggregated_last_is_in_prop_values() { + // Path A via EdgeAggregated convenience + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + // last value: A->B = "metal", C->D = "jazz" + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .is_in([Prop::str("metal"), Prop::str("blues")]); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_aggregated_last_is_not_in_prop_values() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .is_not_in([Prop::str("metal"), Prop::str("blues")]); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["C->D"]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Step 4: EdgeQuantified string ops (any/all + contains/starts_with/ends_with) +// ───────────────────────────────────────────────────────────────────────────── + +fn genre_graph() -> Graph { + let g = Graph::new(); + // A->B has tags: "rock", "metal", "rock-n-roll" (3 updates) + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(3, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); + // C->D has tags: "jazz", "blues" (2 updates) + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("blues"))], None).unwrap(); + g +} + +#[test] +fn test_edge_quantified_any_contains() { + let g = genre_graph(); + // any temporal value of "tag" contains "rock" + let filter = EdgeFilter.property("tag").temporal().any().contains("rock"); + let result = g.filter(filter).unwrap(); + // A->B has "rock" and "rock-n-roll" (contains "rock"), C->D has neither + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_quantified_any_starts_with() { + let g = genre_graph(); + let filter = EdgeFilter.property("tag").temporal().any().starts_with("rock"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_quantified_any_ends_with() { + let g = genre_graph(); + let filter = EdgeFilter.property("tag").temporal().any().ends_with("roll"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_quantified_any_not_contains() { + let g = genre_graph(); + // any temporal value of "tag" does NOT contain "rock" + // A->B: "metal" and "rock-n-roll" don't, but "rock" does → any not_contains is true for A->B + // C->D: "jazz" and "blues" don't contain "rock" → any not_contains is true for C->D + // Both edges pass (any value doesn't contain "rock") + let filter = EdgeFilter.property("tag").temporal().any().not_contains("rock"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B", "C->D"]); +} + +#[test] +fn test_edge_quantified_all_contains() { + let g = Graph::new(); + // A->B: all tags contain "rock" + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); + // C->D: not all tags contain "rock" + g.add_edge(1, "C", "D", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + let filter = EdgeFilter.property("tag").temporal().all().contains("rock"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_quantified_all_starts_with() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("rock-steady"))], None).unwrap(); + + let filter = EdgeFilter.property("tag").temporal().all().starts_with("rock"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Step 4: EdgeQuantified set ops (any/all + is_in/is_not_in) +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn test_edge_quantified_any_is_in() { + let g = genre_graph(); + // any temporal value of "tag" is in {"metal", "blues"} + let filter = EdgeFilter + .property("tag") + .temporal() + .any() + .is_in([Prop::str("metal"), Prop::str("blues")]); + let result = g.filter(filter).unwrap(); + // A->B has "metal" → passes; C->D has "blues" → passes + assert_eq!(sorted_edges(result), vec!["A->B", "C->D"]); +} + +#[test] +fn test_edge_quantified_any_is_not_in() { + let g = genre_graph(); + // any temporal value of "tag" is NOT in {"metal", "blues"} + let filter = EdgeFilter + .property("tag") + .temporal() + .any() + .is_not_in([Prop::str("metal"), Prop::str("blues")]); + let result = g.filter(filter).unwrap(); + // A->B has "rock" and "rock-n-roll" not in set → passes + // C->D has "jazz" not in set → passes + assert_eq!(sorted_edges(result), vec!["A->B", "C->D"]); +} + +#[test] +fn test_edge_quantified_all_is_in() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("metal"))], None).unwrap(); + + // all temporal values in {"rock", "metal"} + let filter = EdgeFilter + .property("tag") + .temporal() + .all() + .is_in([Prop::str("rock"), Prop::str("metal")]); + let result = g.filter(filter).unwrap(); + // A->B: "rock" ✓, "metal" ✓ → passes; C->D: "jazz" ✗ → fails + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_quantified_all_is_not_in() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("blues"))], None).unwrap(); + + // all temporal values are NOT in {"rock", "metal"} + let filter = EdgeFilter + .property("tag") + .temporal() + .all() + .is_not_in([Prop::str("rock"), Prop::str("metal")]); + let result = g.filter(filter).unwrap(); + // A->B: "rock" is in set → fails; C->D: "jazz" ✓, "blues" ✓ → passes + assert_eq!(sorted_edges(result), vec!["C->D"]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Step 5: Re-aggregation chains on EdgeAggregated +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn test_edge_aggregated_last_then_sum() { + // temporal values are themselves lists — re-aggregate the aggregated list + // E.g. .temporal().last() gives the last single value; chaining .sum() wraps it + // in an UnwrapOptPropEdgeExpr and sums what is available. + // Simple case: single temporal value so last == only value, sum of that single number. + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(10))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(20))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(5))], None).unwrap(); + + // last temporal value: A->B = 20, C->D = 5; then sum of that single value = itself + let filter = EdgeFilter.property("score").temporal().last().sum().gt(10i64); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_aggregated_last_then_contains() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + // last value: A->B = "metal", C->D = "jazz" + // re-chain: last().contains("metal") → A->B passes + let filter = EdgeFilter.property("tag").temporal().last().contains("metal"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_aggregated_first_then_ends_with() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("blues"))], None).unwrap(); + + // first value: A->B = "rock-n-roll", C->D = "blues" + let filter = EdgeFilter.property("tag").temporal().first().ends_with("roll"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_aggregated_last_then_len() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(42))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(7))], None).unwrap(); + + // last() gives Option; len() wraps as list of one element → len = 1 + // When None, len = 0. Here both edges have a last value, so len = 1 for both. + let filter = EdgeFilter.property("score").temporal().last().len().eq(1usize); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B", "C->D"]); +} + +#[test] +fn test_edge_aggregated_last_then_any_is_in() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + // last value: A->B = "metal", C->D = "jazz" + // .any().is_in([...]) — since last() produces a single-element list, any == the value itself + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .any() + .is_in([Prop::str("metal"), Prop::str("blues")]); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_aggregated_last_then_all_contains() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + // last value: A->B = "rock-n-roll" (contains "rock"), C->D = "jazz" (doesn't) + let filter = EdgeFilter.property("tag").temporal().last().all().contains("rock"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_aggregated_last_then_is_in() { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .is_in([Prop::str("metal"), Prop::str("blues")]); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Gap 1: EdgePropertyExprOps &str convenience methods (no Prop:: wrapper) +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn test_edge_property_contains_str_literal() { + let g = band_graph(); + let filter = EdgeFilter.property("band").contains("Floyd"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["Jimi->John"]); +} + +#[test] +fn test_edge_property_starts_with_str_literal() { + let g = band_graph(); + let filter = EdgeFilter.property("band").starts_with("Pink"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["Jimi->John"]); +} + +#[test] +fn test_edge_property_ends_with_str_literal() { + let g = band_graph(); + let filter = EdgeFilter.property("band").ends_with("Purple"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["David->Robert"]); +} + +#[test] +fn test_edge_property_not_contains_str_literal() { + let g = band_graph(); + let filter = EdgeFilter.property("band").not_contains("Floyd"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["David->Robert", "John->David"]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Gap 2: is_true / is_false on EdgePropertyExprOps +// ───────────────────────────────────────────────────────────────────────────── + +fn active_graph() -> Graph { + let g = Graph::new(); + g.add_edge(1, "A", "B", [("active", Prop::Bool(true))], None).unwrap(); + g.add_edge(1, "C", "D", [("active", Prop::Bool(false))], None).unwrap(); + g.add_edge(1, "E", "F", [("active", Prop::Bool(true))], None).unwrap(); + g +} + +#[test] +fn test_edge_property_is_true() { + let g = active_graph(); + let filter = EdgeFilter.property("active").is_true(); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B", "E->F"]); +} + +#[test] +fn test_edge_property_is_false() { + let g = active_graph(); + let filter = EdgeFilter.property("active").is_false(); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["C->D"]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Gap 3: EdgeQuantified re-aggregation chains (.any().sum(), .all().min(), etc.) +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn test_edge_quantified_any_sum_gt() { + // .any().sum() on scalar temporal values: sum() is pass-through for scalars, + // validating the re-aggregation chain compiles and produces correct output. + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(3))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(8))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::I64(4))], None).unwrap(); + + // A->B has 8 > 5, C->D has no value > 5 + let filter = EdgeFilter.property("score").temporal().any().sum().gt(5i64); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_quantified_all_min_ge() { + // .all().min().ge(n): validates the all + re-aggregation chain compiles. + let g = Graph::new(); + g.add_edge(1, "A", "B", [("score", Prop::I64(3))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(8))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::I64(9))], None).unwrap(); + + // A->B: all values >= 3 → passes; C->D: 1 < 3 → fails + let filter = EdgeFilter.property("score").temporal().all().min().ge(3i64); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_quantified_any_any_contains() { + // .any().any().contains(s): double-any chain via NestedMapEdgeExpr. + let g = genre_graph(); + let filter = EdgeFilter.property("tag").temporal().any().any().contains("rock"); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} + +#[test] +fn test_edge_quantified_any_last_is_in() { + // .any().last().is_in([...]): re-aggregate with last then set-check. + let g = Graph::new(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + + // A->B: values ["rock", "metal"] → any in {"metal"} → true; C->D: ["jazz"] → false + let filter = EdgeFilter + .property("tag") + .temporal() + .any() + .last() + .is_in([Prop::str("metal")]); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index b8b8cc6891..7e26815c16 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -123,7 +123,7 @@ mod test_property_semantics { api::view::{filter_ops::Filter, StaticGraphViewOps}, graph::views::filter::model::{ node_filter::NodeFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, TemporalPropertyFilterFactory, + PropertyFilterFactory, }, }, errors::GraphError, @@ -468,7 +468,7 @@ mod test_property_semantics { graph::views::filter::{ model::{ edge_filter::EdgeFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, TemporalPropertyFilterFactory, + PropertyFilterFactory, }, CreateFilter, }, @@ -1655,9 +1655,9 @@ mod test_node_filter { db::{ api::view::{filter_ops::NodeSelect, Filter}, graph::views::filter::model::{ - node_filter::ops::{NodeFilterOps, NodeIdFilterOps}, - ComposableFilter, CompositeNodeFilter, NodeViewFilterOps, TryAsCompositeFilter, - ViewWrapOps, + node_filter::ops::NodeFilterOps, + ComposableFilter, CompositeNodeFilter, NodeFilterFactory, NodeViewFilterOps, + TryAsCompositeFilter, ViewWrapOps, }, }, prelude::{ @@ -1676,7 +1676,7 @@ mod test_node_filter { let nodes = graph .nodes() .after(5) - .select(NodeFilter::node_type().contains("x")) + .select(NodeFilter.node_type().contains("x")) .unwrap(); let degrees = nodes.degree(); let degrees_collected = degrees.compute(); @@ -1685,7 +1685,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_name_eq() { - let filter = NodeFilter::name().eq("3"); + let filter = NodeFilter.name().eq("3"); let expected_results = vec!["3"]; assert_filter_nodes_results( init_nodes_graph, @@ -1705,7 +1705,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_name_ne() { - let filter = NodeFilter::name().ne("2"); + let filter = NodeFilter.name().ne("2"); let expected_results = vec!["1", "3", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -1725,7 +1725,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_name_in() { - let filter = NodeFilter::name().is_in(vec!["1"]); + let filter = NodeFilter.name().is_in(vec!["1"]); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -1742,7 +1742,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::name().is_in(vec![""]); + let filter = NodeFilter.name().is_in(vec![""]); let expected_results = Vec::<&str>::new(); assert_filter_nodes_results( init_nodes_graph, @@ -1759,7 +1759,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::name().is_in(vec!["2", "3"]); + let filter = NodeFilter.name().is_in(vec!["2", "3"]); let expected_results = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -1779,7 +1779,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_name_not_in() { - let filter = NodeFilter::name().is_not_in(vec!["1"]); + let filter = NodeFilter.name().is_not_in(vec!["1"]); let expected_results = vec!["2", "3", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -1796,7 +1796,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::name().is_not_in(vec![""]); + let filter = NodeFilter.name().is_not_in(vec![""]); let expected_results = vec!["1", "2", "3", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -1816,7 +1816,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_eq() { - let filter = NodeFilter::node_type().eq("fire_nation"); + let filter = NodeFilter.node_type().eq("fire_nation"); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -1836,7 +1836,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_ne() { - let filter = NodeFilter::node_type().ne("fire_nation"); + let filter = NodeFilter.node_type().ne("fire_nation"); let expected_results = vec!["2", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -1856,7 +1856,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_in() { - let filter = NodeFilter::node_type().is_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_in(vec!["fire_nation"]); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -1873,7 +1873,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::node_type().is_in(vec!["fire_nation", "air_nomads"]); + let filter = NodeFilter.node_type().is_in(vec!["fire_nation", "air_nomads"]); let expected_results = vec!["1", "2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -1893,7 +1893,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_not_in() { - let filter = NodeFilter::node_type().is_not_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]); let expected_results = vec!["2", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -1913,7 +1913,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_starts_with() { - let filter = NodeFilter::node_type().starts_with("fire"); + let filter = NodeFilter.node_type().starts_with("fire"); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -1930,7 +1930,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::node_type().starts_with("rocket"); + let filter = NodeFilter.node_type().starts_with("rocket"); let expected_results = vec![]; assert_filter_nodes_results( init_nodes_graph, @@ -1950,7 +1950,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_ends_with() { - let filter = NodeFilter::node_type().ends_with("nomads"); + let filter = NodeFilter.node_type().ends_with("nomads"); let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, @@ -1967,7 +1967,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::node_type().ends_with("circle"); + let filter = NodeFilter.node_type().ends_with("circle"); let expected_results = vec![]; assert_filter_nodes_results( init_nodes_graph, @@ -1987,7 +1987,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_contains() { - let filter = NodeFilter::node_type().contains("fire"); + let filter = NodeFilter.node_type().contains("fire"); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2007,7 +2007,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_contains_not() { - let filter = NodeFilter::node_type().not_contains("fire"); + let filter = NodeFilter.node_type().not_contains("fire"); let expected_results = vec!["2", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -2027,7 +2027,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_fuzzy_search() { - let filter = NodeFilter::node_type().fuzzy_search("fire", 2, true); + let filter = NodeFilter.node_type().fuzzy_search("fire", 2, true); let expected_results: Vec<&str> = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2037,7 +2037,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::node_type().fuzzy_search("fire", 2, false); + let filter = NodeFilter.node_type().fuzzy_search("fire", 2, false); let expected_results: Vec<&str> = vec![]; assert_filter_nodes_results( init_nodes_graph, @@ -2047,7 +2047,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::node_type().fuzzy_search("air_noma", 2, false); + let filter = NodeFilter.node_type().fuzzy_search("air_noma", 2, false); let expected_results: Vec<&str> = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, @@ -2060,7 +2060,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_not_node_type() { - let filter = NodeFilter::node_type().is_not_in(vec!["fire_nation"]).not(); + let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]).not(); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2080,7 +2080,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_eq_node_id() { - let filter = NodeFilter::id().eq("1"); + let filter = NodeFilter.id().eq("1"); let expected_results = vec!["1"]; assert_filter_nodes_results( @@ -2098,7 +2098,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::id().eq(1); + let filter = NodeFilter.id().eq(1); let expected_results = vec!["1"]; assert_filter_nodes_results( @@ -2119,7 +2119,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_ne_node_id() { - let filter = NodeFilter::id().ne("1"); + let filter = NodeFilter.id().ne("1"); let expected_results = vec!["2", "3", "4"]; assert_filter_nodes_results( @@ -2137,7 +2137,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::id().ne(1); + let filter = NodeFilter.id().ne(1); let expected_results = vec!["2", "3", "4"]; assert_filter_nodes_results( @@ -2158,7 +2158,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_is_in_node_id() { - let filter = NodeFilter::id().is_in(vec!["1", "3", "6"]); + let filter = NodeFilter.id().is_in(vec!["1", "3", "6"]); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( @@ -2176,7 +2176,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::id().is_in(vec![1, 3, 6]); + let filter = NodeFilter.id().is_in(vec![1, 3, 6]); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( @@ -2197,7 +2197,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_is_not_in_node_id() { - let filter = NodeFilter::id().is_not_in(vec!["1", "3", "6"]); + let filter = NodeFilter.id().is_not_in(vec!["1", "3", "6"]); let expected_results = vec!["2", "4"]; assert_filter_nodes_results( @@ -2215,7 +2215,7 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter::id().is_not_in(vec![1, 3, 6]); + let filter = NodeFilter.id().is_not_in(vec![1, 3, 6]); let expected_results = vec!["2", "4"]; assert_filter_nodes_results( @@ -2236,7 +2236,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_lt_node_id() { - let filter = NodeFilter::id().lt(2); + let filter = NodeFilter.id().lt(2); let expected_results = vec!["1"]; assert_filter_nodes_results( @@ -2257,7 +2257,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_le_node_id() { - let filter = NodeFilter::id().le(3); + let filter = NodeFilter.id().le(3); let expected_results = vec!["1", "2", "3"]; assert_filter_nodes_results( @@ -2278,7 +2278,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_gt_node_id() { - let filter = NodeFilter::id().gt(2); + let filter = NodeFilter.id().gt(2); let expected_results = vec!["3", "4"]; assert_filter_nodes_results( @@ -2299,7 +2299,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_ge_node_id() { - let filter = NodeFilter::id().ge(2); + let filter = NodeFilter.id().ge(2); let expected_results = vec!["2", "3", "4"]; assert_filter_nodes_results( @@ -2320,7 +2320,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_starts_with_node_id() { - let filter = NodeFilter::id().starts_with("France"); + let filter = NodeFilter.id().starts_with("France"); let expected_results = vec!["France Paris"]; assert_filter_nodes_results( init_nodes_graph_with_str_ids, @@ -2333,7 +2333,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_ends_with_node_id() { - let filter = NodeFilter::id().ends_with("wo"); + let filter = NodeFilter.id().ends_with("wo"); let expected_results = vec!["Two"]; assert_filter_nodes_results( init_nodes_graph_with_str_ids, @@ -2353,7 +2353,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_contains_node_id() { - let filter = NodeFilter::id().contains("o"); + let filter = NodeFilter.id().contains("o"); let expected_results = vec!["London", "Tokyo", "Two"]; assert_filter_nodes_results( init_nodes_graph_with_str_ids, @@ -2366,7 +2366,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_not_contains_node_id() { - let filter = NodeFilter::id().not_contains("o"); + let filter = NodeFilter.id().not_contains("o"); let expected_results = vec!["France Paris"]; assert_filter_nodes_results( init_nodes_graph_with_str_ids, @@ -2379,7 +2379,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_is_in_node_id_str() { - let filter = NodeFilter::id().is_in(vec!["London", "Tokyo"]); + let filter = NodeFilter.id().is_in(vec!["London", "Tokyo"]); let expected_results = vec!["London", "Tokyo"]; assert_filter_nodes_results( init_nodes_graph_with_str_ids, @@ -2399,7 +2399,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_is_not_in_node_id_str() { - let filter = NodeFilter::id().is_not_in(vec!["London", "Tokyo"]); + let filter = NodeFilter.id().is_not_in(vec!["London", "Tokyo"]); let expected_results = vec!["France Paris", "Two"]; assert_filter_nodes_results( init_nodes_graph_with_str_ids, @@ -2526,7 +2526,7 @@ mod test_node_property_filter { not_filter::NotFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, windowed_filter::Windowed, - ComposableFilter, PropertyFilterFactory, TemporalPropertyFilterFactory, ViewWrapOps, + ComposableFilter, PropertyFilterFactory, ViewWrapOps, }; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ @@ -4593,7 +4593,7 @@ mod test_node_composite_filter { use raphtory::{ db::graph::views::filter::model::{ node_filter::ops::NodeFilterOps, property_filter::ops::PropertyFilterOps, - ComposableFilter, PropertyFilterFactory, TryAsCompositeFilter, + ComposableFilter, NodeFilterFactory, PropertyFilterFactory, TryAsCompositeFilter, }, prelude::NodeFilter, }; @@ -4761,7 +4761,7 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = NodeFilter::node_type() + let filter = NodeFilter.node_type() .eq("fire_nation") .and(NodeFilter.property("p1").eq("prop1")); let expected_results = Vec::<&str>::new(); @@ -4830,7 +4830,7 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = NodeFilter::node_type() + let filter = NodeFilter.node_type() .eq("fire_nation") .and(NodeFilter.property("p1").eq("shivam_kapoor")); let expected_results = vec!["1"]; @@ -4864,7 +4864,7 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = NodeFilter::name() + let filter = NodeFilter.name() .eq("2") .and(NodeFilter.property("p2").eq(2u64)); let expected_results = vec!["2"]; @@ -4898,7 +4898,7 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = NodeFilter::name() + let filter = NodeFilter.name() .eq("2") .and(NodeFilter.property("p2").eq(2u64)) .or(NodeFilter.property("p9").eq(5u64)); @@ -4936,7 +4936,7 @@ mod test_node_composite_filter { #[test] fn test_not_composite_filter_nodes() { - let filter = NodeFilter::name() + let filter = NodeFilter.name() .eq("2") .and(NodeFilter.property("p2").eq(2u64)) .or(NodeFilter.property("p9").eq(5u64)) @@ -4957,7 +4957,7 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = NodeFilter::name() + let filter = NodeFilter.name() .eq("2") .not() .and(NodeFilter.property("p2").eq(2u64)) @@ -4981,7 +4981,7 @@ mod test_node_composite_filter { #[test] fn test_out_neighbours_filter() { - let filter = NodeFilter::name() + let filter = NodeFilter.name() .eq("2") .and(NodeFilter.property("p2").eq(2u64)); let expected_results = vec!["2"]; @@ -5036,7 +5036,7 @@ mod test_node_property_filter_agg { model::{ node_filter::NodeFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - PropertyFilterFactory, TemporalPropertyFilterFactory, TryAsCompositeFilter, + PropertyFilterFactory, TryAsCompositeFilter, }, CreateFilter, }, @@ -8563,9 +8563,9 @@ mod test_edge_filter { }; use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, - node_filter::ops::{NodeFilterOps, NodeIdFilterOps}, + node_filter::ops::NodeFilterOps, property_filter::ops::{ListAggOps, PropertyFilterOps}, - ComposableFilter, EdgeViewFilterOps, PropertyFilterFactory, TemporalPropertyFilterFactory, + ComposableFilter, EdgeViewFilterOps, NodeFilterFactory, PropertyFilterFactory, ViewWrapOps, }; use raphtory_tests::assertions::{ @@ -10043,7 +10043,7 @@ mod test_edge_property_filter { use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - ComposableFilter, PropertyFilterFactory, TemporalPropertyFilterFactory, ViewWrapOps, + ComposableFilter, PropertyFilterFactory, ViewWrapOps, }; use raphtory_api::core::entities::properties::prop::Prop; @@ -11885,8 +11885,8 @@ mod test_edge_property_filter { mod test_edge_composite_filter { use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, - property_filter::ops::PropertyFilterOps, ComposableFilter, PropertyFilterFactory, - TryAsCompositeFilter, + property_filter::ops::PropertyFilterOps, ComposableFilter, NodeFilterFactory, + PropertyFilterFactory, TryAsCompositeFilter, }; use raphtory_tests::assertions::{ assert_filter_edges_results, assert_search_edges_results, TestGraphVariants, TestVariants, diff --git a/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs new file mode 100644 index 0000000000..90e5e6d9b9 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs @@ -0,0 +1,131 @@ +use crate::{ + core::entities::LayerIds, + db::{ + api::{ + properties::internal::{ + InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, + }, + view::internal::{ + Immutable, InheritEdgeHistoryFilter, InheritEdgeLayerFilterOps, + InheritExplodedEdgeFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, + InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, + InheritTimeSemantics, InternalEdgeFilterOps, Static, + }, + }, + graph::views::filter::model::edge_expr::EdgeOp, + }, + prelude::GraphViewOps, +}; +use raphtory_api::{ + core::entities::edges::edge_ref::EdgeRef, + inherit::Base, +}; +use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_storage_ops::EdgeStorageOps}; +use storage::EdgeEntryRef; + +/// Edge-filtered graph: hides edges that fail the predicate `filter`. +/// +/// Parallel to `NodeFilteredGraph` but for edges: `internal_filter_edge` evaluates +/// `filter.apply(storage, edge_ref)` in O(1) after a single compile step. +#[derive(Clone)] +pub struct EdgeExprFilteredGraph { + pub(crate) graph: G, + pub(crate) filter: F, +} + +impl EdgeExprFilteredGraph { + pub fn new(graph: G, filter: F) -> Self { + Self { graph, filter } + } +} + +impl Base for EdgeExprFilteredGraph { + type Base = G; + + fn base(&self) -> &Self::Base { + &self.graph + } +} + +impl Static for EdgeExprFilteredGraph {} +impl Immutable for EdgeExprFilteredGraph {} + +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritCoreGraphOps + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritStorageOps + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritLayerOps + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritListOps + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritMaterialize + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritNodeFilterOps + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritPropertiesOps + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> + InheritNodePropertySchemaOps for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> + InheritEdgePropertySchemaOps for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritTimeSemantics + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritNodeHistoryFilter + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgeHistoryFilter + for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> + InheritExplodedEdgeFilterOps for EdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgeLayerFilterOps + for EdgeExprFilteredGraph +{ +} + +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InternalEdgeFilterOps + for EdgeExprFilteredGraph +{ + #[inline] + fn internal_edge_filtered(&self) -> bool { + true + } + + #[inline] + fn internal_edge_list_trusted(&self) -> bool { + false + } + + #[inline] + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { + if !self.graph.internal_filter_edge(edge, layer_ids) { + return false; + } + let edge_ref: EdgeRef = edge.out_ref(); + self.filter.apply(self.graph.core_graph(), edge_ref) + } +} diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 3fd683d565..57dc6b9f55 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -11,6 +11,7 @@ use crate::{ }; pub mod and_filtered_graph; +pub mod edge_expr_filtered_graph; pub mod edge_node_filtered_graph; pub mod edge_property_filtered_graph; pub mod exploded_edge_node_filtered_graph; diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs new file mode 100644 index 0000000000..c291ca073a --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs @@ -0,0 +1,274 @@ +//! EdgeExpr impls for the shared Property/Metadata structs and scalar types. + +use super::{ + ops::{ + AvgEdgeOp, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, NestedMapEdgeOp, + SumEdgeOp, TemporalEdgePropOp, UnwrapOptPropEdgeOp, + }, + EdgeExpr, EdgeMetaOp, EdgeOp, EdgePropOp, +}; +use crate::db::graph::views::filter::model::property_filter::Op; +use crate::{ + db::api::{state::ops::Const, view::internal::GraphView}, + db::graph::views::filter::model::{Metadata, Property}, + errors::GraphError, +}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::sync::Arc; + +// ───────────────────────────────────────────────────────────────────────────── +// Property / Metadata — EdgeExpr impls +// ───────────────────────────────────────────────────────────────────────────── + +impl EdgeExpr for Property { + type Output = Option; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let (prop_id, _) = graph + .edge_meta() + .get_prop_id_and_type(&self.name, false) + .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; + Ok(Arc::new(EdgePropOp { graph, prop_id })) + } +} + +impl EdgeExpr for Metadata { + type Output = Option; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let (prop_id, _) = graph + .edge_meta() + .get_prop_id_and_type(&self.name, true) + .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; + Ok(Arc::new(EdgeMetaOp { graph, prop_id })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// IntoPropEdgeExpr — normalises RHS values to EdgeExpr> +// ───────────────────────────────────────────────────────────────────────────── + +pub trait IntoPropEdgeExpr { + type Expr: EdgeExpr>; + fn into_prop_edge_expr(self) -> Self::Expr; +} + +impl>> IntoPropEdgeExpr for T { + type Expr = T; + fn into_prop_edge_expr(self) -> T { + self + } +} + + +// ───────────────────────────────────────────────────────────────────────────── +// Scalar EdgeExpr impls — literal RHS values +// ───────────────────────────────────────────────────────────────────────────── + +impl EdgeExpr for Prop { + type Output = Option; + + fn create_edge_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_edge_expr_for_numeric { + ($prim:ty, $variant:ident) => { + impl EdgeExpr for $prim { + type Output = Option; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(Prop::$variant(*self))))) + } + } + }; +} + +impl_edge_expr_for_numeric!(i32, I32); +impl_edge_expr_for_numeric!(i64, I64); +impl_edge_expr_for_numeric!(u32, U32); +impl_edge_expr_for_numeric!(u64, U64); +impl_edge_expr_for_numeric!(f32, F32); +impl_edge_expr_for_numeric!(f64, F64); +impl_edge_expr_for_numeric!(bool, Bool); +impl_edge_expr_for_numeric!(u8, U8); +impl_edge_expr_for_numeric!(u16, U16); + +impl EdgeExpr for &'static str { + type Output = Option; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(Prop::Str( + raphtory_api::core::storage::arc_str::ArcStr::from(*self), + ))))) + } + + fn prop_type(&self) -> PropType { + PropType::Str + } +} + +impl EdgeExpr for usize { + type Output = usize; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(*self))) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalEdgePropExpr — all temporal values of a property in the view window +// ───────────────────────────────────────────────────────────────────────────── + +/// Parallel to `TemporalPropertyExpr` but for edges: reads from `edge_meta()`. +/// +/// Produced by `TemporalProp` internal conversion when `E: EdgeFilterFactory`. +/// Returns `Prop::List` of all temporal values within the view window. +#[derive(Clone)] +pub struct TemporalEdgePropExpr { + pub(crate) view_expr: E, + pub(crate) name: String, +} + +impl TemporalEdgePropExpr { + pub fn new(view_expr: E, name: impl Into) -> Self { + Self { view_expr, name: name.into() } + } +} + +impl + EdgeExpr for TemporalEdgePropExpr +{ + type Output = Prop; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError> { + let (prop_id, _) = graph + .edge_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(TemporalEdgePropOp { graph, prop_id })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Aggregator EdgeExprs — reduce TemporalEdgePropExpr output to a scalar +// ───────────────────────────────────────────────────────────────────────────── + +macro_rules! impl_agg_edge_expr { + ($name:ident, $op_ty:ident) => { + /// Produced by `EdgeTemporalProp::$op_lower()` — wraps a `TemporalEdgePropExpr`. + #[derive(Clone)] + pub struct $name>(pub E); + + impl> EdgeExpr for $name { + type Output = Option; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.0.create_edge_op(graph)?; + Ok(Arc::new($op_ty { inner })) + } + } + }; +} + +impl_agg_edge_expr!(SumEdgeExpr, SumEdgeOp); +impl_agg_edge_expr!(AvgEdgeExpr, AvgEdgeOp); +impl_agg_edge_expr!(MinEdgeExpr, MinEdgeOp); +impl_agg_edge_expr!(MaxEdgeExpr, MaxEdgeOp); +impl_agg_edge_expr!(FirstEdgeExpr, FirstEdgeOp); +impl_agg_edge_expr!(LastEdgeExpr, LastEdgeOp); +// LenEdgeExpr written explicitly: Output = usize, not Option +#[derive(Clone)] +pub struct LenEdgeExpr>(pub E); + +impl> EdgeExpr for LenEdgeExpr { + type Output = usize; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError> { + let inner = self.0.create_edge_op(graph)?; + Ok(Arc::new(LenEdgeOp { inner })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// UnwrapOptPropEdgeExpr — bridges Option → Prop for nested aggregation +// ───────────────────────────────────────────────────────────────────────────── + +/// Bridges `E: EdgeExpr>` to `EdgeExpr`, +/// enabling aggregator exprs to operate on values produced by a prior aggregation. +/// +/// Used when chaining e.g. `.temporal().last().sum()`: +/// `last()` produces `EdgeAggregated>` with `Output = Option`; +/// `sum()` wraps in `SumEdgeExpr>>`. +#[derive(Clone)] +pub struct UnwrapOptPropEdgeExpr>>(pub E); + +impl>> EdgeExpr for UnwrapOptPropEdgeExpr { + type Output = Prop; + + fn create_edge_op<'g, G: crate::db::api::view::internal::GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, crate::errors::GraphError> { + let inner = self.0.create_edge_op(graph)?; + Ok(std::sync::Arc::new(UnwrapOptPropEdgeOp { inner })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// NestedMapEdgeExpr — per-element aggregation / quantification on a Prop::List +// ───────────────────────────────────────────────────────────────────────────── + +/// Applies a per-element op to each element of a `Prop::List` produced by `E`. +/// +/// Used for chained expressions like `.temporal().any().sum()`. +#[derive(Clone)] +pub struct NestedMapEdgeExpr> { + pub inner: E, + pub op: Op, +} + +impl> EdgeExpr for NestedMapEdgeExpr { + type Output = Prop; + + fn create_edge_op<'g, G: crate::db::api::view::internal::GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, crate::errors::GraphError> { + let inner = self.inner.create_edge_op(graph)?; + Ok(std::sync::Arc::new(NestedMapEdgeOp { inner, op: self.op })) + } +} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs new file mode 100644 index 0000000000..40e16888fe --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -0,0 +1,894 @@ +//! Filter types for edge expressions — bridge from EdgeExpr to a filtered graph. +//! +//! Parallel to `node_expr/filters.rs` but for edges: `create_filter` produces +//! an `EdgeExprFilteredGraph` instead of a `NodeFilteredGraph`. + +use super::{ + ops::{ + AllEdgeOp, AllPropEdgeOp, AnyEdgeOp, AnyPropEdgeOp, BinaryCmpEdgeOp, PropListEdgeCmpOp, + PropListInSetEdgeOp, PropListStringEdgeOp, PropValueSetEdgeOp, SetEdgeOp, StringEdgeOp, + UnaryEdgeOp, + }, + EdgeExpr, EdgeOp, +}; +use raphtory_api::core::storage::arc_str::ArcStr; +use crate::{ + db::{ + api::{state::ops::NotANodeFilter, view::internal::GraphView}, + graph::views::filter::{ + edge_expr_filtered_graph::EdgeExprFilteredGraph, + model::{ + edge_filter::CompositeEdgeFilter, + filter_operator::{ + BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp, + }, + ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, + TryAsCompositeFilter, + }, + }, + }, + errors::GraphError, + prelude::GraphViewOps, +}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; + +// ───────────────────────────────────────────────────────────────────────────── +// validate helpers +// ───────────────────────────────────────────────────────────────────────────── + +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(()) +} + +// ───────────────────────────────────────────────────────────────────────────── +// BinaryCmpEdgeFilter +// ───────────────────────────────────────────────────────────────────────────── + +pub struct BinaryCmpEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, +{ + pub left: L, + pub op: BinaryOp, + pub right: R, +} + +impl BinaryCmpEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, +{ + pub fn new(left: L, op: BinaryOp, right: R) -> Self { + Self { left, op, right } + } +} + +impl Clone for BinaryCmpEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, +{ + fn clone(&self) -> Self { + Self { + left: self.left.clone(), + op: self.op, + right: self.right.clone(), + } + } +} + +impl ComposableFilter for BinaryCmpEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, +{ +} + +impl TryAsCompositeFilter for BinaryCmpEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, +{ + 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 CreateFilter for BinaryCmpEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, + L::Output: Comparable, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph.clone())?; + validate_binary_op(&self.op, &left.prop_type())?; + let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { + left, + right, + op: self.op, + }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// UnaryEdgeFilter +// ───────────────────────────────────────────────────────────────────────────── + +pub struct UnaryEdgeFilter +where + E: EdgeExpr>, + I: Clone + Send + Sync + 'static, +{ + pub expr: E, + pub op: UnaryOp, + pub(crate) _phantom: PhantomData, +} + +impl Clone for UnaryEdgeFilter +where + E: EdgeExpr>, + I: Clone + Send + Sync + 'static, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + op: self.op, + _phantom: PhantomData, + } + } +} + +impl ComposableFilter for UnaryEdgeFilter +where + E: EdgeExpr>, + I: Clone + Send + Sync + 'static, +{ +} + +impl TryAsCompositeFilter for UnaryEdgeFilter +where + E: EdgeExpr>, + 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) + } +} + +impl CreateFilter for UnaryEdgeFilter +where + E: EdgeExpr>, + I: Clone + Send + Sync + 'static, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let inner = self.expr.create_edge_op(graph.clone())?; + let op: Arc + 'graph> = + Arc::new(UnaryEdgeOp { inner, op: self.op }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// QuantifiedEdgeFilter +// ───────────────────────────────────────────────────────────────────────────── + +pub struct QuantifiedEdgeFilter +where + E: EdgeExpr, + Q: super::super::node_expr::QuantifierMode, + R: EdgeExpr>, +{ + pub expr: E, + pub op: BinaryOp, + pub rhs: R, + pub(crate) _q: PhantomData, +} + +impl QuantifiedEdgeFilter +where + E: EdgeExpr, + Q: super::super::node_expr::QuantifierMode, + R: EdgeExpr>, +{ + pub fn new(expr: E, op: BinaryOp, rhs: R) -> Self { + Self { expr, op, rhs, _q: PhantomData } + } +} + +impl Clone for QuantifiedEdgeFilter +where + E: EdgeExpr, + Q: super::super::node_expr::QuantifierMode, + R: EdgeExpr>, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + op: self.op, + rhs: self.rhs.clone(), + _q: PhantomData, + } + } +} + +impl ComposableFilter for QuantifiedEdgeFilter +where + E: EdgeExpr, + Q: super::super::node_expr::QuantifierMode, + R: EdgeExpr>, +{ +} + +impl TryAsCompositeFilter for QuantifiedEdgeFilter +where + E: EdgeExpr, + Q: super::super::node_expr::QuantifierMode, + R: EdgeExpr>, +{ + 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) + } +} + +use super::super::node_expr::{AnyMode, AllMode}; + +impl CreateFilter for QuantifiedEdgeFilter +where + E: EdgeExpr, + R: EdgeExpr>, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let temporal_op = self.expr.create_edge_op(graph.clone())?; + let rhs = self.rhs.create_edge_op(graph.clone())?; + let list_cmp: Arc + 'graph> = + Arc::new(PropListEdgeCmpOp { temporal_op, rhs, cmp_op: self.op, any: true }); + let op: Arc + 'graph> = + Arc::new(AnyEdgeOp { inner: list_cmp }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + +impl CreateFilter for QuantifiedEdgeFilter +where + E: EdgeExpr, + R: EdgeExpr>, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let temporal_op = self.expr.create_edge_op(graph.clone())?; + let rhs = self.rhs.create_edge_op(graph.clone())?; + let list_cmp: Arc + 'graph> = + Arc::new(PropListEdgeCmpOp { temporal_op, rhs, cmp_op: self.op, any: false }); + let op: Arc + 'graph> = + Arc::new(AllEdgeOp { inner: list_cmp }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// validate_string_op — reject non-string prop types at compile time +// ───────────────────────────────────────────────────────────────────────────── + +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(()) +} + +// ───────────────────────────────────────────────────────────────────────────── +// StringEdgeFilter — string expression filter for edges +// ───────────────────────────────────────────────────────────────────────────── + +pub struct StringEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, + L::Output: StringComparable, +{ + pub left: L, + pub op: StringOp, + pub right: R, +} + +impl StringEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, + L::Output: StringComparable, +{ + pub fn new(left: L, op: StringOp, right: R) -> Self { + Self { left, op, right } + } +} + +impl Clone for StringEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, + L::Output: StringComparable, +{ + fn clone(&self) -> Self { + Self { left: self.left.clone(), op: self.op, right: self.right.clone() } + } +} + +impl ComposableFilter for StringEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, + L::Output: StringComparable, +{ +} + +impl TryAsCompositeFilter for StringEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, + 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) + } +} + +impl CreateFilter for StringEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, + L::Output: StringComparable, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph.clone())?; + validate_string_op(&left.prop_type())?; + let op: Arc + 'graph> = + Arc::new(StringEdgeOp { left, right, op: self.op }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// PropValueSetEdgeFilter — is_in / is_not_in for Option (linear scan) +// ───────────────────────────────────────────────────────────────────────────── + +pub struct PropValueSetEdgeFilter>> { + pub(crate) expr: E, + pub(crate) values: Vec, + pub(crate) op: SetOp, +} + +impl>> Clone for PropValueSetEdgeFilter { + fn clone(&self) -> Self { + Self { expr: self.expr.clone(), values: self.values.clone(), op: self.op } + } +} + +impl>> ComposableFilter for PropValueSetEdgeFilter {} + +impl>> TryAsCompositeFilter for PropValueSetEdgeFilter { + 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>> CreateFilter for PropValueSetEdgeFilter { + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let inner = self.expr.create_edge_op(graph.clone())?; + let op: Arc + 'graph> = + Arc::new(PropValueSetEdgeOp { inner, values: self.values, op: self.op }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// SetEdgeFilter — is_in / is_not_in for Option (HashSet, O(1)) +// ───────────────────────────────────────────────────────────────────────────── + +pub struct SetEdgeFilter +where + E: EdgeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, +{ + pub expr: E, + pub op: SetOp, + pub values: Arc>, + pub(crate) _phantom: PhantomData, +} + +impl Clone for SetEdgeFilter +where + E: EdgeExpr>, + 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 SetEdgeFilter +where + E: EdgeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, +{} + +impl TryAsCompositeFilter for SetEdgeFilter +where + E: EdgeExpr>, + 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) + } +} + +impl CreateFilter for SetEdgeFilter +where + E: EdgeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let inner = self.expr.create_edge_op(graph.clone())?; + let op: Arc + 'graph> = + Arc::new(SetEdgeOp { inner, values: self.values, op: self.op }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + +use super::super::node_expr::{AnyMode as AnyM, AllMode as AllM, QuantifierMode}; + +// ───────────────────────────────────────────────────────────────────────────── +// QuantifiedIsInEdgeFilter — quantified set-membership filter for edges +// ───────────────────────────────────────────────────────────────────────────── + +pub struct QuantifiedIsInEdgeFilter +where + E: EdgeExpr, + Q: QuantifierMode, +{ + pub(crate) expr: E, + pub(crate) values: Vec, + pub(crate) op: SetOp, + pub(crate) _q: PhantomData, +} + +impl Clone for QuantifiedIsInEdgeFilter +where + E: EdgeExpr, + Q: QuantifierMode, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + values: self.values.clone(), + op: self.op, + _q: PhantomData, + } + } +} + +impl ComposableFilter for QuantifiedIsInEdgeFilter +where + E: EdgeExpr, + Q: QuantifierMode, +{ +} + +impl TryAsCompositeFilter for QuantifiedIsInEdgeFilter +where + E: EdgeExpr, + Q: QuantifierMode, +{ + 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 CreateFilter for QuantifiedIsInEdgeFilter +where + E: EdgeExpr, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let inner: Arc + 'graph> = Arc::new(PropListInSetEdgeOp { + inner: self.expr.create_edge_op(graph.clone())?, + values: self.values, + op: self.op, + }); + let op: Arc + 'graph> = Arc::new(AnyPropEdgeOp { inner }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result { + Err(GraphError::NotNodeFilter) + } +} + +impl CreateFilter for QuantifiedIsInEdgeFilter +where + E: EdgeExpr, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let inner: Arc + 'graph> = Arc::new(PropListInSetEdgeOp { + inner: self.expr.create_edge_op(graph.clone())?, + values: self.values, + op: self.op, + }); + let op: Arc + 'graph> = Arc::new(AllPropEdgeOp { inner }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result { + Err(GraphError::NotNodeFilter) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// QuantifiedStringEdgeFilter — quantified string-comparison filter for edges +// ───────────────────────────────────────────────────────────────────────────── + +pub struct QuantifiedStringEdgeFilter +where + E: EdgeExpr, + Q: QuantifierMode, +{ + pub(crate) expr: E, + pub(crate) rhs: ArcStr, + pub(crate) op: StringOp, + pub(crate) _q: PhantomData, +} + +impl Clone for QuantifiedStringEdgeFilter +where + E: EdgeExpr, + Q: QuantifierMode, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + rhs: self.rhs.clone(), + op: self.op, + _q: PhantomData, + } + } +} + +impl ComposableFilter for QuantifiedStringEdgeFilter +where + E: EdgeExpr, + Q: QuantifierMode, +{ +} + +impl TryAsCompositeFilter for QuantifiedStringEdgeFilter +where + E: EdgeExpr, + Q: QuantifierMode, +{ + 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 CreateFilter for QuantifiedStringEdgeFilter +where + E: EdgeExpr, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let inner: Arc + 'graph> = Arc::new(PropListStringEdgeOp { + inner: self.expr.create_edge_op(graph.clone())?, + rhs: self.rhs, + op: self.op, + }); + let op: Arc + 'graph> = Arc::new(AnyPropEdgeOp { inner }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result { + Err(GraphError::NotNodeFilter) + } +} + +impl CreateFilter for QuantifiedStringEdgeFilter +where + E: EdgeExpr, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let inner: Arc + 'graph> = Arc::new(PropListStringEdgeOp { + inner: self.expr.create_edge_op(graph.clone())?, + rhs: self.rhs, + op: self.op, + }); + let op: Arc + 'graph> = Arc::new(AllPropEdgeOp { inner }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result { + Err(GraphError::NotNodeFilter) + } +} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs new file mode 100644 index 0000000000..baa5d3132a --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -0,0 +1,533 @@ +//! Edge expressions — what value an edge can produce. +//! +//! Mirrors [`node_expr`] exactly, but the subject is an edge rather than a node. +//! +//! # Two-phase pipeline (same as node_expr) +//! +//! ```text +//! ┌─ Build phase (pure data, no graph) ──────────────────────┐ +//! │ EdgeFilter::property("weight") ← EdgePropertyExpr │ +//! │ .eq(5.0f64) ← BinaryCmpEdgeFilter │ +//! └──────────────────────────────────────────────────────────┘ +//! │ create_edge_op(graph)? ← resolve name → prop_id +//! ▼ +//! ┌─ Compile phase (graph-bound op) ─────────────────────────┐ +//! │ EdgePropOp { graph, prop_id } ← EdgeOp │ +//! │ apply(storage, edge_ref) │ +//! │ → edge_ref reads column prop_id in O(1) │ +//! └──────────────────────────────────────────────────────────┘ +//! ``` + +use crate::{ + db::{ + api::view::internal::GraphView, + graph::views::filter::model::{ + filter_operator::{BinaryOp, SetOp, StringComparable, StringOp, UnaryOp}, + node_expr::{AllMode, AnyMode, QuantifierMode}, + node_expr::filters::TemporalProp, + property_filter::Op, + CreateView, EdgeFilterFactory, MetadataExpr, PropertyExpr, + }, + }, + errors::GraphError, +}; +use raphtory_api::core::{ + entities::{ + edges::edge_ref::EdgeRef, + properties::prop::{Prop, PropType}, + }, + storage::arc_str::ArcStr, +}; +use raphtory_storage::graph::graph::GraphStorage; +use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; + +pub mod exprs; +pub mod filters; +pub mod ops; + +pub use exprs::*; +pub use filters::*; +pub use ops::*; +pub use super::{Metadata, Property}; + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeOp — compiled evaluator: EdgeRef → typed value +// ───────────────────────────────────────────────────────────────────────────── + +/// A compiled edge evaluator: given an [`EdgeRef`], returns a typed value. +/// +/// Parallel to [`NodeOp`] — same contract but the subject is an edge. +pub trait EdgeOp: Send + Sync { + type Output: Clone + Send + Sync; + + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Self::Output; + + fn prop_type(&self) -> PropType { + PropType::Empty + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeExpr — typed expression describing what to compute per edge +// ───────────────────────────────────────────────────────────────────────────── + +/// A typed expression that produces a value per edge. +/// +/// Parallel to [`NodeExpr`] — same two-phase design. +pub trait EdgeExpr: Clone + Send + Sync + 'static { + type Output: Clone + Send + Sync + 'static; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError>; + + fn prop_type(&self) -> PropType { + PropType::Empty + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeAggregated — builder that produces BinaryCmpEdgeFilter +// ───────────────────────────────────────────────────────────────────────────── + +/// Returned by `EdgeTemporalProp::sum()`, `.first()`, etc. +/// Calling `.eq()` etc. produces a `BinaryCmpEdgeFilter`. +pub struct EdgeAggregated { + pub(crate) expr: E, +} + +impl>> EdgeAggregated { + fn finish( + self, + op: crate::db::graph::views::filter::model::filter_operator::BinaryOp, + rhs: R, + ) -> BinaryCmpEdgeFilter { + BinaryCmpEdgeFilter::new(self.expr, op, rhs.into_prop_edge_expr()) + } + + pub fn eq(self, rhs: R) -> BinaryCmpEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Eq, rhs) + } + pub fn ne(self, rhs: R) -> BinaryCmpEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Ne, rhs) + } + pub fn gt(self, rhs: R) -> BinaryCmpEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Gt, rhs) + } + pub fn ge(self, rhs: R) -> BinaryCmpEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Ge, rhs) + } + pub fn lt(self, rhs: R) -> BinaryCmpEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Lt, rhs) + } + pub fn le(self, rhs: R) -> BinaryCmpEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Le, rhs) + } + pub fn is_some(self) -> UnaryEdgeFilter { + UnaryEdgeFilter { + expr: self.expr, + op: crate::db::graph::views::filter::model::filter_operator::UnaryOp::IsSome, + _phantom: std::marker::PhantomData, + } + } + pub fn is_none(self) -> UnaryEdgeFilter { + UnaryEdgeFilter { + expr: self.expr, + op: crate::db::graph::views::filter::model::filter_operator::UnaryOp::IsNone, + _phantom: std::marker::PhantomData, + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeQuantified — builder that produces QuantifiedEdgeFilter +// ───────────────────────────────────────────────────────────────────────────── + +/// Returned by `EdgeTemporalProp::any()` / `all()`. +/// Calling `.eq()` etc. produces a `QuantifiedEdgeFilter`. +pub struct EdgeQuantified +where + E: EdgeExpr, + Q: QuantifierMode, +{ + pub(crate) expr: E, + pub(crate) _q: std::marker::PhantomData, +} + +impl EdgeQuantified +where + E: EdgeExpr, + Q: QuantifierMode, +{ + fn finish( + self, + op: crate::db::graph::views::filter::model::filter_operator::BinaryOp, + rhs: R, + ) -> QuantifiedEdgeFilter { + QuantifiedEdgeFilter::new(self.expr, op, rhs.into_prop_edge_expr()) + } + + pub fn eq(self, rhs: R) -> QuantifiedEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Eq, rhs) + } + pub fn ne(self, rhs: R) -> QuantifiedEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Ne, rhs) + } + pub fn gt(self, rhs: R) -> QuantifiedEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Gt, rhs) + } + pub fn ge(self, rhs: R) -> QuantifiedEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Ge, rhs) + } + pub fn lt(self, rhs: R) -> QuantifiedEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Lt, rhs) + } + pub fn le(self, rhs: R) -> QuantifiedEdgeFilter { + self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Le, rhs) + } + + pub fn is_in(self, values: impl IntoIterator) -> QuantifiedIsInEdgeFilter { + QuantifiedIsInEdgeFilter { + expr: self.expr, + values: values.into_iter().collect(), + op: SetOp::IsIn, + _q: PhantomData, + } + } + + pub fn is_not_in(self, values: impl IntoIterator) -> QuantifiedIsInEdgeFilter { + QuantifiedIsInEdgeFilter { + expr: self.expr, + values: values.into_iter().collect(), + op: SetOp::IsNotIn, + _q: PhantomData, + } + } + + fn string_finish(self, op: StringOp, rhs: &str) -> QuantifiedStringEdgeFilter { + QuantifiedStringEdgeFilter { + expr: self.expr, + rhs: ArcStr::from(rhs), + op, + _q: PhantomData, + } + } + + pub fn starts_with(self, rhs: &str) -> QuantifiedStringEdgeFilter { + self.string_finish(StringOp::StartsWith, rhs) + } + + pub fn ends_with(self, rhs: &str) -> QuantifiedStringEdgeFilter { + self.string_finish(StringOp::EndsWith, rhs) + } + + pub fn contains(self, rhs: &str) -> QuantifiedStringEdgeFilter { + self.string_finish(StringOp::Contains, rhs) + } + + pub fn not_contains(self, rhs: &str) -> QuantifiedStringEdgeFilter { + self.string_finish(StringOp::NotContains, rhs) + } + + pub fn sum(self) -> EdgeQuantified, Q> { + EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Sum }, _q: PhantomData } + } + pub fn avg(self) -> EdgeQuantified, Q> { + EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Avg }, _q: PhantomData } + } + pub fn min(self) -> EdgeQuantified, Q> { + EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Min }, _q: PhantomData } + } + pub fn max(self) -> EdgeQuantified, Q> { + EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Max }, _q: PhantomData } + } + pub fn first(self) -> EdgeQuantified, Q> { + EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::First }, _q: PhantomData } + } + pub fn last(self) -> EdgeQuantified, Q> { + EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Last }, _q: PhantomData } + } + pub fn len(self) -> EdgeQuantified, Q> { + EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Len }, _q: PhantomData } + } + pub fn any(self) -> EdgeQuantified, AnyMode> { + EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Any }, _q: PhantomData } + } + pub fn all(self) -> EdgeQuantified, AllMode> { + EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::All }, _q: PhantomData } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// EdgePropertyExprOps — fluent comparison API for edge-side property expressions +// ───────────────────────────────────────────────────────────────────────────── + +pub trait EdgePropertyExprOps: EdgeExpr> + Sized { + fn is_some(self) -> UnaryEdgeFilter { + UnaryEdgeFilter { expr: self, op: UnaryOp::IsSome, _phantom: PhantomData } + } + fn is_none(self) -> UnaryEdgeFilter { + UnaryEdgeFilter { expr: self, op: UnaryOp::IsNone, _phantom: PhantomData } + } + fn is_in( + self, + values: impl IntoIterator, + ) -> PropValueSetEdgeFilter { + PropValueSetEdgeFilter { + expr: self, + values: values.into_iter().collect(), + op: SetOp::IsIn, + } + } + fn is_not_in( + self, + values: impl IntoIterator, + ) -> PropValueSetEdgeFilter { + PropValueSetEdgeFilter { + expr: self, + values: values.into_iter().collect(), + op: SetOp::IsNotIn, + } + } + fn is_true(self) -> BinaryCmpEdgeFilter { + BinaryCmpEdgeFilter::new(self, BinaryOp::Eq, Prop::Bool(true)) + } + fn is_false(self) -> BinaryCmpEdgeFilter { + BinaryCmpEdgeFilter::new(self, BinaryOp::Eq, Prop::Bool(false)) + } +} + +impl EdgePropertyExprOps + for PropertyExpr +{ +} + +impl EdgePropertyExprOps + for MetadataExpr +{ +} + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeTemporalPropOps — fluent temporal API for edge-side TemporalProp +// ───────────────────────────────────────────────────────────────────────────── + +pub trait EdgeTemporalPropOps: Sized { + type ViewExpr: CreateView + EdgeFilterFactory + Clone + Send + Sync + 'static; + fn into_temporal_parts(self) -> (Self::ViewExpr, String); + + fn any(self) -> EdgeQuantified, AnyMode> { + let (view_expr, name) = self.into_temporal_parts(); + EdgeQuantified { expr: TemporalEdgePropExpr::new(view_expr, name), _q: PhantomData } + } + fn all(self) -> EdgeQuantified, AllMode> { + let (view_expr, name) = self.into_temporal_parts(); + EdgeQuantified { expr: TemporalEdgePropExpr::new(view_expr, name), _q: PhantomData } + } + fn sum(self) -> EdgeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + EdgeAggregated { expr: SumEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + } + fn avg(self) -> EdgeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + EdgeAggregated { expr: AvgEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + } + fn min(self) -> EdgeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + EdgeAggregated { expr: MinEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + } + fn max(self) -> EdgeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + EdgeAggregated { expr: MaxEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + } + fn first(self) -> EdgeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + EdgeAggregated { expr: FirstEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + } + fn last(self) -> EdgeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + EdgeAggregated { expr: LastEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + } + fn len(self) -> LenEdgeExpr> { + let (view_expr, name) = self.into_temporal_parts(); + LenEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) + } +} + +impl EdgeTemporalPropOps + for TemporalProp +{ + type ViewExpr = E; + fn into_temporal_parts(self) -> (E, String) { + (self.view_expr, self.name) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeExprFilterOps — comparison operators on any EdgeExpr +// ───────────────────────────────────────────────────────────────────────────── + +/// Comparison operators on any [`EdgeExpr`], regardless of output type. +/// +/// Unlike [`EdgePropertyExprOps`] (which is limited to `Output = Option`), +/// this trait works for any output type — in particular `usize` for `.len()`: +/// +/// ```rust,ignore +/// EdgeFilter.temporal_property("count").len().gt(3usize) +/// EdgeFilter.temporal_property("count").len().eq(0usize) +/// ``` +pub trait EdgeExprFilterOps: EdgeExpr + Sized { + fn gt>(self, rhs: R) -> BinaryCmpEdgeFilter { + BinaryCmpEdgeFilter::new(self, BinaryOp::Gt, rhs) + } + fn ge>(self, rhs: R) -> BinaryCmpEdgeFilter { + BinaryCmpEdgeFilter::new(self, BinaryOp::Ge, rhs) + } + fn lt>(self, rhs: R) -> BinaryCmpEdgeFilter { + BinaryCmpEdgeFilter::new(self, BinaryOp::Lt, rhs) + } + fn le>(self, rhs: R) -> BinaryCmpEdgeFilter { + BinaryCmpEdgeFilter::new(self, BinaryOp::Le, rhs) + } + fn eq>(self, rhs: R) -> BinaryCmpEdgeFilter { + BinaryCmpEdgeFilter::new(self, BinaryOp::Eq, rhs) + } + fn ne>(self, rhs: R) -> BinaryCmpEdgeFilter { + BinaryCmpEdgeFilter::new(self, BinaryOp::Ne, rhs) + } + fn starts_with>( + self, + rhs: R, + ) -> StringEdgeFilter + where + Self::Output: StringComparable, + { + StringEdgeFilter::new(self, StringOp::StartsWith, rhs) + } + fn ends_with>( + self, + rhs: R, + ) -> StringEdgeFilter + where + Self::Output: StringComparable, + { + StringEdgeFilter::new(self, StringOp::EndsWith, rhs) + } + fn contains>( + self, + rhs: R, + ) -> StringEdgeFilter + where + Self::Output: StringComparable, + { + StringEdgeFilter::new(self, StringOp::Contains, rhs) + } + fn not_contains>( + self, + rhs: R, + ) -> StringEdgeFilter + where + Self::Output: StringComparable, + { + StringEdgeFilter::new(self, StringOp::NotContains, rhs) + } + fn fuzzy_search>( + self, + rhs: R, + levenshtein_distance: usize, + prefix_match: bool, + ) -> StringEdgeFilter + where + Self::Output: StringComparable, + { + StringEdgeFilter::new( + self, + StringOp::FuzzySearch { levenshtein_distance, prefix_match }, + rhs, + ) + } +} + +impl EdgeExprFilterOps for E {} + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeAggregated string convenience — mirrors NodeAggregated::contains etc. +// ───────────────────────────────────────────────────────────────────────────── + +impl>> EdgeAggregated { + fn str_finish(self, op: StringOp, rhs: &str) -> StringEdgeFilter { + StringEdgeFilter::new(self.expr, op, Prop::Str(ArcStr::from(rhs))) + } + + pub fn starts_with(self, rhs: &str) -> StringEdgeFilter { + self.str_finish(StringOp::StartsWith, rhs) + } + pub fn ends_with(self, rhs: &str) -> StringEdgeFilter { + self.str_finish(StringOp::EndsWith, rhs) + } + pub fn contains(self, rhs: &str) -> StringEdgeFilter { + self.str_finish(StringOp::Contains, rhs) + } + pub fn not_contains(self, rhs: &str) -> StringEdgeFilter { + self.str_finish(StringOp::NotContains, rhs) + } + pub fn fuzzy_search( + self, + rhs: &str, + levenshtein_distance: usize, + prefix_match: bool, + ) -> StringEdgeFilter { + self.str_finish( + StringOp::FuzzySearch { levenshtein_distance, prefix_match }, + rhs, + ) + } + pub fn is_in( + self, + values: impl IntoIterator, + ) -> PropValueSetEdgeFilter { + PropValueSetEdgeFilter { + expr: self.expr, + values: values.into_iter().collect(), + op: SetOp::IsIn, + } + } + pub fn is_not_in( + self, + values: impl IntoIterator, + ) -> PropValueSetEdgeFilter { + PropValueSetEdgeFilter { + expr: self.expr, + values: values.into_iter().collect(), + op: SetOp::IsNotIn, + } + } + + pub fn sum(self) -> EdgeAggregated>> { + EdgeAggregated { expr: SumEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + } + pub fn avg(self) -> EdgeAggregated>> { + EdgeAggregated { expr: AvgEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + } + pub fn min(self) -> EdgeAggregated>> { + EdgeAggregated { expr: MinEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + } + pub fn max(self) -> EdgeAggregated>> { + EdgeAggregated { expr: MaxEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + } + pub fn first(self) -> EdgeAggregated>> { + EdgeAggregated { expr: FirstEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + } + pub fn last(self) -> EdgeAggregated>> { + EdgeAggregated { expr: LastEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + } + pub fn len(self) -> LenEdgeExpr> { + LenEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) + } + pub fn any(self) -> EdgeQuantified, AnyMode> { + EdgeQuantified { expr: UnwrapOptPropEdgeExpr(self.expr), _q: PhantomData } + } + pub fn all(self) -> EdgeQuantified, AllMode> { + EdgeQuantified { expr: UnwrapOptPropEdgeExpr(self.expr), _q: PhantomData } + } +} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs new file mode 100644 index 0000000000..f054674f10 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -0,0 +1,566 @@ +//! Runtime edge evaluators — given an EdgeRef, return a typed value. +//! +//! Parallel to `node_expr/ops.rs` — same design, different subject. + +use crate::db::{ + api::{ + properties::internal::{InternalMetadataOps, InternalTemporalPropertyViewOps}, + state::ops::Const, + view::internal::GraphView, + }, + graph::{ + edge::EdgeView, + views::filter::model::property_filter::evaluate::aggregate_values, + }, +}; +use raphtory_api::core::entities::{ + edges::edge_ref::EdgeRef, + properties::prop::{Prop, PropType}, +}; +use raphtory_storage::graph::graph::GraphStorage; + +use super::EdgeOp; +use std::sync::Arc; + +// ───────────────────────────────────────────────────────────────────────────── +// Arc — blanket impl so Arc-boxed ops satisfy EdgeOp +// ───────────────────────────────────────────────────────────────────────────── + +impl<'a, V: Clone + Send + Sync> EdgeOp for Arc + 'a> { + type Output = V; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> V { + self.as_ref().apply(storage, edge) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Const — constant literal op (RHS in comparisons) +// ───────────────────────────────────────────────────────────────────────────── + +impl EdgeOp for Const { + type Output = V; + + fn apply(&self, _storage: &GraphStorage, _edge: EdgeRef) -> V { + self.0.clone() + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// EdgePropOp — latest temporal property value by pre-resolved column ID +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct EdgePropOp { + pub(crate) graph: G, + pub(crate) prop_id: usize, +} + +impl EdgeOp for EdgePropOp { + type Output = Option; + + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { + EdgeView::new(&self.graph, edge).temporal_value(self.prop_id) + } + + fn prop_type(&self) -> PropType { + self.graph + .edge_meta() + .temporal_prop_mapper() + .get_dtype(self.prop_id) + .unwrap_or_default() + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeMetaOp — static metadata field by pre-resolved column ID +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct EdgeMetaOp { + pub(crate) graph: G, + pub(crate) prop_id: usize, +} + +impl EdgeOp for EdgeMetaOp { + type Output = Option; + + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { + EdgeView::new(&self.graph, edge).get_metadata(self.prop_id) + } + + fn prop_type(&self) -> PropType { + self.graph + .edge_meta() + .metadata_mapper() + .get_dtype(self.prop_id) + .unwrap_or_default() + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalEdgePropOp — all temporal values for a property in the view window +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct TemporalEdgePropOp { + pub(crate) graph: G, + pub(crate) prop_id: usize, +} + +impl EdgeOp for TemporalEdgePropOp { + type Output = Prop; + + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Prop { + let vals: Vec = EdgeView::new(&self.graph, edge) + .temporal_iter(self.prop_id) + .map(|(_, v)| v) + .collect(); + Prop::List(raphtory_api::core::entities::properties::prop::PropArray::from(vals)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// BinaryCmpEdgeOp<'g> — compares two EdgeOp outputs, returns bool +// ───────────────────────────────────────────────────────────────────────────── + +use crate::db::graph::views::filter::model::filter_operator::{BinaryOp, Comparable}; + +#[derive(Clone)] +pub(crate) struct BinaryCmpEdgeOp<'g, L> { + pub(crate) left: Arc + 'g>, + pub(crate) right: Arc + 'g>, + pub(crate) op: BinaryOp, +} + +impl<'g, L: Comparable + Clone + Send + Sync + 'static> EdgeOp for BinaryCmpEdgeOp<'g, L> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + let l = self.left.apply(storage, edge); + let r = self.right.apply(storage, edge); + L::binary_cmp(&self.op, &l, &r) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// UnaryEdgeOp<'g, I> — is_some / is_none on Option-valued expressions +// ───────────────────────────────────────────────────────────────────────────── + +use crate::db::graph::views::filter::model::filter_operator::UnaryOp; + +#[derive(Clone)] +pub(crate) struct UnaryEdgeOp<'g, I: Clone + Send + Sync + 'static> { + pub(crate) inner: Arc> + 'g>, + pub(crate) op: UnaryOp, +} + +impl<'g, I: Clone + Send + Sync + 'static> EdgeOp for UnaryEdgeOp<'g, I> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + let v = self.inner.apply(storage, edge); + match self.op { + UnaryOp::IsSome => v.is_some(), + UnaryOp::IsNone => v.is_none(), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// AnyEdgeOp<'g> / AllEdgeOp<'g> — quantifier ops over Prop::List +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct AnyEdgeOp<'g> { + pub(crate) inner: Arc + 'g>, +} + +impl<'g> EdgeOp for AnyEdgeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + self.inner.apply(storage, edge) + } +} + +#[derive(Clone)] +pub(crate) struct AllEdgeOp<'g> { + pub(crate) inner: Arc + 'g>, +} + +impl<'g> EdgeOp for AllEdgeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + self.inner.apply(storage, edge) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// PropListEdgeCmpOp<'g> — compares each element of a Prop::List against a RHS +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct PropListEdgeCmpOp<'g> { + pub(crate) temporal_op: Arc + 'g>, + pub(crate) rhs: Arc> + 'g>, + pub(crate) cmp_op: BinaryOp, + pub(crate) any: bool, +} + +impl<'g> EdgeOp for PropListEdgeCmpOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + let list_prop = self.temporal_op.apply(storage, edge); + let rhs_val = self.rhs.apply(storage, edge); + let vals = match list_prop { + Prop::List(v) => v, + _ => return false, + }; + let mut results = vals.iter().map(|v| { + rhs_val + .as_ref() + .map(|r| Prop::binary_cmp(&self.cmp_op, &v, r)) + .unwrap_or(false) + }); + if self.any { + results.any(|b| b) + } else { + results.all(|b| b) + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Aggregator EdgeOps — reduce a Prop::List to a scalar Option +// ───────────────────────────────────────────────────────────────────────────── + +macro_rules! impl_agg_edge_op { + ($name:ident, |$vals:ident: Vec| $body:expr) => { + #[derive(Clone)] + pub(crate) struct $name<'g> { + pub(crate) inner: Arc + 'g>, + } + + impl<'g> EdgeOp for $name<'g> { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + let list_prop = self.inner.apply(storage, edge); + let $vals: Vec = match list_prop { + Prop::List(v) => v.iter().collect(), + _ => return None, + }; + if $vals.is_empty() { + return None; + } + $body + } + } + }; +} + +use crate::db::graph::views::filter::model::{ + filter_operator::{SetOp, StringComparable, StringOp}, + property_filter::Op, +}; +use raphtory_api::core::{ + entities::properties::prop::PropArray, + storage::arc_str::ArcStr, +}; +use std::collections::HashSet; +use std::hash::Hash; + +impl_agg_edge_op!(SumEdgeOp, |vals: Vec| aggregate_values(&vals, Op::Sum)); +impl_agg_edge_op!(AvgEdgeOp, |vals: Vec| aggregate_values(&vals, Op::Avg)); +impl_agg_edge_op!(MinEdgeOp, |vals: Vec| aggregate_values(&vals, Op::Min)); +impl_agg_edge_op!(MaxEdgeOp, |vals: Vec| aggregate_values(&vals, Op::Max)); +impl_agg_edge_op!(FirstEdgeOp, |vals: Vec| vals.into_iter().next()); +impl_agg_edge_op!(LastEdgeOp, |vals: Vec| vals.into_iter().last()); +// LenEdgeOp written explicitly: Output = usize, not Option +#[derive(Clone)] +pub(crate) struct LenEdgeOp<'g> { + pub(crate) inner: Arc + 'g>, +} + +impl<'g> EdgeOp for LenEdgeOp<'g> { + type Output = usize; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> usize { + match self.inner.apply(storage, edge) { + Prop::List(v) => v.iter().count(), + _ => 0, + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// StringEdgeOp<'g, T> — applies a StringOp to two EdgeOp values +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct StringEdgeOp<'g, T: StringComparable> { + pub(crate) left: Arc + 'g>, + pub(crate) right: Arc + 'g>, + pub(crate) op: StringOp, +} + +impl<'g, T: StringComparable> EdgeOp for StringEdgeOp<'g, T> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + T::string_cmp( + &self.op, + &self.left.apply(storage, edge), + &self.right.apply(storage, edge), + ) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// PropValueSetEdgeOp<'g> — is_in / is_not_in for Option (linear scan) +// ───────────────────────────────────────────────────────────────────────────── + +/// Checks whether an `Option` value is in (or not in) a fixed `Vec`. +/// Uses linear scan because `Prop` may contain floats that don't implement `Hash`. +#[derive(Clone)] +pub(crate) struct PropValueSetEdgeOp<'g> { + pub(crate) inner: Arc> + 'g>, + pub(crate) values: Vec, + pub(crate) op: SetOp, +} + +impl<'g> EdgeOp for PropValueSetEdgeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + match self.inner.apply(storage, edge) { + None => false, + Some(v) => match self.op { + SetOp::IsIn => self.values.iter().any(|x| x == &v), + SetOp::IsNotIn => self.values.iter().all(|x| x != &v), + }, + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// SetEdgeOp<'g, I> — is_in / is_not_in for Option (HashSet, O(1)) +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct SetEdgeOp<'g, I: Eq + Hash + Clone + Send + Sync + 'static> { + pub(crate) inner: Arc> + 'g>, + pub(crate) values: Arc>, + pub(crate) op: SetOp, +} + +impl<'g, I: Eq + Hash + Clone + Send + Sync + 'static> EdgeOp for SetEdgeOp<'g, I> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + match self.inner.apply(storage, edge) { + None => false, + Some(v) => match self.op { + SetOp::IsIn => self.values.contains(&v), + SetOp::IsNotIn => !self.values.contains(&v), + }, + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// PropListInSetEdgeOp<'g> — element-wise set-membership test on a Prop::List +// ───────────────────────────────────────────────────────────────────────────── + +/// Checks each element of a `Prop::List` against a fixed `Vec`, producing +/// `Prop::List([Bool, …])`. The result is then reduced by `AnyEdgeOp` or `AllEdgeOp`. +#[derive(Clone)] +pub(crate) struct PropListInSetEdgeOp<'g> { + pub(crate) inner: Arc + 'g>, + pub(crate) values: Vec, + pub(crate) op: SetOp, +} + +impl<'g> EdgeOp for PropListInSetEdgeOp<'g> { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Prop { + let Prop::List(arr) = self.inner.apply(storage, edge) else { + return Prop::List(PropArray::from(vec![])); + }; + let bools: Vec = arr + .iter() + .map(|v| { + Prop::Bool(match self.op { + SetOp::IsIn => self.values.iter().any(|x| x == &v), + SetOp::IsNotIn => self.values.iter().all(|x| x != &v), + }) + }) + .collect(); + Prop::List(PropArray::from(bools)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// PropListStringEdgeOp<'g> — element-wise string comparison on a Prop::List +// ───────────────────────────────────────────────────────────────────────────── + +/// Applies a `StringOp` to each element of a `Prop::List` against a scalar RHS, +/// producing `Prop::List([Bool, …])`. Reduced by `AnyEdgeOp` or `AllEdgeOp`. +#[derive(Clone)] +pub(crate) struct PropListStringEdgeOp<'g> { + pub(crate) inner: Arc + 'g>, + pub(crate) rhs: ArcStr, + pub(crate) op: StringOp, +} + +impl<'g> EdgeOp for PropListStringEdgeOp<'g> { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Prop { + let Prop::List(arr) = self.inner.apply(storage, edge) else { + return Prop::List(PropArray::from(vec![])); + }; + let rhs = Some(Prop::Str(self.rhs.clone())); + let bools: Vec = arr + .iter() + .map(|v| Prop::Bool(Option::::string_cmp(&self.op, &Some(v), &rhs))) + .collect(); + Prop::List(PropArray::from(bools)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// AnyPropEdgeOp / AllPropEdgeOp — reduce a Prop::List([Bool, …]) to bool +// ───────────────────────────────────────────────────────────────────────────── + +fn prop_any_edge(prop: &Prop) -> bool { + match prop { + Prop::Bool(b) => *b, + Prop::List(arr) => arr.iter().any(|p| prop_any_edge(&p)), + _ => false, + } +} + +fn prop_all_edge(prop: &Prop) -> bool { + match prop { + Prop::Bool(b) => *b, + Prop::List(arr) => !arr.is_empty() && arr.iter().all(|p| prop_all_edge(&p)), + _ => false, + } +} + +/// Wraps a `PropListInSetEdgeOp` or `PropListStringEdgeOp` and returns `true` if +/// at least one element of the resulting `Prop::List([Bool, …])` is `true`. +#[derive(Clone)] +pub(crate) struct AnyPropEdgeOp<'g> { + pub(crate) inner: Arc + 'g>, +} + +impl<'g> EdgeOp for AnyPropEdgeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + prop_any_edge(&self.inner.apply(storage, edge)) + } +} + +/// Wraps a `PropListInSetEdgeOp` or `PropListStringEdgeOp` and returns `true` only +/// if every element of the resulting `Prop::List([Bool, …])` is `true` (and the +/// list is non-empty). +#[derive(Clone)] +pub(crate) struct AllPropEdgeOp<'g> { + pub(crate) inner: Arc + 'g>, +} + +impl<'g> EdgeOp for AllPropEdgeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + prop_all_edge(&self.inner.apply(storage, edge)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// UnwrapOptPropEdgeOp<'g> — converts Option → Prop for nested aggregation +// ───────────────────────────────────────────────────────────────────────────── + +/// Converts `Option` → `Prop` so that aggregator ops can operate on a value +/// produced by a prior aggregation step. +/// +/// - `Some(Prop::List(arr))` → `Prop::List(arr)` (pass through) +/// - `Some(v)` → `Prop::List([v])` (single-element list) +/// - `None` → `Prop::List([])` (empty — yields None from next aggregator) +#[derive(Clone)] +pub(crate) struct UnwrapOptPropEdgeOp<'g> { + pub(crate) inner: Arc> + 'g>, +} + +impl<'g> EdgeOp for UnwrapOptPropEdgeOp<'g> { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Prop { + match self.inner.apply(storage, edge) { + Some(Prop::List(arr)) => Prop::List(arr), + Some(v) => Prop::List(PropArray::from(vec![v])), + None => Prop::List(PropArray::from(vec![])), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// NestedMapEdgeOp<'g> — element-wise aggregation / quantification on a Prop::List +// ───────────────────────────────────────────────────────────────────────────── + +/// Applies a per-element operation to each element of an outer `Prop::List`. +/// +/// Used for chained expressions like `.temporal().any().sum()`: +/// the outer list is `Prop::List([list_t1, list_t2, …])` and for each inner +/// `list_ti` the op is applied, producing `Prop::List([result_t1, result_t2, …])`. +/// The outer `AnyPropEdgeOp` / `AllPropEdgeOp` then reduces the result list. +#[derive(Clone)] +pub(crate) struct NestedMapEdgeOp<'g> { + pub(crate) inner: Arc + 'g>, + pub(crate) op: Op, +} + +impl<'g> EdgeOp for NestedMapEdgeOp<'g> { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Prop { + let outer = self.inner.apply(storage, edge); + let Prop::List(arr) = outer else { + return Prop::List(PropArray::from(vec![])); + }; + let mapped: Vec = arr + .iter() + .map(|elem| match elem { + Prop::List(inner_arr) => { + let vals: Vec = inner_arr.iter().collect(); + match self.op { + Op::Sum => aggregate_values(&vals, Op::Sum) + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Avg => aggregate_values(&vals, Op::Avg) + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Min => aggregate_values(&vals, Op::Min) + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Max => aggregate_values(&vals, Op::Max) + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::First => vals.into_iter().next() + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Last => vals.into_iter().last() + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Len => Prop::U64(inner_arr.len() as u64), + Op::Any => Prop::Bool(prop_any_edge(&Prop::List(inner_arr))), + Op::All => Prop::Bool(prop_all_edge(&Prop::List(inner_arr))), + } + } + other => other, + }) + .collect(); + Prop::List(PropArray::from(mapped)) + } +} 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 b4a1152532..14573f3e83 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -20,8 +20,9 @@ use crate::{ property_filter::PropertyFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - AndFilter, CombinedFilter, ComposableFilter, EdgeViewFilterOps, EntityMarker, - InternalViewWrapOps, NotFilter, OrFilter, TryAsCompositeFilter, Wrap, + AndFilter, CombinedFilter, ComposableFilter, CreateView, EdgeFilterFactory, + EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, NotFilter, OrFilter, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -362,3 +363,14 @@ impl TryAsCompositeFilter for CompositeEdgeFilter { Err(GraphError::NotSupported) } } + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeFilterFactory impls +// ───────────────────────────────────────────────────────────────────────────── + +impl EdgeFilterFactory for EdgeFilter {} +impl EdgeFilterFactory for Windowed {} +impl EdgeFilterFactory for Latest {} +impl EdgeFilterFactory for Layered {} +impl EdgeFilterFactory for SnapshotAt {} +impl EdgeFilterFactory for SnapshotLatest {} 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 38ffb8cda1..08b88b390b 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -5,8 +5,8 @@ use crate::{ filter::{ model::{ edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, + CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, + InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -107,6 +107,17 @@ impl CreateFilter for Latest impl ComposableFilter for Latest {} +impl CreateView for Latest { + type View<'graph, G: GraphView + 'graph> = T::View<'graph, G>; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + self.inner.create_view(view) + } +} + impl Wrap for Latest { type Wrapped = Latest; fn wrap(&self, value: T) -> Self::Wrapped { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 19f6e3bb71..3c5cb0f4c8 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -15,13 +15,14 @@ pub use crate::{ UnaryOp, }, node_expr::{ - Aggregated, AllMode, AnyMode, AvgExpr, BinaryCmpNodeFilter, ConstExpr, - DegreeExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, Metadata, MinExpr, - NodeExpr, NodeExprFilterOps, Property, Quantified, QuantifiedNodeFilter, + AllMode, AnyMode, AvgExpr, BinaryCmpNodeFilter, ConstExpr, + DegreeExpr, FirstExpr, IntoPropNodeExpr, LastExpr, LenExpr, MaxExpr, + MinExpr, NodeAggregated, NodeExpr, NodeExprFilterOps, NodePropertyExprOps, + NodeQuantified, NodeTemporalPropOps, PropValueSetFilter, QuantifiedNodeFilter, QuantifierMode, SetNodeFilter, StringNodeFilter, SumExpr, TemporalExprOps, TemporalProp, UnaryNodeFilter, }, - node_filter::NodeFilter, + node_filter::{NodeFilter, NodeFilterFactory}, not_filter::NotFilter, or_filter::OrFilter, }, @@ -54,7 +55,6 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_expr::{NodeMetaOp, NodePropOp}, - node_filter::NodeFilterFactory, property_filter::{ builders::{ InternalPropertyFilterBuilder, PropertyExprBuilder, @@ -71,14 +71,16 @@ use crate::{ prelude::LayerOps, }; pub use node_filter::CompositeNodeFilter; +pub use edge_expr::{EdgeExprFilterOps, EdgePropertyExprOps, EdgeTemporalPropOps}; use raphtory_api::core::{ entities::{properties::prop::Prop, Layer}, - storage::timeindex::{AsTime, EventTime}, + storage::{arc_str::ArcStr, timeindex::{AsTime, EventTime}}, utils::time::IntoTime, }; -use std::{ops::Deref, sync::Arc}; +use std::{marker::PhantomData, ops::Deref, sync::Arc}; pub mod and_filter; +pub mod edge_expr; pub mod edge_filter; pub mod exploded_edge_filter; pub mod filter; @@ -252,6 +254,38 @@ pub enum EntityMarker { ExplodedEdge, } +// ───────────────────────────────────────────────────────────────────────────── +// Shared property name expressions +// +// These structs carry only a property name. They implement both NodeExpr and +// EdgeExpr in their respective modules (node_expr/exprs.rs, edge_expr/exprs.rs), +// reading from node_meta() or edge_meta() depending on the context. +// ───────────────────────────────────────────────────────────────────────────── + +/// Latest temporal property value — implements both `NodeExpr` and `EdgeExpr`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Property { + pub name: String, +} + +impl Property { + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } + } +} + +/// Static metadata field — implements both `NodeExpr` and `EdgeExpr`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Metadata { + pub name: String, +} + +impl Metadata { + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } + } +} + #[derive(Clone)] pub struct PropertyExpr { view_expr: E, @@ -403,6 +437,60 @@ impl PropertyExpr { } } +// ───────────────────────────────────────────────────────────────────────────── +// EdgeFilterFactory — marker for edge-side filter builder types +// ───────────────────────────────────────────────────────────────────────────── + +/// Marker trait for edge filter builder types (`EdgeFilter`, `Windowed`, etc.). +/// +/// Disjoint from `NodeFilterFactory`: no type implements both, so `PropertyExpr` +/// can have two separate sets of comparison methods gated on each. +pub trait EdgeFilterFactory: PropertyFilterFactory + Clone {} + +// ───────────────────────────────────────────────────────────────────────────── +// PropertyExpr / MetadataExpr — EdgeExpr impls +// ───────────────────────────────────────────────────────────────────────────── + +use edge_expr::{ + EdgeExpr, EdgeMetaOp as EMetaOp, EdgeOp, EdgePropOp as EPropOp, +}; + +impl EdgeExpr + for PropertyExpr +{ + type Output = Option; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let prop_id = graph + .edge_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(EPropOp { graph, prop_id })) + } +} + +impl EdgeExpr + for MetadataExpr +{ + type Output = Option; + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let prop_id = graph + .edge_meta() + .get_prop_id(&self.name, true) + .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; + let graph = self.view_expr.create_view(graph)?; + Ok(Arc::new(EMetaOp { graph, prop_id })) + } +} + pub trait TryAsCompositeFilter: Send + Sync { fn try_as_composite_node_filter(&self) -> Result; @@ -575,6 +663,28 @@ impl CreateView for NodeFilter { } } +impl CreateView for EdgeFilter { + type View<'graph, G: GraphView + 'graph> = G; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + Ok(view) + } +} + +impl CreateView for ExplodedEdgeFilter { + type View<'graph, G: GraphView + 'graph> = G; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + Ok(view) + } +} + impl CreateView for Layered { type View<'graph, G: GraphView + 'graph> = LayeredGraph; 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 ed2c6bf514..eb54f549eb 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 @@ -60,9 +60,12 @@ //! ``` use super::{ + filters::{ + BinaryCmpNodeFilter, SetNodeFilter, StringNodeFilter, UnaryNodeFilter, + }, ops::{ - AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NodeMetaOp, - NodePropOp, SumNodeOp, TemporalNodePropOp, + AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NestedMapNodeOp, + NodeMetaOp, NodePropOp, SumNodeOp, TemporalNodePropOp, UnwrapOptPropOp, }, NodeExpr, }; @@ -73,7 +76,10 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::model::{ - filter_operator::Comparable, node_filter::NodeFilter, CreateView, + filter_operator::{BinaryOp, Comparable, SetOp, StringOp, UnaryOp}, + node_filter::NodeFilter, + property_filter::Op, + CreateView, Metadata, Property, }, }, errors::GraphError, @@ -86,7 +92,7 @@ use raphtory_api::core::{ storage::arc_str::ArcStr, Direction, }; -use std::sync::Arc; +use std::{collections::HashSet, marker::PhantomData, sync::Arc}; // ───────────────────────────────────────────────────────────────────────────── // Node field expressions — identity, name, type @@ -219,6 +225,44 @@ impl NodeExpr for &'static str { } } +// ───────────────────────────────────────────────────────────────────────────── +// IntoPropNodeExpr — normalises any RHS value to NodeExpr> +// +// Used as the bound on Quantified::eq/ne/gt/ge/lt/le and NodeAggregated::eq/ne/… +// so that .eq("Alice"), .eq(30i64), and .eq(NodeFilter::property("x")) all work +// with a single method name. +// ───────────────────────────────────────────────────────────────────────────── + +pub trait IntoPropNodeExpr { + type Expr: NodeExpr>; + fn into_prop_node_expr(self) -> Self::Expr; +} + +// Blanket: anything already NodeExpr> passes through unchanged. +// Covers Prop, i64, u64, i32, u32, f32, f64, bool, u8, u16, Property, Metadata, etc. +impl>> IntoPropNodeExpr for T { + type Expr = T; + fn into_prop_node_expr(self) -> T { + self + } +} + +// &'static str has Output = &'static str, so the blanket above does NOT cover it. +// Convert to Prop::Str so .eq("Alice") works transparently. +impl IntoPropNodeExpr for &'static str { + type Expr = Prop; + fn into_prop_node_expr(self) -> Prop { + Prop::Str(ArcStr::from(self)) + } +} + +impl IntoPropNodeExpr for String { + type Expr = Prop; + fn into_prop_node_expr(self) -> Prop { + Prop::Str(ArcStr::from(self)) + } +} + impl NodeExpr for Prop { type Output = Option; @@ -316,27 +360,6 @@ impl NodeExpr for DegreeExpr { } } -/// 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; @@ -352,27 +375,6 @@ impl NodeExpr for Property { } } -/// 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; @@ -441,12 +443,12 @@ impl NodeExpr for TemporalPropert // // Each wraps a NodeExpr (typically TemporalPropertyExpr) and reduces // the Prop::List it produces to a scalar. Not constructed directly — -// TemporalProp / TemporalExprOps methods return Aggregated>: +// TemporalProp / TemporalExprOps methods return NodeAggregated>: // -// .temporal_property("v").sum() → Aggregated>> -// .temporal_property("v").len() → Aggregated>> +// .temporal_property("v").sum() → NodeAggregated>> +// .temporal_property("v").len() → NodeAggregated>> // -// Calling .gt() / .eq() etc. on Aggregated then produces: +// Calling .gt() / .eq() etc. on NodeAggregated then produces: // BinaryCmpNodeFilter>, RHS> // ───────────────────────────────────────────────────────────────────────────── @@ -481,3 +483,60 @@ impl_agg_expr!(MaxExpr, MaxNodeOp, Option); impl_agg_expr!(FirstExpr, FirstNodeOp, Option); impl_agg_expr!(LastExpr, LastNodeOp, Option); impl_agg_expr!(LenExpr, LenNodeOp, usize); + +// ───────────────────────────────────────────────────────────────────────────── +// UnwrapOptPropNodeExpr — bridges Option → Prop for nested aggregation +// ───────────────────────────────────────────────────────────────────────────── + +// ───────────────────────────────────────────────────────────────────────────── +// NestedMapExpr — per-element aggregation / quantification on a Prop::List +// ───────────────────────────────────────────────────────────────────────────── + +/// Applies a per-element operation to each element of a `Prop::List` produced by `E`. +/// +/// Used for chained expressions like `.temporal().any().sum()`: +/// `E` produces `Prop::List([list_t1, list_t2, …])` and each inner `list_ti` is +/// aggregated, yielding `Prop::List([result_t1, result_t2, …])` which is then +/// further quantified by `AnyNodeOp` / `AllNodeOp`. +#[derive(Clone)] +pub struct NestedMapExpr> { + pub inner: E, + pub op: Op, +} + +impl> NodeExpr for NestedMapExpr { + type Output = Prop; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError> { + let inner = self.inner.create_node_op(graph)?; + Ok(Arc::new(NestedMapNodeOp { + inner, + op: self.op, + })) + } +} + +/// Bridges `E: NodeExpr>` to `NodeExpr`, +/// enabling aggregator exprs (`SumExpr`, `AnyMode`, etc.) to operate on values +/// produced by a prior aggregation step. +/// +/// Used when chaining e.g. `.temporal().last().sum()`: +/// `last()` produces `NodeAggregated>` with `Output = Option`; +/// `sum()` on that wraps in `SumExpr>>`. +#[derive(Clone)] +pub struct UnwrapOptPropNodeExpr>>(pub E); + +impl>> NodeExpr for UnwrapOptPropNodeExpr { + type Output = Prop; + + 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(UnwrapOptPropOp { inner })) + } +} 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 880deebf7a..5e05556153 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 @@ -37,23 +37,28 @@ use super::{ ops::{ - AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, PropListCompareOp, SetNodeOp, StringNodeOp, - UnaryNodeOp, + AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, PropListCompareOp, PropListInSetOp, + PropListStringOp, PropValueSetNodeOp, SetNodeOp, StringNodeOp, UnaryNodeOp, }, - AvgExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, NodeExpr, SumExpr, - TemporalPropertyExpr, + AvgExpr, FirstExpr, IntoPropNodeExpr, LastExpr, LenExpr, MaxExpr, MinExpr, NestedMapExpr, + NodeExpr, SumExpr, TemporalPropertyExpr, UnwrapOptPropNodeExpr, }; use crate::{ db::{ - api::{state::ops::NodeOp, view::internal::GraphView}, + api::{ + state::ops::NodeOp, + view::internal::GraphView, + }, graph::views::filter::{ model::{ edge_filter::CompositeEdgeFilter, filter_operator::{ BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp, }, + node_filter::NodeFilterFactory, + property_filter::Op, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, - CreateView, TryAsCompositeFilter, + CreateView, MetadataExpr, PropertyExpr, TryAsCompositeFilter, }, node_filtered_graph::NodeFilteredGraph, }, @@ -61,9 +66,12 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use raphtory_api::core::entities::{ - properties::prop::{Prop, PropType}, - VID, +use raphtory_api::core::{ + entities::{ + properties::prop::{Prop, PropType}, + VID, + }, + storage::arc_str::ArcStr, }; use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; @@ -590,11 +598,11 @@ where /// Not constructed directly — returned by `Quantified::gt/eq/…`: /// ```rust,ignore /// // NodeFilter::temporal_property("score").any().gt(10i64) -/// // → QuantifiedNodeFilter, AnyMode, i64> +/// // → QuantifiedNodeFilter, AnyMode> /// // compiles to: AnyNodeOp { inner: PropListCompareOp { …, op: Gt } } /// /// // NodeFilter::temporal_property("score").all().gt(0i64) -/// // → QuantifiedNodeFilter, AllMode, i64> +/// // → QuantifiedNodeFilter, AllMode> /// // compiles to: AllNodeOp { inner: PropListCompareOp { …, op: Gt } } /// ``` pub struct QuantifiedNodeFilter @@ -674,9 +682,11 @@ where self, graph: G, ) -> Result, GraphError> { + let rhs: Arc> + 'graph> = + self.rhs.create_node_op(graph.clone())?; let inner = Arc::new(PropListCompareOp { - inner: self.expr.create_node_op(graph.clone())?, - rhs: self.rhs.create_node_op(graph)?, + inner: self.expr.create_node_op(graph)?, + rhs, op: self.op, }); Ok(AnyNodeOp { inner }) @@ -708,9 +718,11 @@ where self, graph: G, ) -> Result, GraphError> { + let rhs: Arc> + 'graph> = + self.rhs.create_node_op(graph.clone())?; let inner = Arc::new(PropListCompareOp { - inner: self.expr.create_node_op(graph.clone())?, - rhs: self.rhs.create_node_op(graph)?, + inner: self.expr.create_node_op(graph)?, + rhs, op: self.op, }); Ok(AllNodeOp { inner }) @@ -738,6 +750,334 @@ where } } +// ───────────────────────────────────────────────────────────────────────────── +// QuantifiedIsInNodeFilter — quantified set-membership filter +// ───────────────────────────────────────────────────────────────────────────── + +/// A node filter that checks whether any/all temporal values are in a fixed set. +/// +/// ```rust,ignore +/// NodeFilter::temporal_property("status").any().is_in(vec![Prop::Str("A".into()), Prop::Str("B".into())]) +/// ``` +pub struct QuantifiedIsInNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, +{ + pub(crate) expr: E, + pub(crate) values: Vec, + pub(crate) op: SetOp, + pub(crate) _q: PhantomData, +} + +impl Clone for QuantifiedIsInNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + values: self.values.clone(), + op: self.op, + _q: PhantomData, + } + } +} + +impl ComposableFilter for QuantifiedIsInNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, +{ +} + +impl CreateFilter for QuantifiedIsInNodeFilter +where + E: 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 + 'graph> = Arc::new(PropListInSetOp { + inner: self.expr.create_node_op(graph)?, + values: self.values, + op: self.op, + }); + Ok(AnyNodeOp { inner }) + } +} + +impl CreateFilter for QuantifiedIsInNodeFilter +where + E: 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 + 'graph> = Arc::new(PropListInSetOp { + inner: self.expr.create_node_op(graph)?, + values: self.values, + op: self.op, + }); + Ok(AllNodeOp { inner }) + } +} + +impl TryAsCompositeFilter for QuantifiedIsInNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, +{ + 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) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// QuantifiedStringNodeFilter — quantified string-comparison filter +// ───────────────────────────────────────────────────────────────────────────── + +/// A node filter that applies a string op to every temporal value and reduces with any/all. +/// +/// ```rust,ignore +/// NodeFilter::temporal_property("name").any().starts_with("Al") +/// NodeFilter::temporal_property("tag").all().contains("foo") +/// ``` +pub struct QuantifiedStringNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, +{ + pub(crate) expr: E, + pub(crate) rhs: ArcStr, + pub(crate) op: StringOp, + pub(crate) _q: PhantomData, +} + +impl Clone for QuantifiedStringNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + rhs: self.rhs.clone(), + op: self.op, + _q: PhantomData, + } + } +} + +impl ComposableFilter for QuantifiedStringNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, +{ +} + +impl CreateFilter for QuantifiedStringNodeFilter +where + E: 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 + 'graph> = Arc::new(PropListStringOp { + inner: self.expr.create_node_op(graph)?, + rhs: self.rhs, + op: self.op, + }); + Ok(AnyNodeOp { inner }) + } +} + +impl CreateFilter for QuantifiedStringNodeFilter +where + E: 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 + 'graph> = Arc::new(PropListStringOp { + inner: self.expr.create_node_op(graph)?, + rhs: self.rhs, + op: self.op, + }); + Ok(AllNodeOp { inner }) + } +} + +impl TryAsCompositeFilter for QuantifiedStringNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, +{ + 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) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// PropValueSetFilter — is_in / is_not_in for aggregated Option values +// ───────────────────────────────────────────────────────────────────────────── + +/// A node filter that checks whether an aggregated scalar property value is in +/// (or not in) a fixed set of `Prop` values. Uses linear scan because `Prop` +/// may contain floats that don't implement `Hash`. +pub struct PropValueSetFilter>> { + pub(crate) expr: E, + pub(crate) values: Vec, + pub(crate) op: SetOp, +} + +impl>> Clone for PropValueSetFilter { + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + values: self.values.clone(), + op: self.op, + } + } +} + +impl>> ComposableFilter for PropValueSetFilter {} + +impl>> CreateFilter for PropValueSetFilter { + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodeFilteredGraph>; + type NodeFilter<'graph, G: GraphView + 'graph> = PropValueSetNodeOp<'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(PropValueSetNodeOp { + inner: self.expr.create_node_op(graph)?, + values: self.values, + op: self.op, + }) + } +} + +impl>> TryAsCompositeFilter for PropValueSetFilter { + fn try_as_composite_node_filter(&self) -> Result { + Err(GraphError::NotSupported) + } + + fn try_as_composite_edge_filter(&self) -> Result { + Err(GraphError::NotSupported) + } + + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + Err(GraphError::NotSupported) + } +} + // ───────────────────────────────────────────────────────────────────────────── // Quantified / Aggregated / TemporalProp — intermediate types in the fluent chain // ───────────────────────────────────────────────────────────────────────────── @@ -750,7 +1090,7 @@ where /// NodeFilter::temporal_property("score").any() // → Quantified, AnyMode> /// .gt(10i64) // → QuantifiedNodeFilter, AnyMode, i64> /// ``` -pub struct Quantified +pub struct NodeQuantified where E: NodeExpr, Q: QuantifierMode, @@ -759,42 +1099,121 @@ where pub(crate) _q: PhantomData, } -impl Quantified +impl NodeQuantified where E: NodeExpr, Q: QuantifierMode, { - fn finish>>( + fn finish( self, op: BinaryOp, rhs: R, - ) -> QuantifiedNodeFilter { - QuantifiedNodeFilter::new(self.expr, op, rhs) + ) -> QuantifiedNodeFilter { + QuantifiedNodeFilter::new(self.expr, op, rhs.into_prop_node_expr()) } - 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) } + + pub fn is_in(self, values: impl IntoIterator) -> QuantifiedIsInNodeFilter { + QuantifiedIsInNodeFilter { + expr: self.expr, + values: values.into_iter().collect(), + op: SetOp::IsIn, + _q: PhantomData, + } + } + + pub fn is_not_in(self, values: impl IntoIterator) -> QuantifiedIsInNodeFilter { + QuantifiedIsInNodeFilter { + expr: self.expr, + values: values.into_iter().collect(), + op: SetOp::IsNotIn, + _q: PhantomData, + } + } + + fn string_finish(self, op: StringOp, rhs: &str) -> QuantifiedStringNodeFilter { + QuantifiedStringNodeFilter { + expr: self.expr, + rhs: ArcStr::from(rhs), + op, + _q: PhantomData, + } + } + + pub fn starts_with(self, rhs: &str) -> QuantifiedStringNodeFilter { + self.string_finish(StringOp::StartsWith, rhs) + } + + pub fn ends_with(self, rhs: &str) -> QuantifiedStringNodeFilter { + self.string_finish(StringOp::EndsWith, rhs) + } + + pub fn contains(self, rhs: &str) -> QuantifiedStringNodeFilter { + self.string_finish(StringOp::Contains, rhs) + } + + pub fn not_contains(self, rhs: &str) -> QuantifiedStringNodeFilter { + self.string_finish(StringOp::NotContains, rhs) + } + + pub fn sum(self) -> NodeQuantified, Q> { + NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Sum }, _q: PhantomData } + } + + pub fn avg(self) -> NodeQuantified, Q> { + NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Avg }, _q: PhantomData } + } + + pub fn min(self) -> NodeQuantified, Q> { + NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Min }, _q: PhantomData } + } + + pub fn max(self) -> NodeQuantified, Q> { + NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Max }, _q: PhantomData } + } + + pub fn first(self) -> NodeQuantified, Q> { + NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::First }, _q: PhantomData } + } + + pub fn last(self) -> NodeQuantified, Q> { + NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Last }, _q: PhantomData } + } + + pub fn len(self) -> NodeQuantified, Q> { + NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Len }, _q: PhantomData } + } + + pub fn any(self) -> NodeQuantified, AnyMode> { + NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Any }, _q: PhantomData } + } + + pub fn all(self) -> NodeQuantified, AllMode> { + NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::All }, _q: PhantomData } + } } /// Returned by [`TemporalProp::sum`], `.avg()`, `.min()` etc. @@ -805,42 +1224,136 @@ where /// NodeFilter::temporal_property("price").sum() // → Aggregated>> /// .gt(100i64) // → BinaryCmpNodeFilter>, i64> /// ``` -pub struct Aggregated { +pub struct NodeAggregated { pub(crate) expr: E, } -impl Aggregated { - fn finish>( +impl>> NodeAggregated { + fn finish( self, op: BinaryOp, rhs: R, - ) -> BinaryCmpNodeFilter { - BinaryCmpNodeFilter::new(self.expr, op, rhs) + ) -> BinaryCmpNodeFilter { + BinaryCmpNodeFilter::new(self.expr, op, rhs.into_prop_node_expr()) } - pub fn eq>(self, rhs: R) -> BinaryCmpNodeFilter { + pub fn eq(self, rhs: R) -> BinaryCmpNodeFilter { self.finish(BinaryOp::Eq, rhs) } - pub fn ne>(self, rhs: R) -> BinaryCmpNodeFilter { + pub fn ne(self, rhs: R) -> BinaryCmpNodeFilter { self.finish(BinaryOp::Ne, rhs) } - pub fn gt>(self, rhs: R) -> BinaryCmpNodeFilter { + pub fn gt(self, rhs: R) -> BinaryCmpNodeFilter { self.finish(BinaryOp::Gt, rhs) } - pub fn ge>(self, rhs: R) -> BinaryCmpNodeFilter { + pub fn ge(self, rhs: R) -> BinaryCmpNodeFilter { self.finish(BinaryOp::Ge, rhs) } - pub fn lt>(self, rhs: R) -> BinaryCmpNodeFilter { + pub fn lt(self, rhs: R) -> BinaryCmpNodeFilter { self.finish(BinaryOp::Lt, rhs) } - pub fn le>(self, rhs: R) -> BinaryCmpNodeFilter { + pub fn le(self, rhs: R) -> BinaryCmpNodeFilter { self.finish(BinaryOp::Le, rhs) } + + pub fn is_in(self, values: impl IntoIterator) -> PropValueSetFilter { + PropValueSetFilter { + expr: self.expr, + values: values.into_iter().collect(), + op: SetOp::IsIn, + } + } + + pub fn is_not_in(self, values: impl IntoIterator) -> PropValueSetFilter { + PropValueSetFilter { + expr: self.expr, + values: values.into_iter().collect(), + op: SetOp::IsNotIn, + } + } + + fn string_finish(self, op: StringOp, rhs: &str) -> StringNodeFilter { + StringNodeFilter::new(self.expr, op, Prop::Str(ArcStr::from(rhs))) + } + + pub fn starts_with(self, rhs: &str) -> StringNodeFilter { + self.string_finish(StringOp::StartsWith, rhs) + } + + pub fn ends_with(self, rhs: &str) -> StringNodeFilter { + self.string_finish(StringOp::EndsWith, rhs) + } + + pub fn contains(self, rhs: &str) -> StringNodeFilter { + self.string_finish(StringOp::Contains, rhs) + } + + pub fn not_contains(self, rhs: &str) -> StringNodeFilter { + self.string_finish(StringOp::NotContains, rhs) + } + + pub fn is_some(self) -> UnaryNodeFilter { + UnaryNodeFilter { + expr: self.expr, + op: UnaryOp::IsSome, + _phantom: PhantomData, + } + } + + pub fn is_none(self) -> UnaryNodeFilter { + UnaryNodeFilter { + expr: self.expr, + op: UnaryOp::IsNone, + _phantom: PhantomData, + } + } + + pub fn sum(self) -> NodeAggregated>> { + NodeAggregated { expr: SumExpr(UnwrapOptPropNodeExpr(self.expr)) } + } + + pub fn avg(self) -> NodeAggregated>> { + NodeAggregated { expr: AvgExpr(UnwrapOptPropNodeExpr(self.expr)) } + } + + pub fn min(self) -> NodeAggregated>> { + NodeAggregated { expr: MinExpr(UnwrapOptPropNodeExpr(self.expr)) } + } + + pub fn max(self) -> NodeAggregated>> { + NodeAggregated { expr: MaxExpr(UnwrapOptPropNodeExpr(self.expr)) } + } + + pub fn first(self) -> NodeAggregated>> { + NodeAggregated { expr: FirstExpr(UnwrapOptPropNodeExpr(self.expr)) } + } + + pub fn last(self) -> NodeAggregated>> { + NodeAggregated { expr: LastExpr(UnwrapOptPropNodeExpr(self.expr)) } + } + + pub fn len(self) -> LenExpr> { + LenExpr(UnwrapOptPropNodeExpr(self.expr)) + } + + pub fn any(self) -> NodeQuantified, AnyMode> { + NodeQuantified { + expr: UnwrapOptPropNodeExpr(self.expr), + _q: PhantomData, + } + } + + pub fn all(self) -> NodeQuantified, AllMode> { + NodeQuantified { + expr: UnwrapOptPropNodeExpr(self.expr), + _q: PhantomData, + } + } } // ───────────────────────────────────────────────────────────────────────────── @@ -878,68 +1391,83 @@ impl TemporalProp { name: name.into(), } } +} - fn make_expr(self) -> TemporalPropertyExpr { - TemporalPropertyExpr { - view_expr: self.view_expr, - name: self.name, - } - } +// ───────────────────────────────────────────────────────────────────────────── +// NodePropertyExprOps — fluent comparison API for node-side property expressions +// ───────────────────────────────────────────────────────────────────────────── - pub fn any(self) -> Quantified, AnyMode> { - Quantified { - expr: self.make_expr(), - _q: PhantomData, - } +pub trait NodePropertyExprOps: NodeExpr> + Sized { + fn is_in(self, values: impl IntoIterator) -> PropValueSetFilter { + PropValueSetFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsIn } } - - pub fn all(self) -> Quantified, AllMode> { - Quantified { - expr: self.make_expr(), - _q: PhantomData, - } + fn is_not_in(self, values: impl IntoIterator) -> PropValueSetFilter { + PropValueSetFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsNotIn } } +} - pub fn sum(self) -> Aggregated>> { - Aggregated { - expr: SumExpr(self.make_expr()), - } - } +impl NodePropertyExprOps + for PropertyExpr +{ +} - pub fn avg(self) -> Aggregated>> { - Aggregated { - expr: AvgExpr(self.make_expr()), - } - } +impl NodePropertyExprOps + for MetadataExpr +{ +} - pub fn min(self) -> Aggregated>> { - Aggregated { - expr: MinExpr(self.make_expr()), - } - } +// ───────────────────────────────────────────────────────────────────────────── +// NodeTemporalPropOps — fluent temporal API for node-side TemporalProp +// ───────────────────────────────────────────────────────────────────────────── - pub fn max(self) -> Aggregated>> { - Aggregated { - expr: MaxExpr(self.make_expr()), - } - } +pub trait NodeTemporalPropOps: Sized { + type ViewExpr: CreateView + NodeFilterFactory + Clone + Send + Sync + 'static; + fn into_temporal_parts(self) -> (Self::ViewExpr, String); - pub fn first(self) -> Aggregated>> { - Aggregated { - expr: FirstExpr(self.make_expr()), - } + fn any(self) -> NodeQuantified, AnyMode> { + let (view_expr, name) = self.into_temporal_parts(); + NodeQuantified { expr: TemporalPropertyExpr { view_expr, name }, _q: PhantomData } } - - pub fn last(self) -> Aggregated>> { - Aggregated { - expr: LastExpr(self.make_expr()), - } + fn all(self) -> NodeQuantified, AllMode> { + let (view_expr, name) = self.into_temporal_parts(); + NodeQuantified { expr: TemporalPropertyExpr { view_expr, name }, _q: PhantomData } } + fn sum(self) -> NodeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + NodeAggregated { expr: SumExpr(TemporalPropertyExpr { view_expr, name }) } + } + fn avg(self) -> NodeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + NodeAggregated { expr: AvgExpr(TemporalPropertyExpr { view_expr, name }) } + } + fn min(self) -> NodeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + NodeAggregated { expr: MinExpr(TemporalPropertyExpr { view_expr, name }) } + } + fn max(self) -> NodeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + NodeAggregated { expr: MaxExpr(TemporalPropertyExpr { view_expr, name }) } + } + fn first(self) -> NodeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + NodeAggregated { expr: FirstExpr(TemporalPropertyExpr { view_expr, name }) } + } + fn last(self) -> NodeAggregated>> { + let (view_expr, name) = self.into_temporal_parts(); + NodeAggregated { expr: LastExpr(TemporalPropertyExpr { view_expr, name }) } + } + fn len(self) -> LenExpr> { + let (view_expr, name) = self.into_temporal_parts(); + LenExpr(TemporalPropertyExpr { view_expr, name }) + } +} - pub fn len(self) -> Aggregated>> { - Aggregated { - expr: LenExpr(self.make_expr()), - } +impl NodeTemporalPropOps + for TemporalProp +{ + type ViewExpr = E; + fn into_temporal_parts(self) -> (E, String) { + (self.view_expr, self.name) } } @@ -1065,35 +1593,6 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { self.eq(Prop::Bool(false)) } - fn is_in(self, values: Iter) -> SetNodeFilter - where - Self: NodeExpr>, - Inner: Eq + Hash + Clone + Send + Sync + 'static, - Iter: IntoIterator, - { - let set: HashSet<_> = values.into_iter().collect(); - SetNodeFilter { - expr: self, - op: SetOp::IsIn, - values: Arc::new(set), - _phantom: PhantomData, - } - } - - fn is_not_in(self, values: Iter) -> SetNodeFilter - where - Self: NodeExpr>, - Inner: Eq + Hash + Clone + Send + Sync + 'static, - Iter: IntoIterator, - { - let set: HashSet<_> = values.into_iter().collect(); - SetNodeFilter { - expr: self, - op: SetOp::IsNotIn, - values: Arc::new(set), - _phantom: PhantomData, - } - } } impl NodeExprFilterOps for E {} @@ -1106,15 +1605,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) -> Quantified { - Quantified { + fn any(self) -> NodeQuantified { + NodeQuantified { expr: self, _q: PhantomData, } } - fn all(self) -> Quantified { - Quantified { + fn all(self) -> NodeQuantified { + NodeQuantified { expr: self, _q: PhantomData, } 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 index 0d14e116a6..7fcfc07580 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -15,6 +15,7 @@ mod tests; pub use exprs::*; pub use filters::*; pub use ops::*; +pub use super::{Metadata, Property}; // ───────────────────────────────────────────────────────────────────────────── // NodeExpr — typed node expression with associated Output type 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 index df8da53159..4414c7c48f 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -50,9 +50,12 @@ use crate::{ }, prelude::GraphViewOps, }; -use raphtory_api::core::entities::{ - properties::prop::{Prop, PropArray, PropType}, - VID, +use raphtory_api::core::{ + entities::{ + properties::prop::{Prop, PropArray, PropType}, + VID, + }, + storage::arc_str::ArcStr, }; use raphtory_storage::graph::graph::GraphStorage; use std::{collections::HashSet, hash::Hash, sync::Arc}; @@ -208,7 +211,7 @@ impl_agg_node_op!(FirstNodeOp, Option, |vals: Vec| { impl_agg_node_op!(LastNodeOp, Option, |vals: Vec| { vals.into_iter().last() }); -impl_agg_node_op!(LenNodeOp, usize, |vals: Vec| { vals.len() }); +impl_agg_node_op!(LenNodeOp, usize, |vals: Vec| vals.len()); // ───────────────────────────────────────────────────────────────────────────── // AnyNodeOp / AllNodeOp — unary reducers over a Prop::List of booleans @@ -323,6 +326,225 @@ impl<'g> NodeOp for PropListCompareOp<'g> { } } +// ───────────────────────────────────────────────────────────────────────────── +// PropListInSetOp<'g> — element-wise set-membership test on a Prop::List +// ───────────────────────────────────────────────────────────────────────────── + +/// Checks each element of a `Prop::List` against a fixed `Vec`, producing +/// `Prop::List([Bool, …])`. The result is then reduced by [`AnyNodeOp`] or [`AllNodeOp`]. +pub(crate) struct PropListInSetOp<'g> { + pub(crate) inner: Arc + 'g>, + pub(crate) values: Vec, + pub(crate) op: SetOp, +} + +impl<'g> Clone for PropListInSetOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + values: self.values.clone(), + op: self.op, + } + } +} + +impl<'g> NodeOp for PropListInSetOp<'g> { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Prop { + let Prop::List(arr) = self.inner.apply(storage, node) else { + return Prop::List(PropArray::from(vec![])); + }; + let bools: Vec = arr + .iter() + .map(|v| { + Prop::Bool(match self.op { + SetOp::IsIn => self.values.iter().any(|x| x == &v), + SetOp::IsNotIn => self.values.iter().all(|x| x != &v), + }) + }) + .collect(); + Prop::List(PropArray::from(bools)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// PropListStringOp<'g> — element-wise string comparison on a Prop::List +// ───────────────────────────────────────────────────────────────────────────── + +/// Applies a [`StringOp`] to each element of a `Prop::List` against a scalar RHS, +/// producing `Prop::List([Bool, …])`. Reduced by [`AnyNodeOp`] or [`AllNodeOp`]. +pub(crate) struct PropListStringOp<'g> { + pub(crate) inner: Arc + 'g>, + pub(crate) rhs: ArcStr, + pub(crate) op: StringOp, +} + +impl<'g> Clone for PropListStringOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + rhs: self.rhs.clone(), + op: self.op, + } + } +} + +impl<'g> NodeOp for PropListStringOp<'g> { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Prop { + let Prop::List(arr) = self.inner.apply(storage, node) else { + return Prop::List(PropArray::from(vec![])); + }; + let rhs = Some(Prop::Str(self.rhs.clone())); + let bools: Vec = arr + .iter() + .map(|v| Prop::Bool(Option::::string_cmp(&self.op, &Some(v), &rhs))) + .collect(); + Prop::List(PropArray::from(bools)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// NestedMapNodeOp<'g> — element-wise aggregation / quantification on a Prop::List +// ───────────────────────────────────────────────────────────────────────────── + +/// Applies a per-element operation to each element of an outer `Prop::List`. +/// +/// Used for chained expressions like `.temporal().any().sum()`: +/// the outer list is `Prop::List([list_t1, list_t2, …])` and for each inner +/// `list_ti` the op is applied, producing `Prop::List([result_t1, result_t2, …])`. +/// The outer `AnyNodeOp` / `AllNodeOp` then reduces the result list. +/// +/// Scalar elements are passed through unchanged. +pub(crate) struct NestedMapNodeOp<'g> { + pub(crate) inner: Arc + 'g>, + pub(crate) op: Op, +} + +impl<'g> Clone for NestedMapNodeOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + op: self.op, + } + } +} + +impl<'g> NodeOp for NestedMapNodeOp<'g> { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Prop { + let outer = self.inner.apply(storage, node); + let Prop::List(arr) = outer else { + return Prop::List(PropArray::from(vec![])); + }; + let mapped: Vec = arr + .iter() + .map(|elem| match elem { + Prop::List(inner_arr) => { + let vals: Vec = inner_arr.iter().collect(); + match self.op { + Op::Sum => aggregate_values(&vals, Op::Sum) + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Avg => aggregate_values(&vals, Op::Avg) + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Min => aggregate_values(&vals, Op::Min) + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Max => aggregate_values(&vals, Op::Max) + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::First => vals.into_iter().next() + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Last => vals.into_iter().last() + .unwrap_or(Prop::List(PropArray::from(vec![]))), + Op::Len => Prop::U64(inner_arr.len() as u64), + Op::Any => Prop::Bool(prop_any(&Prop::List(inner_arr))), + Op::All => Prop::Bool(prop_all(&Prop::List(inner_arr))), + } + } + other => other, + }) + .collect(); + Prop::List(PropArray::from(mapped)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// UnwrapOptPropOp<'g> — converts Option → Prop for nested aggregation +// ───────────────────────────────────────────────────────────────────────────── + +/// Converts `Option` → `Prop` so that aggregator ops (`SumNodeOp`, etc.) +/// can operate on a value produced by a prior aggregation step. +/// +/// Used internally when chaining e.g. `.temporal().last().sum()`: +/// `LastExpr` outputs `Option { + pub(crate) inner: Arc> + 'g>, +} + +impl<'g> Clone for UnwrapOptPropOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl<'g> NodeOp for UnwrapOptPropOp<'g> { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Prop { + match self.inner.apply(storage, node) { + Some(Prop::List(arr)) => Prop::List(arr), + Some(v) => Prop::List(PropArray::from(vec![v])), + None => Prop::List(PropArray::from(vec![])), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// PropValueSetNodeOp<'g> — is_in / is_not_in for Option (linear scan) +// ───────────────────────────────────────────────────────────────────────────── + +/// Checks whether an `Option` value is in (or not in) a fixed `Vec`. +/// Uses linear scan because `Prop` may contain floats (`F32`, `F64`) which don't +/// implement `Hash`. +pub struct PropValueSetNodeOp<'g> { + pub(crate) inner: Arc> + 'g>, + pub(crate) values: Vec, + pub(crate) op: SetOp, +} + +impl<'g> Clone for PropValueSetNodeOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + values: self.values.clone(), + op: self.op, + } + } +} + +impl<'g> NodeOp for PropValueSetNodeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + match self.inner.apply(storage, node) { + None => false, + Some(v) => match self.op { + SetOp::IsIn => self.values.iter().any(|x| x == &v), + SetOp::IsNotIn => self.values.iter().all(|x| x != &v), + }, + } + } +} + // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpNodeOp<'g, T> — compares two NodeOp using BinaryOp // ───────────────────────────────────────────────────────────────────────────── 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 fe22e04944..4ce5ce6d5f 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 @@ -78,7 +78,6 @@ pub trait NodeFilterFactory: PropertyFilterFactory + Clone { /// Build a filter from a boolean column inside a TypedNodeState. fn by_column<'graph, V, G, T>( - &self, state: &TypedNodeState<'graph, V, G, T>, col: &str, ) -> Result @@ -169,6 +168,12 @@ impl InternalViewWrapOps for NodeFilter { } } +impl NodeFilterFactory for Windowed {} +impl NodeFilterFactory for Latest {} +impl NodeFilterFactory for SnapshotAt {} +impl NodeFilterFactory for SnapshotLatest {} +impl NodeFilterFactory for Layered {} + #[derive(Debug, Clone)] pub struct NodeIdFilter(pub Filter); 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 f34a9c5904..c56a923751 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -4,7 +4,7 @@ use crate::{ graph::views::filter::{ model::{ edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, + CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, @@ -113,6 +113,17 @@ impl CreateFilter for SnapshotA impl ComposableFilter for SnapshotAt {} +impl CreateView for SnapshotAt { + type View<'graph, G: GraphView + 'graph> = T::View<'graph, G>; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + self.inner.create_view(view) + } +} + impl Wrap for SnapshotAt { type Wrapped = SnapshotAt; fn wrap(&self, value: T) -> Self::Wrapped { @@ -211,6 +222,17 @@ impl CreateFilter for SnapshotL impl ComposableFilter for SnapshotLatest {} +impl CreateView for SnapshotLatest { + type View<'graph, G: GraphView + 'graph> = T::View<'graph, G>; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + self.inner.create_view(view) + } +} + impl Wrap for SnapshotLatest { type Wrapped = SnapshotLatest; fn wrap(&self, value: T) -> Self::Wrapped { diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 980816865f..833389f3c4 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -147,14 +147,19 @@ pub mod prelude { }; pub use crate::db::graph::views::filter::model::{ - filter::Filter, property_filter::PropertyFilter, + filter::Filter, property_filter::PropertyFilter, NodeAggregated, NodeQuantified, + NodeExprFilterOps, NodePropertyExprOps, NodeTemporalPropOps, + EdgeExprFilterOps, EdgePropertyExprOps, EdgeTemporalPropOps, }; pub use crate::db::graph::views::{ property_redacted_graph::PropertyRedaction, PropertyRedactedGraph, }; - pub use crate::db::graph::views::filter::model::{node_filter::NodeFilter, EdgeFilter}; + pub use crate::db::graph::views::filter::model::{ + node_filter::{NodeFilter, NodeFilterFactory}, + EdgeFilter, + }; pub use storage::{persist::config::ConfigOps, Config}; From 02bb98439cdff00b7c42a5109399924ff3ab5178 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:09:05 +0100 Subject: [PATCH 024/155] output type is now option of prop, ref --- raphtory-api/src/core/entities/mod.rs | 11 +- .../entities/properties/prop/prop_enum.rs | 12 + .../views/filter/model/edge_expr/exprs.rs | 120 +- .../graph/views/filter/model/edge_expr/mod.rs | 47 +- .../graph/views/filter/model/edge_expr/ops.rs | 140 +-- .../src/db/graph/views/filter/model/mod.rs | 23 +- .../views/filter/model/node_expr/exprs.rs | 301 ++--- .../views/filter/model/node_expr/filters.rs | 942 +-------------- .../graph/views/filter/model/node_expr/mod.rs | 11 +- .../graph/views/filter/model/node_expr/ops.rs | 252 ++--- .../filter/model/property_filter/evaluate.rs | 1006 +++++++++-------- 11 files changed, 886 insertions(+), 1979 deletions(-) diff --git a/raphtory-api/src/core/entities/mod.rs b/raphtory-api/src/core/entities/mod.rs index 18dd85f92b..562ac55c46 100644 --- a/raphtory-api/src/core/entities/mod.rs +++ b/raphtory-api/src/core/entities/mod.rs @@ -14,7 +14,7 @@ pub mod edges; pub mod layers; pub mod properties; -use crate::core::entities::properties::prop::PropType; +use crate::core::entities::properties::prop::{Prop, PropType}; pub use layers::*; // The only reason this is public is because the physical IDs of the nodes don’t move. @@ -243,6 +243,15 @@ pub enum GID { Str(String), } +impl From for Prop { + fn from(gid: GID) -> Self { + match gid { + GID::U64(id) => Prop::U64(id), + GID::Str(str) => Prop::Str(str.into()), + } + } +} + impl PartialEq for GID { fn eq(&self, other: &str) -> bool { match self { diff --git a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs index 132f02e06b..060e8c1b15 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs @@ -158,6 +158,18 @@ pub enum Prop { Decimal(BigDecimal), } +impl From for Prop { + fn from(value: usize) -> Self { + Prop::U64(value as u64) + } +} + +impl FromIterator for PropArray { + fn from_iter>(iter: T) -> Self { + PropArray::Vec(Vec::from_iter(iter).into()) + } +} + impl From> for Prop { fn from(value: GidRef<'_>) -> Self { match value { diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs index c291ca073a..9f5ddb3574 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs @@ -1,12 +1,6 @@ //! EdgeExpr impls for the shared Property/Metadata structs and scalar types. -use super::{ - ops::{ - AvgEdgeOp, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, NestedMapEdgeOp, - SumEdgeOp, TemporalEdgePropOp, UnwrapOptPropEdgeOp, - }, - EdgeExpr, EdgeMetaOp, EdgeOp, EdgePropOp, -}; +use super::{EdgeExpr, EdgeMetaOp, EdgeOp, EdgePropOp, LenEdgeOp, TemporalEdgePropOp}; use crate::db::graph::views::filter::model::property_filter::Op; use crate::{ db::api::{state::ops::Const, view::internal::GraphView}, @@ -15,7 +9,7 @@ use crate::{ }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; - +use crate::db::graph::views::filter::model::node_expr::{AvgEdgeOp, FirstEdgeOp, LastEdgeOp, MaxEdgeOp, MinEdgeOp, SumEdgeOp}; // ───────────────────────────────────────────────────────────────────────────── // Property / Metadata — EdgeExpr impls // ───────────────────────────────────────────────────────────────────────────── @@ -177,36 +171,6 @@ impl { - /// Produced by `EdgeTemporalProp::$op_lower()` — wraps a `TemporalEdgePropExpr`. - #[derive(Clone)] - pub struct $name>(pub E); - - impl> EdgeExpr for $name { - type Output = Option; - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.0.create_edge_op(graph)?; - Ok(Arc::new($op_ty { inner })) - } - } - }; -} - -impl_agg_edge_expr!(SumEdgeExpr, SumEdgeOp); -impl_agg_edge_expr!(AvgEdgeExpr, AvgEdgeOp); -impl_agg_edge_expr!(MinEdgeExpr, MinEdgeOp); -impl_agg_edge_expr!(MaxEdgeExpr, MaxEdgeOp); -impl_agg_edge_expr!(FirstEdgeExpr, FirstEdgeOp); -impl_agg_edge_expr!(LastEdgeExpr, LastEdgeOp); // LenEdgeExpr written explicitly: Output = usize, not Option #[derive(Clone)] pub struct LenEdgeExpr>(pub E); @@ -227,48 +191,48 @@ impl> EdgeExpr for LenEdgeExpr { // UnwrapOptPropEdgeExpr — bridges Option → Prop for nested aggregation // ───────────────────────────────────────────────────────────────────────────── -/// Bridges `E: EdgeExpr>` to `EdgeExpr`, -/// enabling aggregator exprs to operate on values produced by a prior aggregation. -/// -/// Used when chaining e.g. `.temporal().last().sum()`: -/// `last()` produces `EdgeAggregated>` with `Output = Option`; -/// `sum()` wraps in `SumEdgeExpr>>`. -#[derive(Clone)] -pub struct UnwrapOptPropEdgeExpr>>(pub E); - -impl>> EdgeExpr for UnwrapOptPropEdgeExpr { - type Output = Prop; - - fn create_edge_op<'g, G: crate::db::api::view::internal::GraphView + 'g>( - &self, - graph: G, - ) -> Result + 'g>, crate::errors::GraphError> { - let inner = self.0.create_edge_op(graph)?; - Ok(std::sync::Arc::new(UnwrapOptPropEdgeOp { inner })) - } -} +// Bridges `E: EdgeExpr>` to `EdgeExpr`, +// enabling aggregator exprs to operate on values produced by a prior aggregation. +// +// Used when chaining e.g. `.temporal().last().sum()`: +// `last()` produces `EdgeAggregated>` with `Output = Option`; +// `sum()` wraps in `SumEdgeExpr>>`. +// #[derive(Clone)] +// pub struct UnwrapOptPropEdgeExpr>>(pub E); +// +// impl>> EdgeExpr for UnwrapOptPropEdgeExpr { +// type Output = Prop; +// +// fn create_edge_op<'g, G: crate::db::api::view::internal::GraphView + 'g>( +// &self, +// graph: G, +// ) -> Result + 'g>, crate::errors::GraphError> { +// let inner = self.0.create_edge_op(graph)?; +// Ok(std::sync::Arc::new(UnwrapOptPropEdgeOp { inner })) +// } +// } // ───────────────────────────────────────────────────────────────────────────── // NestedMapEdgeExpr — per-element aggregation / quantification on a Prop::List // ───────────────────────────────────────────────────────────────────────────── -/// Applies a per-element op to each element of a `Prop::List` produced by `E`. -/// -/// Used for chained expressions like `.temporal().any().sum()`. -#[derive(Clone)] -pub struct NestedMapEdgeExpr> { - pub inner: E, - pub op: Op, -} - -impl> EdgeExpr for NestedMapEdgeExpr { - type Output = Prop; - - fn create_edge_op<'g, G: crate::db::api::view::internal::GraphView + 'g>( - &self, - graph: G, - ) -> Result + 'g>, crate::errors::GraphError> { - let inner = self.inner.create_edge_op(graph)?; - Ok(std::sync::Arc::new(NestedMapEdgeOp { inner, op: self.op })) - } -} +// Applies a per-element op to each element of a `Prop::List` produced by `E`. +// +// Used for chained expressions like `.temporal().any().sum()`. +// #[derive(Clone)] +// pub struct NestedMapEdgeExpr> { +// pub inner: E, +// pub op: Op, +// } +// +// impl> EdgeExpr for NestedMapEdgeExpr { +// type Output = Prop; +// +// fn create_edge_op<'g, G: crate::db::api::view::internal::GraphView + 'g>( +// &self, +// graph: G, +// ) -> Result + 'g>, crate::errors::GraphError> { +// let inner = self.inner.create_edge_op(graph)?; +// Ok(std::sync::Arc::new(NestedMapEdgeOp { inner, op: self.op })) +// } +// } diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index baa5d3132a..edf9b2f88a 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -48,6 +48,7 @@ pub mod ops; pub use exprs::*; pub use filters::*; pub use ops::*; +use crate::db::graph::views::filter::model::{AvgExpr, FirstExpr, LastExpr, MaxExpr, MinExpr, SumExpr}; pub use super::{Metadata, Property}; // ───────────────────────────────────────────────────────────────────────────── @@ -324,29 +325,29 @@ pub trait EdgeTemporalPropOps: Sized { let (view_expr, name) = self.into_temporal_parts(); EdgeQuantified { expr: TemporalEdgePropExpr::new(view_expr, name), _q: PhantomData } } - fn sum(self) -> EdgeAggregated>> { + fn sum(self) -> EdgeAggregated>> { let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: SumEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + EdgeAggregated { expr: SumExpr(TemporalEdgePropExpr::new(view_expr, name)) } } - fn avg(self) -> EdgeAggregated>> { + fn avg(self) -> EdgeAggregated>> { let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: AvgEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + EdgeAggregated { expr: AvgExpr(TemporalEdgePropExpr::new(view_expr, name)) } } - fn min(self) -> EdgeAggregated>> { + fn min(self) -> EdgeAggregated>> { let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: MinEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + EdgeAggregated { expr: MinExpr(TemporalEdgePropExpr::new(view_expr, name)) } } - fn max(self) -> EdgeAggregated>> { + fn max(self) -> EdgeAggregated>> { let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: MaxEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + EdgeAggregated { expr: MaxExpr(TemporalEdgePropExpr::new(view_expr, name)) } } - fn first(self) -> EdgeAggregated>> { + fn first(self) -> EdgeAggregated>> { let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: FirstEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + EdgeAggregated { expr: FirstExpr(TemporalEdgePropExpr::new(view_expr, name)) } } - fn last(self) -> EdgeAggregated>> { + fn last(self) -> EdgeAggregated>> { let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: LastEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) } + EdgeAggregated { expr: LastExpr(TemporalEdgePropExpr::new(view_expr, name)) } } fn len(self) -> LenEdgeExpr> { let (view_expr, name) = self.into_temporal_parts(); @@ -503,22 +504,22 @@ impl>> EdgeAggregated { } } - pub fn sum(self) -> EdgeAggregated>> { - EdgeAggregated { expr: SumEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + pub fn sum(self) -> EdgeAggregated>> { + EdgeAggregated { expr: SumExpr(UnwrapOptPropEdgeExpr(self.expr)) } } - pub fn avg(self) -> EdgeAggregated>> { - EdgeAggregated { expr: AvgEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + pub fn avg(self) -> EdgeAggregated>> { + EdgeAggregated { expr: AvgExpr(UnwrapOptPropEdgeExpr(self.expr)) } } - pub fn min(self) -> EdgeAggregated>> { - EdgeAggregated { expr: MinEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + pub fn min(self) -> EdgeAggregated>> { + EdgeAggregated { expr: MinExpr(UnwrapOptPropEdgeExpr(self.expr)) } } - pub fn max(self) -> EdgeAggregated>> { - EdgeAggregated { expr: MaxEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + pub fn max(self) -> EdgeAggregated>> { + EdgeAggregated { expr: MaxExpr(UnwrapOptPropEdgeExpr(self.expr)) } } - pub fn first(self) -> EdgeAggregated>> { - EdgeAggregated { expr: FirstEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + pub fn first(self) -> EdgeAggregated>> { + EdgeAggregated { expr: FirstExpr(UnwrapOptPropEdgeExpr(self.expr)) } } - pub fn last(self) -> EdgeAggregated>> { + pub fn last(self) -> EdgeAggregated>> { EdgeAggregated { expr: LastEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } } pub fn len(self) -> LenEdgeExpr> { diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index f054674f10..9d34a6c9e3 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -2,16 +2,15 @@ //! //! Parallel to `node_expr/ops.rs` — same design, different subject. +use std::collections::HashSet; +use std::hash::Hash; use crate::db::{ api::{ properties::internal::{InternalMetadataOps, InternalTemporalPropertyViewOps}, state::ops::Const, view::internal::GraphView, }, - graph::{ - edge::EdgeView, - views::filter::model::property_filter::evaluate::aggregate_values, - }, + graph::edge::EdgeView, }; use raphtory_api::core::entities::{ edges::edge_ref::EdgeRef, @@ -21,7 +20,8 @@ use raphtory_storage::graph::graph::GraphStorage; use super::EdgeOp; use std::sync::Arc; - +use raphtory_api::core::entities::properties::prop::PropArray; +use raphtory_api::core::storage::arc_str::ArcStr; // ───────────────────────────────────────────────────────────────────────────── // Arc — blanket impl so Arc-boxed ops satisfy EdgeOp // ───────────────────────────────────────────────────────────────────────────── @@ -148,6 +148,7 @@ impl<'g, L: Comparable + Clone + Send + Sync + 'static> EdgeOp for BinaryCmpEdge // ───────────────────────────────────────────────────────────────────────────── use crate::db::graph::views::filter::model::filter_operator::UnaryOp; +use crate::db::graph::views::filter::model::{SetOp, StringComparable, StringOp}; #[derive(Clone)] pub(crate) struct UnaryEdgeOp<'g, I: Clone + Send + Sync + 'static> { @@ -167,36 +168,6 @@ impl<'g, I: Clone + Send + Sync + 'static> EdgeOp for UnaryEdgeOp<'g, I> { } } -// ───────────────────────────────────────────────────────────────────────────── -// AnyEdgeOp<'g> / AllEdgeOp<'g> — quantifier ops over Prop::List -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct AnyEdgeOp<'g> { - pub(crate) inner: Arc + 'g>, -} - -impl<'g> EdgeOp for AnyEdgeOp<'g> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - self.inner.apply(storage, edge) - } -} - -#[derive(Clone)] -pub(crate) struct AllEdgeOp<'g> { - pub(crate) inner: Arc + 'g>, -} - -impl<'g> EdgeOp for AllEdgeOp<'g> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - self.inner.apply(storage, edge) - } -} - // ───────────────────────────────────────────────────────────────────────────── // PropListEdgeCmpOp<'g> — compares each element of a Prop::List against a RHS // ───────────────────────────────────────────────────────────────────────────── @@ -233,52 +204,6 @@ impl<'g> EdgeOp for PropListEdgeCmpOp<'g> { } } -// ───────────────────────────────────────────────────────────────────────────── -// Aggregator EdgeOps — reduce a Prop::List to a scalar Option -// ───────────────────────────────────────────────────────────────────────────── - -macro_rules! impl_agg_edge_op { - ($name:ident, |$vals:ident: Vec| $body:expr) => { - #[derive(Clone)] - pub(crate) struct $name<'g> { - pub(crate) inner: Arc + 'g>, - } - - impl<'g> EdgeOp for $name<'g> { - type Output = Option; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let list_prop = self.inner.apply(storage, edge); - let $vals: Vec = match list_prop { - Prop::List(v) => v.iter().collect(), - _ => return None, - }; - if $vals.is_empty() { - return None; - } - $body - } - } - }; -} - -use crate::db::graph::views::filter::model::{ - filter_operator::{SetOp, StringComparable, StringOp}, - property_filter::Op, -}; -use raphtory_api::core::{ - entities::properties::prop::PropArray, - storage::arc_str::ArcStr, -}; -use std::collections::HashSet; -use std::hash::Hash; - -impl_agg_edge_op!(SumEdgeOp, |vals: Vec| aggregate_values(&vals, Op::Sum)); -impl_agg_edge_op!(AvgEdgeOp, |vals: Vec| aggregate_values(&vals, Op::Avg)); -impl_agg_edge_op!(MinEdgeOp, |vals: Vec| aggregate_values(&vals, Op::Min)); -impl_agg_edge_op!(MaxEdgeOp, |vals: Vec| aggregate_values(&vals, Op::Max)); -impl_agg_edge_op!(FirstEdgeOp, |vals: Vec| vals.into_iter().next()); -impl_agg_edge_op!(LastEdgeOp, |vals: Vec| vals.into_iter().last()); // LenEdgeOp written explicitly: Output = usize, not Option #[derive(Clone)] pub(crate) struct LenEdgeOp<'g> { @@ -511,56 +436,3 @@ impl<'g> EdgeOp for UnwrapOptPropEdgeOp<'g> { } } -// ───────────────────────────────────────────────────────────────────────────── -// NestedMapEdgeOp<'g> — element-wise aggregation / quantification on a Prop::List -// ───────────────────────────────────────────────────────────────────────────── - -/// Applies a per-element operation to each element of an outer `Prop::List`. -/// -/// Used for chained expressions like `.temporal().any().sum()`: -/// the outer list is `Prop::List([list_t1, list_t2, …])` and for each inner -/// `list_ti` the op is applied, producing `Prop::List([result_t1, result_t2, …])`. -/// The outer `AnyPropEdgeOp` / `AllPropEdgeOp` then reduces the result list. -#[derive(Clone)] -pub(crate) struct NestedMapEdgeOp<'g> { - pub(crate) inner: Arc + 'g>, - pub(crate) op: Op, -} - -impl<'g> EdgeOp for NestedMapEdgeOp<'g> { - type Output = Prop; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Prop { - let outer = self.inner.apply(storage, edge); - let Prop::List(arr) = outer else { - return Prop::List(PropArray::from(vec![])); - }; - let mapped: Vec = arr - .iter() - .map(|elem| match elem { - Prop::List(inner_arr) => { - let vals: Vec = inner_arr.iter().collect(); - match self.op { - Op::Sum => aggregate_values(&vals, Op::Sum) - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Avg => aggregate_values(&vals, Op::Avg) - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Min => aggregate_values(&vals, Op::Min) - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Max => aggregate_values(&vals, Op::Max) - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::First => vals.into_iter().next() - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Last => vals.into_iter().last() - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Len => Prop::U64(inner_arr.len() as u64), - Op::Any => Prop::Bool(prop_any_edge(&Prop::List(inner_arr))), - Op::All => Prop::Bool(prop_all_edge(&Prop::List(inner_arr))), - } - } - other => other, - }) - .collect(); - Prop::List(PropArray::from(mapped)) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 53ada61ffe..e0b1d59b2f 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -15,8 +15,8 @@ pub use crate::{ UnaryOp, }, node_expr::{ - AllMode, AnyMode, AvgExpr, BinaryCmpNodeFilter, ConstExpr, - DegreeExpr, FirstExpr, IntoPropNodeExpr, LastExpr, LenExpr, MaxExpr, + AllMode, AnyMode, BinaryCmpNodeFilter, + FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, NodeAggregated, NodeExpr, NodeExprFilterOps, NodePropertyExprOps, NodeQuantified, NodeTemporalPropOps, PropValueSetFilter, QuantifiedNodeFilter, QuantifierMode, SetNodeFilter, StringNodeFilter, SumExpr, TemporalExprOps, @@ -293,15 +293,16 @@ pub struct PropertyExpr { name: String, } +impl EntityExpr for PropertyExpr {} + impl NodeExpr for PropertyExpr { - type Output = Option; fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, - ) -> Result + 'g>, GraphError> { + ) -> Result> + 'g>, GraphError> { let prop_id = graph .node_meta() .get_prop_id(&self.name, false) @@ -317,15 +318,15 @@ pub struct MetadataExpr { name: String, } +impl EntityExpr for MetadataExpr {} + impl NodeExpr for MetadataExpr { - type Output = Option; - fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, - ) -> Result + 'g>, GraphError> { + ) -> Result> + 'g>, GraphError> { let prop_id = graph .node_meta() .get_prop_id(&self.name, true) @@ -453,8 +454,10 @@ pub trait EdgeFilterFactory: PropertyFilterFactory + Clone {} // ───────────────────────────────────────────────────────────────────────────── use edge_expr::{ - EdgeExpr, EdgeMetaOp as EMetaOp, EdgeOp, EdgePropOp as EPropOp, + EdgeExpr, EdgeOp }; +use crate::db::graph::views::filter::model::edge_expr::{EdgeMetaOp, EdgePropOp}; +use crate::db::graph::views::filter::model::node_expr::EntityExpr; impl EdgeExpr for PropertyExpr @@ -470,7 +473,7 @@ impl EdgeExpr .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(EPropOp { graph, prop_id })) + Ok(Arc::new(EdgePropOp { graph, prop_id })) } } @@ -488,7 +491,7 @@ impl EdgeExpr .get_prop_id(&self.name, true) .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; let graph = self.view_expr.create_view(graph)?; - Ok(Arc::new(EMetaOp { graph, prop_id })) + Ok(Arc::new(EdgeMetaOp { graph, prop_id })) } } 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 eb54f549eb..20c0eb7ad7 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 @@ -7,7 +7,7 @@ //! # Field expressions //! //! ```rust,ignore -//! NodeFilter::id() // Id — NodeExpr — e.g. .eq(GID::Str("v1".into())) +//! 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() //! ``` @@ -59,16 +59,10 @@ //! // ConstExpr for custom comparable types not covered above //! ``` -use super::{ - filters::{ - BinaryCmpNodeFilter, SetNodeFilter, StringNodeFilter, UnaryNodeFilter, - }, - ops::{ - AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NestedMapNodeOp, - NodeMetaOp, NodePropOp, SumNodeOp, TemporalNodePropOp, UnwrapOptPropOp, - }, - NodeExpr, -}; +use super::{ops::{ + AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NodeMetaOp, + NodePropOp, SumNodeOp, TemporalNodePropOp, +}, AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, NodeExpr, SumEdgeOp}; use crate::{ db::{ api::{ @@ -76,14 +70,13 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::model::{ - filter_operator::{BinaryOp, Comparable, SetOp, StringOp, UnaryOp}, - node_filter::NodeFilter, - property_filter::Op, - CreateView, Metadata, Property, + filter_operator::Comparable, node_filter::NodeFilter, CreateView, + Metadata, Property, }, }, errors::GraphError, }; +use raphtory_api::core::entities::properties::prop::IntoProp; use raphtory_api::core::{ entities::{ properties::prop::{Prop, PropType}, @@ -92,8 +85,7 @@ use raphtory_api::core::{ storage::arc_str::ArcStr, Direction, }; -use std::{collections::HashSet, marker::PhantomData, sync::Arc}; - +use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── // Node field expressions — identity, name, type // @@ -104,55 +96,55 @@ use std::{collections::HashSet, marker::PhantomData, sync::Arc}; // NodeFilter::node_type() uses Type — NodeExpr> // ───────────────────────────────────────────────────────────────────────────── -impl NodeExpr for Id { - type Output = GID; +impl EntityExpr for Id {} +impl NodeExpr for Id { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result + 'g>, GraphError> { - Ok(Arc::new(Id)) + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Id.map(|a| Some(a.into_prop())))) } } -impl NodeExpr for GID { - type Output = GID; +impl EntityExpr for GID {} +impl NodeExpr for GID { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result + 'g>, GraphError> { - Ok(Arc::new(Const(self.clone()))) + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.clone().into_prop())))) } } -impl NodeExpr for Name { - type Output = String; +impl EntityExpr for Name { + fn prop_type(&self) -> PropType { + PropType::Str + } +} +impl NodeExpr for Name { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result + 'g>, GraphError> { - Ok(Arc::new(Name)) + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Name.map(|a| Some(a.into_prop())))) } +} +impl EntityExpr for Type { 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 + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Type.map(|a| a.map(|b| b.into_prop())))) } } @@ -165,63 +157,63 @@ impl NodeExpr for Type { // NodeFilter::property("age").gt(30i64) // ───────────────────────────────────────────────────────────────────────────── -impl NodeExpr for usize { - type Output = usize; +impl EntityExpr for usize { + fn prop_type(&self) -> PropType { + PropType::U64 + } +} +impl NodeExpr for usize { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result + 'g>, GraphError> { - Ok(Arc::new(Const(*self))) + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(*self.into()))) } +} +impl EntityExpr for String { fn prop_type(&self) -> PropType { - PropType::U64 + PropType::Str } } 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()))) + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(self.clone().into_prop()))) } +} +impl EntityExpr for ArcStr { 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())))) + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.clone().into_prop())))) } +} +impl EntityExpr for &'static str { 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 + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(*self.into_prop())))) } } @@ -233,61 +225,35 @@ impl NodeExpr for &'static str { // with a single method name. // ───────────────────────────────────────────────────────────────────────────── -pub trait IntoPropNodeExpr { - type Expr: NodeExpr>; - fn into_prop_node_expr(self) -> Self::Expr; -} - -// Blanket: anything already NodeExpr> passes through unchanged. -// Covers Prop, i64, u64, i32, u32, f32, f64, bool, u8, u16, Property, Metadata, etc. -impl>> IntoPropNodeExpr for T { - type Expr = T; - fn into_prop_node_expr(self) -> T { - self - } -} - -// &'static str has Output = &'static str, so the blanket above does NOT cover it. -// Convert to Prop::Str so .eq("Alice") works transparently. -impl IntoPropNodeExpr for &'static str { - type Expr = Prop; - fn into_prop_node_expr(self) -> Prop { - Prop::Str(ArcStr::from(self)) - } -} - -impl IntoPropNodeExpr for String { - type Expr = Prop; - fn into_prop_node_expr(self) -> Prop { - Prop::Str(ArcStr::from(self)) +impl EntityExpr for Prop { + fn prop_type(&self) -> PropType { + self.dtype() } } 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; + impl EntityExpr for $prim { + fn prop_type(&self) -> PropType { + PropType::$variant + } + } + impl NodeExpr for $prim { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(Prop::$variant(*self))))) + Ok(Arc::new(Const(Some(*self)))) } } }; @@ -315,13 +281,13 @@ impl_node_expr_for_numeric!(u16, U16); #[derive(Clone)] pub struct ConstExpr(pub T); -impl NodeExpr for ConstExpr { - type Output = T; +impl EntityExpr for ConstExpr {} +impl NodeExpr for ConstExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result + 'g>, GraphError> { + ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Const(self.0.clone()))) } } @@ -346,23 +312,30 @@ pub struct DegreeExpr { pub view_expr: E, } -impl NodeExpr for DegreeExpr { - type Output = usize; +impl EntityExpr for DegreeExpr { + fn prop_type(&self) -> PropType { + PropType::U64 + } +} +impl NodeExpr for DegreeExpr { 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)?, - })) + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new( + Degree { + dir: self.dir, + view: self.view_expr.create_view(graph)?, + } + .map(|a| Some(Prop::U64(a.into()))), + )) } } -impl NodeExpr for Property { - type Output = Option; +impl EntityExpr for Property {} +impl NodeExpr for Property { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -375,9 +348,9 @@ impl NodeExpr for Property { } } -impl NodeExpr for Metadata { - type Output = Option; +impl EntityExpr for Metadata {} +impl NodeExpr for Metadata { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -422,19 +395,21 @@ impl TemporalPropertyExpr { } } -impl NodeExpr for TemporalPropertyExpr { - type Output = Prop; +impl EntityExpr for TemporalPropertyExpr {} +impl NodeExpr for TemporalPropertyExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, - ) -> Result + 'g>, GraphError> { + ) -> 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 })) + Ok(Arc::new( + TemporalNodePropOp { graph, prop_id }.map(|a| Some(a)), + )) } } @@ -453,90 +428,40 @@ impl NodeExpr for TemporalPropert // ───────────────────────────────────────────────────────────────────────────── 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()) - } - } + ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident) => { + #[derive(Clone)] + pub struct $expr(pub E); - impl> NodeExpr for $expr { - type Output = $output; + impl EntityExpr for $expr {} + impl NodeExpr for $expr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, - ) -> Result + 'g>, GraphError> { + ) -> Result> + 'g>, GraphError> { let inner = self.0.create_node_op(graph)?; - Ok(Arc::new($op_ty { inner })) + Ok(Arc::new($node_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); - -// ───────────────────────────────────────────────────────────────────────────── -// UnwrapOptPropNodeExpr — bridges Option → Prop for nested aggregation -// ───────────────────────────────────────────────────────────────────────────── - -// ───────────────────────────────────────────────────────────────────────────── -// NestedMapExpr — per-element aggregation / quantification on a Prop::List -// ───────────────────────────────────────────────────────────────────────────── - -/// Applies a per-element operation to each element of a `Prop::List` produced by `E`. -/// -/// Used for chained expressions like `.temporal().any().sum()`: -/// `E` produces `Prop::List([list_t1, list_t2, …])` and each inner `list_ti` is -/// aggregated, yielding `Prop::List([result_t1, result_t2, …])` which is then -/// further quantified by `AnyNodeOp` / `AllNodeOp`. -#[derive(Clone)] -pub struct NestedMapExpr> { - pub inner: E, - pub op: Op, -} - -impl> NodeExpr for NestedMapExpr { - type Output = Prop; - - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result + 'g>, GraphError> { - let inner = self.inner.create_node_op(graph)?; - Ok(Arc::new(NestedMapNodeOp { - inner, - op: self.op, - })) - } + impl EdgeExpr for $expr { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.0.create_edge_op(graph)?; + Ok(Arc::new($edge_op_ty { inner })) + } + } + }; } -/// Bridges `E: NodeExpr>` to `NodeExpr`, -/// enabling aggregator exprs (`SumExpr`, `AnyMode`, etc.) to operate on values -/// produced by a prior aggregation step. -/// -/// Used when chaining e.g. `.temporal().last().sum()`: -/// `last()` produces `NodeAggregated>` with `Output = Option`; -/// `sum()` on that wraps in `SumExpr>>`. -#[derive(Clone)] -pub struct UnwrapOptPropNodeExpr>>(pub E); - -impl>> NodeExpr for UnwrapOptPropNodeExpr { - type Output = Prop; - - 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(UnwrapOptPropOp { inner })) - } -} +impl_agg_expr!(SumExpr, SumNodeOp, SumEdgeOp); +impl_agg_expr!(AvgExpr, AvgNodeOp, AvgEdgeOp); +impl_agg_expr!(MinExpr, MinNodeOp, MinEdgeOp); +impl_agg_expr!(MaxExpr, MaxNodeOp, MaxEdgeOp); +impl_agg_expr!(FirstExpr, FirstNodeOp, FirstEdgeOp); +impl_agg_expr!(LastExpr, LastNodeOp, LastEdgeOp); +impl_agg_expr!(LenExpr, LenNodeOp, LenEdgeOp); +impl_agg_expr!(AnyExpr, AnyNodeOp, AnyEdgeOp); +impl_agg_expr!(AllExpr, AllNodeOp, AllEdgeOp); 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 5e05556153..ccc4f72944 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 @@ -40,8 +40,7 @@ use super::{ AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, PropListCompareOp, PropListInSetOp, PropListStringOp, PropValueSetNodeOp, SetNodeOp, StringNodeOp, UnaryNodeOp, }, - AvgExpr, FirstExpr, IntoPropNodeExpr, LastExpr, LenExpr, MaxExpr, MinExpr, NestedMapExpr, - NodeExpr, SumExpr, TemporalPropertyExpr, UnwrapOptPropNodeExpr, + NodeExpr, TemporalPropertyExpr, }; use crate::{ db::{ @@ -74,38 +73,7 @@ use raphtory_api::core::{ storage::arc_str::ArcStr, }; use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; - -// ───────────────────────────────────────────────────────────────────────────── -// Sealed trait for QuantifierMode -// ───────────────────────────────────────────────────────────────────────────── - -mod sealed { - pub trait Sealed {} -} - -// ───────────────────────────────────────────────────────────────────────────── -// QuantifierMode — AnyMode / AllMode -// ───────────────────────────────────────────────────────────────────────────── - -/// Sealed marker trait used as a type parameter on [`QuantifiedNodeFilter`] and -/// [`Quantified`] to distinguish `any` vs `all` semantics at compile time. -/// Never instantiated — only used as `` / `` in type positions. -pub trait QuantifierMode: sealed::Sealed + Clone + Copy + Send + Sync + 'static {} - -/// Marker for "pass if *any* temporal value matches" — used as `Q` in -/// `QuantifiedNodeFilter`. Selects [`AnyNodeOp`] at compile time. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AnyMode; - -/// Marker for "pass if *all* temporal values match" — used as `Q` in -/// `QuantifiedNodeFilter`. Selects [`AllNodeOp`] at compile time. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AllMode; - -impl sealed::Sealed for AnyMode {} -impl sealed::Sealed for AllMode {} -impl QuantifierMode for AnyMode {} -impl QuantifierMode for AllMode {} +use crate::db::graph::views::filter::model::{FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr}; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpNodeFilter — binary expression filter @@ -131,7 +99,7 @@ impl QuantifierMode for AllMode {} pub struct BinaryCmpNodeFilter where L: NodeExpr, - R: NodeExpr, + R: NodeExpr, { pub left: L, pub op: BinaryOp, @@ -141,7 +109,7 @@ where impl BinaryCmpNodeFilter where L: NodeExpr, - R: NodeExpr, + R: NodeExpr, { pub fn new(left: L, op: BinaryOp, right: R) -> Self { Self { left, op, right } @@ -151,7 +119,7 @@ where impl Clone for BinaryCmpNodeFilter where L: NodeExpr, - R: NodeExpr, + R: NodeExpr, { fn clone(&self) -> Self { Self { @@ -165,7 +133,7 @@ where impl ComposableFilter for BinaryCmpNodeFilter where L: NodeExpr, - R: NodeExpr, + R: NodeExpr, { } @@ -202,8 +170,7 @@ fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { impl CreateFilter for BinaryCmpNodeFilter where L: NodeExpr, - R: NodeExpr, - L::Output: Comparable, + R: NodeExpr, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; @@ -242,7 +209,7 @@ where impl TryAsCompositeFilter for BinaryCmpNodeFilter where L: NodeExpr, - R: NodeExpr, + R: NodeExpr, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -275,7 +242,7 @@ where /// ``` pub struct UnaryNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Clone + Send + Sync + 'static, { pub expr: E, @@ -285,7 +252,7 @@ where impl Clone for UnaryNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Clone + Send + Sync + 'static, { fn clone(&self) -> Self { @@ -299,14 +266,14 @@ where impl ComposableFilter for UnaryNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Clone + Send + Sync + 'static, { } impl CreateFilter for UnaryNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Clone + Send + Sync + 'static, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = @@ -339,7 +306,7 @@ where impl TryAsCompositeFilter for UnaryNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Clone + Send + Sync + 'static, { fn try_as_composite_node_filter(&self) -> Result { @@ -374,7 +341,7 @@ where /// ``` pub struct SetNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Eq + Hash + Clone + Send + Sync + 'static, { pub expr: E, @@ -385,7 +352,7 @@ where impl Clone for SetNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Eq + Hash + Clone + Send + Sync + 'static, { fn clone(&self) -> Self { @@ -400,14 +367,14 @@ where impl ComposableFilter for SetNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Eq + Hash + Clone + Send + Sync + 'static, { } impl CreateFilter for SetNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Eq + Hash + Clone + Send + Sync + 'static, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = @@ -444,7 +411,7 @@ where impl TryAsCompositeFilter for SetNodeFilter where - E: NodeExpr>, + E: NodeExpr, I: Eq + Hash + Clone + Send + Sync + 'static, { fn try_as_composite_node_filter(&self) -> Result { @@ -485,8 +452,7 @@ where pub struct StringNodeFilter where L: NodeExpr, - R: NodeExpr, - L::Output: StringComparable, + R: NodeExpr, { pub left: L, pub op: StringOp, @@ -496,8 +462,7 @@ where impl StringNodeFilter where L: NodeExpr, - R: NodeExpr, - L::Output: StringComparable, + R: NodeExpr, { pub fn new(left: L, op: StringOp, right: R) -> Self { Self { left, op, right } @@ -507,8 +472,7 @@ where impl Clone for StringNodeFilter where L: NodeExpr, - R: NodeExpr, - L::Output: StringComparable, + R: NodeExpr, { fn clone(&self) -> Self { Self { @@ -522,16 +486,14 @@ where impl ComposableFilter for StringNodeFilter where L: NodeExpr, - R: NodeExpr, - L::Output: StringComparable, + R: NodeExpr, { } impl CreateFilter for StringNodeFilter where L: NodeExpr, - R: NodeExpr, - L::Output: StringComparable, + R: NodeExpr, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; @@ -570,427 +532,7 @@ where impl TryAsCompositeFilter for StringNodeFilter where L: NodeExpr, - R: NodeExpr, - L::Output: StringComparable, -{ - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// QuantifiedNodeFilter — leaf filter wrapping a quantified comparison -// ───────────────────────────────────────────────────────────────────────────── - -/// A node filter that applies a [`BinaryOp`] to every temporal value and reduces -/// the results using `Q` ([`AnyMode`] or [`AllMode`]). -/// -/// Not constructed directly — returned by `Quantified::gt/eq/…`: -/// ```rust,ignore -/// // NodeFilter::temporal_property("score").any().gt(10i64) -/// // → QuantifiedNodeFilter, AnyMode> -/// // compiles to: AnyNodeOp { inner: PropListCompareOp { …, op: Gt } } -/// -/// // NodeFilter::temporal_property("score").all().gt(0i64) -/// // → QuantifiedNodeFilter, AllMode> -/// // 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 rhs: Arc> + 'graph> = - self.rhs.create_node_op(graph.clone())?; - let inner = Arc::new(PropListCompareOp { - inner: self.expr.create_node_op(graph)?, - rhs, - 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 rhs: Arc> + 'graph> = - self.rhs.create_node_op(graph.clone())?; - let inner = Arc::new(PropListCompareOp { - inner: self.expr.create_node_op(graph)?, - rhs, - 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) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// QuantifiedIsInNodeFilter — quantified set-membership filter -// ───────────────────────────────────────────────────────────────────────────── - -/// A node filter that checks whether any/all temporal values are in a fixed set. -/// -/// ```rust,ignore -/// NodeFilter::temporal_property("status").any().is_in(vec![Prop::Str("A".into()), Prop::Str("B".into())]) -/// ``` -pub struct QuantifiedIsInNodeFilter -where - E: NodeExpr, - Q: QuantifierMode, -{ - pub(crate) expr: E, - pub(crate) values: Vec, - pub(crate) op: SetOp, - pub(crate) _q: PhantomData, -} - -impl Clone for QuantifiedIsInNodeFilter -where - E: NodeExpr, - Q: QuantifierMode, -{ - fn clone(&self) -> Self { - Self { - expr: self.expr.clone(), - values: self.values.clone(), - op: self.op, - _q: PhantomData, - } - } -} - -impl ComposableFilter for QuantifiedIsInNodeFilter -where - E: NodeExpr, - Q: QuantifierMode, -{ -} - -impl CreateFilter for QuantifiedIsInNodeFilter -where - E: 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 + 'graph> = Arc::new(PropListInSetOp { - inner: self.expr.create_node_op(graph)?, - values: self.values, - op: self.op, - }); - Ok(AnyNodeOp { inner }) - } -} - -impl CreateFilter for QuantifiedIsInNodeFilter -where - E: 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 + 'graph> = Arc::new(PropListInSetOp { - inner: self.expr.create_node_op(graph)?, - values: self.values, - op: self.op, - }); - Ok(AllNodeOp { inner }) - } -} - -impl TryAsCompositeFilter for QuantifiedIsInNodeFilter -where - E: NodeExpr, - Q: QuantifierMode, -{ - 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) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// QuantifiedStringNodeFilter — quantified string-comparison filter -// ───────────────────────────────────────────────────────────────────────────── - -/// A node filter that applies a string op to every temporal value and reduces with any/all. -/// -/// ```rust,ignore -/// NodeFilter::temporal_property("name").any().starts_with("Al") -/// NodeFilter::temporal_property("tag").all().contains("foo") -/// ``` -pub struct QuantifiedStringNodeFilter -where - E: NodeExpr, - Q: QuantifierMode, -{ - pub(crate) expr: E, - pub(crate) rhs: ArcStr, - pub(crate) op: StringOp, - pub(crate) _q: PhantomData, -} - -impl Clone for QuantifiedStringNodeFilter -where - E: NodeExpr, - Q: QuantifierMode, -{ - fn clone(&self) -> Self { - Self { - expr: self.expr.clone(), - rhs: self.rhs.clone(), - op: self.op, - _q: PhantomData, - } - } -} - -impl ComposableFilter for QuantifiedStringNodeFilter -where - E: NodeExpr, - Q: QuantifierMode, -{ -} - -impl CreateFilter for QuantifiedStringNodeFilter -where - E: 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 + 'graph> = Arc::new(PropListStringOp { - inner: self.expr.create_node_op(graph)?, - rhs: self.rhs, - op: self.op, - }); - Ok(AnyNodeOp { inner }) - } -} - -impl CreateFilter for QuantifiedStringNodeFilter -where - E: 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 + 'graph> = Arc::new(PropListStringOp { - inner: self.expr.create_node_op(graph)?, - rhs: self.rhs, - op: self.op, - }); - Ok(AllNodeOp { inner }) - } -} - -impl TryAsCompositeFilter for QuantifiedStringNodeFilter -where - E: NodeExpr, - Q: QuantifierMode, + R: NodeExpr, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -1014,13 +556,13 @@ where /// A node filter that checks whether an aggregated scalar property value is in /// (or not in) a fixed set of `Prop` values. Uses linear scan because `Prop` /// may contain floats that don't implement `Hash`. -pub struct PropValueSetFilter>> { +pub struct PropValueSetFilter { pub(crate) expr: E, pub(crate) values: Vec, pub(crate) op: SetOp, } -impl>> Clone for PropValueSetFilter { +impl Clone for PropValueSetFilter { fn clone(&self) -> Self { Self { expr: self.expr.clone(), @@ -1030,9 +572,9 @@ impl>> Clone for PropValueSetFilter { } } -impl>> ComposableFilter for PropValueSetFilter {} +impl ComposableFilter for PropValueSetFilter {} -impl>> CreateFilter for PropValueSetFilter { +impl CreateFilter for PropValueSetFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; type NodeFilter<'graph, G: GraphView + 'graph> = PropValueSetNodeOp<'graph>; @@ -1062,7 +604,7 @@ impl>> CreateFilter for PropValueSetFilter } } -impl>> TryAsCompositeFilter for PropValueSetFilter { +impl TryAsCompositeFilter for PropValueSetFilter { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) } @@ -1078,284 +620,6 @@ impl>> TryAsCompositeFilter for PropValueSetFi } } -// ───────────────────────────────────────────────────────────────────────────── -// Quantified / Aggregated / TemporalProp — intermediate types in the fluent chain -// ───────────────────────────────────────────────────────────────────────────── - -/// Returned by [`TemporalProp::any`] / [`TemporalProp::all`]. -/// -/// Carries the temporal expression `E` and the quantifier `Q` until a comparison -/// operator is called, which produces the final [`QuantifiedNodeFilter`]: -/// ```rust,ignore -/// NodeFilter::temporal_property("score").any() // → Quantified, AnyMode> -/// .gt(10i64) // → QuantifiedNodeFilter, AnyMode, i64> -/// ``` -pub struct NodeQuantified -where - E: NodeExpr, - Q: QuantifierMode, -{ - pub(crate) expr: E, - pub(crate) _q: PhantomData, -} - -impl NodeQuantified -where - E: NodeExpr, - Q: QuantifierMode, -{ - fn finish( - self, - op: BinaryOp, - rhs: R, - ) -> QuantifiedNodeFilter { - QuantifiedNodeFilter::new(self.expr, op, rhs.into_prop_node_expr()) - } - - 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) - } - - pub fn is_in(self, values: impl IntoIterator) -> QuantifiedIsInNodeFilter { - QuantifiedIsInNodeFilter { - expr: self.expr, - values: values.into_iter().collect(), - op: SetOp::IsIn, - _q: PhantomData, - } - } - - pub fn is_not_in(self, values: impl IntoIterator) -> QuantifiedIsInNodeFilter { - QuantifiedIsInNodeFilter { - expr: self.expr, - values: values.into_iter().collect(), - op: SetOp::IsNotIn, - _q: PhantomData, - } - } - - fn string_finish(self, op: StringOp, rhs: &str) -> QuantifiedStringNodeFilter { - QuantifiedStringNodeFilter { - expr: self.expr, - rhs: ArcStr::from(rhs), - op, - _q: PhantomData, - } - } - - pub fn starts_with(self, rhs: &str) -> QuantifiedStringNodeFilter { - self.string_finish(StringOp::StartsWith, rhs) - } - - pub fn ends_with(self, rhs: &str) -> QuantifiedStringNodeFilter { - self.string_finish(StringOp::EndsWith, rhs) - } - - pub fn contains(self, rhs: &str) -> QuantifiedStringNodeFilter { - self.string_finish(StringOp::Contains, rhs) - } - - pub fn not_contains(self, rhs: &str) -> QuantifiedStringNodeFilter { - self.string_finish(StringOp::NotContains, rhs) - } - - pub fn sum(self) -> NodeQuantified, Q> { - NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Sum }, _q: PhantomData } - } - - pub fn avg(self) -> NodeQuantified, Q> { - NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Avg }, _q: PhantomData } - } - - pub fn min(self) -> NodeQuantified, Q> { - NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Min }, _q: PhantomData } - } - - pub fn max(self) -> NodeQuantified, Q> { - NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Max }, _q: PhantomData } - } - - pub fn first(self) -> NodeQuantified, Q> { - NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::First }, _q: PhantomData } - } - - pub fn last(self) -> NodeQuantified, Q> { - NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Last }, _q: PhantomData } - } - - pub fn len(self) -> NodeQuantified, Q> { - NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Len }, _q: PhantomData } - } - - pub fn any(self) -> NodeQuantified, AnyMode> { - NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::Any }, _q: PhantomData } - } - - pub fn all(self) -> NodeQuantified, AllMode> { - NodeQuantified { expr: NestedMapExpr { inner: self.expr, op: Op::All }, _q: PhantomData } - } -} - -/// Returned by [`TemporalProp::sum`], `.avg()`, `.min()` etc. -/// -/// Wraps the aggregator expression `E` (e.g. `SumExpr>`) until -/// a comparison operator is called, which produces a [`BinaryCmpNodeFilter`]: -/// ```rust,ignore -/// NodeFilter::temporal_property("price").sum() // → Aggregated>> -/// .gt(100i64) // → BinaryCmpNodeFilter>, i64> -/// ``` -pub struct NodeAggregated { - pub(crate) expr: E, -} - -impl>> NodeAggregated { - fn finish( - self, - op: BinaryOp, - rhs: R, - ) -> BinaryCmpNodeFilter { - BinaryCmpNodeFilter::new(self.expr, op, rhs.into_prop_node_expr()) - } - - 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) - } - - pub fn is_in(self, values: impl IntoIterator) -> PropValueSetFilter { - PropValueSetFilter { - expr: self.expr, - values: values.into_iter().collect(), - op: SetOp::IsIn, - } - } - - pub fn is_not_in(self, values: impl IntoIterator) -> PropValueSetFilter { - PropValueSetFilter { - expr: self.expr, - values: values.into_iter().collect(), - op: SetOp::IsNotIn, - } - } - - fn string_finish(self, op: StringOp, rhs: &str) -> StringNodeFilter { - StringNodeFilter::new(self.expr, op, Prop::Str(ArcStr::from(rhs))) - } - - pub fn starts_with(self, rhs: &str) -> StringNodeFilter { - self.string_finish(StringOp::StartsWith, rhs) - } - - pub fn ends_with(self, rhs: &str) -> StringNodeFilter { - self.string_finish(StringOp::EndsWith, rhs) - } - - pub fn contains(self, rhs: &str) -> StringNodeFilter { - self.string_finish(StringOp::Contains, rhs) - } - - pub fn not_contains(self, rhs: &str) -> StringNodeFilter { - self.string_finish(StringOp::NotContains, rhs) - } - - pub fn is_some(self) -> UnaryNodeFilter { - UnaryNodeFilter { - expr: self.expr, - op: UnaryOp::IsSome, - _phantom: PhantomData, - } - } - - pub fn is_none(self) -> UnaryNodeFilter { - UnaryNodeFilter { - expr: self.expr, - op: UnaryOp::IsNone, - _phantom: PhantomData, - } - } - - pub fn sum(self) -> NodeAggregated>> { - NodeAggregated { expr: SumExpr(UnwrapOptPropNodeExpr(self.expr)) } - } - - pub fn avg(self) -> NodeAggregated>> { - NodeAggregated { expr: AvgExpr(UnwrapOptPropNodeExpr(self.expr)) } - } - - pub fn min(self) -> NodeAggregated>> { - NodeAggregated { expr: MinExpr(UnwrapOptPropNodeExpr(self.expr)) } - } - - pub fn max(self) -> NodeAggregated>> { - NodeAggregated { expr: MaxExpr(UnwrapOptPropNodeExpr(self.expr)) } - } - - pub fn first(self) -> NodeAggregated>> { - NodeAggregated { expr: FirstExpr(UnwrapOptPropNodeExpr(self.expr)) } - } - - pub fn last(self) -> NodeAggregated>> { - NodeAggregated { expr: LastExpr(UnwrapOptPropNodeExpr(self.expr)) } - } - - pub fn len(self) -> LenExpr> { - LenExpr(UnwrapOptPropNodeExpr(self.expr)) - } - - pub fn any(self) -> NodeQuantified, AnyMode> { - NodeQuantified { - expr: UnwrapOptPropNodeExpr(self.expr), - _q: PhantomData, - } - } - - pub fn all(self) -> NodeQuantified, AllMode> { - NodeQuantified { - expr: UnwrapOptPropNodeExpr(self.expr), - _q: PhantomData, - } - } -} - // ───────────────────────────────────────────────────────────────────────────── // TemporalProp — entry point returned from `.temporal_property(name)` // ───────────────────────────────────────────────────────────────────────────── @@ -1397,7 +661,7 @@ impl TemporalProp { // NodePropertyExprOps — fluent comparison API for node-side property expressions // ───────────────────────────────────────────────────────────────────────────── -pub trait NodePropertyExprOps: NodeExpr> + Sized { +pub trait NodePropertyExprOps: NodeExpr + Sized { fn is_in(self, values: impl IntoIterator) -> PropValueSetFilter { PropValueSetFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsIn } } @@ -1416,61 +680,6 @@ impl NodeProp { } -// ───────────────────────────────────────────────────────────────────────────── -// NodeTemporalPropOps — fluent temporal API for node-side TemporalProp -// ───────────────────────────────────────────────────────────────────────────── - -pub trait NodeTemporalPropOps: Sized { - type ViewExpr: CreateView + NodeFilterFactory + Clone + Send + Sync + 'static; - fn into_temporal_parts(self) -> (Self::ViewExpr, String); - - fn any(self) -> NodeQuantified, AnyMode> { - let (view_expr, name) = self.into_temporal_parts(); - NodeQuantified { expr: TemporalPropertyExpr { view_expr, name }, _q: PhantomData } - } - fn all(self) -> NodeQuantified, AllMode> { - let (view_expr, name) = self.into_temporal_parts(); - NodeQuantified { expr: TemporalPropertyExpr { view_expr, name }, _q: PhantomData } - } - fn sum(self) -> NodeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - NodeAggregated { expr: SumExpr(TemporalPropertyExpr { view_expr, name }) } - } - fn avg(self) -> NodeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - NodeAggregated { expr: AvgExpr(TemporalPropertyExpr { view_expr, name }) } - } - fn min(self) -> NodeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - NodeAggregated { expr: MinExpr(TemporalPropertyExpr { view_expr, name }) } - } - fn max(self) -> NodeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - NodeAggregated { expr: MaxExpr(TemporalPropertyExpr { view_expr, name }) } - } - fn first(self) -> NodeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - NodeAggregated { expr: FirstExpr(TemporalPropertyExpr { view_expr, name }) } - } - fn last(self) -> NodeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - NodeAggregated { expr: LastExpr(TemporalPropertyExpr { view_expr, name }) } - } - fn len(self) -> LenExpr> { - let (view_expr, name) = self.into_temporal_parts(); - LenExpr(TemporalPropertyExpr { view_expr, name }) - } -} - -impl NodeTemporalPropOps - for TemporalProp -{ - type ViewExpr = E; - fn into_temporal_parts(self) -> (E, String) { - (self.view_expr, self.name) - } -} - // ───────────────────────────────────────────────────────────────────────────── // NodeExprFilterOps — comparison and set operators on NodeExpr // ───────────────────────────────────────────────────────────────────────────── @@ -1484,66 +693,58 @@ impl NodeTemp /// DegreeExpr(Direction::BOTH).is_in([2usize, 3usize]) /// ``` pub trait NodeExprFilterOps: NodeExpr + Sized { - fn gt>(self, rhs: R) -> BinaryCmpNodeFilter { + fn gt(self, rhs: R) -> BinaryCmpNodeFilter { BinaryCmpNodeFilter::new(self, BinaryOp::Gt, rhs) } - fn ge>(self, rhs: R) -> BinaryCmpNodeFilter { + fn ge(self, rhs: R) -> BinaryCmpNodeFilter { BinaryCmpNodeFilter::new(self, BinaryOp::Ge, rhs) } - fn lt>(self, rhs: R) -> BinaryCmpNodeFilter { + fn lt(self, rhs: R) -> BinaryCmpNodeFilter { BinaryCmpNodeFilter::new(self, BinaryOp::Lt, rhs) } - fn le>(self, rhs: R) -> BinaryCmpNodeFilter { + fn le(self, rhs: R) -> BinaryCmpNodeFilter { BinaryCmpNodeFilter::new(self, BinaryOp::Le, rhs) } - fn eq>(self, rhs: R) -> BinaryCmpNodeFilter { + fn eq(self, rhs: R) -> BinaryCmpNodeFilter { BinaryCmpNodeFilter::new(self, BinaryOp::Eq, rhs) } - fn ne>(self, rhs: R) -> BinaryCmpNodeFilter { + fn ne(self, rhs: R) -> BinaryCmpNodeFilter { BinaryCmpNodeFilter::new(self, BinaryOp::Ne, rhs) } - fn starts_with>(self, rhs: R) -> StringNodeFilter - where - Self::Output: StringComparable, + fn starts_with(self, rhs: R) -> StringNodeFilter { StringNodeFilter::new(self, StringOp::StartsWith, rhs) } - fn ends_with>(self, rhs: R) -> StringNodeFilter + 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, + fn contains(self, rhs: R) -> StringNodeFilter { StringNodeFilter::new(self, StringOp::Contains, rhs) } - fn not_contains>(self, rhs: R) -> StringNodeFilter + fn not_contains(self, rhs: R) -> StringNodeFilter where - Self::Output: StringComparable, { StringNodeFilter::new(self, StringOp::NotContains, rhs) } - fn fuzzy_search>( + fn fuzzy_search( self, rhs: R, levenshtein_distance: usize, prefix_match: bool, ) -> StringNodeFilter - where - Self::Output: StringComparable, { StringNodeFilter::new( self, @@ -1557,7 +758,7 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { fn is_some(self) -> UnaryNodeFilter where - Self: NodeExpr>, + Self: NodeExpr, Inner: Clone + Send + Sync + 'static, { UnaryNodeFilter { @@ -1569,7 +770,7 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { fn is_none(self) -> UnaryNodeFilter where - Self: NodeExpr>, + Self: NodeExpr, Inner: Clone + Send + Sync + 'static, { UnaryNodeFilter { @@ -1581,14 +782,14 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { fn is_true(self) -> BinaryCmpNodeFilter where - Self: NodeExpr>, + Self: NodeExpr, { self.eq(Prop::Bool(true)) } fn is_false(self) -> BinaryCmpNodeFilter where - Self: NodeExpr>, + Self: NodeExpr, { self.eq(Prop::Bool(false)) } @@ -1596,56 +797,3 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { } 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) -> NodeQuantified { - NodeQuantified { - expr: self, - _q: PhantomData, - } - } - - fn all(self) -> NodeQuantified { - NodeQuantified { - 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 index 7fcfc07580..4e98bd7f99 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -2,7 +2,7 @@ use crate::{ db::api::{state::ops::NodeOp, view::internal::GraphView}, errors::GraphError, }; -use raphtory_api::core::entities::properties::prop::PropType; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; pub mod exprs; @@ -15,6 +15,7 @@ mod tests; pub use exprs::*; pub use filters::*; pub use ops::*; +use crate::db::graph::views::filter::model::edge_expr::EdgeOp; pub use super::{Metadata, Property}; // ───────────────────────────────────────────────────────────────────────────── @@ -38,17 +39,17 @@ pub use super::{Metadata, Property}; /// NodeFilter::name().eq("Alice") /// ``` /// -pub trait NodeExpr: Clone + Send + Sync + 'static { - type Output: Clone + Send + Sync + 'static; - +pub trait NodeExpr: EntityExpr + 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>; + ) -> Result> + 'g>, GraphError>; +} +pub trait EntityExpr: Clone + Send + Sync + 'static { /// 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 index 4414c7c48f..a07468c159 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -36,6 +36,10 @@ //! AllNodeOp → false (not all matched) //! ``` +use super::EdgeOp; +use crate::db::graph::views::filter::model::property_filter::evaluate::{ + scan_f64_sum_count, scan_i64_sum, scan_u64_sum, +}; use crate::{ db::{ api::{ @@ -45,11 +49,12 @@ use crate::{ }, graph::views::filter::model::{ filter_operator::{BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp}, - property_filter::{evaluate::aggregate_values, Op}, + property_filter::evaluate::aggregate_values, }, }, prelude::GraphViewOps, }; +use raphtory_api::core::entities::edges::edge_ref::EdgeRef; use raphtory_api::core::{ entities::{ properties::prop::{Prop, PropArray, PropType}, @@ -59,7 +64,6 @@ use raphtory_api::core::{ }; use raphtory_storage::graph::graph::GraphStorage; use std::{collections::HashSet, hash::Hash, sync::Arc}; - // ───────────────────────────────────────────────────────────────────────────── // NodePropOp — latest property value by pre-resolved column ID // ───────────────────────────────────────────────────────────────────────────── @@ -165,53 +169,122 @@ impl NodeOp for TemporalNodePropOp { // 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>, +macro_rules! impl_agg_entity_op { + ($node_name:ident, $edge_name:ident, $body:expr) => { + #[derive(Clone)] + pub struct $node_name<'g> { + pub inner: Arc> + 'g>, } - impl<'g> Clone for $name<'g> { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } + impl<'g> NodeOp for $node_name<'g> { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { + ($body)(self.inner.apply(storage, node)) } } - impl<'g> NodeOp for $name<'g> { - type Output = $output; + #[derive(Clone)] + pub struct $edge_name<'g> { + pub inner: Arc + 'g>, + } + + impl<'g> EdgeOp for $edge_name<'g> { + type Output = Option; - 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) + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + ($body)(self.inner.apply(storage, edge)) } } }; } -impl_agg_node_op!(SumNodeOp, Option, |vals: Vec| { - aggregate_values(&vals, Op::Sum) +impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { + aggregate_values(vals, |pi| { + let mut vals = pi.peekable(); + if vals.peek().is_none() { + return None; + } + let inner = vals.peek().unwrap().borrow().dtype(); + match inner { + PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { + let (promoted, s64, s128, _) = scan_u64_sum(vals)?; + Some(if promoted { + Prop::U64(u64::try_from(s128).ok()?) + } else { + Prop::U64(s64) + }) + } + PropType::I32 | PropType::I64 => { + let (promoted, s64, s128, _) = scan_i64_sum(vals)?; + Some(if promoted { + Prop::I64(i64::try_from(s128).ok()?) + } else { + Prop::I64(s64) + }) + } + PropType::F32 | PropType::F64 => { + scan_f64_sum_count(vals).map(|(sum, _)| Prop::F64(sum)) + } + _ => None, + } + }) +}); + +impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { + aggregate_values(vals, |pi| { + let mut vals = pi.peekable(); + if vals.peek().is_none() { + return None; + } + let inner = vals.peek().unwrap().borrow().dtype(); + match inner { + PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { + let (promoted, s64, s128, count) = scan_u64_sum(vals)?; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) + } + + PropType::I32 | PropType::I64 => { + let (promoted, s64, s128, count) = scan_i64_sum(vals)?; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) + } + + PropType::F32 | PropType::F64 => { + let (sum, count) = scan_f64_sum_count(vals)?; + Some(Prop::F64(sum / (count as f64))) + } + + _ => None, + } + }) }); -impl_agg_node_op!(AvgNodeOp, Option, |vals: Vec| { - aggregate_values(&vals, Op::Avg) +impl_agg_entity_op!(MinNodeOp, MinEdgeOp, |vals| { + aggregate_values(vals, |pi| pi.min()) }); -impl_agg_node_op!(MinNodeOp, Option, |vals: Vec| { - aggregate_values(&vals, Op::Min) +impl_agg_entity_op!(MaxNodeOp, MaxEdgeOp, |vals| { + aggregate_values(vals, |pi| pi.max()) }); -impl_agg_node_op!(MaxNodeOp, Option, |vals: Vec| { - aggregate_values(&vals, Op::Max) +impl_agg_entity_op!(FirstNodeOp, FirstEdgeOp, |vals| { + aggregate_values(vals, |pi| pi.next()) }); -impl_agg_node_op!(FirstNodeOp, Option, |vals: Vec| { - vals.into_iter().next() +impl_agg_entity_op!(LastNodeOp, LastEdgeOp, |vals| { + aggregate_values(vals, |pi| pi.last()) }); -impl_agg_node_op!(LastNodeOp, Option, |vals: Vec| { - vals.into_iter().last() +impl_agg_entity_op!(LenNodeOp, LenEdgeOp, |vals| { + aggregate_values(vals, |pi| pi.count().into_prop()) +}); +impl_agg_entity_op!(AnyNodeOp, AnyEdgeOp, |vals| { + aggregate_values(vals, |pi| { + Some(Prop::Bool(pi.any(|r| r == Prop::Bool(true)))) + }) +}); +impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { + aggregate_values(vals, |pi| { + Some(Prop::Bool(pi.all(|r| r == Prop::Bool(true)))) + }) }); -impl_agg_node_op!(LenNodeOp, usize, |vals: Vec| vals.len()); // ───────────────────────────────────────────────────────────────────────────── // AnyNodeOp / AllNodeOp — unary reducers over a Prop::List of booleans @@ -233,57 +306,6 @@ fn prop_all(prop: &Prop) -> bool { } } -/// 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`) @@ -406,70 +428,6 @@ impl<'g> NodeOp for PropListStringOp<'g> { } } -// ───────────────────────────────────────────────────────────────────────────── -// NestedMapNodeOp<'g> — element-wise aggregation / quantification on a Prop::List -// ───────────────────────────────────────────────────────────────────────────── - -/// Applies a per-element operation to each element of an outer `Prop::List`. -/// -/// Used for chained expressions like `.temporal().any().sum()`: -/// the outer list is `Prop::List([list_t1, list_t2, …])` and for each inner -/// `list_ti` the op is applied, producing `Prop::List([result_t1, result_t2, …])`. -/// The outer `AnyNodeOp` / `AllNodeOp` then reduces the result list. -/// -/// Scalar elements are passed through unchanged. -pub(crate) struct NestedMapNodeOp<'g> { - pub(crate) inner: Arc + 'g>, - pub(crate) op: Op, -} - -impl<'g> Clone for NestedMapNodeOp<'g> { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - op: self.op, - } - } -} - -impl<'g> NodeOp for NestedMapNodeOp<'g> { - type Output = Prop; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Prop { - let outer = self.inner.apply(storage, node); - let Prop::List(arr) = outer else { - return Prop::List(PropArray::from(vec![])); - }; - let mapped: Vec = arr - .iter() - .map(|elem| match elem { - Prop::List(inner_arr) => { - let vals: Vec = inner_arr.iter().collect(); - match self.op { - Op::Sum => aggregate_values(&vals, Op::Sum) - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Avg => aggregate_values(&vals, Op::Avg) - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Min => aggregate_values(&vals, Op::Min) - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Max => aggregate_values(&vals, Op::Max) - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::First => vals.into_iter().next() - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Last => vals.into_iter().last() - .unwrap_or(Prop::List(PropArray::from(vec![]))), - Op::Len => Prop::U64(inner_arr.len() as u64), - Op::Any => Prop::Bool(prop_any(&Prop::List(inner_arr))), - Op::All => Prop::Bool(prop_all(&Prop::List(inner_arr))), - } - } - other => other, - }) - .collect(); - Prop::List(PropArray::from(mapped)) - } -} - // ───────────────────────────────────────────────────────────────────────────── // UnwrapOptPropOp<'g> — converts Option → Prop for nested aggregation // ───────────────────────────────────────────────────────────────────────────── diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index d9e1712507..46753845e7 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -1,4 +1,4 @@ -use crate::{db::graph::views::filter::model::Op, prelude::PropertyFilter}; +use crate::db::graph::views::filter::model::Op; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::borrow::Borrow; @@ -7,541 +7,555 @@ enum ValueType { Scalar(Option), } -pub fn aggregate_values, I: IntoIterator>( - vals: I, - op: Op, +pub fn aggregate_values>( + vals: Option, + op: impl Fn(T) -> Option, ) -> Option { - let mut vals = vals.into_iter().peekable(); - fn scan_u64_sum>( - vals: impl IntoIterator, - ) -> Option<(bool, u64, u128, usize)> { - let mut sum64: u64 = 0; - let mut sum128: u128 = 0; - let mut promoted = false; - let mut count = 0usize; - - for p in vals { - let p = p.borrow(); - let x = p.as_u64_lossless()?; - if !promoted { - if let Some(s) = sum64.checked_add(x) { - sum64 = s; - } else { - promoted = true; - sum128 = (sum64 as u128) + (x as u128); - } - } else { - sum128 += x as u128; + match vals? { + Prop::List(x) => match x.dtype() { + PropType::List(_) => { + let s = x + .iter_all() + .map(|y| aggregate_values(y, op)) + .flatten() + .collect(); + Some(Prop::List(s)) } - count += 1; - } - Some((promoted, sum64, sum128, count)) + _ => op(x.iter()), + }, + _ => None, } +} - fn scan_i64_sum>( - vals: impl IntoIterator, - ) -> Option<(bool, i64, i128, usize)> { - let mut sum64: i64 = 0; - let mut sum128: i128 = 0; - let mut promoted = false; - let mut count = 0; - - for p in vals { - let p = p.borrow(); - let x = p.as_i64_lossless()?; - if !promoted { - if let Some(s) = sum64.checked_add(x) { - sum64 = s; - } else { - promoted = true; - sum128 = (sum64 as i128) + (x as i128); - } +pub fn scan_u64_sum>( + vals: impl IntoIterator, +) -> Option<(bool, u64, u128, usize)> { + let mut sum64: u64 = 0; + let mut sum128: u128 = 0; + let mut promoted = false; + let mut count = 0usize; + + for p in vals { + let p = p.borrow(); + let x = p.as_u64_lossless()?; + if !promoted { + if let Some(s) = sum64.checked_add(x) { + sum64 = s; } else { - sum128 += x as i128; + promoted = true; + sum128 = (sum64 as u128) + (x as u128); } - count += 1; + } else { + sum128 += x as u128; } - Some((promoted, sum64, sum128, count)) + count += 1; } + Some((promoted, sum64, sum128, count)) +} - fn scan_u64_min_max>(vals: impl IntoIterator) -> Option<(u64, u64)> { - let mut it = vals.into_iter(); - let first = it.next()?.borrow().as_u64_lossless()?; - let mut min_v = first; - let mut max_v = first; - for p in it { - let p = p.borrow(); - let x = p.as_u64_lossless()?; - if x < min_v { - min_v = x; - } - if x > max_v { - max_v = x; +pub fn scan_i64_sum>( + vals: impl IntoIterator, +) -> Option<(bool, i64, i128, usize)> { + let mut sum64: i64 = 0; + let mut sum128: i128 = 0; + let mut promoted = false; + let mut count = 0; + + for p in vals { + let p = p.borrow(); + let x = p.as_i64_lossless()?; + if !promoted { + if let Some(s) = sum64.checked_add(x) { + sum64 = s; + } else { + promoted = true; + sum128 = (sum64 as i128) + (x as i128); } + } else { + sum128 += x as i128; } - Some((min_v, max_v)) + count += 1; } + Some((promoted, sum64, sum128, count)) +} - fn scan_i64_min_max>(vals: impl IntoIterator) -> Option<(i64, i64)> { - let mut it = vals.into_iter(); - let first = it.next()?.borrow().as_i64_lossless()?; - let mut min_v = first; - let mut max_v = first; - for p in it { - let p = p.borrow(); - let x = p.as_i64_lossless()?; - if x < min_v { - min_v = x; - } - if x > max_v { - max_v = x; - } +pub fn scan_u64_min_max>(vals: impl IntoIterator) -> Option<(u64, u64)> { + let mut it = vals.into_iter(); + let first = it.next()?.borrow().as_u64_lossless()?; + let mut min_v = first; + let mut max_v = first; + for p in it { + let p = p.borrow(); + let x = p.as_u64_lossless()?; + if x < min_v { + min_v = x; } - Some((min_v, max_v)) - } - - fn scan_f64_sum_count>( - vals: impl IntoIterator, - ) -> Option<(f64, u64)> { - let mut sum = 0.0f64; - let mut count = 0u64; - for p in vals { - let p = p.borrow(); - let x = p.as_f64_lossless()?; - if !x.is_finite() { - return None; - } - sum += x; - count += 1; + if x > max_v { + max_v = x; } - Some((sum, count)) } + Some((min_v, max_v)) +} - fn scan_f64_min_max>(vals: impl IntoIterator) -> Option<(f64, f64)> { - let mut it = vals.into_iter(); - let first = it.next()?.borrow().as_f64_lossless()?; - if !first.is_finite() { - return None; +pub fn scan_i64_min_max>(vals: impl IntoIterator) -> Option<(i64, i64)> { + let mut it = vals.into_iter(); + let first = it.next()?.borrow().as_i64_lossless()?; + let mut min_v = first; + let mut max_v = first; + for p in it { + let p = p.borrow(); + let x = p.as_i64_lossless()?; + if x < min_v { + min_v = x; } - let mut min_v = first; - let mut max_v = first; - for p in it { - let p = p.borrow(); - let x = p.as_f64_lossless()?; - if !x.is_finite() { - return None; - } - if x < min_v { - min_v = x; - } - if x > max_v { - max_v = x; - } + if x > max_v { + max_v = x; } - Some((min_v, max_v)) } + Some((min_v, max_v)) +} - fn reduce_unsigned>( - vals: impl IntoIterator, - ret_minmax: fn(u64) -> Prop, - op: Op, - ) -> Option { - match op { - Op::Sum => { - let (promoted, s64, s128, _) = scan_u64_sum(vals)?; - Some(if promoted { - Prop::U64(u64::try_from(s128).ok()?) - } else { - Prop::U64(s64) - }) - } - Op::Avg => { - let (promoted, s64, s128, count) = scan_u64_sum(vals)?; - let s = if promoted { s128 as f64 } else { s64 as f64 }; - Some(Prop::F64(s / (count as f64))) - } - Op::Min => scan_u64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), - Op::Max => scan_u64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), - Op::Len | Op::First | Op::Last | Op::Any | Op::All => unreachable!(), +pub fn scan_f64_sum_count>( + vals: impl IntoIterator, +) -> Option<(f64, u64)> { + let mut sum = 0.0f64; + let mut count = 0u64; + for p in vals { + let p = p.borrow(); + let x = p.as_f64_lossless()?; + if !x.is_finite() { + return None; } + sum += x; + count += 1; } + Some((sum, count)) +} - fn reduce_signed>( - vals: impl IntoIterator, - ret_minmax: fn(i64) -> Prop, - op: Op, - ) -> Option { - match op { - Op::Sum => { - let (promoted, s64, s128, _) = scan_i64_sum(vals)?; - Some(if promoted { - Prop::I64(i64::try_from(s128).ok()?) - } else { - Prop::I64(s64) - }) - } - Op::Avg => { - let (promoted, s64, s128, count) = scan_i64_sum(vals)?; - let s = if promoted { s128 as f64 } else { s64 as f64 }; - Some(Prop::F64(s / (count as f64))) - } - Op::Min => scan_i64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), - Op::Max => scan_i64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), - Op::Len | Op::First | Op::Last | Op::Any | Op::All => unreachable!(), - } +fn scan_f64_min_max>(vals: impl IntoIterator) -> Option<(f64, f64)> { + let mut it = vals.into_iter(); + let first = it.next()?.borrow().as_f64_lossless()?; + if !first.is_finite() { + return None; } - - fn reduce_float>( - vals: impl IntoIterator, - ret_minmax: fn(f64) -> Prop, - op: Op, - ) -> Option { - match op { - Op::Sum => scan_f64_sum_count(vals).map(|(sum, _)| Prop::F64(sum)), - Op::Avg => { - let (sum, count) = scan_f64_sum_count(vals)?; - Some(Prop::F64(sum / (count as f64))) - } - Op::Min => scan_f64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), - Op::Max => scan_f64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), - Op::Len | Op::First | Op::Last | Op::Any | Op::All => unreachable!(), + let mut min_v = first; + let mut max_v = first; + for p in it { + let p = p.borrow(); + let x = p.as_f64_lossless()?; + if !x.is_finite() { + return None; } - } - - match op { - Op::Len => Some(Prop::U64(vals.count() as u64)), - Op::Sum | Op::Avg | Op::Min | Op::Max => { - if vals.peek().is_none() { - return None; - } - let inner = vals.peek().unwrap().borrow().dtype(); - match inner { - PropType::U8 => reduce_unsigned(vals, |x| Prop::U8(x as u8), op), - PropType::U16 => reduce_unsigned(vals, |x| Prop::U16(x as u16), op), - PropType::U32 => reduce_unsigned(vals, |x| Prop::U32(x as u32), op), - PropType::U64 => reduce_unsigned(vals, |x| Prop::U64(x), op), - - PropType::I32 => reduce_signed(vals, |x| Prop::I32(x as i32), op), - PropType::I64 => reduce_signed(vals, |x| Prop::I64(x), op), - - PropType::F32 => reduce_float(vals, |x| Prop::F32(x as f32), op), - PropType::F64 => reduce_float(vals, |x| Prop::F64(x), op), - _ => None, - } + if x < min_v { + min_v = x; + } + if x > max_v { + max_v = x; } - Op::First | Op::Last | Op::Any | Op::All => unreachable!(), } + Some((min_v, max_v)) } -pub fn apply_agg_to_prop(p: &Prop, op: Op) -> Option { - match (op, p) { - (Op::Len, Prop::List(inner)) => Some(Prop::U64(inner.len() as u64)), - (Op::Sum, Prop::List(inner)) - | (Op::Avg, Prop::List(inner)) - | (Op::Min, Prop::List(inner)) - | (Op::Max, Prop::List(inner)) => aggregate_values(inner.iter(), op), - - (Op::Len, _) => Some(Prop::U64(1)), - - (Op::Sum, Prop::U8(x)) => Some(Prop::U8(*x)), - (Op::Sum, Prop::U16(x)) => Some(Prop::U16(*x)), - (Op::Sum, Prop::U32(x)) => Some(Prop::U32(*x)), - (Op::Sum, Prop::U64(x)) => Some(Prop::U64(*x)), - (Op::Sum, Prop::I32(x)) => Some(Prop::I32(*x)), - (Op::Sum, Prop::I64(x)) => Some(Prop::I64(*x)), - (Op::Sum, Prop::F32(x)) => { - if x.is_finite() { - Some(Prop::F32(*x)) - } else { - None - } - } - (Op::Sum, Prop::F64(x)) => { - if x.is_finite() { - Some(Prop::F64(*x)) - } else { - None - } - } - - (Op::Avg, Prop::U8(x)) => Some(Prop::F64(*x as f64)), - (Op::Avg, Prop::U16(x)) => Some(Prop::F64(*x as f64)), - (Op::Avg, Prop::U32(x)) => Some(Prop::F64(*x as f64)), - (Op::Avg, Prop::U64(x)) => Some(Prop::F64(*x as f64)), - (Op::Avg, Prop::I32(x)) => Some(Prop::F64(*x as f64)), - (Op::Avg, Prop::I64(x)) => Some(Prop::F64(*x as f64)), - (Op::Avg, Prop::F32(x)) => { - if x.is_finite() { - Some(Prop::F32(*x)) - } else { - None - } - } - (Op::Avg, Prop::F64(x)) => { - if x.is_finite() { - Some(Prop::F64(*x)) - } else { - None - } - } - - (Op::Min, Prop::U8(x)) => Some(Prop::U8(*x)), - (Op::Min, Prop::U16(x)) => Some(Prop::U16(*x)), - (Op::Min, Prop::U32(x)) => Some(Prop::U32(*x)), - (Op::Min, Prop::U64(x)) => Some(Prop::U64(*x)), - (Op::Min, Prop::I32(x)) => Some(Prop::I32(*x)), - (Op::Min, Prop::I64(x)) => Some(Prop::I64(*x)), - (Op::Min, Prop::F32(x)) => { - if x.is_finite() { - Some(Prop::F32(*x)) - } else { - None - } - } - (Op::Min, Prop::F64(x)) => { - if x.is_finite() { - Some(Prop::F64(*x)) - } else { - None - } - } - - (Op::Max, Prop::U8(x)) => Some(Prop::U8(*x)), - (Op::Max, Prop::U16(x)) => Some(Prop::U16(*x)), - (Op::Max, Prop::U32(x)) => Some(Prop::U32(*x)), - (Op::Max, Prop::U64(x)) => Some(Prop::U64(*x)), - (Op::Max, Prop::I32(x)) => Some(Prop::I32(*x)), - (Op::Max, Prop::I64(x)) => Some(Prop::I64(*x)), - (Op::Max, Prop::F32(x)) => { - if x.is_finite() { - Some(Prop::F32(*x)) +pub fn reduce_unsigned>( + vals: impl IntoIterator, + ret_minmax: fn(u64) -> Prop, + op: Op, +) -> Option { + match op { + Op::Sum => { + let (promoted, s64, s128, _) = scan_u64_sum(vals)?; + Some(if promoted { + Prop::U64(u64::try_from(s128).ok()?) } else { - None - } + Prop::U64(s64) + }) } - (Op::Max, Prop::F64(x)) => { - if x.is_finite() { - Some(Prop::F64(*x)) - } else { - None - } + Op::Avg => { + let (promoted, s64, s128, count) = scan_u64_sum(vals)?; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) } - - (Op::Sum, _) | (Op::Avg, _) | (Op::Min, _) | (Op::Max, _) => None, - - _ => None, + Op::Min => scan_u64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), + Op::Max => scan_u64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), + Op::Len | Op::First | Op::Last | Op::Any | Op::All => unreachable!(), } } -impl PropertyFilter { - fn reduce_qualifiers_rec( - &self, - quals: &[Op], - v: &Prop, - predicate: &dyn Fn(&Prop) -> bool, - ) -> bool { - if quals.is_empty() { - return predicate(v); - } - let (q, rest) = (quals[0], &quals[1..]); - - if let Prop::List(inner) = v { - let mut elems = inner.iter().peekable(); - let check = |e: &Prop| { - if rest.is_empty() { - predicate(e) - } else { - self.reduce_qualifiers_rec(rest, e, predicate) - } - }; - return match q { - Op::Any => elems.any(|p| check(&p)), - Op::All => !elems.peek().is_none() && elems.all(|p| check(&p)), - _ => unreachable!(), - }; - } - - if rest.is_empty() { - return match q { - Op::Any | Op::All => predicate(v), - _ => unreachable!(), - }; - } - self.reduce_qualifiers_rec(rest, v, predicate) - } - - fn eval_ops(&self, mut state: ValueType) -> (Option, Option>, Vec, bool) { - let mut qualifiers: Vec = Vec::new(); - let mut seq_is_temporal = matches!(state, ValueType::Seq(..)); - let mut seen_qual_before_agg = false; - - let per_step_map = |vals: Vec, op: Op| -> Vec { - vals.into_iter() - .filter_map(|p| apply_agg_to_prop(&p, op)) - .collect() - }; - - // Aggregate OVER TIME (when no prior qualifier). - let reduce_over_seq = |vs: Vec, op: Op| -> Option { - match op { - Op::Len => Some(Prop::U64(vs.len() as u64)), - Op::Sum | Op::Avg | Op::Min | Op::Max => { - if vs.is_empty() || matches!(vs.first(), Some(Prop::List(_))) { - return None; - } - aggregate_values(&vs, op) - } - _ => None, - } - }; - - for op in &self.ops { - match *op { - Op::First | Op::Last => { - state = match state { - ValueType::Seq(vs) => { - seq_is_temporal = false; - let v = if matches!(*op, Op::First) { - vs.first() - } else { - vs.last() - }; - ValueType::Scalar(v.cloned()) - } - s @ ValueType::Scalar(_) => s, - }; - } - - Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => { - state = match state { - ValueType::Seq(vs) if seen_qual_before_agg => { - ValueType::Seq(per_step_map(vs, *op)) - } - ValueType::Seq(vs) => { - seq_is_temporal = false; - ValueType::Scalar(reduce_over_seq(vs, *op)) - } - ValueType::Scalar(Some(Prop::List(inner))) => { - ValueType::Scalar(aggregate_values(inner.iter(), *op)) - } - ValueType::Scalar(Some(p)) => ValueType::Scalar(apply_agg_to_prop(&p, *op)), - ValueType::Scalar(None) => ValueType::Scalar(None), - }; - } - - Op::Any | Op::All => { - qualifiers.push(*op); - seen_qual_before_agg = true; - state = match state { - ValueType::Seq(vs) => ValueType::Seq(vs), // still temporal - ValueType::Scalar(Some(Prop::List(inner))) => { - seq_is_temporal = false; - ValueType::Seq(vec![Prop::List(inner)]) - } - ValueType::Scalar(Some(p)) => { - seq_is_temporal = false; - ValueType::Seq(vec![p]) - } - ValueType::Scalar(None) => { - seq_is_temporal = false; - ValueType::Seq(vec![]) - } - }; - } - } - } - - match state { - ValueType::Scalar(v) => (v, None, qualifiers, seq_is_temporal), - ValueType::Seq(vs) => (None, Some(vs), qualifiers, seq_is_temporal), - } - } - - fn apply_eval( - &self, - reduced: Option, - maybe_seq: Option>, - qualifiers: Vec, - seq_is_temporal: bool, - ) -> bool { - if let Some(value) = reduced { - return self - .operator - .apply_to_property(&self.prop_value, Some(&value)); - } - - if !qualifiers.is_empty() { - let (temporal_q_opt, elem_quals) = if seq_is_temporal { - (Some(qualifiers[0]), &qualifiers[1..]) - } else { - (None, &qualifiers[..]) - }; - let pred = |p: &Prop| self.operator.apply_to_property(&self.prop_value, Some(p)); - let Some(seq) = maybe_seq else { return false }; - - if let Some(tq) = temporal_q_opt { - let mut saw = false; - match tq { - Op::All => { - for p in &seq { - saw = true; - let ok = if elem_quals.is_empty() { - pred(p) - } else { - self.reduce_qualifiers_rec(elem_quals, p, &pred) - }; - if !ok { - return false; - } - } - return saw; - } - Op::Any => { - for p in &seq { - let ok = if elem_quals.is_empty() { - pred(p) - } else { - self.reduce_qualifiers_rec(elem_quals, p, &pred) - }; - if ok { - return true; - } - } - return false; - } - _ => unreachable!(), - } +pub fn reduce_signed>( + vals: impl IntoIterator, + ret_minmax: fn(i64) -> Prop, + op: Op, +) -> Option { + match op { + Op::Sum => { + let (promoted, s64, s128, _) = scan_i64_sum(vals)?; + Some(if promoted { + Prop::I64(i64::try_from(s128).ok()?) } else { - for p in &seq { - let ok = if elem_quals.is_empty() { - pred(p) - } else { - self.reduce_qualifiers_rec(elem_quals, p, &pred) - }; - if ok { - return true; - } - } - return false; - } + Prop::I64(s64) + }) } - - if let Some(seq) = maybe_seq { - let full = Prop::List(seq.into()); - self.operator - .apply_to_property(&self.prop_value, Some(&full)) - } else { - self.operator.apply_to_property(&self.prop_value, None) + Op::Avg => { + let (promoted, s64, s128, count) = scan_i64_sum(vals)?; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) } + Op::Min => scan_i64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), + Op::Max => scan_i64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), + Op::Len | Op::First | Op::Last | Op::Any | Op::All => unreachable!(), } +} - pub fn eval_scalar_and_apply(&self, prop: Option) -> bool { - let (r, s, q, is_t) = self.eval_ops(ValueType::Scalar(prop)); - self.apply_eval(r, s, q, is_t) - } - - pub fn eval_temporal_and_apply(&self, props: Vec) -> bool { - let (r, s, q, is_t) = self.eval_ops(ValueType::Seq(props)); - self.apply_eval(r, s, q, is_t) +pub fn reduce_float>( + vals: impl IntoIterator, + ret_minmax: fn(f64) -> Prop, + op: Op, +) -> Option { + match op { + Op::Sum => scan_f64_sum_count(vals).map(|(sum, _)| Prop::F64(sum)), + Op::Avg => { + let (sum, count) = scan_f64_sum_count(vals)?; + Some(Prop::F64(sum / (count as f64))) + } + Op::Min => scan_f64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), + Op::Max => scan_f64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), + Op::Len | Op::First | Op::Last | Op::Any | Op::All => unreachable!(), } } + +// match op { +// Op::Len => Some(Prop::U64(vals.count() as u64)), +// Op::Sum | Op::Avg | Op::Min | Op::Max => { +// if vals.peek().is_none() { +// return None; +// } +// let inner = vals.peek().unwrap().borrow().dtype(); +// match inner { +// PropType::U8 => reduce_unsigned(vals, |x| Prop::U8(x as u8), op), +// PropType::U16 => reduce_unsigned(vals, |x| Prop::U16(x as u16), op), +// PropType::U32 => reduce_unsigned(vals, |x| Prop::U32(x as u32), op), +// PropType::U64 => reduce_unsigned(vals, |x| Prop::U64(x), op), +// +// PropType::I32 => reduce_signed(vals, |x| Prop::I32(x as i32), op), +// PropType::I64 => reduce_signed(vals, |x| Prop::I64(x), op), +// +// PropType::F32 => reduce_float(vals, |x| Prop::F32(x as f32), op), +// PropType::F64 => reduce_float(vals, |x| Prop::F64(x), op), +// _ => None, +// } +// } +// Op::First | Op::Last | Op::Any | Op::All => unreachable!(), +// } + +// pub fn apply_agg_to_prop(p: &Prop, op: Op) -> Option { +// match (op, p) { +// (Op::Len, Prop::List(inner)) => Some(Prop::U64(inner.len() as u64)), +// (Op::Sum, Prop::List(inner)) +// | (Op::Avg, Prop::List(inner)) +// | (Op::Min, Prop::List(inner)) +// | (Op::Max, Prop::List(inner)) => aggregate_values(inner.iter(), op), +// +// (Op::Len, _) => Some(Prop::U64(1)), +// +// (Op::Sum, Prop::U8(x)) => Some(Prop::U8(*x)), +// (Op::Sum, Prop::U16(x)) => Some(Prop::U16(*x)), +// (Op::Sum, Prop::U32(x)) => Some(Prop::U32(*x)), +// (Op::Sum, Prop::U64(x)) => Some(Prop::U64(*x)), +// (Op::Sum, Prop::I32(x)) => Some(Prop::I32(*x)), +// (Op::Sum, Prop::I64(x)) => Some(Prop::I64(*x)), +// (Op::Sum, Prop::F32(x)) => { +// if x.is_finite() { +// Some(Prop::F32(*x)) +// } else { +// None +// } +// } +// (Op::Sum, Prop::F64(x)) => { +// if x.is_finite() { +// Some(Prop::F64(*x)) +// } else { +// None +// } +// } +// +// (Op::Avg, Prop::U8(x)) => Some(Prop::F64(*x as f64)), +// (Op::Avg, Prop::U16(x)) => Some(Prop::F64(*x as f64)), +// (Op::Avg, Prop::U32(x)) => Some(Prop::F64(*x as f64)), +// (Op::Avg, Prop::U64(x)) => Some(Prop::F64(*x as f64)), +// (Op::Avg, Prop::I32(x)) => Some(Prop::F64(*x as f64)), +// (Op::Avg, Prop::I64(x)) => Some(Prop::F64(*x as f64)), +// (Op::Avg, Prop::F32(x)) => { +// if x.is_finite() { +// Some(Prop::F32(*x)) +// } else { +// None +// } +// } +// (Op::Avg, Prop::F64(x)) => { +// if x.is_finite() { +// Some(Prop::F64(*x)) +// } else { +// None +// } +// } +// +// (Op::Min, Prop::U8(x)) => Some(Prop::U8(*x)), +// (Op::Min, Prop::U16(x)) => Some(Prop::U16(*x)), +// (Op::Min, Prop::U32(x)) => Some(Prop::U32(*x)), +// (Op::Min, Prop::U64(x)) => Some(Prop::U64(*x)), +// (Op::Min, Prop::I32(x)) => Some(Prop::I32(*x)), +// (Op::Min, Prop::I64(x)) => Some(Prop::I64(*x)), +// (Op::Min, Prop::F32(x)) => { +// if x.is_finite() { +// Some(Prop::F32(*x)) +// } else { +// None +// } +// } +// (Op::Min, Prop::F64(x)) => { +// if x.is_finite() { +// Some(Prop::F64(*x)) +// } else { +// None +// } +// } +// +// (Op::Max, Prop::U8(x)) => Some(Prop::U8(*x)), +// (Op::Max, Prop::U16(x)) => Some(Prop::U16(*x)), +// (Op::Max, Prop::U32(x)) => Some(Prop::U32(*x)), +// (Op::Max, Prop::U64(x)) => Some(Prop::U64(*x)), +// (Op::Max, Prop::I32(x)) => Some(Prop::I32(*x)), +// (Op::Max, Prop::I64(x)) => Some(Prop::I64(*x)), +// (Op::Max, Prop::F32(x)) => { +// if x.is_finite() { +// Some(Prop::F32(*x)) +// } else { +// None +// } +// } +// (Op::Max, Prop::F64(x)) => { +// if x.is_finite() { +// Some(Prop::F64(*x)) +// } else { +// None +// } +// } +// +// (Op::Sum, _) | (Op::Avg, _) | (Op::Min, _) | (Op::Max, _) => None, +// +// _ => None, +// } +// } + +// impl PropertyFilter { +// fn reduce_qualifiers_rec( +// &self, +// quals: &[Op], +// v: &Prop, +// predicate: &dyn Fn(&Prop) -> bool, +// ) -> bool { +// if quals.is_empty() { +// return predicate(v); +// } +// let (q, rest) = (quals[0], &quals[1..]); +// +// if let Prop::List(inner) = v { +// let mut elems = inner.iter().peekable(); +// let check = |e: &Prop| { +// if rest.is_empty() { +// predicate(e) +// } else { +// self.reduce_qualifiers_rec(rest, e, predicate) +// } +// }; +// return match q { +// Op::Any => elems.any(|p| check(&p)), +// Op::All => !elems.peek().is_none() && elems.all(|p| check(&p)), +// _ => unreachable!(), +// }; +// } +// +// if rest.is_empty() { +// return match q { +// Op::Any | Op::All => predicate(v), +// _ => unreachable!(), +// }; +// } +// self.reduce_qualifiers_rec(rest, v, predicate) +// } +// +// fn eval_ops(&self, mut state: ValueType) -> (Option, Option>, Vec, bool) { +// let mut qualifiers: Vec = Vec::new(); +// let mut seq_is_temporal = matches!(state, ValueType::Seq(..)); +// let mut seen_qual_before_agg = false; +// +// let per_step_map = |vals: Vec, op: Op| -> Vec { +// vals.into_iter() +// .filter_map(|p| apply_agg_to_prop(&p, op)) +// .collect() +// }; +// +// // Aggregate OVER TIME (when no prior qualifier). +// let reduce_over_seq = |vs: Vec, op: Op| -> Option { +// match op { +// Op::Len => Some(Prop::U64(vs.len() as u64)), +// Op::Sum | Op::Avg | Op::Min | Op::Max => { +// if vs.is_empty() || matches!(vs.first(), Some(Prop::List(_))) { +// return None; +// } +// aggregate_values(&vs, op) +// } +// _ => None, +// } +// }; +// +// for op in &self.ops { +// match *op { +// Op::First | Op::Last => { +// state = match state { +// ValueType::Seq(vs) => { +// seq_is_temporal = false; +// let v = if matches!(*op, Op::First) { +// vs.first() +// } else { +// vs.last() +// }; +// ValueType::Scalar(v.cloned()) +// } +// s @ ValueType::Scalar(_) => s, +// }; +// } +// +// Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => { +// state = match state { +// ValueType::Seq(vs) if seen_qual_before_agg => { +// ValueType::Seq(per_step_map(vs, *op)) +// } +// ValueType::Seq(vs) => { +// seq_is_temporal = false; +// ValueType::Scalar(reduce_over_seq(vs, *op)) +// } +// ValueType::Scalar(Some(Prop::List(inner))) => { +// ValueType::Scalar(aggregate_values(inner.iter(), *op)) +// } +// ValueType::Scalar(Some(p)) => ValueType::Scalar(apply_agg_to_prop(&p, *op)), +// ValueType::Scalar(None) => ValueType::Scalar(None), +// }; +// } +// +// Op::Any | Op::All => { +// qualifiers.push(*op); +// seen_qual_before_agg = true; +// state = match state { +// ValueType::Seq(vs) => ValueType::Seq(vs), // still temporal +// ValueType::Scalar(Some(Prop::List(inner))) => { +// seq_is_temporal = false; +// ValueType::Seq(vec![Prop::List(inner)]) +// } +// ValueType::Scalar(Some(p)) => { +// seq_is_temporal = false; +// ValueType::Seq(vec![p]) +// } +// ValueType::Scalar(None) => { +// seq_is_temporal = false; +// ValueType::Seq(vec![]) +// } +// }; +// } +// } +// } +// +// match state { +// ValueType::Scalar(v) => (v, None, qualifiers, seq_is_temporal), +// ValueType::Seq(vs) => (None, Some(vs), qualifiers, seq_is_temporal), +// } +// } +// +// fn apply_eval( +// &self, +// reduced: Option, +// maybe_seq: Option>, +// qualifiers: Vec, +// seq_is_temporal: bool, +// ) -> bool { +// if let Some(value) = reduced { +// return self +// .operator +// .apply_to_property(&self.prop_value, Some(&value)); +// } +// +// if !qualifiers.is_empty() { +// let (temporal_q_opt, elem_quals) = if seq_is_temporal { +// (Some(qualifiers[0]), &qualifiers[1..]) +// } else { +// (None, &qualifiers[..]) +// }; +// let pred = |p: &Prop| self.operator.apply_to_property(&self.prop_value, Some(p)); +// let Some(seq) = maybe_seq else { return false }; +// +// if let Some(tq) = temporal_q_opt { +// let mut saw = false; +// match tq { +// Op::All => { +// for p in &seq { +// saw = true; +// let ok = if elem_quals.is_empty() { +// pred(p) +// } else { +// self.reduce_qualifiers_rec(elem_quals, p, &pred) +// }; +// if !ok { +// return false; +// } +// } +// return saw; +// } +// Op::Any => { +// for p in &seq { +// let ok = if elem_quals.is_empty() { +// pred(p) +// } else { +// self.reduce_qualifiers_rec(elem_quals, p, &pred) +// }; +// if ok { +// return true; +// } +// } +// return false; +// } +// _ => unreachable!(), +// } +// } else { +// for p in &seq { +// let ok = if elem_quals.is_empty() { +// pred(p) +// } else { +// self.reduce_qualifiers_rec(elem_quals, p, &pred) +// }; +// if ok { +// return true; +// } +// } +// return false; +// } +// } +// +// if let Some(seq) = maybe_seq { +// let full = Prop::List(seq.into()); +// self.operator +// .apply_to_property(&self.prop_value, Some(&full)) +// } else { +// self.operator.apply_to_property(&self.prop_value, None) +// } +// } +// +// pub fn eval_scalar_and_apply(&self, prop: Option) -> bool { +// let (r, s, q, is_t) = self.eval_ops(ValueType::Scalar(prop)); +// self.apply_eval(r, s, q, is_t) +// } +// +// pub fn eval_temporal_and_apply(&self, props: Vec) -> bool { +// let (r, s, q, is_t) = self.eval_ops(ValueType::Seq(props)); +// self.apply_eval(r, s, q, is_t) +// } +// } From 4a000c800dea765810001e76d648aae42fdceb35 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:26:18 +0100 Subject: [PATCH 025/155] fix --- .../src/db/graph/views/filter/model/node_expr/exprs.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 20c0eb7ad7..c15c96dc3f 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 @@ -168,7 +168,7 @@ impl NodeExpr for usize { &self, _graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(*self.into()))) + Ok(Arc::new(Const(Some(Prop::U64(*self as u64))))) } } @@ -183,7 +183,7 @@ impl NodeExpr for String { &self, _graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(self.clone().into_prop()))) + Ok(Arc::new(Const(Some(self.clone().into_prop())))) } } @@ -213,7 +213,7 @@ impl NodeExpr for &'static str { &self, _graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(*self.into_prop())))) + Ok(Arc::new(Const(Some((*self).into_prop())))) } } @@ -253,7 +253,7 @@ macro_rules! impl_node_expr_for_numeric { &self, _graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(*self)))) + Ok(Arc::new(Const(Some(Prop::$variant(*self))))) } } }; From 30e4d7f9db4ad2ff48373d1a396ed4f8b69d33f9 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:32:21 +0100 Subject: [PATCH 026/155] ref --- raphtory/src/db/graph/views/filter/model/node_expr/ops.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index a07468c159..3ac72ae487 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -57,7 +57,7 @@ use crate::{ use raphtory_api::core::entities::edges::edge_ref::EdgeRef; use raphtory_api::core::{ entities::{ - properties::prop::{Prop, PropArray, PropType}, + properties::prop::{IntoProp, Prop, PropArray, PropType}, VID, }, storage::arc_str::ArcStr, @@ -186,7 +186,7 @@ macro_rules! impl_agg_entity_op { #[derive(Clone)] pub struct $edge_name<'g> { - pub inner: Arc + 'g>, + pub inner: Arc> + 'g>, } impl<'g> EdgeOp for $edge_name<'g> { @@ -273,7 +273,7 @@ impl_agg_entity_op!(LastNodeOp, LastEdgeOp, |vals| { aggregate_values(vals, |pi| pi.last()) }); impl_agg_entity_op!(LenNodeOp, LenEdgeOp, |vals| { - aggregate_values(vals, |pi| pi.count().into_prop()) + aggregate_values(vals, |pi| Some(pi.count().into_prop())) }); impl_agg_entity_op!(AnyNodeOp, AnyEdgeOp, |vals| { aggregate_values(vals, |pi| { From 0277285e223cb986b558ada90b63e1fc4da2aba4 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:50:27 +0100 Subject: [PATCH 027/155] refactor(filter): unify NodeExpr output to Option, replace quantifier ops with list-aware aggregates --- .../views/filter/model/node_expr/filters.rs | 56 +++++- .../graph/views/filter/model/node_expr/ops.rs | 183 ++++++------------ 2 files changed, 115 insertions(+), 124 deletions(-) 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 ccc4f72944..479c7af8fb 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 @@ -37,10 +37,10 @@ use super::{ ops::{ - AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, PropListCompareOp, PropListInSetOp, - PropListStringOp, PropValueSetNodeOp, SetNodeOp, StringNodeOp, UnaryNodeOp, + AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, + ListAwareStringNodeOp, PropValueSetNodeOp, SetNodeOp, StringNodeOp, UnaryNodeOp, }, - NodeExpr, TemporalPropertyExpr, + EntityExpr, NodeExpr, TemporalPropertyExpr, }; use crate::{ db::{ @@ -797,3 +797,53 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { } impl NodeExprFilterOps for E {} + +// ───────────────────────────────────────────────────────────────────────────── +// NodeExpr impls for filter types — enables mid-chain use before .any()/.all() +// +// e.g. temporal().sum().gt(5).any() +// temporal().contains("rock").all() +// temporal().is_in([...]).any() +// ───────────────────────────────────────────────────────────────────────────── + +impl EntityExpr for BinaryCmpNodeFilter {} + +impl NodeExpr for BinaryCmpNodeFilter { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + Ok(Arc::new(ListAwareCmpNodeOp { left, right, op: self.op })) + } +} + +impl EntityExpr for StringNodeFilter {} + +impl NodeExpr for StringNodeFilter { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + Ok(Arc::new(ListAwareStringNodeOp { left, right, op: self.op })) + } +} + +impl EntityExpr for PropValueSetFilter {} + +impl NodeExpr for PropValueSetFilter { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.expr.create_node_op(graph)?; + Ok(Arc::new(ListAwareSetNodeOp { + inner, + values: self.values.clone(), + op: self.op, + })) + } +} 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 index 3ac72ae487..4f4330f588 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -60,7 +60,6 @@ use raphtory_api::core::{ properties::prop::{IntoProp, Prop, PropArray, PropType}, VID, }, - storage::arc_str::ArcStr, }; use raphtory_storage::graph::graph::GraphStorage; use std::{collections::HashSet, hash::Hash, sync::Arc}; @@ -287,144 +286,86 @@ impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { }); // ───────────────────────────────────────────────────────────────────────────── -// AnyNodeOp / AllNodeOp — unary reducers over a Prop::List of booleans +// ListAwareCmpNodeOp / ListAwareStringNodeOp / ListAwareSetNodeOp +// +// These ops implement NodeExpr for BinaryCmpNodeFilter, StringNodeFilter, and +// PropValueSetFilter respectively, enabling mid-chain use before .any()/.all(). +// +// Each uses aggregate_values so arbitrary nesting depth is handled automatically: +// temporal().sum().gt(5).any() +// temporal().contains("rock").all() +// temporal().is_in([...]).any() // ───────────────────────────────────────────────────────────────────────────── -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 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>, +#[derive(Clone)] +pub(crate) struct ListAwareCmpNodeOp<'g> { + pub(crate) left: Arc> + 'g>, + pub(crate) right: 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; +impl<'g> NodeOp for ListAwareCmpNodeOp<'g> { + type Output = Option; - 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)), - } + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + let lv = self.left.apply(storage, node); + let rhs = self.right.apply(storage, node)?; + let op = &self.op; + aggregate_values(lv, |pi| { + let bools: Vec = pi + .map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))) + .collect(); + if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + }) } } -// ───────────────────────────────────────────────────────────────────────────── -// PropListInSetOp<'g> — element-wise set-membership test on a Prop::List -// ───────────────────────────────────────────────────────────────────────────── - -/// Checks each element of a `Prop::List` against a fixed `Vec`, producing -/// `Prop::List([Bool, …])`. The result is then reduced by [`AnyNodeOp`] or [`AllNodeOp`]. -pub(crate) struct PropListInSetOp<'g> { - pub(crate) inner: Arc + 'g>, - pub(crate) values: Vec, - pub(crate) op: SetOp, -} - -impl<'g> Clone for PropListInSetOp<'g> { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - values: self.values.clone(), - op: self.op, - } - } +#[derive(Clone)] +pub(crate) struct ListAwareStringNodeOp<'g> { + pub(crate) left: Arc> + 'g>, + pub(crate) right: Arc> + 'g>, + pub(crate) op: StringOp, } -impl<'g> NodeOp for PropListInSetOp<'g> { - type Output = Prop; +impl<'g> NodeOp for ListAwareStringNodeOp<'g> { + type Output = Option; - fn apply(&self, storage: &GraphStorage, node: VID) -> Prop { - let Prop::List(arr) = self.inner.apply(storage, node) else { - return Prop::List(PropArray::from(vec![])); - }; - let bools: Vec = arr - .iter() - .map(|v| { - Prop::Bool(match self.op { - SetOp::IsIn => self.values.iter().any(|x| x == &v), - SetOp::IsNotIn => self.values.iter().all(|x| x != &v), - }) - }) - .collect(); - Prop::List(PropArray::from(bools)) + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + let lv = self.left.apply(storage, node); + let rhs = self.right.apply(storage, node); + let op = &self.op; + aggregate_values(lv, |pi| { + let bools: Vec = pi + .map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))) + .collect(); + if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + }) } } -// ───────────────────────────────────────────────────────────────────────────── -// PropListStringOp<'g> — element-wise string comparison on a Prop::List -// ───────────────────────────────────────────────────────────────────────────── - -/// Applies a [`StringOp`] to each element of a `Prop::List` against a scalar RHS, -/// producing `Prop::List([Bool, …])`. Reduced by [`AnyNodeOp`] or [`AllNodeOp`]. -pub(crate) struct PropListStringOp<'g> { - pub(crate) inner: Arc + 'g>, - pub(crate) rhs: ArcStr, - pub(crate) op: StringOp, -} - -impl<'g> Clone for PropListStringOp<'g> { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - rhs: self.rhs.clone(), - op: self.op, - } - } +#[derive(Clone)] +pub(crate) struct ListAwareSetNodeOp<'g> { + pub(crate) inner: Arc> + 'g>, + pub(crate) values: Vec, + pub(crate) op: SetOp, } -impl<'g> NodeOp for PropListStringOp<'g> { - type Output = Prop; +impl<'g> NodeOp for ListAwareSetNodeOp<'g> { + type Output = Option; - fn apply(&self, storage: &GraphStorage, node: VID) -> Prop { - let Prop::List(arr) = self.inner.apply(storage, node) else { - return Prop::List(PropArray::from(vec![])); - }; - let rhs = Some(Prop::Str(self.rhs.clone())); - let bools: Vec = arr - .iter() - .map(|v| Prop::Bool(Option::::string_cmp(&self.op, &Some(v), &rhs))) - .collect(); - Prop::List(PropArray::from(bools)) + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + let vals = self.inner.apply(storage, node); + let values = &self.values; + let op = &self.op; + aggregate_values(vals, |pi| { + let bools: Vec = pi + .map(|v| Prop::Bool(match op { + SetOp::IsIn => values.iter().any(|x| x == &v), + SetOp::IsNotIn => values.iter().all(|x| x != &v), + })) + .collect(); + if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + }) } } From 66786078195c43d5966c517958d40b5ff8d92210 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:09:21 +0100 Subject: [PATCH 028/155] more changes --- .../views/filter/model/edge_expr/exprs.rs | 129 +--- .../views/filter/model/edge_expr/filters.rs | 601 +++------------- .../graph/views/filter/model/edge_expr/mod.rs | 481 ++++--------- .../graph/views/filter/model/edge_expr/ops.rs | 217 ++---- .../src/db/graph/views/filter/model/mod.rs | 17 +- .../views/filter/model/node_expr/exprs.rs | 123 ++-- .../views/filter/model/node_expr/filters.rs | 225 ++---- .../graph/views/filter/model/node_expr/mod.rs | 131 +++- .../graph/views/filter/model/node_expr/ops.rs | 75 +- .../filter/model/property_filter/evaluate.rs | 680 +++++++++--------- raphtory/src/lib.rs | 2 +- 11 files changed, 928 insertions(+), 1753 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs index 9f5ddb3574..5019f23a11 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs @@ -1,22 +1,18 @@ //! EdgeExpr impls for the shared Property/Metadata structs and scalar types. -use super::{EdgeExpr, EdgeMetaOp, EdgeOp, EdgePropOp, LenEdgeOp, TemporalEdgePropOp}; -use crate::db::graph::views::filter::model::property_filter::Op; +use super::{ops::{EdgeMetaOp, EdgePropOp, TemporalEdgePropOp}, EdgeExpr, EdgeOp}; use crate::{ db::api::{state::ops::Const, view::internal::GraphView}, - db::graph::views::filter::model::{Metadata, Property}, + db::graph::views::filter::model::{node_expr::EntityExpr, Metadata, Property}, errors::GraphError, }; -use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use raphtory_api::core::entities::properties::prop::Prop; use std::sync::Arc; -use crate::db::graph::views::filter::model::node_expr::{AvgEdgeOp, FirstEdgeOp, LastEdgeOp, MaxEdgeOp, MinEdgeOp, SumEdgeOp}; // ───────────────────────────────────────────────────────────────────────────── // Property / Metadata — EdgeExpr impls // ───────────────────────────────────────────────────────────────────────────── impl EdgeExpr for Property { - type Output = Option; - fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -30,8 +26,6 @@ impl EdgeExpr for Property { } impl EdgeExpr for Metadata { - type Output = Option; - fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -44,47 +38,22 @@ impl EdgeExpr for Metadata { } } -// ───────────────────────────────────────────────────────────────────────────── -// IntoPropEdgeExpr — normalises RHS values to EdgeExpr> -// ───────────────────────────────────────────────────────────────────────────── - -pub trait IntoPropEdgeExpr { - type Expr: EdgeExpr>; - fn into_prop_edge_expr(self) -> Self::Expr; -} - -impl>> IntoPropEdgeExpr for T { - type Expr = T; - fn into_prop_edge_expr(self) -> T { - self - } -} - - // ───────────────────────────────────────────────────────────────────────────── // Scalar EdgeExpr impls — literal RHS values // ───────────────────────────────────────────────────────────────────────────── impl EdgeExpr for Prop { - type Output = Option; - fn create_edge_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_edge_expr_for_numeric { ($prim:ty, $variant:ident) => { impl EdgeExpr for $prim { - type Output = Option; - fn create_edge_op<'g, G: GraphView + 'g>( &self, _graph: G, @@ -106,8 +75,6 @@ impl_edge_expr_for_numeric!(u8, U8); impl_edge_expr_for_numeric!(u16, U16); impl EdgeExpr for &'static str { - type Output = Option; - fn create_edge_op<'g, G: GraphView + 'g>( &self, _graph: G, @@ -116,20 +83,14 @@ impl EdgeExpr for &'static str { raphtory_api::core::storage::arc_str::ArcStr::from(*self), ))))) } - - fn prop_type(&self) -> PropType { - PropType::Str - } } impl EdgeExpr for usize { - type Output = usize; - fn create_edge_op<'g, G: GraphView + 'g>( &self, _graph: G, - ) -> Result + 'g>, GraphError> { - Ok(Arc::new(Const(*self))) + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(Prop::U64(*self as u64))))) } } @@ -139,8 +100,9 @@ impl EdgeExpr for usize { /// Parallel to `TemporalPropertyExpr` but for edges: reads from `edge_meta()`. /// -/// Produced by `TemporalProp` internal conversion when `E: EdgeFilterFactory`. -/// Returns `Prop::List` of all temporal values within the view window. +/// Produced by `EdgeTemporalPropOps::into_expr()` on `TemporalProp` when `E: EdgeFilterFactory`, +/// or implicitly inside `.sum()`, `.any()`, etc. +/// Returns `Some(Prop::List([...]))` of all temporal values within the view window. #[derive(Clone)] pub struct TemporalEdgePropExpr { pub(crate) view_expr: E, @@ -154,14 +116,17 @@ impl TemporalEdgePropExpr { } impl - EdgeExpr for TemporalEdgePropExpr + EntityExpr for TemporalEdgePropExpr { - type Output = Prop; +} +impl + EdgeExpr for TemporalEdgePropExpr +{ fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, - ) -> Result + 'g>, GraphError> { + ) -> Result> + 'g>, GraphError> { let (prop_id, _) = graph .edge_meta() .get_prop_id_and_type(&self.name, false) @@ -170,69 +135,3 @@ impl -#[derive(Clone)] -pub struct LenEdgeExpr>(pub E); - -impl> EdgeExpr for LenEdgeExpr { - type Output = usize; - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result + 'g>, GraphError> { - let inner = self.0.create_edge_op(graph)?; - Ok(Arc::new(LenEdgeOp { inner })) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// UnwrapOptPropEdgeExpr — bridges Option → Prop for nested aggregation -// ───────────────────────────────────────────────────────────────────────────── - -// Bridges `E: EdgeExpr>` to `EdgeExpr`, -// enabling aggregator exprs to operate on values produced by a prior aggregation. -// -// Used when chaining e.g. `.temporal().last().sum()`: -// `last()` produces `EdgeAggregated>` with `Output = Option`; -// `sum()` wraps in `SumEdgeExpr>>`. -// #[derive(Clone)] -// pub struct UnwrapOptPropEdgeExpr>>(pub E); -// -// impl>> EdgeExpr for UnwrapOptPropEdgeExpr { -// type Output = Prop; -// -// fn create_edge_op<'g, G: crate::db::api::view::internal::GraphView + 'g>( -// &self, -// graph: G, -// ) -> Result + 'g>, crate::errors::GraphError> { -// let inner = self.0.create_edge_op(graph)?; -// Ok(std::sync::Arc::new(UnwrapOptPropEdgeOp { inner })) -// } -// } - -// ───────────────────────────────────────────────────────────────────────────── -// NestedMapEdgeExpr — per-element aggregation / quantification on a Prop::List -// ───────────────────────────────────────────────────────────────────────────── - -// Applies a per-element op to each element of a `Prop::List` produced by `E`. -// -// Used for chained expressions like `.temporal().any().sum()`. -// #[derive(Clone)] -// pub struct NestedMapEdgeExpr> { -// pub inner: E, -// pub op: Op, -// } -// -// impl> EdgeExpr for NestedMapEdgeExpr { -// type Output = Prop; -// -// fn create_edge_op<'g, G: crate::db::api::view::internal::GraphView + 'g>( -// &self, -// graph: G, -// ) -> Result + 'g>, crate::errors::GraphError> { -// let inner = self.inner.create_edge_op(graph)?; -// Ok(std::sync::Arc::new(NestedMapEdgeOp { inner, op: self.op })) -// } -// } diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index 40e16888fe..f8c2fa8a9e 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -5,13 +5,11 @@ use super::{ ops::{ - AllEdgeOp, AllPropEdgeOp, AnyEdgeOp, AnyPropEdgeOp, BinaryCmpEdgeOp, PropListEdgeCmpOp, - PropListInSetEdgeOp, PropListStringEdgeOp, PropValueSetEdgeOp, SetEdgeOp, StringEdgeOp, - UnaryEdgeOp, + BinaryCmpEdgeOp, ListAwareCmpEdgeOp, ListAwareSetEdgeOp, + ListAwareStringEdgeOp, PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, }, EdgeExpr, EdgeOp, }; -use raphtory_api::core::storage::arc_str::ArcStr; use crate::{ db::{ api::{state::ops::NotANodeFilter, view::internal::GraphView}, @@ -19,11 +17,10 @@ use crate::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, model::{ edge_filter::CompositeEdgeFilter, - filter_operator::{ - BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp, - }, - ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, - TryAsCompositeFilter, + filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, + node_expr::EntityExpr, + ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, CreateFilter, TryAsCompositeFilter, }, }, }, @@ -31,7 +28,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; -use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; +use std::{marker::PhantomData, sync::Arc}; // ───────────────────────────────────────────────────────────────────────────── // validate helpers @@ -57,7 +54,7 @@ fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), GraphEr pub struct BinaryCmpEdgeFilter where L: EdgeExpr, - R: EdgeExpr, + R: EdgeExpr, { pub left: L, pub op: BinaryOp, @@ -67,7 +64,7 @@ where impl BinaryCmpEdgeFilter where L: EdgeExpr, - R: EdgeExpr, + R: EdgeExpr, { pub fn new(left: L, op: BinaryOp, right: R) -> Self { Self { left, op, right } @@ -77,7 +74,7 @@ where impl Clone for BinaryCmpEdgeFilter where L: EdgeExpr, - R: EdgeExpr, + R: EdgeExpr, { fn clone(&self) -> Self { Self { @@ -88,17 +85,39 @@ where } } +impl EntityExpr for BinaryCmpEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, +{ +} + +impl EdgeExpr for BinaryCmpEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, +{ + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph)?; + Ok(Arc::new(ListAwareCmpEdgeOp { left, right, op: self.op })) + } +} + impl ComposableFilter for BinaryCmpEdgeFilter where L: EdgeExpr, - R: EdgeExpr, + R: EdgeExpr, { } impl TryAsCompositeFilter for BinaryCmpEdgeFilter where L: EdgeExpr, - R: EdgeExpr, + R: EdgeExpr, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -118,8 +137,7 @@ where impl CreateFilter for BinaryCmpEdgeFilter where L: EdgeExpr, - R: EdgeExpr, - L::Output: Comparable, + R: EdgeExpr, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; @@ -139,11 +157,8 @@ where let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph.clone())?; validate_binary_op(&self.op, &left.prop_type())?; - let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { - left, - right, - op: self.op, - }); + let op: Arc + 'graph> = + Arc::new(BinaryCmpEdgeOp { left, right, op: self.op }); Ok(EdgeExprFilteredGraph::new(graph, op)) } @@ -161,7 +176,7 @@ where pub struct UnaryEdgeFilter where - E: EdgeExpr>, + E: EdgeExpr, I: Clone + Send + Sync + 'static, { pub expr: E, @@ -171,7 +186,7 @@ where impl Clone for UnaryEdgeFilter where - E: EdgeExpr>, + E: EdgeExpr, I: Clone + Send + Sync + 'static, { fn clone(&self) -> Self { @@ -185,14 +200,14 @@ where impl ComposableFilter for UnaryEdgeFilter where - E: EdgeExpr>, + E: EdgeExpr, I: Clone + Send + Sync + 'static, { } impl TryAsCompositeFilter for UnaryEdgeFilter where - E: EdgeExpr>, + E: EdgeExpr, I: Clone + Send + Sync + 'static, { fn try_as_composite_node_filter(&self) -> Result { @@ -212,7 +227,7 @@ where impl CreateFilter for UnaryEdgeFilter where - E: EdgeExpr>, + E: EdgeExpr, I: Clone + Send + Sync + 'static, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = @@ -242,150 +257,6 @@ where } } -// ───────────────────────────────────────────────────────────────────────────── -// QuantifiedEdgeFilter -// ───────────────────────────────────────────────────────────────────────────── - -pub struct QuantifiedEdgeFilter -where - E: EdgeExpr, - Q: super::super::node_expr::QuantifierMode, - R: EdgeExpr>, -{ - pub expr: E, - pub op: BinaryOp, - pub rhs: R, - pub(crate) _q: PhantomData, -} - -impl QuantifiedEdgeFilter -where - E: EdgeExpr, - Q: super::super::node_expr::QuantifierMode, - R: EdgeExpr>, -{ - pub fn new(expr: E, op: BinaryOp, rhs: R) -> Self { - Self { expr, op, rhs, _q: PhantomData } - } -} - -impl Clone for QuantifiedEdgeFilter -where - E: EdgeExpr, - Q: super::super::node_expr::QuantifierMode, - R: EdgeExpr>, -{ - fn clone(&self) -> Self { - Self { - expr: self.expr.clone(), - op: self.op, - rhs: self.rhs.clone(), - _q: PhantomData, - } - } -} - -impl ComposableFilter for QuantifiedEdgeFilter -where - E: EdgeExpr, - Q: super::super::node_expr::QuantifierMode, - R: EdgeExpr>, -{ -} - -impl TryAsCompositeFilter for QuantifiedEdgeFilter -where - E: EdgeExpr, - Q: super::super::node_expr::QuantifierMode, - R: EdgeExpr>, -{ - 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) - } -} - -use super::super::node_expr::{AnyMode, AllMode}; - -impl CreateFilter for QuantifiedEdgeFilter -where - E: EdgeExpr, - R: EdgeExpr>, -{ - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - - fn create_filter<'graph, G: GraphViewOps<'graph>>( - self, - graph: G, - ) -> Result, GraphError> { - let temporal_op = self.expr.create_edge_op(graph.clone())?; - let rhs = self.rhs.create_edge_op(graph.clone())?; - let list_cmp: Arc + 'graph> = - Arc::new(PropListEdgeCmpOp { temporal_op, rhs, cmp_op: self.op, any: true }); - let op: Arc + 'graph> = - Arc::new(AnyEdgeOp { inner: list_cmp }); - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph>( - self, - _graph: G, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } -} - -impl CreateFilter for QuantifiedEdgeFilter -where - E: EdgeExpr, - R: EdgeExpr>, -{ - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - - fn create_filter<'graph, G: GraphViewOps<'graph>>( - self, - graph: G, - ) -> Result, GraphError> { - let temporal_op = self.expr.create_edge_op(graph.clone())?; - let rhs = self.rhs.create_edge_op(graph.clone())?; - let list_cmp: Arc + 'graph> = - Arc::new(PropListEdgeCmpOp { temporal_op, rhs, cmp_op: self.op, any: false }); - let op: Arc + 'graph> = - Arc::new(AllEdgeOp { inner: list_cmp }); - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph>( - self, - _graph: G, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } -} - // ───────────────────────────────────────────────────────────────────────────── // validate_string_op — reject non-string prop types at compile time // ───────────────────────────────────────────────────────────────────────────── @@ -407,8 +278,7 @@ fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { pub struct StringEdgeFilter where L: EdgeExpr, - R: EdgeExpr, - L::Output: StringComparable, + R: EdgeExpr, { pub left: L, pub op: StringOp, @@ -418,8 +288,7 @@ where impl StringEdgeFilter where L: EdgeExpr, - R: EdgeExpr, - L::Output: StringComparable, + R: EdgeExpr, { pub fn new(left: L, op: StringOp, right: R) -> Self { Self { left, op, right } @@ -429,27 +298,46 @@ where impl Clone for StringEdgeFilter where L: EdgeExpr, - R: EdgeExpr, - L::Output: StringComparable, + R: EdgeExpr, { fn clone(&self) -> Self { Self { left: self.left.clone(), op: self.op, right: self.right.clone() } } } +impl EntityExpr for StringEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, +{ +} + +impl EdgeExpr for StringEdgeFilter +where + L: EdgeExpr, + R: EdgeExpr, +{ + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph)?; + Ok(Arc::new(ListAwareStringEdgeOp { left, right, op: self.op })) + } +} + impl ComposableFilter for StringEdgeFilter where L: EdgeExpr, - R: EdgeExpr, - L::Output: StringComparable, + R: EdgeExpr, { } impl TryAsCompositeFilter for StringEdgeFilter where L: EdgeExpr, - R: EdgeExpr, - L::Output: StringComparable, + R: EdgeExpr, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -467,8 +355,7 @@ where impl CreateFilter for StringEdgeFilter where L: EdgeExpr, - R: EdgeExpr, - L::Output: StringComparable, + R: EdgeExpr, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; @@ -503,103 +390,33 @@ where // PropValueSetEdgeFilter — is_in / is_not_in for Option (linear scan) // ───────────────────────────────────────────────────────────────────────────── -pub struct PropValueSetEdgeFilter>> { +pub struct PropValueSetEdgeFilter { pub(crate) expr: E, pub(crate) values: Vec, pub(crate) op: SetOp, } -impl>> Clone for PropValueSetEdgeFilter { +impl Clone for PropValueSetEdgeFilter { fn clone(&self) -> Self { Self { expr: self.expr.clone(), values: self.values.clone(), op: self.op } } } -impl>> ComposableFilter for PropValueSetEdgeFilter {} +impl EntityExpr for PropValueSetEdgeFilter {} -impl>> TryAsCompositeFilter for PropValueSetEdgeFilter { - 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( +impl EdgeExpr for PropValueSetEdgeFilter { + fn create_edge_op<'g, G: GraphView + 'g>( &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - -impl>> CreateFilter for PropValueSetEdgeFilter { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - - fn create_filter<'graph, G: GraphViewOps<'graph>>( - self, graph: G, - ) -> Result, GraphError> { - let inner = self.expr.create_edge_op(graph.clone())?; - let op: Arc + 'graph> = - Arc::new(PropValueSetEdgeOp { inner, values: self.values, op: self.op }); - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph>( - self, - _graph: G, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) + ) -> Result> + 'g>, GraphError> { + let inner = self.expr.create_edge_op(graph)?; + Ok(Arc::new(ListAwareSetEdgeOp { inner, values: self.values.clone(), op: self.op })) } } -// ───────────────────────────────────────────────────────────────────────────── -// SetEdgeFilter — is_in / is_not_in for Option (HashSet, O(1)) -// ───────────────────────────────────────────────────────────────────────────── - -pub struct SetEdgeFilter -where - E: EdgeExpr>, - I: Eq + Hash + Clone + Send + Sync + 'static, -{ - pub expr: E, - pub op: SetOp, - pub values: Arc>, - pub(crate) _phantom: PhantomData, -} - -impl Clone for SetEdgeFilter -where - E: EdgeExpr>, - 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 SetEdgeFilter -where - E: EdgeExpr>, - I: Eq + Hash + Clone + Send + Sync + 'static, -{} +impl ComposableFilter for PropValueSetEdgeFilter {} -impl TryAsCompositeFilter for SetEdgeFilter -where - E: EdgeExpr>, - I: Eq + Hash + Clone + Send + Sync + 'static, -{ +impl TryAsCompositeFilter for PropValueSetEdgeFilter { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) } @@ -613,11 +430,7 @@ where } } -impl CreateFilter for SetEdgeFilter -where - E: EdgeExpr>, - I: Eq + Hash + Clone + Send + Sync + 'static, -{ +impl CreateFilter for PropValueSetEdgeFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; @@ -633,7 +446,7 @@ where ) -> Result, GraphError> { let inner = self.expr.create_edge_op(graph.clone())?; let op: Arc + 'graph> = - Arc::new(SetEdgeOp { inner, values: self.values, op: self.op }); + Arc::new(PropValueSetEdgeOp { inner, values: self.values, op: self.op }); Ok(EdgeExprFilteredGraph::new(graph, op)) } @@ -645,250 +458,6 @@ where } } -use super::super::node_expr::{AnyMode as AnyM, AllMode as AllM, QuantifierMode}; - -// ───────────────────────────────────────────────────────────────────────────── -// QuantifiedIsInEdgeFilter — quantified set-membership filter for edges -// ───────────────────────────────────────────────────────────────────────────── - -pub struct QuantifiedIsInEdgeFilter -where - E: EdgeExpr, - Q: QuantifierMode, -{ - pub(crate) expr: E, - pub(crate) values: Vec, - pub(crate) op: SetOp, - pub(crate) _q: PhantomData, -} - -impl Clone for QuantifiedIsInEdgeFilter -where - E: EdgeExpr, - Q: QuantifierMode, -{ - fn clone(&self) -> Self { - Self { - expr: self.expr.clone(), - values: self.values.clone(), - op: self.op, - _q: PhantomData, - } - } -} - -impl ComposableFilter for QuantifiedIsInEdgeFilter -where - E: EdgeExpr, - Q: QuantifierMode, -{ -} - -impl TryAsCompositeFilter for QuantifiedIsInEdgeFilter -where - E: EdgeExpr, - Q: QuantifierMode, -{ - 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 CreateFilter for QuantifiedIsInEdgeFilter -where - E: EdgeExpr, -{ - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - - fn create_filter<'graph, G: GraphViewOps<'graph>>( - self, - graph: G, - ) -> Result, GraphError> { - let inner: Arc + 'graph> = Arc::new(PropListInSetEdgeOp { - inner: self.expr.create_edge_op(graph.clone())?, - values: self.values, - op: self.op, - }); - let op: Arc + 'graph> = Arc::new(AnyPropEdgeOp { inner }); - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph>( - self, - _graph: G, - ) -> Result { - Err(GraphError::NotNodeFilter) - } -} - -impl CreateFilter for QuantifiedIsInEdgeFilter -where - E: EdgeExpr, -{ - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - - fn create_filter<'graph, G: GraphViewOps<'graph>>( - self, - graph: G, - ) -> Result, GraphError> { - let inner: Arc + 'graph> = Arc::new(PropListInSetEdgeOp { - inner: self.expr.create_edge_op(graph.clone())?, - values: self.values, - op: self.op, - }); - let op: Arc + 'graph> = Arc::new(AllPropEdgeOp { inner }); - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph>( - self, - _graph: G, - ) -> Result { - Err(GraphError::NotNodeFilter) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// QuantifiedStringEdgeFilter — quantified string-comparison filter for edges -// ───────────────────────────────────────────────────────────────────────────── - -pub struct QuantifiedStringEdgeFilter -where - E: EdgeExpr, - Q: QuantifierMode, -{ - pub(crate) expr: E, - pub(crate) rhs: ArcStr, - pub(crate) op: StringOp, - pub(crate) _q: PhantomData, -} - -impl Clone for QuantifiedStringEdgeFilter -where - E: EdgeExpr, - Q: QuantifierMode, -{ - fn clone(&self) -> Self { - Self { - expr: self.expr.clone(), - rhs: self.rhs.clone(), - op: self.op, - _q: PhantomData, - } - } -} - -impl ComposableFilter for QuantifiedStringEdgeFilter -where - E: EdgeExpr, - Q: QuantifierMode, -{ -} - -impl TryAsCompositeFilter for QuantifiedStringEdgeFilter -where - E: EdgeExpr, - Q: QuantifierMode, -{ - 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 CreateFilter for QuantifiedStringEdgeFilter -where - E: EdgeExpr, -{ - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - - fn create_filter<'graph, G: GraphViewOps<'graph>>( - self, - graph: G, - ) -> Result, GraphError> { - let inner: Arc + 'graph> = Arc::new(PropListStringEdgeOp { - inner: self.expr.create_edge_op(graph.clone())?, - rhs: self.rhs, - op: self.op, - }); - let op: Arc + 'graph> = Arc::new(AnyPropEdgeOp { inner }); - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph>( - self, - _graph: G, - ) -> Result { - Err(GraphError::NotNodeFilter) - } -} - -impl CreateFilter for QuantifiedStringEdgeFilter -where - E: EdgeExpr, -{ - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - - fn create_filter<'graph, G: GraphViewOps<'graph>>( - self, - graph: G, - ) -> Result, GraphError> { - let inner: Arc + 'graph> = Arc::new(PropListStringEdgeOp { - inner: self.expr.create_edge_op(graph.clone())?, - rhs: self.rhs, - op: self.op, - }); - let op: Arc + 'graph> = Arc::new(AllPropEdgeOp { inner }); - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph>( - self, - _graph: G, - ) -> Result { - Err(GraphError::NotNodeFilter) - } -} +// (AnyExpr and AllExpr terminate via +// BinaryCmpEdgeFilter, Prop> / BinaryCmpEdgeFilter, Prop> +// produced by EdgeExprFilterOps::any() / all()) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index edf9b2f88a..49bd6679c3 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -1,12 +1,13 @@ //! Edge expressions — what value an edge can produce. //! //! Mirrors [`node_expr`] exactly, but the subject is an edge rather than a node. +//! All expressions produce `Option` — no associated output type. //! //! # Two-phase pipeline (same as node_expr) //! //! ```text //! ┌─ Build phase (pure data, no graph) ──────────────────────┐ -//! │ EdgeFilter::property("weight") ← EdgePropertyExpr │ +//! │ EdgeFilter.property("weight") ← EdgeExpr │ //! │ .eq(5.0f64) ← BinaryCmpEdgeFilter │ //! └──────────────────────────────────────────────────────────┘ //! │ create_edge_op(graph)? ← resolve name → prop_id @@ -22,24 +23,20 @@ use crate::{ db::{ api::view::internal::GraphView, graph::views::filter::model::{ - filter_operator::{BinaryOp, SetOp, StringComparable, StringOp, UnaryOp}, - node_expr::{AllMode, AnyMode, QuantifierMode}, + filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, + node_expr::EntityExpr, node_expr::filters::TemporalProp, - property_filter::Op, CreateView, EdgeFilterFactory, MetadataExpr, PropertyExpr, }, }, errors::GraphError, }; -use raphtory_api::core::{ - entities::{ - edges::edge_ref::EdgeRef, - properties::prop::{Prop, PropType}, - }, - storage::arc_str::ArcStr, +use raphtory_api::core::entities::{ + edges::edge_ref::EdgeRef, + properties::prop::{Prop, PropType}, }; use raphtory_storage::graph::graph::GraphStorage; -use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; +use std::{marker::PhantomData, sync::Arc}; pub mod exprs; pub mod filters; @@ -47,8 +44,9 @@ pub mod ops; pub use exprs::*; pub use filters::*; -pub use ops::*; -use crate::db::graph::views::filter::model::{AvgExpr, FirstExpr, LastExpr, MaxExpr, MinExpr, SumExpr}; +use filters::BinaryCmpEdgeFilter; +use crate::db::graph::views::filter::model::{ FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr}; +use crate::db::graph::views::filter::model::node_expr::{AllExpr, AnyExpr, AvgExpr}; pub use super::{Metadata, Property}; // ───────────────────────────────────────────────────────────────────────────── @@ -74,197 +72,29 @@ pub trait EdgeOp: Send + Sync { /// A typed expression that produces a value per edge. /// -/// Parallel to [`NodeExpr`] — same two-phase design. -pub trait EdgeExpr: Clone + Send + Sync + 'static { - type Output: Clone + Send + Sync + 'static; - +/// Parallel to [`NodeExpr`] — all expressions produce `Option`; no associated output type. +/// +/// Usage: +/// ```rust,ignore +/// EdgeFilter.property("weight").gt(5.0f64) +/// EdgeFilter.property("tag").temporal().sum().gt(100i64) +/// EdgeFilter.property("label").temporal().into_expr().contains("foo").any() +/// ``` +pub trait EdgeExpr: EntityExpr + Clone + Send + Sync + 'static { fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, - ) -> Result + 'g>, GraphError>; - - fn prop_type(&self) -> PropType { - PropType::Empty - } + ) -> Result> + 'g>, GraphError>; } -// ───────────────────────────────────────────────────────────────────────────── -// EdgeAggregated — builder that produces BinaryCmpEdgeFilter -// ───────────────────────────────────────────────────────────────────────────── - -/// Returned by `EdgeTemporalProp::sum()`, `.first()`, etc. -/// Calling `.eq()` etc. produces a `BinaryCmpEdgeFilter`. -pub struct EdgeAggregated { - pub(crate) expr: E, -} - -impl>> EdgeAggregated { - fn finish( - self, - op: crate::db::graph::views::filter::model::filter_operator::BinaryOp, - rhs: R, - ) -> BinaryCmpEdgeFilter { - BinaryCmpEdgeFilter::new(self.expr, op, rhs.into_prop_edge_expr()) - } - - pub fn eq(self, rhs: R) -> BinaryCmpEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Eq, rhs) - } - pub fn ne(self, rhs: R) -> BinaryCmpEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Ne, rhs) - } - pub fn gt(self, rhs: R) -> BinaryCmpEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Gt, rhs) - } - pub fn ge(self, rhs: R) -> BinaryCmpEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Ge, rhs) - } - pub fn lt(self, rhs: R) -> BinaryCmpEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Lt, rhs) - } - pub fn le(self, rhs: R) -> BinaryCmpEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Le, rhs) - } - pub fn is_some(self) -> UnaryEdgeFilter { - UnaryEdgeFilter { - expr: self.expr, - op: crate::db::graph::views::filter::model::filter_operator::UnaryOp::IsSome, - _phantom: std::marker::PhantomData, - } - } - pub fn is_none(self) -> UnaryEdgeFilter { - UnaryEdgeFilter { - expr: self.expr, - op: crate::db::graph::views::filter::model::filter_operator::UnaryOp::IsNone, - _phantom: std::marker::PhantomData, - } - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// EdgeQuantified — builder that produces QuantifiedEdgeFilter -// ───────────────────────────────────────────────────────────────────────────── - -/// Returned by `EdgeTemporalProp::any()` / `all()`. -/// Calling `.eq()` etc. produces a `QuantifiedEdgeFilter`. -pub struct EdgeQuantified -where - E: EdgeExpr, - Q: QuantifierMode, -{ - pub(crate) expr: E, - pub(crate) _q: std::marker::PhantomData, -} - -impl EdgeQuantified -where - E: EdgeExpr, - Q: QuantifierMode, -{ - fn finish( - self, - op: crate::db::graph::views::filter::model::filter_operator::BinaryOp, - rhs: R, - ) -> QuantifiedEdgeFilter { - QuantifiedEdgeFilter::new(self.expr, op, rhs.into_prop_edge_expr()) - } - - pub fn eq(self, rhs: R) -> QuantifiedEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Eq, rhs) - } - pub fn ne(self, rhs: R) -> QuantifiedEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Ne, rhs) - } - pub fn gt(self, rhs: R) -> QuantifiedEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Gt, rhs) - } - pub fn ge(self, rhs: R) -> QuantifiedEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Ge, rhs) - } - pub fn lt(self, rhs: R) -> QuantifiedEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Lt, rhs) - } - pub fn le(self, rhs: R) -> QuantifiedEdgeFilter { - self.finish(crate::db::graph::views::filter::model::filter_operator::BinaryOp::Le, rhs) - } - - pub fn is_in(self, values: impl IntoIterator) -> QuantifiedIsInEdgeFilter { - QuantifiedIsInEdgeFilter { - expr: self.expr, - values: values.into_iter().collect(), - op: SetOp::IsIn, - _q: PhantomData, - } - } - - pub fn is_not_in(self, values: impl IntoIterator) -> QuantifiedIsInEdgeFilter { - QuantifiedIsInEdgeFilter { - expr: self.expr, - values: values.into_iter().collect(), - op: SetOp::IsNotIn, - _q: PhantomData, - } - } - - fn string_finish(self, op: StringOp, rhs: &str) -> QuantifiedStringEdgeFilter { - QuantifiedStringEdgeFilter { - expr: self.expr, - rhs: ArcStr::from(rhs), - op, - _q: PhantomData, - } - } - - pub fn starts_with(self, rhs: &str) -> QuantifiedStringEdgeFilter { - self.string_finish(StringOp::StartsWith, rhs) - } - - pub fn ends_with(self, rhs: &str) -> QuantifiedStringEdgeFilter { - self.string_finish(StringOp::EndsWith, rhs) - } - - pub fn contains(self, rhs: &str) -> QuantifiedStringEdgeFilter { - self.string_finish(StringOp::Contains, rhs) - } - - pub fn not_contains(self, rhs: &str) -> QuantifiedStringEdgeFilter { - self.string_finish(StringOp::NotContains, rhs) - } - - pub fn sum(self) -> EdgeQuantified, Q> { - EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Sum }, _q: PhantomData } - } - pub fn avg(self) -> EdgeQuantified, Q> { - EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Avg }, _q: PhantomData } - } - pub fn min(self) -> EdgeQuantified, Q> { - EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Min }, _q: PhantomData } - } - pub fn max(self) -> EdgeQuantified, Q> { - EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Max }, _q: PhantomData } - } - pub fn first(self) -> EdgeQuantified, Q> { - EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::First }, _q: PhantomData } - } - pub fn last(self) -> EdgeQuantified, Q> { - EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Last }, _q: PhantomData } - } - pub fn len(self) -> EdgeQuantified, Q> { - EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Len }, _q: PhantomData } - } - pub fn any(self) -> EdgeQuantified, AnyMode> { - EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::Any }, _q: PhantomData } - } - pub fn all(self) -> EdgeQuantified, AllMode> { - EdgeQuantified { expr: NestedMapEdgeExpr { inner: self.expr, op: Op::All }, _q: PhantomData } - } -} // ───────────────────────────────────────────────────────────────────────────── // EdgePropertyExprOps — fluent comparison API for edge-side property expressions // ───────────────────────────────────────────────────────────────────────────── -pub trait EdgePropertyExprOps: EdgeExpr> + Sized { +/// Presence and set-membership operators for `PropertyExpr` and `MetadataExpr` +/// on the edge side. +pub trait EdgePropertyExprOps: EdgeExpr + Sized { fn is_some(self) -> UnaryEdgeFilter { UnaryEdgeFilter { expr: self, op: UnaryOp::IsSome, _phantom: PhantomData } } @@ -313,45 +143,102 @@ impl EdgeProp // EdgeTemporalPropOps — fluent temporal API for edge-side TemporalProp // ───────────────────────────────────────────────────────────────────────────── +/// Temporal aggregation/quantification on `TemporalProp` when `E: EdgeFilterFactory`. +/// +/// Provides both aggregation (`.sum()`, `.avg()`, `.len()`, …) and direct +/// element-wise comparison (`.gt()`, `.eq()`, `.contains()`, …) so users +/// never have to call `.into_expr()` explicitly: +/// +/// ```rust,ignore +/// EdgeFilter.property("score").temporal().sum().gt(100i64) +/// EdgeFilter.property("score").temporal().gt(10i64).any() +/// EdgeFilter.property("score").temporal().len().gt(3usize) +/// EdgeFilter.property("label").temporal().contains("rock").any() +/// ``` pub trait EdgeTemporalPropOps: Sized { type ViewExpr: CreateView + EdgeFilterFactory + Clone + Send + Sync + 'static; fn into_temporal_parts(self) -> (Self::ViewExpr, String); - fn any(self) -> EdgeQuantified, AnyMode> { + fn into_expr(self) -> TemporalEdgePropExpr { let (view_expr, name) = self.into_temporal_parts(); - EdgeQuantified { expr: TemporalEdgePropExpr::new(view_expr, name), _q: PhantomData } + TemporalEdgePropExpr::new(view_expr, name) } - fn all(self) -> EdgeQuantified, AllMode> { - let (view_expr, name) = self.into_temporal_parts(); - EdgeQuantified { expr: TemporalEdgePropExpr::new(view_expr, name), _q: PhantomData } + fn sum(self) -> SumExpr> { + SumExpr(self.into_expr()) } - fn sum(self) -> EdgeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: SumExpr(TemporalEdgePropExpr::new(view_expr, name)) } + fn avg(self) -> AvgExpr> { + AvgExpr(self.into_expr()) } - fn avg(self) -> EdgeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: AvgExpr(TemporalEdgePropExpr::new(view_expr, name)) } + fn min(self) -> MinExpr> { + MinExpr(self.into_expr()) } - fn min(self) -> EdgeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: MinExpr(TemporalEdgePropExpr::new(view_expr, name)) } + fn max(self) -> MaxExpr> { + MaxExpr(self.into_expr()) } - fn max(self) -> EdgeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: MaxExpr(TemporalEdgePropExpr::new(view_expr, name)) } + fn first(self) -> FirstExpr> { + FirstExpr(self.into_expr()) } - fn first(self) -> EdgeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: FirstExpr(TemporalEdgePropExpr::new(view_expr, name)) } + fn last(self) -> LastExpr> { + LastExpr(self.into_expr()) } - fn last(self) -> EdgeAggregated>> { - let (view_expr, name) = self.into_temporal_parts(); - EdgeAggregated { expr: LastExpr(TemporalEdgePropExpr::new(view_expr, name)) } + fn len(self) -> LenExpr> { + LenExpr(self.into_expr()) } - fn len(self) -> LenEdgeExpr> { - let (view_expr, name) = self.into_temporal_parts(); - LenEdgeExpr(TemporalEdgePropExpr::new(view_expr, name)) + fn any(self) -> AnyExpr> { + AnyExpr(self.into_expr()) + } + fn all(self) -> AllExpr> { + AllExpr(self.into_expr()) + } + + // Direct comparison — no .into_expr() needed + fn gt(self, rhs: R) -> BinaryCmpEdgeFilter, R> { + BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Gt, rhs) + } + fn ge(self, rhs: R) -> BinaryCmpEdgeFilter, R> { + BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Ge, rhs) + } + fn lt(self, rhs: R) -> BinaryCmpEdgeFilter, R> { + BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Lt, rhs) + } + fn le(self, rhs: R) -> BinaryCmpEdgeFilter, R> { + BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Le, rhs) + } + fn eq(self, rhs: R) -> BinaryCmpEdgeFilter, R> { + BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Eq, rhs) + } + fn ne(self, rhs: R) -> BinaryCmpEdgeFilter, R> { + BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Ne, rhs) + } + fn contains(self, rhs: R) -> StringEdgeFilter, R> { + StringEdgeFilter::new(self.into_expr(), StringOp::Contains, rhs) + } + fn starts_with(self, rhs: R) -> StringEdgeFilter, R> { + StringEdgeFilter::new(self.into_expr(), StringOp::StartsWith, rhs) + } + fn ends_with(self, rhs: R) -> StringEdgeFilter, R> { + StringEdgeFilter::new(self.into_expr(), StringOp::EndsWith, rhs) + } + fn not_contains(self, rhs: R) -> StringEdgeFilter, R> { + StringEdgeFilter::new(self.into_expr(), StringOp::NotContains, rhs) + } + fn fuzzy_search( + self, + rhs: R, + levenshtein_distance: usize, + prefix_match: bool, + ) -> StringEdgeFilter, R> { + StringEdgeFilter::new( + self.into_expr(), + StringOp::FuzzySearch { levenshtein_distance, prefix_match }, + rhs, + ) + } + fn is_true(self) -> BinaryCmpEdgeFilter, Prop> { + BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(true)) + } + fn is_false(self) -> BinaryCmpEdgeFilter, Prop> { + BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(false)) } } @@ -368,167 +255,73 @@ impl EdgeTemp // EdgeExprFilterOps — comparison operators on any EdgeExpr // ───────────────────────────────────────────────────────────────────────────── -/// Comparison operators on any [`EdgeExpr`], regardless of output type. +/// Comparison, string, set, and presence operators on any [`EdgeExpr`]. /// -/// Unlike [`EdgePropertyExprOps`] (which is limited to `Output = Option`), -/// this trait works for any output type — in particular `usize` for `.len()`: +/// `.any()` / `.all()` are terminal: they wrap `self` in `AnyExpr`/`AllExpr` and compare +/// to `Bool(true)`. For element-wise comparison before reduction, chain in order: +/// `.gt(10i64).any()` not `.any().gt(10i64)`. /// /// ```rust,ignore -/// EdgeFilter.temporal_property("count").len().gt(3usize) -/// EdgeFilter.temporal_property("count").len().eq(0usize) +/// EdgeFilter.property("weight").gt(5.0f64) +/// EdgeFilter.property("tag").temporal().into_expr().contains("foo").any() +/// EdgeFilter.property("count").temporal().sum().gt(100i64) /// ``` pub trait EdgeExprFilterOps: EdgeExpr + Sized { - fn gt>(self, rhs: R) -> BinaryCmpEdgeFilter { + fn gt(self, rhs: R) -> BinaryCmpEdgeFilter { BinaryCmpEdgeFilter::new(self, BinaryOp::Gt, rhs) } - fn ge>(self, rhs: R) -> BinaryCmpEdgeFilter { + fn ge(self, rhs: R) -> BinaryCmpEdgeFilter { BinaryCmpEdgeFilter::new(self, BinaryOp::Ge, rhs) } - fn lt>(self, rhs: R) -> BinaryCmpEdgeFilter { + fn lt(self, rhs: R) -> BinaryCmpEdgeFilter { BinaryCmpEdgeFilter::new(self, BinaryOp::Lt, rhs) } - fn le>(self, rhs: R) -> BinaryCmpEdgeFilter { + fn le(self, rhs: R) -> BinaryCmpEdgeFilter { BinaryCmpEdgeFilter::new(self, BinaryOp::Le, rhs) } - fn eq>(self, rhs: R) -> BinaryCmpEdgeFilter { + fn eq(self, rhs: R) -> BinaryCmpEdgeFilter { BinaryCmpEdgeFilter::new(self, BinaryOp::Eq, rhs) } - fn ne>(self, rhs: R) -> BinaryCmpEdgeFilter { + fn ne(self, rhs: R) -> BinaryCmpEdgeFilter { BinaryCmpEdgeFilter::new(self, BinaryOp::Ne, rhs) } - fn starts_with>( - self, - rhs: R, - ) -> StringEdgeFilter - where - Self::Output: StringComparable, - { + fn starts_with(self, rhs: R) -> StringEdgeFilter { StringEdgeFilter::new(self, StringOp::StartsWith, rhs) } - fn ends_with>( - self, - rhs: R, - ) -> StringEdgeFilter - where - Self::Output: StringComparable, - { + fn ends_with(self, rhs: R) -> StringEdgeFilter { StringEdgeFilter::new(self, StringOp::EndsWith, rhs) } - fn contains>( - self, - rhs: R, - ) -> StringEdgeFilter - where - Self::Output: StringComparable, - { + fn contains(self, rhs: R) -> StringEdgeFilter { StringEdgeFilter::new(self, StringOp::Contains, rhs) } - fn not_contains>( - self, - rhs: R, - ) -> StringEdgeFilter - where - Self::Output: StringComparable, - { + fn not_contains(self, rhs: R) -> StringEdgeFilter { StringEdgeFilter::new(self, StringOp::NotContains, rhs) } - fn fuzzy_search>( + fn fuzzy_search( self, rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringEdgeFilter - where - Self::Output: StringComparable, - { + ) -> StringEdgeFilter { StringEdgeFilter::new( self, StringOp::FuzzySearch { levenshtein_distance, prefix_match }, rhs, ) } -} - -impl EdgeExprFilterOps for E {} - -// ───────────────────────────────────────────────────────────────────────────── -// EdgeAggregated string convenience — mirrors NodeAggregated::contains etc. -// ───────────────────────────────────────────────────────────────────────────── - -impl>> EdgeAggregated { - fn str_finish(self, op: StringOp, rhs: &str) -> StringEdgeFilter { - StringEdgeFilter::new(self.expr, op, Prop::Str(ArcStr::from(rhs))) - } - - pub fn starts_with(self, rhs: &str) -> StringEdgeFilter { - self.str_finish(StringOp::StartsWith, rhs) - } - pub fn ends_with(self, rhs: &str) -> StringEdgeFilter { - self.str_finish(StringOp::EndsWith, rhs) - } - pub fn contains(self, rhs: &str) -> StringEdgeFilter { - self.str_finish(StringOp::Contains, rhs) - } - pub fn not_contains(self, rhs: &str) -> StringEdgeFilter { - self.str_finish(StringOp::NotContains, rhs) - } - pub fn fuzzy_search( - self, - rhs: &str, - levenshtein_distance: usize, - prefix_match: bool, - ) -> StringEdgeFilter { - self.str_finish( - StringOp::FuzzySearch { levenshtein_distance, prefix_match }, - rhs, - ) - } - pub fn is_in( - self, - values: impl IntoIterator, - ) -> PropValueSetEdgeFilter { - PropValueSetEdgeFilter { - expr: self.expr, - values: values.into_iter().collect(), - op: SetOp::IsIn, - } - } - pub fn is_not_in( - self, - values: impl IntoIterator, - ) -> PropValueSetEdgeFilter { - PropValueSetEdgeFilter { - expr: self.expr, - values: values.into_iter().collect(), - op: SetOp::IsNotIn, - } + fn is_in(self, values: impl IntoIterator) -> PropValueSetEdgeFilter { + PropValueSetEdgeFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsIn } } - - pub fn sum(self) -> EdgeAggregated>> { - EdgeAggregated { expr: SumExpr(UnwrapOptPropEdgeExpr(self.expr)) } - } - pub fn avg(self) -> EdgeAggregated>> { - EdgeAggregated { expr: AvgExpr(UnwrapOptPropEdgeExpr(self.expr)) } - } - pub fn min(self) -> EdgeAggregated>> { - EdgeAggregated { expr: MinExpr(UnwrapOptPropEdgeExpr(self.expr)) } - } - pub fn max(self) -> EdgeAggregated>> { - EdgeAggregated { expr: MaxExpr(UnwrapOptPropEdgeExpr(self.expr)) } - } - pub fn first(self) -> EdgeAggregated>> { - EdgeAggregated { expr: FirstExpr(UnwrapOptPropEdgeExpr(self.expr)) } + fn is_not_in(self, values: impl IntoIterator) -> PropValueSetEdgeFilter { + PropValueSetEdgeFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsNotIn } } - pub fn last(self) -> EdgeAggregated>> { - EdgeAggregated { expr: LastEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) } + fn any(self) -> BinaryCmpEdgeFilter, Prop> { + BinaryCmpEdgeFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true)) } - pub fn len(self) -> LenEdgeExpr> { - LenEdgeExpr(UnwrapOptPropEdgeExpr(self.expr)) - } - pub fn any(self) -> EdgeQuantified, AnyMode> { - EdgeQuantified { expr: UnwrapOptPropEdgeExpr(self.expr), _q: PhantomData } - } - pub fn all(self) -> EdgeQuantified, AllMode> { - EdgeQuantified { expr: UnwrapOptPropEdgeExpr(self.expr), _q: PhantomData } + fn all(self) -> BinaryCmpEdgeFilter, Prop> { + BinaryCmpEdgeFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true)) } } + +impl EdgeExprFilterOps for E {} + diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 9d34a6c9e3..4db1cb52f6 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -21,7 +21,7 @@ use raphtory_storage::graph::graph::GraphStorage; use super::EdgeOp; use std::sync::Arc; use raphtory_api::core::entities::properties::prop::PropArray; -use raphtory_api::core::storage::arc_str::ArcStr; +use crate::db::graph::views::filter::model::property_filter::evaluate::aggregate_values; // ───────────────────────────────────────────────────────────────────────────── // Arc — blanket impl so Arc-boxed ops satisfy EdgeOp // ───────────────────────────────────────────────────────────────────────────── @@ -109,14 +109,14 @@ pub(crate) struct TemporalEdgePropOp { } impl EdgeOp for TemporalEdgePropOp { - type Output = Prop; + type Output = Option; - fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Prop { + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { let vals: Vec = EdgeView::new(&self.graph, edge) .temporal_iter(self.prop_id) .map(|(_, v)| v) .collect(); - Prop::List(raphtory_api::core::entities::properties::prop::PropArray::from(vals)) + Some(Prop::List(PropArray::from(vals))) } } @@ -168,59 +168,6 @@ impl<'g, I: Clone + Send + Sync + 'static> EdgeOp for UnaryEdgeOp<'g, I> { } } -// ───────────────────────────────────────────────────────────────────────────── -// PropListEdgeCmpOp<'g> — compares each element of a Prop::List against a RHS -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct PropListEdgeCmpOp<'g> { - pub(crate) temporal_op: Arc + 'g>, - pub(crate) rhs: Arc> + 'g>, - pub(crate) cmp_op: BinaryOp, - pub(crate) any: bool, -} - -impl<'g> EdgeOp for PropListEdgeCmpOp<'g> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - let list_prop = self.temporal_op.apply(storage, edge); - let rhs_val = self.rhs.apply(storage, edge); - let vals = match list_prop { - Prop::List(v) => v, - _ => return false, - }; - let mut results = vals.iter().map(|v| { - rhs_val - .as_ref() - .map(|r| Prop::binary_cmp(&self.cmp_op, &v, r)) - .unwrap_or(false) - }); - if self.any { - results.any(|b| b) - } else { - results.all(|b| b) - } - } -} - -// LenEdgeOp written explicitly: Output = usize, not Option -#[derive(Clone)] -pub(crate) struct LenEdgeOp<'g> { - pub(crate) inner: Arc + 'g>, -} - -impl<'g> EdgeOp for LenEdgeOp<'g> { - type Output = usize; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> usize { - match self.inner.apply(storage, edge) { - Prop::List(v) => v.iter().count(), - _ => 0, - } - } -} - // ───────────────────────────────────────────────────────────────────────────── // StringEdgeOp<'g, T> — applies a StringOp to two EdgeOp values // ───────────────────────────────────────────────────────────────────────────── @@ -297,142 +244,80 @@ impl<'g, I: Eq + Hash + Clone + Send + Sync + 'static> EdgeOp for SetEdgeOp<'g, } // ───────────────────────────────────────────────────────────────────────────── -// PropListInSetEdgeOp<'g> — element-wise set-membership test on a Prop::List +// ListAwareCmpEdgeOp<'g> — element-wise comparison via aggregate_values // ───────────────────────────────────────────────────────────────────────────── -/// Checks each element of a `Prop::List` against a fixed `Vec`, producing -/// `Prop::List([Bool, …])`. The result is then reduced by `AnyEdgeOp` or `AllEdgeOp`. #[derive(Clone)] -pub(crate) struct PropListInSetEdgeOp<'g> { - pub(crate) inner: Arc + 'g>, - pub(crate) values: Vec, - pub(crate) op: SetOp, -} - -impl<'g> EdgeOp for PropListInSetEdgeOp<'g> { - type Output = Prop; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Prop { - let Prop::List(arr) = self.inner.apply(storage, edge) else { - return Prop::List(PropArray::from(vec![])); - }; - let bools: Vec = arr - .iter() - .map(|v| { - Prop::Bool(match self.op { - SetOp::IsIn => self.values.iter().any(|x| x == &v), - SetOp::IsNotIn => self.values.iter().all(|x| x != &v), - }) - }) - .collect(); - Prop::List(PropArray::from(bools)) - } +pub(crate) struct ListAwareCmpEdgeOp<'g> { + pub(crate) left: Arc> + 'g>, + pub(crate) right: Arc> + 'g>, + pub(crate) op: BinaryOp, } -// ───────────────────────────────────────────────────────────────────────────── -// PropListStringEdgeOp<'g> — element-wise string comparison on a Prop::List -// ───────────────────────────────────────────────────────────────────────────── - -/// Applies a `StringOp` to each element of a `Prop::List` against a scalar RHS, -/// producing `Prop::List([Bool, …])`. Reduced by `AnyEdgeOp` or `AllEdgeOp`. -#[derive(Clone)] -pub(crate) struct PropListStringEdgeOp<'g> { - pub(crate) inner: Arc + 'g>, - pub(crate) rhs: ArcStr, - pub(crate) op: StringOp, -} +impl<'g> EdgeOp for ListAwareCmpEdgeOp<'g> { + type Output = Option; -impl<'g> EdgeOp for PropListStringEdgeOp<'g> { - type Output = Prop; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Prop { - let Prop::List(arr) = self.inner.apply(storage, edge) else { - return Prop::List(PropArray::from(vec![])); - }; - let rhs = Some(Prop::Str(self.rhs.clone())); - let bools: Vec = arr - .iter() - .map(|v| Prop::Bool(Option::::string_cmp(&self.op, &Some(v), &rhs))) - .collect(); - Prop::List(PropArray::from(bools)) + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + let lv = self.left.apply(storage, edge); + let rhs = self.right.apply(storage, edge)?; + let op = &self.op; + aggregate_values(lv, &|pi| { + let bools: Vec = pi.map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))).collect(); + if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + }) } } // ───────────────────────────────────────────────────────────────────────────── -// AnyPropEdgeOp / AllPropEdgeOp — reduce a Prop::List([Bool, …]) to bool +// ListAwareStringEdgeOp<'g> — element-wise string comparison via aggregate_values // ───────────────────────────────────────────────────────────────────────────── -fn prop_any_edge(prop: &Prop) -> bool { - match prop { - Prop::Bool(b) => *b, - Prop::List(arr) => arr.iter().any(|p| prop_any_edge(&p)), - _ => false, - } -} - -fn prop_all_edge(prop: &Prop) -> bool { - match prop { - Prop::Bool(b) => *b, - Prop::List(arr) => !arr.is_empty() && arr.iter().all(|p| prop_all_edge(&p)), - _ => false, - } -} - -/// Wraps a `PropListInSetEdgeOp` or `PropListStringEdgeOp` and returns `true` if -/// at least one element of the resulting `Prop::List([Bool, …])` is `true`. #[derive(Clone)] -pub(crate) struct AnyPropEdgeOp<'g> { - pub(crate) inner: Arc + 'g>, -} - -impl<'g> EdgeOp for AnyPropEdgeOp<'g> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - prop_any_edge(&self.inner.apply(storage, edge)) - } -} - -/// Wraps a `PropListInSetEdgeOp` or `PropListStringEdgeOp` and returns `true` only -/// if every element of the resulting `Prop::List([Bool, …])` is `true` (and the -/// list is non-empty). -#[derive(Clone)] -pub(crate) struct AllPropEdgeOp<'g> { - pub(crate) inner: Arc + 'g>, +pub(crate) struct ListAwareStringEdgeOp<'g> { + pub(crate) left: Arc> + 'g>, + pub(crate) right: Arc> + 'g>, + pub(crate) op: StringOp, } -impl<'g> EdgeOp for AllPropEdgeOp<'g> { - type Output = bool; +impl<'g> EdgeOp for ListAwareStringEdgeOp<'g> { + type Output = Option; - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - prop_all_edge(&self.inner.apply(storage, edge)) + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + let lv = self.left.apply(storage, edge); + let rhs = self.right.apply(storage, edge); + let op = &self.op; + aggregate_values(lv, &|pi| { + let bools: Vec = pi.map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))).collect(); + if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + }) } } // ───────────────────────────────────────────────────────────────────────────── -// UnwrapOptPropEdgeOp<'g> — converts Option → Prop for nested aggregation +// ListAwareSetEdgeOp<'g> — element-wise set membership via aggregate_values // ───────────────────────────────────────────────────────────────────────────── -/// Converts `Option` → `Prop` so that aggregator ops can operate on a value -/// produced by a prior aggregation step. -/// -/// - `Some(Prop::List(arr))` → `Prop::List(arr)` (pass through) -/// - `Some(v)` → `Prop::List([v])` (single-element list) -/// - `None` → `Prop::List([])` (empty — yields None from next aggregator) #[derive(Clone)] -pub(crate) struct UnwrapOptPropEdgeOp<'g> { +pub(crate) struct ListAwareSetEdgeOp<'g> { pub(crate) inner: Arc> + 'g>, + pub(crate) values: Vec, + pub(crate) op: SetOp, } -impl<'g> EdgeOp for UnwrapOptPropEdgeOp<'g> { - type Output = Prop; +impl<'g> EdgeOp for ListAwareSetEdgeOp<'g> { + type Output = Option; - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Prop { - match self.inner.apply(storage, edge) { - Some(Prop::List(arr)) => Prop::List(arr), - Some(v) => Prop::List(PropArray::from(vec![v])), - None => Prop::List(PropArray::from(vec![])), - } + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + let lv = self.inner.apply(storage, edge); + let values = &self.values; + let op = &self.op; + aggregate_values(lv, &|pi| { + let bools: Vec = pi.map(|v| Prop::Bool(match op { + SetOp::IsIn => values.iter().any(|x| x == &v), + SetOp::IsNotIn => values.iter().all(|x| x != &v), + })).collect(); + if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + }) } } diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index e0b1d59b2f..2d6707357f 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -15,11 +15,10 @@ pub use crate::{ UnaryOp, }, node_expr::{ - AllMode, AnyMode, BinaryCmpNodeFilter, + AllExpr, AnyExpr, AvgExpr, BinaryCmpNodeFilter, FirstExpr, LastExpr, LenExpr, MaxExpr, - MinExpr, NodeAggregated, NodeExpr, NodeExprFilterOps, NodePropertyExprOps, - NodeQuantified, NodeTemporalPropOps, PropValueSetFilter, QuantifiedNodeFilter, - QuantifierMode, SetNodeFilter, StringNodeFilter, SumExpr, TemporalExprOps, + MinExpr, NodeExpr, NodeExprFilterOps, NodePropertyExprOps, NodeTemporalPropOps, + PropValueSetFilter, StringNodeFilter, SumExpr, TemporalProp, UnaryNodeFilter, }, node_filter::{NodeFilter, NodeFilterFactory}, @@ -293,7 +292,7 @@ pub struct PropertyExpr { name: String, } -impl EntityExpr for PropertyExpr {} +impl EntityExpr for PropertyExpr {} impl NodeExpr for PropertyExpr @@ -318,7 +317,7 @@ pub struct MetadataExpr { name: String, } -impl EntityExpr for MetadataExpr {} +impl EntityExpr for MetadataExpr {} impl NodeExpr for MetadataExpr @@ -456,14 +455,12 @@ pub trait EdgeFilterFactory: PropertyFilterFactory + Clone {} use edge_expr::{ EdgeExpr, EdgeOp }; -use crate::db::graph::views::filter::model::edge_expr::{EdgeMetaOp, EdgePropOp}; +use crate::db::graph::views::filter::model::edge_expr::ops::{EdgeMetaOp, EdgePropOp}; use crate::db::graph::views::filter::model::node_expr::EntityExpr; impl EdgeExpr for PropertyExpr { - type Output = Option; - fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -480,8 +477,6 @@ impl EdgeExpr impl EdgeExpr for MetadataExpr { - type Output = Option; - fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, 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 c15c96dc3f..27dad2f803 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 @@ -6,57 +6,59 @@ //! //! # Field expressions //! +//! All expressions produce `Option` — field values are mapped via `into_prop()`. +//! //! ```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() +//! NodeFilter.id() // Id — e.g. .eq(GID::Str("v1".into())) +//! NodeFilter.name() // Name — e.g. .eq("Alice") +//! NodeFilter.node_type() // Type — 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()) +//! NodeFilter.degree() // DegreeExpr — e.g. .gt(2usize) +//! NodeFilter.in_degree() // DegreeExpr — e.g. .eq(0usize) +//! NodeFilter.out_degree() // DegreeExpr — 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())) +//! NodeFilter.property("age") // Property — e.g. .gt(30i64) +//! NodeFilter.property("score").is_some::() // nodes where "score" is set +//! NodeFilter.metadata("region") // Metadata — 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) +//! Accessed via `.temporal()` on `PropertyExpr` (returned by `.property("name")`): //! -//! // 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 +//! ```rust,ignore +//! // Quantifiers — compare element-wise then reduce with .any() / .all(): +//! NodeFilter.property("score").temporal().gt(10i64).any() // pass if any value > 10 +//! NodeFilter.property("score").temporal().gt(0i64).all() // 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 +//! // Aggregators: +//! NodeFilter.property("price").temporal().sum().gt(100i64) // SumExpr — pass if total > 100 +//! NodeFilter.property("price").temporal().avg().lt(50i64) // AvgExpr — pass if average < 50 +//! NodeFilter.property("ts").temporal().len().gt(3usize) // LenExpr — pass if more than 3 updates +//! NodeFilter.property("ts").temporal().first().eq(Prop::I64(0)) // FirstExpr — pass if first value == 0 +//! NodeFilter.property("ts").temporal().last().eq(Prop::I64(1)) // LastExpr — pass if last value == 1 +//! NodeFilter.property("v").temporal().min().gt(0i64) // MinExpr — pass if minimum > 0 +//! NodeFilter.property("v").temporal().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 +//! // Plain Rust values implement NodeExpr and produce Option — pass directly as RHS: +//! NodeFilter.degree().gt(2usize) // usize → Prop::U64 +//! NodeFilter.name().eq("Alice") // &str → Prop::Str +//! NodeFilter.name().eq("Bob".to_string()) // String → Prop::Str +//! NodeFilter.property("age").gt(30i64) // i64 → Prop::I64 +//! NodeFilter.property("score").eq(Prop::F64(9.5)) // Prop → passed as-is +//! // ConstExpr for custom comparable types not covered above //! ``` use super::{ops::{ @@ -86,14 +88,16 @@ use raphtory_api::core::{ Direction, }; use std::sync::Arc; +use crate::db::graph::views::filter::model::edge_expr::{EdgeExpr, EdgeOp}; // ───────────────────────────────────────────────────────────────────────────── // 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> +// All map their native types into Option via into_prop(): +// NodeFilter.id() uses Id — produces Option (GID mapped to Prop) +// NodeFilter.name() uses Name — produces Option (String as Prop::Str) +// NodeFilter.node_type() uses Type — produces Option (ArcStr as Prop::Str, None if unset) // ───────────────────────────────────────────────────────────────────────────── impl EntityExpr for Id {} @@ -152,9 +156,9 @@ impl NodeExpr for Type { // 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) +// NodeFilter.degree().gt(2usize) +// NodeFilter.name().eq("Alice") +// NodeFilter.property("age").gt(30i64) // ───────────────────────────────────────────────────────────────────────────── impl EntityExpr for usize { @@ -218,11 +222,11 @@ impl NodeExpr for &'static str { } // ───────────────────────────────────────────────────────────────────────────── -// IntoPropNodeExpr — normalises any RHS value to NodeExpr> +// Prop scalar — NodeExpr impl // -// Used as the bound on Quantified::eq/ne/gt/ge/lt/le and NodeAggregated::eq/ne/… -// so that .eq("Alice"), .eq(30i64), and .eq(NodeFilter::property("x")) all work -// with a single method name. +// All exprs produce Option, so Prop itself (and numeric/string primitives) +// implement NodeExpr directly. Pass them as the RHS of any comparison: +// .eq("Alice"), .gt(30i64), .eq(NodeFilter.property("x")) all share the same type. // ───────────────────────────────────────────────────────────────────────────── impl EntityExpr for Prop { @@ -283,12 +287,12 @@ pub struct ConstExpr(pub T); impl EntityExpr for ConstExpr {} -impl NodeExpr for ConstExpr { +impl + Clone + Send + Sync + 'static> NodeExpr for ConstExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(self.0.clone()))) + Ok(Arc::new(Const(Some(self.0.clone().into())))) } } @@ -298,13 +302,13 @@ impl NodeExpr for ConstExpr { /// Degree of a node in a given direction. /// -/// Created by `NodeFilter::degree()` / `::in_degree()` / `::out_degree()`. +/// 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`. +/// Compiles to `Degree { dir, view }.map(|a| Some(Prop::U64(a as u64)))`. /// /// ```rust,ignore -/// NodeFilter::degree().gt(2usize) -/// NodeFilter::out_degree().gt(NodeFilter::in_degree()) +/// NodeFilter.degree().gt(2usize) +/// NodeFilter.out_degree().gt(NodeFilter.in_degree()) /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub struct DegreeExpr { @@ -328,7 +332,7 @@ impl NodeExpr for DegreeExpr { dir: self.dir, view: self.view_expr.create_view(graph)?, } - .map(|a| Some(Prop::U64(a.into()))), + .map(|a| Some(Prop::U64(a as u64))), )) } } @@ -369,16 +373,16 @@ impl NodeExpr for Metadata { /// All temporal values of a named property over the current view window. /// -/// Produces `Prop::List` of every recorded value within the view. +/// Produces `Some(Prop::List([...]))` of every recorded value within the view. /// -/// Not constructed directly — created internally by the fluent chain started -/// by `NodeFilter::temporal_property(name)`: +/// Not constructed directly — obtained from `NodeTemporalPropOps::into_expr()`, +/// or implicitly via `.sum()` / `.any()` / etc. on `TemporalProp`: /// /// ```rust,ignore -/// // NodeFilter::temporal_property("score") returns TemporalProp, not this type. -/// // TemporalPropertyExpr is created inside .any() / .all() / .sum() etc., e.g.: -/// // .any().gt(10i64) → QuantifiedNodeFilter, AnyMode, i64> -/// // .sum().gt(100i64) → BinaryCmpNodeFilter>, i64> +/// // NodeFilter.property("score").temporal() returns TemporalProp, not this type. +/// // TemporalPropertyExpr is produced implicitly by NodeTemporalPropOps methods: +/// // .gt(10i64).any() → BinaryCmpNodeFilter>, Prop> +/// // .sum().gt(100i64) → BinaryCmpNodeFilter>, i64> /// ``` #[derive(Clone)] pub struct TemporalPropertyExpr { @@ -416,14 +420,15 @@ 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. Not constructed directly — -// TemporalProp / TemporalExprOps methods return NodeAggregated>: +// Each wraps an inner NodeExpr (typically TemporalPropertyExpr) and reduces +// the Prop::List it produces. Not constructed directly — +// TemporalProp methods return these exprs directly: // -// .temporal_property("v").sum() → NodeAggregated>> -// .temporal_property("v").len() → NodeAggregated>> +// .property("v").temporal().sum() → SumExpr> +// .property("v").temporal().len() → LenExpr> +// .property("v").temporal().any() → AnyExpr> // -// Calling .gt() / .eq() etc. on NodeAggregated then produces: +// Calling .gt() / .eq() etc. on any of these (via NodeExprFilterOps) 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 479c7af8fb..489f781c5a 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 @@ -8,39 +8,44 @@ //! //! ```text //! Phase 1 — Build (pure Rust data, no graph): -//! NodeFilter::property("age").gt(30i64) -//! ──► BinaryCmpNodeFilter { left: Property("age"), op: Gt, right: ConstExpr(30i64) } +//! NodeFilter.property("age").gt(30i64) +//! ──► BinaryCmpNodeFilter { left: Property("age"), op: Gt, right: 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 } +//! = BinaryCmpNodeOp { left: NodePropOp(id=3), right: Const(Some(I64(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 +//! Prop::binary_cmp(Gt, age_value, Some(I64(30))) → true/false //! ``` //! //! # Temporal quantification //! +//! Filter types also implement `NodeExpr` (producing list-aware ops), enabling chaining +//! before `.any()`/`.all()`: +//! //! ```rust,ignore //! // "pass if any temporal value of 'score' > 10" -//! NodeFilter::temporal_property("score").any().gt(10i64) -//! ──► QuantifiedNodeFilter> +//! NodeFilter.property("score").temporal().gt(10i64).any() +//! ──► BinaryCmpNodeFilter>, Prop> //! create_node_filter(graph)? -//! ──► AnyNodeOp { inner: PropListCompareOp { temporal_op, rhs: ConstNodeOp(10), op: Gt } } +//! ──► BinaryCmpNodeOp { left: AnyNodeOp { inner: ListAwareCmpNodeOp { TemporalNodePropOp, +//! Const(I64(10)), Gt } }, +//! right: Const(Bool(true)), op: Eq } //! //! // "pass if sum of 'score' > 100" -//! NodeFilter::temporal_property("score").sum().gt(100i64) -//! ──► BinaryCmpNodeFilter, ConstExpr> +//! NodeFilter.property("score").temporal().sum().gt(100i64) +//! ──► BinaryCmpNodeFilter, i64> //! ``` use super::{ ops::{ - AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, - ListAwareStringNodeOp, PropValueSetNodeOp, SetNodeOp, StringNodeOp, UnaryNodeOp, + BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, + ListAwareStringNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, - EntityExpr, NodeExpr, TemporalPropertyExpr, + AllExpr, AnyExpr, EntityExpr, NodeExpr, }; use crate::{ db::{ @@ -52,10 +57,10 @@ use crate::{ model::{ edge_filter::CompositeEdgeFilter, filter_operator::{ - BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp, + BinaryOp, SetOp, StringOp, UnaryOp, }, - node_filter::NodeFilterFactory, - property_filter::Op, + node_filter::NodeFilterFactory + , ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, CreateView, MetadataExpr, PropertyExpr, TryAsCompositeFilter, }, @@ -65,15 +70,8 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use raphtory_api::core::{ - entities::{ - properties::prop::{Prop, PropType}, - VID, - }, - storage::arc_str::ArcStr, -}; -use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; -use crate::db::graph::views::filter::model::{FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::{marker::PhantomData, sync::Arc}; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpNodeFilter — binary expression filter @@ -81,20 +79,20 @@ use crate::db::graph::views::filter::model::{FirstExpr, LastExpr, LenExpr, MaxEx /// 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. +/// Both sides produce `Option` at runtime. Created by [`NodeExprFilterOps`] methods +/// (`.gt`, `.lt`, `.eq`, `.ne`, `.ge`, `.le`). /// -/// Created by [`NodeExprFilterOps`] methods (`.gt`, `.lt`, `.eq`, `.ne`, `.ge`, `.le`). -/// Compiles to a `BinaryCmpNodeOp` wrapped in `Arc>`. +/// As a **terminal filter** (`CreateFilter`): compiles to `BinaryCmpNodeOp` → bool. +/// As a **mid-chain expression** (`NodeExpr`): compiles to `ListAwareCmpNodeOp` → `Option`. /// /// ```rust,ignore -/// NodeFilter::degree().gt(2usize) +/// NodeFilter.degree().gt(2usize) /// → BinaryCmpNodeFilter, usize> -/// → BinaryCmpNodeOp { left: Degree(..), right: ConstNodeOp(2), op: Gt } +/// → BinaryCmpNodeOp { left: Degree(..).map(Prop::U64), right: Const(Some(U64(2))), op: Gt } /// -/// NodeFilter::property("age").eq(30i64) +/// NodeFilter.property("age").eq(30i64) /// → BinaryCmpNodeFilter -/// → BinaryCmpNodeOp { left: NodePropOp(prop_id=N), right: ConstNodeOp(30), op: Eq } +/// → BinaryCmpNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(I64(30))), op: Eq } /// ``` pub struct BinaryCmpNodeFilter where @@ -236,7 +234,7 @@ where /// Compiles to a `UnaryNodeOp { inner, op }`. /// /// ```rust,ignore -/// NodeFilter::property("age").is_some() +/// NodeFilter.property("age").is_some::() /// → UnaryNodeFilter /// → UnaryNodeOp { inner: NodePropOp(prop_id=N), op: IsSome } /// ``` @@ -271,15 +269,14 @@ where { } -impl CreateFilter for UnaryNodeFilter +impl CreateFilter for UnaryNodeFilter where E: NodeExpr, - I: Clone + Send + Sync + 'static, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - NodeFilteredGraph>; + NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, I>; + type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, Prop>; type FilteredGraph<'graph, G> = G @@ -324,111 +321,6 @@ where } } -// ───────────────────────────────────────────────────────────────────────────── -// 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 // ───────────────────────────────────────────────────────────────────────────── @@ -441,13 +333,13 @@ where /// Compiles to a `StringNodeOp` wrapped in `Arc>`. /// /// ```rust,ignore -/// NodeFilter::name().starts_with("Al") +/// NodeFilter.name().starts_with("Al") /// → StringNodeFilter -/// → StringNodeOp { left: NameOp, right: ConstNodeOp("Al"), op: StartsWith } +/// → StringNodeOp { left: Name.map(...), right: Const(Some(Str("Al"))), op: StartsWith } /// -/// NodeFilter::property("tag").contains(Prop::Str("foo".into())) +/// NodeFilter.property("tag").contains(Prop::Str("foo".into())) /// → StringNodeFilter -/// → StringNodeOp { left: NodePropOp(prop_id=N), right: ConstNodeOp(Str("foo")), op: Contains } +/// → StringNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(Str("foo"))), op: Contains } /// ``` pub struct StringNodeFilter where @@ -621,27 +513,27 @@ impl TryAsCompositeFilter for PropValueSetFilter { } // ───────────────────────────────────────────────────────────────────────────── -// TemporalProp — entry point returned from `.temporal_property(name)` +// TemporalProp — entry point returned from `.property(name).temporal()` // ───────────────────────────────────────────────────────────────────────────── -/// Entry point returned by `NodeFilter::temporal_property(name)`. +/// Entry point returned by `PropertyExpr::temporal()`. /// /// `E` is the view expression (e.g. `NodeFilter`, `Windowed`, `Layered`) /// that scopes which temporal property values are visible. /// /// Calling a method produces the next step in the chain: /// ```rust,ignore -/// NodeFilter::temporal_property("score") // → TemporalProp -/// .any() // → Quantified, AnyMode> -/// .gt(10i64) // → QuantifiedNodeFilter<.., AnyMode, i64> +/// NodeFilter.property("score").temporal() // → TemporalProp +/// .gt(10i64) // → BinaryCmpNodeFilter +/// .any() // → BinaryCmpNodeFilter, Prop> /// -/// NodeFilter::temporal_property("price") // → TemporalProp -/// .sum() // → Aggregated>> -/// .gt(100i64) // → BinaryCmpNodeFilter, i64> +/// NodeFilter.property("price").temporal() // → TemporalProp +/// .sum() // → SumExpr> +/// .gt(100i64) // → BinaryCmpNodeFilter, i64> /// -/// NodeFilter.window(0, 100) -/// .temporal_property("score") // → TemporalProp> -/// .any().gt(10i64) +/// NodeFilter.window(0, 100).property("score") +/// .temporal() // → TemporalProp> +/// .gt(10i64).any() /// ``` pub struct TemporalProp { pub(crate) view_expr: E, @@ -686,11 +578,15 @@ impl NodeProp /// Comparison, string, set, and presence operators on any [`NodeExpr`]. /// +/// `.any()` / `.all()` are terminal: they wrap `self` in `AnyExpr`/`AllExpr` and compare the +/// result to `Bool(true)`. For element-wise comparison before reduction, chain in order: +/// `.gt(10i64).any()` not `.any().gt(10i64)`. +/// /// ```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]) +/// NodeFilter.degree().gt(2usize) +/// NodeFilter.out_degree().gt(NodeFilter.in_degree()) +/// NodeFilter.property("age").gt(30i64) +/// NodeFilter.property("score").temporal().gt(10i64).any() /// ``` pub trait NodeExprFilterOps: NodeExpr + Sized { fn gt(self, rhs: R) -> BinaryCmpNodeFilter { @@ -794,6 +690,13 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { self.eq(Prop::Bool(false)) } + fn any(self) -> BinaryCmpNodeFilter, Prop> { + BinaryCmpNodeFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true)) + } + + fn all(self) -> BinaryCmpNodeFilter, Prop> { + BinaryCmpNodeFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true)) + } } impl NodeExprFilterOps 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 index 4e98bd7f99..41124fcb61 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -1,5 +1,10 @@ use crate::{ db::api::{state::ops::NodeOp, view::internal::GraphView}, + db::graph::views::filter::model::{ + CreateView, + filter_operator::{BinaryOp, StringOp}, + node_filter::NodeFilterFactory, + }, errors::GraphError, }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; @@ -24,19 +29,20 @@ pub use super::{Metadata, Property}; /// 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. +/// All expressions produce `Option` — field values (`id`, `name`, `degree`) are +/// mapped into `Prop` variants; absent values (missing property, unset node type) map to `None`. /// /// 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") +/// NodeFilter.degree().gt(2usize) +/// NodeFilter.out_degree().gt(NodeFilter.in_degree()) +/// NodeFilter.property("age").gt(30i64) +/// NodeFilter.name().eq("Alice") +/// NodeFilter.property("score").temporal().sum().gt(100i64) +/// NodeFilter.property("score").temporal().gt(10i64).any() /// ``` /// pub trait NodeExpr: EntityExpr + Clone + Send + Sync + 'static { @@ -55,3 +61,114 @@ pub trait EntityExpr: Clone + Send + Sync + 'static { PropType::Empty } } + +// ───────────────────────────────────────────────────────────────────────────── +// NodeTemporalPropOps — aggregation and direct comparison on TemporalProp +// ───────────────────────────────────────────────────────────────────────────── + +/// Aggregation and comparison operators on `TemporalProp` when `E: NodeFilterFactory`. +/// +/// Provides both aggregation (`.sum()`, `.avg()`, `.len()`, …) and direct +/// element-wise comparison (`.gt()`, `.eq()`, `.contains()`, …) so users +/// never have to call `.into_expr()` explicitly: +/// +/// ```rust,ignore +/// NodeFilter.property("score").temporal().sum().gt(100i64) +/// NodeFilter.property("score").temporal().gt(10i64).any() +/// NodeFilter.property("score").temporal().len().gt(3usize) +/// NodeFilter.property("label").temporal().contains("rock").any() +/// ``` +pub trait NodeTemporalPropOps: Sized { + type ViewExpr: CreateView + NodeFilterFactory + Clone + Send + Sync + 'static; + fn into_temporal_parts(self) -> (Self::ViewExpr, String); + + fn into_expr(self) -> TemporalPropertyExpr { + let (view_expr, name) = self.into_temporal_parts(); + TemporalPropertyExpr { view_expr, name } + } + fn sum(self) -> SumExpr> { + SumExpr(self.into_expr()) + } + fn avg(self) -> AvgExpr> { + AvgExpr(self.into_expr()) + } + fn min(self) -> MinExpr> { + MinExpr(self.into_expr()) + } + fn max(self) -> MaxExpr> { + MaxExpr(self.into_expr()) + } + fn first(self) -> FirstExpr> { + FirstExpr(self.into_expr()) + } + fn last(self) -> LastExpr> { + LastExpr(self.into_expr()) + } + fn len(self) -> LenExpr> { + LenExpr(self.into_expr()) + } + fn any(self) -> AnyExpr> { + AnyExpr(self.into_expr()) + } + fn all(self) -> AllExpr> { + AllExpr(self.into_expr()) + } + + fn gt(self, rhs: R) -> BinaryCmpNodeFilter, R> { + BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Gt, rhs) + } + fn ge(self, rhs: R) -> BinaryCmpNodeFilter, R> { + BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Ge, rhs) + } + fn lt(self, rhs: R) -> BinaryCmpNodeFilter, R> { + BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Lt, rhs) + } + fn le(self, rhs: R) -> BinaryCmpNodeFilter, R> { + BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Le, rhs) + } + fn eq(self, rhs: R) -> BinaryCmpNodeFilter, R> { + BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Eq, rhs) + } + fn ne(self, rhs: R) -> BinaryCmpNodeFilter, R> { + BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Ne, rhs) + } + fn contains(self, rhs: R) -> StringNodeFilter, R> { + StringNodeFilter::new(self.into_expr(), StringOp::Contains, rhs) + } + fn starts_with(self, rhs: R) -> StringNodeFilter, R> { + StringNodeFilter::new(self.into_expr(), StringOp::StartsWith, rhs) + } + fn ends_with(self, rhs: R) -> StringNodeFilter, R> { + StringNodeFilter::new(self.into_expr(), StringOp::EndsWith, rhs) + } + fn not_contains(self, rhs: R) -> StringNodeFilter, R> { + StringNodeFilter::new(self.into_expr(), StringOp::NotContains, rhs) + } + fn fuzzy_search( + self, + rhs: R, + levenshtein_distance: usize, + prefix_match: bool, + ) -> StringNodeFilter, R> { + StringNodeFilter::new( + self.into_expr(), + StringOp::FuzzySearch { levenshtein_distance, prefix_match }, + rhs, + ) + } + fn is_true(self) -> BinaryCmpNodeFilter, Prop> { + BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(true)) + } + fn is_false(self) -> BinaryCmpNodeFilter, Prop> { + BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(false)) + } +} + +impl NodeTemporalPropOps + for TemporalProp +{ + type ViewExpr = E; + fn into_temporal_parts(self) -> (E, String) { + (self.view_expr, self.name) + } +} 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 index 4f4330f588..e11fd6f50d 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -9,16 +9,16 @@ //! # Evaluation pipeline //! //! ```text -//! NodeFilter::property("age") ← NodeExpr (pure data) -//! .create_node_op(graph)? ← resolve "age" → prop_id = 3 +//! 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) +//! 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) +//! ──► BinaryCmpNodeOp { left: NodePropOp, right: Const(Some(I64(30))), op: Gt } +//! apply: Prop::binary_cmp(Gt, age_value, Some(I64(30))) //! -//! NodeFilter::temporal_property("score").sum() ← SumExpr (pure data) +//! NodeFilter.property("score").temporal().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) @@ -26,17 +26,18 @@ //! //! # Quantified evaluation //! -//! [`PropListCompareOp`] applies a [`BinaryOp`] element-wise to a `Prop::List`, -//! then [`AnyNodeOp`] / [`AllNodeOp`] reduce the boolean list: +//! Filter types (`BinaryCmpNodeFilter`, `StringNodeFilter`, `PropValueSetFilter`) also +//! implement `NodeExpr`, producing list-aware ops for mid-chain use before `.any()`/`.all()`: //! //! ```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) +//! .gt(10i64) as NodeExpr → ListAwareCmpNodeOp → Prop::List([false, true, false]) +//! .any() → AnyNodeOp reduces boolean list → Prop::Bool(true) +//! Eq Bool(true) → true (at least one matched) //! ``` use super::EdgeOp; +use std::borrow::Borrow; use crate::db::graph::views::filter::model::property_filter::evaluate::{ scan_f64_sum_count, scan_i64_sum, scan_u64_sum, }; @@ -129,9 +130,9 @@ impl NodeOp for NodeMetaOp { /// Internal op produced by [`TemporalPropertyExpr::create_node_op`] — not constructed directly. /// -/// Collects all recorded values within the current view window into a `Prop::List`. +/// Collects all recorded values within the current view window into a `Some(Prop::List([...]))`. /// That list is then consumed by aggregator ops (`SumNodeOp`, `LenNodeOp`, …) or -/// by `PropListCompareOp` for quantified comparisons. +/// by `ListAwareCmpNodeOp` for element-wise comparisons before `.any()`/`.all()` reduction. #[derive(Clone)] pub(crate) struct TemporalNodePropOp { pub(crate) graph: G, @@ -199,7 +200,7 @@ macro_rules! impl_agg_entity_op { } impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { - aggregate_values(vals, |pi| { + aggregate_values(vals, &|pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { return None; @@ -231,7 +232,7 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { }); impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { - aggregate_values(vals, |pi| { + aggregate_values(vals, &|pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { return None; @@ -260,27 +261,35 @@ impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { }) }); impl_agg_entity_op!(MinNodeOp, MinEdgeOp, |vals| { - aggregate_values(vals, |pi| pi.min()) + aggregate_values(vals, &|pi| { + let mut it = pi; + let first = it.next()?; + it.fold(Some(first), |acc, v| acc.and_then(|a| a.min(v))) + }) }); impl_agg_entity_op!(MaxNodeOp, MaxEdgeOp, |vals| { - aggregate_values(vals, |pi| pi.max()) + aggregate_values(vals, &|pi| { + let mut it = pi; + let first = it.next()?; + it.fold(Some(first), |acc, v| acc.and_then(|a| a.max(v))) + }) }); impl_agg_entity_op!(FirstNodeOp, FirstEdgeOp, |vals| { - aggregate_values(vals, |pi| pi.next()) + aggregate_values(vals, &|mut pi| pi.next()) }); impl_agg_entity_op!(LastNodeOp, LastEdgeOp, |vals| { - aggregate_values(vals, |pi| pi.last()) + aggregate_values(vals, &|pi| pi.last()) }); impl_agg_entity_op!(LenNodeOp, LenEdgeOp, |vals| { - aggregate_values(vals, |pi| Some(pi.count().into_prop())) + aggregate_values(vals, &|pi| Some(pi.count().into_prop())) }); impl_agg_entity_op!(AnyNodeOp, AnyEdgeOp, |vals| { - aggregate_values(vals, |pi| { + aggregate_values(vals, &|mut pi| { Some(Prop::Bool(pi.any(|r| r == Prop::Bool(true)))) }) }); impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { - aggregate_values(vals, |pi| { + aggregate_values(vals, &|mut pi| { Some(Prop::Bool(pi.all(|r| r == Prop::Bool(true)))) }) }); @@ -311,7 +320,7 @@ impl<'g> NodeOp for ListAwareCmpNodeOp<'g> { let lv = self.left.apply(storage, node); let rhs = self.right.apply(storage, node)?; let op = &self.op; - aggregate_values(lv, |pi| { + aggregate_values(lv, &|pi| { let bools: Vec = pi .map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))) .collect(); @@ -334,7 +343,7 @@ impl<'g> NodeOp for ListAwareStringNodeOp<'g> { let lv = self.left.apply(storage, node); let rhs = self.right.apply(storage, node); let op = &self.op; - aggregate_values(lv, |pi| { + aggregate_values(lv, &|pi| { let bools: Vec = pi .map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))) .collect(); @@ -357,7 +366,7 @@ impl<'g> NodeOp for ListAwareSetNodeOp<'g> { let vals = self.inner.apply(storage, node); let values = &self.values; let op = &self.op; - aggregate_values(vals, |pi| { + aggregate_values(vals, &|pi| { let bools: Vec = pi .map(|v| Prop::Bool(match op { SetOp::IsIn => values.iter().any(|x| x == &v), @@ -453,8 +462,8 @@ impl<'g> NodeOp for PropValueSetNodeOp<'g> { /// 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 }` +/// e.g. `NodeFilter.property("age").gt(30i64)` compiles to: +/// `BinaryCmpNodeOp { left: NodePropOp(prop_id=3), right: Const(Some(I64(30))), op: Gt }` #[derive(Clone)] pub struct BinaryCmpNodeOp<'g, T: Comparable> { pub(crate) left: Arc + 'g>, @@ -482,8 +491,8 @@ impl<'g, T: Comparable + Clone + Send + Sync + 'static> NodeOp for BinaryCmpNode /// 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 }` +/// e.g. `NodeFilter.name().starts_with("Al")` compiles to: +/// `StringNodeOp { left: Name.map(...), right: Const(Some(Str("Al"))), op: StartsWith }` #[derive(Clone)] pub struct StringNodeOp<'g, T: StringComparable> { pub(crate) left: Arc + 'g>, @@ -509,7 +518,7 @@ impl<'g, T: StringComparable> NodeOp for StringNodeOp<'g, T> { /// Internal op produced by [`UnaryNodeFilter::create_node_filter`]. /// -/// e.g. `NodeFilter::property("age").is_some()` compiles to: +/// 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> { @@ -537,10 +546,10 @@ impl<'g, I: Clone + Send + Sync + 'static> NodeOp for UnaryNodeOp<'g, I> { // SetNodeOp<'g, T> — evaluates is_in / is_not_in // ───────────────────────────────────────────────────────────────────────────── -/// Internal op produced by [`SetNodeFilter::create_node_filter`]. +/// Internal op for hash-set membership on typed values. /// -/// e.g. `NodeFilter::node_type().is_in(["Person", "Account"])` compiles to: -/// `SetNodeOp { inner: TypeOp, op: IsIn, values: {"Person", "Account"} }` +/// e.g. `NodeFilter.node_type().is_in(["Person", "Account"])` compiles to: +/// `SetNodeOp { inner: Type.map(...), op: IsIn, values: {"Person", "Account"} }` #[derive(Clone)] pub struct SetNodeOp<'g, I: Eq + Hash + Clone + Send + Sync + 'static> { pub(crate) inner: Arc> + 'g>, diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index 46753845e7..b8771706d5 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -1,15 +1,16 @@ use crate::db::graph::views::filter::model::Op; -use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use raphtory_api::core::entities::properties::prop::{Prop, PropArray, PropType}; use std::borrow::Borrow; +use super::PropertyFilter; enum ValueType { Seq(Vec), Scalar(Option), } -pub fn aggregate_values>( +pub fn aggregate_values( vals: Option, - op: impl Fn(T) -> Option, + op: &dyn Fn(Box>) -> Option, ) -> Option { match vals? { Prop::List(x) => match x.dtype() { @@ -21,7 +22,10 @@ pub fn aggregate_values>( .collect(); Some(Prop::List(s)) } - _ => op(x.iter()), + _ => { + let items: Vec = x.iter().collect(); + op(Box::new(items.into_iter())) + } }, _ => None, } @@ -223,339 +227,335 @@ pub fn reduce_float>( } } -// match op { -// Op::Len => Some(Prop::U64(vals.count() as u64)), -// Op::Sum | Op::Avg | Op::Min | Op::Max => { -// if vals.peek().is_none() { -// return None; -// } -// let inner = vals.peek().unwrap().borrow().dtype(); -// match inner { -// PropType::U8 => reduce_unsigned(vals, |x| Prop::U8(x as u8), op), -// PropType::U16 => reduce_unsigned(vals, |x| Prop::U16(x as u16), op), -// PropType::U32 => reduce_unsigned(vals, |x| Prop::U32(x as u32), op), -// PropType::U64 => reduce_unsigned(vals, |x| Prop::U64(x), op), -// -// PropType::I32 => reduce_signed(vals, |x| Prop::I32(x as i32), op), -// PropType::I64 => reduce_signed(vals, |x| Prop::I64(x), op), -// -// PropType::F32 => reduce_float(vals, |x| Prop::F32(x as f32), op), -// PropType::F64 => reduce_float(vals, |x| Prop::F64(x), op), -// _ => None, -// } -// } -// Op::First | Op::Last | Op::Any | Op::All => unreachable!(), -// } - -// pub fn apply_agg_to_prop(p: &Prop, op: Op) -> Option { -// match (op, p) { -// (Op::Len, Prop::List(inner)) => Some(Prop::U64(inner.len() as u64)), -// (Op::Sum, Prop::List(inner)) -// | (Op::Avg, Prop::List(inner)) -// | (Op::Min, Prop::List(inner)) -// | (Op::Max, Prop::List(inner)) => aggregate_values(inner.iter(), op), -// -// (Op::Len, _) => Some(Prop::U64(1)), -// -// (Op::Sum, Prop::U8(x)) => Some(Prop::U8(*x)), -// (Op::Sum, Prop::U16(x)) => Some(Prop::U16(*x)), -// (Op::Sum, Prop::U32(x)) => Some(Prop::U32(*x)), -// (Op::Sum, Prop::U64(x)) => Some(Prop::U64(*x)), -// (Op::Sum, Prop::I32(x)) => Some(Prop::I32(*x)), -// (Op::Sum, Prop::I64(x)) => Some(Prop::I64(*x)), -// (Op::Sum, Prop::F32(x)) => { -// if x.is_finite() { -// Some(Prop::F32(*x)) -// } else { -// None -// } -// } -// (Op::Sum, Prop::F64(x)) => { -// if x.is_finite() { -// Some(Prop::F64(*x)) -// } else { -// None -// } -// } -// -// (Op::Avg, Prop::U8(x)) => Some(Prop::F64(*x as f64)), -// (Op::Avg, Prop::U16(x)) => Some(Prop::F64(*x as f64)), -// (Op::Avg, Prop::U32(x)) => Some(Prop::F64(*x as f64)), -// (Op::Avg, Prop::U64(x)) => Some(Prop::F64(*x as f64)), -// (Op::Avg, Prop::I32(x)) => Some(Prop::F64(*x as f64)), -// (Op::Avg, Prop::I64(x)) => Some(Prop::F64(*x as f64)), -// (Op::Avg, Prop::F32(x)) => { -// if x.is_finite() { -// Some(Prop::F32(*x)) -// } else { -// None -// } -// } -// (Op::Avg, Prop::F64(x)) => { -// if x.is_finite() { -// Some(Prop::F64(*x)) -// } else { -// None -// } -// } -// -// (Op::Min, Prop::U8(x)) => Some(Prop::U8(*x)), -// (Op::Min, Prop::U16(x)) => Some(Prop::U16(*x)), -// (Op::Min, Prop::U32(x)) => Some(Prop::U32(*x)), -// (Op::Min, Prop::U64(x)) => Some(Prop::U64(*x)), -// (Op::Min, Prop::I32(x)) => Some(Prop::I32(*x)), -// (Op::Min, Prop::I64(x)) => Some(Prop::I64(*x)), -// (Op::Min, Prop::F32(x)) => { -// if x.is_finite() { -// Some(Prop::F32(*x)) -// } else { -// None -// } -// } -// (Op::Min, Prop::F64(x)) => { -// if x.is_finite() { -// Some(Prop::F64(*x)) -// } else { -// None -// } -// } -// -// (Op::Max, Prop::U8(x)) => Some(Prop::U8(*x)), -// (Op::Max, Prop::U16(x)) => Some(Prop::U16(*x)), -// (Op::Max, Prop::U32(x)) => Some(Prop::U32(*x)), -// (Op::Max, Prop::U64(x)) => Some(Prop::U64(*x)), -// (Op::Max, Prop::I32(x)) => Some(Prop::I32(*x)), -// (Op::Max, Prop::I64(x)) => Some(Prop::I64(*x)), -// (Op::Max, Prop::F32(x)) => { -// if x.is_finite() { -// Some(Prop::F32(*x)) -// } else { -// None -// } -// } -// (Op::Max, Prop::F64(x)) => { -// if x.is_finite() { -// Some(Prop::F64(*x)) -// } else { -// None -// } -// } -// -// (Op::Sum, _) | (Op::Avg, _) | (Op::Min, _) | (Op::Max, _) => None, -// -// _ => None, -// } -// } - -// impl PropertyFilter { -// fn reduce_qualifiers_rec( -// &self, -// quals: &[Op], -// v: &Prop, -// predicate: &dyn Fn(&Prop) -> bool, -// ) -> bool { -// if quals.is_empty() { -// return predicate(v); -// } -// let (q, rest) = (quals[0], &quals[1..]); -// -// if let Prop::List(inner) = v { -// let mut elems = inner.iter().peekable(); -// let check = |e: &Prop| { -// if rest.is_empty() { -// predicate(e) -// } else { -// self.reduce_qualifiers_rec(rest, e, predicate) -// } -// }; -// return match q { -// Op::Any => elems.any(|p| check(&p)), -// Op::All => !elems.peek().is_none() && elems.all(|p| check(&p)), -// _ => unreachable!(), -// }; -// } -// -// if rest.is_empty() { -// return match q { -// Op::Any | Op::All => predicate(v), -// _ => unreachable!(), -// }; -// } -// self.reduce_qualifiers_rec(rest, v, predicate) -// } -// -// fn eval_ops(&self, mut state: ValueType) -> (Option, Option>, Vec, bool) { -// let mut qualifiers: Vec = Vec::new(); -// let mut seq_is_temporal = matches!(state, ValueType::Seq(..)); -// let mut seen_qual_before_agg = false; -// -// let per_step_map = |vals: Vec, op: Op| -> Vec { -// vals.into_iter() -// .filter_map(|p| apply_agg_to_prop(&p, op)) -// .collect() -// }; -// -// // Aggregate OVER TIME (when no prior qualifier). -// let reduce_over_seq = |vs: Vec, op: Op| -> Option { -// match op { -// Op::Len => Some(Prop::U64(vs.len() as u64)), -// Op::Sum | Op::Avg | Op::Min | Op::Max => { -// if vs.is_empty() || matches!(vs.first(), Some(Prop::List(_))) { -// return None; -// } -// aggregate_values(&vs, op) -// } -// _ => None, -// } -// }; -// -// for op in &self.ops { -// match *op { -// Op::First | Op::Last => { -// state = match state { -// ValueType::Seq(vs) => { -// seq_is_temporal = false; -// let v = if matches!(*op, Op::First) { -// vs.first() -// } else { -// vs.last() -// }; -// ValueType::Scalar(v.cloned()) -// } -// s @ ValueType::Scalar(_) => s, -// }; -// } -// -// Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => { -// state = match state { -// ValueType::Seq(vs) if seen_qual_before_agg => { -// ValueType::Seq(per_step_map(vs, *op)) -// } -// ValueType::Seq(vs) => { -// seq_is_temporal = false; -// ValueType::Scalar(reduce_over_seq(vs, *op)) -// } -// ValueType::Scalar(Some(Prop::List(inner))) => { -// ValueType::Scalar(aggregate_values(inner.iter(), *op)) -// } -// ValueType::Scalar(Some(p)) => ValueType::Scalar(apply_agg_to_prop(&p, *op)), -// ValueType::Scalar(None) => ValueType::Scalar(None), -// }; -// } -// -// Op::Any | Op::All => { -// qualifiers.push(*op); -// seen_qual_before_agg = true; -// state = match state { -// ValueType::Seq(vs) => ValueType::Seq(vs), // still temporal -// ValueType::Scalar(Some(Prop::List(inner))) => { -// seq_is_temporal = false; -// ValueType::Seq(vec![Prop::List(inner)]) -// } -// ValueType::Scalar(Some(p)) => { -// seq_is_temporal = false; -// ValueType::Seq(vec![p]) -// } -// ValueType::Scalar(None) => { -// seq_is_temporal = false; -// ValueType::Seq(vec![]) -// } -// }; -// } -// } -// } -// -// match state { -// ValueType::Scalar(v) => (v, None, qualifiers, seq_is_temporal), -// ValueType::Seq(vs) => (None, Some(vs), qualifiers, seq_is_temporal), -// } -// } -// -// fn apply_eval( -// &self, -// reduced: Option, -// maybe_seq: Option>, -// qualifiers: Vec, -// seq_is_temporal: bool, -// ) -> bool { -// if let Some(value) = reduced { -// return self -// .operator -// .apply_to_property(&self.prop_value, Some(&value)); -// } -// -// if !qualifiers.is_empty() { -// let (temporal_q_opt, elem_quals) = if seq_is_temporal { -// (Some(qualifiers[0]), &qualifiers[1..]) -// } else { -// (None, &qualifiers[..]) -// }; -// let pred = |p: &Prop| self.operator.apply_to_property(&self.prop_value, Some(p)); -// let Some(seq) = maybe_seq else { return false }; -// -// if let Some(tq) = temporal_q_opt { -// let mut saw = false; -// match tq { -// Op::All => { -// for p in &seq { -// saw = true; -// let ok = if elem_quals.is_empty() { -// pred(p) -// } else { -// self.reduce_qualifiers_rec(elem_quals, p, &pred) -// }; -// if !ok { -// return false; -// } -// } -// return saw; -// } -// Op::Any => { -// for p in &seq { -// let ok = if elem_quals.is_empty() { -// pred(p) -// } else { -// self.reduce_qualifiers_rec(elem_quals, p, &pred) -// }; -// if ok { -// return true; -// } -// } -// return false; -// } -// _ => unreachable!(), -// } -// } else { -// for p in &seq { -// let ok = if elem_quals.is_empty() { -// pred(p) -// } else { -// self.reduce_qualifiers_rec(elem_quals, p, &pred) -// }; -// if ok { -// return true; -// } -// } -// return false; -// } -// } -// -// if let Some(seq) = maybe_seq { -// let full = Prop::List(seq.into()); -// self.operator -// .apply_to_property(&self.prop_value, Some(&full)) -// } else { -// self.operator.apply_to_property(&self.prop_value, None) -// } -// } -// -// pub fn eval_scalar_and_apply(&self, prop: Option) -> bool { -// let (r, s, q, is_t) = self.eval_ops(ValueType::Scalar(prop)); -// self.apply_eval(r, s, q, is_t) -// } -// -// pub fn eval_temporal_and_apply(&self, props: Vec) -> bool { -// let (r, s, q, is_t) = self.eval_ops(ValueType::Seq(props)); -// self.apply_eval(r, s, q, is_t) -// } -// } +/// Reduce a flat `Vec` using `op`. All elements must have the same numeric type. +/// Returns `None` if the vector is empty, the type is non-numeric, or the op is not +/// a numeric aggregator (Sum/Avg/Min/Max). +pub fn reduce_seq(vs: Vec, op: Op) -> Option { + if vs.is_empty() || matches!(vs.first(), Some(Prop::List(_))) { + return None; + } + let inner_dtype = vs.first()?.dtype(); + match inner_dtype { + PropType::U8 => reduce_unsigned(vs, |x| Prop::U8(x as u8), op), + PropType::U16 => reduce_unsigned(vs, |x| Prop::U16(x as u16), op), + PropType::U32 => reduce_unsigned(vs, |x| Prop::U32(x as u32), op), + PropType::U64 => reduce_unsigned(vs, |x| Prop::U64(x), op), + PropType::I32 => reduce_signed(vs, |x| Prop::I32(x as i32), op), + PropType::I64 => reduce_signed(vs, |x| Prop::I64(x), op), + PropType::F32 => reduce_float(vs, |x| Prop::F32(x as f32), op), + PropType::F64 => reduce_float(vs, |x| Prop::F64(x), op), + _ => None, + } +} + +pub fn apply_agg_to_prop(p: &Prop, op: Op) -> Option { + match (op, p) { + (Op::Len, Prop::List(inner)) => Some(Prop::U64(inner.len() as u64)), + (Op::Sum, Prop::List(inner)) + | (Op::Avg, Prop::List(inner)) + | (Op::Min, Prop::List(inner)) + | (Op::Max, Prop::List(inner)) => { + let vs: Vec = inner.iter().collect(); + reduce_seq(vs, op) + } + + (Op::Len, _) => Some(Prop::U64(1)), + + (Op::Sum, Prop::U8(x)) => Some(Prop::U8(*x)), + (Op::Sum, Prop::U16(x)) => Some(Prop::U16(*x)), + (Op::Sum, Prop::U32(x)) => Some(Prop::U32(*x)), + (Op::Sum, Prop::U64(x)) => Some(Prop::U64(*x)), + (Op::Sum, Prop::I32(x)) => Some(Prop::I32(*x)), + (Op::Sum, Prop::I64(x)) => Some(Prop::I64(*x)), + (Op::Sum, Prop::F32(x)) => { + if x.is_finite() { + Some(Prop::F32(*x)) + } else { + None + } + } + (Op::Sum, Prop::F64(x)) => { + if x.is_finite() { + Some(Prop::F64(*x)) + } else { + None + } + } + + (Op::Avg, Prop::U8(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::U16(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::U32(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::U64(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::I32(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::I64(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::F32(x)) => { + if x.is_finite() { + Some(Prop::F32(*x)) + } else { + None + } + } + (Op::Avg, Prop::F64(x)) => { + if x.is_finite() { + Some(Prop::F64(*x)) + } else { + None + } + } + + (Op::Min, Prop::U8(x)) => Some(Prop::U8(*x)), + (Op::Min, Prop::U16(x)) => Some(Prop::U16(*x)), + (Op::Min, Prop::U32(x)) => Some(Prop::U32(*x)), + (Op::Min, Prop::U64(x)) => Some(Prop::U64(*x)), + (Op::Min, Prop::I32(x)) => Some(Prop::I32(*x)), + (Op::Min, Prop::I64(x)) => Some(Prop::I64(*x)), + (Op::Min, Prop::F32(x)) => { + if x.is_finite() { + Some(Prop::F32(*x)) + } else { + None + } + } + (Op::Min, Prop::F64(x)) => { + if x.is_finite() { + Some(Prop::F64(*x)) + } else { + None + } + } + + (Op::Max, Prop::U8(x)) => Some(Prop::U8(*x)), + (Op::Max, Prop::U16(x)) => Some(Prop::U16(*x)), + (Op::Max, Prop::U32(x)) => Some(Prop::U32(*x)), + (Op::Max, Prop::U64(x)) => Some(Prop::U64(*x)), + (Op::Max, Prop::I32(x)) => Some(Prop::I32(*x)), + (Op::Max, Prop::I64(x)) => Some(Prop::I64(*x)), + (Op::Max, Prop::F32(x)) => { + if x.is_finite() { + Some(Prop::F32(*x)) + } else { + None + } + } + (Op::Max, Prop::F64(x)) => { + if x.is_finite() { + Some(Prop::F64(*x)) + } else { + None + } + } + + (Op::Sum, _) | (Op::Avg, _) | (Op::Min, _) | (Op::Max, _) => None, + + _ => None, + } +} + +impl PropertyFilter { + fn reduce_qualifiers_rec( + &self, + quals: &[Op], + v: &Prop, + predicate: &dyn Fn(&Prop) -> bool, + ) -> bool { + if quals.is_empty() { + return predicate(v); + } + let (q, rest) = (quals[0], &quals[1..]); + + if let Prop::List(inner) = v { + let mut elems = inner.iter().peekable(); + let check = |e: &Prop| { + if rest.is_empty() { + predicate(e) + } else { + self.reduce_qualifiers_rec(rest, e, predicate) + } + }; + return match q { + Op::Any => elems.any(|p| check(&p)), + Op::All => !elems.peek().is_none() && elems.all(|p| check(&p)), + _ => unreachable!(), + }; + } + + if rest.is_empty() { + return match q { + Op::Any | Op::All => predicate(v), + _ => unreachable!(), + }; + } + self.reduce_qualifiers_rec(rest, v, predicate) + } + + fn eval_ops(&self, mut state: ValueType) -> (Option, Option>, Vec, bool) { + let mut qualifiers: Vec = Vec::new(); + let mut seq_is_temporal = matches!(state, ValueType::Seq(..)); + let mut seen_qual_before_agg = false; + + let per_step_map = |vals: Vec, op: Op| -> Vec { + vals.into_iter() + .filter_map(|p| apply_agg_to_prop(&p, op)) + .collect() + }; + + // Aggregate OVER TIME (when no prior qualifier). + let reduce_over_seq = |vs: Vec, op: Op| -> Option { + match op { + Op::Len => Some(Prop::U64(vs.len() as u64)), + Op::Sum | Op::Avg | Op::Min | Op::Max => reduce_seq(vs, op), + _ => None, + } + }; + + for op in &self.ops { + match *op { + Op::First | Op::Last => { + state = match state { + ValueType::Seq(vs) => { + seq_is_temporal = false; + let v = if matches!(*op, Op::First) { + vs.first() + } else { + vs.last() + }; + ValueType::Scalar(v.cloned()) + } + s @ ValueType::Scalar(_) => s, + }; + } + + Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => { + state = match state { + ValueType::Seq(vs) if seen_qual_before_agg => { + ValueType::Seq(per_step_map(vs, *op)) + } + ValueType::Seq(vs) => { + seq_is_temporal = false; + ValueType::Scalar(reduce_over_seq(vs, *op)) + } + ValueType::Scalar(Some(Prop::List(inner))) => { + let vs: Vec = inner.iter().collect(); + ValueType::Scalar(reduce_over_seq(vs, *op)) + } + ValueType::Scalar(Some(p)) => ValueType::Scalar(apply_agg_to_prop(&p, *op)), + ValueType::Scalar(None) => ValueType::Scalar(None), + }; + } + + Op::Any | Op::All => { + qualifiers.push(*op); + seen_qual_before_agg = true; + state = match state { + ValueType::Seq(vs) => ValueType::Seq(vs), // still temporal + ValueType::Scalar(Some(Prop::List(inner))) => { + seq_is_temporal = false; + ValueType::Seq(vec![Prop::List(inner)]) + } + ValueType::Scalar(Some(p)) => { + seq_is_temporal = false; + ValueType::Seq(vec![p]) + } + ValueType::Scalar(None) => { + seq_is_temporal = false; + ValueType::Seq(vec![]) + } + }; + } + } + } + + match state { + ValueType::Scalar(v) => (v, None, qualifiers, seq_is_temporal), + ValueType::Seq(vs) => (None, Some(vs), qualifiers, seq_is_temporal), + } + } + + fn apply_eval( + &self, + reduced: Option, + maybe_seq: Option>, + qualifiers: Vec, + seq_is_temporal: bool, + ) -> bool { + if let Some(value) = reduced { + return self + .operator + .apply_to_property(&self.prop_value, Some(&value)); + } + + if !qualifiers.is_empty() { + let (temporal_q_opt, elem_quals) = if seq_is_temporal { + (Some(qualifiers[0]), &qualifiers[1..]) + } else { + (None, &qualifiers[..]) + }; + let pred = |p: &Prop| self.operator.apply_to_property(&self.prop_value, Some(p)); + let Some(seq) = maybe_seq else { return false }; + + if let Some(tq) = temporal_q_opt { + let mut saw = false; + match tq { + Op::All => { + for p in &seq { + saw = true; + let ok = if elem_quals.is_empty() { + pred(p) + } else { + self.reduce_qualifiers_rec(elem_quals, p, &pred) + }; + if !ok { + return false; + } + } + return saw; + } + Op::Any => { + for p in &seq { + let ok = if elem_quals.is_empty() { + pred(p) + } else { + self.reduce_qualifiers_rec(elem_quals, p, &pred) + }; + if ok { + return true; + } + } + return false; + } + _ => unreachable!(), + } + } else { + for p in &seq { + let ok = if elem_quals.is_empty() { + pred(p) + } else { + self.reduce_qualifiers_rec(elem_quals, p, &pred) + }; + if ok { + return true; + } + } + return false; + } + } + + if let Some(seq) = maybe_seq { + let full = Prop::List(PropArray::from(seq)); + self.operator + .apply_to_property(&self.prop_value, Some(&full)) + } else { + self.operator.apply_to_property(&self.prop_value, None) + } + } + + pub fn eval_scalar_and_apply(&self, prop: Option) -> bool { + let (r, s, q, is_t) = self.eval_ops(ValueType::Scalar(prop)); + self.apply_eval(r, s, q, is_t) + } + + pub fn eval_temporal_and_apply(&self, props: Vec) -> bool { + let (r, s, q, is_t) = self.eval_ops(ValueType::Seq(props)); + self.apply_eval(r, s, q, is_t) + } +} diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 833389f3c4..c65b1de285 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -147,7 +147,7 @@ pub mod prelude { }; pub use crate::db::graph::views::filter::model::{ - filter::Filter, property_filter::PropertyFilter, NodeAggregated, NodeQuantified, + filter::Filter, property_filter::PropertyFilter, NodeExprFilterOps, NodePropertyExprOps, NodeTemporalPropOps, EdgeExprFilterOps, EdgePropertyExprOps, EdgeTemporalPropOps, }; From 89b8c3802bf26cbc4112d45983866327253cea0c Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:27:06 +0100 Subject: [PATCH 029/155] fix ops --- .../views/filter/model/node_expr/filters.rs | 7 +- .../graph/views/filter/model/node_expr/ops.rs | 131 +++++++++++++----- .../filter/model/property_filter/evaluate.rs | 2 +- 3 files changed, 105 insertions(+), 35 deletions(-) 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 489f781c5a..86c1dfef80 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 @@ -136,6 +136,7 @@ where } /// Reject ordering operators on boolean properties. +//. TODO: Also check if both the types are comparable. fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), GraphError> { if *prop_type != PropType::Empty && matches!( @@ -195,7 +196,7 @@ where ) -> 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())?; + validate_binary_op(&self.op, &left.prop_type(), &right.prop_type())?; Ok(Arc::new(BinaryCmpNodeOp { left, right, @@ -245,7 +246,7 @@ where { pub expr: E, pub op: UnaryOp, - pub(crate) _phantom: PhantomData, + pub(crate) _phantom: PhantomData, // TODO: Not needed anymore } impl Clone for UnaryNodeFilter @@ -584,12 +585,14 @@ impl NodeProp /// /// ```rust,ignore /// NodeFilter.degree().gt(2usize) +/// NodeFilter.degree().sum() // TODO: Throw an error /// NodeFilter.out_degree().gt(NodeFilter.in_degree()) /// NodeFilter.property("age").gt(30i64) /// NodeFilter.property("score").temporal().gt(10i64).any() /// ``` pub trait NodeExprFilterOps: NodeExpr + Sized { fn gt(self, rhs: R) -> BinaryCmpNodeFilter { + // TODO: validate ops BinaryCmpNodeFilter::new(self, BinaryOp::Gt, rhs) } 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 index e11fd6f50d..d26de555d9 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -37,7 +37,6 @@ //! ``` use super::EdgeOp; -use std::borrow::Borrow; use crate::db::graph::views::filter::model::property_filter::evaluate::{ scan_f64_sum_count, scan_i64_sum, scan_u64_sum, }; @@ -56,13 +55,12 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::core::entities::edges::edge_ref::EdgeRef; -use raphtory_api::core::{ - entities::{ - properties::prop::{IntoProp, Prop, PropArray, PropType}, - VID, - }, +use raphtory_api::core::entities::{ + properties::prop::{IntoProp, Prop, PropArray, PropType}, + VID, }; use raphtory_storage::graph::graph::GraphStorage; +use std::borrow::Borrow; use std::{collections::HashSet, hash::Hash, sync::Arc}; // ───────────────────────────────────────────────────────────────────────────── // NodePropOp — latest property value by pre-resolved column ID @@ -261,35 +259,35 @@ impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { }) }); impl_agg_entity_op!(MinNodeOp, MinEdgeOp, |vals| { - aggregate_values(vals, &|pi| { + aggregate_values(vals, |pi| { let mut it = pi; let first = it.next()?; it.fold(Some(first), |acc, v| acc.and_then(|a| a.min(v))) }) }); impl_agg_entity_op!(MaxNodeOp, MaxEdgeOp, |vals| { - aggregate_values(vals, &|pi| { + aggregate_values(vals, |pi| { let mut it = pi; let first = it.next()?; it.fold(Some(first), |acc, v| acc.and_then(|a| a.max(v))) }) }); impl_agg_entity_op!(FirstNodeOp, FirstEdgeOp, |vals| { - aggregate_values(vals, &|mut pi| pi.next()) + aggregate_values(vals, |mut pi| pi.next()) }); impl_agg_entity_op!(LastNodeOp, LastEdgeOp, |vals| { - aggregate_values(vals, &|pi| pi.last()) + aggregate_values(vals, |pi| pi.last()) }); impl_agg_entity_op!(LenNodeOp, LenEdgeOp, |vals| { - aggregate_values(vals, &|pi| Some(pi.count().into_prop())) + aggregate_values(vals, |pi| Some(pi.count().into_prop())) }); impl_agg_entity_op!(AnyNodeOp, AnyEdgeOp, |vals| { - aggregate_values(vals, &|mut pi| { + aggregate_values(vals, |mut pi| { Some(Prop::Bool(pi.any(|r| r == Prop::Bool(true)))) }) }); impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { - aggregate_values(vals, &|mut pi| { + aggregate_values(vals, |mut pi| { Some(Prop::Bool(pi.all(|r| r == Prop::Bool(true)))) }) }); @@ -318,13 +316,20 @@ impl<'g> NodeOp for ListAwareCmpNodeOp<'g> { fn apply(&self, storage: &GraphStorage, node: VID) -> Option { let lv = self.left.apply(storage, node); - let rhs = self.right.apply(storage, node)?; + let rhs = self.right.apply(storage, node); let op = &self.op; - aggregate_values(lv, &|pi| { - let bools: Vec = pi - .map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))) - .collect(); - if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + // aggregate_values(lv, &|pi| { + // let bools: Vec = pi + // .map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))) + // .collect(); + // if bools.is_empty() { + // None + // } else { + // Some(Prop::List(PropArray::from(bools))) + // } + // }) + broadcast_binary(lv, rhs, |lv, rhs| { + Some(Prop::Bool(Prop::binary_cmp(op, &lv?, &rhs?))) }) } } @@ -343,15 +348,64 @@ impl<'g> NodeOp for ListAwareStringNodeOp<'g> { let lv = self.left.apply(storage, node); let rhs = self.right.apply(storage, node); let op = &self.op; - aggregate_values(lv, &|pi| { - let bools: Vec = pi - .map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))) - .collect(); - if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + // aggregate_values(lv, &|pi| { + // let bools: Vec = pi + // .map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))) + // .collect(); + // if bools.is_empty() { + // None + // } else { + // Some(Prop::List(PropArray::from(bools))) + // } + // }) + broadcast_binary(lv, rhs, |lv, rhs| { + Some(Prop::Bool(Option::::string_cmp(op, &lv, &rhs))) }) } } +// [1,2,3] == [1,2,3] +// [4,5,6] > [1,2,3] + +pub fn broadcast_unary(v: Option, op: impl Fn(Option) -> Option) -> Option { + match v { + Some(Prop::List(v)) => Some(Prop::List(v.iter_all().map(|l| op(l)).flatten().collect())), + _ => op(v), + } +} + +pub fn broadcast_binary( + l: Option, + r: Option, + op: impl Fn(Option, Option) -> Option, +) -> Option { + let l = l?; + let r = r?; + + match (l, r) { + (Prop::List(l), Prop::List(r)) => { + if l.len() == r.len() { + Some(Prop::List( + l.iter_all() + .zip(r.iter_all()) + .map(|(l, r)| op(l, r)) + .flatten() + .collect(), + )) + } else { + None + } + } + (Prop::List(l), r) => Some(Prop::List( + l.iter_all().map(|(l)| op(l, Some(r))).flatten().collect(), + )), + (l, Prop::List(r)) => Some(Prop::List( + r.iter_all().map(|(r)| op(Some(l), r)).flatten().collect(), + )), + (l, r) => op(Some(l), Some(r)), + } +} + #[derive(Clone)] pub(crate) struct ListAwareSetNodeOp<'g> { pub(crate) inner: Arc> + 'g>, @@ -366,14 +420,27 @@ impl<'g> NodeOp for ListAwareSetNodeOp<'g> { let vals = self.inner.apply(storage, node); let values = &self.values; let op = &self.op; - aggregate_values(vals, &|pi| { - let bools: Vec = pi - .map(|v| Prop::Bool(match op { - SetOp::IsIn => values.iter().any(|x| x == &v), - SetOp::IsNotIn => values.iter().all(|x| x != &v), - })) - .collect(); - if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + // aggregate_values(vals, &|pi| { + // let bools: Vec = pi + // .map(|v| { + // Prop::Bool(match op { + // SetOp::IsIn => values.iter().any(|x| x == &v), + // SetOp::IsNotIn => values.iter().all(|x| x != &v), + // }) + // }) + // .collect(); + // if bools.is_empty() { + // None + // } else { + // Some(Prop::List(PropArray::from(bools))) + // } + // }) + broadcast_unary(vals, |v, rhs| { + let v = v?; + Some(Prop::Bool(match op { + SetOp::IsIn => values.iter().any(|x| x == &v), + SetOp::IsNotIn => values.iter().all(|x| x != &v), + })) }) } } diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index b8771706d5..05540eae1d 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -10,7 +10,7 @@ enum ValueType { pub fn aggregate_values( vals: Option, - op: &dyn Fn(Box>) -> Option, + op: impl Fn(Box>) -> Option, ) -> Option { match vals? { Prop::List(x) => match x.dtype() { From 998e5266bd84abc6140bfc157d891f7500c9434e Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:55:33 +0100 Subject: [PATCH 030/155] experimenting --- .../views/filter/model/edge_expr/filters.rs | 143 +---------- .../graph/views/filter/model/edge_expr/mod.rs | 126 +++++----- .../src/db/graph/views/filter/model/mod.rs | 10 +- .../views/filter/model/node_expr/filters.rs | 238 ++++++------------ .../graph/views/filter/model/node_expr/mod.rs | 54 ++-- .../graph/views/filter/model/node_expr/ops.rs | 2 +- .../views/filter/model/node_expr/tests.rs | 2 +- raphtory/src/lib.rs | 2 +- 8 files changed, 184 insertions(+), 393 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index f8c2fa8a9e..70d29939ad 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -29,7 +29,7 @@ use crate::{ }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::{marker::PhantomData, sync::Arc}; - +pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpFilter, StringFilter, UnaryFilter}; // ───────────────────────────────────────────────────────────────────────────── // validate helpers // ───────────────────────────────────────────────────────────────────────────── @@ -50,49 +50,7 @@ fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), GraphEr // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpEdgeFilter // ───────────────────────────────────────────────────────────────────────────── - -pub struct BinaryCmpEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ - pub left: L, - pub op: BinaryOp, - pub right: R, -} - -impl BinaryCmpEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ - pub fn new(left: L, op: BinaryOp, right: R) -> Self { - Self { left, op, right } - } -} - -impl Clone for BinaryCmpEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ - fn clone(&self) -> Self { - Self { - left: self.left.clone(), - op: self.op, - right: self.right.clone(), - } - } -} - -impl EntityExpr for BinaryCmpEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ -} - -impl EdgeExpr for BinaryCmpEdgeFilter +impl EdgeExpr for BinaryCmpFilter where L: EdgeExpr, R: EdgeExpr, @@ -107,14 +65,7 @@ where } } -impl ComposableFilter for BinaryCmpEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ -} - -impl TryAsCompositeFilter for BinaryCmpEdgeFilter +impl TryAsCompositeFilter for BinaryCmpFilter where L: EdgeExpr, R: EdgeExpr, @@ -134,7 +85,7 @@ where } } -impl CreateFilter for BinaryCmpEdgeFilter +impl CreateFilter for BinaryCmpFilter where L: EdgeExpr, R: EdgeExpr, @@ -174,41 +125,9 @@ where // UnaryEdgeFilter // ───────────────────────────────────────────────────────────────────────────── -pub struct UnaryEdgeFilter -where - E: EdgeExpr, - I: Clone + Send + Sync + 'static, -{ - pub expr: E, - pub op: UnaryOp, - pub(crate) _phantom: PhantomData, -} - -impl Clone for UnaryEdgeFilter -where - E: EdgeExpr, - I: Clone + Send + Sync + 'static, -{ - fn clone(&self) -> Self { - Self { - expr: self.expr.clone(), - op: self.op, - _phantom: PhantomData, - } - } -} - -impl ComposableFilter for UnaryEdgeFilter +impl TryAsCompositeFilter for UnaryFilter where E: EdgeExpr, - I: Clone + Send + Sync + 'static, -{ -} - -impl TryAsCompositeFilter for UnaryEdgeFilter -where - E: EdgeExpr, - I: Clone + Send + Sync + 'static, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -225,10 +144,9 @@ where } } -impl CreateFilter for UnaryEdgeFilter +impl CreateFilter for UnaryFilter where E: EdgeExpr, - I: Clone + Send + Sync + 'static, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; @@ -275,44 +193,8 @@ fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { // StringEdgeFilter — string expression filter for edges // ───────────────────────────────────────────────────────────────────────────── -pub struct StringEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ - pub left: L, - pub op: StringOp, - pub right: R, -} - -impl StringEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ - pub fn new(left: L, op: StringOp, right: R) -> Self { - Self { left, op, right } - } -} - -impl Clone for StringEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ - fn clone(&self) -> Self { - Self { left: self.left.clone(), op: self.op, right: self.right.clone() } - } -} -impl EntityExpr for StringEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ -} - -impl EdgeExpr for StringEdgeFilter +impl EdgeExpr for StringFilter where L: EdgeExpr, R: EdgeExpr, @@ -327,14 +209,7 @@ where } } -impl ComposableFilter for StringEdgeFilter -where - L: EdgeExpr, - R: EdgeExpr, -{ -} - -impl TryAsCompositeFilter for StringEdgeFilter +impl TryAsCompositeFilter for StringFilter where L: EdgeExpr, R: EdgeExpr, @@ -352,7 +227,7 @@ where } } -impl CreateFilter for StringEdgeFilter +impl CreateFilter for StringFilter where L: EdgeExpr, R: EdgeExpr, diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index 49bd6679c3..c21aa4d1a8 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -8,7 +8,7 @@ //! ```text //! ┌─ Build phase (pure data, no graph) ──────────────────────┐ //! │ EdgeFilter.property("weight") ← EdgeExpr │ -//! │ .eq(5.0f64) ← BinaryCmpEdgeFilter │ +//! │ .eq(5.0f64) ← BinaryCmpFilter │ //! └──────────────────────────────────────────────────────────┘ //! │ create_edge_op(graph)? ← resolve name → prop_id //! ▼ @@ -44,7 +44,7 @@ pub mod ops; pub use exprs::*; pub use filters::*; -use filters::BinaryCmpEdgeFilter; +use filters::BinaryCmpFilter; use crate::db::graph::views::filter::model::{ FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr}; use crate::db::graph::views::filter::model::node_expr::{AllExpr, AnyExpr, AvgExpr}; pub use super::{Metadata, Property}; @@ -80,7 +80,7 @@ pub trait EdgeOp: Send + Sync { /// EdgeFilter.property("tag").temporal().sum().gt(100i64) /// EdgeFilter.property("label").temporal().into_expr().contains("foo").any() /// ``` -pub trait EdgeExpr: EntityExpr + Clone + Send + Sync + 'static { +pub(crate) trait EdgeExpr: EntityExpr + Clone + Send + Sync + 'static { fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -95,11 +95,11 @@ pub trait EdgeExpr: EntityExpr + Clone + Send + Sync + 'static { /// Presence and set-membership operators for `PropertyExpr` and `MetadataExpr` /// on the edge side. pub trait EdgePropertyExprOps: EdgeExpr + Sized { - fn is_some(self) -> UnaryEdgeFilter { - UnaryEdgeFilter { expr: self, op: UnaryOp::IsSome, _phantom: PhantomData } + fn is_some(self) -> UnaryFilter { + UnaryFilter { expr: self, op: UnaryOp::IsSome} } - fn is_none(self) -> UnaryEdgeFilter { - UnaryEdgeFilter { expr: self, op: UnaryOp::IsNone, _phantom: PhantomData } + fn is_none(self) -> UnaryFilter { + UnaryFilter { expr: self, op: UnaryOp::IsNone } } fn is_in( self, @@ -121,11 +121,11 @@ pub trait EdgePropertyExprOps: EdgeExpr + Sized { op: SetOp::IsNotIn, } } - fn is_true(self) -> BinaryCmpEdgeFilter { - BinaryCmpEdgeFilter::new(self, BinaryOp::Eq, Prop::Bool(true)) + fn is_true(self) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Eq, Prop::Bool(true)) } - fn is_false(self) -> BinaryCmpEdgeFilter { - BinaryCmpEdgeFilter::new(self, BinaryOp::Eq, Prop::Bool(false)) + fn is_false(self) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Eq, Prop::Bool(false)) } } @@ -192,53 +192,53 @@ pub trait EdgeTemporalPropOps: Sized { } // Direct comparison — no .into_expr() needed - fn gt(self, rhs: R) -> BinaryCmpEdgeFilter, R> { - BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Gt, rhs) + fn gt(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Gt, rhs) } - fn ge(self, rhs: R) -> BinaryCmpEdgeFilter, R> { - BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Ge, rhs) + fn ge(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Ge, rhs) } - fn lt(self, rhs: R) -> BinaryCmpEdgeFilter, R> { - BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Lt, rhs) + fn lt(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Lt, rhs) } - fn le(self, rhs: R) -> BinaryCmpEdgeFilter, R> { - BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Le, rhs) + fn le(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Le, rhs) } - fn eq(self, rhs: R) -> BinaryCmpEdgeFilter, R> { - BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Eq, rhs) + fn eq(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, rhs) } - fn ne(self, rhs: R) -> BinaryCmpEdgeFilter, R> { - BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Ne, rhs) + fn ne(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Ne, rhs) } - fn contains(self, rhs: R) -> StringEdgeFilter, R> { - StringEdgeFilter::new(self.into_expr(), StringOp::Contains, rhs) + fn contains(self, rhs: R) -> StringFilter, R> { + StringFilter::new(self.into_expr(), StringOp::Contains, rhs) } - fn starts_with(self, rhs: R) -> StringEdgeFilter, R> { - StringEdgeFilter::new(self.into_expr(), StringOp::StartsWith, rhs) + fn starts_with(self, rhs: R) -> StringFilter, R> { + StringFilter::new(self.into_expr(), StringOp::StartsWith, rhs) } - fn ends_with(self, rhs: R) -> StringEdgeFilter, R> { - StringEdgeFilter::new(self.into_expr(), StringOp::EndsWith, rhs) + fn ends_with(self, rhs: R) -> StringFilter, R> { + StringFilter::new(self.into_expr(), StringOp::EndsWith, rhs) } - fn not_contains(self, rhs: R) -> StringEdgeFilter, R> { - StringEdgeFilter::new(self.into_expr(), StringOp::NotContains, rhs) + fn not_contains(self, rhs: R) -> StringFilter, R> { + StringFilter::new(self.into_expr(), StringOp::NotContains, rhs) } fn fuzzy_search( self, rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringEdgeFilter, R> { - StringEdgeFilter::new( + ) -> StringFilter, R> { + StringFilter::new( self.into_expr(), StringOp::FuzzySearch { levenshtein_distance, prefix_match }, rhs, ) } - fn is_true(self) -> BinaryCmpEdgeFilter, Prop> { - BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(true)) + fn is_true(self) -> BinaryCmpFilter, Prop> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(true)) } - fn is_false(self) -> BinaryCmpEdgeFilter, Prop> { - BinaryCmpEdgeFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(false)) + fn is_false(self) -> BinaryCmpFilter, Prop> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(false)) } } @@ -267,43 +267,43 @@ impl EdgeTemp /// EdgeFilter.property("count").temporal().sum().gt(100i64) /// ``` pub trait EdgeExprFilterOps: EdgeExpr + Sized { - fn gt(self, rhs: R) -> BinaryCmpEdgeFilter { - BinaryCmpEdgeFilter::new(self, BinaryOp::Gt, rhs) + fn gt(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Gt, rhs) } - fn ge(self, rhs: R) -> BinaryCmpEdgeFilter { - BinaryCmpEdgeFilter::new(self, BinaryOp::Ge, rhs) + fn ge(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Ge, rhs) } - fn lt(self, rhs: R) -> BinaryCmpEdgeFilter { - BinaryCmpEdgeFilter::new(self, BinaryOp::Lt, rhs) + fn lt(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Lt, rhs) } - fn le(self, rhs: R) -> BinaryCmpEdgeFilter { - BinaryCmpEdgeFilter::new(self, BinaryOp::Le, rhs) + fn le(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Le, rhs) } - fn eq(self, rhs: R) -> BinaryCmpEdgeFilter { - BinaryCmpEdgeFilter::new(self, BinaryOp::Eq, rhs) + fn eq(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Eq, rhs) } - fn ne(self, rhs: R) -> BinaryCmpEdgeFilter { - BinaryCmpEdgeFilter::new(self, BinaryOp::Ne, rhs) + fn ne(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Ne, rhs) } - fn starts_with(self, rhs: R) -> StringEdgeFilter { - StringEdgeFilter::new(self, StringOp::StartsWith, rhs) + fn starts_with(self, rhs: R) -> StringFilter { + StringFilter::new(self, StringOp::StartsWith, rhs) } - fn ends_with(self, rhs: R) -> StringEdgeFilter { - StringEdgeFilter::new(self, StringOp::EndsWith, rhs) + fn ends_with(self, rhs: R) -> StringFilter { + StringFilter::new(self, StringOp::EndsWith, rhs) } - fn contains(self, rhs: R) -> StringEdgeFilter { - StringEdgeFilter::new(self, StringOp::Contains, rhs) + fn contains(self, rhs: R) -> StringFilter { + StringFilter::new(self, StringOp::Contains, rhs) } - fn not_contains(self, rhs: R) -> StringEdgeFilter { - StringEdgeFilter::new(self, StringOp::NotContains, rhs) + fn not_contains(self, rhs: R) -> StringFilter { + StringFilter::new(self, StringOp::NotContains, rhs) } fn fuzzy_search( self, rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringEdgeFilter { - StringEdgeFilter::new( + ) -> StringFilter { + StringFilter::new( self, StringOp::FuzzySearch { levenshtein_distance, prefix_match }, rhs, @@ -315,11 +315,11 @@ pub trait EdgeExprFilterOps: EdgeExpr + Sized { fn is_not_in(self, values: impl IntoIterator) -> PropValueSetEdgeFilter { PropValueSetEdgeFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsNotIn } } - fn any(self) -> BinaryCmpEdgeFilter, Prop> { - BinaryCmpEdgeFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true)) + fn any(self) -> BinaryCmpFilter, Prop> { + BinaryCmpFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true)) } - fn all(self) -> BinaryCmpEdgeFilter, Prop> { - BinaryCmpEdgeFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true)) + fn all(self) -> BinaryCmpFilter, Prop> { + BinaryCmpFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true)) } } diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 2d6707357f..b9680ba591 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -15,11 +15,11 @@ pub use crate::{ UnaryOp, }, node_expr::{ - AllExpr, AnyExpr, AvgExpr, BinaryCmpNodeFilter, + AllExpr, AnyExpr, AvgExpr, BinaryCmpFilter, FirstExpr, LastExpr, LenExpr, MaxExpr, - MinExpr, NodeExpr, NodeExprFilterOps, NodePropertyExprOps, NodeTemporalPropOps, - PropValueSetFilter, StringNodeFilter, SumExpr, - TemporalProp, UnaryNodeFilter, + MinExpr, EntityExprFilterOps, NodePropertyExprOps, NodeTemporalPropOps, + PropValueSetFilter, StringFilter, SumExpr, + TemporalProp, UnaryFilter, }, node_filter::{NodeFilter, NodeFilterFactory}, not_filter::NotFilter, @@ -456,7 +456,7 @@ use edge_expr::{ EdgeExpr, EdgeOp }; use crate::db::graph::views::filter::model::edge_expr::ops::{EdgeMetaOp, EdgePropOp}; -use crate::db::graph::views::filter::model::node_expr::EntityExpr; +use crate::db::graph::views::filter::model::node_expr::{EntityExpr, NodeExpr}; impl EdgeExpr for PropertyExpr 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 86c1dfef80..c0e136bbd6 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 @@ -79,7 +79,7 @@ use std::{marker::PhantomData, sync::Arc}; /// A node filter that compares two [`NodeExpr`] values using a [`BinaryOp`]. /// -/// Both sides produce `Option` at runtime. Created by [`NodeExprFilterOps`] methods +/// Both sides produce `Option` at runtime. Created by [`EntityExprFilterOps`] methods /// (`.gt`, `.lt`, `.eq`, `.ne`, `.ge`, `.le`). /// /// As a **terminal filter** (`CreateFilter`): compiles to `BinaryCmpNodeOp` → bool. @@ -94,44 +94,21 @@ use std::{marker::PhantomData, sync::Arc}; /// → BinaryCmpNodeFilter /// → BinaryCmpNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(I64(30))), op: Eq } /// ``` -pub struct BinaryCmpNodeFilter -where - L: NodeExpr, - R: NodeExpr, -{ +#[derive(Clone)] +pub struct BinaryCmpFilter { pub left: L, pub op: BinaryOp, pub right: R, } -impl BinaryCmpNodeFilter -where - L: NodeExpr, - R: NodeExpr, +impl BinaryCmpFilter { 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, +impl ComposableFilter for BinaryCmpFilter { } @@ -166,7 +143,7 @@ fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { Ok(()) } -impl CreateFilter for BinaryCmpNodeFilter +impl CreateFilter for BinaryCmpFilter where L: NodeExpr, R: NodeExpr, @@ -196,7 +173,8 @@ where ) -> 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(), &right.prop_type())?; + validate_binary_op(&self.op, &left.prop_type())?; + // TODO: validate_binary_op(&self.op, &left.prop_type(), &right.prop_type())?; Ok(Arc::new(BinaryCmpNodeOp { left, right, @@ -205,7 +183,7 @@ where } } -impl TryAsCompositeFilter for BinaryCmpNodeFilter +impl TryAsCompositeFilter for BinaryCmpFilter where L: NodeExpr, R: NodeExpr, @@ -239,38 +217,17 @@ where /// → UnaryNodeFilter /// → UnaryNodeOp { inner: NodePropOp(prop_id=N), op: IsSome } /// ``` -pub struct UnaryNodeFilter -where - E: NodeExpr, - I: Clone + Send + Sync + 'static, +#[derive(Clone)] +pub struct UnaryFilter { pub expr: E, pub op: UnaryOp, - pub(crate) _phantom: PhantomData, // TODO: Not needed anymore -} - -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 ComposableFilter for UnaryFilter { } -impl CreateFilter for UnaryNodeFilter +impl CreateFilter for UnaryFilter where E: NodeExpr, { @@ -302,10 +259,9 @@ where } } -impl TryAsCompositeFilter for UnaryNodeFilter +impl TryAsCompositeFilter for UnaryFilter where E: NodeExpr, - I: Clone + Send + Sync + 'static, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -329,7 +285,7 @@ where /// 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`, +/// Created by the string methods on [`EntityExprFilterOps`] (`.starts_with`, `.ends_with`, /// `.contains`, `.not_contains`, `.fuzzy_search`). /// Compiles to a `StringNodeOp` wrapped in `Arc>`. /// @@ -342,51 +298,24 @@ where /// → StringNodeFilter /// → StringNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(Str("foo"))), op: Contains } /// ``` -pub struct StringNodeFilter -where - L: NodeExpr, - R: NodeExpr, -{ +#[derive(Clone)] +pub struct StringFilter { pub left: L, pub op: StringOp, pub right: R, } -impl StringNodeFilter -where - L: NodeExpr, - R: NodeExpr, +impl StringFilter { pub fn new(left: L, op: StringOp, right: R) -> Self { Self { left, op, right } } } -impl Clone for StringNodeFilter -where - L: NodeExpr, - R: NodeExpr, -{ - 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, -{ +impl ComposableFilter for StringFilter { } -impl CreateFilter for StringNodeFilter -where - L: NodeExpr, - R: NodeExpr, +impl CreateFilter for StringFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; @@ -422,7 +351,7 @@ where } } -impl TryAsCompositeFilter for StringNodeFilter +impl TryAsCompositeFilter for StringFilter where L: NodeExpr, R: NodeExpr, @@ -449,22 +378,13 @@ where /// A node filter that checks whether an aggregated scalar property value is in /// (or not in) a fixed set of `Prop` values. Uses linear scan because `Prop` /// may contain floats that don't implement `Hash`. -pub struct PropValueSetFilter { +#[derive(Clone)] +pub struct PropValueSetFilter { pub(crate) expr: E, pub(crate) values: Vec, pub(crate) op: SetOp, } -impl Clone for PropValueSetFilter { - fn clone(&self) -> Self { - Self { - expr: self.expr.clone(), - values: self.values.clone(), - op: self.op, - } - } -} - impl ComposableFilter for PropValueSetFilter {} impl CreateFilter for PropValueSetFilter { @@ -590,62 +510,60 @@ impl NodeProp /// NodeFilter.property("age").gt(30i64) /// NodeFilter.property("score").temporal().gt(10i64).any() /// ``` -pub trait NodeExprFilterOps: NodeExpr + Sized { - fn gt(self, rhs: R) -> BinaryCmpNodeFilter { +pub trait EntityExprFilterOps: EntityExpr + Sized { + fn gt(self, rhs: R) -> BinaryCmpFilter { // TODO: validate ops - BinaryCmpNodeFilter::new(self, BinaryOp::Gt, rhs) + BinaryCmpFilter::new(self, BinaryOp::Gt, rhs) } - fn ge(self, rhs: R) -> BinaryCmpNodeFilter { - BinaryCmpNodeFilter::new(self, BinaryOp::Ge, rhs) + fn ge(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Ge, rhs) } - fn lt(self, rhs: R) -> BinaryCmpNodeFilter { - BinaryCmpNodeFilter::new(self, BinaryOp::Lt, rhs) + fn lt(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Lt, rhs) } - fn le(self, rhs: R) -> BinaryCmpNodeFilter { - BinaryCmpNodeFilter::new(self, BinaryOp::Le, rhs) + fn le(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Le, rhs) } - fn eq(self, rhs: R) -> BinaryCmpNodeFilter { - BinaryCmpNodeFilter::new(self, BinaryOp::Eq, rhs) + fn eq(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Eq, rhs) } - fn ne(self, rhs: R) -> BinaryCmpNodeFilter { - BinaryCmpNodeFilter::new(self, BinaryOp::Ne, rhs) + fn ne(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Ne, rhs) } - fn starts_with(self, rhs: R) -> StringNodeFilter + fn starts_with(self, rhs: R) -> StringFilter { - StringNodeFilter::new(self, StringOp::StartsWith, rhs) + StringFilter::new(self, StringOp::StartsWith, rhs) } - fn ends_with(self, rhs: R) -> StringNodeFilter - where + fn ends_with(self, rhs: R) -> StringFilter { - StringNodeFilter::new(self, StringOp::EndsWith, rhs) + StringFilter::new(self, StringOp::EndsWith, rhs) } - fn contains(self, rhs: R) -> StringNodeFilter + fn contains(self, rhs: R) -> StringFilter { - StringNodeFilter::new(self, StringOp::Contains, rhs) + StringFilter::new(self, StringOp::Contains, rhs) } - fn not_contains(self, rhs: R) -> StringNodeFilter - where + fn not_contains(self, rhs: R) -> StringFilter { - StringNodeFilter::new(self, StringOp::NotContains, rhs) + StringFilter::new(self, StringOp::NotContains, rhs) } - fn fuzzy_search( + fn fuzzy_search( self, rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringNodeFilter + ) -> StringFilter { - StringNodeFilter::new( + StringFilter::new( self, StringOp::FuzzySearch { levenshtein_distance, @@ -655,54 +573,37 @@ pub trait NodeExprFilterOps: NodeExpr + Sized { ) } - fn is_some(self) -> UnaryNodeFilter - where - Self: NodeExpr, - Inner: Clone + Send + Sync + 'static, + fn is_some(self) -> UnaryFilter { - UnaryNodeFilter { + UnaryFilter { expr: self, op: UnaryOp::IsSome, - _phantom: PhantomData, } } - fn is_none(self) -> UnaryNodeFilter - where - Self: NodeExpr, - Inner: Clone + Send + Sync + 'static, + fn is_none(self) -> UnaryFilter { - UnaryNodeFilter { + UnaryFilter { expr: self, op: UnaryOp::IsNone, - _phantom: PhantomData, } } - fn is_true(self) -> BinaryCmpNodeFilter - where - Self: NodeExpr, - { - self.eq(Prop::Bool(true)) - } - - fn is_false(self) -> BinaryCmpNodeFilter - where - Self: NodeExpr, + fn not(self) -> BinaryCmpFilter { self.eq(Prop::Bool(false)) } - fn any(self) -> BinaryCmpNodeFilter, Prop> { - BinaryCmpNodeFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true)) + fn any(self) -> BinaryCmpFilter, Prop> { + BinaryCmpFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true)) } - fn all(self) -> BinaryCmpNodeFilter, Prop> { - BinaryCmpNodeFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true)) + fn all(self) -> BinaryCmpFilter, Prop> { + BinaryCmpFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true)) } } -impl NodeExprFilterOps for E {} +impl EntityExprFilterOps for E {} // ───────────────────────────────────────────────────────────────────────────── // NodeExpr impls for filter types — enables mid-chain use before .any()/.all() @@ -712,9 +613,14 @@ impl NodeExprFilterOps for E {} // temporal().is_in([...]).any() // ───────────────────────────────────────────────────────────────────────────── -impl EntityExpr for BinaryCmpNodeFilter {} +impl EntityExpr for BinaryCmpFilter { + fn prop_type(&self) -> PropType { + // TODO: depending on the types of left and right, we should figure out the type to return here + PropType::Empty + } +} -impl NodeExpr for BinaryCmpNodeFilter { +impl NodeExpr for BinaryCmpFilter { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -725,9 +631,14 @@ impl NodeExpr for BinaryCmpNodeFilter { } } -impl EntityExpr for StringNodeFilter {} +impl EntityExpr for StringFilter { + fn prop_type(&self) -> PropType { + // TODO: depending on the types of left and right, we should figure out the type to return here + PropType::Empty + } +} -impl NodeExpr for StringNodeFilter { +impl NodeExpr for StringFilter { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -738,7 +649,12 @@ impl NodeExpr for StringNodeFilter { } } -impl EntityExpr for PropValueSetFilter {} +impl EntityExpr for PropValueSetFilter { + fn prop_type(&self) -> PropType { + // TODO: Figure the type to return + PropType::Empty + } +} impl NodeExpr for PropValueSetFilter { fn create_node_op<'g, G: GraphView + 'g>( 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 index 41124fcb61..fe5062b9d2 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -45,7 +45,7 @@ pub use super::{Metadata, Property}; /// NodeFilter.property("score").temporal().gt(10i64).any() /// ``` /// -pub trait NodeExpr: EntityExpr + Clone + Send + Sync + 'static { +pub(crate) trait NodeExpr: EntityExpr + Clone + Send + Sync + 'static { /// Compile the expression against a specific graph view. /// /// Any name→ID resolution (property, metadata) happens here, once. @@ -114,53 +114,53 @@ pub trait NodeTemporalPropOps: Sized { AllExpr(self.into_expr()) } - fn gt(self, rhs: R) -> BinaryCmpNodeFilter, R> { - BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Gt, rhs) + fn gt(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Gt, rhs) } - fn ge(self, rhs: R) -> BinaryCmpNodeFilter, R> { - BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Ge, rhs) + fn ge(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Ge, rhs) } - fn lt(self, rhs: R) -> BinaryCmpNodeFilter, R> { - BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Lt, rhs) + fn lt(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Lt, rhs) } - fn le(self, rhs: R) -> BinaryCmpNodeFilter, R> { - BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Le, rhs) + fn le(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Le, rhs) } - fn eq(self, rhs: R) -> BinaryCmpNodeFilter, R> { - BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Eq, rhs) + fn eq(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, rhs) } - fn ne(self, rhs: R) -> BinaryCmpNodeFilter, R> { - BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Ne, rhs) + fn ne(self, rhs: R) -> BinaryCmpFilter, R> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Ne, rhs) } - fn contains(self, rhs: R) -> StringNodeFilter, R> { - StringNodeFilter::new(self.into_expr(), StringOp::Contains, rhs) + fn contains(self, rhs: R) -> StringFilter, R> { + StringFilter::new(self.into_expr(), StringOp::Contains, rhs) } - fn starts_with(self, rhs: R) -> StringNodeFilter, R> { - StringNodeFilter::new(self.into_expr(), StringOp::StartsWith, rhs) + fn starts_with(self, rhs: R) -> StringFilter, R> { + StringFilter::new(self.into_expr(), StringOp::StartsWith, rhs) } - fn ends_with(self, rhs: R) -> StringNodeFilter, R> { - StringNodeFilter::new(self.into_expr(), StringOp::EndsWith, rhs) + fn ends_with(self, rhs: R) -> StringFilter, R> { + StringFilter::new(self.into_expr(), StringOp::EndsWith, rhs) } - fn not_contains(self, rhs: R) -> StringNodeFilter, R> { - StringNodeFilter::new(self.into_expr(), StringOp::NotContains, rhs) + fn not_contains(self, rhs: R) -> StringFilter, R> { + StringFilter::new(self.into_expr(), StringOp::NotContains, rhs) } fn fuzzy_search( self, rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringNodeFilter, R> { - StringNodeFilter::new( + ) -> StringFilter, R> { + StringFilter::new( self.into_expr(), StringOp::FuzzySearch { levenshtein_distance, prefix_match }, rhs, ) } - fn is_true(self) -> BinaryCmpNodeFilter, Prop> { - BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(true)) + fn is_true(self) -> BinaryCmpFilter, Prop> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(true)) } - fn is_false(self) -> BinaryCmpNodeFilter, Prop> { - BinaryCmpNodeFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(false)) + fn is_false(self) -> BinaryCmpFilter, Prop> { + BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(false)) } } 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 index d26de555d9..e6fd700e37 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -400,7 +400,7 @@ pub fn broadcast_binary( l.iter_all().map(|(l)| op(l, Some(r))).flatten().collect(), )), (l, Prop::List(r)) => Some(Prop::List( - r.iter_all().map(|(r)| op(Some(l), r)).flatten().collect(), + r.iter_all().map(|r| op(Some(l), r)).flatten().collect(), )), (l, r) => op(Some(l), Some(r)), } 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 498e5286e8..dec7ac84e3 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 @@ -148,7 +148,7 @@ fn total_gt_in_degree_selects_nodes_with_outgoing_edges() { #[test] fn const_expr_works() { - let filter = BinaryCmpNodeFilter::new(ConstExpr(2usize), BinaryOp::Eq, ConstExpr(2usize)); + let filter = BinaryCmpFilter::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/lib.rs b/raphtory/src/lib.rs index c65b1de285..530f68626b 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -148,7 +148,7 @@ pub mod prelude { pub use crate::db::graph::views::filter::model::{ filter::Filter, property_filter::PropertyFilter, - NodeExprFilterOps, NodePropertyExprOps, NodeTemporalPropOps, + EntityExprFilterOps, NodePropertyExprOps, NodeTemporalPropOps, EdgeExprFilterOps, EdgePropertyExprOps, EdgeTemporalPropOps, }; From 9048bbd9f827905ef5bfcb2e1aaed05716c23553 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 16 Jun 2026 15:00:52 +0200 Subject: [PATCH 031/155] fix the other issues --- .../graph/views/filter/model/edge_expr/ops.rs | 57 ++++++++++++------- .../graph/views/filter/model/node_expr/ops.rs | 28 +++++---- .../filter/model/property_filter/evaluate.rs | 4 +- 3 files changed, 56 insertions(+), 33 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 4db1cb52f6..d1e92c62aa 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -2,8 +2,6 @@ //! //! Parallel to `node_expr/ops.rs` — same design, different subject. -use std::collections::HashSet; -use std::hash::Hash; use crate::db::{ api::{ properties::internal::{InternalMetadataOps, InternalTemporalPropertyViewOps}, @@ -17,11 +15,12 @@ use raphtory_api::core::entities::{ properties::prop::{Prop, PropType}, }; use raphtory_storage::graph::graph::GraphStorage; +use std::{collections::HashSet, hash::Hash}; use super::EdgeOp; -use std::sync::Arc; -use raphtory_api::core::entities::properties::prop::PropArray; use crate::db::graph::views::filter::model::property_filter::evaluate::aggregate_values; +use raphtory_api::core::entities::properties::prop::PropArray; +use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── // Arc — blanket impl so Arc-boxed ops satisfy EdgeOp // ───────────────────────────────────────────────────────────────────────────── @@ -147,8 +146,9 @@ impl<'g, L: Comparable + Clone + Send + Sync + 'static> EdgeOp for BinaryCmpEdge // UnaryEdgeOp<'g, I> — is_some / is_none on Option-valued expressions // ───────────────────────────────────────────────────────────────────────────── -use crate::db::graph::views::filter::model::filter_operator::UnaryOp; -use crate::db::graph::views::filter::model::{SetOp, StringComparable, StringOp}; +use crate::db::graph::views::filter::model::{ + filter_operator::UnaryOp, SetOp, StringComparable, StringOp, +}; #[derive(Clone)] pub(crate) struct UnaryEdgeOp<'g, I: Clone + Send + Sync + 'static> { @@ -261,9 +261,15 @@ impl<'g> EdgeOp for ListAwareCmpEdgeOp<'g> { let lv = self.left.apply(storage, edge); let rhs = self.right.apply(storage, edge)?; let op = &self.op; - aggregate_values(lv, &|pi| { - let bools: Vec = pi.map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))).collect(); - if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + aggregate_values(lv, |pi| { + let bools: Vec = pi + .map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))) + .collect(); + if bools.is_empty() { + None + } else { + Some(Prop::List(PropArray::from(bools))) + } }) } } @@ -286,9 +292,15 @@ impl<'g> EdgeOp for ListAwareStringEdgeOp<'g> { let lv = self.left.apply(storage, edge); let rhs = self.right.apply(storage, edge); let op = &self.op; - aggregate_values(lv, &|pi| { - let bools: Vec = pi.map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))).collect(); - if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + aggregate_values(lv, |pi| { + let bools: Vec = pi + .map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))) + .collect(); + if bools.is_empty() { + None + } else { + Some(Prop::List(PropArray::from(bools))) + } }) } } @@ -311,13 +323,20 @@ impl<'g> EdgeOp for ListAwareSetEdgeOp<'g> { let lv = self.inner.apply(storage, edge); let values = &self.values; let op = &self.op; - aggregate_values(lv, &|pi| { - let bools: Vec = pi.map(|v| Prop::Bool(match op { - SetOp::IsIn => values.iter().any(|x| x == &v), - SetOp::IsNotIn => values.iter().all(|x| x != &v), - })).collect(); - if bools.is_empty() { None } else { Some(Prop::List(PropArray::from(bools))) } + aggregate_values(lv, |pi| { + let bools: Vec = pi + .map(|v| { + Prop::Bool(match op { + SetOp::IsIn => values.iter().any(|x| x == &v), + SetOp::IsNotIn => values.iter().all(|x| x != &v), + }) + }) + .collect(); + if bools.is_empty() { + None + } else { + Some(Prop::List(PropArray::from(bools))) + } }) } } - 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 index e6fd700e37..7b586f0a15 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -37,9 +37,6 @@ //! ``` use super::EdgeOp; -use crate::db::graph::views::filter::model::property_filter::evaluate::{ - scan_f64_sum_count, scan_i64_sum, scan_u64_sum, -}; use crate::{ db::{ api::{ @@ -49,19 +46,20 @@ use crate::{ }, graph::views::filter::model::{ filter_operator::{BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp}, - property_filter::evaluate::aggregate_values, + property_filter::evaluate::{ + aggregate_values, scan_f64_sum_count, scan_i64_sum, scan_u64_sum, + }, }, }, prelude::GraphViewOps, }; -use raphtory_api::core::entities::edges::edge_ref::EdgeRef; use raphtory_api::core::entities::{ + edges::edge_ref::EdgeRef, properties::prop::{IntoProp, Prop, PropArray, PropType}, VID, }; use raphtory_storage::graph::graph::GraphStorage; -use std::borrow::Borrow; -use std::{collections::HashSet, hash::Hash, sync::Arc}; +use std::{borrow::Borrow, collections::HashSet, hash::Hash, sync::Arc}; // ───────────────────────────────────────────────────────────────────────────── // NodePropOp — latest property value by pre-resolved column ID // ───────────────────────────────────────────────────────────────────────────── @@ -198,7 +196,7 @@ macro_rules! impl_agg_entity_op { } impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { - aggregate_values(vals, &|pi| { + aggregate_values(vals, |pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { return None; @@ -230,7 +228,7 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { }); impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { - aggregate_values(vals, &|pi| { + aggregate_values(vals, |pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { return None; @@ -397,10 +395,16 @@ pub fn broadcast_binary( } } (Prop::List(l), r) => Some(Prop::List( - l.iter_all().map(|(l)| op(l, Some(r))).flatten().collect(), + l.iter_all() + .map(|(l)| op(l, Some(r.clone()))) + .flatten() + .collect(), )), (l, Prop::List(r)) => Some(Prop::List( - r.iter_all().map(|r| op(Some(l), r)).flatten().collect(), + r.iter_all() + .map(|r| op(Some(l.clone()), r)) + .flatten() + .collect(), )), (l, r) => op(Some(l), Some(r)), } @@ -435,7 +439,7 @@ impl<'g> NodeOp for ListAwareSetNodeOp<'g> { // Some(Prop::List(PropArray::from(bools))) // } // }) - broadcast_unary(vals, |v, rhs| { + broadcast_unary(vals, |v| { let v = v?; Some(Prop::Bool(match op { SetOp::IsIn => values.iter().any(|x| x == &v), diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index 05540eae1d..d8faa9f526 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -1,7 +1,7 @@ +use super::PropertyFilter; use crate::db::graph::views::filter::model::Op; use raphtory_api::core::entities::properties::prop::{Prop, PropArray, PropType}; use std::borrow::Borrow; -use super::PropertyFilter; enum ValueType { Seq(Vec), @@ -17,7 +17,7 @@ pub fn aggregate_values( PropType::List(_) => { let s = x .iter_all() - .map(|y| aggregate_values(y, op)) + .map(|y| aggregate_values(y, &op)) .flatten() .collect(); Some(Prop::List(s)) From 49f6b94846ce6d10f8d95775923075431749bcc2 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 16 Jun 2026 15:30:50 +0200 Subject: [PATCH 032/155] add markers --- .../views/filter/model/node_expr/exprs.rs | 39 ++++-- .../views/filter/model/node_expr/filters.rs | 112 +++++++++--------- .../graph/views/filter/model/node_expr/mod.rs | 52 ++++++-- 3 files changed, 125 insertions(+), 78 deletions(-) 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 27dad2f803..72ffbe6469 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 @@ -64,7 +64,7 @@ use super::{ops::{ AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NodeMetaOp, NodePropOp, SumNodeOp, TemporalNodePropOp, -}, AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, NodeExpr, SumEdgeOp}; +}, AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, NodeExpr, NodeExprMarker, SumEdgeOp}; use crate::{ db::{ api::{ @@ -72,23 +72,23 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::model::{ - filter_operator::Comparable, node_filter::NodeFilter, CreateView, - Metadata, Property, + edge_expr::{EdgeExpr, EdgeOp}, + filter_operator::Comparable, + node_filter::NodeFilter, + CreateView, Metadata, Property, }, }, errors::GraphError, }; -use raphtory_api::core::entities::properties::prop::IntoProp; use raphtory_api::core::{ entities::{ - properties::prop::{Prop, PropType}, + properties::prop::{IntoProp, Prop, PropType}, GID, }, storage::arc_str::ArcStr, Direction, }; use std::sync::Arc; -use crate::db::graph::views::filter::model::edge_expr::{EdgeExpr, EdgeOp}; // ───────────────────────────────────────────────────────────────────────────── // Node field expressions — identity, name, type // @@ -100,7 +100,12 @@ use crate::db::graph::views::filter::model::edge_expr::{EdgeExpr, EdgeOp}; // NodeFilter.node_type() uses Type — produces Option (ArcStr as Prop::Str, None if unset) // ───────────────────────────────────────────────────────────────────────────── -impl EntityExpr for Id {} +#[derive(Copy, Clone, Debug, Default)] +pub struct ConstFilter; + +impl EntityExpr for Id { + type Marker = NodeFilter; +} impl NodeExpr for Id { fn create_node_op<'g, G: GraphView + 'g>( @@ -111,7 +116,9 @@ impl NodeExpr for Id { } } -impl EntityExpr for GID {} +impl EntityExpr for GID { + type Marker = NodeFilter; +} impl NodeExpr for GID { fn create_node_op<'g, G: GraphView + 'g>( @@ -123,6 +130,7 @@ impl NodeExpr for GID { } impl EntityExpr for Name { + type Marker = NodeFilter; fn prop_type(&self) -> PropType { PropType::Str } @@ -138,12 +146,14 @@ impl NodeExpr for Name { } impl EntityExpr for Type { + type Marker = NodeFilter; fn prop_type(&self) -> PropType { PropType::Str } } impl NodeExpr for Type { + fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, @@ -162,6 +172,7 @@ impl NodeExpr for Type { // ───────────────────────────────────────────────────────────────────────────── impl EntityExpr for usize { + type Marker = ConstFilter; fn prop_type(&self) -> PropType { PropType::U64 } @@ -177,6 +188,7 @@ impl NodeExpr for usize { } impl EntityExpr for String { + type Marker = ConstFilter; fn prop_type(&self) -> PropType { PropType::Str } @@ -192,6 +204,7 @@ impl NodeExpr for String { } impl EntityExpr for ArcStr { + type Marker = ConstFilter; fn prop_type(&self) -> PropType { PropType::Str } @@ -206,7 +219,9 @@ impl NodeExpr for ArcStr { } } -impl EntityExpr for &'static str { +impl EntityExpr for &'static str +{ + type Marker = ConstFilter; fn prop_type(&self) -> PropType { PropType::Str } @@ -230,6 +245,7 @@ impl NodeExpr for &'static str { // ───────────────────────────────────────────────────────────────────────────── impl EntityExpr for Prop { + type Marker = ConstFilter; fn prop_type(&self) -> PropType { self.dtype() } @@ -285,7 +301,9 @@ impl_node_expr_for_numeric!(u16, U16); #[derive(Clone)] pub struct ConstExpr(pub T); -impl EntityExpr for ConstExpr {} +impl EntityExpr for ConstExpr { + type Marker = ConstFilter; +} impl + Clone + Send + Sync + 'static> NodeExpr for ConstExpr { fn create_node_op<'g, G: GraphView + 'g>( @@ -317,6 +335,7 @@ pub struct DegreeExpr { } impl EntityExpr for DegreeExpr { + type Marker = NodeFilter; fn prop_type(&self) -> PropType { PropType::U64 } 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 c0e136bbd6..5c75ef485e 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 @@ -42,25 +42,19 @@ use super::{ ops::{ - BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, - ListAwareStringNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, + BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, + PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, - AllExpr, AnyExpr, EntityExpr, NodeExpr, + AllExpr, AnyExpr, EntityExpr, NodeExpr, NodeExprMarker, }; use crate::{ db::{ - api::{ - state::ops::NodeOp, - view::internal::GraphView, - }, + api::{state::ops::NodeOp, view::internal::GraphView}, graph::views::filter::{ model::{ edge_filter::CompositeEdgeFilter, - filter_operator::{ - BinaryOp, SetOp, StringOp, UnaryOp, - }, - node_filter::NodeFilterFactory - , + filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, + node_filter::NodeFilterFactory, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, CreateView, MetadataExpr, PropertyExpr, TryAsCompositeFilter, }, @@ -68,11 +62,11 @@ use crate::{ }, }, errors::GraphError, - prelude::GraphViewOps, + prelude::{GraphViewOps, NodeFilter}, }; +use neo4rs::Node; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::{marker::PhantomData, sync::Arc}; - // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpNodeFilter — binary expression filter // ───────────────────────────────────────────────────────────────────────────── @@ -95,22 +89,25 @@ use std::{marker::PhantomData, sync::Arc}; /// → BinaryCmpNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(I64(30))), op: Eq } /// ``` #[derive(Clone)] -pub struct BinaryCmpFilter { +pub struct BinaryCmpFilter { pub left: L, pub op: BinaryOp, pub right: R, + pub entity: Entity, } -impl BinaryCmpFilter -{ - pub fn new(left: L, op: BinaryOp, right: R) -> Self { - Self { left, op, right } +impl BinaryCmpFilter { + pub fn new(left: L, op: BinaryOp, right: R, entity: E) -> Self { + Self { + left, + op, + right, + entity, + } } } -impl ComposableFilter for BinaryCmpFilter -{ -} +impl ComposableFilter for BinaryCmpFilter {} /// Reject ordering operators on boolean properties. //. TODO: Also check if both the types are comparable. @@ -143,7 +140,7 @@ fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { Ok(()) } -impl CreateFilter for BinaryCmpFilter +impl CreateFilter for BinaryCmpFilter where L: NodeExpr, R: NodeExpr, @@ -183,7 +180,7 @@ where } } -impl TryAsCompositeFilter for BinaryCmpFilter +impl TryAsCompositeFilter for BinaryCmpFilter where L: NodeExpr, R: NodeExpr, @@ -218,14 +215,12 @@ where /// → UnaryNodeOp { inner: NodePropOp(prop_id=N), op: IsSome } /// ``` #[derive(Clone)] -pub struct UnaryFilter -{ +pub struct UnaryFilter { pub expr: E, pub op: UnaryOp, } -impl ComposableFilter for UnaryFilter { -} +impl ComposableFilter for UnaryFilter {} impl CreateFilter for UnaryFilter where @@ -305,18 +300,15 @@ pub struct StringFilter { pub right: R, } -impl StringFilter -{ +impl StringFilter { pub fn new(left: L, op: StringOp, right: R) -> Self { Self { left, op, right } } } -impl ComposableFilter for StringFilter { -} +impl ComposableFilter for StringFilter {} -impl CreateFilter for StringFilter -{ +impl CreateFilter for StringFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; @@ -474,12 +466,20 @@ impl TemporalProp { // NodePropertyExprOps — fluent comparison API for node-side property expressions // ───────────────────────────────────────────────────────────────────────────── -pub trait NodePropertyExprOps: NodeExpr + Sized { +pub trait NodePropertyExprOps: EntityExpr + Sized { fn is_in(self, values: impl IntoIterator) -> PropValueSetFilter { - PropValueSetFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsIn } + PropValueSetFilter { + expr: self, + values: values.into_iter().collect(), + op: SetOp::IsIn, + } } fn is_not_in(self, values: impl IntoIterator) -> PropValueSetFilter { - PropValueSetFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsNotIn } + PropValueSetFilter { + expr: self, + values: values.into_iter().collect(), + op: SetOp::IsNotIn, + } } } @@ -536,23 +536,19 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { BinaryCmpFilter::new(self, BinaryOp::Ne, rhs) } - fn starts_with(self, rhs: R) -> StringFilter - { + fn starts_with(self, rhs: R) -> StringFilter { StringFilter::new(self, StringOp::StartsWith, rhs) } - fn ends_with(self, rhs: R) -> StringFilter - { + fn ends_with(self, rhs: R) -> StringFilter { StringFilter::new(self, StringOp::EndsWith, rhs) } - fn contains(self, rhs: R) -> StringFilter - { + fn contains(self, rhs: R) -> StringFilter { StringFilter::new(self, StringOp::Contains, rhs) } - fn not_contains(self, rhs: R) -> StringFilter - { + fn not_contains(self, rhs: R) -> StringFilter { StringFilter::new(self, StringOp::NotContains, rhs) } @@ -561,8 +557,7 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringFilter - { + ) -> StringFilter { StringFilter::new( self, StringOp::FuzzySearch { @@ -573,24 +568,21 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { ) } - fn is_some(self) -> UnaryFilter - { + fn is_some(self) -> UnaryFilter { UnaryFilter { expr: self, op: UnaryOp::IsSome, } } - fn is_none(self) -> UnaryFilter - { + fn is_none(self) -> UnaryFilter { UnaryFilter { expr: self, op: UnaryOp::IsNone, } } - fn not(self) -> BinaryCmpFilter - { + fn not(self) -> BinaryCmpFilter { self.eq(Prop::Bool(false)) } @@ -620,6 +612,8 @@ impl EntityExpr for BinaryCmpFilter { } } +impl NodeExprMarker for BinaryCmpFilter {} + impl NodeExpr for BinaryCmpFilter { fn create_node_op<'g, G: GraphView + 'g>( &self, @@ -627,7 +621,11 @@ impl NodeExpr for BinaryCmpFilter { ) -> Result> + 'g>, GraphError> { let left = self.left.create_node_op(graph.clone())?; let right = self.right.create_node_op(graph)?; - Ok(Arc::new(ListAwareCmpNodeOp { left, right, op: self.op })) + Ok(Arc::new(ListAwareCmpNodeOp { + left, + right, + op: self.op, + })) } } @@ -645,7 +643,11 @@ impl NodeExpr for StringFilter { ) -> Result> + 'g>, GraphError> { let left = self.left.create_node_op(graph.clone())?; let right = self.right.create_node_op(graph)?; - Ok(Arc::new(ListAwareStringNodeOp { left, right, op: self.op })) + Ok(Arc::new(ListAwareStringNodeOp { + left, + right, + op: self.op, + })) } } 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 index fe5062b9d2..df8da2417b 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -1,9 +1,11 @@ use crate::{ - db::api::{state::ops::NodeOp, view::internal::GraphView}, - db::graph::views::filter::model::{ - CreateView, - filter_operator::{BinaryOp, StringOp}, - node_filter::NodeFilterFactory, + db::{ + api::{state::ops::NodeOp, view::internal::GraphView}, + graph::views::filter::model::{ + filter_operator::{BinaryOp, StringOp}, + node_filter::NodeFilterFactory, + CreateView, + }, }, errors::GraphError, }; @@ -17,11 +19,11 @@ pub mod ops; #[cfg(test)] mod tests; +pub use super::{Metadata, Property}; +use crate::db::graph::views::filter::model::edge_expr::EdgeOp; pub use exprs::*; pub use filters::*; pub use ops::*; -use crate::db::graph::views::filter::model::edge_expr::EdgeOp; -pub use super::{Metadata, Property}; // ───────────────────────────────────────────────────────────────────────────── // NodeExpr — typed node expression with associated Output type @@ -45,7 +47,7 @@ pub use super::{Metadata, Property}; /// NodeFilter.property("score").temporal().gt(10i64).any() /// ``` /// -pub(crate) trait NodeExpr: EntityExpr + Clone + Send + Sync + 'static { +pub trait NodeExpr: EntityExpr + Clone + Send + Sync + 'static { /// Compile the expression against a specific graph view. /// /// Any name→ID resolution (property, metadata) happens here, once. @@ -56,12 +58,21 @@ pub(crate) trait NodeExpr: EntityExpr + Clone + Send + Sync + 'static { } pub trait EntityExpr: Clone + Send + Sync + 'static { + type Marker: Copy + Default + 'static; + + fn entity() -> Self::Marker { + Self::Marker::default() + } + /// A priory known type (for early validation where possible) fn prop_type(&self) -> PropType { PropType::Empty } } +/// Sealed trait to avoid conflicts between node and edge expressions +pub(crate) trait NodeExprMarker {} + // ───────────────────────────────────────────────────────────────────────────── // NodeTemporalPropOps — aggregation and direct comparison on TemporalProp // ───────────────────────────────────────────────────────────────────────────── @@ -132,16 +143,28 @@ pub trait NodeTemporalPropOps: Sized { fn ne(self, rhs: R) -> BinaryCmpFilter, R> { BinaryCmpFilter::new(self.into_expr(), BinaryOp::Ne, rhs) } - fn contains(self, rhs: R) -> StringFilter, R> { + fn contains( + self, + rhs: R, + ) -> StringFilter, R> { StringFilter::new(self.into_expr(), StringOp::Contains, rhs) } - fn starts_with(self, rhs: R) -> StringFilter, R> { + fn starts_with( + self, + rhs: R, + ) -> StringFilter, R> { StringFilter::new(self.into_expr(), StringOp::StartsWith, rhs) } - fn ends_with(self, rhs: R) -> StringFilter, R> { + fn ends_with( + self, + rhs: R, + ) -> StringFilter, R> { StringFilter::new(self.into_expr(), StringOp::EndsWith, rhs) } - fn not_contains(self, rhs: R) -> StringFilter, R> { + fn not_contains( + self, + rhs: R, + ) -> StringFilter, R> { StringFilter::new(self.into_expr(), StringOp::NotContains, rhs) } fn fuzzy_search( @@ -152,7 +175,10 @@ pub trait NodeTemporalPropOps: Sized { ) -> StringFilter, R> { StringFilter::new( self.into_expr(), - StringOp::FuzzySearch { levenshtein_distance, prefix_match }, + StringOp::FuzzySearch { + levenshtein_distance, + prefix_match, + }, rhs, ) } From ba90796fcc1a940e1e2082f2ce90123cd9e8d410 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:24:43 +0100 Subject: [PATCH 033/155] impl entity marker and rid duplications --- raphtory-tests/tests/edge_expr_filter.rs | 1 - .../views/filter/model/edge_expr/exprs.rs | 45 +--- .../views/filter/model/edge_expr/filters.rs | 55 ++-- .../graph/views/filter/model/edge_expr/mod.rs | 248 +----------------- .../graph/views/filter/model/edge_expr/ops.rs | 6 +- .../src/db/graph/views/filter/model/mod.rs | 53 +++- .../views/filter/model/node_expr/exprs.rs | 75 +++--- .../views/filter/model/node_expr/filters.rs | 184 ++++++------- .../graph/views/filter/model/node_expr/mod.rs | 148 ++++------- .../graph/views/filter/model/node_expr/ops.rs | 18 +- .../views/filter/model/node_expr/tests.rs | 28 +- .../filter/model/property_filter/evaluate.rs | 4 +- raphtory/src/lib.rs | 3 +- 13 files changed, 286 insertions(+), 582 deletions(-) diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs index ce81282aa6..5a1dafc05d 100644 --- a/raphtory-tests/tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -5,7 +5,6 @@ use raphtory::{ }, prelude::*, }; -use raphtory::prelude::EdgeExprFilterOps; fn sorted_edges(g: impl GraphViewOps<'static>) -> Vec { let mut edges: Vec = g diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs index 5019f23a11..4075769fa4 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs @@ -1,9 +1,9 @@ //! EdgeExpr impls for the shared Property/Metadata structs and scalar types. -use super::{ops::{EdgeMetaOp, EdgePropOp, TemporalEdgePropOp}, EdgeExpr, EdgeOp}; +use super::{ops::{EdgeMetaOp, EdgePropOp}, EdgeExpr, EdgeOp}; use crate::{ db::api::{state::ops::Const, view::internal::GraphView}, - db::graph::views::filter::model::{node_expr::EntityExpr, Metadata, Property}, + db::graph::views::filter::model::{edge_filter::EdgeFilter, node_expr::EntityExpr, Metadata, Property}, errors::GraphError, }; use raphtory_api::core::entities::properties::prop::Prop; @@ -94,44 +94,3 @@ impl EdgeExpr for usize { } } -// ───────────────────────────────────────────────────────────────────────────── -// TemporalEdgePropExpr — all temporal values of a property in the view window -// ───────────────────────────────────────────────────────────────────────────── - -/// Parallel to `TemporalPropertyExpr` but for edges: reads from `edge_meta()`. -/// -/// Produced by `EdgeTemporalPropOps::into_expr()` on `TemporalProp` when `E: EdgeFilterFactory`, -/// or implicitly inside `.sum()`, `.any()`, etc. -/// Returns `Some(Prop::List([...]))` of all temporal values within the view window. -#[derive(Clone)] -pub struct TemporalEdgePropExpr { - pub(crate) view_expr: E, - pub(crate) name: String, -} - -impl TemporalEdgePropExpr { - pub fn new(view_expr: E, name: impl Into) -> Self { - Self { view_expr, name: name.into() } - } -} - -impl - EntityExpr for TemporalEdgePropExpr -{ -} - -impl - EdgeExpr for TemporalEdgePropExpr -{ - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let (prop_id, _) = graph - .edge_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(TemporalEdgePropOp { graph, prop_id })) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index 70d29939ad..0bc49ed78b 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -10,6 +10,9 @@ use super::{ }, EdgeExpr, EdgeOp, }; +use crate::db::graph::views::filter::model::edge_filter::EdgeFilter; +use crate::db::graph::views::filter::model::node_expr::filters::PropValueSetFilter; +pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpFilter, StringFilter, UnaryFilter}; use crate::{ db::{ api::{state::ops::NotANodeFilter, view::internal::GraphView}, @@ -17,9 +20,9 @@ use crate::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, model::{ edge_filter::CompositeEdgeFilter, - filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, - node_expr::EntityExpr, - ComposableFilter, CompositeExplodedEdgeFilter, + filter_operator::BinaryOp + + , CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, TryAsCompositeFilter, }, }, @@ -28,8 +31,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; -use std::{marker::PhantomData, sync::Arc}; -pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpFilter, StringFilter, UnaryFilter}; +use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── // validate helpers // ───────────────────────────────────────────────────────────────────────────── @@ -50,7 +52,7 @@ fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), GraphEr // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpEdgeFilter // ───────────────────────────────────────────────────────────────────────────── -impl EdgeExpr for BinaryCmpFilter +impl EdgeExpr for BinaryCmpFilter where L: EdgeExpr, R: EdgeExpr, @@ -65,7 +67,7 @@ where } } -impl TryAsCompositeFilter for BinaryCmpFilter +impl TryAsCompositeFilter for BinaryCmpFilter where L: EdgeExpr, R: EdgeExpr, @@ -85,7 +87,7 @@ where } } -impl CreateFilter for BinaryCmpFilter +impl CreateFilter for BinaryCmpFilter where L: EdgeExpr, R: EdgeExpr, @@ -125,7 +127,7 @@ where // UnaryEdgeFilter // ───────────────────────────────────────────────────────────────────────────── -impl TryAsCompositeFilter for UnaryFilter +impl TryAsCompositeFilter for UnaryFilter where E: EdgeExpr, { @@ -144,7 +146,7 @@ where } } -impl CreateFilter for UnaryFilter +impl CreateFilter for UnaryFilter where E: EdgeExpr, { @@ -194,7 +196,7 @@ fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { // ───────────────────────────────────────────────────────────────────────────── -impl EdgeExpr for StringFilter +impl EdgeExpr for StringFilter where L: EdgeExpr, R: EdgeExpr, @@ -209,7 +211,7 @@ where } } -impl TryAsCompositeFilter for StringFilter +impl TryAsCompositeFilter for StringFilter where L: EdgeExpr, R: EdgeExpr, @@ -227,7 +229,7 @@ where } } -impl CreateFilter for StringFilter +impl CreateFilter for StringFilter where L: EdgeExpr, R: EdgeExpr, @@ -262,24 +264,10 @@ where } // ───────────────────────────────────────────────────────────────────────────── -// PropValueSetEdgeFilter — is_in / is_not_in for Option (linear scan) +// PropValueSetFilter — is_in / is_not_in for edge-side exprs // ───────────────────────────────────────────────────────────────────────────── -pub struct PropValueSetEdgeFilter { - pub(crate) expr: E, - pub(crate) values: Vec, - pub(crate) op: SetOp, -} - -impl Clone for PropValueSetEdgeFilter { - fn clone(&self) -> Self { - Self { expr: self.expr.clone(), values: self.values.clone(), op: self.op } - } -} - -impl EntityExpr for PropValueSetEdgeFilter {} - -impl EdgeExpr for PropValueSetEdgeFilter { +impl EdgeExpr for PropValueSetFilter { fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -289,9 +277,8 @@ impl EdgeExpr for PropValueSetEdgeFilter { } } -impl ComposableFilter for PropValueSetEdgeFilter {} -impl TryAsCompositeFilter for PropValueSetEdgeFilter { +impl TryAsCompositeFilter for PropValueSetFilter { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) } @@ -305,7 +292,7 @@ impl TryAsCompositeFilter for PropValueSetEdgeFilter { } } -impl CreateFilter for PropValueSetEdgeFilter { +impl CreateFilter for PropValueSetFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; @@ -332,7 +319,3 @@ impl CreateFilter for PropValueSetEdgeFilter { Err(GraphError::NotNodeFilter) } } - -// (AnyExpr and AllExpr terminate via -// BinaryCmpEdgeFilter, Prop> / BinaryCmpEdgeFilter, Prop> -// produced by EdgeExprFilterOps::any() / all()) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index c21aa4d1a8..f6ac3865ad 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -22,12 +22,7 @@ use crate::{ db::{ api::view::internal::GraphView, - graph::views::filter::model::{ - filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, - node_expr::EntityExpr, - node_expr::filters::TemporalProp, - CreateView, EdgeFilterFactory, MetadataExpr, PropertyExpr, - }, + graph::views::filter::model::node_expr::EntityExpr, }, errors::GraphError, }; @@ -36,7 +31,7 @@ use raphtory_api::core::entities::{ properties::prop::{Prop, PropType}, }; use raphtory_storage::graph::graph::GraphStorage; -use std::{marker::PhantomData, sync::Arc}; +use std::sync::Arc; pub mod exprs; pub mod filters; @@ -44,9 +39,6 @@ pub mod ops; pub use exprs::*; pub use filters::*; -use filters::BinaryCmpFilter; -use crate::db::graph::views::filter::model::{ FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr}; -use crate::db::graph::views::filter::model::node_expr::{AllExpr, AnyExpr, AvgExpr}; pub use super::{Metadata, Property}; // ───────────────────────────────────────────────────────────────────────────── @@ -88,240 +80,4 @@ pub(crate) trait EdgeExpr: EntityExpr + Clone + Send + Sync + 'static { } -// ───────────────────────────────────────────────────────────────────────────── -// EdgePropertyExprOps — fluent comparison API for edge-side property expressions -// ───────────────────────────────────────────────────────────────────────────── - -/// Presence and set-membership operators for `PropertyExpr` and `MetadataExpr` -/// on the edge side. -pub trait EdgePropertyExprOps: EdgeExpr + Sized { - fn is_some(self) -> UnaryFilter { - UnaryFilter { expr: self, op: UnaryOp::IsSome} - } - fn is_none(self) -> UnaryFilter { - UnaryFilter { expr: self, op: UnaryOp::IsNone } - } - fn is_in( - self, - values: impl IntoIterator, - ) -> PropValueSetEdgeFilter { - PropValueSetEdgeFilter { - expr: self, - values: values.into_iter().collect(), - op: SetOp::IsIn, - } - } - fn is_not_in( - self, - values: impl IntoIterator, - ) -> PropValueSetEdgeFilter { - PropValueSetEdgeFilter { - expr: self, - values: values.into_iter().collect(), - op: SetOp::IsNotIn, - } - } - fn is_true(self) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Eq, Prop::Bool(true)) - } - fn is_false(self) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Eq, Prop::Bool(false)) - } -} - -impl EdgePropertyExprOps - for PropertyExpr -{ -} - -impl EdgePropertyExprOps - for MetadataExpr -{ -} - -// ───────────────────────────────────────────────────────────────────────────── -// EdgeTemporalPropOps — fluent temporal API for edge-side TemporalProp -// ───────────────────────────────────────────────────────────────────────────── - -/// Temporal aggregation/quantification on `TemporalProp` when `E: EdgeFilterFactory`. -/// -/// Provides both aggregation (`.sum()`, `.avg()`, `.len()`, …) and direct -/// element-wise comparison (`.gt()`, `.eq()`, `.contains()`, …) so users -/// never have to call `.into_expr()` explicitly: -/// -/// ```rust,ignore -/// EdgeFilter.property("score").temporal().sum().gt(100i64) -/// EdgeFilter.property("score").temporal().gt(10i64).any() -/// EdgeFilter.property("score").temporal().len().gt(3usize) -/// EdgeFilter.property("label").temporal().contains("rock").any() -/// ``` -pub trait EdgeTemporalPropOps: Sized { - type ViewExpr: CreateView + EdgeFilterFactory + Clone + Send + Sync + 'static; - fn into_temporal_parts(self) -> (Self::ViewExpr, String); - - fn into_expr(self) -> TemporalEdgePropExpr { - let (view_expr, name) = self.into_temporal_parts(); - TemporalEdgePropExpr::new(view_expr, name) - } - fn sum(self) -> SumExpr> { - SumExpr(self.into_expr()) - } - fn avg(self) -> AvgExpr> { - AvgExpr(self.into_expr()) - } - fn min(self) -> MinExpr> { - MinExpr(self.into_expr()) - } - fn max(self) -> MaxExpr> { - MaxExpr(self.into_expr()) - } - fn first(self) -> FirstExpr> { - FirstExpr(self.into_expr()) - } - fn last(self) -> LastExpr> { - LastExpr(self.into_expr()) - } - fn len(self) -> LenExpr> { - LenExpr(self.into_expr()) - } - fn any(self) -> AnyExpr> { - AnyExpr(self.into_expr()) - } - fn all(self) -> AllExpr> { - AllExpr(self.into_expr()) - } - - // Direct comparison — no .into_expr() needed - fn gt(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Gt, rhs) - } - fn ge(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Ge, rhs) - } - fn lt(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Lt, rhs) - } - fn le(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Le, rhs) - } - fn eq(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, rhs) - } - fn ne(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Ne, rhs) - } - fn contains(self, rhs: R) -> StringFilter, R> { - StringFilter::new(self.into_expr(), StringOp::Contains, rhs) - } - fn starts_with(self, rhs: R) -> StringFilter, R> { - StringFilter::new(self.into_expr(), StringOp::StartsWith, rhs) - } - fn ends_with(self, rhs: R) -> StringFilter, R> { - StringFilter::new(self.into_expr(), StringOp::EndsWith, rhs) - } - fn not_contains(self, rhs: R) -> StringFilter, R> { - StringFilter::new(self.into_expr(), StringOp::NotContains, rhs) - } - fn fuzzy_search( - self, - rhs: R, - levenshtein_distance: usize, - prefix_match: bool, - ) -> StringFilter, R> { - StringFilter::new( - self.into_expr(), - StringOp::FuzzySearch { levenshtein_distance, prefix_match }, - rhs, - ) - } - fn is_true(self) -> BinaryCmpFilter, Prop> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(true)) - } - fn is_false(self) -> BinaryCmpFilter, Prop> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(false)) - } -} - -impl EdgeTemporalPropOps - for TemporalProp -{ - type ViewExpr = E; - fn into_temporal_parts(self) -> (E, String) { - (self.view_expr, self.name) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// EdgeExprFilterOps — comparison operators on any EdgeExpr -// ───────────────────────────────────────────────────────────────────────────── - -/// Comparison, string, set, and presence operators on any [`EdgeExpr`]. -/// -/// `.any()` / `.all()` are terminal: they wrap `self` in `AnyExpr`/`AllExpr` and compare -/// to `Bool(true)`. For element-wise comparison before reduction, chain in order: -/// `.gt(10i64).any()` not `.any().gt(10i64)`. -/// -/// ```rust,ignore -/// EdgeFilter.property("weight").gt(5.0f64) -/// EdgeFilter.property("tag").temporal().into_expr().contains("foo").any() -/// EdgeFilter.property("count").temporal().sum().gt(100i64) -/// ``` -pub trait EdgeExprFilterOps: EdgeExpr + Sized { - fn gt(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Gt, rhs) - } - fn ge(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Ge, rhs) - } - fn lt(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Lt, rhs) - } - fn le(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Le, rhs) - } - fn eq(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Eq, rhs) - } - fn ne(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Ne, rhs) - } - fn starts_with(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::StartsWith, rhs) - } - fn ends_with(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::EndsWith, rhs) - } - fn contains(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::Contains, rhs) - } - fn not_contains(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::NotContains, rhs) - } - fn fuzzy_search( - self, - rhs: R, - levenshtein_distance: usize, - prefix_match: bool, - ) -> StringFilter { - StringFilter::new( - self, - StringOp::FuzzySearch { levenshtein_distance, prefix_match }, - rhs, - ) - } - fn is_in(self, values: impl IntoIterator) -> PropValueSetEdgeFilter { - PropValueSetEdgeFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsIn } - } - fn is_not_in(self, values: impl IntoIterator) -> PropValueSetEdgeFilter { - PropValueSetEdgeFilter { expr: self, values: values.into_iter().collect(), op: SetOp::IsNotIn } - } - fn any(self) -> BinaryCmpFilter, Prop> { - BinaryCmpFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true)) - } - fn all(self) -> BinaryCmpFilter, Prop> { - BinaryCmpFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true)) - } -} - -impl EdgeExprFilterOps for E {} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index d1e92c62aa..1159b31182 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -261,7 +261,7 @@ impl<'g> EdgeOp for ListAwareCmpEdgeOp<'g> { let lv = self.left.apply(storage, edge); let rhs = self.right.apply(storage, edge)?; let op = &self.op; - aggregate_values(lv, |pi| { + aggregate_values(lv, &|pi| { let bools: Vec = pi .map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))) .collect(); @@ -292,7 +292,7 @@ impl<'g> EdgeOp for ListAwareStringEdgeOp<'g> { let lv = self.left.apply(storage, edge); let rhs = self.right.apply(storage, edge); let op = &self.op; - aggregate_values(lv, |pi| { + aggregate_values(lv, &|pi| { let bools: Vec = pi .map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))) .collect(); @@ -323,7 +323,7 @@ impl<'g> EdgeOp for ListAwareSetEdgeOp<'g> { let lv = self.inner.apply(storage, edge); let values = &self.values; let op = &self.op; - aggregate_values(lv, |pi| { + aggregate_values(lv, &|pi| { let bools: Vec = pi .map(|v| { Prop::Bool(match op { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index b9680ba591..fb06969045 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -17,9 +17,9 @@ pub use crate::{ node_expr::{ AllExpr, AnyExpr, AvgExpr, BinaryCmpFilter, FirstExpr, LastExpr, LenExpr, MaxExpr, - MinExpr, EntityExprFilterOps, NodePropertyExprOps, NodeTemporalPropOps, + MinExpr, EntityExprFilterOps, TemporalPropOps, PropValueSetFilter, StringFilter, SumExpr, - TemporalProp, UnaryFilter, + TemporalExpr, TemporalProp, UnaryFilter, }, node_filter::{NodeFilter, NodeFilterFactory}, not_filter::NotFilter, @@ -70,7 +70,6 @@ use crate::{ prelude::LayerOps, }; pub use node_filter::CompositeNodeFilter; -pub use edge_expr::{EdgeExprFilterOps, EdgePropertyExprOps, EdgeTemporalPropOps}; use raphtory_api::core::{ entities::{properties::prop::Prop, Layer}, storage::{arc_str::ArcStr, timeindex::{AsTime, EventTime}}, @@ -292,9 +291,11 @@ pub struct PropertyExpr { name: String, } -impl EntityExpr for PropertyExpr {} +impl EntityExpr for PropertyExpr { + type Marker = E::Marker; +} -impl NodeExpr +impl NodeExpr for PropertyExpr { @@ -317,9 +318,11 @@ pub struct MetadataExpr { name: String, } -impl EntityExpr for MetadataExpr {} +impl EntityExpr for MetadataExpr { + type Marker = E::Marker; +} -impl NodeExpr +impl NodeExpr for MetadataExpr { fn create_node_op<'g, G: GraphView + 'g>( @@ -458,7 +461,7 @@ use edge_expr::{ use crate::db::graph::views::filter::model::edge_expr::ops::{EdgeMetaOp, EdgePropOp}; use crate::db::graph::views::filter::model::node_expr::{EntityExpr, NodeExpr}; -impl EdgeExpr +impl EdgeExpr for PropertyExpr { fn create_edge_op<'g, G: GraphView + 'g>( @@ -474,7 +477,7 @@ impl EdgeExpr } } -impl EdgeExpr +impl EdgeExpr for MetadataExpr { fn create_edge_op<'g, G: GraphView + 'g>( @@ -662,6 +665,10 @@ impl CreateView for NodeFilter { } } +impl EntityExpr for NodeFilter { + type Marker = NodeFilter; +} + impl CreateView for EdgeFilter { type View<'graph, G: GraphView + 'graph> = G; @@ -673,6 +680,10 @@ impl CreateView for EdgeFilter { } } +impl EntityExpr for EdgeFilter { + type Marker = EdgeFilter; +} + impl CreateView for ExplodedEdgeFilter { type View<'graph, G: GraphView + 'graph> = G; @@ -684,6 +695,30 @@ impl CreateView for ExplodedEdgeFilter { } } +impl EntityExpr for ExplodedEdgeFilter { + type Marker = EdgeFilter; +} + +impl EntityExpr for Windowed { + type Marker = T::Marker; +} + +impl EntityExpr for Layered { + type Marker = T::Marker; +} + +impl EntityExpr for Latest { + type Marker = T::Marker; +} + +impl EntityExpr for SnapshotAt { + type Marker = T::Marker; +} + +impl EntityExpr for SnapshotLatest { + type Marker = T::Marker; +} + impl CreateView for Layered { type View<'graph, G: GraphView + 'graph> = LayeredGraph; 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 72ffbe6469..0a7611065c 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 @@ -72,10 +72,10 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::model::{ - edge_expr::{EdgeExpr, EdgeOp}, + edge_expr::{EdgeExpr, EdgeOp, ops::TemporalEdgePropOp}, filter_operator::Comparable, - node_filter::NodeFilter, - CreateView, Metadata, Property, + node_filter::{NodeFilter, NodeFilterFactory}, + EdgeFilterFactory, CreateView, Metadata, Property, }, }, errors::GraphError, @@ -263,6 +263,7 @@ impl NodeExpr for Prop { macro_rules! impl_node_expr_for_numeric { ($prim:ty, $variant:ident) => { impl EntityExpr for $prim { + type Marker = ConstFilter; fn prop_type(&self) -> PropType { PropType::$variant } @@ -356,7 +357,9 @@ impl NodeExpr for DegreeExpr { } } -impl EntityExpr for Property {} +impl EntityExpr for Property { + type Marker = NodeFilter; +} impl NodeExpr for Property { fn create_node_op<'g, G: GraphView + 'g>( @@ -371,7 +374,9 @@ impl NodeExpr for Property { } } -impl EntityExpr for Metadata {} +impl EntityExpr for Metadata { + type Marker = NodeFilter; +} impl NodeExpr for Metadata { fn create_node_op<'g, G: GraphView + 'g>( @@ -387,40 +392,34 @@ impl NodeExpr for Metadata { } // ───────────────────────────────────────────────────────────────────────────── -// Temporal property expression — returns Prop::List of all values in the window +// TemporalExpr — all temporal values of a property over the view window +// +// Unified replacement for TemporalPropertyExpr (node) and TemporalEdgePropExpr (edge). +// Implements NodeExpr when E: NodeFilterFactory, EdgeExpr when E: EdgeFilterFactory. // ───────────────────────────────────────────────────────────────────────────── /// All temporal values of a named property over the current view window. /// -/// Produces `Some(Prop::List([...]))` of every recorded value within the view. -/// -/// Not constructed directly — obtained from `NodeTemporalPropOps::into_expr()`, -/// or implicitly via `.sum()` / `.any()` / etc. on `TemporalProp`: -/// -/// ```rust,ignore -/// // NodeFilter.property("score").temporal() returns TemporalProp, not this type. -/// // TemporalPropertyExpr is produced implicitly by NodeTemporalPropOps methods: -/// // .gt(10i64).any() → BinaryCmpNodeFilter>, Prop> -/// // .sum().gt(100i64) → BinaryCmpNodeFilter>, i64> -/// ``` +/// Implements `NodeExpr` when `E: NodeFilterFactory` and `EdgeExpr` when `E: EdgeFilterFactory`. +/// Produced implicitly by `TemporalPropOps` methods (`.sum()`, `.gt()`, etc.) — not usually +/// constructed directly. #[derive(Clone)] -pub struct TemporalPropertyExpr { - pub view_expr: E, - pub name: String, +pub struct TemporalExpr { + pub(crate) view_expr: E, + pub(crate) name: String, } -impl TemporalPropertyExpr { +impl TemporalExpr { pub fn new(name: impl Into) -> Self { - Self { - view_expr: NodeFilter, - name: name.into(), - } + Self { view_expr: NodeFilter, name: name.into() } } } -impl EntityExpr for TemporalPropertyExpr {} +impl EntityExpr for TemporalExpr { + type Marker = E::Marker; +} -impl NodeExpr for TemporalPropertyExpr { +impl NodeExpr for TemporalExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -430,9 +429,21 @@ impl NodeExpr for TemporalPropert .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 }.map(|a| Some(a)), - )) + Ok(Arc::new(TemporalNodePropOp { graph, prop_id }.map(|a| Some(a)))) + } +} + +impl EdgeExpr for TemporalExpr { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let (prop_id, _) = graph + .edge_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(TemporalEdgePropOp { graph, prop_id })) } } @@ -456,7 +467,9 @@ macro_rules! impl_agg_expr { #[derive(Clone)] pub struct $expr(pub E); - impl EntityExpr for $expr {} + impl EntityExpr for $expr { + type Marker = E::Marker; + } impl NodeExpr for $expr { fn create_node_op<'g, G: GraphView + 'g>( 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 5c75ef485e..b816112412 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 @@ -64,9 +64,8 @@ use crate::{ errors::GraphError, prelude::{GraphViewOps, NodeFilter}, }; -use neo4rs::Node; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; -use std::{marker::PhantomData, sync::Arc}; +use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpNodeFilter — binary expression filter // ───────────────────────────────────────────────────────────────────────────── @@ -215,14 +214,15 @@ where /// → UnaryNodeOp { inner: NodePropOp(prop_id=N), op: IsSome } /// ``` #[derive(Clone)] -pub struct UnaryFilter { +pub struct UnaryFilter { pub expr: E, pub op: UnaryOp, + pub entity: Entity, } -impl ComposableFilter for UnaryFilter {} +impl ComposableFilter for UnaryFilter {} -impl CreateFilter for UnaryFilter +impl CreateFilter for UnaryFilter where E: NodeExpr, { @@ -254,7 +254,7 @@ where } } -impl TryAsCompositeFilter for UnaryFilter +impl TryAsCompositeFilter for UnaryFilter where E: NodeExpr, { @@ -294,21 +294,22 @@ where /// → StringNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(Str("foo"))), op: Contains } /// ``` #[derive(Clone)] -pub struct StringFilter { +pub struct StringFilter { pub left: L, pub op: StringOp, pub right: R, + pub entity: Entity, } -impl StringFilter { - pub fn new(left: L, op: StringOp, right: R) -> Self { - Self { left, op, right } +impl StringFilter { + pub fn new(left: L, op: StringOp, right: R, entity: Entity) -> Self { + Self { left, op, right, entity } } } -impl ComposableFilter for StringFilter {} +impl ComposableFilter for StringFilter {} -impl CreateFilter for StringFilter { +impl CreateFilter for StringFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; @@ -343,7 +344,7 @@ impl CreateFilter for StringFilter { } } -impl TryAsCompositeFilter for StringFilter +impl TryAsCompositeFilter for StringFilter where L: NodeExpr, R: NodeExpr, @@ -367,19 +368,21 @@ where // PropValueSetFilter — is_in / is_not_in for aggregated Option values // ───────────────────────────────────────────────────────────────────────────── -/// A node filter that checks whether an aggregated scalar property value is in -/// (or not in) a fixed set of `Prop` values. Uses linear scan because `Prop` -/// may contain floats that don't implement `Hash`. +/// A filter that checks whether a scalar property value is in (or not in) a fixed set. +/// +/// Uses linear scan because `Prop` may contain floats that don't implement `Hash`. +/// Works for both nodes (`Entity = NodeFilter`) and edges (`Entity = EdgeFilter`). #[derive(Clone)] -pub struct PropValueSetFilter { +pub struct PropValueSetFilter { pub(crate) expr: E, pub(crate) values: Vec, pub(crate) op: SetOp, + pub(crate) entity: Entity, } -impl ComposableFilter for PropValueSetFilter {} +impl ComposableFilter for PropValueSetFilter {} -impl CreateFilter for PropValueSetFilter { +impl CreateFilter for PropValueSetFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; type NodeFilter<'graph, G: GraphView + 'graph> = PropValueSetNodeOp<'graph>; @@ -409,7 +412,7 @@ impl CreateFilter for PropValueSetFilter { } } -impl TryAsCompositeFilter for PropValueSetFilter { +impl TryAsCompositeFilter for PropValueSetFilter { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) } @@ -463,38 +466,7 @@ impl TemporalProp { } // ───────────────────────────────────────────────────────────────────────────── -// NodePropertyExprOps — fluent comparison API for node-side property expressions -// ───────────────────────────────────────────────────────────────────────────── - -pub trait NodePropertyExprOps: EntityExpr + Sized { - fn is_in(self, values: impl IntoIterator) -> PropValueSetFilter { - PropValueSetFilter { - expr: self, - values: values.into_iter().collect(), - op: SetOp::IsIn, - } - } - fn is_not_in(self, values: impl IntoIterator) -> PropValueSetFilter { - PropValueSetFilter { - expr: self, - values: values.into_iter().collect(), - op: SetOp::IsNotIn, - } - } -} - -impl NodePropertyExprOps - for PropertyExpr -{ -} - -impl NodePropertyExprOps - for MetadataExpr -{ -} - -// ───────────────────────────────────────────────────────────────────────────── -// NodeExprFilterOps — comparison and set operators on NodeExpr +// EntityExprFilterOps — comparison and set operators on any EntityExpr // ───────────────────────────────────────────────────────────────────────────── /// Comparison, string, set, and presence operators on any [`NodeExpr`]. @@ -511,45 +483,45 @@ impl NodeProp /// NodeFilter.property("score").temporal().gt(10i64).any() /// ``` pub trait EntityExprFilterOps: EntityExpr + Sized { - fn gt(self, rhs: R) -> BinaryCmpFilter { + fn gt(self, rhs: R) -> BinaryCmpFilter { // TODO: validate ops - BinaryCmpFilter::new(self, BinaryOp::Gt, rhs) + BinaryCmpFilter::new(self, BinaryOp::Gt, rhs, Self::Marker::default()) } - fn ge(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Ge, rhs) + fn ge(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Ge, rhs, Self::Marker::default()) } - fn lt(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Lt, rhs) + fn lt(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Lt, rhs, Self::Marker::default()) } - fn le(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Le, rhs) + fn le(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Le, rhs, Self::Marker::default()) } - fn eq(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Eq, rhs) + fn eq(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Eq, rhs, Self::Marker::default()) } - fn ne(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Ne, rhs) + fn ne(self, rhs: R) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Ne, rhs, Self::Marker::default()) } - fn starts_with(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::StartsWith, rhs) + fn starts_with(self, rhs: R) -> StringFilter { + StringFilter::new(self, StringOp::StartsWith, rhs, Self::Marker::default()) } - fn ends_with(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::EndsWith, rhs) + fn ends_with(self, rhs: R) -> StringFilter { + StringFilter::new(self, StringOp::EndsWith, rhs, Self::Marker::default()) } - fn contains(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::Contains, rhs) + fn contains(self, rhs: R) -> StringFilter { + StringFilter::new(self, StringOp::Contains, rhs, Self::Marker::default()) } - fn not_contains(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::NotContains, rhs) + fn not_contains(self, rhs: R) -> StringFilter { + StringFilter::new(self, StringOp::NotContains, rhs, Self::Marker::default()) } fn fuzzy_search( @@ -557,7 +529,7 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringFilter { + ) -> StringFilter { StringFilter::new( self, StringOp::FuzzySearch { @@ -565,33 +537,62 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { prefix_match, }, rhs, + Self::Marker::default(), ) } - fn is_some(self) -> UnaryFilter { + fn is_some(self) -> UnaryFilter { UnaryFilter { expr: self, op: UnaryOp::IsSome, + entity: Self::Marker::default(), } } - fn is_none(self) -> UnaryFilter { + fn is_none(self) -> UnaryFilter { UnaryFilter { expr: self, op: UnaryOp::IsNone, + entity: Self::Marker::default(), + } + } + + fn is_in(self, values: impl IntoIterator) -> PropValueSetFilter { + PropValueSetFilter { + expr: self, + values: values.into_iter().collect(), + op: SetOp::IsIn, + entity: Self::Marker::default(), } } - fn not(self) -> BinaryCmpFilter { + fn is_not_in(self, values: impl IntoIterator) -> PropValueSetFilter { + PropValueSetFilter { + expr: self, + values: values.into_iter().collect(), + op: SetOp::IsNotIn, + entity: Self::Marker::default(), + } + } + + fn is_true(self) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) + } + + fn is_false(self) -> BinaryCmpFilter { + BinaryCmpFilter::new(self, BinaryOp::Eq, Prop::Bool(false), Self::Marker::default()) + } + + fn not(self) -> BinaryCmpFilter { self.eq(Prop::Bool(false)) } - fn any(self) -> BinaryCmpFilter, Prop> { - BinaryCmpFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true)) + fn any(self) -> BinaryCmpFilter, Prop, Self::Marker> { + BinaryCmpFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) } - fn all(self) -> BinaryCmpFilter, Prop> { - BinaryCmpFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true)) + fn all(self) -> BinaryCmpFilter, Prop, Self::Marker> { + BinaryCmpFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) } } @@ -605,16 +606,17 @@ impl EntityExprFilterOps for E {} // temporal().is_in([...]).any() // ───────────────────────────────────────────────────────────────────────────── -impl EntityExpr for BinaryCmpFilter { +impl EntityExpr for BinaryCmpFilter { + type Marker = E; fn prop_type(&self) -> PropType { // TODO: depending on the types of left and right, we should figure out the type to return here PropType::Empty } } -impl NodeExprMarker for BinaryCmpFilter {} +impl NodeExprMarker for BinaryCmpFilter {} -impl NodeExpr for BinaryCmpFilter { +impl NodeExpr for BinaryCmpFilter { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -629,14 +631,14 @@ impl NodeExpr for BinaryCmpFilter { } } -impl EntityExpr for StringFilter { +impl EntityExpr for StringFilter { + type Marker = Entity; fn prop_type(&self) -> PropType { - // TODO: depending on the types of left and right, we should figure out the type to return here PropType::Empty } } -impl NodeExpr for StringFilter { +impl NodeExpr for StringFilter { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -651,14 +653,18 @@ impl NodeExpr for StringFilter { } } -impl EntityExpr for PropValueSetFilter { +impl EntityExpr for PropValueSetFilter { + type Marker = Entity; fn prop_type(&self) -> PropType { - // TODO: Figure the type to return PropType::Empty } } -impl NodeExpr for PropValueSetFilter { +impl EntityExpr for UnaryFilter { + type Marker = Entity; +} + +impl NodeExpr for PropValueSetFilter { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, 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 index df8da2417b..c65cefaa02 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -1,11 +1,7 @@ use crate::{ db::{ api::{state::ops::NodeOp, view::internal::GraphView}, - graph::views::filter::model::{ - filter_operator::{BinaryOp, StringOp}, - node_filter::NodeFilterFactory, - CreateView, - }, + graph::views::filter::model::CreateView, }, errors::GraphError, }; @@ -20,7 +16,7 @@ pub mod ops; mod tests; pub use super::{Metadata, Property}; -use crate::db::graph::views::filter::model::edge_expr::EdgeOp; +use crate::db::graph::views::filter::model::{edge_expr::EdgeOp, node_filter::NodeFilter}; pub use exprs::*; pub use filters::*; pub use ops::*; @@ -74,125 +70,83 @@ pub trait EntityExpr: Clone + Send + Sync + 'static { pub(crate) trait NodeExprMarker {} // ───────────────────────────────────────────────────────────────────────────── -// NodeTemporalPropOps — aggregation and direct comparison on TemporalProp +// TemporalPropOps — unified aggregation and comparison on TemporalProp // ───────────────────────────────────────────────────────────────────────────── -/// Aggregation and comparison operators on `TemporalProp` when `E: NodeFilterFactory`. -/// -/// Provides both aggregation (`.sum()`, `.avg()`, `.len()`, …) and direct -/// element-wise comparison (`.gt()`, `.eq()`, `.contains()`, …) so users -/// never have to call `.into_expr()` explicitly: +/// Aggregation and comparison operators on `TemporalProp`, unified for both +/// node-side (`E: NodeFilterFactory`) and edge-side (`E: EdgeFilterFactory`). /// /// ```rust,ignore /// NodeFilter.property("score").temporal().sum().gt(100i64) /// NodeFilter.property("score").temporal().gt(10i64).any() -/// NodeFilter.property("score").temporal().len().gt(3usize) -/// NodeFilter.property("label").temporal().contains("rock").any() +/// EdgeFilter.property("score").temporal().sum().gt(100i64) +/// EdgeFilter.property("score").temporal().gt(10i64).any() /// ``` -pub trait NodeTemporalPropOps: Sized { - type ViewExpr: CreateView + NodeFilterFactory + Clone + Send + Sync + 'static; +pub trait TemporalPropOps: Sized { + type ViewExpr: CreateView + EntityExpr + Clone + Send + Sync + 'static; fn into_temporal_parts(self) -> (Self::ViewExpr, String); - fn into_expr(self) -> TemporalPropertyExpr { + fn into_expr(self) -> TemporalExpr { let (view_expr, name) = self.into_temporal_parts(); - TemporalPropertyExpr { view_expr, name } - } - fn sum(self) -> SumExpr> { - SumExpr(self.into_expr()) - } - fn avg(self) -> AvgExpr> { - AvgExpr(self.into_expr()) - } - fn min(self) -> MinExpr> { - MinExpr(self.into_expr()) - } - fn max(self) -> MaxExpr> { - MaxExpr(self.into_expr()) - } - fn first(self) -> FirstExpr> { - FirstExpr(self.into_expr()) - } - fn last(self) -> LastExpr> { - LastExpr(self.into_expr()) - } - fn len(self) -> LenExpr> { - LenExpr(self.into_expr()) - } - fn any(self) -> AnyExpr> { - AnyExpr(self.into_expr()) - } - fn all(self) -> AllExpr> { - AllExpr(self.into_expr()) - } + TemporalExpr { view_expr, name } + } + fn sum(self) -> SumExpr> { SumExpr(self.into_expr()) } + fn avg(self) -> AvgExpr> { AvgExpr(self.into_expr()) } + fn min(self) -> MinExpr> { MinExpr(self.into_expr()) } + fn max(self) -> MaxExpr> { MaxExpr(self.into_expr()) } + fn first(self) -> FirstExpr> { FirstExpr(self.into_expr()) } + fn last(self) -> LastExpr> { LastExpr(self.into_expr()) } + fn len(self) -> LenExpr> { LenExpr(self.into_expr()) } + fn any(self) -> AnyExpr> { AnyExpr(self.into_expr()) } + fn all(self) -> AllExpr> { AllExpr(self.into_expr()) } - fn gt(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Gt, rhs) + fn gt(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + self.into_expr().gt(rhs) } - fn ge(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Ge, rhs) + fn ge(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + self.into_expr().ge(rhs) } - fn lt(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Lt, rhs) + fn lt(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + self.into_expr().lt(rhs) } - fn le(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Le, rhs) + fn le(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + self.into_expr().le(rhs) } - fn eq(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, rhs) + fn eq(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + self.into_expr().eq(rhs) } - fn ne(self, rhs: R) -> BinaryCmpFilter, R> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Ne, rhs) + fn ne(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + self.into_expr().ne(rhs) } - fn contains( - self, - rhs: R, - ) -> StringFilter, R> { - StringFilter::new(self.into_expr(), StringOp::Contains, rhs) + fn contains(self, rhs: R) -> StringFilter, R, ::Marker> { + self.into_expr().contains(rhs) } - fn starts_with( - self, - rhs: R, - ) -> StringFilter, R> { - StringFilter::new(self.into_expr(), StringOp::StartsWith, rhs) + fn starts_with(self, rhs: R) -> StringFilter, R, ::Marker> { + self.into_expr().starts_with(rhs) } - fn ends_with( - self, - rhs: R, - ) -> StringFilter, R> { - StringFilter::new(self.into_expr(), StringOp::EndsWith, rhs) + fn ends_with(self, rhs: R) -> StringFilter, R, ::Marker> { + self.into_expr().ends_with(rhs) } - fn not_contains( - self, - rhs: R, - ) -> StringFilter, R> { - StringFilter::new(self.into_expr(), StringOp::NotContains, rhs) + fn not_contains(self, rhs: R) -> StringFilter, R, ::Marker> { + self.into_expr().not_contains(rhs) } - fn fuzzy_search( + fn fuzzy_search( self, rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringFilter, R> { - StringFilter::new( - self.into_expr(), - StringOp::FuzzySearch { - levenshtein_distance, - prefix_match, - }, - rhs, - ) - } - fn is_true(self) -> BinaryCmpFilter, Prop> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(true)) - } - fn is_false(self) -> BinaryCmpFilter, Prop> { - BinaryCmpFilter::new(self.into_expr(), BinaryOp::Eq, Prop::Bool(false)) + ) -> StringFilter, R, ::Marker> { + self.into_expr().fuzzy_search(rhs, levenshtein_distance, prefix_match) + } + fn is_true(self) -> BinaryCmpFilter, Prop, ::Marker> { + self.into_expr().is_true() + } + fn is_false(self) -> BinaryCmpFilter, Prop, ::Marker> { + self.into_expr().is_false() } } -impl NodeTemporalPropOps - for TemporalProp -{ +impl TemporalPropOps for TemporalProp { type ViewExpr = E; fn into_temporal_parts(self) -> (E, String) { (self.view_expr, self.name) 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 index 7b586f0a15..0bf82887e8 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -196,7 +196,7 @@ macro_rules! impl_agg_entity_op { } impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { - aggregate_values(vals, |pi| { + aggregate_values(vals, &|pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { return None; @@ -228,7 +228,7 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { }); impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { - aggregate_values(vals, |pi| { + aggregate_values(vals, &|pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { return None; @@ -257,35 +257,35 @@ impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { }) }); impl_agg_entity_op!(MinNodeOp, MinEdgeOp, |vals| { - aggregate_values(vals, |pi| { + aggregate_values(vals, &|pi| { let mut it = pi; let first = it.next()?; it.fold(Some(first), |acc, v| acc.and_then(|a| a.min(v))) }) }); impl_agg_entity_op!(MaxNodeOp, MaxEdgeOp, |vals| { - aggregate_values(vals, |pi| { + aggregate_values(vals, &|pi| { let mut it = pi; let first = it.next()?; it.fold(Some(first), |acc, v| acc.and_then(|a| a.max(v))) }) }); impl_agg_entity_op!(FirstNodeOp, FirstEdgeOp, |vals| { - aggregate_values(vals, |mut pi| pi.next()) + aggregate_values(vals, &|mut pi| pi.next()) }); impl_agg_entity_op!(LastNodeOp, LastEdgeOp, |vals| { - aggregate_values(vals, |pi| pi.last()) + aggregate_values(vals, &|pi| pi.last()) }); impl_agg_entity_op!(LenNodeOp, LenEdgeOp, |vals| { - aggregate_values(vals, |pi| Some(pi.count().into_prop())) + aggregate_values(vals, &|pi| Some(pi.count().into_prop())) }); impl_agg_entity_op!(AnyNodeOp, AnyEdgeOp, |vals| { - aggregate_values(vals, |mut pi| { + aggregate_values(vals, &|mut pi| { Some(Prop::Bool(pi.any(|r| r == Prop::Bool(true)))) }) }); impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { - aggregate_values(vals, |mut pi| { + aggregate_values(vals, &|mut pi| { Some(Prop::Bool(pi.all(|r| r == Prop::Bool(true)))) }) }); 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 dec7ac84e3..93dbdcf69f 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 @@ -208,7 +208,7 @@ 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); + let filter = TemporalExpr::new("score").any().eq(5i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -216,7 +216,7 @@ fn temporal_any_eq_selects_nodes_with_matching_value() { 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); + let filter = TemporalExpr::new("score").any().gt(4i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -224,7 +224,7 @@ fn temporal_any_gt_selects_nodes_with_at_least_one_value_above_threshold() { 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); + let filter = TemporalExpr::new("score").any().gt(1i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); } @@ -234,7 +234,7 @@ fn temporal_any_gt_both_nodes_qualify() { 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); + let filter = TemporalExpr::new("score").all().gt(0i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); } @@ -242,7 +242,7 @@ fn temporal_all_gt_requires_every_value() { 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); + let filter = TemporalExpr::new("score").all().gt(4i64); assert!(temporal_filtered_names(filter, g).is_empty()); } @@ -250,7 +250,7 @@ fn temporal_all_gt_rejects_if_any_value_fails() { 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 filter = TemporalExpr::new("score").all().ge(0i64); let names = temporal_filtered_names(filter, g); assert!(!names.contains(&"carol".to_string())); } @@ -261,7 +261,7 @@ fn temporal_all_requires_non_empty_sequence() { 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); + let filter = TemporalExpr::new("score").sum().gt(10i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -269,7 +269,7 @@ fn temporal_sum_gt_threshold() { fn temporal_sum_eq() { // bob sum = 5 → sum == 5 → bob only let g = build_temporal_graph(); - let filter = TemporalPropertyExpr::new("score").sum().eq(5i64); + let filter = TemporalExpr::new("score").sum().eq(5i64); assert_eq!(temporal_filtered_names(filter, g), vec!["bob"]); } @@ -279,7 +279,7 @@ fn temporal_sum_eq() { 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); + let filter = TemporalExpr::new("score").first().eq(1i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -287,7 +287,7 @@ fn temporal_first_value() { fn temporal_last_value() { // alice last = 10 → last > 9 → alice only let g = build_temporal_graph(); - let filter = TemporalPropertyExpr::new("score").last().gt(9i64); + let filter = TemporalExpr::new("score").last().gt(9i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -297,7 +297,7 @@ fn temporal_last_value() { 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); + let filter = TemporalExpr::new("score").len().eq(3usize); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -305,7 +305,7 @@ fn temporal_len_count() { 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); + let filter = TemporalExpr::new("score").len().ge(2usize); assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); } @@ -322,9 +322,9 @@ fn node_filter_temporal_property_entry_point() { #[test] fn temporal_expr_ops_blanket_any() { - // Using the blanket TemporalExprOps on TemporalPropertyExpr directly + // Using TemporalPropOps blanket on TemporalExpr directly let g = build_temporal_graph(); - let filter = TemporalPropertyExpr::new("score").any().eq(10i64); + let filter = TemporalExpr::new("score").any().eq(10i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index d8faa9f526..91fccf292b 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -10,14 +10,14 @@ enum ValueType { pub fn aggregate_values( vals: Option, - op: impl Fn(Box>) -> Option, + op: &dyn Fn(Box>) -> Option, ) -> Option { match vals? { Prop::List(x) => match x.dtype() { PropType::List(_) => { let s = x .iter_all() - .map(|y| aggregate_values(y, &op)) + .map(|y| aggregate_values(y, op)) .flatten() .collect(); Some(Prop::List(s)) diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 530f68626b..70c5d3b422 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -148,8 +148,7 @@ pub mod prelude { pub use crate::db::graph::views::filter::model::{ filter::Filter, property_filter::PropertyFilter, - EntityExprFilterOps, NodePropertyExprOps, NodeTemporalPropOps, - EdgeExprFilterOps, EdgePropertyExprOps, EdgeTemporalPropOps, + EntityExprFilterOps, TemporalPropOps, }; pub use crate::db::graph::views::{ From 49acda0bde291e9ed2fd38ed01f17ac544350011 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:52:16 +0100 Subject: [PATCH 034/155] fix tests --- .../graph/views/filter/model/node_expr/ops.rs | 7 ++- .../views/filter/model/node_expr/tests.rs | 46 +++++++++---------- 2 files changed, 29 insertions(+), 24 deletions(-) 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 index 0bf82887e8..ea4b9c8390 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -286,7 +286,12 @@ impl_agg_entity_op!(AnyNodeOp, AnyEdgeOp, |vals| { }); impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { aggregate_values(vals, &|mut pi| { - Some(Prop::Bool(pi.all(|r| r == Prop::Bool(true)))) + let mut saw_any = false; + let all_true = pi.all(|r| { + saw_any = true; + r == Prop::Bool(true) + }); + Some(Prop::Bool(saw_any && all_true)) }) }); 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 93dbdcf69f..ea855de0ac 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 @@ -148,7 +148,7 @@ fn total_gt_in_degree_selects_nodes_with_outgoing_edges() { #[test] fn const_expr_works() { - let filter = BinaryCmpFilter::new(ConstExpr(2usize), BinaryOp::Eq, ConstExpr(2usize)); + let filter = BinaryCmpFilter::new(ConstExpr(2usize), BinaryOp::Eq, ConstExpr(2usize), NodeFilter); let g = build_test_graph(); assert_eq!(filtered_names(filter, g), vec!["a", "b", "c"]); } @@ -208,7 +208,7 @@ 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 = TemporalExpr::new("score").any().eq(5i64); + let filter = NodeFilter.property("score").temporal().eq(5i64).any(); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -216,7 +216,7 @@ fn temporal_any_eq_selects_nodes_with_matching_value() { 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 = TemporalExpr::new("score").any().gt(4i64); + let filter = NodeFilter.property("score").temporal().gt(4i64).any(); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -224,7 +224,7 @@ fn temporal_any_gt_selects_nodes_with_at_least_one_value_above_threshold() { fn temporal_any_gt_both_nodes_qualify() { // any > 1 → alice (5, 10), bob (2, 3) — both qualify let g = build_temporal_graph(); - let filter = TemporalExpr::new("score").any().gt(1i64); + let filter = NodeFilter.property("score").temporal().gt(1i64).any(); assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); } @@ -234,7 +234,7 @@ fn temporal_any_gt_both_nodes_qualify() { 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 = TemporalExpr::new("score").all().gt(0i64); + let filter = NodeFilter.property("score").temporal().gt(0i64).all(); assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); } @@ -242,7 +242,7 @@ fn temporal_all_gt_requires_every_value() { 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 = TemporalExpr::new("score").all().gt(4i64); + let filter = NodeFilter.property("score").temporal().gt(4i64).all(); assert!(temporal_filtered_names(filter, g).is_empty()); } @@ -250,7 +250,7 @@ fn temporal_all_gt_rejects_if_any_value_fails() { fn temporal_all_requires_non_empty_sequence() { // carol has no score → "all" over empty sequence returns false let g = build_temporal_graph(); - let filter = TemporalExpr::new("score").all().ge(0i64); + let filter = NodeFilter.property("score").temporal().ge(0i64).all(); let names = temporal_filtered_names(filter, g); assert!(!names.contains(&"carol".to_string())); } @@ -261,7 +261,7 @@ fn temporal_all_requires_non_empty_sequence() { fn temporal_sum_gt_threshold() { // alice sum = 16, bob sum = 5 → sum > 10 → alice only let g = build_temporal_graph(); - let filter = TemporalExpr::new("score").sum().gt(10i64); + let filter = NodeFilter.property("score").temporal().sum().gt(10i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -269,7 +269,7 @@ fn temporal_sum_gt_threshold() { fn temporal_sum_eq() { // bob sum = 5 → sum == 5 → bob only let g = build_temporal_graph(); - let filter = TemporalExpr::new("score").sum().eq(5i64); + let filter = NodeFilter.property("score").temporal().sum().eq(5i64); assert_eq!(temporal_filtered_names(filter, g), vec!["bob"]); } @@ -279,7 +279,7 @@ fn temporal_sum_eq() { fn temporal_first_value() { // alice first = 1, bob first = 2 → first == 1 → alice only let g = build_temporal_graph(); - let filter = TemporalExpr::new("score").first().eq(1i64); + let filter = NodeFilter.property("score").temporal().first().eq(1i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -287,7 +287,7 @@ fn temporal_first_value() { fn temporal_last_value() { // alice last = 10 → last > 9 → alice only let g = build_temporal_graph(); - let filter = TemporalExpr::new("score").last().gt(9i64); + let filter = NodeFilter.property("score").temporal().last().gt(9i64); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -297,7 +297,7 @@ fn temporal_last_value() { fn temporal_len_count() { // alice has 3 updates, bob has 2 → len == 3 → alice only let g = build_temporal_graph(); - let filter = TemporalExpr::new("score").len().eq(3usize); + let filter = NodeFilter.property("score").temporal().len().eq(3usize); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -305,7 +305,7 @@ fn temporal_len_count() { fn temporal_len_ge_2() { // alice (3), bob (2) both have len >= 2; carol has 0 let g = build_temporal_graph(); - let filter = TemporalExpr::new("score").len().ge(2usize); + let filter = NodeFilter.property("score").temporal().len().ge(2usize); assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); } @@ -314,7 +314,7 @@ fn temporal_len_ge_2() { #[test] fn node_filter_temporal_property_entry_point() { let g = build_temporal_graph(); - let filter = NodeFilter.property("score").temporal().any().eq(5i64); + let filter = NodeFilter.property("score").temporal().eq(5i64).any(); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -324,7 +324,7 @@ fn node_filter_temporal_property_entry_point() { fn temporal_expr_ops_blanket_any() { // Using TemporalPropOps blanket on TemporalExpr directly let g = build_temporal_graph(); - let filter = TemporalExpr::new("score").any().eq(10i64); + let filter = NodeFilter.property("score").temporal().eq(10i64).any(); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); } @@ -357,8 +357,8 @@ fn windowed_temporal_any_restricts_to_window() { .window(1, 2) .property("score") .temporal() - .any() - .eq(5i64); + .eq(5i64) + .any(); // window [1,2) shows t=1 only → alice has score=1, not 5 assert!(windowed_filtered_names(filter, g).is_empty()); } @@ -371,8 +371,8 @@ fn windowed_temporal_any_matches_in_window() { .window(2, 3) .property("score") .temporal() - .any() - .eq(5i64); + .eq(5i64) + .any(); assert_eq!(windowed_filtered_names(filter, g), vec!["alice"]); } @@ -461,8 +461,8 @@ fn layered_temporal_any_restricts_to_layer_a_updates() { .layer("layer_a") .property("score") .temporal() - .any() - .eq(5i64); + .eq(5i64) + .any(); assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); } @@ -475,8 +475,8 @@ fn layered_temporal_any_restricts_to_layer_b_updates() { .layer("layer_b") .property("score") .temporal() - .any() - .gt(2i64); + .gt(2i64) + .any(); assert_eq!(layered_filtered_names(filter, g), vec!["bob"]); } From 5ea726e82bc7e0e2fb16bf7af354a0a0ffe80212 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:36:15 +0100 Subject: [PATCH 035/155] fix tests --- raphtory-tests/tests/views_test.rs | 40 +++++++-------- .../graph/views/filter/model/latest_filter.rs | 32 ++++++++++-- .../views/filter/model/layered_filter.rs | 29 ++++++++++- .../src/db/graph/views/filter/model/mod.rs | 17 ------- .../views/filter/model/snapshot_filter.rs | 50 ++++++++++++++++++- .../views/filter/model/windowed_filter.rs | 31 ++++++++++-- 6 files changed, 152 insertions(+), 47 deletions(-) diff --git a/raphtory-tests/tests/views_test.rs b/raphtory-tests/tests/views_test.rs index 435df284a8..295b7c70b5 100644 --- a/raphtory-tests/tests/views_test.rs +++ b/raphtory-tests/tests/views_test.rs @@ -765,7 +765,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_for_node_name_eq() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::name().eq("N2"); + let filter = NodeFilter.name().eq("N2"); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -785,7 +785,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_name_eq() { - let filter = NodeFilter::name().eq("N2"); + let filter = NodeFilter.name().eq("N2"); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -806,7 +806,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_for_node_name_ne() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::name().ne("N2"); + let filter = NodeFilter.name().ne("N2"); let expected_results = vec!["N1", "N3", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -826,7 +826,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_name_ne() { - let filter = NodeFilter::name().ne("N2"); + let filter = NodeFilter.name().ne("N2"); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N14", "N15", "N3", "N5", "N6", "N7", "N8", "N9", ]; @@ -849,7 +849,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_for_node_name_in() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::name().is_in(vec!["N2"]); + let filter = NodeFilter.name().is_in(vec!["N2"]); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -866,7 +866,7 @@ mod test_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter::name().is_in(vec!["N2", "N5"]); + let filter = NodeFilter.name().is_in(vec!["N2", "N5"]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -886,7 +886,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_name_in() { - let filter = NodeFilter::name().is_in(vec!["N2"]); + let filter = NodeFilter.name().is_in(vec!["N2"]); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -903,7 +903,7 @@ mod test_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter::name().is_in(vec!["N2", "N5"]); + let filter = NodeFilter.name().is_in(vec!["N2", "N5"]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -924,7 +924,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_for_node_name_not_in() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::name().is_not_in(vec!["N5"]); + let filter = NodeFilter.name().is_not_in(vec!["N5"]); let expected_results = vec!["N1", "N2", "N3", "N6"]; assert_filter_nodes_results( init_graph, @@ -944,7 +944,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_name_not_in() { - let filter = NodeFilter::name().is_not_in(vec!["N5"]); + let filter = NodeFilter.name().is_not_in(vec!["N5"]); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N14", "N15", "N2", "N3", "N6", "N7", "N8", "N9", ]; @@ -967,7 +967,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_for_node_type_eq() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::node_type().eq("fire_nation"); + let filter = NodeFilter.node_type().eq("fire_nation"); let expected_results = vec!["N6"]; assert_filter_nodes_results( init_graph, @@ -987,7 +987,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_type_eq() { - let filter = NodeFilter::node_type().eq("fire_nation"); + let filter = NodeFilter.node_type().eq("fire_nation"); let expected_results = vec!["N6", "N8"]; assert_filter_nodes_results( init_graph, @@ -1008,7 +1008,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_for_node_type_ne() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::node_type().ne("fire_nation"); + let filter = NodeFilter.node_type().ne("fire_nation"); let expected_results = vec!["N1", "N2", "N3", "N5"]; assert_filter_nodes_results( init_graph, @@ -1028,7 +1028,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_type_ne() { - let filter = NodeFilter::node_type().ne("fire_nation"); + let filter = NodeFilter.node_type().ne("fire_nation"); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N14", "N15", "N2", "N3", "N5", "N7", "N9", ]; @@ -1051,7 +1051,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_for_node_type_in() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::node_type().is_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_in(vec!["fire_nation"]); let expected_results = vec!["N6"]; assert_filter_nodes_results( init_graph, @@ -1068,7 +1068,7 @@ mod test_filters_window_graph { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::node_type().is_in(vec!["fire_nation", "air_nomad"]); + let filter = NodeFilter.node_type().is_in(vec!["fire_nation", "air_nomad"]); let expected_results = vec!["N1", "N3", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -1088,7 +1088,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_type_in() { - let filter = NodeFilter::node_type().is_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_in(vec!["fire_nation"]); let expected_results = vec!["N6", "N8"]; assert_filter_nodes_results( init_graph, @@ -1105,7 +1105,7 @@ mod test_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter::node_type().is_in(vec!["fire_nation", "air_nomad"]); + let filter = NodeFilter.node_type().is_in(vec!["fire_nation", "air_nomad"]); let expected_results = vec!["N1", "N3", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -1126,7 +1126,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_for_node_type_not_in() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::node_type().is_not_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]); let expected_results = vec!["N1", "N2", "N3", "N5"]; assert_filter_nodes_results( init_graph, @@ -1146,7 +1146,7 @@ mod test_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_type_not_in() { - let filter = NodeFilter::node_type().is_not_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N14", "N15", "N2", "N3", "N5", "N7", "N9", ]; 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 08b88b390b..3755556d73 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -4,9 +4,15 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, - InternalViewWrapOps, TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, + is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + windowed_filter::Windowed, + CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -124,3 +130,23 @@ impl Wrap for Latest { Latest::new(value) } } + +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 1ec1fc6322..fea296197d 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -4,8 +4,13 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, + edge_filter::CompositeEdgeFilter, + is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, EdgeViewFilterOps, InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, @@ -127,3 +132,23 @@ impl Wrap for Layered { Layered::new(self.layer.clone(), value) } } + +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 fb06969045..6ecdabffe6 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -936,20 +936,3 @@ impl EdgeViewFilterOps for DynEdgeViewProps { } } -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/snapshot_filter.rs b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs index c56a923751..7f34bfd0aa 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -3,8 +3,14 @@ use crate::{ api::view::internal::GraphView, graph::views::filter::{ model::{ - edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, InternalViewWrapOps, + edge_filter::CompositeEdgeFilter, + is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + windowed_filter::Windowed, + CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, @@ -134,6 +140,26 @@ impl Wrap for SnapshotAt { } } +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, @@ -239,3 +265,23 @@ impl Wrap for SnapshotLatest { SnapshotLatest::new(value) } } + +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 5ecf957a2c..2ed1e931c7 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -4,9 +4,14 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, - InternalViewWrapOps, TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, + is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -152,3 +157,23 @@ impl CreateView for Windowed { Ok(inner.window(self.start.t(), self.end.t())) } } + +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()) + } +} From 7061d8b2c6b78bfc88aa7a61106c7426cae5528d Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:21:01 +0100 Subject: [PATCH 036/155] fix tests --- raphtory-tests/tests/test_filters.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 9e6cf3610d..6575b3640d 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -1670,8 +1670,8 @@ mod test_node_filter { }, errors::GraphError, prelude::{ - AdditionOps, Graph, GraphViewOps, IntoProp, NodeFilter, NodeStateOps, NodeViewOps, - TimeOps, NO_PROPS, + AdditionOps, EntityExprFilterOps, Graph, GraphViewOps, IntoProp, NodeFilter, + NodeFilterFactory, NodeStateOps, NodeViewOps, TimeOps, NO_PROPS, }, }; use raphtory_api::core::{entities::properties::prop::Prop, Direction}; @@ -3239,6 +3239,7 @@ mod test_node_property_filter { ComposableFilter, PropertyFilterFactory, ViewWrapOps, }; use raphtory_api::core::entities::properties::prop::Prop; + use raphtory::prelude::{EntityExprFilterOps, TemporalPropOps}; use raphtory_tests::assertions::{ assert_filter_nodes_results, assert_search_nodes_results, TestVariants, }; @@ -5751,7 +5752,7 @@ mod test_node_property_filter_agg { CreateFilter, }, }, - prelude::{AdditionOps, GraphViewOps, PropertyAdditionOps}, + prelude::{AdditionOps, GraphViewOps, PropertyAdditionOps, TemporalPropOps}, }; use raphtory_api::core::{ entities::properties::prop::{IntoProp, Prop}, @@ -10757,6 +10758,7 @@ mod test_edge_property_filter { }; use raphtory_api::core::entities::properties::prop::Prop; + use raphtory::prelude::{EntityExprFilterOps, TemporalPropOps}; use raphtory_tests::assertions::{ assert_filter_edges_results, assert_search_edges_results, TestGraphVariants, TestVariants, }; From ea7f63d1767ebd4335546ce8aa9f9edcdf027976 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:41:16 +0100 Subject: [PATCH 037/155] fix tests --- raphtory-tests/tests/test_filters.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 6575b3640d..add87fe0d2 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -402,11 +402,11 @@ mod test_property_semantics { let filter = NodeFilter.property("p1").ge(1u64); let graph = init_graph(Graph::new()); assert!(matches!( - graph.filter(filter.clone()).unwrap_err(), + graph.filter(filter.clone()).err().expect("expected PropertyMissingError"), GraphError::PropertyMissingError(ref name) if name == "p1" )); assert!(matches!( - graph.persistent_graph().filter(filter).unwrap_err(), + graph.persistent_graph().filter(filter).err().expect("expected PropertyMissingError"), GraphError::PropertyMissingError(ref name) if name == "p1" )); } @@ -953,11 +953,11 @@ mod test_property_semantics { let filter = EdgeFilter.property("p1").eq(1u64); let graph = init_graph(Graph::new()); assert!(matches!( - graph.filter(filter.clone()).unwrap_err(), + graph.filter(filter.clone()).err().expect("expected PropertyMissingError"), GraphError::PropertyMissingError(ref name) if name == "p1" )); assert!(matches!( - graph.persistent_graph().filter(filter).unwrap_err(), + graph.persistent_graph().filter(filter).err().expect("expected PropertyMissingError"), GraphError::PropertyMissingError(ref name) if name == "p1" )); } From eb2f92135913f3d47e98d6eb2e7f37113c45f3f4 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:45:59 +0100 Subject: [PATCH 038/155] remove degree temporal aggregation entries now caught at compile time --- raphtory-tests/tests/test_filters.rs | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index add87fe0d2..f096b31021 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -2190,33 +2190,6 @@ mod test_node_filter { NodeFilter.out_degree().contains("1"), NodeFilter.out_degree().not_contains("1"), NodeFilter.out_degree().fuzzy_search("1", 1, false), - NodeFilter.degree().any().eq(1u64), - NodeFilter.degree().all().eq(1u64), - NodeFilter.degree().len().gt(0u64), - NodeFilter.degree().sum().eq(1u64), - NodeFilter.degree().avg().eq(1u64), - NodeFilter.degree().min().eq(1u64), - NodeFilter.degree().max().eq(1u64), - NodeFilter.degree().first().eq(1u64), - NodeFilter.degree().last().eq(1u64), - NodeFilter.in_degree().any().eq(1u64), - NodeFilter.in_degree().all().eq(1u64), - NodeFilter.in_degree().len().gt(0u64), - NodeFilter.in_degree().sum().eq(1u64), - NodeFilter.in_degree().avg().eq(1u64), - NodeFilter.in_degree().min().eq(1u64), - NodeFilter.in_degree().max().eq(1u64), - NodeFilter.in_degree().first().eq(1u64), - NodeFilter.in_degree().last().eq(1u64), - NodeFilter.out_degree().any().eq(1u64), - NodeFilter.out_degree().all().eq(1u64), - NodeFilter.out_degree().len().gt(0u64), - NodeFilter.out_degree().sum().eq(1u64), - NodeFilter.out_degree().avg().eq(1u64), - NodeFilter.out_degree().min().eq(1u64), - NodeFilter.out_degree().max().eq(1u64), - NodeFilter.out_degree().first().eq(1u64), - NodeFilter.out_degree().last().eq(1u64), ]; for filter in invalid_filters { From 95b2b880181c05d4b60a6a79c5b6ebf22bf37f70 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:17:10 +0100 Subject: [PATCH 039/155] fix ordering of filters --- raphtory-tests/tests/test_filters.rs | 462 +++++++++++++-------------- 1 file changed, 231 insertions(+), 231 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index f096b31021..9b8b74d593 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -5725,7 +5725,7 @@ mod test_node_property_filter_agg { CreateFilter, }, }, - prelude::{AdditionOps, GraphViewOps, PropertyAdditionOps, TemporalPropOps}, + prelude::{AdditionOps, EntityExprFilterOps, GraphViewOps, PropertyAdditionOps, TemporalPropOps}, }; use raphtory_api::core::{ entities::properties::prop::{IntoProp, Prop}, @@ -7175,9 +7175,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .all() .sum() - .eq(Prop::U64(6)); + .eq(Prop::U64(6)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7187,9 +7187,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .all() .sum() - .eq(Prop::U64(6)); + .eq(Prop::U64(6)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7199,9 +7199,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .all() .sum() - .eq(Prop::U64(6)); + .eq(Prop::U64(6)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7211,9 +7211,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .all() .sum() - .eq(Prop::U64(6)); + .eq(Prop::U64(6)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7223,9 +7223,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .all() .sum() - .eq(Prop::I64(6)); + .eq(Prop::I64(6)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7235,9 +7235,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .all() .sum() - .eq(Prop::I64(0)); + .eq(Prop::I64(0)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7247,9 +7247,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .all() .sum() - .eq(Prop::F64(6.5)); + .eq(Prop::F64(6.5)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7259,9 +7259,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .all() .sum() - .eq(Prop::F64(90.0)); + .eq(Prop::F64(90.0)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7272,9 +7272,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .all() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7284,9 +7284,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .all() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7296,9 +7296,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .all() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7308,9 +7308,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .all() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7320,9 +7320,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .all() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7332,9 +7332,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .all() .avg() - .eq(Prop::F64(0.0)); + .eq(Prop::F64(0.0)) + .all(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } @@ -7344,9 +7344,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .all() .avg() - .eq(Prop::F64(2.1666666666666665)); + .eq(Prop::F64(2.1666666666666665)) + .all(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } @@ -7356,9 +7356,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .all() .avg() - .eq(Prop::F64(45.0)); + .eq(Prop::F64(45.0)) + .all(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } @@ -7369,9 +7369,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .all() .min() - .eq(Prop::U8(1)); + .eq(Prop::U8(1)) + .all(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -7381,9 +7381,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .all() .min() - .eq(Prop::U16(1)); + .eq(Prop::U16(1)) + .all(); let expected = vec!["n1", "n10"]; apply_assertion(filter, &expected); } @@ -7393,9 +7393,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .all() .min() - .eq(Prop::U32(1)); + .eq(Prop::U32(1)) + .all(); let expected = vec!["n1", "n10"]; apply_assertion(filter, &expected); } @@ -7405,9 +7405,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .all() .min() - .eq(Prop::U64(1)); + .eq(Prop::U64(1)) + .all(); let expected = vec!["n1", "n10", "n2", "n5"]; apply_assertion(filter, &expected); } @@ -7417,9 +7417,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .all() .min() - .eq(Prop::I32(-2)); + .eq(Prop::I32(-2)) + .all(); let expected = vec!["n6"]; apply_assertion(filter, &expected); } @@ -7429,9 +7429,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .all() .min() - .eq(Prop::I64(-3)); + .eq(Prop::I64(-3)) + .all(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } @@ -7441,9 +7441,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .all() .min() - .eq(Prop::F32(1.0)); + .eq(Prop::F32(1.0)) + .all(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -7453,9 +7453,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .all() .min() - .eq(Prop::F64(30.0)); + .eq(Prop::F64(30.0)) + .all(); let expected = vec!["n2"]; apply_assertion(filter, &expected); } @@ -7466,9 +7466,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .all() .max() - .eq(Prop::U8(3)); + .eq(Prop::U8(3)) + .all(); let expected = vec!["n10"]; apply_assertion(filter, &expected); } @@ -7478,9 +7478,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .all() .max() - .eq(Prop::U16(3)); + .eq(Prop::U16(3)) + .all(); let expected = vec!["n10"]; apply_assertion(filter, &expected); } @@ -7490,9 +7490,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .all() .max() - .eq(Prop::U32(3)); + .eq(Prop::U32(3)) + .all(); let expected = vec!["n10"]; apply_assertion(filter, &expected); } @@ -7502,9 +7502,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .all() .max() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .all(); let expected = vec!["n2"]; apply_assertion(filter, &expected); } @@ -7514,9 +7514,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .all() .max() - .eq(Prop::I32(3)); + .eq(Prop::I32(3)) + .all(); let expected = vec!["n10", "n6"]; apply_assertion(filter, &expected); } @@ -7526,9 +7526,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .all() .max() - .eq(Prop::I64(2)); + .eq(Prop::I64(2)) + .all(); let expected = vec!["n10"]; apply_assertion(filter, &expected); } @@ -7538,9 +7538,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .all() .max() - .eq(Prop::F32(3.5)); + .eq(Prop::F32(3.5)) + .all(); let expected = vec!["n10"]; apply_assertion(filter, &expected); } @@ -7550,9 +7550,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .all() .max() - .eq(Prop::F64(50.0)); + .eq(Prop::F64(50.0)) + .all(); let expected = vec!["n1", "n10", "n2"]; apply_assertion(filter, &expected); } @@ -7563,9 +7563,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .all() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .all(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } @@ -7575,9 +7575,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .all() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .all(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } @@ -7587,9 +7587,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .all() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .all(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } @@ -7599,9 +7599,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .all() .len() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .all(); let expected = vec!["n2"]; apply_assertion(filter, &expected); } @@ -7611,9 +7611,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .all() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .all(); let expected = vec!["n10", "n3", "n4", "n6"]; apply_assertion(filter, &expected); } @@ -7623,9 +7623,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .all() .len() - .eq(Prop::U64(2)); + .eq(Prop::U64(2)) + .all(); let expected = vec!["n5"]; apply_assertion(filter, &expected); } @@ -7635,9 +7635,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .all() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .all(); let expected = vec!["n10", "n3", "n4"]; apply_assertion(filter, &expected); } @@ -7647,9 +7647,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .all() .len() - .eq(Prop::U64(2)); + .eq(Prop::U64(2)) + .all(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } @@ -8145,18 +8145,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .any() .sum() - .eq(Prop::U64(6)); + .eq(Prop::U64(6)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u8s") .temporal() - .any() .sum() - .eq(Prop::U64(10)); + .eq(Prop::U64(10)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8166,18 +8166,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .any() .sum() - .eq(Prop::U64(6)); + .eq(Prop::U64(6)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u16s") .temporal() - .any() .sum() - .eq(Prop::U64(10)); + .eq(Prop::U64(10)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8187,18 +8187,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .any() .sum() - .eq(Prop::U64(6)); + .eq(Prop::U64(6)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u32s") .temporal() - .any() .sum() - .eq(Prop::U64(10)); + .eq(Prop::U64(10)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8208,18 +8208,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .any() .sum() - .eq(Prop::U64(6)); + .eq(Prop::U64(6)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u64s") .temporal() - .any() .sum() - .eq(Prop::U64(10)); + .eq(Prop::U64(10)) + .any(); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); } @@ -8229,18 +8229,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .any() .sum() - .eq(Prop::I64(6)); + .eq(Prop::I64(6)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i32s") .temporal() - .any() .sum() - .eq(Prop::I64(60)); + .eq(Prop::I64(60)) + .any(); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -8250,18 +8250,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .any() .sum() - .eq(Prop::I64(0)); + .eq(Prop::I64(0)) + .any(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i64s") .temporal() - .any() .sum() - .eq(Prop::I64(10)); + .eq(Prop::I64(10)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8271,18 +8271,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .any() .sum() - .eq(Prop::F64(6.5)); + .eq(Prop::F64(6.5)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_f32s") .temporal() - .any() .sum() - .eq(Prop::F64(60.0)); + .eq(Prop::F64(60.0)) + .any(); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -8292,18 +8292,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .any() .sum() - .eq(Prop::F64(90.0)); + .eq(Prop::F64(90.0)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_f64s") .temporal() - .any() .sum() - .eq(Prop::F64(120.0)); + .eq(Prop::F64(120.0)) + .any(); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); } @@ -8314,18 +8314,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .any() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u8s") .temporal() - .any() .avg() - .eq(Prop::F64(2.5)); + .eq(Prop::F64(2.5)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8335,18 +8335,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .any() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u16s") .temporal() - .any() .avg() - .eq(Prop::F64(2.5)); + .eq(Prop::F64(2.5)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8356,18 +8356,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .any() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u32s") .temporal() - .any() .avg() - .eq(Prop::F64(2.5)); + .eq(Prop::F64(2.5)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8377,18 +8377,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .any() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u64s") .temporal() - .any() .avg() - .eq(Prop::F64(2.5)); + .eq(Prop::F64(2.5)) + .any(); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); } @@ -8398,18 +8398,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .any() .avg() - .eq(Prop::F64(2.0)); + .eq(Prop::F64(2.0)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i32s") .temporal() - .any() .avg() - .eq(Prop::F64(2.5)); + .eq(Prop::F64(2.5)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8419,18 +8419,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .any() .avg() - .eq(Prop::F64(0.0)); + .eq(Prop::F64(0.0)) + .any(); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i64s") .temporal() - .any() .avg() - .eq(Prop::F64(2.5)); + .eq(Prop::F64(2.5)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8440,18 +8440,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .any() .avg() - .eq(Prop::F64(2.1666666666666665)); + .eq(Prop::F64(2.1666666666666665)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_f32s") .temporal() - .any() .avg() - .eq(Prop::F64(20.0)); + .eq(Prop::F64(20.0)) + .any(); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -8461,18 +8461,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .any() .avg() - .eq(Prop::F64(45.0)); + .eq(Prop::F64(45.0)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_f64s") .temporal() - .any() .avg() - .eq(Prop::F64(40.0)); + .eq(Prop::F64(40.0)) + .any(); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); } @@ -8483,9 +8483,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .any() .min() - .eq(Prop::U8(1)); + .eq(Prop::U8(1)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -8495,9 +8495,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .any() .min() - .eq(Prop::U16(1)); + .eq(Prop::U16(1)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -8507,9 +8507,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .any() .min() - .eq(Prop::U32(1)); + .eq(Prop::U32(1)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -8519,9 +8519,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .any() .min() - .eq(Prop::U64(1)); + .eq(Prop::U64(1)) + .any(); let expected = vec!["n1", "n10", "n2", "n3", "n4", "n5"]; apply_assertion(filter, &expected); } @@ -8531,18 +8531,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .any() .min() - .eq(Prop::I32(-2)); + .eq(Prop::I32(-2)) + .any(); let expected = vec!["n6"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i32s") .temporal() - .any() .min() - .eq(Prop::I32(10)); + .eq(Prop::I32(10)) + .any(); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -8552,18 +8552,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .any() .min() - .eq(Prop::I64(-3)); + .eq(Prop::I64(-3)) + .any(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i64s") .temporal() - .any() .min() - .eq(Prop::I64(1)); + .eq(Prop::I64(1)) + .any(); let expected = vec!["n1", "n5"]; apply_assertion(filter, &expected); } @@ -8573,18 +8573,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .any() .min() - .eq(Prop::F32(1.0)); + .eq(Prop::F32(1.0)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_f32s") .temporal() - .any() .min() - .eq(Prop::F32(10.0)); + .eq(Prop::F32(10.0)) + .any(); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -8594,18 +8594,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .any() .min() - .eq(Prop::F64(30.0)); + .eq(Prop::F64(30.0)) + .any(); let expected = vec!["n1", "n2", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_f64s") .temporal() - .any() .min() - .eq(Prop::F64(40.0)); + .eq(Prop::F64(40.0)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -8616,18 +8616,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .any() .max() - .eq(Prop::U8(3)); + .eq(Prop::U8(3)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u8s") .temporal() - .any() .max() - .eq(Prop::U8(4)); + .eq(Prop::U8(4)) + .any(); let expected = vec!["n1", "n3"]; apply_assertion(filter, &expected); } @@ -8637,18 +8637,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .any() .max() - .eq(Prop::U16(3)); + .eq(Prop::U16(3)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u16s") .temporal() - .any() .max() - .eq(Prop::U16(4)); + .eq(Prop::U16(4)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8658,18 +8658,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .any() .max() - .eq(Prop::U32(3)); + .eq(Prop::U32(3)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u32s") .temporal() - .any() .max() - .eq(Prop::U32(4)); + .eq(Prop::U32(4)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8679,18 +8679,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .any() .max() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .any(); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u64s") .temporal() - .any() .max() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); } @@ -8700,18 +8700,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .any() .max() - .eq(Prop::I32(3)); + .eq(Prop::I32(3)) + .any(); let expected = vec!["n1", "n10", "n3", "n4", "n6"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i32s") .temporal() - .any() .max() - .eq(Prop::I32(30)); + .eq(Prop::I32(30)) + .any(); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -8721,18 +8721,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .any() .max() - .eq(Prop::I64(2)); + .eq(Prop::I64(2)) + .any(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i64s") .temporal() - .any() .max() - .eq(Prop::I64(2)); + .eq(Prop::I64(2)) + .any(); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } @@ -8742,18 +8742,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .any() .max() - .eq(Prop::F32(3.5)); + .eq(Prop::F32(3.5)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_f32s") .temporal() - .any() .max() - .eq(Prop::F32(30.0)); + .eq(Prop::F32(30.0)) + .any(); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -8763,9 +8763,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .any() .max() - .eq(Prop::F64(50.0)); + .eq(Prop::F64(50.0)) + .any(); let expected = vec!["n1", "n10", "n2", "n3"]; apply_assertion(filter, &expected); } @@ -8776,18 +8776,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u8s") .temporal() - .any() .len() - .is_in(vec![Prop::U64(3)]); + .is_in(vec![Prop::U64(3)]) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u8s") .temporal() - .any() .len() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8797,18 +8797,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u16s") .temporal() - .any() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u16s") .temporal() - .any() .len() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8818,18 +8818,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u32s") .temporal() - .any() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u32s") .temporal() - .any() .len() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8839,18 +8839,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_u64s") .temporal() - .any() .len() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .any(); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_u64s") .temporal() - .any() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); } @@ -8860,18 +8860,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i32s") .temporal() - .any() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .any(); let expected = vec!["n1", "n10", "n3", "n4", "n6"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i32s") .temporal() - .any() .len() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8881,18 +8881,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_i64s") .temporal() - .any() .len() - .eq(Prop::U64(2)); + .eq(Prop::U64(2)) + .any(); let expected = vec!["n5"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_i64s") .temporal() - .any() .len() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8902,18 +8902,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .any() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_f32s") .temporal() - .any() .len() - .eq(Prop::U64(4)); + .eq(Prop::U64(4)) + .any(); let expected = vec!["n1"]; apply_assertion(filter, &expected); } @@ -8923,18 +8923,18 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f64s") .temporal() - .any() .len() - .eq(Prop::U64(2)); + .eq(Prop::U64(2)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); let filter = NodeFilter .property("p_f64s") .temporal() - .any() .len() - .eq(Prop::U64(3)); + .eq(Prop::U64(3)) + .any(); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); } From a81ee5d0d4241e0b4891da950424a3ac19c28d44 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:19:25 +0100 Subject: [PATCH 040/155] fix tests --- raphtory-tests/tests/views_test.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/raphtory-tests/tests/views_test.rs b/raphtory-tests/tests/views_test.rs index 295b7c70b5..6dc1c6bec7 100644 --- a/raphtory-tests/tests/views_test.rs +++ b/raphtory-tests/tests/views_test.rs @@ -552,6 +552,7 @@ mod test_filters_window_graph { api::view::filter_ops::Filter, graph::views::filter::model::{ node_filter::{ops::NodeFilterOps, NodeFilter}, + NodeFilterFactory, property_filter::ops::PropertyFilterOps, PropertyFilterFactory, }, From 58688de2d58cf48f2f7dba997a3a964ccbd83344 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:55:36 +0100 Subject: [PATCH 041/155] add EntityExpr/NodeExpr/EdgeExpr impls for AndFilter/OrFilter to enable .not() chaining on composed filters --- .../db/graph/views/filter/model/and_filter.rs | 47 ++++++++++++++++- .../graph/views/filter/model/edge_expr/ops.rs | 49 ++++++++++++++++++ .../src/db/graph/views/filter/model/mod.rs | 4 -- .../graph/views/filter/model/node_expr/ops.rs | 50 +++++++++++++++++++ .../db/graph/views/filter/model/or_filter.rs | 47 ++++++++++++++++- 5 files changed, 189 insertions(+), 8 deletions(-) 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 16ef5b9d09..aec4089288 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -1,14 +1,16 @@ use crate::{ db::{ api::{ - state::ops::{filter::AndOp, NodeFilterOp}, + state::ops::{filter::AndOp, NodeFilterOp, NodeOp}, view::internal::GraphView, }, graph::views::filter::{ and_filtered_graph::AndFilteredGraph, model::{ + edge_expr::{EdgeExpr, EdgeOp, ops::AndBoolEdgeOp}, edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, + node_expr::{EntityExpr, NodeExpr, ops::AndBoolNodeOp}, node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, }, CreateFilter, @@ -17,8 +19,9 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; +use raphtory_api::core::entities::properties::prop::Prop; use raphtory_storage::layer_ops::InternalLayerOps; -use std::{fmt, fmt::Display}; +use std::{fmt, fmt::Display, sync::Arc}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AndFilter { @@ -103,3 +106,43 @@ impl TryAsCompositeFilter for )) } } + +impl EntityExpr for AndFilter +where + L: EntityExpr, + R: EntityExpr, +{ + type Marker = L::Marker; +} + +impl NodeExpr for AndFilter +where + L: NodeExpr, + R: NodeExpr, + R: EntityExpr, +{ + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + Ok(Arc::new(AndBoolNodeOp { left, right })) + } +} + +impl EdgeExpr for AndFilter +where + L: EdgeExpr, + R: EdgeExpr, + R: EntityExpr, +{ + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph)?; + Ok(Arc::new(AndBoolEdgeOp { left, right })) + } +} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 1159b31182..8d6c7367db 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -340,3 +340,52 @@ impl<'g> EdgeOp for ListAwareSetEdgeOp<'g> { }) } } + +// ───────────────────────────────────────────────────────────────────────────── +// AndBoolEdgeOp / OrBoolEdgeOp — boolean AND/OR over two Option edge ops +// +// Used by AndFilter / OrFilter when they implement EdgeExpr so that +// .not() (and other EntityExprFilterOps) can be chained on composed edge filters. +// ───────────────────────────────────────────────────────────────────────────── + +pub(crate) struct AndBoolEdgeOp<'g> { + pub(crate) left: Arc> + 'g>, + pub(crate) right: Arc> + 'g>, +} + +impl<'g> Clone for AndBoolEdgeOp<'g> { + fn clone(&self) -> Self { + Self { left: self.left.clone(), right: self.right.clone() } + } +} + +impl<'g> EdgeOp for AndBoolEdgeOp<'g> { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + let l = matches!(self.left.apply(storage, edge), Some(Prop::Bool(true))); + let r = matches!(self.right.apply(storage, edge), Some(Prop::Bool(true))); + Some(Prop::Bool(l && r)) + } +} + +pub(crate) struct OrBoolEdgeOp<'g> { + pub(crate) left: Arc> + 'g>, + pub(crate) right: Arc> + 'g>, +} + +impl<'g> Clone for OrBoolEdgeOp<'g> { + fn clone(&self) -> Self { + Self { left: self.left.clone(), right: self.right.clone() } + } +} + +impl<'g> EdgeOp for OrBoolEdgeOp<'g> { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + let l = matches!(self.left.apply(storage, edge), Some(Prop::Bool(true))); + let r = matches!(self.right.apply(storage, edge), Some(Prop::Bool(true))); + Some(Prop::Bool(l || r)) + } +} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 6ecdabffe6..c20c9f7582 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -180,10 +180,6 @@ pub trait ComposableFilter: Sized { right: other, } } - - fn not(self) -> NotFilter { - NotFilter(self) - } } pub trait DynCreateFilter: TryAsCompositeFilter + Send + Sync + 'static { 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 index ea4b9c8390..5aefeedcfd 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -454,6 +454,56 @@ impl<'g> NodeOp for ListAwareSetNodeOp<'g> { } } +// ───────────────────────────────────────────────────────────────────────────── +// AndBoolNodeOp / OrBoolNodeOp — boolean AND/OR over two Option node ops +// +// Used by AndFilter / OrFilter when they implement NodeExpr so that +// .not() (and other EntityExprFilterOps) can be chained on composed filters: +// NodeFilter.degree().lt(5).and(NodeFilter.name().eq("alice")).not() +// ───────────────────────────────────────────────────────────────────────────── + +pub(crate) struct AndBoolNodeOp<'g> { + pub(crate) left: Arc> + 'g>, + pub(crate) right: Arc> + 'g>, +} + +impl<'g> Clone for AndBoolNodeOp<'g> { + fn clone(&self) -> Self { + Self { left: self.left.clone(), right: self.right.clone() } + } +} + +impl<'g> NodeOp for AndBoolNodeOp<'g> { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + let l = matches!(self.left.apply(storage, node), Some(Prop::Bool(true))); + let r = matches!(self.right.apply(storage, node), Some(Prop::Bool(true))); + Some(Prop::Bool(l && r)) + } +} + +pub(crate) struct OrBoolNodeOp<'g> { + pub(crate) left: Arc> + 'g>, + pub(crate) right: Arc> + 'g>, +} + +impl<'g> Clone for OrBoolNodeOp<'g> { + fn clone(&self) -> Self { + Self { left: self.left.clone(), right: self.right.clone() } + } +} + +impl<'g> NodeOp for OrBoolNodeOp<'g> { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + let l = matches!(self.left.apply(storage, node), Some(Prop::Bool(true))); + let r = matches!(self.right.apply(storage, node), Some(Prop::Bool(true))); + Some(Prop::Bool(l || r)) + } +} + // ───────────────────────────────────────────────────────────────────────────── // UnwrapOptPropOp<'g> — converts Option → Prop for nested aggregation // ───────────────────────────────────────────────────────────────────────────── 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 639fd0aa63..aed5b4723c 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -1,13 +1,15 @@ use crate::{ db::{ api::{ - state::ops::{filter::OrOp, NodeFilterOp}, + state::ops::{filter::OrOp, NodeFilterOp, NodeOp}, view::internal::GraphView, }, graph::views::filter::{ model::{ + edge_expr::{EdgeExpr, EdgeOp, ops::OrBoolEdgeOp}, edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, + node_expr::{EntityExpr, NodeExpr, ops::OrBoolNodeOp}, node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, }, or_filtered_graph::OrFilteredGraph, @@ -17,7 +19,8 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use std::{fmt, fmt::Display}; +use raphtory_api::core::entities::properties::prop::Prop; +use std::{fmt, fmt::Display, sync::Arc}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct OrFilter { @@ -93,3 +96,43 @@ impl TryAsCompositeFilter for )) } } + +impl EntityExpr for OrFilter +where + L: EntityExpr, + R: EntityExpr, +{ + type Marker = L::Marker; +} + +impl NodeExpr for OrFilter +where + L: NodeExpr, + R: NodeExpr, + R: EntityExpr, +{ + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + Ok(Arc::new(OrBoolNodeOp { left, right })) + } +} + +impl EdgeExpr for OrFilter +where + L: EdgeExpr, + R: EdgeExpr, + R: EntityExpr, +{ + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph)?; + Ok(Arc::new(OrBoolEdgeOp { left, right })) + } +} From f27560810c9e64d3d56fcb38b91d618c12f832a5 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:25:28 +0100 Subject: [PATCH 042/155] fix exploded edgefilter --- raphtory/src/db/graph/views/filter/model/edge_filter.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 14573f3e83..dd3976ada2 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -7,7 +7,7 @@ use crate::{ graph::views::filter::{ edge_node_filtered_graph::EdgeNodeFilteredGraph, model::{ - exploded_edge_filter::CompositeExplodedEdgeFilter, + exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, @@ -369,6 +369,7 @@ impl TryAsCompositeFilter for CompositeEdgeFilter { // ───────────────────────────────────────────────────────────────────────────── impl EdgeFilterFactory for EdgeFilter {} +impl EdgeFilterFactory for ExplodedEdgeFilter {} impl EdgeFilterFactory for Windowed {} impl EdgeFilterFactory for Latest {} impl EdgeFilterFactory for Layered {} From 9d2ee27c7099a5a450965031374d25f5be19201f Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:10:28 +0100 Subject: [PATCH 043/155] fix: complete type-safe filter API migration - EntityAggOps, EdgeEndpointWrapper new-API support, ergonomic is_in/is_not_in --- raphtory-tests/tests/test_filters.rs | 100 +++++++++++------- .../graph/views/filter/model/edge_expr/ops.rs | 31 +++++- .../graph/views/filter/model/edge_filter.rs | 94 +++++++++++++++- .../src/db/graph/views/filter/model/mod.rs | 7 +- .../views/filter/model/node_expr/exprs.rs | 27 +++++ .../views/filter/model/node_expr/filters.rs | 8 +- raphtory/src/lib.rs | 2 +- 7 files changed, 216 insertions(+), 53 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 9b8b74d593..88daa154b6 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -2171,31 +2171,39 @@ mod test_node_filter { let invalid_filters = vec![ NodeFilter.degree().is_none(), NodeFilter.degree().is_some(), + NodeFilter.in_degree().is_none(), + NodeFilter.in_degree().is_some(), + NodeFilter.out_degree().is_none(), + NodeFilter.out_degree().is_some(), + ]; + for filter in invalid_filters { + assert!( + matches!(graph.filter(filter), Err(GraphError::InvalidFilter(_))), + "expected InvalidFilter for unsupported degree filter operation" + ); + } + + let string_invalid_filters = vec![ NodeFilter.degree().starts_with("1"), NodeFilter.degree().ends_with("1"), NodeFilter.degree().contains("1"), NodeFilter.degree().not_contains("1"), NodeFilter.degree().fuzzy_search("1", 1, false), - NodeFilter.in_degree().is_none(), - NodeFilter.in_degree().is_some(), NodeFilter.in_degree().starts_with("1"), NodeFilter.in_degree().ends_with("1"), NodeFilter.in_degree().contains("1"), NodeFilter.in_degree().not_contains("1"), NodeFilter.in_degree().fuzzy_search("1", 1, false), - NodeFilter.out_degree().is_none(), - NodeFilter.out_degree().is_some(), NodeFilter.out_degree().starts_with("1"), NodeFilter.out_degree().ends_with("1"), NodeFilter.out_degree().contains("1"), NodeFilter.out_degree().not_contains("1"), NodeFilter.out_degree().fuzzy_search("1", 1, false), ]; - - for filter in invalid_filters { + for filter in string_invalid_filters { assert!( matches!(graph.filter(filter), Err(GraphError::InvalidFilter(_))), - "expected InvalidFilter for unsupported degree filter operation" + "expected InvalidFilter for string op on numeric degree" ); } } @@ -2336,20 +2344,28 @@ mod test_node_filter { NodeFilter.out_degree().ne(value_a.clone()), NodeFilter.out_degree().ge(value_a.clone()), NodeFilter.out_degree().gt(value_a.clone()), - NodeFilter.degree().is_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.degree().is_not_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.in_degree().is_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.in_degree().is_not_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.out_degree().is_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.out_degree().is_not_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), ]; - for filter in invalid_filters { assert!( matches!(graph.filter(filter), Err(GraphError::InvalidFilter(_))), "expected InvalidFilter for non-numeric string values" ); } + + let set_invalid_filters = vec![ + NodeFilter.degree().is_in(vec![value_a.clone(), value_b.clone()]), + NodeFilter.degree().is_not_in(vec![value_a.clone(), value_b.clone()]), + NodeFilter.in_degree().is_in(vec![value_a.clone(), value_b.clone()]), + NodeFilter.in_degree().is_not_in(vec![value_a.clone(), value_b.clone()]), + NodeFilter.out_degree().is_in(vec![value_a.clone(), value_b.clone()]), + NodeFilter.out_degree().is_not_in(vec![value_a.clone(), value_b.clone()]), + ]; + for filter in set_invalid_filters { + assert!( + matches!(graph.filter(filter), Err(GraphError::InvalidFilter(_))), + "expected InvalidFilter for is_in/is_not_in on numeric degree" + ); + } } } @@ -5270,14 +5286,16 @@ mod test_node_property_filter { } } -mod test_node_composite_filter { +// TODO: delete when search is dropped and graphql composite path is gone +mod composite_node_filter_tests { use raphtory_api::core::Direction; use crate::{init_edges_graph, init_nodes_graph, IdentityGraphTransformer}; use raphtory::{ db::graph::views::filter::model::{ - node_filter::ops::NodeFilterOps, property_filter::ops::PropertyFilterOps, - ComposableFilter, NodeFilterFactory, PropertyFilterFactory, TryAsCompositeFilter, + node_filter::ops::NodeFilterOps, not_filter::NotFilter, + property_filter::ops::PropertyFilterOps, ComposableFilter, NodeFilterFactory, + PropertyFilterFactory, TryAsCompositeFilter, }, prelude::NodeFilter, }; @@ -5620,11 +5638,13 @@ mod test_node_composite_filter { #[test] fn test_not_composite_filter_nodes() { - let filter = NodeFilter.name() - .eq("2") - .and(NodeFilter.property("p2").eq(2u64)) - .or(NodeFilter.property("p9").eq(5u64)) - .not(); + let filter = NotFilter( + NodeFilter + .name() + .eq("2") + .and(NodeFilter.property("p2").eq(2u64)) + .or(NodeFilter.property("p9").eq(5u64)), + ); let expected_results = vec!["3", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -5641,9 +5661,7 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = NodeFilter.name() - .eq("2") - .not() + let filter = NotFilter(NodeFilter.name().eq("2")) .and(NodeFilter.property("p2").eq(2u64)) .or(NodeFilter.property("p9").eq(5u64)); let expected_results = vec!["1"]; @@ -5725,7 +5743,7 @@ mod test_node_property_filter_agg { CreateFilter, }, }, - prelude::{AdditionOps, EntityExprFilterOps, GraphViewOps, PropertyAdditionOps, TemporalPropOps}, + prelude::{AdditionOps, EntityAggOps, EntityExprFilterOps, GraphViewOps, PropertyAdditionOps, TemporalPropOps}, }; use raphtory_api::core::{ entities::properties::prop::{IntoProp, Prop}, @@ -9247,8 +9265,7 @@ mod test_edge_filter { }; use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, - node_filter::ops::NodeFilterOps, - property_filter::ops::{ListAggOps, PropertyFilterOps}, + node_expr::EntityExprFilterOps, ComposableFilter, EdgeViewFilterOps, NodeFilterFactory, PropertyFilterFactory, ViewWrapOps, }; @@ -12567,9 +12584,10 @@ mod test_edge_property_filter { } } -mod test_edge_composite_filter { +// TODO: delete when search is dropped and graphql composite path is gone +mod composite_edge_filter_tests { use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, + edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, not_filter::NotFilter, property_filter::ops::PropertyFilterOps, ComposableFilter, NodeFilterFactory, PropertyFilterFactory, TryAsCompositeFilter, }; @@ -12942,11 +12960,12 @@ mod test_edge_composite_filter { #[test] fn test_not_composite_filter_edges() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. Search API uses filter API internally for this filter. - let filter = EdgeFilter::src() - .name() - .eq("13") - .and(EdgeFilter.property("p1").eq("prop1")) - .not(); + let filter = NotFilter( + EdgeFilter::src() + .name() + .eq("13") + .and(EdgeFilter.property("p1").eq("prop1")), + ); let expected_results = vec![ "1->2", "2->1", @@ -12971,11 +12990,12 @@ mod test_edge_composite_filter { TestVariants::EventOnly, ); - let filter = EdgeFilter::src() - .name() - .eq("13") - .and(EdgeFilter.property("p1").eq("prop1").not()) - .not(); + let filter = NotFilter( + EdgeFilter::src() + .name() + .eq("13") + .and(NotFilter(EdgeFilter.property("p1").eq("prop1"))), + ); let expected_results = vec![ "1->2", "2->1", diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 8d6c7367db..443db8e2c3 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -18,7 +18,10 @@ use raphtory_storage::graph::graph::GraphStorage; use std::{collections::HashSet, hash::Hash}; use super::EdgeOp; -use crate::db::graph::views::filter::model::property_filter::evaluate::aggregate_values; +use crate::db::{ + api::state::ops::NodeOp, + graph::views::filter::model::{edge_filter::Endpoint, property_filter::evaluate::aggregate_values}, +}; use raphtory_api::core::entities::properties::prop::PropArray; use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── @@ -389,3 +392,29 @@ impl<'g> EdgeOp for OrBoolEdgeOp<'g> { Some(Prop::Bool(l || r)) } } + +// ───────────────────────────────────────────────────────────────────────────── +// EdgeEndpointNodeOp — applies a node op to the src or dst VID of an edge +// +// Bridges EdgeEndpointWrapper into the EdgeExpr system: +// EdgeFilter::src().name().eq("Alice") compiles the name NodeOp once, then +// at evaluation time looks up the src VID and applies the node op to it. +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct EdgeEndpointNodeOp<'g> { + pub(crate) node_op: Arc> + 'g>, + pub(crate) endpoint: Endpoint, +} + +impl<'g> EdgeOp for EdgeEndpointNodeOp<'g> { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + let vid = match self.endpoint { + Endpoint::Src => edge.src(), + Endpoint::Dst => edge.dst(), + }; + self.node_op.apply(storage, vid) + } +} 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 dd3976ada2..89d22a2cac 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -7,6 +7,7 @@ use crate::{ graph::views::filter::{ edge_node_filtered_graph::EdgeNodeFilteredGraph, model::{ + edge_expr::{ops::EdgeEndpointNodeOp, EdgeExpr, EdgeOp}, exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, @@ -14,15 +15,18 @@ use crate::{ is_valid_filter::IsValidEdge, latest_filter::Latest, layered_filter::Layered, + node_expr::{EntityExpr, NodeExpr, TemporalPropOps}, node_filter::{ builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, }, property_filter::PropertyFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - AndFilter, CombinedFilter, ComposableFilter, CreateView, EdgeFilterFactory, - EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, NotFilter, OrFilter, - TryAsCompositeFilter, Wrap, + AllExpr, AndFilter, AnyExpr, AvgExpr, CombinedFilter, ComposableFilter, + CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, + FirstExpr, InternalViewWrapOps, LastExpr, LenExpr, MaxExpr, MetadataExpr, + MinExpr, NotFilter, OrFilter, PropertyExpr, PropertyFilterFactory, + SumExpr, TemporalExpr, TemporalProp, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -30,7 +34,10 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; +use raphtory_api::core::{ + entities::{properties::prop::Prop, GID}, + storage::timeindex::EventTime, +}; use std::{fmt, fmt::Display, sync::Arc}; // User facing entry for building edge filters. @@ -142,6 +149,85 @@ impl EdgeEndpointWrapper { pub fn node_type(&self) -> EdgeEndpointWrapper { EdgeEndpointWrapper::new(Type, self.endpoint) } + + #[inline] + pub fn property(&self, name: impl Into) -> EdgeEndpointWrapper> { + EdgeEndpointWrapper::new(NodeFilter.property(name), self.endpoint) + } + + #[inline] + pub fn metadata(&self, name: impl Into) -> EdgeEndpointWrapper> { + EdgeEndpointWrapper::new(NodeFilter.metadata(name), self.endpoint) + } +} + +impl EdgeEndpointWrapper> { + #[inline] + pub fn temporal(&self) -> EdgeEndpointWrapper> { + EdgeEndpointWrapper::new(self.inner.temporal(), self.endpoint) + } +} + +impl EdgeEndpointWrapper> { + #[inline] + pub fn sum(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.sum(), endpoint) + } + #[inline] + pub fn avg(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.avg(), endpoint) + } + #[inline] + pub fn min(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.min(), endpoint) + } + #[inline] + pub fn max(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.max(), endpoint) + } + #[inline] + pub fn first(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.first(), endpoint) + } + #[inline] + pub fn last(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.last(), endpoint) + } + #[inline] + pub fn len(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.len(), endpoint) + } + #[inline] + pub fn any(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.any(), endpoint) + } + #[inline] + pub fn all(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.all(), endpoint) + } +} + +impl EntityExpr for EdgeEndpointWrapper { + type Marker = EdgeFilter; +} + +impl EdgeExpr for EdgeEndpointWrapper { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let node_op = self.inner.create_node_op(graph)?; + Ok(Arc::new(EdgeEndpointNodeOp { node_op, endpoint: self.endpoint })) + } } impl Wrap for EdgeEndpointWrapper { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index c20c9f7582..88c5add48e 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -16,10 +16,11 @@ pub use crate::{ }, node_expr::{ AllExpr, AnyExpr, AvgExpr, BinaryCmpFilter, - FirstExpr, LastExpr, LenExpr, MaxExpr, - MinExpr, EntityExprFilterOps, TemporalPropOps, + EntityAggOps, EntityExprFilterOps, FirstExpr, + LastExpr, LenExpr, MaxExpr, MinExpr, PropValueSetFilter, StringFilter, SumExpr, - TemporalExpr, TemporalProp, UnaryFilter, + TemporalExpr, TemporalProp, TemporalPropOps, + UnaryFilter, }, node_filter::{NodeFilter, NodeFilterFactory}, 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 0a7611065c..11e4fe6ef5 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 @@ -462,6 +462,23 @@ impl>, RHS> // ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── +// EntityAggOps — secondary aggregate operators on filter expression types +// +// Enables chains like: .temporal().first().sum().eq(6).any() +// Only implemented for the agg expr wrappers (not primitive EntityExpr types like u64). +// ───────────────────────────────────────────────────────────────────────────── + +pub trait EntityAggOps: EntityExpr + Sized { + fn sum(self) -> SumExpr; + fn avg(self) -> AvgExpr; + fn min(self) -> MinExpr; + fn max(self) -> MaxExpr; + fn first(self) -> FirstExpr; + fn last(self) -> LastExpr; + fn len(self) -> LenExpr; +} + macro_rules! impl_agg_expr { ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident) => { #[derive(Clone)] @@ -471,6 +488,16 @@ macro_rules! impl_agg_expr { type Marker = E::Marker; } + impl EntityAggOps for $expr { + 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 NodeExpr for $expr { fn create_node_op<'g, G: GraphView + 'g>( &self, 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 b816112412..345171ca66 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 @@ -557,19 +557,19 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { } } - fn is_in(self, values: impl IntoIterator) -> PropValueSetFilter { + fn is_in>(self, values: impl IntoIterator) -> PropValueSetFilter { PropValueSetFilter { expr: self, - values: values.into_iter().collect(), + values: values.into_iter().map(Into::into).collect(), op: SetOp::IsIn, entity: Self::Marker::default(), } } - fn is_not_in(self, values: impl IntoIterator) -> PropValueSetFilter { + fn is_not_in>(self, values: impl IntoIterator) -> PropValueSetFilter { PropValueSetFilter { expr: self, - values: values.into_iter().collect(), + values: values.into_iter().map(Into::into).collect(), op: SetOp::IsNotIn, entity: Self::Marker::default(), } diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 70c5d3b422..162d979fcf 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -148,7 +148,7 @@ pub mod prelude { pub use crate::db::graph::views::filter::model::{ filter::Filter, property_filter::PropertyFilter, - EntityExprFilterOps, TemporalPropOps, + EntityAggOps, EntityExprFilterOps, TemporalPropOps, }; pub use crate::db::graph::views::{ From 6629d7057c516d95a45afad457183c5be7fb62e7 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 18 Jun 2026 07:44:20 +0100 Subject: [PATCH 044/155] fix tests --- raphtory-tests/tests/test_filters.rs | 130 +++++++++--------- .../graph/views/filter/model/node_expr/ops.rs | 14 +- 2 files changed, 77 insertions(+), 67 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 88daa154b6..e74c1b2891 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -243,7 +243,7 @@ mod test_property_semantics { #[test] fn test_temporal_any_semantics() { - let filter = NodeFilter.property("p1").temporal().any().eq(1u64); + let filter = NodeFilter.property("p1").temporal().eq(1u64).any(); let expected_results = vec!["N1", "N2", "N3", "N4", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -263,7 +263,7 @@ mod test_property_semantics { #[test] fn test_temporal_any_semantics_for_event_ids() { - let filter = NodeFilter.property("p1").temporal().any().eq(1u64); + let filter = NodeFilter.property("p1").temporal().eq(1u64).any(); let expected_results = vec!["N1", "N16", "N17", "N2", "N3", "N4", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( @@ -779,7 +779,7 @@ mod test_property_semantics { #[test] fn test_temporal_any_semantics() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter.property("p1").temporal().any().eq(1u64); + let filter = EdgeFilter.property("p1").temporal().eq(1u64).any(); let expected_results = vec![ "N1->N2", "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", "N7->N8", "N8->N9", ]; @@ -802,7 +802,7 @@ mod test_property_semantics { #[test] fn test_temporal_any_semantics_for_event_ids() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter.property("p1").temporal().any().lt(2u64); + let filter = EdgeFilter.property("p1").temporal().lt(2u64).any(); let expected_results = vec![ "N1->N2", "N16->N15", "N17->N16", "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", "N7->N8", "N8->N9", @@ -3328,7 +3328,7 @@ mod test_node_property_filter { TestVariants::All, ); - let filter = NodeFilter.property("p20").temporal().all().eq("Gold_ship"); + let filter = NodeFilter.property("p20").temporal().eq("Gold_ship").all(); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -3382,7 +3382,7 @@ mod test_node_property_filter { TestVariants::All, ); - let filter = NodeFilter.property("p1").temporal().all().ne("Gold_ship"); + let filter = NodeFilter.property("p1").temporal().ne("Gold_ship").all(); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -3436,7 +3436,7 @@ mod test_node_property_filter { TestVariants::All, ); - let filter = NodeFilter.property("p9").temporal().all().lt(10u64); + let filter = NodeFilter.property("p9").temporal().lt(10u64).all(); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -3490,7 +3490,7 @@ mod test_node_property_filter { TestVariants::All, ); - let filter = NodeFilter.property("p2").temporal().all().le(10u64); + let filter = NodeFilter.property("p2").temporal().le(10u64).all(); let expected_results = vec!["3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3544,7 +3544,7 @@ mod test_node_property_filter { TestVariants::All, ); - let filter = NodeFilter.property("p9").temporal().all().gt(1u64); + let filter = NodeFilter.property("p9").temporal().gt(1u64).all(); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -3598,7 +3598,7 @@ mod test_node_property_filter { TestVariants::All, ); - let filter = NodeFilter.property("p40").temporal().all().ge(5u64); + let filter = NodeFilter.property("p40").temporal().ge(5u64).all(); let expected_results = vec!["1", "2"]; assert_filter_nodes_results( init_nodes_graph, @@ -3678,8 +3678,8 @@ mod test_node_property_filter { let filter = NodeFilter .property("p2") .temporal() - .any() - .is_in(vec![Prop::U64(2)]); + .eq(Prop::U64(2)) + .any(); let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, @@ -3736,8 +3736,8 @@ mod test_node_property_filter { let filter = NodeFilter .property("p2") .temporal() - .all() - .is_not_in(vec![Prop::U64(2)]); + .ne(Prop::U64(2)) + .all(); let expected_results = vec!["3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3851,8 +3851,8 @@ mod test_node_property_filter { let filter = NodeFilter .property("p10") .temporal() - .any() - .starts_with("Pap"); + .starts_with("Pap") + .any(); let expected_results: Vec<&str> = vec!["1", "2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3935,8 +3935,8 @@ mod test_node_property_filter { let filter = NodeFilter .property("p20") .temporal() - .all() - .starts_with("Gold"); + .starts_with("Gold") + .all(); let expected_results: Vec<&str> = vec!["1", "2", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -3976,8 +3976,8 @@ mod test_node_property_filter { let filter = NodeFilter .property("p10") .temporal() - .any() - .ends_with("ship"); + .ends_with("ship") + .any(); let expected_results: Vec<&str> = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, @@ -4060,8 +4060,8 @@ mod test_node_property_filter { let filter = NodeFilter .property("p20") .temporal() - .all() - .ends_with("ship"); + .ends_with("ship") + .all(); let expected_results: Vec<&str> = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -4101,8 +4101,8 @@ mod test_node_property_filter { let filter = NodeFilter .property("p10") .temporal() - .any() - .contains("Paper"); + .contains("Paper") + .any(); let expected_results: Vec<&str> = vec!["1", "2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -4161,7 +4161,7 @@ mod test_node_property_filter { TestVariants::All, ); - let filter = NodeFilter.property("p30").temporal().all().contains("Gold"); + let filter = NodeFilter.property("p30").temporal().contains("Gold").all(); let expected_results: Vec<&str> = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -4201,8 +4201,8 @@ mod test_node_property_filter { let filter = NodeFilter .property("p10") .temporal() - .any() - .not_contains("ship"); + .not_contains("ship") + .any(); let expected_results: Vec<&str> = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -4264,8 +4264,8 @@ mod test_node_property_filter { let filter = NodeFilter .property("p30") .temporal() - .all() - .not_contains("boat"); + .not_contains("boat") + .all(); let expected_results: Vec<&str> = vec!["1", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -4545,8 +4545,8 @@ mod test_node_property_filter { .window(3, 5) .property("p20") .temporal() - .any() - .eq("Gold_boat"); + .eq("Gold_boat") + .any(); let expected_results = vec!["4"]; assert_filter_nodes_results( @@ -4568,8 +4568,8 @@ mod test_node_property_filter { .window(3, 5) .property("p20") .temporal() - .all() - .eq("Gold_boat"); + .eq("Gold_boat") + .all(); let expected_results = vec![]; assert_filter_nodes_results( @@ -4595,8 +4595,8 @@ mod test_node_property_filter { .window(1, 4) .property("p10") .temporal() - .any() - .eq("Paper_airplane"); + .eq("Paper_airplane") + .any(); // Filters only node 3 let filter2 = NodeFilter @@ -9082,7 +9082,7 @@ mod test_node_property_filter_agg { // ------ Property: any ------ #[test] fn test_node_property_any() { - let filter = NodeFilter.property("p_u8s").any().eq(Prop::U8(3)); + let filter = NodeFilter.property("p_u8s").eq(Prop::U8(3)).any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -9101,7 +9101,7 @@ mod test_node_property_filter_agg { // ------ Metadata: any ------ #[test] fn test_node_metadata_any() { - let filter = NodeFilter.metadata("p_u64s").any().eq(Prop::U64(1)); + let filter = NodeFilter.metadata("p_u64s").eq(Prop::U64(1)).any(); let expected = vec!["n10", "n7"]; apply_assertion(filter, &expected); } @@ -9109,7 +9109,7 @@ mod test_node_property_filter_agg { // ------ Metadata: all ------ #[test] fn test_node_metadata_all() { - let filter = NodeFilter.metadata("p_strs").all().eq("a"); + let filter = NodeFilter.metadata("p_strs").eq("a").all(); let expected = vec!["n6", "n7"]; apply_assertion(filter, &expected); } @@ -9121,8 +9121,8 @@ mod test_node_property_filter_agg { .property("p_bools") .temporal() .first() - .any() - .eq(false); + .eq(false) + .any(); let expected = vec!["n1", "n10", "n2", "n3", "n4"]; apply_assertion(filter, &expected); } @@ -9134,8 +9134,8 @@ mod test_node_property_filter_agg { .property("p_bools_all") .temporal() .first() - .all() - .eq(true); + .eq(true) + .all(); let expected = vec!["n10", "n4"]; apply_assertion(filter, &expected); } @@ -9147,8 +9147,8 @@ mod test_node_property_filter_agg { .property("p_f32s") .temporal() .last() - .any() - .eq(Prop::F32(3.5)); + .eq(Prop::F32(3.5)) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -9160,8 +9160,8 @@ mod test_node_property_filter_agg { .property("p_bools_all") .temporal() .last() - .all() - .eq(true); + .eq(true) + .all(); let expected = vec!["n10", "n4"]; apply_assertion(filter, &expected); } @@ -9173,8 +9173,8 @@ mod test_node_property_filter_agg { .property("p_f32s") .temporal() .any() - .any() - .eq(Prop::F32(3.5)); + .eq(Prop::F32(3.5)) + .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); @@ -9182,8 +9182,8 @@ mod test_node_property_filter_agg { .property("p_f32s") .temporal() .any() - .any() - .eq(Prop::F32(30.0)); + .eq(Prop::F32(30.0)) + .any(); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -9195,8 +9195,8 @@ mod test_node_property_filter_agg { .property("p_bools") .temporal() .any() - .all() - .eq(false); + .eq(false) + .all(); let expected = vec!["n2", "n4"]; apply_assertion(filter, &expected); } @@ -9224,8 +9224,8 @@ mod test_node_property_filter_agg { .all() .all() .all() - .any() - .gt(45.0); + .gt(45.0) + .any(); let expected = vec!["n3"]; apply_assertion(filter, &expected); @@ -9238,8 +9238,8 @@ mod test_node_property_filter_agg { .property("p_bools") .temporal() .all() - .any() - .eq(true); + .eq(true) + .any(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -9251,8 +9251,8 @@ mod test_node_property_filter_agg { .property("p_bools_all") .temporal() .all() - .all() - .eq(true); + .eq(true) + .all(); let expected = vec!["n4", "n10"]; apply_assertion(filter, &expected); } @@ -10790,7 +10790,7 @@ mod test_edge_property_filter { TestVariants::All, ); - let filter = EdgeFilter.property("p20").temporal().all().eq("Gold_ship"); + let filter = EdgeFilter.property("p20").temporal().eq("Gold_ship").all(); let expected_results = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -10851,7 +10851,7 @@ mod test_edge_property_filter { TestVariants::All, ); - let filter = EdgeFilter.property("p30").temporal().all().ne("Classic"); + let filter = EdgeFilter.property("p30").temporal().ne("Classic").all(); let expected_results = vec!["1->2", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -10913,7 +10913,7 @@ mod test_edge_property_filter { TestVariants::All, ); - let filter = EdgeFilter.property("p2").temporal().all().lt(10u64); + let filter = EdgeFilter.property("p2").temporal().lt(10u64).all(); let expected_results = vec![ "1->2", "2->1", @@ -10982,7 +10982,7 @@ mod test_edge_property_filter { TestVariants::All, ); - let filter = EdgeFilter.property("p2").temporal().all().le(5u64); + let filter = EdgeFilter.property("p2").temporal().le(5u64).all(); let expected_results = vec!["1->2", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -11048,7 +11048,7 @@ mod test_edge_property_filter { TestVariants::All, ); - let filter = EdgeFilter.property("p2").temporal().all().gt(5u64); + let filter = EdgeFilter.property("p2").temporal().gt(5u64).all(); let expected_results = vec![ "2->1", "3->1", @@ -11120,7 +11120,7 @@ mod test_edge_property_filter { TestVariants::All, ); - let filter = EdgeFilter.property("p2").temporal().all().ge(6u64); + let filter = EdgeFilter.property("p2").temporal().ge(6u64).all(); let expected_results = vec![ "2->1", "3->1", @@ -11734,7 +11734,7 @@ mod test_edge_property_filter { TestVariants::All, ); - let filter = EdgeFilter.property("p20").temporal().all().contains("ship"); + let filter = EdgeFilter.property("p20").temporal().contains("ship").all(); let expected_results: Vec<&str> = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, 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 index 5aefeedcfd..e5b50e0e20 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -271,10 +271,20 @@ impl_agg_entity_op!(MaxNodeOp, MaxEdgeOp, |vals| { }) }); impl_agg_entity_op!(FirstNodeOp, FirstEdgeOp, |vals| { - aggregate_values(vals, &|mut pi| pi.next()) + // Pick the first temporal entry as-is (whether scalar or list). + // aggregate_values would recurse into list entries and pick the first + // *element* within each entry, which is wrong for list-typed properties. + match vals? { + Prop::List(x) => x.iter_all().find_map(|v| v), + _ => None, + } }); impl_agg_entity_op!(LastNodeOp, LastEdgeOp, |vals| { - aggregate_values(vals, &|pi| pi.last()) + // Pick the last temporal entry as-is (whether scalar or list). + match vals? { + Prop::List(x) => x.iter_all().filter_map(|v| v).last(), + _ => None, + } }); impl_agg_entity_op!(LenNodeOp, LenEdgeOp, |vals| { aggregate_values(vals, &|pi| Some(pi.count().into_prop())) From a611bc364fd45dbc7f19fe859a548031e9f4cdfe Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:38:46 +0100 Subject: [PATCH 045/155] reorder quantifier chains and make broadcast_binary recursive to fix test failures --- raphtory-tests/tests/test_filters.rs | 58 +++++++++++++------ .../graph/views/filter/model/node_expr/ops.rs | 10 ++-- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index e74c1b2891..baf73023a5 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -5729,6 +5729,26 @@ mod composite_node_filter_tests { } } +// Chain order convention for quantifier filters: +// +// Always place the comparison (.eq/.gt/etc.) BEFORE quantifiers. +// Quantifiers after the comparison are applied innermost→outermost (left to right). +// +// .property("p").eq(x).any().all() +// ↑ ↑ +// ∃ elem ∀ timestamp → ∀t ∃e: e == x +// +// For temporal + nested list (n levels deep): +// .temporal().gt(x).any().all().all() +// ↑ ↑ ↑ +// ∃ leaf ∀ d2 ∀ d1/timestamp +// +// This is the OPPOSITE of the old broken convention (.all().eq(x).any()) +// where quantifiers before the comparison received raw typed values and +// checked `elem == Bool(true)`, always returning false for non-Bool types. +// +// temporal() semantics: iterates only timestamps where the property IS defined. +// A node with the property absent at some timestamps is unaffected by those gaps. mod test_node_property_filter_agg { use crate::IdentityGraphTransformer; use raphtory::{ @@ -9172,8 +9192,8 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .any() .eq(Prop::F32(3.5)) + .any() .any(); let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); @@ -9181,8 +9201,8 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_f32s") .temporal() - .any() .eq(Prop::F32(30.0)) + .any() .any(); let expected = vec!["n4"]; apply_assertion(filter, &expected); @@ -9194,9 +9214,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_bools") .temporal() - .any() .eq(false) - .all(); + .all() + .any(); let expected = vec!["n2", "n4"]; apply_assertion(filter, &expected); } @@ -9205,11 +9225,11 @@ mod test_node_property_filter_agg { fn test_node_nested_list_property_all_all_all_any() { let filter = NodeFilter .property("nested_list") + .gt(45.0) + .any() .all() .all() - .all() - .any() - .gt(45.0); + .all(); let expected = vec!["n1", "n3"]; apply_assertion(filter, &expected); @@ -9220,14 +9240,14 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("nested_list") .temporal() + .gt(45.0) + .any() .all() .all() .all() - .all() - .gt(45.0) - .any(); + .all(); - let expected = vec!["n3"]; + let expected = vec!["n1", "n3"]; apply_assertion(filter, &expected); } @@ -9237,9 +9257,9 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_bools") .temporal() - .all() .eq(true) - .any(); + .any() + .all(); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -12036,8 +12056,8 @@ mod test_edge_property_filter { .window(2, 4) .property("p20") .temporal() - .any() - .eq("Gold_boat"); + .eq("Gold_boat") + .any(); let expected_any = vec!["2->3"]; assert_filter_edges_results( @@ -12059,8 +12079,8 @@ mod test_edge_property_filter { .window(2, 4) .property("p20") .temporal() - .all() - .eq("Gold_boat"); + .eq("Gold_boat") + .all(); let expected_all: Vec<&str> = vec![]; assert_filter_edges_results( @@ -12085,8 +12105,8 @@ mod test_edge_property_filter { .window(3, 6) .property("p10") .temporal() - .any() - .eq("Paper_airplane"); + .eq("Paper_airplane") + .any(); let filter2 = EdgeFilter .window(3, 6) 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 index e5b50e0e20..b9cf6085a1 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -341,7 +341,7 @@ impl<'g> NodeOp for ListAwareCmpNodeOp<'g> { // Some(Prop::List(PropArray::from(bools))) // } // }) - broadcast_binary(lv, rhs, |lv, rhs| { + broadcast_binary(lv, rhs, &|lv, rhs| { Some(Prop::Bool(Prop::binary_cmp(op, &lv?, &rhs?))) }) } @@ -371,7 +371,7 @@ impl<'g> NodeOp for ListAwareStringNodeOp<'g> { // Some(Prop::List(PropArray::from(bools))) // } // }) - broadcast_binary(lv, rhs, |lv, rhs| { + broadcast_binary(lv, rhs, &|lv, rhs| { Some(Prop::Bool(Option::::string_cmp(op, &lv, &rhs))) }) } @@ -390,7 +390,7 @@ pub fn broadcast_unary(v: Option, op: impl Fn(Option) -> Option, r: Option, - op: impl Fn(Option, Option) -> Option, + op: &impl Fn(Option, Option) -> Option, ) -> Option { let l = l?; let r = r?; @@ -411,13 +411,13 @@ pub fn broadcast_binary( } (Prop::List(l), r) => Some(Prop::List( l.iter_all() - .map(|(l)| op(l, Some(r.clone()))) + .map(|l| broadcast_binary(l, Some(r.clone()), op)) .flatten() .collect(), )), (l, Prop::List(r)) => Some(Prop::List( r.iter_all() - .map(|r| op(Some(l.clone()), r)) + .map(|r| broadcast_binary(Some(l.clone()), r, op)) .flatten() .collect(), )), From 3ed6c10ad0369e889be64fa43e201ae740e51f5d Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:45:55 +0100 Subject: [PATCH 046/155] use Prop::try_cast for cross-type numeric comparisons in filters --- .../db/graph/views/filter/model/edge_expr/ops.rs | 8 ++++---- .../graph/views/filter/model/filter_operator.rs | 16 ++++++++-------- .../db/graph/views/filter/model/node_expr/ops.rs | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 443db8e2c3..445faa3d8f 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -214,8 +214,8 @@ impl<'g> EdgeOp for PropValueSetEdgeOp<'g> { match self.inner.apply(storage, edge) { None => false, Some(v) => match self.op { - SetOp::IsIn => self.values.iter().any(|x| x == &v), - SetOp::IsNotIn => self.values.iter().all(|x| x != &v), + SetOp::IsIn => self.values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), + SetOp::IsNotIn => self.values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), }, } } @@ -330,8 +330,8 @@ impl<'g> EdgeOp for ListAwareSetEdgeOp<'g> { let bools: Vec = pi .map(|v| { Prop::Bool(match op { - SetOp::IsIn => values.iter().any(|x| x == &v), - SetOp::IsNotIn => values.iter().all(|x| x != &v), + SetOp::IsIn => values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), + SetOp::IsNotIn => values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), }) }) .collect(); 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 0f32aace4c..cb135e30f0 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -54,18 +54,18 @@ impl_comparable_str!(&'static str); impl Comparable for Prop { fn binary_cmp(op: &BinaryOp, left: &Prop, right: &Prop) -> bool { use std::cmp::Ordering::*; + + // Try casting right to left's type for cross-type numeric comparisons + // (e.g. Prop::I32(1) vs Prop::U64(1), or Prop::F64(3.0) vs Prop::U64(3)). + let right_casted = right.clone().try_cast(left.dtype()); + let right = right_casted.as_ref().unwrap_or(right); + 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::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), } } 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 index b9cf6085a1..4b02a5634f 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -457,8 +457,8 @@ impl<'g> NodeOp for ListAwareSetNodeOp<'g> { broadcast_unary(vals, |v| { let v = v?; Some(Prop::Bool(match op { - SetOp::IsIn => values.iter().any(|x| x == &v), - SetOp::IsNotIn => values.iter().all(|x| x != &v), + SetOp::IsIn => values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), + SetOp::IsNotIn => values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), })) }) } @@ -582,8 +582,8 @@ impl<'g> NodeOp for PropValueSetNodeOp<'g> { match self.inner.apply(storage, node) { None => false, Some(v) => match self.op { - SetOp::IsIn => self.values.iter().any(|x| x == &v), - SetOp::IsNotIn => self.values.iter().all(|x| x != &v), + SetOp::IsIn => self.values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), + SetOp::IsNotIn => self.values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), }, } } From 8df044712efd46f2c3ba814c666029d91c98d64e Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:51:21 +0100 Subject: [PATCH 047/155] fix: apply view transforms (window/latest/snapshot/layer) before delegating to inner filter --- .../graph/views/filter/model/latest_filter.rs | 17 ++--- .../views/filter/model/layered_filter.rs | 22 +++--- .../src/db/graph/views/filter/model/mod.rs | 7 +- .../views/filter/model/snapshot_filter.rs | 71 ++++++++++--------- .../views/filter/model/windowed_filter.rs | 16 ++--- 5 files changed, 72 insertions(+), 61 deletions(-) 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 3755556d73..c325303813 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -75,14 +75,14 @@ impl TryAsCompositeFilter for Latest { impl CreateFilter for Latest { type EntityFiltered<'graph, G> - = T::EntityFiltered<'graph, G> + = T::EntityFiltered<'graph, WindowedGraph> where - G: GraphViewOps<'graph> + TimeOps<'graph> + Clone; + G: GraphViewOps<'graph> + TimeOps<'graph, WindowedViewType = WindowedGraph> + Clone; type NodeFilter<'graph, G> - = T::NodeFilter<'graph, G> + = T::NodeFilter<'graph, WindowedGraph> where - G: GraphView + TimeOps<'graph> + Clone + 'graph; + G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + Clone + 'graph; type FilteredGraph<'graph, G> = G @@ -97,7 +97,7 @@ impl CreateFilter for Latest where G: GraphViewOps<'graph> + TimeOps<'graph, WindowedViewType = WindowedGraph> + Clone, { - self.inner.create_filter(graph) + self.inner.create_filter(graph.latest()) } fn create_node_filter<'graph, G>( @@ -107,20 +107,21 @@ impl CreateFilter for Latest where G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + Clone + 'graph, { - self.inner.create_node_filter(graph) + self.inner.create_node_filter(graph.latest()) } } impl ComposableFilter for Latest {} impl CreateView for Latest { - type View<'graph, G: GraphView + 'graph> = T::View<'graph, G>; + type View<'graph, G: GraphView + 'graph> = WindowedGraph>; fn create_view<'graph, G: GraphView + 'graph>( &self, view: G, ) -> Result, GraphError> { - self.inner.create_view(view) + let inner = self.inner.create_view(view)?; + Ok(inner.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 fea296197d..1d0af9196a 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -1,6 +1,6 @@ use crate::{ db::{ - api::view::internal::GraphView, + api::view::internal::{GraphView, InternalFilter}, graph::views::{ filter::{ model::{ @@ -87,14 +87,16 @@ impl TryAsCompositeFilter for Layered { impl CreateFilter for Layered { type EntityFiltered<'graph, G> - = T::EntityFiltered<'graph, G> + = T::EntityFiltered<'graph, >::LayeredViewType> where - G: GraphViewOps<'graph>; + G: GraphViewOps<'graph> + InternalFilter<'graph>, + >::LayeredViewType: GraphViewOps<'graph>; type NodeFilter<'graph, G> - = T::NodeFilter<'graph, G> + = T::NodeFilter<'graph, >::LayeredViewType> where - G: GraphView + 'graph; + G: GraphView + InternalFilter<'graph> + 'graph, + >::LayeredViewType: GraphView + 'graph; type FilteredGraph<'graph, G> = G @@ -107,9 +109,10 @@ impl CreateFilter for Layered Result, GraphError> where - G: GraphViewOps<'graph>, + G: GraphViewOps<'graph> + InternalFilter<'graph>, + >::LayeredViewType: GraphViewOps<'graph>, { - self.inner.create_filter(graph) + self.inner.create_filter(graph.layers(self.layer)?) } fn create_node_filter<'graph, G>( @@ -117,9 +120,10 @@ impl CreateFilter for Layered Result, GraphError> where - G: GraphView + 'graph, + G: GraphView + InternalFilter<'graph> + 'graph, + >::LayeredViewType: GraphView + 'graph, { - self.inner.create_node_filter(graph) + self.inner.create_node_filter(graph.layers(self.layer)?) } } diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 88c5add48e..b70bb69496 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -717,13 +717,14 @@ impl EntityExpr for SnapshotLatest { } impl CreateView for Layered { - type View<'graph, G: GraphView + 'graph> = LayeredGraph; + type View<'graph, G: GraphView + 'graph> = LayeredGraph>; fn create_view<'graph, G: GraphView + 'graph>( &self, view: G, - ) -> Result, GraphError> { - view.layers(self.layer.clone()) + ) -> Result>, GraphError> { + let inner = self.inner.create_view(view)?; + inner.layers(self.layer.clone()) } } 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 7f34bfd0aa..e147f9929a 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -1,23 +1,26 @@ use crate::{ db::{ api::view::internal::GraphView, - graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, + graph::views::{ + filter::{ + model::{ + edge_filter::CompositeEdgeFilter, + is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + windowed_filter::Windowed, + CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, + }, + CreateFilter, }, - CreateFilter, + window_graph::WindowedGraph, }, }, errors::GraphError, - prelude::GraphViewOps, + prelude::{GraphViewOps, TimeOps}, }; use raphtory_api::core::{storage::timeindex::EventTime, utils::time::IntoTime}; use std::{fmt, fmt::Display}; @@ -81,14 +84,14 @@ impl TryAsCompositeFilter for SnapshotAt { impl CreateFilter for SnapshotAt { type EntityFiltered<'graph, G> - = T::EntityFiltered<'graph, G> + = T::EntityFiltered<'graph, WindowedGraph> where - G: GraphViewOps<'graph>; + G: GraphViewOps<'graph> + TimeOps<'graph, WindowedViewType = WindowedGraph>; type NodeFilter<'graph, G> - = T::NodeFilter<'graph, G> + = T::NodeFilter<'graph, WindowedGraph> where - G: GraphView + 'graph; + G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph; type FilteredGraph<'graph, G> = G @@ -101,9 +104,9 @@ impl CreateFilter for SnapshotA graph: G, ) -> Result, GraphError> where - G: GraphViewOps<'graph>, + G: GraphViewOps<'graph> + TimeOps<'graph, WindowedViewType = WindowedGraph>, { - self.inner.create_filter(graph) + self.inner.create_filter(graph.snapshot_at(self.time)) } fn create_node_filter<'graph, G>( @@ -111,22 +114,23 @@ impl CreateFilter for SnapshotA graph: G, ) -> Result, GraphError> where - G: GraphView + 'graph, + G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph, { - self.inner.create_node_filter(graph) + self.inner.create_node_filter(graph.snapshot_at(self.time)) } } impl ComposableFilter for SnapshotAt {} impl CreateView for SnapshotAt { - type View<'graph, G: GraphView + 'graph> = T::View<'graph, G>; + type View<'graph, G: GraphView + 'graph> = WindowedGraph>; fn create_view<'graph, G: GraphView + 'graph>( &self, view: G, ) -> Result, GraphError> { - self.inner.create_view(view) + let inner = self.inner.create_view(view)?; + Ok(inner.snapshot_at(self.time)) } } @@ -210,14 +214,14 @@ impl TryAsCompositeFilter for SnapshotLatest { impl CreateFilter for SnapshotLatest { type EntityFiltered<'graph, G> - = T::EntityFiltered<'graph, G> + = T::EntityFiltered<'graph, WindowedGraph> where - G: GraphViewOps<'graph>; + G: GraphViewOps<'graph> + TimeOps<'graph, WindowedViewType = WindowedGraph>; type NodeFilter<'graph, G> - = T::NodeFilter<'graph, G> + = T::NodeFilter<'graph, WindowedGraph> where - G: GraphView + 'graph; + G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph; type FilteredGraph<'graph, G> = G @@ -230,9 +234,9 @@ impl CreateFilter for SnapshotL graph: G, ) -> Result, GraphError> where - G: GraphViewOps<'graph>, + G: GraphViewOps<'graph> + TimeOps<'graph, WindowedViewType = WindowedGraph>, { - self.inner.create_filter(graph) + self.inner.create_filter(graph.snapshot_latest()) } fn create_node_filter<'graph, G>( @@ -240,22 +244,23 @@ impl CreateFilter for SnapshotL graph: G, ) -> Result, GraphError> where - G: GraphView + 'graph, + G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph, { - self.inner.create_node_filter(graph) + self.inner.create_node_filter(graph.snapshot_latest()) } } impl ComposableFilter for SnapshotLatest {} impl CreateView for SnapshotLatest { - type View<'graph, G: GraphView + 'graph> = T::View<'graph, G>; + type View<'graph, G: GraphView + 'graph> = WindowedGraph>; fn create_view<'graph, G: GraphView + 'graph>( &self, view: G, ) -> Result, GraphError> { - self.inner.create_view(view) + let inner = self.inner.create_view(view)?; + Ok(inner.snapshot_latest()) } } 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 2ed1e931c7..8f824157a2 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -100,14 +100,14 @@ impl TryAsCompositeFilter for Windowed { impl CreateFilter for Windowed { type EntityFiltered<'graph, G> - = T::EntityFiltered<'graph, G> + = T::EntityFiltered<'graph, WindowedGraph> where - G: GraphViewOps<'graph>; + G: GraphViewOps<'graph> + TimeOps<'graph, WindowedViewType = WindowedGraph>; type NodeFilter<'graph, G> - = T::NodeFilter<'graph, G> + = T::NodeFilter<'graph, WindowedGraph> where - G: GraphView + 'graph; + G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph; type FilteredGraph<'graph, G> = G @@ -120,9 +120,9 @@ impl CreateFilter for Windowed< graph: G, ) -> Result, GraphError> where - G: GraphViewOps<'graph>, + G: GraphViewOps<'graph> + TimeOps<'graph, WindowedViewType = WindowedGraph>, { - self.inner.create_filter(graph) + self.inner.create_filter(graph.window(self.start.t(), self.end.t())) } fn create_node_filter<'graph, G>( @@ -130,9 +130,9 @@ impl CreateFilter for Windowed< graph: G, ) -> Result, GraphError> where - G: GraphView + 'graph, + G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph, { - self.inner.create_node_filter(graph) + self.inner.create_node_filter(graph.window(self.start.t(), self.end.t())) } } From 5c393639aff69d1ee608b6b9601a0f876cfcc04e Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:39:44 +0100 Subject: [PATCH 048/155] fix tests --- raphtory-tests/tests/test_filters.rs | 36 +++++++++---------- .../graph/views/filter/model/node_expr/mod.rs | 6 ++++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index baf73023a5..a7e7f3a266 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -11242,8 +11242,8 @@ mod test_edge_property_filter { let filter = EdgeFilter .property("p2") .temporal() - .all() - .is_in(vec![Prop::U64(6)]); + .is_in(vec![Prop::U64(6)]) + .all(); let expected_results = vec![ "2->1", "3->1", @@ -11310,8 +11310,8 @@ mod test_edge_property_filter { let filter = EdgeFilter .property("p2") .temporal() - .all() - .is_not_in(vec![Prop::U64(6)]); + .is_not_in(vec![Prop::U64(6)]) + .all(); let expected_results = vec!["1->2", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -11442,8 +11442,8 @@ mod test_edge_property_filter { let filter = EdgeFilter .property("p10") .temporal() - .any() - .starts_with("Pape"); + .starts_with("Pape") + .any(); let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -11526,8 +11526,8 @@ mod test_edge_property_filter { let filter = EdgeFilter .property("p20") .temporal() - .all() - .starts_with("Gold"); + .starts_with("Gold") + .all(); let expected_results: Vec<&str> = vec!["1->2", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -11568,8 +11568,8 @@ mod test_edge_property_filter { let filter = EdgeFilter .property("p10") .temporal() - .any() - .ends_with("ship"); + .ends_with("ship") + .any(); let expected_results: Vec<&str> = vec!["2->3"]; assert_filter_edges_results( init_edges_graph, @@ -11652,8 +11652,8 @@ mod test_edge_property_filter { let filter = EdgeFilter .property("p20") .temporal() - .all() - .ends_with("ship"); + .ends_with("ship") + .all(); let expected_results: Vec<&str> = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -11694,8 +11694,8 @@ mod test_edge_property_filter { let filter = EdgeFilter .property("p10") .temporal() - .any() - .contains("Paper"); + .contains("Paper") + .any(); let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -11795,8 +11795,8 @@ mod test_edge_property_filter { let filter = EdgeFilter .property("p10") .temporal() - .any() - .not_contains("ship"); + .not_contains("ship") + .any(); let expected_results: Vec<&str> = vec!["1->2", "2->1"]; assert_filter_edges_results( init_edges_graph, @@ -11858,8 +11858,8 @@ mod test_edge_property_filter { let filter = EdgeFilter .property("p30") .temporal() - .all() - .not_contains("ship"); + .not_contains("ship") + .all(); let expected_results: Vec<&str> = vec!["2->3"]; assert_filter_edges_results( init_edges_graph, 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 index c65cefaa02..0ae1f84599 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -138,6 +138,12 @@ pub trait TemporalPropOps: Sized { ) -> StringFilter, R, ::Marker> { self.into_expr().fuzzy_search(rhs, levenshtein_distance, prefix_match) } + fn is_in>(self, values: impl IntoIterator) -> PropValueSetFilter, ::Marker> { + self.into_expr().is_in(values) + } + fn is_not_in>(self, values: impl IntoIterator) -> PropValueSetFilter, ::Marker> { + self.into_expr().is_not_in(values) + } fn is_true(self) -> BinaryCmpFilter, Prop, ::Marker> { self.into_expr().is_true() } From 4a5c80477b6891c267c2c2cf28d920a340a36475 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:01:29 +0100 Subject: [PATCH 049/155] reorder temporal quantifier chains: comparator before quantifier; restore is_in/is_not_in replaced as eq/ne --- raphtory-tests/tests/edge_expr_filter.rs | 29 ++++++++++-------------- raphtory-tests/tests/test_filters.rs | 4 ++-- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs index 5a1dafc05d..15abdf2d5a 100644 --- a/raphtory-tests/tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -302,7 +302,7 @@ fn genre_graph() -> Graph { fn test_edge_quantified_any_contains() { let g = genre_graph(); // any temporal value of "tag" contains "rock" - let filter = EdgeFilter.property("tag").temporal().any().contains("rock"); + let filter = EdgeFilter.property("tag").temporal().contains("rock").any(); let result = g.filter(filter).unwrap(); // A->B has "rock" and "rock-n-roll" (contains "rock"), C->D has neither assert_eq!(sorted_edges(result), vec!["A->B"]); @@ -311,7 +311,7 @@ fn test_edge_quantified_any_contains() { #[test] fn test_edge_quantified_any_starts_with() { let g = genre_graph(); - let filter = EdgeFilter.property("tag").temporal().any().starts_with("rock"); + let filter = EdgeFilter.property("tag").temporal().starts_with("rock").any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -319,7 +319,7 @@ fn test_edge_quantified_any_starts_with() { #[test] fn test_edge_quantified_any_ends_with() { let g = genre_graph(); - let filter = EdgeFilter.property("tag").temporal().any().ends_with("roll"); + let filter = EdgeFilter.property("tag").temporal().ends_with("roll").any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -331,7 +331,7 @@ fn test_edge_quantified_any_not_contains() { // A->B: "metal" and "rock-n-roll" don't, but "rock" does → any not_contains is true for A->B // C->D: "jazz" and "blues" don't contain "rock" → any not_contains is true for C->D // Both edges pass (any value doesn't contain "rock") - let filter = EdgeFilter.property("tag").temporal().any().not_contains("rock"); + let filter = EdgeFilter.property("tag").temporal().not_contains("rock").any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B", "C->D"]); } @@ -346,7 +346,7 @@ fn test_edge_quantified_all_contains() { g.add_edge(1, "C", "D", [("tag", Prop::str("rock"))], None).unwrap(); g.add_edge(2, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); - let filter = EdgeFilter.property("tag").temporal().all().contains("rock"); + let filter = EdgeFilter.property("tag").temporal().contains("rock").all(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -359,7 +359,7 @@ fn test_edge_quantified_all_starts_with() { g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); g.add_edge(2, "C", "D", [("tag", Prop::str("rock-steady"))], None).unwrap(); - let filter = EdgeFilter.property("tag").temporal().all().starts_with("rock"); + let filter = EdgeFilter.property("tag").temporal().starts_with("rock").all(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -375,8 +375,8 @@ fn test_edge_quantified_any_is_in() { let filter = EdgeFilter .property("tag") .temporal() - .any() - .is_in([Prop::str("metal"), Prop::str("blues")]); + .is_in([Prop::str("metal"), Prop::str("blues")]) + .any(); let result = g.filter(filter).unwrap(); // A->B has "metal" → passes; C->D has "blues" → passes assert_eq!(sorted_edges(result), vec!["A->B", "C->D"]); @@ -409,8 +409,8 @@ fn test_edge_quantified_all_is_in() { let filter = EdgeFilter .property("tag") .temporal() - .all() - .is_in([Prop::str("rock"), Prop::str("metal")]); + .is_in([Prop::str("rock"), Prop::str("metal")]) + .all(); let result = g.filter(filter).unwrap(); // A->B: "rock" ✓, "metal" ✓ → passes; C->D: "jazz" ✗ → fails assert_eq!(sorted_edges(result), vec!["A->B"]); @@ -428,8 +428,8 @@ fn test_edge_quantified_all_is_not_in() { let filter = EdgeFilter .property("tag") .temporal() - .all() - .is_not_in([Prop::str("rock"), Prop::str("metal")]); + .is_not_in([Prop::str("rock"), Prop::str("metal")]) + .all(); let result = g.filter(filter).unwrap(); // A->B: "rock" is in set → fails; C->D: "jazz" ✓, "blues" ✓ → passes assert_eq!(sorted_edges(result), vec!["C->D"]); @@ -614,8 +614,6 @@ fn test_edge_property_is_false() { #[test] fn test_edge_quantified_any_sum_gt() { - // .any().sum() on scalar temporal values: sum() is pass-through for scalars, - // validating the re-aggregation chain compiles and produces correct output. let g = Graph::new(); g.add_edge(1, "A", "B", [("score", Prop::I64(3))], None).unwrap(); g.add_edge(2, "A", "B", [("score", Prop::I64(8))], None).unwrap(); @@ -630,7 +628,6 @@ fn test_edge_quantified_any_sum_gt() { #[test] fn test_edge_quantified_all_min_ge() { - // .all().min().ge(n): validates the all + re-aggregation chain compiles. let g = Graph::new(); g.add_edge(1, "A", "B", [("score", Prop::I64(3))], None).unwrap(); g.add_edge(2, "A", "B", [("score", Prop::I64(8))], None).unwrap(); @@ -645,7 +642,6 @@ fn test_edge_quantified_all_min_ge() { #[test] fn test_edge_quantified_any_any_contains() { - // .any().any().contains(s): double-any chain via NestedMapEdgeExpr. let g = genre_graph(); let filter = EdgeFilter.property("tag").temporal().any().any().contains("rock"); let result = g.filter(filter).unwrap(); @@ -654,7 +650,6 @@ fn test_edge_quantified_any_any_contains() { #[test] fn test_edge_quantified_any_last_is_in() { - // .any().last().is_in([...]): re-aggregate with last then set-check. let g = Graph::new(); g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index a7e7f3a266..0e3af8e2f4 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -3678,7 +3678,7 @@ mod test_node_property_filter { let filter = NodeFilter .property("p2") .temporal() - .eq(Prop::U64(2)) + .is_in(vec![Prop::U64(2)]) .any(); let expected_results = vec!["2"]; assert_filter_nodes_results( @@ -3736,7 +3736,7 @@ mod test_node_property_filter { let filter = NodeFilter .property("p2") .temporal() - .ne(Prop::U64(2)) + .is_not_in(vec![Prop::U64(2)]) .all(); let expected_results = vec!["3"]; assert_filter_nodes_results( From a571fbf14b5d2018ce5af3cacf69d430341541e9 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:00:26 +0100 Subject: [PATCH 050/155] rewrite tests --- raphtory-tests/tests/edge_expr_filter.rs | 54 +++++++++++++++--------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs index 15abdf2d5a..27fbe2234c 100644 --- a/raphtory-tests/tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -615,13 +615,15 @@ fn test_edge_property_is_false() { #[test] fn test_edge_quantified_any_sum_gt() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(3))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(8))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "C", "D", [("score", Prop::I64(4))], None).unwrap(); + // A->B: t=1 has a list whose sum = 11 (> 5); t=2 has a list whose sum = 3 + g.add_edge(1, "A", "B", [("score", Prop::List(vec![Prop::I64(3), Prop::I64(8)].into()))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2)].into()))], None).unwrap(); + // C->D: t=1 sum = 3, t=2 sum = 5 (neither > 5) + g.add_edge(1, "C", "D", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2)].into()))], None).unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(4)].into()))], None).unwrap(); - // A->B has 8 > 5, C->D has no value > 5 - let filter = EdgeFilter.property("score").temporal().any().sum().gt(5i64); + // any temporal snapshot where sum of list > 5 + let filter = EdgeFilter.property("score").temporal().sum().gt(5i64).any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -629,21 +631,31 @@ fn test_edge_quantified_any_sum_gt() { #[test] fn test_edge_quantified_all_min_ge() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(3))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(8))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "C", "D", [("score", Prop::I64(9))], None).unwrap(); + // A->B: t=1 min = 3, t=2 min = 5 (all ≥ 3) + g.add_edge(1, "A", "B", [("score", Prop::List(vec![Prop::I64(3), Prop::I64(8)].into()))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::List(vec![Prop::I64(5), Prop::I64(9)].into()))], None).unwrap(); + // C->D: t=1 min = 1 (< 3), so not all snapshots pass + g.add_edge(1, "C", "D", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(9)].into()))], None).unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::List(vec![Prop::I64(3), Prop::I64(5)].into()))], None).unwrap(); - // A->B: all values >= 3 → passes; C->D: 1 < 3 → fails - let filter = EdgeFilter.property("score").temporal().all().min().ge(3i64); + // all temporal snapshots where min of list >= 3 + let filter = EdgeFilter.property("score").temporal().min().ge(3i64).all(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } #[test] fn test_edge_quantified_any_any_contains() { - let g = genre_graph(); - let filter = EdgeFilter.property("tag").temporal().any().any().contains("rock"); + let g = Graph::new(); + // A->B: t=1 list has "rock" (any element contains "rock") + g.add_edge(1, "A", "B", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("metal")].into()))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::List(vec![Prop::str("jazz"), Prop::str("blues")].into()))], None).unwrap(); + // C->D: no list has any element containing "rock" + g.add_edge(1, "C", "D", [("tag", Prop::List(vec![Prop::str("jazz"), Prop::str("blues")].into()))], None).unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::List(vec![Prop::str("folk"), Prop::str("pop")].into()))], None).unwrap(); + + // any temporal snapshot where any list element contains "rock" + let filter = EdgeFilter.property("tag").temporal().contains("rock").any().any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -651,17 +663,19 @@ fn test_edge_quantified_any_any_contains() { #[test] fn test_edge_quantified_any_last_is_in() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + // A->B: last snapshot (t=2) has "metal" as an element + g.add_edge(1, "A", "B", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("folk")].into()))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::List(vec![Prop::str("pop"), Prop::str("metal")].into()))], None).unwrap(); + // C->D: last (and only) snapshot has no element in {"metal"} + g.add_edge(1, "C", "D", [("tag", Prop::List(vec![Prop::str("jazz"), Prop::str("blues")].into()))], None).unwrap(); - // A->B: values ["rock", "metal"] → any in {"metal"} → true; C->D: ["jazz"] → false + // last temporal snapshot's list — any element is in {"metal"} let filter = EdgeFilter .property("tag") .temporal() - .any() .last() - .is_in([Prop::str("metal")]); + .is_in([Prop::str("metal")]) + .any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } From 4d8c56ff2128f91ededf0106b0ffb04ea2aa9162 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:58:51 +0100 Subject: [PATCH 051/155] fix tests --- raphtory-tests/tests/edge_expr_filter.rs | 59 +++++++++++++----------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs index 27fbe2234c..80eea2ea1a 100644 --- a/raphtory-tests/tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -441,16 +441,15 @@ fn test_edge_quantified_all_is_not_in() { #[test] fn test_edge_aggregated_last_then_sum() { - // temporal values are themselves lists — re-aggregate the aggregated list - // E.g. .temporal().last() gives the last single value; chaining .sum() wraps it - // in an UnwrapOptPropEdgeExpr and sums what is available. - // Simple case: single temporal value so last == only value, sum of that single number. + // Property is list-valued at each timestamp. + // .last() picks the last snapshot's list, .sum() reduces it to a scalar. let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(10))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(20))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(5))], None).unwrap(); + // A->B: last snapshot = [3,4,5], sum = 12 (> 10) + g.add_edge(1, "A", "B", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2)].into()))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::List(vec![Prop::I64(3), Prop::I64(4), Prop::I64(5)].into()))], None).unwrap(); + // C->D: last (and only) snapshot = [1,2,3], sum = 6 (not > 10) + g.add_edge(1, "C", "D", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2), Prop::I64(3)].into()))], None).unwrap(); - // last temporal value: A->B = 20, C->D = 5; then sum of that single value = itself let filter = EdgeFilter.property("score").temporal().last().sum().gt(10i64); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); @@ -485,45 +484,53 @@ fn test_edge_aggregated_first_then_ends_with() { #[test] fn test_edge_aggregated_last_then_len() { + // Property is list-valued at each timestamp. + // .last() picks the last snapshot's list, .len() returns its length. let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(42))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(7))], None).unwrap(); + // A->B: last snapshot = [20, 30], len = 2 + g.add_edge(1, "A", "B", [("score", Prop::List(vec![Prop::I64(10)].into()))], None).unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::List(vec![Prop::I64(20), Prop::I64(30)].into()))], None).unwrap(); + // C->D: last snapshot = [5, 10, 15], len = 3 + g.add_edge(1, "C", "D", [("score", Prop::List(vec![Prop::I64(5), Prop::I64(10), Prop::I64(15)].into()))], None).unwrap(); - // last() gives Option; len() wraps as list of one element → len = 1 - // When None, len = 0. Here both edges have a last value, so len = 1 for both. - let filter = EdgeFilter.property("score").temporal().last().len().eq(1usize); + let filter = EdgeFilter.property("score").temporal().last().len().eq(2usize); let result = g.filter(filter).unwrap(); - assert_eq!(sorted_edges(result), vec!["A->B", "C->D"]); + assert_eq!(sorted_edges(result), vec!["A->B"]); } #[test] fn test_edge_aggregated_last_then_any_is_in() { + // Property is list-valued at each timestamp. + // .last() picks the last snapshot's list, .is_in([...]).any() checks if any element is in the set. let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + // A->B: last snapshot = ["folk","metal"] — "metal" ∈ {"metal","blues"} + g.add_edge(1, "A", "B", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("pop")].into()))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::List(vec![Prop::str("folk"), Prop::str("metal")].into()))], None).unwrap(); + // C->D: last (and only) snapshot = ["jazz","pop"] — neither in {"metal","blues"} + g.add_edge(1, "C", "D", [("tag", Prop::List(vec![Prop::str("jazz"), Prop::str("pop")].into()))], None).unwrap(); - // last value: A->B = "metal", C->D = "jazz" - // .any().is_in([...]) — since last() produces a single-element list, any == the value itself let filter = EdgeFilter .property("tag") .temporal() .last() - .any() - .is_in([Prop::str("metal"), Prop::str("blues")]); + .is_in([Prop::str("metal"), Prop::str("blues")]) + .any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } #[test] fn test_edge_aggregated_last_then_all_contains() { + // Property is list-valued at each timestamp. + // .last() picks the last snapshot's list, .contains("rock").all() checks all elements contain "rock". let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + // A->B: last snapshot = ["rock","rock-n-roll"] — all contain "rock" + g.add_edge(1, "A", "B", [("tag", Prop::List(vec![Prop::str("jazz")].into()))], None).unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("rock-n-roll")].into()))], None).unwrap(); + // C->D: last (and only) snapshot = ["rock","jazz"] — "jazz" doesn't contain "rock" + g.add_edge(1, "C", "D", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("jazz")].into()))], None).unwrap(); - // last value: A->B = "rock-n-roll" (contains "rock"), C->D = "jazz" (doesn't) - let filter = EdgeFilter.property("tag").temporal().last().all().contains("rock"); + let filter = EdgeFilter.property("tag").temporal().last().contains("rock").all(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } From bff4ce7a354bcb1b69319ecfbeb7915d674eb76a Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:25:48 +0100 Subject: [PATCH 052/155] fix test --- raphtory-tests/tests/test_filters.rs | 9 ++++++--- .../src/db/graph/views/filter/model/filter_operator.rs | 3 +-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 0e3af8e2f4..e28d64d4cd 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -2536,7 +2536,8 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_ne() { let filter = NodeFilter.node_type().ne("fire_nation"); - let expected_results = vec!["2", "4"]; + // node 4 has no node_type; None cannot satisfy a value comparison. + let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -2593,7 +2594,8 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_not_in() { let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]); - let expected_results = vec!["2", "4"]; + // node 4 has no node_type; None cannot satisfy a value comparison. + let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -2707,7 +2709,8 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_node_type_contains_not() { let filter = NodeFilter.node_type().not_contains("fire"); - let expected_results = vec!["2", "4"]; + // node 4 has no node_type; None cannot satisfy a value comparison. + let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, 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 cb135e30f0..cec15079e4 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -92,8 +92,7 @@ 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), + _ => false, } } } From a19e539c0d0b03cbead94f482d9fc418fc41b8b8 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:39:19 +0100 Subject: [PATCH 053/155] fix tests --- raphtory-tests/tests/test_filters.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index e28d64d4cd..fae5d5bb87 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -3386,7 +3386,9 @@ mod test_node_property_filter { ); let filter = NodeFilter.property("p1").temporal().ne("Gold_ship").all(); - let expected_results = vec!["1"]; + // Both nodes have non-empty p1 streams whose values are all ne "Gold_ship". + // Nodes 3 and 4 have empty p1 streams and are correctly rejected. + let expected_results = vec!["1", "2"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -3494,7 +3496,9 @@ mod test_node_property_filter { ); let filter = NodeFilter.property("p2").temporal().le(10u64).all(); - let expected_results = vec!["3"]; + // Node 2 (p2=2) and node 3 (p2=6) both have non-empty streams satisfying le(10). + // Nodes 1 and 4 have empty p2 streams and are correctly rejected. + let expected_results = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, From ca43cd4f7fd1106146578fe3d2a41596bc5c8d6a Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:36:06 +0100 Subject: [PATCH 054/155] fix tests --- .../views/filter/model/node_expr/exprs.rs | 3 + .../views/filter/model/node_expr/filters.rs | 90 +++++++++++++++++-- .../graph/views/filter/model/node_expr/mod.rs | 10 +++ 3 files changed, 98 insertions(+), 5 deletions(-) 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 11e4fe6ef5..f358033cbd 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 @@ -340,6 +340,9 @@ impl EntityExpr for DegreeExpr fn prop_type(&self) -> PropType { PropType::U64 } + fn nullable(&self) -> bool { + false + } } impl NodeExpr for DegreeExpr { 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 345171ca66..e286d3e220 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 @@ -139,6 +139,73 @@ fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { Ok(()) } +/// Pick the more specific of the two known prop types. +/// +/// Compiled `NodeOp`s and `EntityExpr`s may both have a known prop type, but +/// expression-level info (e.g. `DegreeExpr::prop_type()` → U64) is not always +/// propagated through generic wrappers like `Map`. Prefer whichever side +/// has a concrete type so validation can fire early. +fn resolved_prop_type(expr_pt: PropType, op_pt: PropType) -> PropType { + if expr_pt != PropType::Empty { + expr_pt + } else { + op_pt + } +} + +/// Reject a constant RHS value whose type cannot be coerced to the LHS type. +/// +/// Only fires when both sides are known and the RHS is a literal/const. Defers +/// to runtime when the LHS type is unknown (`PropType::Empty`) or the RHS isn't +/// a const value. +fn validate_const_castable( + lhs_pt: &PropType, + rhs_const: Option<&Prop>, +) -> Result<(), GraphError> { + if *lhs_pt == PropType::Empty { + return Ok(()); + } + if let Some(rhs) = rhs_const { + if rhs.dtype() != *lhs_pt && rhs.clone().try_cast(lhs_pt.clone()).is_none() { + return Err(GraphError::InvalidFilter(format!( + "value {:?} of type {} cannot be coerced to {}", + rhs, + rhs.dtype(), + lhs_pt + ))); + } + } + Ok(()) +} + +/// Cast every value in an `is_in`/`is_not_in` set to the LHS type. +/// +/// If the LHS type is unknown (`PropType::Empty`), the values are returned +/// unchanged and coercion is deferred to runtime. Otherwise, any value whose +/// type cannot be coerced produces `Err(InvalidFilter)`. Successful casts are +/// substituted so the runtime set comparison sees same-typed values. +fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result, GraphError> { + if *lhs_pt == PropType::Empty { + return Ok(values); + } + values + .into_iter() + .map(|v| { + if v.dtype() == *lhs_pt { + Ok(v) + } else { + let original_dtype = v.dtype(); + v.clone().try_cast(lhs_pt.clone()).ok_or_else(|| { + GraphError::InvalidFilter(format!( + "value {:?} of type {} cannot be coerced to {}", + v, original_dtype, lhs_pt + )) + }) + } + }) + .collect() +} + impl CreateFilter for BinaryCmpFilter where L: NodeExpr, @@ -167,10 +234,12 @@ where self, graph: G, ) -> Result, GraphError> { + let expr_pt = self.left.prop_type(); 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())?; - // TODO: validate_binary_op(&self.op, &left.prop_type(), &right.prop_type())?; + let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); + validate_binary_op(&self.op, &lhs_pt)?; + validate_const_castable(&lhs_pt, right.const_value().as_ref().and_then(|o| o.as_ref()))?; Ok(Arc::new(BinaryCmpNodeOp { left, right, @@ -249,6 +318,12 @@ where self, graph: G, ) -> Result, GraphError> { + if !self.expr.nullable() { + return Err(GraphError::InvalidFilter(format!( + "operator {:?} is not valid for non-nullable expression", + self.op + ))); + } let inner = self.expr.create_node_op(graph)?; Ok(UnaryNodeOp { inner, op: self.op }) } @@ -333,9 +408,10 @@ impl CreateFilter for StringFilter { self, graph: G, ) -> Result, GraphError> { + let expr_pt = self.left.prop_type(); let left = self.left.create_node_op(graph.clone())?; let right = self.right.create_node_op(graph)?; - validate_string_op(&left.prop_type())?; + validate_string_op(&resolved_prop_type(expr_pt, left.prop_type()))?; Ok(Arc::new(StringNodeOp { left, right, @@ -404,9 +480,13 @@ impl CreateFilter for PropValueSetFilter { self, graph: G, ) -> Result, GraphError> { + let expr_pt = self.expr.prop_type(); + let inner = self.expr.create_node_op(graph)?; + let lhs_pt = resolved_prop_type(expr_pt, inner.prop_type()); + let values = coerce_set_values(&lhs_pt, self.values)?; Ok(PropValueSetNodeOp { - inner: self.expr.create_node_op(graph)?, - values: self.values, + inner, + values, op: self.op, }) } 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 index 0ae1f84599..4227d5fd24 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -64,6 +64,16 @@ pub trait EntityExpr: Clone + Send + Sync + 'static { fn prop_type(&self) -> PropType { PropType::Empty } + + /// Whether this expression can produce `None` at runtime. + /// + /// Defaults to `true` (most expressions read optional properties). + /// Override to `false` for expressions that always produce `Some(_)` + /// (e.g. degree). Filters like `is_some`/`is_none` are meaningless on + /// non-nullable expressions and should be rejected at compile time. + fn nullable(&self) -> bool { + true + } } /// Sealed trait to avoid conflicts between node and edge expressions From 0383aa0ac3a8e30e48b50563fe35307718646beb Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:58:08 +0100 Subject: [PATCH 055/155] fix tests --- raphtory-tests/tests/test_filters.rs | 35 ++++--------- .../graph/views/filter/model/edge_expr/ops.rs | 52 ++++++------------- 2 files changed, 28 insertions(+), 59 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index fae5d5bb87..d739b01b28 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -1661,6 +1661,7 @@ mod test_node_filter { model::{ degree_filter::DegreeFilterFactory, node_filter::ops::{NodeFilterOps}, + not_filter::NotFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, ComposableFilter, CompositeNodeFilter, NodeViewFilterOps, PropertyFilterFactory, TryAsCompositeFilter, ViewWrapOps, @@ -3134,12 +3135,10 @@ mod test_node_filter { #[test] fn test_is_active_node_window_not() { - let filter = NodeFilter - .window(1, 10) - .is_active() - .try_as_composite_node_filter() - .unwrap(); - let filter = CompositeNodeFilter::Not(Box::new(filter)); + // is_active() returns true/false (no None case), so set-complement and + // SQL-NULL agree. Use NotFilter wrapper directly to avoid the composite + // path. + let filter = NotFilter(NodeFilter.window(1, 10).is_active()); let expected_results = vec![]; assert_filter_nodes_results( init_nodes_graph, @@ -4292,24 +4291,12 @@ mod test_node_property_filter { #[test] fn test_filter_nodes_for_not_property() { - let filter = NotFilter(NodeFilter.property("p10").contains("Paper")); - let expected_results: Vec<&str> = vec!["4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p10").contains("Paper").not(); + // Under SQL-NULL semantics, .not() rejects nodes whose property is absent + // (None cannot satisfy a value comparison). Use "ship" so nodes 1 and 3 + // (p10 = "Paper_airplane", does not contain "ship") pass; node 2 + // (p10 = "Paper_ship") and node 4 (no p10) are rejected. + let filter = NodeFilter.property("p10").contains("ship").not(); + let expected_results: Vec<&str> = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 445faa3d8f..f22731e1fd 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -20,7 +20,11 @@ use std::{collections::HashSet, hash::Hash}; use super::EdgeOp; use crate::db::{ api::state::ops::NodeOp, - graph::views::filter::model::{edge_filter::Endpoint, property_filter::evaluate::aggregate_values}, + graph::views::filter::model::{ + edge_filter::Endpoint, + node_expr::ops::{broadcast_binary, broadcast_unary}, + property_filter::evaluate::aggregate_values, + }, }; use raphtory_api::core::entities::properties::prop::PropArray; use std::sync::Arc; @@ -262,17 +266,10 @@ impl<'g> EdgeOp for ListAwareCmpEdgeOp<'g> { fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { let lv = self.left.apply(storage, edge); - let rhs = self.right.apply(storage, edge)?; + let rhs = self.right.apply(storage, edge); let op = &self.op; - aggregate_values(lv, &|pi| { - let bools: Vec = pi - .map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))) - .collect(); - if bools.is_empty() { - None - } else { - Some(Prop::List(PropArray::from(bools))) - } + broadcast_binary(lv, rhs, &|lv, rhs| { + Some(Prop::Bool(Prop::binary_cmp(op, &lv?, &rhs?))) }) } } @@ -295,15 +292,8 @@ impl<'g> EdgeOp for ListAwareStringEdgeOp<'g> { let lv = self.left.apply(storage, edge); let rhs = self.right.apply(storage, edge); let op = &self.op; - aggregate_values(lv, &|pi| { - let bools: Vec = pi - .map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))) - .collect(); - if bools.is_empty() { - None - } else { - Some(Prop::List(PropArray::from(bools))) - } + broadcast_binary(lv, rhs, &|lv, rhs| { + Some(Prop::Bool(Option::::string_cmp(op, &lv, &rhs))) }) } } @@ -323,23 +313,15 @@ impl<'g> EdgeOp for ListAwareSetEdgeOp<'g> { type Output = Option; fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let lv = self.inner.apply(storage, edge); + let vals = self.inner.apply(storage, edge); let values = &self.values; let op = &self.op; - aggregate_values(lv, &|pi| { - let bools: Vec = pi - .map(|v| { - Prop::Bool(match op { - SetOp::IsIn => values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), - SetOp::IsNotIn => values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), - }) - }) - .collect(); - if bools.is_empty() { - None - } else { - Some(Prop::List(PropArray::from(bools))) - } + broadcast_unary(vals, |v| { + let v = v?; + Some(Prop::Bool(match op { + SetOp::IsIn => values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), + SetOp::IsNotIn => values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), + })) }) } } From 969b9b90772cdeaf584220f094b61ee3bf795e68 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:23:36 +0100 Subject: [PATCH 056/155] cleanup: stale comments, dead code, get_prop_id, drop static NodeFilter::property, reduce_seq iterator --- .../views/filter/model/edge_expr/exprs.rs | 8 ++-- .../views/filter/model/node_expr/exprs.rs | 18 ++++---- .../views/filter/model/node_expr/filters.rs | 4 +- .../graph/views/filter/model/node_expr/mod.rs | 3 -- .../graph/views/filter/model/node_expr/ops.rs | 43 ++----------------- .../views/filter/model/node_expr/tests.rs | 10 ++--- .../views/filter/model/node_filter/mod.rs | 14 ------ .../filter/model/property_filter/evaluate.rs | 41 +++++++++--------- 8 files changed, 43 insertions(+), 98 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs index 4075769fa4..cbcd7b45bf 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs @@ -17,9 +17,9 @@ impl EdgeExpr for Property { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let (prop_id, _) = graph + let prop_id = graph .edge_meta() - .get_prop_id_and_type(&self.name, false) + .get_prop_id(&self.name, false) .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; Ok(Arc::new(EdgePropOp { graph, prop_id })) } @@ -30,9 +30,9 @@ impl EdgeExpr for Metadata { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let (prop_id, _) = graph + let prop_id = graph .edge_meta() - .get_prop_id_and_type(&self.name, true) + .get_prop_id(&self.name, true) .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; Ok(Arc::new(EdgeMetaOp { graph, prop_id })) } 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 f358033cbd..e0e6f75dad 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 @@ -64,7 +64,7 @@ use super::{ops::{ AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NodeMetaOp, NodePropOp, SumNodeOp, TemporalNodePropOp, -}, AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, NodeExpr, NodeExprMarker, SumEdgeOp}; +}, AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, NodeExpr, SumEdgeOp}; use crate::{ db::{ api::{ @@ -369,9 +369,9 @@ impl NodeExpr for Property { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let (prop_id, _) = graph + let prop_id = graph .node_meta() - .get_prop_id_and_type(&self.name, false) + .get_prop_id(&self.name, false) .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; Ok(Arc::new(NodePropOp { graph, prop_id })) } @@ -386,9 +386,9 @@ impl NodeExpr for Metadata { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let (prop_id, _) = graph + let prop_id = graph .node_meta() - .get_prop_id_and_type(&self.name, true) + .get_prop_id(&self.name, true) .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; Ok(Arc::new(NodeMetaOp { graph, prop_id })) } @@ -427,9 +427,9 @@ impl Result> + 'g>, GraphError> { - let (prop_id, _) = graph + let prop_id = graph .node_meta() - .get_prop_id_and_type(&self.name, false) + .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(TemporalNodePropOp { graph, prop_id }.map(|a| Some(a)))) @@ -441,9 +441,9 @@ impl Result> + 'g>, GraphError> { - let (prop_id, _) = graph + let prop_id = graph .edge_meta() - .get_prop_id_and_type(&self.name, false) + .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(TemporalEdgePropOp { graph, prop_id })) 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 e286d3e220..9f7d9f28cb 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 @@ -45,7 +45,7 @@ use super::{ BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, - AllExpr, AnyExpr, EntityExpr, NodeExpr, NodeExprMarker, + AllExpr, AnyExpr, EntityExpr, NodeExpr, }; use crate::{ db::{ @@ -694,8 +694,6 @@ impl En } } -impl NodeExprMarker for BinaryCmpFilter {} - impl NodeExpr for BinaryCmpFilter { fn create_node_op<'g, G: GraphView + 'g>( &self, 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 index 4227d5fd24..fe4854c0f7 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -76,9 +76,6 @@ pub trait EntityExpr: Clone + Send + Sync + 'static { } } -/// Sealed trait to avoid conflicts between node and edge expressions -pub(crate) trait NodeExprMarker {} - // ───────────────────────────────────────────────────────────────────────────── // TemporalPropOps — unified aggregation and comparison on TemporalProp // ───────────────────────────────────────────────────────────────────────────── 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 index 4b02a5634f..79c0c1d02f 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -311,8 +311,9 @@ impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { // These ops implement NodeExpr for BinaryCmpNodeFilter, StringNodeFilter, and // PropValueSetFilter respectively, enabling mid-chain use before .any()/.all(). // -// Each uses aggregate_values so arbitrary nesting depth is handled automatically: -// temporal().sum().gt(5).any() +// Each uses broadcasting so that comparisons applied to a `Prop::List(...)` +// fan out element-wise; scalar inputs are passed through to the op directly. +// temporal().gt(5).any() // temporal().contains("rock").all() // temporal().is_in([...]).any() // ───────────────────────────────────────────────────────────────────────────── @@ -514,44 +515,6 @@ impl<'g> NodeOp for OrBoolNodeOp<'g> { } } -// ───────────────────────────────────────────────────────────────────────────── -// UnwrapOptPropOp<'g> — converts Option → Prop for nested aggregation -// ───────────────────────────────────────────────────────────────────────────── - -/// Converts `Option` → `Prop` so that aggregator ops (`SumNodeOp`, etc.) -/// can operate on a value produced by a prior aggregation step. -/// -/// Used internally when chaining e.g. `.temporal().last().sum()`: -/// `LastExpr` outputs `Option { - pub(crate) inner: Arc> + 'g>, -} - -impl<'g> Clone for UnwrapOptPropOp<'g> { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } -} - -impl<'g> NodeOp for UnwrapOptPropOp<'g> { - type Output = Prop; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Prop { - match self.inner.apply(storage, node) { - Some(Prop::List(arr)) => Prop::List(arr), - Some(v) => Prop::List(PropArray::from(vec![v])), - None => Prop::List(PropArray::from(vec![])), - } - } -} - // ───────────────────────────────────────────────────────────────────────────── // PropValueSetNodeOp<'g> — is_in / is_not_in for Option (linear scan) // ───────────────────────────────────────────────────────────────────────────── 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 ea855de0ac..07033171dc 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 @@ -513,14 +513,14 @@ fn build_bool_graph() -> Graph { #[test] fn is_true_keeps_only_true_nodes() { let g = build_bool_graph(); - let filter = NodeFilter::property("active").is_true(); + 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(); + let filter = NodeFilter.property("active").is_false(); assert_eq!(filtered_names(filter, g), vec!["off"]); } @@ -528,7 +528,7 @@ fn is_false_keeps_only_false_nodes() { 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 filter = NodeFilter.property("active").is_true(); let names = filtered_names(filter, g); assert!(!names.contains(&"na".to_string())); } @@ -538,7 +538,7 @@ fn is_true_excludes_absent_property() { #[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 filter = NodeFilter.property("score").starts_with(Prop::Str("x".into())); let result = filter.create_filter(g); assert!( result.is_err(), @@ -552,7 +552,7 @@ fn ordering_op_on_bool_prop_returns_error() { 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 filter = NodeFilter.property("flag").gt(Prop::Bool(false)); let result = filter.create_filter(g); assert!( result.is_err(), 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 abadc674d7..0ed698a89f 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,20 +140,6 @@ impl TryAsCompositeFilter for NodeFilter { } } -impl NodeFilter { - /// 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 { type Wrapped = T; diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index 91fccf292b..874dc6e3b5 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -227,23 +227,27 @@ pub fn reduce_float>( } } -/// Reduce a flat `Vec` using `op`. All elements must have the same numeric type. -/// Returns `None` if the vector is empty, the type is non-numeric, or the op is not -/// a numeric aggregator (Sum/Avg/Min/Max). -pub fn reduce_seq(vs: Vec, op: Op) -> Option { - if vs.is_empty() || matches!(vs.first(), Some(Prop::List(_))) { - return None; - } - let inner_dtype = vs.first()?.dtype(); +/// Reduce a sequence of `Prop` values using `op`. All elements must have the +/// same numeric type. Returns `None` if the sequence is empty, the type is +/// non-numeric, or the op is not a numeric aggregator (Sum/Avg/Min/Max). +pub fn reduce_seq(vs: impl IntoIterator, op: Op) -> Option { + let mut iter = vs.into_iter().peekable(); + let inner_dtype = { + let first = iter.peek()?; + if matches!(first, Prop::List(_)) { + return None; + } + first.dtype() + }; match inner_dtype { - PropType::U8 => reduce_unsigned(vs, |x| Prop::U8(x as u8), op), - PropType::U16 => reduce_unsigned(vs, |x| Prop::U16(x as u16), op), - PropType::U32 => reduce_unsigned(vs, |x| Prop::U32(x as u32), op), - PropType::U64 => reduce_unsigned(vs, |x| Prop::U64(x), op), - PropType::I32 => reduce_signed(vs, |x| Prop::I32(x as i32), op), - PropType::I64 => reduce_signed(vs, |x| Prop::I64(x), op), - PropType::F32 => reduce_float(vs, |x| Prop::F32(x as f32), op), - PropType::F64 => reduce_float(vs, |x| Prop::F64(x), op), + PropType::U8 => reduce_unsigned(iter, |x| Prop::U8(x as u8), op), + PropType::U16 => reduce_unsigned(iter, |x| Prop::U16(x as u16), op), + PropType::U32 => reduce_unsigned(iter, |x| Prop::U32(x as u32), op), + PropType::U64 => reduce_unsigned(iter, |x| Prop::U64(x), op), + PropType::I32 => reduce_signed(iter, |x| Prop::I32(x as i32), op), + PropType::I64 => reduce_signed(iter, |x| Prop::I64(x), op), + PropType::F32 => reduce_float(iter, |x| Prop::F32(x as f32), op), + PropType::F64 => reduce_float(iter, |x| Prop::F64(x), op), _ => None, } } @@ -254,10 +258,7 @@ pub fn apply_agg_to_prop(p: &Prop, op: Op) -> Option { (Op::Sum, Prop::List(inner)) | (Op::Avg, Prop::List(inner)) | (Op::Min, Prop::List(inner)) - | (Op::Max, Prop::List(inner)) => { - let vs: Vec = inner.iter().collect(); - reduce_seq(vs, op) - } + | (Op::Max, Prop::List(inner)) => reduce_seq(inner.iter(), op), (Op::Len, _) => Some(Prop::U64(1)), From c2180d10d9871ed893a15082df6b54d9b1b81421 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:55:43 +0100 Subject: [PATCH 057/155] rename BinaryCmpFilter/StringFilter/UnaryFilter/PropValueSetFilter to *Expr --- raphtory-tests/tests/edge_expr_filter.rs | 4 +- .../views/filter/model/edge_expr/filters.rs | 34 ++-- .../graph/views/filter/model/edge_expr/mod.rs | 2 +- .../views/filter/model/filter_operator.rs | 8 +- .../src/db/graph/views/filter/model/mod.rs | 6 +- .../views/filter/model/node_expr/exprs.rs | 2 +- .../views/filter/model/node_expr/filters.rs | 160 +++++++++--------- .../graph/views/filter/model/node_expr/mod.rs | 30 ++-- .../graph/views/filter/model/node_expr/ops.rs | 14 +- .../views/filter/model/node_expr/tests.rs | 2 +- 10 files changed, 131 insertions(+), 131 deletions(-) diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs index 80eea2ea1a..d10f14dd8f 100644 --- a/raphtory-tests/tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -221,13 +221,13 @@ fn test_edge_aggregated_first_starts_with_str_convenience() { } // ───────────────────────────────────────────────────────────────────────────── -// Set ops — PropValueSetEdgeFilter (linear scan, Option) and +// Set ops — PropValueSetExpr (linear scan, Option) and // SetEdgeFilter (HashSet, Option) // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_edge_property_is_in_prop_values() { - // Path A: EdgePropertyExprOps::is_in — PropValueSetEdgeFilter + // Path A: EdgePropertyExprOps::is_in — PropValueSetExpr let g = band_graph(); let filter = EdgeFilter.property("band").is_in([ Prop::str("Pink Floyd"), diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index 0bc49ed78b..bb73779b6c 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -11,8 +11,8 @@ use super::{ EdgeExpr, EdgeOp, }; use crate::db::graph::views::filter::model::edge_filter::EdgeFilter; -use crate::db::graph::views::filter::model::node_expr::filters::PropValueSetFilter; -pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpFilter, StringFilter, UnaryFilter}; +use crate::db::graph::views::filter::model::node_expr::filters::PropValueSetExpr; +pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpExpr, StringExpr, UnaryExpr}; use crate::{ db::{ api::{state::ops::NotANodeFilter, view::internal::GraphView}, @@ -50,9 +50,9 @@ fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), GraphEr } // ───────────────────────────────────────────────────────────────────────────── -// BinaryCmpEdgeFilter +// BinaryCmpExpr // ───────────────────────────────────────────────────────────────────────────── -impl EdgeExpr for BinaryCmpFilter +impl EdgeExpr for BinaryCmpExpr where L: EdgeExpr, R: EdgeExpr, @@ -67,7 +67,7 @@ where } } -impl TryAsCompositeFilter for BinaryCmpFilter +impl TryAsCompositeFilter for BinaryCmpExpr where L: EdgeExpr, R: EdgeExpr, @@ -87,7 +87,7 @@ where } } -impl CreateFilter for BinaryCmpFilter +impl CreateFilter for BinaryCmpExpr where L: EdgeExpr, R: EdgeExpr, @@ -124,10 +124,10 @@ where } // ───────────────────────────────────────────────────────────────────────────── -// UnaryEdgeFilter +// UnaryExpr // ───────────────────────────────────────────────────────────────────────────── -impl TryAsCompositeFilter for UnaryFilter +impl TryAsCompositeFilter for UnaryExpr where E: EdgeExpr, { @@ -146,7 +146,7 @@ where } } -impl CreateFilter for UnaryFilter +impl CreateFilter for UnaryExpr where E: EdgeExpr, { @@ -192,11 +192,11 @@ fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { } // ───────────────────────────────────────────────────────────────────────────── -// StringEdgeFilter — string expression filter for edges +// StringExpr — string expression filter for edges // ───────────────────────────────────────────────────────────────────────────── -impl EdgeExpr for StringFilter +impl EdgeExpr for StringExpr where L: EdgeExpr, R: EdgeExpr, @@ -211,7 +211,7 @@ where } } -impl TryAsCompositeFilter for StringFilter +impl TryAsCompositeFilter for StringExpr where L: EdgeExpr, R: EdgeExpr, @@ -229,7 +229,7 @@ where } } -impl CreateFilter for StringFilter +impl CreateFilter for StringExpr where L: EdgeExpr, R: EdgeExpr, @@ -264,10 +264,10 @@ where } // ───────────────────────────────────────────────────────────────────────────── -// PropValueSetFilter — is_in / is_not_in for edge-side exprs +// PropValueSetExpr — is_in / is_not_in for edge-side exprs // ───────────────────────────────────────────────────────────────────────────── -impl EdgeExpr for PropValueSetFilter { +impl EdgeExpr for PropValueSetExpr { fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -278,7 +278,7 @@ impl EdgeExpr for PropValueSetFilter { } -impl TryAsCompositeFilter for PropValueSetFilter { +impl TryAsCompositeFilter for PropValueSetExpr { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) } @@ -292,7 +292,7 @@ impl TryAsCompositeFilter for PropValueSetFilter { } } -impl CreateFilter for PropValueSetFilter { +impl CreateFilter for PropValueSetExpr { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index f6ac3865ad..0d6b91ae0d 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -8,7 +8,7 @@ //! ```text //! ┌─ Build phase (pure data, no graph) ──────────────────────┐ //! │ EdgeFilter.property("weight") ← EdgeExpr │ -//! │ .eq(5.0f64) ← BinaryCmpFilter │ +//! │ .eq(5.0f64) ← BinaryCmpExpr │ //! └──────────────────────────────────────────────────────────┘ //! │ create_edge_op(graph)? ← resolve name → prop_id //! ▼ 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 cec15079e4..8e54c90575 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -166,7 +166,7 @@ impl StringComparable for Option { // Focused operator enums for the NodeExpr expression system // ───────────────────────────────────────────────────────────────────────────── -/// Ordering and equality operators used by `BinaryCmpNodeFilter`. +/// Ordering and equality operators used by `BinaryCmpExpr`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BinaryOp { Eq, @@ -190,7 +190,7 @@ impl Display for BinaryOp { } } -/// String-only operators used by `StringNodeFilter`. +/// String-only operators used by `StringExpr`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StringOp { StartsWith, @@ -218,7 +218,7 @@ impl Display for StringOp { } } -/// Unary presence operators used by `UnaryNodeFilter`. +/// Unary presence operators used by `UnaryExpr`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnaryOp { IsSome, @@ -549,7 +549,7 @@ impl FilterOperator { /// Compare two optional values symmetrically. /// - /// Used by `BinaryCmpNodeFilter` where both sides are expressions that may return `None`. + /// Used by `BinaryCmpExpr` 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/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index b70bb69496..aff97f043c 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -15,12 +15,12 @@ pub use crate::{ UnaryOp, }, node_expr::{ - AllExpr, AnyExpr, AvgExpr, BinaryCmpFilter, + AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, EntityAggOps, EntityExprFilterOps, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, - PropValueSetFilter, StringFilter, SumExpr, + PropValueSetExpr, StringExpr, SumExpr, TemporalExpr, TemporalProp, TemporalPropOps, - UnaryFilter, + UnaryExpr, }, node_filter::{NodeFilter, NodeFilterFactory}, 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 e0e6f75dad..30be6bfcc4 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 @@ -462,7 +462,7 @@ impl> // // Calling .gt() / .eq() etc. on any of these (via NodeExprFilterOps) produces: -// BinaryCmpNodeFilter>, RHS> +// BinaryCmpExpr>, 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 9f7d9f28cb..a1d87091b5 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 @@ -9,10 +9,10 @@ //! ```text //! Phase 1 — Build (pure Rust data, no graph): //! NodeFilter.property("age").gt(30i64) -//! ──► BinaryCmpNodeFilter { left: Property("age"), op: Gt, right: 30i64 } +//! ──► BinaryCmpExpr { left: Property("age"), op: Gt, right: 30i64 } //! //! Phase 2 — Compile (bind to graph, resolve names): -//! BinaryCmpNodeFilter::create_node_filter(graph)? +//! BinaryCmpExpr::create_node_filter(graph)? //! ──► Arc> //! = BinaryCmpNodeOp { left: NodePropOp(id=3), right: Const(Some(I64(30))), op: Gt } //! @@ -29,7 +29,7 @@ //! ```rust,ignore //! // "pass if any temporal value of 'score' > 10" //! NodeFilter.property("score").temporal().gt(10i64).any() -//! ──► BinaryCmpNodeFilter>, Prop> +//! ──► BinaryCmpExpr>, Prop> //! create_node_filter(graph)? //! ──► BinaryCmpNodeOp { left: AnyNodeOp { inner: ListAwareCmpNodeOp { TemporalNodePropOp, //! Const(I64(10)), Gt } }, @@ -37,7 +37,7 @@ //! //! // "pass if sum of 'score' > 100" //! NodeFilter.property("score").temporal().sum().gt(100i64) -//! ──► BinaryCmpNodeFilter, i64> +//! ──► BinaryCmpExpr, i64> //! ``` use super::{ @@ -67,7 +67,7 @@ use crate::{ use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── -// BinaryCmpNodeFilter — binary expression filter +// BinaryCmpExpr — binary expression filter // ───────────────────────────────────────────────────────────────────────────── /// A node filter that compares two [`NodeExpr`] values using a [`BinaryOp`]. @@ -80,22 +80,22 @@ use std::sync::Arc; /// /// ```rust,ignore /// NodeFilter.degree().gt(2usize) -/// → BinaryCmpNodeFilter, usize> +/// → BinaryCmpExpr, usize> /// → BinaryCmpNodeOp { left: Degree(..).map(Prop::U64), right: Const(Some(U64(2))), op: Gt } /// /// NodeFilter.property("age").eq(30i64) -/// → BinaryCmpNodeFilter +/// → BinaryCmpExpr /// → BinaryCmpNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(I64(30))), op: Eq } /// ``` #[derive(Clone)] -pub struct BinaryCmpFilter { +pub struct BinaryCmpExpr { pub left: L, pub op: BinaryOp, pub right: R, pub entity: Entity, } -impl BinaryCmpFilter { +impl BinaryCmpExpr { pub fn new(left: L, op: BinaryOp, right: R, entity: E) -> Self { Self { left, @@ -106,7 +106,7 @@ impl BinaryCmpFilter { } } -impl ComposableFilter for BinaryCmpFilter {} +impl ComposableFilter for BinaryCmpExpr {} /// Reject ordering operators on boolean properties. //. TODO: Also check if both the types are comparable. @@ -206,7 +206,7 @@ fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result, .collect() } -impl CreateFilter for BinaryCmpFilter +impl CreateFilter for BinaryCmpExpr where L: NodeExpr, R: NodeExpr, @@ -248,7 +248,7 @@ where } } -impl TryAsCompositeFilter for BinaryCmpFilter +impl TryAsCompositeFilter for BinaryCmpExpr where L: NodeExpr, R: NodeExpr, @@ -269,7 +269,7 @@ where } // ───────────────────────────────────────────────────────────────────────────── -// UnaryNodeFilter — is_some / is_none on nullable expressions +// UnaryExpr — is_some / is_none on nullable expressions // ───────────────────────────────────────────────────────────────────────────── /// A node filter that tests the presence of an `Option`-valued expression. @@ -279,19 +279,19 @@ where /// /// ```rust,ignore /// NodeFilter.property("age").is_some::() -/// → UnaryNodeFilter +/// → UnaryExpr /// → UnaryNodeOp { inner: NodePropOp(prop_id=N), op: IsSome } /// ``` #[derive(Clone)] -pub struct UnaryFilter { +pub struct UnaryExpr { pub expr: E, pub op: UnaryOp, pub entity: Entity, } -impl ComposableFilter for UnaryFilter {} +impl ComposableFilter for UnaryExpr {} -impl CreateFilter for UnaryFilter +impl CreateFilter for UnaryExpr where E: NodeExpr, { @@ -329,7 +329,7 @@ where } } -impl TryAsCompositeFilter for UnaryFilter +impl TryAsCompositeFilter for UnaryExpr where E: NodeExpr, { @@ -349,7 +349,7 @@ where } // ───────────────────────────────────────────────────────────────────────────── -// StringNodeFilter — string expression filter +// StringExpr — string expression filter // ───────────────────────────────────────────────────────────────────────────── /// A node filter that applies a [`StringOp`] to two [`NodeExpr`] values. @@ -361,30 +361,30 @@ where /// /// ```rust,ignore /// NodeFilter.name().starts_with("Al") -/// → StringNodeFilter +/// → StringExpr /// → StringNodeOp { left: Name.map(...), right: Const(Some(Str("Al"))), op: StartsWith } /// /// NodeFilter.property("tag").contains(Prop::Str("foo".into())) -/// → StringNodeFilter +/// → StringExpr /// → StringNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(Str("foo"))), op: Contains } /// ``` #[derive(Clone)] -pub struct StringFilter { +pub struct StringExpr { pub left: L, pub op: StringOp, pub right: R, pub entity: Entity, } -impl StringFilter { +impl StringExpr { pub fn new(left: L, op: StringOp, right: R, entity: Entity) -> Self { Self { left, op, right, entity } } } -impl ComposableFilter for StringFilter {} +impl ComposableFilter for StringExpr {} -impl CreateFilter for StringFilter { +impl CreateFilter for StringExpr { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; @@ -420,7 +420,7 @@ impl CreateFilter for StringFilter { } } -impl TryAsCompositeFilter for StringFilter +impl TryAsCompositeFilter for StringExpr where L: NodeExpr, R: NodeExpr, @@ -441,7 +441,7 @@ where } // ───────────────────────────────────────────────────────────────────────────── -// PropValueSetFilter — is_in / is_not_in for aggregated Option values +// PropValueSetExpr — is_in / is_not_in for aggregated Option values // ───────────────────────────────────────────────────────────────────────────── /// A filter that checks whether a scalar property value is in (or not in) a fixed set. @@ -449,16 +449,16 @@ where /// Uses linear scan because `Prop` may contain floats that don't implement `Hash`. /// Works for both nodes (`Entity = NodeFilter`) and edges (`Entity = EdgeFilter`). #[derive(Clone)] -pub struct PropValueSetFilter { +pub struct PropValueSetExpr { pub(crate) expr: E, pub(crate) values: Vec, pub(crate) op: SetOp, pub(crate) entity: Entity, } -impl ComposableFilter for PropValueSetFilter {} +impl ComposableFilter for PropValueSetExpr {} -impl CreateFilter for PropValueSetFilter { +impl CreateFilter for PropValueSetExpr { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; type NodeFilter<'graph, G: GraphView + 'graph> = PropValueSetNodeOp<'graph>; @@ -492,7 +492,7 @@ impl CreateFilter for PropValueSetFilter { } } -impl TryAsCompositeFilter for PropValueSetFilter { +impl TryAsCompositeFilter for PropValueSetExpr { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) } @@ -520,12 +520,12 @@ impl TryAsCompositeFilter for PropValueSetFilter { /// Calling a method produces the next step in the chain: /// ```rust,ignore /// NodeFilter.property("score").temporal() // → TemporalProp -/// .gt(10i64) // → BinaryCmpNodeFilter -/// .any() // → BinaryCmpNodeFilter, Prop> +/// .gt(10i64) // → BinaryCmpExpr +/// .any() // → BinaryCmpExpr, Prop> /// /// NodeFilter.property("price").temporal() // → TemporalProp /// .sum() // → SumExpr> -/// .gt(100i64) // → BinaryCmpNodeFilter, i64> +/// .gt(100i64) // → BinaryCmpExpr, i64> /// /// NodeFilter.window(0, 100).property("score") /// .temporal() // → TemporalProp> @@ -563,45 +563,45 @@ impl TemporalProp { /// NodeFilter.property("score").temporal().gt(10i64).any() /// ``` pub trait EntityExprFilterOps: EntityExpr + Sized { - fn gt(self, rhs: R) -> BinaryCmpFilter { + fn gt(self, rhs: R) -> BinaryCmpExpr { // TODO: validate ops - BinaryCmpFilter::new(self, BinaryOp::Gt, rhs, Self::Marker::default()) + BinaryCmpExpr::new(self, BinaryOp::Gt, rhs, Self::Marker::default()) } - fn ge(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Ge, rhs, Self::Marker::default()) + fn ge(self, rhs: R) -> BinaryCmpExpr { + BinaryCmpExpr::new(self, BinaryOp::Ge, rhs, Self::Marker::default()) } - fn lt(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Lt, rhs, Self::Marker::default()) + fn lt(self, rhs: R) -> BinaryCmpExpr { + BinaryCmpExpr::new(self, BinaryOp::Lt, rhs, Self::Marker::default()) } - fn le(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Le, rhs, Self::Marker::default()) + fn le(self, rhs: R) -> BinaryCmpExpr { + BinaryCmpExpr::new(self, BinaryOp::Le, rhs, Self::Marker::default()) } - fn eq(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Eq, rhs, Self::Marker::default()) + fn eq(self, rhs: R) -> BinaryCmpExpr { + BinaryCmpExpr::new(self, BinaryOp::Eq, rhs, Self::Marker::default()) } - fn ne(self, rhs: R) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Ne, rhs, Self::Marker::default()) + fn ne(self, rhs: R) -> BinaryCmpExpr { + BinaryCmpExpr::new(self, BinaryOp::Ne, rhs, Self::Marker::default()) } - fn starts_with(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::StartsWith, rhs, Self::Marker::default()) + fn starts_with(self, rhs: R) -> StringExpr { + StringExpr::new(self, StringOp::StartsWith, rhs, Self::Marker::default()) } - fn ends_with(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::EndsWith, rhs, Self::Marker::default()) + fn ends_with(self, rhs: R) -> StringExpr { + StringExpr::new(self, StringOp::EndsWith, rhs, Self::Marker::default()) } - fn contains(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::Contains, rhs, Self::Marker::default()) + fn contains(self, rhs: R) -> StringExpr { + StringExpr::new(self, StringOp::Contains, rhs, Self::Marker::default()) } - fn not_contains(self, rhs: R) -> StringFilter { - StringFilter::new(self, StringOp::NotContains, rhs, Self::Marker::default()) + fn not_contains(self, rhs: R) -> StringExpr { + StringExpr::new(self, StringOp::NotContains, rhs, Self::Marker::default()) } fn fuzzy_search( @@ -609,8 +609,8 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringFilter { - StringFilter::new( + ) -> StringExpr { + StringExpr::new( self, StringOp::FuzzySearch { levenshtein_distance, @@ -621,24 +621,24 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { ) } - fn is_some(self) -> UnaryFilter { - UnaryFilter { + fn is_some(self) -> UnaryExpr { + UnaryExpr { expr: self, op: UnaryOp::IsSome, entity: Self::Marker::default(), } } - fn is_none(self) -> UnaryFilter { - UnaryFilter { + fn is_none(self) -> UnaryExpr { + UnaryExpr { expr: self, op: UnaryOp::IsNone, entity: Self::Marker::default(), } } - fn is_in>(self, values: impl IntoIterator) -> PropValueSetFilter { - PropValueSetFilter { + fn is_in>(self, values: impl IntoIterator) -> PropValueSetExpr { + PropValueSetExpr { expr: self, values: values.into_iter().map(Into::into).collect(), op: SetOp::IsIn, @@ -646,8 +646,8 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { } } - fn is_not_in>(self, values: impl IntoIterator) -> PropValueSetFilter { - PropValueSetFilter { + fn is_not_in>(self, values: impl IntoIterator) -> PropValueSetExpr { + PropValueSetExpr { expr: self, values: values.into_iter().map(Into::into).collect(), op: SetOp::IsNotIn, @@ -655,24 +655,24 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { } } - fn is_true(self) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) + fn is_true(self) -> BinaryCmpExpr { + BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) } - fn is_false(self) -> BinaryCmpFilter { - BinaryCmpFilter::new(self, BinaryOp::Eq, Prop::Bool(false), Self::Marker::default()) + fn is_false(self) -> BinaryCmpExpr { + BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(false), Self::Marker::default()) } - fn not(self) -> BinaryCmpFilter { + fn not(self) -> BinaryCmpExpr { self.eq(Prop::Bool(false)) } - fn any(self) -> BinaryCmpFilter, Prop, Self::Marker> { - BinaryCmpFilter::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) + fn any(self) -> BinaryCmpExpr, Prop, Self::Marker> { + BinaryCmpExpr::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) } - fn all(self) -> BinaryCmpFilter, Prop, Self::Marker> { - BinaryCmpFilter::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) + fn all(self) -> BinaryCmpExpr, Prop, Self::Marker> { + BinaryCmpExpr::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) } } @@ -686,7 +686,7 @@ impl EntityExprFilterOps for E {} // temporal().is_in([...]).any() // ───────────────────────────────────────────────────────────────────────────── -impl EntityExpr for BinaryCmpFilter { +impl EntityExpr for BinaryCmpExpr { type Marker = E; fn prop_type(&self) -> PropType { // TODO: depending on the types of left and right, we should figure out the type to return here @@ -694,7 +694,7 @@ impl En } } -impl NodeExpr for BinaryCmpFilter { +impl NodeExpr for BinaryCmpExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -709,14 +709,14 @@ impl NodeExpr for BinaryCmpFilter { } } -impl EntityExpr for StringFilter { +impl EntityExpr for StringExpr { type Marker = Entity; fn prop_type(&self) -> PropType { PropType::Empty } } -impl NodeExpr for StringFilter { +impl NodeExpr for StringExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -731,18 +731,18 @@ impl NodeExpr for StringFilter { } } -impl EntityExpr for PropValueSetFilter { +impl EntityExpr for PropValueSetExpr { type Marker = Entity; fn prop_type(&self) -> PropType { PropType::Empty } } -impl EntityExpr for UnaryFilter { +impl EntityExpr for UnaryExpr { type Marker = Entity; } -impl NodeExpr for PropValueSetFilter { +impl NodeExpr for PropValueSetExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, 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 index fe4854c0f7..620ef0e8fc 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -107,34 +107,34 @@ pub trait TemporalPropOps: Sized { fn any(self) -> AnyExpr> { AnyExpr(self.into_expr()) } fn all(self) -> AllExpr> { AllExpr(self.into_expr()) } - fn gt(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + fn gt(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { self.into_expr().gt(rhs) } - fn ge(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + fn ge(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { self.into_expr().ge(rhs) } - fn lt(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + fn lt(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { self.into_expr().lt(rhs) } - fn le(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + fn le(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { self.into_expr().le(rhs) } - fn eq(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + fn eq(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { self.into_expr().eq(rhs) } - fn ne(self, rhs: R) -> BinaryCmpFilter, R, ::Marker> { + fn ne(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { self.into_expr().ne(rhs) } - fn contains(self, rhs: R) -> StringFilter, R, ::Marker> { + fn contains(self, rhs: R) -> StringExpr, R, ::Marker> { self.into_expr().contains(rhs) } - fn starts_with(self, rhs: R) -> StringFilter, R, ::Marker> { + fn starts_with(self, rhs: R) -> StringExpr, R, ::Marker> { self.into_expr().starts_with(rhs) } - fn ends_with(self, rhs: R) -> StringFilter, R, ::Marker> { + fn ends_with(self, rhs: R) -> StringExpr, R, ::Marker> { self.into_expr().ends_with(rhs) } - fn not_contains(self, rhs: R) -> StringFilter, R, ::Marker> { + fn not_contains(self, rhs: R) -> StringExpr, R, ::Marker> { self.into_expr().not_contains(rhs) } fn fuzzy_search( @@ -142,19 +142,19 @@ pub trait TemporalPropOps: Sized { rhs: R, levenshtein_distance: usize, prefix_match: bool, - ) -> StringFilter, R, ::Marker> { + ) -> StringExpr, R, ::Marker> { self.into_expr().fuzzy_search(rhs, levenshtein_distance, prefix_match) } - fn is_in>(self, values: impl IntoIterator) -> PropValueSetFilter, ::Marker> { + fn is_in>(self, values: impl IntoIterator) -> PropValueSetExpr, ::Marker> { self.into_expr().is_in(values) } - fn is_not_in>(self, values: impl IntoIterator) -> PropValueSetFilter, ::Marker> { + fn is_not_in>(self, values: impl IntoIterator) -> PropValueSetExpr, ::Marker> { self.into_expr().is_not_in(values) } - fn is_true(self) -> BinaryCmpFilter, Prop, ::Marker> { + fn is_true(self) -> BinaryCmpExpr, Prop, ::Marker> { self.into_expr().is_true() } - fn is_false(self) -> BinaryCmpFilter, Prop, ::Marker> { + fn is_false(self) -> BinaryCmpExpr, Prop, ::Marker> { self.into_expr().is_false() } } 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 index 79c0c1d02f..1796f9f8f8 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -13,7 +13,7 @@ //! .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) +//! NodeFilter.property("age").gt(30i64) ← BinaryCmpExpr (pure data) //! .create_node_filter(graph)? //! ──► BinaryCmpNodeOp { left: NodePropOp, right: Const(Some(I64(30))), op: Gt } //! apply: Prop::binary_cmp(Gt, age_value, Some(I64(30))) @@ -26,7 +26,7 @@ //! //! # Quantified evaluation //! -//! Filter types (`BinaryCmpNodeFilter`, `StringNodeFilter`, `PropValueSetFilter`) also +//! Filter types (`BinaryCmpExpr`, `StringExpr`, `PropValueSetExpr`) also //! implement `NodeExpr`, producing list-aware ops for mid-chain use before `.any()`/`.all()`: //! //! ```text @@ -308,8 +308,8 @@ impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { // ───────────────────────────────────────────────────────────────────────────── // ListAwareCmpNodeOp / ListAwareStringNodeOp / ListAwareSetNodeOp // -// These ops implement NodeExpr for BinaryCmpNodeFilter, StringNodeFilter, and -// PropValueSetFilter respectively, enabling mid-chain use before .any()/.all(). +// These ops implement NodeExpr for BinaryCmpExpr, StringExpr, and +// PropValueSetExpr respectively, enabling mid-chain use before .any()/.all(). // // Each uses broadcasting so that comparisons applied to a `Prop::List(...)` // fan out element-wise; scalar inputs are passed through to the op directly. @@ -556,7 +556,7 @@ impl<'g> NodeOp for PropValueSetNodeOp<'g> { // BinaryCmpNodeOp<'g, T> — compares two NodeOp using BinaryOp // ───────────────────────────────────────────────────────────────────────────── -/// Internal op produced by [`BinaryCmpNodeFilter::create_node_filter`]. +/// Internal op produced by [`BinaryCmpExpr::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. @@ -588,7 +588,7 @@ impl<'g, T: Comparable + Clone + Send + Sync + 'static> NodeOp for BinaryCmpNode // StringNodeOp<'g, T> — applies a StringOp to two NodeOp // ───────────────────────────────────────────────────────────────────────────── -/// Internal op produced by [`StringNodeFilter::create_node_filter`]. +/// Internal op produced by [`StringExpr::create_node_filter`]. /// /// e.g. `NodeFilter.name().starts_with("Al")` compiles to: /// `StringNodeOp { left: Name.map(...), right: Const(Some(Str("Al"))), op: StartsWith }` @@ -615,7 +615,7 @@ impl<'g, T: StringComparable> NodeOp for StringNodeOp<'g, T> { // UnaryNodeOp<'g, T> — evaluates is_some / is_none // ───────────────────────────────────────────────────────────────────────────── -/// Internal op produced by [`UnaryNodeFilter::create_node_filter`]. +/// Internal op produced by [`UnaryExpr::create_node_filter`]. /// /// e.g. `NodeFilter.property("age").is_some::()` compiles to: /// `UnaryNodeOp { inner: NodePropOp(prop_id=3), op: IsSome }` 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 07033171dc..3b9ca2023c 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 @@ -148,7 +148,7 @@ fn total_gt_in_degree_selects_nodes_with_outgoing_edges() { #[test] fn const_expr_works() { - let filter = BinaryCmpFilter::new(ConstExpr(2usize), BinaryOp::Eq, ConstExpr(2usize), NodeFilter); + let filter = BinaryCmpExpr::new(ConstExpr(2usize), BinaryOp::Eq, ConstExpr(2usize), NodeFilter); let g = build_test_graph(); assert_eq!(filtered_names(filter, g), vec!["a", "b", "c"]); } From b54460aaa50e0a830e5cd5c2e70af77d82a7a8bf Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:29:01 +0100 Subject: [PATCH 058/155] unify TemporalProp into TemporalExpr; delete TemporalPropOps shim trait --- raphtory-tests/tests/test_filters.rs | 6 +- .../graph/views/filter/model/edge_filter.rs | 19 ++-- .../src/db/graph/views/filter/model/mod.rs | 9 +- .../views/filter/model/node_expr/exprs.rs | 23 +++-- .../views/filter/model/node_expr/filters.rs | 37 -------- .../graph/views/filter/model/node_expr/mod.rs | 89 ------------------- .../views/filter/model/node_expr/tests.rs | 2 +- raphtory/src/lib.rs | 2 +- .../python/graph/properties/temporal_props.rs | 2 +- raphtory/src/python/packages/base_modules.rs | 4 +- 10 files changed, 42 insertions(+), 151 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index d739b01b28..b032a61bdb 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -3230,7 +3230,7 @@ mod test_node_property_filter { ComposableFilter, PropertyFilterFactory, ViewWrapOps, }; use raphtory_api::core::entities::properties::prop::Prop; - use raphtory::prelude::{EntityExprFilterOps, TemporalPropOps}; + use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; use raphtory_tests::assertions::{ assert_filter_nodes_results, assert_search_nodes_results, TestVariants, }; @@ -5757,7 +5757,7 @@ mod test_node_property_filter_agg { CreateFilter, }, }, - prelude::{AdditionOps, EntityAggOps, EntityExprFilterOps, GraphViewOps, PropertyAdditionOps, TemporalPropOps}, + prelude::{AdditionOps, EntityAggOps, EntityExprFilterOps, GraphViewOps, PropertyAdditionOps}, }; use raphtory_api::core::{ entities::properties::prop::{IntoProp, Prop}, @@ -10762,7 +10762,7 @@ mod test_edge_property_filter { }; use raphtory_api::core::entities::properties::prop::Prop; - use raphtory::prelude::{EntityExprFilterOps, TemporalPropOps}; + use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; use raphtory_tests::assertions::{ assert_filter_edges_results, assert_search_edges_results, TestGraphVariants, TestVariants, }; 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 89d22a2cac..0967dc15c3 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -15,7 +15,7 @@ use crate::{ is_valid_filter::IsValidEdge, latest_filter::Latest, layered_filter::Layered, - node_expr::{EntityExpr, NodeExpr, TemporalPropOps}, + node_expr::{EntityExpr, NodeExpr}, node_filter::{ builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, }, @@ -23,10 +23,11 @@ use crate::{ snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, AllExpr, AndFilter, AnyExpr, AvgExpr, CombinedFilter, ComposableFilter, - CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, - FirstExpr, InternalViewWrapOps, LastExpr, LenExpr, MaxExpr, MetadataExpr, - MinExpr, NotFilter, OrFilter, PropertyExpr, PropertyFilterFactory, - SumExpr, TemporalExpr, TemporalProp, TryAsCompositeFilter, Wrap, + CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityAggOps, + EntityExprFilterOps, EntityMarker, FirstExpr, InternalViewWrapOps, + LastExpr, LenExpr, MaxExpr, MetadataExpr, MinExpr, NotFilter, OrFilter, + PropertyExpr, PropertyFilterFactory, SumExpr, TemporalExpr, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -163,12 +164,12 @@ impl EdgeEndpointWrapper { impl EdgeEndpointWrapper> { #[inline] - pub fn temporal(&self) -> EdgeEndpointWrapper> { + pub fn temporal(&self) -> EdgeEndpointWrapper> { EdgeEndpointWrapper::new(self.inner.temporal(), self.endpoint) } } -impl EdgeEndpointWrapper> { +impl EdgeEndpointWrapper> { #[inline] pub fn sum(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; @@ -207,12 +208,12 @@ impl EdgeEndpointWra #[inline] pub fn any(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; - EdgeEndpointWrapper::new(self.inner.any(), endpoint) + EdgeEndpointWrapper::new(AnyExpr(self.inner), endpoint) } #[inline] pub fn all(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; - EdgeEndpointWrapper::new(self.inner.all(), endpoint) + EdgeEndpointWrapper::new(AllExpr(self.inner), endpoint) } } diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index aff97f043c..c2e6a020f3 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -19,7 +19,7 @@ pub use crate::{ EntityAggOps, EntityExprFilterOps, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, PropValueSetExpr, StringExpr, SumExpr, - TemporalExpr, TemporalProp, TemporalPropOps, + TemporalExpr, UnaryExpr, }, node_filter::{NodeFilter, NodeFilterFactory}, @@ -433,8 +433,11 @@ where } impl PropertyExpr { - pub fn temporal(&self) -> TemporalProp { - TemporalProp::new(self.view_expr.clone(), self.name.clone()) + pub fn temporal(&self) -> TemporalExpr { + TemporalExpr { + view_expr: self.view_expr.clone(), + name: self.name.clone(), + } } } 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 30be6bfcc4..dfec15b821 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 @@ -404,8 +404,10 @@ impl NodeExpr for Metadata { /// All temporal values of a named property over the current view window. /// /// Implements `NodeExpr` when `E: NodeFilterFactory` and `EdgeExpr` when `E: EdgeFilterFactory`. -/// Produced implicitly by `TemporalPropOps` methods (`.sum()`, `.gt()`, etc.) — not usually -/// constructed directly. +/// Constructed by `PropertyExpr::temporal()`. Implements `EntityExpr` so all +/// `EntityExprFilterOps` chain methods (`.gt()`, `.contains()`, `.any()`, etc.) +/// are available, plus `EntityAggOps` for aggregators (`.sum()`, `.last()`, +/// `.len()`, etc.). #[derive(Clone)] pub struct TemporalExpr { pub(crate) view_expr: E, @@ -422,6 +424,16 @@ impl EntityExpr for TemporalExpr< type Marker = E::Marker; } +impl EntityAggOps for TemporalExpr { + 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 NodeExpr for TemporalExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, @@ -455,7 +467,7 @@ impl> // .property("v").temporal().len() → LenExpr> @@ -468,8 +480,9 @@ impl TryAsCompositeFilter for PropValueSetExpr { } } -// ───────────────────────────────────────────────────────────────────────────── -// TemporalProp — entry point returned from `.property(name).temporal()` -// ───────────────────────────────────────────────────────────────────────────── - -/// Entry point returned by `PropertyExpr::temporal()`. -/// -/// `E` is the view expression (e.g. `NodeFilter`, `Windowed`, `Layered`) -/// that scopes which temporal property values are visible. -/// -/// Calling a method produces the next step in the chain: -/// ```rust,ignore -/// NodeFilter.property("score").temporal() // → TemporalProp -/// .gt(10i64) // → BinaryCmpExpr -/// .any() // → BinaryCmpExpr, Prop> -/// -/// NodeFilter.property("price").temporal() // → TemporalProp -/// .sum() // → SumExpr> -/// .gt(100i64) // → BinaryCmpExpr, i64> -/// -/// NodeFilter.window(0, 100).property("score") -/// .temporal() // → TemporalProp> -/// .gt(10i64).any() -/// ``` -pub struct TemporalProp { - pub(crate) view_expr: E, - pub(crate) name: String, -} - -impl TemporalProp { - pub(crate) fn new(view_expr: E, name: impl Into) -> Self { - Self { - view_expr, - name: name.into(), - } - } -} - // ───────────────────────────────────────────────────────────────────────────── // EntityExprFilterOps — comparison and set operators on any EntityExpr // ───────────────────────────────────────────────────────────────────────────── 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 index 620ef0e8fc..ee08faab6f 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -76,92 +76,3 @@ pub trait EntityExpr: Clone + Send + Sync + 'static { } } -// ───────────────────────────────────────────────────────────────────────────── -// TemporalPropOps — unified aggregation and comparison on TemporalProp -// ───────────────────────────────────────────────────────────────────────────── - -/// Aggregation and comparison operators on `TemporalProp`, unified for both -/// node-side (`E: NodeFilterFactory`) and edge-side (`E: EdgeFilterFactory`). -/// -/// ```rust,ignore -/// NodeFilter.property("score").temporal().sum().gt(100i64) -/// NodeFilter.property("score").temporal().gt(10i64).any() -/// EdgeFilter.property("score").temporal().sum().gt(100i64) -/// EdgeFilter.property("score").temporal().gt(10i64).any() -/// ``` -pub trait TemporalPropOps: Sized { - type ViewExpr: CreateView + EntityExpr + Clone + Send + Sync + 'static; - fn into_temporal_parts(self) -> (Self::ViewExpr, String); - - fn into_expr(self) -> TemporalExpr { - let (view_expr, name) = self.into_temporal_parts(); - TemporalExpr { view_expr, name } - } - fn sum(self) -> SumExpr> { SumExpr(self.into_expr()) } - fn avg(self) -> AvgExpr> { AvgExpr(self.into_expr()) } - fn min(self) -> MinExpr> { MinExpr(self.into_expr()) } - fn max(self) -> MaxExpr> { MaxExpr(self.into_expr()) } - fn first(self) -> FirstExpr> { FirstExpr(self.into_expr()) } - fn last(self) -> LastExpr> { LastExpr(self.into_expr()) } - fn len(self) -> LenExpr> { LenExpr(self.into_expr()) } - fn any(self) -> AnyExpr> { AnyExpr(self.into_expr()) } - fn all(self) -> AllExpr> { AllExpr(self.into_expr()) } - - fn gt(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { - self.into_expr().gt(rhs) - } - fn ge(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { - self.into_expr().ge(rhs) - } - fn lt(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { - self.into_expr().lt(rhs) - } - fn le(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { - self.into_expr().le(rhs) - } - fn eq(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { - self.into_expr().eq(rhs) - } - fn ne(self, rhs: R) -> BinaryCmpExpr, R, ::Marker> { - self.into_expr().ne(rhs) - } - fn contains(self, rhs: R) -> StringExpr, R, ::Marker> { - self.into_expr().contains(rhs) - } - fn starts_with(self, rhs: R) -> StringExpr, R, ::Marker> { - self.into_expr().starts_with(rhs) - } - fn ends_with(self, rhs: R) -> StringExpr, R, ::Marker> { - self.into_expr().ends_with(rhs) - } - fn not_contains(self, rhs: R) -> StringExpr, R, ::Marker> { - self.into_expr().not_contains(rhs) - } - fn fuzzy_search( - self, - rhs: R, - levenshtein_distance: usize, - prefix_match: bool, - ) -> StringExpr, R, ::Marker> { - self.into_expr().fuzzy_search(rhs, levenshtein_distance, prefix_match) - } - fn is_in>(self, values: impl IntoIterator) -> PropValueSetExpr, ::Marker> { - self.into_expr().is_in(values) - } - fn is_not_in>(self, values: impl IntoIterator) -> PropValueSetExpr, ::Marker> { - self.into_expr().is_not_in(values) - } - fn is_true(self) -> BinaryCmpExpr, Prop, ::Marker> { - self.into_expr().is_true() - } - fn is_false(self) -> BinaryCmpExpr, Prop, ::Marker> { - self.into_expr().is_false() - } -} - -impl TemporalPropOps for TemporalProp { - type ViewExpr = E; - fn into_temporal_parts(self) -> (E, String) { - (self.view_expr, self.name) - } -} 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 3b9ca2023c..fecac6af40 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 @@ -322,7 +322,7 @@ fn node_filter_temporal_property_entry_point() { #[test] fn temporal_expr_ops_blanket_any() { - // Using TemporalPropOps blanket on TemporalExpr directly + // Using EntityAggOps / EntityExprFilterOps on TemporalExpr directly let g = build_temporal_graph(); let filter = NodeFilter.property("score").temporal().eq(10i64).any(); assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 162d979fcf..627ef7d232 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -148,7 +148,7 @@ pub mod prelude { pub use crate::db::graph::views::filter::model::{ filter::Filter, property_filter::PropertyFilter, - EntityAggOps, EntityExprFilterOps, TemporalPropOps, + EntityAggOps, EntityExprFilterOps, }; pub use crate::db::graph::views::{ diff --git a/raphtory/src/python/graph/properties/temporal_props.rs b/raphtory/src/python/graph/properties/temporal_props.rs index 27c1a1d879..bde0abff85 100644 --- a/raphtory/src/python/graph/properties/temporal_props.rs +++ b/raphtory/src/python/graph/properties/temporal_props.rs @@ -225,7 +225,7 @@ impl From for PyTemporalPropCmp { } } -py_eq!(PyTemporalProp, PyTemporalPropCmp); +py_eq!(Py PyTemporalPropCmp); #[pymethods] impl PyTemporalProp { diff --git a/raphtory/src/python/packages/base_modules.rs b/raphtory/src/python/packages/base_modules.rs index 23e183b73f..24650ed949 100644 --- a/raphtory/src/python/packages/base_modules.rs +++ b/raphtory/src/python/packages/base_modules.rs @@ -19,7 +19,7 @@ use crate::{ properties::{ MetadataView, PropertiesView, PyMetadata, PyPropHistItemsList, PyPropHistItemsListList, PyPropHistValueList, PyPropHistValueListList, - PyPropValueList, PyPropValueListList, PyProperties, PyTemporalProp, + PyPropValueList, PyPropValueListList, PyProperties, Py PyTemporalPropList, PyTemporalPropListList, PyTemporalProperties, PyTemporalPropsList, PyTemporalPropsListList, }, @@ -93,7 +93,7 @@ pub fn add_raphtory_classes(m: &Bound) -> PyResult<()> { MetadataView, PyTemporalProperties, PropertiesView, - PyTemporalProp, + Py PyEventTime, PyOptionalEventTime, PyHistory, From a68023b42851a2de368e81b9e68d9b0ecb0272fc Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:34:48 +0100 Subject: [PATCH 059/155] And/Or bool ops broadcast over Prop::List inputs --- .../graph/views/filter/model/edge_expr/ops.rs | 20 +++++++++++++------ .../graph/views/filter/model/node_expr/ops.rs | 20 +++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index f22731e1fd..939d42f1b3 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -348,9 +348,13 @@ impl<'g> EdgeOp for AndBoolEdgeOp<'g> { type Output = Option; fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let l = matches!(self.left.apply(storage, edge), Some(Prop::Bool(true))); - let r = matches!(self.right.apply(storage, edge), Some(Prop::Bool(true))); - Some(Prop::Bool(l && r)) + let l = self.left.apply(storage, edge); + let r = self.right.apply(storage, edge); + broadcast_binary(l, r, &|lv, rv| { + let lb = matches!(lv, Some(Prop::Bool(true))); + let rb = matches!(rv, Some(Prop::Bool(true))); + Some(Prop::Bool(lb && rb)) + }) } } @@ -369,9 +373,13 @@ impl<'g> EdgeOp for OrBoolEdgeOp<'g> { type Output = Option; fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let l = matches!(self.left.apply(storage, edge), Some(Prop::Bool(true))); - let r = matches!(self.right.apply(storage, edge), Some(Prop::Bool(true))); - Some(Prop::Bool(l || r)) + let l = self.left.apply(storage, edge); + let r = self.right.apply(storage, edge); + broadcast_binary(l, r, &|lv, rv| { + let lb = matches!(lv, Some(Prop::Bool(true))); + let rb = matches!(rv, Some(Prop::Bool(true))); + Some(Prop::Bool(lb || rb)) + }) } } 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 index 1796f9f8f8..b7a9d3827d 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -488,9 +488,13 @@ impl<'g> NodeOp for AndBoolNodeOp<'g> { type Output = Option; fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - let l = matches!(self.left.apply(storage, node), Some(Prop::Bool(true))); - let r = matches!(self.right.apply(storage, node), Some(Prop::Bool(true))); - Some(Prop::Bool(l && r)) + let l = self.left.apply(storage, node); + let r = self.right.apply(storage, node); + broadcast_binary(l, r, &|lv, rv| { + let lb = matches!(lv, Some(Prop::Bool(true))); + let rb = matches!(rv, Some(Prop::Bool(true))); + Some(Prop::Bool(lb && rb)) + }) } } @@ -509,9 +513,13 @@ impl<'g> NodeOp for OrBoolNodeOp<'g> { type Output = Option; fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - let l = matches!(self.left.apply(storage, node), Some(Prop::Bool(true))); - let r = matches!(self.right.apply(storage, node), Some(Prop::Bool(true))); - Some(Prop::Bool(l || r)) + let l = self.left.apply(storage, node); + let r = self.right.apply(storage, node); + broadcast_binary(l, r, &|lv, rv| { + let lb = matches!(lv, Some(Prop::Bool(true))); + let rb = matches!(rv, Some(Prop::Bool(true))); + Some(Prop::Bool(lb || rb)) + }) } } From 762038d22121a02d619af6d2781f5c12e7c088f9 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:45:36 +0100 Subject: [PATCH 060/155] ref --- raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 939d42f1b3..bdedd1dfb7 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -23,7 +23,6 @@ use crate::db::{ graph::views::filter::model::{ edge_filter::Endpoint, node_expr::ops::{broadcast_binary, broadcast_unary}, - property_filter::evaluate::aggregate_values, }, }; use raphtory_api::core::entities::properties::prop::PropArray; @@ -251,7 +250,7 @@ impl<'g, I: Eq + Hash + Clone + Send + Sync + 'static> EdgeOp for SetEdgeOp<'g, } // ───────────────────────────────────────────────────────────────────────────── -// ListAwareCmpEdgeOp<'g> — element-wise comparison via aggregate_values +// ListAwareCmpEdgeOp<'g> — element-wise comparison via broadcast_binary // ───────────────────────────────────────────────────────────────────────────── #[derive(Clone)] @@ -275,7 +274,7 @@ impl<'g> EdgeOp for ListAwareCmpEdgeOp<'g> { } // ───────────────────────────────────────────────────────────────────────────── -// ListAwareStringEdgeOp<'g> — element-wise string comparison via aggregate_values +// ListAwareStringEdgeOp<'g> — element-wise string comparison via broadcast_binary // ───────────────────────────────────────────────────────────────────────────── #[derive(Clone)] @@ -299,7 +298,7 @@ impl<'g> EdgeOp for ListAwareStringEdgeOp<'g> { } // ───────────────────────────────────────────────────────────────────────────── -// ListAwareSetEdgeOp<'g> — element-wise set membership via aggregate_values +// ListAwareSetEdgeOp<'g> — element-wise set membership via broadcast_unary // ───────────────────────────────────────────────────────────────────────────── #[derive(Clone)] From baee2e3701e78b78703d67564fdfffdeae1d30d4 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:10:21 +0100 Subject: [PATCH 061/155] rid search tests rust --- raphtory-tests/src/assertions.rs | 108 - raphtory-tests/tests/cached_view.rs | 46 +- raphtory-tests/tests/graph_index.rs | 968 ------- raphtory-tests/tests/subgraph_tests.rs | 88 +- raphtory-tests/tests/test_filters.rs | 2283 +---------------- raphtory-tests/tests/test_layers.rs | 158 +- .../tests_node_type_filtered_subgraph.rs | 109 +- raphtory-tests/tests/views_test.rs | 1311 +--------- 8 files changed, 21 insertions(+), 5050 deletions(-) delete mode 100644 raphtory-tests/tests/graph_index.rs diff --git a/raphtory-tests/src/assertions.rs b/raphtory-tests/src/assertions.rs index d819cc1e4f..614090960a 100644 --- a/raphtory-tests/src/assertions.rs +++ b/raphtory-tests/src/assertions.rs @@ -4,8 +4,6 @@ use raphtory::{ }; use std::ops::Range; -#[cfg(feature = "search")] -use raphtory::prelude::{IndexMutationOps, SearchableGraphOps}; use raphtory::{ db::{ api::view::filter_ops::{EdgeSelect, NodeSelect}, @@ -122,26 +120,6 @@ impl ApplyFilter for FilterNeigh } } -pub struct SearchNodes(F); - -impl ApplyFilter for SearchNodes { - fn apply(&self, graph: G) -> Vec { - #[cfg(feature = "search")] - { - let mut results = graph - .search_nodes(self.0.clone(), 20, 0) - .unwrap() - .into_iter() - .map(|nv| nv.name()) - .collect::>(); - results.sort(); - return results; - } - #[cfg(not(feature = "search"))] - Vec::::new() - } -} - pub struct FilterEdges(F); impl ApplyFilter for FilterEdges { @@ -174,26 +152,6 @@ impl ApplyFilter for SelectEdges } } -pub struct SearchEdges(F); - -impl ApplyFilter for SearchEdges { - fn apply(&self, graph: G) -> Vec { - #[cfg(feature = "search")] - { - let mut results = graph - .search_edges(self.0.clone(), 20, 0) - .unwrap() - .into_iter() - .map(|ev| format!("{}->{}", ev.src().name(), ev.dst().name())) - .collect::>(); - results.sort(); - return results; - } - #[cfg(not(feature = "search"))] - Vec::::new() - } -} - #[track_caller] pub fn assert_filter_nodes_results( init_graph: impl FnOnce(Graph) -> Graph, @@ -295,27 +253,6 @@ pub fn assert_filter_neighbours_results( ) } -#[track_caller] -pub fn assert_search_nodes_results( - init_graph: impl FnOnce(Graph) -> Graph, - transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, - expected: &[&str], - variants: impl Into>, -) { - #[cfg(feature = "search")] - { - assert_results( - init_graph, - |graph: &Graph| graph.create_index_in_ram().unwrap(), - transform, - expected, - variants.into(), - SearchNodes(filter), - ) - } -} - #[track_caller] pub fn assert_filter_edges_results( init_graph: impl FnOnce(Graph) -> Graph, @@ -352,27 +289,6 @@ pub fn assert_select_edges_results( ) } -#[track_caller] -pub fn assert_search_edges_results( - init_graph: impl FnOnce(Graph) -> Graph, - transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, - expected: &[&str], - variants: impl Into>, -) { - #[cfg(feature = "search")] - { - assert_results( - init_graph, - |graph: &Graph| graph.create_index_in_ram().unwrap(), - transform, - expected, - variants.into(), - SearchEdges(filter), - ) - } -} - #[track_caller] fn assert_results( init_graph: impl FnOnce(Graph) -> Graph, @@ -426,18 +342,6 @@ pub fn filter_nodes(graph: &Graph, filter: impl CreateFilter) -> Vec { results } -#[cfg(feature = "search")] -pub fn search_nodes(graph: &Graph, filter: impl TryAsCompositeFilter) -> Vec { - let mut results = graph - .search_nodes(filter, 10, 0) - .expect("Failed to search nodes") - .into_iter() - .map(|v| v.name()) - .collect::>(); - results.sort(); - results -} - pub fn filter_edges(graph: &Graph, filter: impl CreateFilter) -> Vec { let mut results = graph .filter(filter) @@ -450,18 +354,6 @@ pub fn filter_edges(graph: &Graph, filter: impl CreateFilter) -> Vec { results } -#[cfg(feature = "search")] -pub fn search_edges(graph: &Graph, filter: impl TryAsCompositeFilter) -> Vec { - let mut results = graph - .search_edges(filter, 10, 0) - .expect("Failed to filter edges") - .into_iter() - .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) - .collect::>(); - results.sort(); - results -} - pub type EdgeRow = (u64, u64, i64, String, i64); pub fn assert_ok_or_missing_edges( diff --git a/raphtory-tests/tests/cached_view.rs b/raphtory-tests/tests/cached_view.rs index 79905cd829..79e4cf34a4 100644 --- a/raphtory-tests/tests/cached_view.rs +++ b/raphtory-tests/tests/cached_view.rs @@ -170,7 +170,7 @@ mod test_filters_cached_view { }; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_search_nodes_results, TestGraphVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; @@ -216,13 +216,6 @@ mod test_filters_cached_view { &expected_results, TestVariants::EventOnly, ); - assert_search_nodes_results( - init_graph, - CachedGraphTransformer, - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -237,13 +230,6 @@ mod test_filters_cached_view { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowedCachedGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -257,13 +243,6 @@ mod test_filters_cached_view { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowedCachedGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } } @@ -274,7 +253,7 @@ mod test_filters_cached_view { }; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_search_edges_results, TestVariants, + assert_filter_edges_results, TestVariants, }; use crate::test_filters_cached_view::{ @@ -323,13 +302,6 @@ mod test_filters_cached_view { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - CachedGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -343,13 +315,6 @@ mod test_filters_cached_view { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowedCachedGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -364,13 +329,6 @@ mod test_filters_cached_view { &expected_results, vec![], ); - assert_search_edges_results( - init_graph, - WindowedCachedGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } } } diff --git a/raphtory-tests/tests/graph_index.rs b/raphtory-tests/tests/graph_index.rs deleted file mode 100644 index 7dd254d1f4..0000000000 --- a/raphtory-tests/tests/graph_index.rs +++ /dev/null @@ -1,968 +0,0 @@ -#[cfg(all(test, feature = "search"))] -mod graph_index_test { - use raphtory::prelude::{AdditionOps, Graph, GraphViewOps, IndexMutationOps}; - - use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::NodeFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, - }; - use raphtory_tests::assertions::{search_edges, search_nodes}; - - fn init_nodes_graph(graph: Graph) -> Graph { - graph - .add_node(1, 1, [("p1", 1), ("p2", 2)], Some("fire_nation"), None) - .unwrap(); - graph - .add_node(2, 1, [("p6", 6)], Some("fire_nation"), None) - .unwrap(); - graph - .add_node(2, 2, [("p4", 5)], Some("fire_nation"), None) - .unwrap(); - graph - .add_node(3, 3, [("p2", 4), ("p3", 3)], Some("water_tribe"), None) - .unwrap(); - graph - } - - fn init_edges_graph(graph: Graph) -> Graph { - graph - .add_edge(1, 1, 2, [("p1", 1), ("p2", 2)], None) - .unwrap(); - graph.add_edge(2, 1, 2, [("p6", 6)], None).unwrap(); - graph.add_edge(2, 2, 3, [("p4", 5)], None).unwrap(); - graph - .add_edge(3, 3, 4, [("p2", 4), ("p3", 3)], None) - .unwrap(); - graph - } - - #[test] - fn test_if_bulk_load_create_graph_index_is_ok() { - let graph = Graph::new(); - let graph = init_nodes_graph(graph); - - assert_eq!(graph.count_nodes(), 3); - - graph.create_index_in_ram().unwrap(); - } - - #[test] - fn test_if_adding_nodes_to_existing_graph_index_is_ok() { - let graph = Graph::new(); - graph.create_index_in_ram().unwrap(); - - let graph = init_nodes_graph(graph); - - assert_eq!(graph.count_nodes(), 3); - } - - #[test] - fn test_if_adding_edges_to_existing_graph_index_is_ok() { - let graph = Graph::new(); - // Creates graph index - graph.create_index_in_ram().unwrap(); - - let graph = init_edges_graph(graph); - - assert_eq!(graph.count_edges(), 3); - } - - #[test] - fn test_node_metadata_graph_index_is_ok() { - let graph = Graph::new(); - let graph = init_nodes_graph(graph); - graph.create_index_in_ram().unwrap(); - graph.node(1).unwrap().add_metadata([("x", 1u64)]).unwrap(); - - let filter = NodeFilter.metadata("x").eq(1u64); - assert_eq!(search_nodes(&graph, filter.clone()), vec!["1"]); - - graph - .node(1) - .unwrap() - .update_metadata([("x", 2u64)]) - .unwrap(); - let filter = NodeFilter.metadata("x").eq(1u64); - assert_eq!(search_nodes(&graph, filter.clone()), Vec::<&str>::new()); - - graph - .node(1) - .unwrap() - .update_metadata([("x", 2u64)]) - .unwrap(); - let filter = NodeFilter.metadata("x").eq(2u64); - assert_eq!(search_nodes(&graph, filter.clone()), vec!["1"]); - } - - #[test] - fn test_edge_metadata_graph_index_is_ok() { - let graph = Graph::new(); - let graph = init_edges_graph(graph); - graph.create_index_in_ram().unwrap(); - graph - .edge(1, 2) - .unwrap() - .add_metadata([("x", 1u64)], None) - .unwrap(); - - let filter = EdgeFilter.metadata("x").eq(1u64); - assert_eq!(search_edges(&graph, filter.clone()), vec!["1->2"]); - - graph - .edge(1, 2) - .unwrap() - .update_metadata([("x", 2u64)], None) - .unwrap(); - let filter = EdgeFilter.metadata("x").eq(1u64); - assert_eq!(search_edges(&graph, filter.clone()), Vec::<&str>::new()); - - graph - .edge(1, 2) - .unwrap() - .update_metadata([("x", 2u64)], None) - .unwrap(); - let filter = EdgeFilter.metadata("x").eq(2u64); - assert_eq!(search_edges(&graph, filter.clone()), vec!["1->2"]); - } -} - -#[cfg(all(test, feature = "search"))] -mod test_index { - mod test_index_io { - use raphtory::{ - db::{ - api::view::{internal::InternalStorageOps, ResolvedIndexSpec}, - graph::views::filter::model::{ - node_filter::{ops::NodeFilterOps, NodeFilter}, - TryAsCompositeFilter, - }, - }, - errors::GraphError, - prelude::*, - serialise::GraphFolder, - }; - use raphtory_api::core::{ - entities::properties::prop::Prop, storage::arc_str::ArcStr, - utils::logging::global_info_logger, - }; - use tempfile::TempDir; - - fn init_graph() -> Graph { - let graph = Graph::new(); - - graph - .add_node( - 1, - "Alice", - vec![("p1", Prop::U64(1000u64))], - Some("fire_nation"), - None, - ) - .unwrap(); - graph - } - - fn assert_search_results( - graph: &Graph, - filter: &T, - expected: Vec<&str>, - ) { - let res = graph - .search_nodes(filter.clone(), 2, 0) - .unwrap() - .into_iter() - .map(|n| n.name()) - .collect::>(); - assert_eq!(res, expected); - } - - #[test] - fn test_create_no_index_persist_no_index_on_encode_load_no_index_on_decode() { - // No index persisted since it was never created - let graph = init_graph(); - - let filter = NodeFilter::name().eq("Alice"); - assert_search_results(&graph, &filter, vec!["Alice"]); - - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - let graph = Graph::decode(path).unwrap(); - let is_indexed = graph.get_storage().unwrap().is_indexed(); - assert!(!is_indexed); - } - - #[test] - fn test_create_index_persist_index_on_encode_load_index_on_decode() { - let graph = init_graph(); - - // Created index - graph.create_index().unwrap(); - - let filter = NodeFilter::name().eq("Alice"); - assert_search_results(&graph, &filter, vec!["Alice"]); - - // Persisted both graph and index - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - // Loaded index that was persisted - let graph = Graph::decode(path).unwrap(); - let is_indexed = graph.get_storage().unwrap().is_indexed(); - assert!(is_indexed); - - assert_search_results(&graph, &filter, vec!["Alice"]); - } - - #[test] - fn test_encoding_graph_twice_to_same_storage_path_fails() { - let graph = init_graph(); - graph.create_index().unwrap(); - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - let result = graph.encode(path); - - match result { - Err(GraphError::NonEmptyGraphFolder(err_path)) => { - assert_eq!(path, err_path); - } - Ok(_) => panic!("Expected error on second encode, got Ok"), - Err(e) => panic!("Unexpected error type: {:?}", e), - } - } - - #[test] - fn test_create_index_persist_index_on_encode_update_index_load_persisted_index_on_decode() { - let graph = init_graph(); - - // Created index - graph.create_index().unwrap(); - - let filter1 = NodeFilter::name().eq("Alice"); - assert_search_results(&graph, &filter1, vec!["Alice"]); - - // Persisted both graph and index - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - // Updated both graph and index - graph - .add_node( - 2, - "Tommy", - vec![("p1", Prop::U64(5u64))], - Some("water_tribe"), - None, - ) - .unwrap(); - let filter2 = NodeFilter::name().eq("Tommy"); - assert_search_results(&graph, &filter2, vec!["Tommy"]); - - // Loaded index that was persisted - let graph = Graph::decode(path).unwrap(); - let is_indexed = graph.get_storage().unwrap().is_indexed(); - assert!(is_indexed); - assert_search_results(&graph, &filter1, vec!["Alice"]); - assert_search_results(&graph, &filter2, Vec::<&str>::new()); - - // Updating and encode the graph and index should decode the updated the graph as well as index - // So far we have the index that was created and persisted for the first time - graph - .add_node( - 2, - "Tommy", - vec![("p1", Prop::U64(5u64))], - Some("water_tribe"), - None, - ) - .unwrap(); - let filter2 = NodeFilter::name().eq("Tommy"); - assert_search_results(&graph, &filter2, vec!["Tommy"]); - - // Should persist the updated graph and index - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - // Should load the updated graph and index - let graph = Graph::decode(path).unwrap(); - let is_indexed = graph.get_storage().unwrap().is_indexed(); - assert!(is_indexed); - assert_search_results(&graph, &filter1, vec!["Alice"]); - assert_search_results(&graph, &filter2, vec!["Tommy"]); - } - - #[test] - fn test_zip_encode_decode_index() { - let graph = init_graph(); - graph.create_index().unwrap(); - let tmp_dir = TempDir::new().unwrap(); - let zip_path = tmp_dir.path().join("graph.zip"); - let folder = GraphFolder::new_as_zip(zip_path); - graph.encode(&folder).unwrap(); - - let graph = Graph::decode(&folder).unwrap(); - let node = graph.node("Alice").unwrap(); - let node_type = node.node_type(); - assert_eq!(node_type, Some(ArcStr::from("fire_nation"))); - - let filter = NodeFilter::name().eq("Alice"); - assert_search_results(&graph, &filter, vec!["Alice"]); - } - - #[test] - fn test_encoding_graph_twice_to_same_storage_path_fails_zip() { - let graph = init_graph(); - graph.create_index().unwrap(); - let tmp_dir = TempDir::new().unwrap(); - let zip_path = tmp_dir.path().join("graph.zip"); - let folder = GraphFolder::new_as_zip(&zip_path); - graph.encode(&folder).unwrap(); - graph - .add_node(1, "Ozai", [("prop", 1)], Some("fire_nation"), None) - .unwrap(); - let result = graph.encode(folder); - match result { - Err(GraphError::IOError { source, .. }) => { - assert!( - format!("{source}").to_lowercase().contains("file exists"), - "{}", - source - ); - } - Ok(_) => panic!("Expected error on second encode, got Ok"), - Err(e) => panic!("Unexpected error type: {:?}", e), - } - } - - #[test] - fn test_immutable_graph_index_persistence() { - let graph = init_graph(); - graph.create_index().unwrap(); - - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - // This gives us immutable index - let graph = Graph::decode(path).unwrap(); - - // This tests that we are able to persist the immutable index - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - let graph = Graph::decode(path).unwrap(); - let filter1 = NodeFilter::name().eq("Alice"); - assert_search_results(&graph, &filter1, vec!["Alice"]); - } - - #[test] - fn test_mutable_graph_index_persistence() { - let graph = init_graph(); - graph.create_index().unwrap(); - - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - // This gives us immutable index - let graph = Graph::decode(path).unwrap(); - - // This converts immutable index to mutable index - graph - .add_node(1, "Ozai", [("prop", 1)], Some("fire_nation"), None) - .unwrap(); - - // This tests that we are able to persist the mutable index - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - let graph = Graph::decode(path).unwrap(); - let filter = NodeFilter::name().eq("Ozai"); - assert_search_results(&graph, &filter, vec!["Ozai"]); - } - - #[test] - fn test_loading_zip_index_creates_mutable_index() { - let graph = init_graph(); - graph.create_index().unwrap(); - let tmp_dir = TempDir::new().unwrap(); - let zip_path = tmp_dir.path().join("graph.zip"); - let folder = GraphFolder::new_as_zip(&zip_path); - graph.encode(&folder).unwrap(); - - let graph = Graph::decode(&folder).unwrap(); - let immutable = graph - .get_storage() - .unwrap() - .index() - .read_recursive() - .is_immutable(); - assert! {!immutable}; - } - - #[test] - fn test_loading_index_creates_immutable_index() { - let graph = init_graph(); - graph.create_index().unwrap(); - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - let graph = Graph::decode(path).unwrap(); - let immutable = graph - .get_storage() - .unwrap() - .index() - .read_recursive() - .is_immutable(); - assert! {immutable}; - } - - #[test] - fn test_create_index_in_ram() { - global_info_logger(); - - let graph = init_graph(); - graph.create_index_in_ram().unwrap(); - - let filter = NodeFilter::name().eq("Alice"); - assert_search_results(&graph, &filter, vec!["Alice"]); - - let binding = TempDir::new().unwrap(); - let path = binding.path(); - graph.encode(path).unwrap(); - - let graph = Graph::decode(path).unwrap(); - let is_indexed = graph.get_storage().unwrap().is_indexed(); - assert!(!is_indexed); - - assert_search_results(&graph, &filter, vec!["Alice"]); - } - - #[test] - #[ignore] - fn test_too_many_open_files_graph_index() { - let mut graphs = vec![]; - - for _ in 0..1000 { - let graph = init_graph(); - if let Err(e) = graph.create_index() { - match &e { - GraphError::IndexError { source } => { - panic!("Hit file descriptor limit after {} graphs. {:?}", 0, source); - } - other => { - panic!("Unexpected GraphError: {:?}", other); - } - } - } - graphs.push(graph); - } - } - - #[test] - fn test_graph_index_creation_with_too_many_properties() { - let graph = init_graph(); - let props: Vec<(String, Prop)> = (1..=100) - .map(|i| (format!("p{i}"), Prop::U64(i as u64))) - .collect(); - graph.node("Alice").unwrap().add_metadata(props).unwrap(); - - if let Err(e) = graph.create_index() { - match &e { - GraphError::IndexError { source } => { - panic!("Hit file descriptor limit after {} graphs. {:?}", 0, source); - } - other => { - panic!("Unexpected GraphError: {:?}", other); - } - } - } - } - - #[test] - // No new const prop index created because when index were created - // these properties did not exist. - fn test_graph_index_creation_for_incremental_node_update_no_new_prop_indexed() { - let graph = init_graph(); - graph.create_index().unwrap(); - let props: Vec<(String, Prop)> = (1..=100) - .map(|i| (format!("p{i}"), Prop::U64(i as u64))) - .collect(); - graph.node("Alice").unwrap().add_metadata(props).unwrap(); - - let tmp_dir = TempDir::new().unwrap(); - let path = tmp_dir.path().to_path_buf(); - graph.encode(&path).unwrap(); - let graph = Graph::decode(&path).unwrap(); - - let spec = graph.get_index_spec().unwrap().props(&graph); - assert_eq!( - spec, - ResolvedIndexSpec { - node_properties: vec!["p1".to_string()], - node_metadata: vec![], - edge_metadata: vec![], - edge_properties: vec![] - } - ); - } - } - - mod test_index_spec { - use raphtory::{ - db::{ - api::view::{IndexSpec, IndexSpecBuilder}, - graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::NodeFilter, - property_filter::ops::PropertyFilterOps, ComposableFilter, - PropertyFilterFactory, TemporalPropertyFilterFactory, - }, - }, - errors::GraphError, - prelude::{AdditionOps, Graph, IndexMutationOps, SearchableGraphOps, StableDecode}, - serialise::{GraphFolder, StableEncode}, - }; - use raphtory_tests::assertions::{search_edges, search_nodes}; - use tempfile::{tempdir, TempDir}; - - fn init_graph() -> Graph { - let graph = Graph::new(); - - let nodes = vec![ - ( - 1, - "pometry", - [("p1", 5u64), ("p2", 50u64)], - Some("fire_nation"), - [("x", true)], - ), - ( - 1, - "raphtory", - [("p1", 10u64), ("p2", 100u64)], - Some("water_tribe"), - [("y", false)], - ), - ]; - - for (time, name, props, group, metadata) in nodes { - let node = graph.add_node(time, name, props, group, None).unwrap(); - node.add_metadata(metadata).unwrap(); - } - - let edges = vec![ - ( - 1, - "pometry", - "raphtory", - [("e_p1", 3.2f64), ("e_p2", 10f64)], - None, - [("e_x", true)], - ), - ( - 1, - "raphtory", - "pometry", - [("e_p1", 4.0f64), ("e_p2", 20f64)], - None, - [("e_y", false)], - ), - ]; - - for (time, src, dst, props, label, metadata) in edges { - let edge = graph.add_edge(time, src, dst, props, label).unwrap(); - edge.add_metadata(metadata, label).unwrap(); - } - - graph - } - - #[test] - #[ignore = "TODO: #2372"] - fn test_with_all_props_index_spec() { - let graph = init_graph(); - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_all_node_properties_and_metadata() - .with_all_edge_properties_and_metadata() - .build(); - assert_eq!( - index_spec.props(&graph).to_vec(), - vec![ - vec!["x", "y"], - vec!["p1", "p2"], - vec!["e_x", "e_y"], - vec!["e_p1", "e_p2"] - ] - ); - graph.create_index_in_ram_with_spec(index_spec).unwrap(); - - let filter = NodeFilter - .property("p1") - .eq(5u64) - .and(NodeFilter.metadata("x").eq(true)); - let results = search_nodes(&graph, filter); - assert_eq!(results, vec!["pometry"]); - - let filter = EdgeFilter - .property("e_p1") - .lt(5f64) - .and(EdgeFilter.metadata("e_y").eq(false)); - let results = search_edges(&graph, filter); - assert_eq!(results, vec!["raphtory->pometry"]); - } - - #[test] - #[ignore = "TODO: #2372"] - fn test_with_selected_props_index_spec() { - let graph = init_graph(); - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_node_metadata(vec!["y"]) - .unwrap() - .with_node_properties(vec!["p1"]) - .unwrap() - .with_edge_metadata(vec!["e_y"]) - .unwrap() - .with_edge_properties(vec!["e_p1"]) - .unwrap() - .build(); - assert_eq!( - index_spec.props(&graph).to_vec(), - vec![vec!["y"], vec!["p1"], vec!["e_y"], vec!["e_p1"]] - ); - graph.create_index_in_ram_with_spec(index_spec).unwrap(); - - let filter = NodeFilter - .property("p1") - .eq(5u64) - .or(NodeFilter.metadata("y").eq(false)); - let results = search_nodes(&graph, filter); - assert_eq!(results, vec!["pometry", "raphtory"]); - - let filter = NodeFilter.metadata("y").eq(false); - let results = search_nodes(&graph, filter); - assert_eq!(results, vec!["raphtory"]); - - let filter = EdgeFilter - .property("e_p1") - .lt(5f64) - .or(EdgeFilter.metadata("e_y").eq(false)); - let results = search_edges(&graph, filter); - assert_eq!(results, vec!["pometry->raphtory", "raphtory->pometry"]); - } - - #[test] - fn test_with_invalid_property_returns_error() { - let graph = init_graph(); - let result = IndexSpecBuilder::new(graph.clone()).with_node_metadata(["xyz"]); - - assert!(matches!(result, Err(GraphError::PropertyMissingError(p)) if p == "xyz")); - } - - #[test] - fn test_build_empty_spec_by_default() { - let graph = init_graph(); - let index_spec = IndexSpecBuilder::new(graph.clone()).build(); - - assert!(index_spec.node_metadata().is_empty()); - assert!(index_spec.node_properties().is_empty()); - assert!(index_spec.edge_metadata().is_empty()); - assert!(index_spec.edge_properties().is_empty()); - - graph.create_index_in_ram_with_spec(index_spec).unwrap(); - - let filter = NodeFilter - .property("p1") - .eq(5u64) - .and(NodeFilter.metadata("x").eq(true)); - let results = search_nodes(&graph, filter); - assert_eq!(results, vec!["pometry"]); - - let filter = EdgeFilter - .property("e_p1") - .lt(5f64) - .or(EdgeFilter.metadata("e_y").eq(false)); - let results = search_edges(&graph, filter); - assert_eq!(results, vec!["pometry->raphtory", "raphtory->pometry"]); - } - - #[test] - #[ignore = "TODO: #2372"] - fn test_mixed_node_and_edge_props_index_spec() { - let graph = init_graph(); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_node_metadata(vec!["x"]) - .unwrap() - .with_all_node_properties() - .with_all_edge_properties_and_metadata() - .build(); - assert_eq!( - index_spec.props(&graph).to_vec(), - vec![ - vec!["x"], - vec!["p1", "p2"], - vec!["e_x", "e_y"], - vec!["e_p1", "e_p2"] - ] - ); - - graph.create_index_in_ram_with_spec(index_spec).unwrap(); - - let filter = NodeFilter - .property("p1") - .eq(5u64) - .or(NodeFilter.metadata("y").eq(false)); - let results = search_nodes(&graph, filter); - assert_eq!(results, vec!["pometry", "raphtory"]); - - let filter = EdgeFilter - .property("e_p1") - .lt(5f64) - .or(EdgeFilter.metadata("e_y").eq(false)); - let results = search_edges(&graph, filter); - assert_eq!(results, vec!["pometry->raphtory", "raphtory->pometry"]); - } - - #[test] - fn test_get_index_spec_newly_created_index() { - let graph = init_graph(); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_node_metadata(vec!["x"]) - .unwrap() - .with_all_node_properties() - .with_all_edge_properties_and_metadata() - .build(); - - graph - .create_index_in_ram_with_spec(index_spec.clone()) - .unwrap(); - - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - } - - #[test] - #[ignore = "TODO: #2372"] - fn test_get_index_spec_updated_index() { - let graph = init_graph(); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_edge_metadata(vec!["e_y"]) - .unwrap() - .build(); - graph.create_index_with_spec(index_spec.clone()).unwrap(); - - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let results = search_nodes(&graph, NodeFilter.metadata("y").eq(false)); - assert_eq!(results, vec!["raphtory"]); - let results = search_edges(&graph, EdgeFilter.metadata("e_y").eq(false)); - assert_eq!(results, vec!["raphtory->pometry"]); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_node_metadata(vec!["y"]) - .unwrap() - .with_node_properties(vec!["p2"]) - .unwrap() - .with_edge_metadata(vec!["e_y"]) - .unwrap() - .build(); - graph.create_index_with_spec(index_spec.clone()).unwrap(); - - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let results = search_nodes(&graph, NodeFilter.metadata("y").eq(false)); - assert_eq!(results, vec!["raphtory"]); - let results = search_edges(&graph, EdgeFilter.metadata("e_y").eq(false)); - assert_eq!(results, vec!["raphtory->pometry"]); - } - - #[test] - #[ignore = "TODO: #2372"] - fn test_get_index_spec_updated_index_persisted_and_loaded() { - let graph = init_graph(); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_edge_metadata(vec!["e_y"]) - .unwrap() - .build(); - graph.create_index_with_spec(index_spec.clone()).unwrap(); - - let tmp_graph_dir = tempdir().unwrap(); - let path = tmp_graph_dir.path().to_path_buf(); - graph.encode(&path).unwrap(); - let graph = Graph::decode(&path).unwrap(); - - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let results = search_nodes(&graph, NodeFilter.metadata("y").eq(false)); - assert_eq!(results, vec!["raphtory"]); - let results = search_edges(&graph, EdgeFilter.metadata("e_y").eq(false)); - assert_eq!(results, vec!["raphtory->pometry"]); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_node_metadata(vec!["y"]) - .unwrap() - .with_node_properties(vec!["p2"]) - .unwrap() - .with_edge_metadata(vec!["e_y"]) - .unwrap() - .build(); - graph.create_index_with_spec(index_spec.clone()).unwrap(); - let tmp_graph_dir = tempdir().unwrap(); - let path = tmp_graph_dir.path().to_path_buf(); - graph.encode(path.clone()).unwrap(); - let graph = Graph::decode(&path).unwrap(); - - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let results = search_nodes(&graph, NodeFilter.metadata("y").eq(false)); - assert_eq!(results, vec!["raphtory"]); - let results = search_edges(&graph, EdgeFilter.metadata("e_y").eq(false)); - assert_eq!(results, vec!["raphtory->pometry"]); - } - - #[test] - fn test_get_index_spec_loaded_index() { - let graph = init_graph(); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_node_metadata(vec!["y"]) - .unwrap() - .with_node_properties(vec!["p2"]) - .unwrap() - .with_edge_metadata(vec!["e_y"]) - .unwrap() - .with_edge_properties(vec!["e_p2"]) - .unwrap() - .build(); - - graph.create_index_with_spec(index_spec.clone()).unwrap(); - let tmp_graph_dir = tempdir().unwrap(); - let path = tmp_graph_dir.path().to_path_buf(); - graph.encode(path.clone()).unwrap(); - - let graph = Graph::decode(&path).unwrap(); - let index_spec2 = graph.get_index_spec().unwrap(); - - assert_eq!(index_spec, index_spec2); - } - - #[test] - fn test_get_index_spec_loaded_index_zip() { - let graph = init_graph(); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_node_metadata(vec!["y"]) - .unwrap() - .with_node_properties(vec!["p2"]) - .unwrap() - .with_edge_metadata(vec!["e_y"]) - .unwrap() - .build(); - graph.create_index_with_spec(index_spec.clone()).unwrap(); - - let binding = TempDir::new().unwrap(); - let path = binding.path(); - let folder = GraphFolder::new_as_zip(path); - graph.encode(folder).unwrap(); - - let graph = Graph::decode(path).unwrap(); - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - } - - #[test] - fn test_no_new_node_prop_index_created_via_update_apis() { - run_node_index_test(|graph, index_spec| { - graph.create_index_with_spec(index_spec.clone()) - }); - - run_node_index_test(|graph, index_spec| { - graph.create_index_in_ram_with_spec(index_spec.clone()) - }); - } - - #[test] - fn test_no_new_edge_prop_index_created_via_update_apis() { - run_edge_index_test(|graph, index_spec| { - graph.create_index_with_spec(index_spec.clone()) - }); - - run_edge_index_test(|graph, index_spec| { - graph.create_index_in_ram_with_spec(index_spec.clone()) - }); - } - - fn run_node_index_test(create_index_fn: F) - where - F: Fn(&Graph, IndexSpec) -> Result<(), GraphError>, - { - let graph = init_graph(); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_node_metadata(vec!["y"]) - .unwrap() - .with_node_properties(vec!["p1"]) - .unwrap() - .build(); - create_index_fn(&graph, index_spec.clone()).unwrap(); - - let filter = NodeFilter.property("p2").temporal().last().eq(50u64); - assert_eq!(search_nodes(&graph, filter.clone()), vec!["pometry"]); - - let node = graph - .add_node(1, "shivam", [("p1", 100u64)], Some("fire_nation"), None) - .unwrap(); - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - - let filter = NodeFilter.property("p1").temporal().last().eq(100u64); - assert_eq!(search_nodes(&graph, filter.clone()), vec!["shivam"]); - - node.add_metadata([("z", true)]).unwrap(); - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = NodeFilter.metadata("z").eq(true); - assert_eq!(search_nodes(&graph, filter.clone()), vec!["shivam"]); - - node.update_metadata([("z", false)]).unwrap(); - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = NodeFilter.metadata("z").eq(false); - assert_eq!(search_nodes(&graph, filter.clone()), vec!["shivam"]); - } - - fn run_edge_index_test(create_index_fn: F) - where - F: Fn(&Graph, IndexSpec) -> Result<(), GraphError>, - { - let graph = init_graph(); - - let index_spec = IndexSpecBuilder::new(graph.clone()) - .with_node_metadata(vec!["y"]) - .unwrap() - .with_node_properties(vec!["p2"]) - .unwrap() - .build(); - create_index_fn(&graph, index_spec.clone()).unwrap(); - - let edge = graph - .add_edge(1, "shivam", "kapoor", [("p1", 100u64)], None) - .unwrap(); - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = EdgeFilter.property("p1").temporal().last().eq(100u64); - assert_eq!(search_edges(&graph, filter.clone()), vec!["shivam->kapoor"]); - - edge.add_metadata([("z", true)], None).unwrap(); - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = EdgeFilter.metadata("z").eq(true); - assert_eq!(search_edges(&graph, filter.clone()), vec!["shivam->kapoor"]); - - edge.update_metadata([("z", false)], None).unwrap(); - assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = EdgeFilter.metadata("z").eq(false); - assert_eq!(search_edges(&graph, filter.clone()), vec!["shivam->kapoor"]); - } - } -} diff --git a/raphtory-tests/tests/subgraph_tests.rs b/raphtory-tests/tests/subgraph_tests.rs index 18ec426856..2f3cfb4955 100644 --- a/raphtory-tests/tests/subgraph_tests.rs +++ b/raphtory-tests/tests/subgraph_tests.rs @@ -188,7 +188,7 @@ pub mod test_filters_node_subgraph { }; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_search_nodes_results, TestGraphVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; @@ -230,13 +230,6 @@ pub mod test_filters_node_subgraph { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - NodeSubgraphTransformer(None), - filter, - &expected_results, - TestVariants::All, - ); let node_names: Option> = Some(vec!["N2".into(), "N3".into(), "N4".into(), "N5".into()]); @@ -249,13 +242,6 @@ pub mod test_filters_node_subgraph { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - NodeSubgraphTransformer(node_names), - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -270,13 +256,6 @@ pub mod test_filters_node_subgraph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowedNodeSubgraphTransformer(None, 6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let node_names: Option> = Some(vec!["N3".into()]); let filter = NodeFilter.property("p1").gt(0u64); @@ -288,13 +267,6 @@ pub mod test_filters_node_subgraph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowedNodeSubgraphTransformer(node_names, 6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -308,13 +280,6 @@ pub mod test_filters_node_subgraph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowedNodeSubgraphTransformer(None, 6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let node_names: Option> = Some(vec!["N2".into(), "N3".into(), "N4".into(), "N5".into()]); @@ -327,13 +292,6 @@ pub mod test_filters_node_subgraph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowedNodeSubgraphTransformer(node_names, 6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } } @@ -349,7 +307,7 @@ pub mod test_filters_node_subgraph { }; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_search_edges_results, TestVariants, + assert_filter_edges_results, TestVariants, }; use crate::test_filters_node_subgraph::{ @@ -393,13 +351,6 @@ pub mod test_filters_node_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - NodeSubgraphTransformer(None), - filter, - &expected_results, - TestVariants::All, - ); let node_names: Option> = Some(vec!["N2".into(), "N3".into(), "N4".into(), "N5".into()]); @@ -412,13 +363,6 @@ pub mod test_filters_node_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - NodeSubgraphTransformer(node_names), - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -432,13 +376,6 @@ pub mod test_filters_node_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowedNodeSubgraphTransformer(None, 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let node_names: Option> = Some(vec!["N2".into(), "N3".into(), "N4".into(), "N5".into()]); @@ -451,13 +388,6 @@ pub mod test_filters_node_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowedNodeSubgraphTransformer(node_names, 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -472,13 +402,6 @@ pub mod test_filters_node_subgraph { &expected_results, vec![], ); - assert_search_edges_results( - init_graph, - WindowedNodeSubgraphTransformer(None, 6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let node_names: Option> = Some(vec![ "N2".into(), @@ -496,13 +419,6 @@ pub mod test_filters_node_subgraph { &expected_results, vec![], ); - assert_search_edges_results( - init_graph, - WindowedNodeSubgraphTransformer(node_names, 6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } } } diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index b032a61bdb..4914cdc98d 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -134,7 +134,7 @@ mod test_property_semantics { addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, }; use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_search_nodes_results, TestVariants, + assert_filter_nodes_results, TestVariants, }; fn init_graph(graph: G) -> G { @@ -232,13 +232,6 @@ mod test_property_semantics { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -252,13 +245,6 @@ mod test_property_semantics { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -273,13 +259,6 @@ mod test_property_semantics { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -293,13 +272,6 @@ mod test_property_semantics { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -313,13 +285,6 @@ mod test_property_semantics { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -334,13 +299,6 @@ mod test_property_semantics { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -354,13 +312,6 @@ mod test_property_semantics { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -450,13 +401,6 @@ mod test_property_semantics { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } } @@ -481,7 +425,7 @@ mod test_property_semantics { addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, }; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_search_edges_results, TestGraphVariants, + assert_filter_edges_results, TestGraphVariants, TestVariants, WindowGraphTransformer, }; @@ -612,13 +556,6 @@ mod test_property_semantics { &expected_empty, vec![TestGraphVariants::PersistentGraph], ); - assert_search_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_empty, - TestVariants::PersistentOnly, - ); // Window(1,10); Expected emtpy because the first update is at time 0 and the value of p1 is expected to be 1u64. assert_filter_edges_results( @@ -628,13 +565,6 @@ mod test_property_semantics { &expected_empty, vec![TestGraphVariants::PersistentGraph], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(1..10), - filter.clone(), - &expected_empty, - TestVariants::PersistentOnly, - ); // Window(2,10); Expected update at time 2 and the value of p1 is expected to be 2u64. assert_filter_edges_results( @@ -644,13 +574,6 @@ mod test_property_semantics { &expected_found, vec![TestGraphVariants::PersistentGraph], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(2..10), - filter.clone(), - &expected_found, - TestVariants::PersistentOnly, - ); // Window(3,10); Expected update at time 2 (even if it is outside the window) and the value of p1 is expected to be 2u64. assert_filter_edges_results( @@ -660,13 +583,6 @@ mod test_property_semantics { &expected_found, vec![TestGraphVariants::PersistentGraph], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(3..10), - filter.clone(), - &expected_found, - TestVariants::PersistentOnly, - ); // Window(4,10); Expected update at time 2 (even if it is outside the window) and the value of p1 is expected to be 2u64. assert_filter_edges_results( @@ -676,13 +592,6 @@ mod test_property_semantics { &expected_found, vec![TestGraphVariants::PersistentGraph], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(4..10), - filter.clone(), - &expected_found, - TestVariants::PersistentOnly, - ); // Window(5,10); Expected update at time 5 (even if it is outside the window) and the value of p1 is expected to be 5u64. assert_filter_edges_results( @@ -692,13 +601,6 @@ mod test_property_semantics { &expected_empty, vec![TestGraphVariants::PersistentGraph], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(5..10), - filter.clone(), - &expected_empty, - TestVariants::PersistentOnly, - ); } #[test] @@ -717,13 +619,6 @@ mod test_property_semantics { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -790,13 +685,6 @@ mod test_property_semantics { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -814,13 +702,6 @@ mod test_property_semantics { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -835,13 +716,6 @@ mod test_property_semantics { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -857,13 +731,6 @@ mod test_property_semantics { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -881,13 +748,6 @@ mod test_property_semantics { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -904,13 +764,6 @@ mod test_property_semantics { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -1000,13 +853,6 @@ mod test_property_semantics { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } } } @@ -1677,7 +1523,7 @@ mod test_node_filter { }; use raphtory_api::core::{entities::properties::prop::Prop, Direction}; use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_search_nodes_results, assert_select_nodes_results, + assert_filter_nodes_results, assert_select_nodes_results, TestVariants, }; @@ -2394,13 +2240,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2414,13 +2253,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2434,13 +2266,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.name().is_in(vec![""]); let expected_results = Vec::<&str>::new(); @@ -2451,13 +2276,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.name().is_in(vec!["2", "3"]); let expected_results = vec!["2", "3"]; @@ -2468,13 +2286,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2488,13 +2299,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.name().is_not_in(vec![""]); let expected_results = vec!["1", "2", "3", "4"]; @@ -2505,13 +2309,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2525,13 +2322,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2546,13 +2336,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2566,13 +2349,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.node_type().is_in(vec!["fire_nation", "air_nomads"]); let expected_results = vec!["1", "2", "3"]; @@ -2583,13 +2359,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2604,13 +2373,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2624,13 +2386,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.node_type().starts_with("rocket"); let expected_results = vec![]; @@ -2641,13 +2396,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2661,13 +2409,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.node_type().ends_with("circle"); let expected_results = vec![]; @@ -2678,13 +2419,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2698,13 +2432,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2719,13 +2446,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2772,13 +2492,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2793,13 +2506,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.id().eq(1); let expected_results = vec!["1"]; @@ -2832,13 +2538,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.id().ne(1); let expected_results = vec!["2", "3", "4"]; @@ -2850,13 +2549,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2871,13 +2563,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.id().is_in(vec![1, 3, 6]); let expected_results = vec!["1", "3"]; @@ -2889,13 +2574,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2910,13 +2588,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.id().is_not_in(vec![1, 3, 6]); let expected_results = vec!["2", "4"]; @@ -2928,13 +2599,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -2949,13 +2613,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -2970,13 +2627,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -2991,13 +2641,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -3012,13 +2655,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -3045,13 +2681,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -3091,13 +2720,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -3111,13 +2733,6 @@ mod test_node_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -3232,7 +2847,7 @@ mod test_node_property_filter { use raphtory_api::core::entities::properties::prop::Prop; use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_search_nodes_results, TestVariants, + assert_filter_nodes_results, TestVariants, }; use std::vec; @@ -3248,13 +2863,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p10").eq(""); let expected_results = Vec::<&str>::new(); @@ -3265,13 +2873,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3285,13 +2886,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3305,13 +2899,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p30").temporal().first().eq("Old_boat"); let expected_results = vec!["2"]; @@ -3322,13 +2909,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p20").temporal().eq("Gold_ship").all(); let expected_results = vec!["1"]; @@ -3339,13 +2919,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3359,13 +2932,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p30").temporal().first().ne("Old_boat"); let expected_results = vec!["1", "4"]; @@ -3376,13 +2942,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p1").temporal().ne("Gold_ship").all(); // Both nodes have non-empty p1 streams whose values are all ne "Gold_ship". @@ -3395,13 +2954,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3415,13 +2967,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p40").temporal().first().lt(10u64); let expected_results = vec!["1"]; @@ -3432,13 +2977,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p9").temporal().lt(10u64).all(); let expected_results = vec!["1"]; @@ -3449,13 +2987,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3469,13 +3000,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p9").temporal().first().le(10u64); let expected_results = vec!["1"]; @@ -3486,13 +3010,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p2").temporal().le(10u64).all(); // Node 2 (p2=2) and node 3 (p2=6) both have non-empty streams satisfying le(10). @@ -3505,13 +3022,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3525,13 +3035,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p40").temporal().first().gt(5u64); let expected_results = vec!["2"]; @@ -3542,13 +3045,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p9").temporal().gt(1u64).all(); let expected_results = vec!["1"]; @@ -3559,13 +3055,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3579,13 +3068,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p40").temporal().first().ge(5u64); let expected_results = vec!["1", "2"]; @@ -3596,13 +3078,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p40").temporal().ge(5u64).all(); let expected_results = vec!["1", "2"]; @@ -3613,13 +3088,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3633,13 +3101,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p2") @@ -3652,13 +3113,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p40") @@ -3673,13 +3127,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p2") @@ -3694,13 +3141,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3714,13 +3154,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p40").is_not_in(vec![Prop::U64(6)]); let expected_results = vec!["1", "2"]; @@ -3731,13 +3164,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p2") @@ -3752,13 +3178,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3772,13 +3191,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p40").is_some(); let expected_results = vec!["1", "2"]; @@ -3789,13 +3201,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3809,13 +3214,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p40").is_none(); let expected_results = vec!["3", "4"]; @@ -3826,13 +3224,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3846,13 +3237,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -3867,13 +3251,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -3888,13 +3265,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -3909,13 +3279,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p30") @@ -3930,13 +3293,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p20") @@ -3951,13 +3307,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -3971,13 +3320,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -3992,13 +3334,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -4013,13 +3348,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -4034,13 +3362,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p20") @@ -4055,13 +3376,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p20") @@ -4076,13 +3390,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4096,13 +3403,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -4117,13 +3417,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -4138,13 +3431,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p30") @@ -4159,13 +3445,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p30").temporal().contains("Gold").all(); let expected_results: Vec<&str> = vec!["1"]; @@ -4176,13 +3455,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4196,13 +3468,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -4217,13 +3482,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p10") @@ -4238,13 +3496,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p30") @@ -4259,13 +3510,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p30") @@ -4280,13 +3524,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4304,13 +3541,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4324,13 +3554,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4344,13 +3567,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4368,13 +3584,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4392,13 +3601,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4412,13 +3614,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4438,13 +3633,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); // Wider window includes node 3 let filter = NodeFilter @@ -4462,13 +3650,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4487,13 +3668,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter1.clone(), - &expected_results, - TestVariants::All, - ); let expected_results = vec![]; assert_filter_nodes_results( @@ -4503,13 +3677,6 @@ mod test_node_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter2.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = NodeFilter .window(100, 200) @@ -4524,13 +3691,6 @@ mod test_node_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -4550,13 +3710,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .window(3, 5) @@ -4573,13 +3726,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -4610,13 +3756,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4632,13 +3771,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); // Only time=3 contributes; node 3 has p2=6 at t=3 let filter = NodeFilter.at(3).property("p2").temporal().sum().eq(6u64); @@ -4651,13 +3783,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4673,13 +3798,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4700,13 +3818,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); // And node 3 shouldn't match, because its p2=6 lives at t=3. let filter = NodeFilter @@ -4724,13 +3835,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4746,13 +3850,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4773,13 +3870,6 @@ mod test_node_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_snapshot, - &expected_results, - TestVariants::EventOnly, - ); // before(t+1) assert_filter_nodes_results( @@ -4789,13 +3879,6 @@ mod test_node_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_before, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -4816,13 +3899,6 @@ mod test_node_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_snapshot, - &expected_results, - TestVariants::PersistentOnly, - ); // at(t) assert_filter_nodes_results( @@ -4832,13 +3908,6 @@ mod test_node_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_at, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -4864,13 +3933,6 @@ mod test_node_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_snapshot_latest, - &expected_results, - TestVariants::EventOnly, - ); // no-op baseline assert_filter_nodes_results( @@ -4880,13 +3942,6 @@ mod test_node_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_noop, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -4908,13 +3963,6 @@ mod test_node_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_snapshot_latest, - &expected_results, - TestVariants::PersistentOnly, - ); // latest assert_filter_nodes_results( @@ -4924,13 +3972,6 @@ mod test_node_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_latest, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -4951,13 +3992,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4979,13 +4013,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -5007,13 +4034,6 @@ mod test_node_property_filter { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -5294,7 +4314,7 @@ mod composite_node_filter_tests { prelude::NodeFilter, }; use raphtory_tests::assertions::{ - assert_filter_neighbours_results, assert_filter_nodes_results, assert_search_nodes_results, + assert_filter_neighbours_results, assert_filter_nodes_results, TestVariants, }; @@ -5312,13 +4332,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -5364,13 +4377,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_node_filter().unwrap(); assert_filter_nodes_results( init_nodes_graph, @@ -5379,13 +4385,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p2") @@ -5399,13 +4398,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_node_filter().unwrap(); assert_filter_nodes_results( init_nodes_graph, @@ -5414,13 +4406,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p1").eq("pometry").or(NodeFilter .property("p2") @@ -5434,13 +4419,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_node_filter().unwrap(); assert_filter_nodes_results( init_nodes_graph, @@ -5449,13 +4427,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.node_type() .eq("fire_nation") @@ -5468,13 +4439,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_node_filter().unwrap(); assert_filter_nodes_results( init_nodes_graph, @@ -5483,13 +4447,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p9") @@ -5503,13 +4460,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_node_filter().unwrap(); assert_filter_nodes_results( init_nodes_graph, @@ -5518,13 +4468,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.node_type() .eq("fire_nation") @@ -5537,13 +4480,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_node_filter().unwrap(); assert_filter_nodes_results( init_nodes_graph, @@ -5552,13 +4488,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.name() .eq("2") @@ -5571,13 +4500,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_node_filter().unwrap(); assert_filter_nodes_results( init_nodes_graph, @@ -5586,13 +4508,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.name() .eq("2") @@ -5606,13 +4521,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_node_filter().unwrap(); assert_filter_nodes_results( init_nodes_graph, @@ -5621,13 +4529,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -5647,13 +4548,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NotFilter(NodeFilter.name().eq("2")) .and(NodeFilter.property("p2").eq(2u64)) @@ -5666,13 +4560,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -5767,7 +4654,7 @@ mod test_node_property_filter_agg { addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, }; use raphtory_tests::assertions::{ - assert_filter_nodes_err, assert_filter_nodes_results, assert_search_nodes_results, + assert_filter_nodes_err, assert_filter_nodes_results, TestVariants::All, }; @@ -6096,13 +4983,6 @@ mod test_node_property_filter_agg { All, ); - assert_search_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter, - &expected, - All, - ); } fn apply_assertion_err( @@ -6117,13 +4997,6 @@ mod test_node_property_filter_agg { All, ); - // assert_search_nodes_err( - // init_nodes_graph, - // IdentityGraphTransformer, - // filter, - // expected, - // All, - // ); } // ------ Property: SUM ---- @@ -9284,7 +8157,7 @@ mod test_edge_filter { ViewWrapOps, }; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_search_edges_results, assert_select_edges_results, + assert_filter_edges_results, assert_select_edges_results, TestGraphVariants, TestVariants, }; @@ -9300,13 +8173,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - g, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9325,13 +8191,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - g, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9346,13 +8205,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - g, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9366,13 +8218,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9392,13 +8237,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9412,13 +8250,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src().name().is_in(vec!["1", "2"]); let expected_results = vec!["1->2", "2->1", "2->3"]; @@ -9429,13 +8260,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9455,13 +8279,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9475,13 +8292,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9501,13 +8311,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9521,13 +8324,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst().name().is_in(vec!["2", "3"]); let expected_results = vec!["1->2", "2->3"]; @@ -9538,13 +8334,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9563,13 +8352,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9583,13 +8365,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src().name().starts_with("Joker"); let expected_results: Vec<&str> = vec![]; @@ -9600,13 +8375,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst().name().starts_with("Jimmy"); let expected_results: Vec<&str> = vec!["John Mayer->Jimmy Page"]; @@ -9617,13 +8385,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst().name().starts_with("Tango"); let expected_results: Vec<&str> = vec![]; @@ -9634,13 +8395,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9654,13 +8408,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src().name().ends_with("Cruise"); let expected_results: Vec<&str> = vec![]; @@ -9671,13 +8418,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst().name().ends_with("Page"); let expected_results: Vec<&str> = vec!["John Mayer->Jimmy Page"]; @@ -9688,13 +8428,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst().name().ends_with("Cruise"); let expected_results: Vec<&str> = vec![]; @@ -9705,13 +8438,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9725,13 +8451,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9746,13 +8465,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9799,13 +8511,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9819,13 +8524,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src().id().eq(3); let expected_results = vec!["3->1"]; @@ -9836,13 +8534,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9856,13 +8547,6 @@ mod test_edge_filter { // &expected_results, // TestVariants::All, // ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst().id().eq(3); let expected_results = vec!["2->3"]; @@ -9873,13 +8557,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9899,13 +8576,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src().id().ne(3); let expected_results = vec!["1->2", "2->1", "2->3"]; @@ -9916,13 +8586,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9942,13 +8605,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst().id().ne(3); let expected_results = vec!["1->2", "2->1", "3->1"]; @@ -9959,13 +8615,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -9979,13 +8628,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src().id().is_in(vec![3]); let expected_results = vec!["3->1"]; @@ -9996,13 +8638,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10016,13 +8651,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst().id().is_in(vec![3]); let expected_results = vec!["2->3"]; @@ -10033,13 +8661,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10059,13 +8680,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src().id().is_not_in(vec![3]); let expected_results = vec!["1->2", "2->1", "2->3"]; @@ -10076,13 +8690,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10102,13 +8709,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst().id().is_not_in(vec![3]); let expected_results = vec!["1->2", "2->1", "3->1"]; @@ -10119,13 +8719,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10139,13 +8732,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10159,13 +8745,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10179,13 +8758,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10199,13 +8771,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10219,13 +8784,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10239,13 +8797,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10259,13 +8810,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10279,13 +8823,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10299,13 +8836,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10319,13 +8849,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10371,13 +8894,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10391,13 +8907,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10416,13 +8925,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10436,13 +8938,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10460,13 +8955,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10480,13 +8968,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10500,13 +8981,6 @@ mod test_edge_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -10527,13 +9001,6 @@ mod test_edge_filter { &expected_results, TestGraphVariants::PersistentGraph, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); let filter = EdgeFilter.window(1, 4).is_valid(); let expected_results = vec!["Three->One", "Two->One", "Two->Three"]; @@ -10551,13 +9018,6 @@ mod test_edge_filter { &expected_results, TestGraphVariants::PersistentGraph, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); } #[test] @@ -10578,13 +9038,6 @@ mod test_edge_filter { &expected_results, TestGraphVariants::PersistentGraph, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); let filter = EdgeFilter.snapshot_at(3).is_valid(); let expected_results = vec!["Three->One", "Two->One", "Two->Three"]; @@ -10602,13 +9055,6 @@ mod test_edge_filter { &expected_results, TestGraphVariants::PersistentGraph, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); } #[test] @@ -10636,13 +9082,6 @@ mod test_edge_filter { &expected_results, TestGraphVariants::PersistentGraph, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); } // Disk graph doesn't support deletions @@ -10664,13 +9103,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } // Disk graph doesn't support deletions @@ -10692,13 +9124,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10719,13 +9144,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.window(1, 6).is_self_loop(); let expected_results = vec!["Bangalore->Bangalore"]; @@ -10743,13 +9161,6 @@ mod test_edge_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } } @@ -10764,7 +9175,7 @@ mod test_edge_property_filter { use raphtory_api::core::entities::properties::prop::Prop; use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_search_edges_results, TestGraphVariants, TestVariants, + assert_filter_edges_results, TestGraphVariants, TestVariants, }; #[test] @@ -10779,13 +9190,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p30").temporal().first().eq("Old_boat"); let expected_results = vec!["2->3"]; @@ -10796,13 +9200,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p20").temporal().eq("Gold_ship").all(); let expected_results = vec!["1->2"]; @@ -10813,13 +9210,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10840,13 +9230,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p30").temporal().first().ne("Old_boat"); let expected_results = vec!["1->2"]; @@ -10857,13 +9240,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p30").temporal().ne("Classic").all(); let expected_results = vec!["1->2", "2->3"]; @@ -10874,13 +9250,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10902,13 +9271,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p2").temporal().first().lt(5u64); let expected_results = vec!["1->2", "2->3"]; @@ -10919,13 +9281,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p2").temporal().lt(10u64).all(); let expected_results = vec![ @@ -10943,13 +9298,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10971,13 +9319,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p2").temporal().first().le(3u64); let expected_results = vec!["2->3"]; @@ -10988,13 +9329,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p2").temporal().le(5u64).all(); let expected_results = vec!["1->2", "2->3"]; @@ -11005,13 +9339,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11032,13 +9359,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p2").temporal().first().gt(5u64); let expected_results = vec![ @@ -11054,13 +9374,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p2").temporal().gt(5u64).all(); let expected_results = vec![ @@ -11076,13 +9389,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11104,13 +9410,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p2").temporal().first().ge(6u64); let expected_results = vec![ @@ -11126,13 +9425,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p2").temporal().ge(6u64).all(); let expected_results = vec![ @@ -11148,13 +9440,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11174,13 +9459,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p2") @@ -11199,13 +9477,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p2") @@ -11225,13 +9496,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p2") @@ -11251,13 +9515,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11272,13 +9529,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p2") @@ -11293,13 +9543,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p2") @@ -11314,13 +9557,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11342,13 +9578,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p2").temporal().first().is_some(); let expected_results = vec![ @@ -11366,13 +9595,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11387,13 +9609,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("p2").temporal().first().is_none(); let expected_results = vec![]; @@ -11404,13 +9619,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11425,13 +9633,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11446,13 +9647,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11467,13 +9661,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11488,13 +9675,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p30") @@ -11509,13 +9689,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p20") @@ -11530,13 +9703,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11551,13 +9717,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11572,13 +9731,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11593,13 +9745,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11614,13 +9759,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p20") @@ -11635,13 +9773,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p20") @@ -11656,13 +9787,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11677,13 +9801,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11698,13 +9815,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11719,13 +9829,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p20") @@ -11740,13 +9843,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p20").temporal().contains("ship").all(); let expected_results: Vec<&str> = vec!["1->2"]; @@ -11757,13 +9853,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11778,13 +9867,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11799,13 +9881,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p10") @@ -11820,13 +9895,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p20") @@ -11841,13 +9909,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p30") @@ -11855,19 +9916,12 @@ mod test_edge_property_filter { .not_contains("ship") .all(); let expected_results: Vec<&str> = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); - assert_search_edges_results( + assert_filter_edges_results( init_edges_graph, IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::All, + TestVariants::EventOnly, ); } @@ -11918,13 +9972,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -11944,13 +9991,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .window(1, 5) @@ -11974,13 +10014,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -11999,13 +10032,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter1.clone(), - &expected_results, - TestVariants::All, - ); let expected_results = vec![]; assert_filter_edges_results( @@ -12015,13 +10041,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter2.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter2 = EdgeFilter .window(100, 200) @@ -12035,13 +10054,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter2.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -12061,13 +10073,6 @@ mod test_edge_property_filter { &expected_any, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_any.clone(), - &expected_any, - TestVariants::All, - ); let filter_all = EdgeFilter .window(2, 4) @@ -12084,13 +10089,6 @@ mod test_edge_property_filter { &expected_all, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_all.clone(), - &expected_all, - TestVariants::All, - ); } #[test] @@ -12119,13 +10117,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -12145,13 +10136,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -12167,13 +10151,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); // Only time=3 contributes; edge 3->1 has p2=6 at t=3 let filter = EdgeFilter.at(3).property("p2").temporal().sum().eq(6u64); @@ -12186,13 +10163,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -12215,13 +10185,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -12243,13 +10206,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); // And p2=6 edges shouldn't match, because their p2=6 lives at t=3+. let filter = EdgeFilter @@ -12267,13 +10223,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -12290,13 +10239,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -12327,13 +10269,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_snapshot, - &expected_results, - TestVariants::EventOnly, - ); // before(t+1) assert_filter_edges_results( @@ -12343,13 +10278,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_before, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -12375,13 +10303,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_snapshot, - &expected_results, - TestVariants::PersistentOnly, - ); // at(t) assert_filter_edges_results( @@ -12391,13 +10312,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_at, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -12427,13 +10341,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_snapshot_latest, - &expected_results, - TestVariants::EventOnly, - ); // no-op baseline assert_filter_edges_results( @@ -12443,13 +10350,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_noop, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -12476,13 +10376,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_snapshot_latest, - &expected_results, - TestVariants::PersistentOnly, - ); // latest assert_filter_edges_results( @@ -12492,13 +10385,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_latest, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -12519,13 +10405,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -12546,13 +10425,6 @@ mod test_edge_property_filter { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -12606,7 +10478,7 @@ mod composite_edge_filter_tests { PropertyFilterFactory, TryAsCompositeFilter, }; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_search_edges_results, TestGraphVariants, TestVariants, + assert_filter_edges_results, TestGraphVariants, TestVariants, }; use crate::{init_edges_graph, IdentityGraphTransformer}; @@ -12626,13 +10498,6 @@ mod composite_edge_filter_tests { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -12695,13 +10560,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_edge_filter().unwrap(); assert_filter_edges_results( init_edges_graph, @@ -12710,13 +10568,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p2") @@ -12730,13 +10581,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_edge_filter().unwrap(); assert_filter_edges_results( init_edges_graph, @@ -12745,13 +10589,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter.property("p1").eq("pometry").or(EdgeFilter .property("p2") @@ -12770,13 +10607,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_edge_filter().unwrap(); assert_filter_edges_results( init_edges_graph, @@ -12785,13 +10615,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src() .name() @@ -12805,13 +10628,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_edge_filter().unwrap(); assert_filter_edges_results( init_edges_graph, @@ -12820,13 +10636,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p2") @@ -12840,13 +10649,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_edge_filter().unwrap(); assert_filter_edges_results( init_edges_graph, @@ -12855,13 +10657,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src() .name() @@ -12875,13 +10670,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_edge_filter().unwrap(); assert_filter_edges_results( init_edges_graph, @@ -12890,13 +10678,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::dst() .name() @@ -12910,13 +10691,6 @@ mod composite_edge_filter_tests { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_edge_filter().unwrap(); assert_filter_edges_results( init_edges_graph, @@ -12925,13 +10699,6 @@ mod composite_edge_filter_tests { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter::src() .name() @@ -12946,13 +10713,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = filter.try_as_composite_edge_filter().unwrap(); assert_filter_edges_results( @@ -12962,13 +10722,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -12996,13 +10749,6 @@ mod composite_edge_filter_tests { TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = NotFilter( EdgeFilter::src() @@ -13026,12 +10772,5 @@ mod composite_edge_filter_tests { TestVariants::EventOnly, ); - assert_search_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); } } diff --git a/raphtory-tests/tests/test_layers.rs b/raphtory-tests/tests/test_layers.rs index 78b07db14b..1b57c08c9b 100644 --- a/raphtory-tests/tests/test_layers.rs +++ b/raphtory-tests/tests/test_layers.rs @@ -254,7 +254,7 @@ pub mod test_filters_layer_graph { }; use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_search_nodes_results, TestGraphVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; fn init_graph(graph: G) -> G { @@ -321,13 +321,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); let layers: Vec = vec!["layer1".into()]; let filter = NodeFilter.property("p1").ge(2u64); @@ -339,13 +332,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); let layers: Vec = vec!["layer2".into()]; let filter = NodeFilter.property("p1").le(1u64); @@ -357,13 +343,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); let layers: Vec = vec!["layer1".into()]; let filter = NodeFilter.property("p1").lt(2u64); @@ -375,13 +354,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); let layers: Vec = vec!["layer2".into()]; let filter = NodeFilter.property("p1").gt(1u64); @@ -393,13 +365,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -415,13 +380,6 @@ pub mod test_filters_layer_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - LayeredGraphWindowTransformer(layers.clone(), 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let layers: Vec = vec!["layer1".into()]; let filter = NodeFilter.property("p1").ge(2u64); @@ -433,13 +391,6 @@ pub mod test_filters_layer_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - LayeredGraphWindowTransformer(layers.clone(), 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let layers: Vec = vec!["layer2".into()]; let filter = NodeFilter.property("p1").lt(2u64); @@ -451,13 +402,6 @@ pub mod test_filters_layer_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - LayeredGraphWindowTransformer(layers.clone(), 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -472,13 +416,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - LayeredGraphWindowTransformer(layers.clone(), 6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let layers: Vec = vec!["layer1".into()]; let filter = NodeFilter.property("p1").lt(2u64); @@ -490,13 +427,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - LayeredGraphWindowTransformer(layers.clone(), 6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let layers: Vec = vec!["layer2".into()]; let filter = NodeFilter.property("p1").gt(1u64); @@ -508,13 +438,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - LayeredGraphWindowTransformer(layers.clone(), 6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } } @@ -530,7 +453,7 @@ pub mod test_filters_layer_graph { }; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_search_edges_results, TestGraphVariants, + assert_filter_edges_results, TestGraphVariants, TestVariants, }; @@ -578,13 +501,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); let layers: Vec = vec!["layer1".into()]; let filter = EdgeFilter.property("p1").le(1u64); @@ -598,13 +514,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); let layers: Vec = vec!["layer2".into()]; let filter = EdgeFilter.property("p1").ge(2u64); @@ -616,13 +525,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); let layers: Vec = vec!["layer1".into()]; let filter = EdgeFilter.property("p1").lt(2u64); @@ -636,13 +538,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); let layers: Vec = vec!["layer2".into()]; let filter = EdgeFilter.property("p1").gt(1u64); @@ -654,13 +549,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredGraphTransformer(layers), - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -679,13 +567,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredGraphWindowTransformer(layers, 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); // Edge Property Semantics: // When filtering by specific layer, filter criteria (p1==1) and latest semantics is applicable @@ -700,13 +581,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredGraphWindowTransformer(layers, 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let layers: Vec = vec!["layer2".into()]; let filter = EdgeFilter.property("p1").gt(1u64); @@ -718,13 +592,6 @@ pub mod test_filters_layer_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredGraphWindowTransformer(layers, 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -751,13 +618,6 @@ pub mod test_filters_layer_graph { &expected_results, vec![TestGraphVariants::PersistentGraph], ); - assert_search_edges_results( - init_graph, - LayeredGraphWindowTransformer(layers, 6..9), - filter, - &expected_results, - vec![TestGraphVariants::PersistentGraph], - ); let layers: Vec = vec!["layer1".into()]; let filter = EdgeFilter.property("p1").le(1u64); @@ -769,13 +629,6 @@ pub mod test_filters_layer_graph { &expected_results, vec![TestGraphVariants::PersistentGraph], ); - assert_search_edges_results( - init_graph, - LayeredGraphWindowTransformer(layers, 6..9), - filter, - &expected_results, - vec![TestGraphVariants::PersistentGraph], - ); let layers: Vec = vec!["layer2".into()]; let filter = EdgeFilter.property("p1").ge(2u64); @@ -787,13 +640,6 @@ pub mod test_filters_layer_graph { &expected_results, vec![TestGraphVariants::PersistentGraph], ); - assert_search_edges_results( - init_graph, - LayeredGraphWindowTransformer(layers, 6..9), - filter, - &expected_results, - vec![TestGraphVariants::PersistentGraph], - ); } } } diff --git a/raphtory-tests/tests/tests_node_type_filtered_subgraph.rs b/raphtory-tests/tests/tests_node_type_filtered_subgraph.rs index fb8190bb59..5658892d6f 100644 --- a/raphtory-tests/tests/tests_node_type_filtered_subgraph.rs +++ b/raphtory-tests/tests/tests_node_type_filtered_subgraph.rs @@ -254,7 +254,7 @@ mod test_filters_node_type_filtered_subgraph { db::graph::views::filter::model::PropertyFilterFactory, prelude::NodeFilter, }; use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_search_nodes_results, TestGraphVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; #[test] @@ -268,13 +268,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - NodeTypeGraphTransformer(None), - filter, - &expected_results, - TestVariants::All, - ); let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); @@ -287,13 +280,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::All, ); - assert_search_nodes_results( - init_graph, - NodeTypeGraphTransformer(node_types), - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -308,13 +294,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowedNodeTypeGraphTransformer(None, 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); @@ -327,13 +306,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowedNodeTypeGraphTransformer(node_types, 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -347,13 +319,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowedNodeTypeGraphTransformer(None, 6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); @@ -366,13 +331,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowedNodeTypeGraphTransformer(node_types, 6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } } @@ -499,7 +457,7 @@ mod test_filters_node_type_filtered_subgraph { }; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_search_edges_results, TestVariants, + assert_filter_edges_results, TestVariants, }; #[test] fn test_edges_filters() { @@ -512,13 +470,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - NodeTypeGraphTransformer(None), - filter, - &expected_results, - TestVariants::All, - ); let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); @@ -531,13 +482,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - NodeTypeGraphTransformer(node_types.clone()), - filter.clone(), - &expected_results, - TestVariants::All, - ); let layers = vec!["fire_nation".to_string()]; let expected_results = vec!["N3->N4"]; @@ -548,13 +492,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredNodeTypeGraphTransformer(node_types.clone(), layers), - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -568,13 +505,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowedNodeTypeGraphTransformer(None, 6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); @@ -587,13 +517,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowedNodeTypeGraphTransformer(node_types.clone(), 6..9), - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let layers = vec!["fire_nation".to_string()]; let expected_results = vec!["N3->N4"]; @@ -604,13 +527,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - LayeredWindowedNodeTypeGraphTransformer(node_types.clone(), 6..9, layers), - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -624,13 +540,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowedNodeTypeGraphTransformer(None, 6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); @@ -643,13 +552,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowedNodeTypeGraphTransformer(node_types.clone(), 6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let layers = vec!["fire_nation".to_string()]; let expected_results = vec!["N3->N4"]; @@ -660,13 +562,6 @@ mod test_filters_node_type_filtered_subgraph { &expected_results, vec![], ); - assert_search_edges_results( - init_graph, - LayeredWindowedNodeTypeGraphTransformer(node_types.clone(), 6..9, layers), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); } } } diff --git a/raphtory-tests/tests/views_test.rs b/raphtory-tests/tests/views_test.rs index 6dc1c6bec7..d12f477990 100644 --- a/raphtory-tests/tests/views_test.rs +++ b/raphtory-tests/tests/views_test.rs @@ -543,7 +543,7 @@ mod test_filters_window_graph { addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, }; use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_search_nodes_results, TestGraphVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; @@ -775,13 +775,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -795,13 +788,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -816,13 +802,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -838,13 +817,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -859,13 +831,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = NodeFilter.name().is_in(vec!["N2", "N5"]); let expected_results = vec!["N2", "N5"]; @@ -876,13 +841,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -896,13 +854,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.name().is_in(vec!["N2", "N5"]); let expected_results = vec!["N2", "N5"]; @@ -913,13 +864,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -934,13 +878,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -956,13 +893,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -977,13 +907,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -997,13 +920,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -1018,13 +934,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -1040,13 +949,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -1061,13 +963,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.node_type().is_in(vec!["fire_nation", "air_nomad"]); let expected_results = vec!["N1", "N3", "N5", "N6"]; @@ -1078,13 +973,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -1098,13 +986,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.node_type().is_in(vec!["fire_nation", "air_nomad"]); let expected_results = vec!["N1", "N3", "N5", "N6", "N7", "N8"]; @@ -1115,13 +996,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -1136,13 +1010,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -1158,13 +1025,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -1179,13 +1039,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k1").eq(2i64); let expected_results = vec!["N2"]; @@ -1196,13 +1049,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k2").eq("Paper_Airplane"); let expected_results = vec!["N1"]; @@ -1213,13 +1059,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = NodeFilter.property("k3").eq(true); let expected_results = vec!["N2"]; @@ -1230,13 +1069,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = NodeFilter.property("k4").eq(6.0f64); let expected_results = vec!["N1"]; @@ -1247,13 +1079,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = NodeFilter.property("x").eq(Prop::list(vec![ Prop::U64(1), @@ -1276,14 +1101,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - // TODO: Search APIs don't support list yet - // assert_search_nodes_results( - // init_graph, - // WindowGraphTransformer(6..9), - // filter, - // &expected_results, - // TestVariants::EventOnly, - // ); } #[test] @@ -1297,13 +1114,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k1").eq(2i64); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; @@ -1314,13 +1124,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k2").eq("Paper_Airplane"); let expected_results = vec!["N1"]; @@ -1331,13 +1134,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); // TODO: Const properties not supported for disk_graph. let filter = NodeFilter.property("k3").eq(true); @@ -1349,13 +1145,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k4").eq(6.0f64); let expected_results = vec!["N1"]; @@ -1366,13 +1155,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("x").eq(Prop::list(vec![ Prop::U64(1), @@ -1395,14 +1177,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::PersistentGraph], ); - // TODO: Search APIs don't support list yet - // assert_search_nodes_results( - // init_graph, - // WindowGraphTransformer(6..9), - // filter, - // &expected_results, - // vec![TestGraphVariants::PersistentGraph], - // ); } #[test] @@ -1417,13 +1191,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k1").ne(2i64); let expected_results = vec!["N1"]; @@ -1434,13 +1201,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k2").ne("Paper_Airplane"); let expected_results = vec!["N2", "N5"]; @@ -1451,13 +1211,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k3").ne(true); let expected_results = vec!["N1"]; @@ -1468,13 +1221,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k4").ne(6.0f64); let expected_results = vec!["N2", "N5", "N6"]; @@ -1485,13 +1231,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -1505,13 +1244,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k1").ne(2i64); let expected_results = vec!["N1"]; @@ -1522,13 +1254,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k2").ne("Paper_Airplane"); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; @@ -1539,13 +1264,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k3").ne(true); let expected_results = vec!["N1"]; @@ -1556,13 +1274,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k4").ne(6.0f64); let expected_results = vec!["N12", "N2", "N5", "N6", "N7", "N8"]; @@ -1573,13 +1284,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -1594,13 +1298,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k1").lt(3i64); let expected_results = vec!["N2"]; @@ -1611,13 +1308,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k4").lt(10.0f64); let expected_results = vec!["N1", "N5", "N6"]; @@ -1628,13 +1318,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -1648,13 +1331,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k1").lt(3i64); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; @@ -1665,13 +1341,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k4").lt(10.0f64); let expected_results = vec!["N1", "N5", "N6"]; @@ -1682,13 +1351,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -1703,13 +1365,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k1").le(2i64); let expected_results = vec!["N2"]; @@ -1720,13 +1375,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k4").le(6.0f64); let expected_results = vec!["N1", "N5", "N6"]; @@ -1737,13 +1385,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -1757,13 +1398,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k1").le(2i64); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; @@ -1774,13 +1408,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k4").le(6.0f64); let expected_results = vec!["N1", "N5", "N6"]; @@ -1791,13 +1418,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -1812,13 +1432,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k1").gt(2i64); let expected_results = vec!["N1"]; @@ -1829,13 +1442,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k4").gt(6.0f64); let expected_results = vec!["N2"]; @@ -1846,13 +1452,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("x").gt(Prop::List( vec![Prop::U64(1), Prop::U64(6), Prop::U64(9)].into(), @@ -1879,13 +1478,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k1").gt(2i64); let expected_results = vec!["N1"]; @@ -1896,13 +1488,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k4").gt(6.0f64); let expected_results = vec!["N12", "N2", "N7", "N8"]; @@ -1913,13 +1498,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -1934,13 +1512,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k1").ge(2i64); let expected_results = vec!["N1", "N2"]; @@ -1951,13 +1522,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k4").ge(6.0f64); let expected_results = vec!["N1", "N2"]; @@ -1968,13 +1532,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -1990,13 +1547,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k1").ge(2i64); let expected_results = vec!["N1", "N12", "N2", "N5", "N7", "N8"]; @@ -2007,13 +1557,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k4").ge(6.0f64); let expected_results = vec!["N1", "N12", "N2", "N7", "N8"]; @@ -2024,13 +1567,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -2045,13 +1581,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k1").is_in(vec![2i64.into()]); let expected_results = vec!["N2"]; @@ -2062,13 +1591,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter .property("k2") @@ -2081,13 +1603,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k3").is_in(vec![true.into()]); let expected_results = vec!["N2"]; @@ -2098,13 +1613,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k4").is_in(vec![6.0f64.into()]); let expected_results = vec!["N1"]; @@ -2115,13 +1623,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -2135,13 +1636,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k1").is_in(vec![2i64.into()]); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; @@ -2152,13 +1646,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter .property("k2") @@ -2171,13 +1658,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); // TODO: Const properties not supported for disk_graph. let filter = NodeFilter.property("k3").is_in(vec![true.into()]); @@ -2189,13 +1669,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k4").is_in(vec![6.0f64.into()]); let expected_results = vec!["N1"]; @@ -2206,13 +1679,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -2227,13 +1693,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k1").is_not_in(vec![2i64.into()]); let expected_results = vec!["N1"]; @@ -2244,13 +1703,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter .property("k2") @@ -2263,13 +1715,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k3").is_not_in(vec![true.into()]); let expected_results = vec!["N1"]; @@ -2280,13 +1725,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k4").is_not_in(vec![6.0f64.into()]); let expected_results = vec!["N2", "N5", "N6"]; @@ -2297,13 +1735,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -2317,13 +1748,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k1").is_not_in(vec![2i64.into()]); let expected_results = vec!["N1"]; @@ -2334,13 +1758,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter .property("k2") @@ -2353,13 +1770,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k3").is_not_in(vec![true.into()]); let expected_results = vec!["N1"]; @@ -2370,13 +1780,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k4").is_not_in(vec![6.0f64.into()]); let expected_results = vec!["N12", "N2", "N5", "N6", "N7", "N8"]; @@ -2387,13 +1790,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -2408,13 +1804,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - vec![TestGraphVariants::Graph], - ); let expected_results = Vec::<&str>::new(); assert_filter_nodes_results( @@ -2424,13 +1813,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(1..2), - filter.clone(), - &expected_results, - vec![TestGraphVariants::Graph], - ); assert_filter_nodes_results( init_graph, @@ -2439,13 +1821,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(10..12), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -2461,13 +1836,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let expected_results = Vec::<&str>::new(); assert_filter_nodes_results( @@ -2477,13 +1845,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(1..2), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N2", "N3", "N4", "N5", "N6", "N7", "N8", "N9", @@ -2495,13 +1856,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(10..12), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -2518,13 +1872,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(1..4), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -2541,13 +1888,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -2564,13 +1904,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -2586,13 +1919,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -2607,13 +1933,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = NodeFilter.property("k2").fuzzy_search("Pa", 2, false); let expected_results = Vec::<&str>::new(); @@ -2624,13 +1943,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] @@ -2644,13 +1956,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = NodeFilter.property("k2").fuzzy_search("Pa", 2, false); let expected_results = Vec::<&str>::new(); @@ -2661,13 +1966,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_nodes_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } } @@ -2686,7 +1984,7 @@ mod test_filters_window_graph { }; use raphtory_api::core::{entities::properties::prop::Prop, storage::arc_str::ArcStr}; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_search_edges_results, TestGraphVariants, + assert_filter_edges_results, TestGraphVariants, TestVariants, WindowGraphTransformer, }; @@ -2944,13 +2242,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -2965,13 +2256,6 @@ mod test_filters_window_graph { &expected_results, vec![], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -2985,13 +2269,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -3009,13 +2286,6 @@ mod test_filters_window_graph { &expected_results, vec![], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - vec![TestGraphVariants::PersistentGraph], - ); } #[test] @@ -3029,13 +2299,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter::dst().name().is_in(vec!["N2", "N5"]); let expected_results = vec!["N1->N2"]; @@ -3046,13 +2309,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -3067,13 +2323,6 @@ mod test_filters_window_graph { &expected_results, vec![], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter::dst().name().is_in(vec!["N2", "N5"]); let expected_results = vec!["N1->N2"]; @@ -3084,13 +2333,6 @@ mod test_filters_window_graph { &expected_results, vec![], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -3104,13 +2346,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -3128,13 +2363,6 @@ mod test_filters_window_graph { &expected_results, vec![], ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -3148,13 +2376,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k1").eq(2i64); let expected_results = vec!["N2->N3"]; @@ -3165,13 +2386,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k2").eq("Paper_Airplane"); let expected_results = vec!["N1->N2"]; @@ -3182,13 +2396,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k3").eq(true); let expected_results = vec!["N2->N3"]; @@ -3199,13 +2406,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k4").eq(6.0f64); let expected_results = vec!["N1->N2"]; @@ -3216,13 +2416,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("x").eq(Prop::list(vec![ Prop::U64(1), @@ -3245,14 +2438,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::Graph], ); - // TODO: Search APIs don't support list yet - // assert_search_edges_results( - // init_graph2, - // WindowGraphTransformer(6..9), - // filter, - // &expected_results, - // TestVariants::PersistentOnly, - // ); } #[test] @@ -3268,13 +2453,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k1").eq(2i64); @@ -3286,13 +2464,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k2").eq("Paper_Airplane"); let expected_results = vec!["N1->N2"]; @@ -3303,13 +2474,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k3").eq(true); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; @@ -3320,13 +2484,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k4").eq(6.0f64); let expected_results = vec!["N1->N2"]; @@ -3337,13 +2494,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("x").eq(Prop::list(vec![ Prop::U64(1), @@ -3366,14 +2516,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::PersistentGraph], ); - // TODO: Search APIs don't support list yet - // assert_search_edges_results( - // init_graph2, - // WindowGraphTransformer(1..9), - // filter.clone(), - // &expected_results, - // vec![TestGraphVariants::PersistentGraph], - // ); } #[test] @@ -3387,13 +2529,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k1").ne(2i64); let expected_results = vec!["N1->N2"]; @@ -3404,13 +2539,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k2").ne("Paper_Airplane"); let expected_results = vec!["N2->N3", "N5->N6"]; @@ -3421,13 +2549,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k3").ne(true); let expected_results = vec!["N1->N2"]; @@ -3438,13 +2559,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k4").ne(6.0f64); let expected_results = vec!["N2->N3", "N5->N6", "N6->N7"]; @@ -3455,13 +2569,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -3479,13 +2586,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k1").ne(2i64); let expected_results = vec!["N1->N2"]; @@ -3496,13 +2596,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k2").ne("Paper_Airplane"); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; @@ -3513,13 +2606,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k3").ne(true); let expected_results = vec!["N1->N2"]; @@ -3530,13 +2616,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k4").ne(6.0f64); let expected_results = @@ -3548,13 +2627,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("x").ne(Prop::list(vec![ Prop::U64(1), @@ -3569,14 +2641,6 @@ mod test_filters_window_graph { &expected_results, vec![TestGraphVariants::PersistentGraph], ); - // TODO: Search APIs don't support list yet - // assert_search_edges_results( - // init_graph2, - // WindowGraphTransformer(1..9), - // filter.clone(), - // &expected_results, - // TestVariants::PersistentOnly, - // ); } #[test] @@ -3590,13 +2654,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k1").lt(3i64); let expected_results = vec!["N2->N3"]; @@ -3607,13 +2664,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k4").lt(10.0f64); let expected_results = vec!["N1->N2", "N5->N6", "N6->N7"]; @@ -3624,13 +2674,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -3647,13 +2690,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k1").lt(3i64); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; @@ -3664,13 +2700,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k4").lt(10.0f64); let expected_results = vec!["N1->N2", "N5->N6", "N6->N7"]; @@ -3681,13 +2710,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -3701,13 +2723,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k1").le(2i64); let expected_results = vec!["N2->N3"]; @@ -3718,13 +2733,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k4").le(6.0f64); let expected_results = vec!["N1->N2", "N5->N6", "N6->N7"]; @@ -3735,13 +2743,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -3756,13 +2757,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k1").le(2i64); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; @@ -3773,13 +2767,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k4").le(6.0f64); let expected_results = vec!["N1->N2", "N5->N6", "N6->N7"]; @@ -3790,13 +2777,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -3810,13 +2790,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k1").gt(2i64); let expected_results = vec!["N1->N2"]; @@ -3827,13 +2800,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k4").gt(6.0f64); let expected_results = vec!["N2->N3"]; @@ -3844,13 +2810,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("x").gt(Prop::List( vec![Prop::U64(1), Prop::U64(6), Prop::U64(9)].into(), @@ -3881,13 +2840,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k1").gt(2i64); let expected_results = vec!["N1->N2"]; @@ -3898,13 +2850,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k4").gt(6.0f64); let expected_results = vec!["N12->N13", "N2->N3", "N7->N8", "N8->N9"]; @@ -3915,13 +2860,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -3935,13 +2873,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k1").ge(2i64); let expected_results = vec!["N1->N2", "N2->N3"]; @@ -3952,13 +2883,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k4").ge(6.0f64); let expected_results = vec!["N1->N2", "N2->N3"]; @@ -3969,13 +2893,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -3993,13 +2910,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k1").ge(2i64); let expected_results = @@ -4011,13 +2921,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k4").ge(6.0f64); let expected_results = vec!["N1->N2", "N12->N13", "N2->N3", "N7->N8", "N8->N9"]; @@ -4028,13 +2931,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -4048,13 +2944,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k1").is_in(vec![2i64.into()]); let expected_results = vec!["N2->N3"]; @@ -4065,13 +2954,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter .property("k2") @@ -4084,13 +2966,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k3").is_in(vec![true.into()]); let expected_results = vec!["N2->N3"]; @@ -4101,13 +2976,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k4").is_in(vec![6.0f64.into()]); let expected_results = vec!["N1->N2"]; @@ -4118,13 +2986,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -4139,13 +3000,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k1").is_in(vec![2i64.into()]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; @@ -4156,13 +3010,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter .property("k2") @@ -4175,13 +3022,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k3").is_in(vec![true.into()]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; @@ -4192,13 +3032,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k4").is_in(vec![6.0f64.into()]); let expected_results = vec!["N1->N2"]; @@ -4209,13 +3042,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -4229,13 +3055,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k1").is_not_in(vec![2i64.into()]); let expected_results = vec!["N1->N2"]; @@ -4246,13 +3065,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter .property("k2") @@ -4265,13 +3077,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k3").is_not_in(vec![true.into()]); let expected_results = vec!["N1->N2"]; @@ -4282,13 +3087,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k4").is_not_in(vec![6.0f64.into()]); let expected_results = vec!["N2->N3", "N5->N6", "N6->N7"]; @@ -4299,13 +3097,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -4323,13 +3114,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k1").is_not_in(vec![2i64.into()]); let expected_results = vec!["N1->N2"]; @@ -4340,13 +3124,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter .property("k2") @@ -4359,13 +3136,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k3").is_not_in(vec![true.into()]); let expected_results = vec!["N1->N2"]; @@ -4376,13 +3146,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k4").is_not_in(vec![6.0f64.into()]); let expected_results = @@ -4394,13 +3157,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -4414,13 +3170,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -4438,13 +3187,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -4461,13 +3203,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::All, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::All, - ); } #[test] @@ -4483,13 +3218,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -4506,13 +3234,6 @@ mod test_filters_window_graph { TestVariants::PersistentOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } #[test] @@ -4526,13 +3247,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter.property("k2").fuzzy_search("Pa", 2, true); let expected_results = vec!["N1->N2", "N2->N3"]; @@ -4543,13 +3257,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::EventOnly, ); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::EventOnly, - ); } #[test] @@ -4560,23 +3267,9 @@ mod test_filters_window_graph { let expected_results = vec![ "N1->N2", "N12->N13", "N13->N14", "N2->N3", "N5->N6", "N7->N8", "N8->N9", ]; - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); let filter = EdgeFilter.property("k2").fuzzy_search("Pa", 2, false); let expected_results = Vec::<&str>::new(); - assert_search_edges_results( - init_graph, - WindowGraphTransformer(6..9), - filter, - &expected_results, - TestVariants::PersistentOnly, - ); } } } From 67ec8a662bbcbe8613fbc2367a66a3dd4d3c4f8c Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:21:16 +0100 Subject: [PATCH 062/155] rid search tests py --- .../test_graphdb/test_graphdb.py | 27 - .../test_graphql/misc/test_index_off.py | 29 - .../test_graphql/test_gql_graph_surface.py | 3 - .../tests/test_search/test_gql_index_spec.py | 169 -- python/tests/test_search/test_index.py | 1532 ----------------- python/tests/test_search/test_index_spec.py | 154 -- 6 files changed, 1914 deletions(-) delete mode 100644 python/tests/test_base_install/test_graphql/misc/test_index_off.py delete mode 100644 python/tests/test_search/test_gql_index_spec.py delete mode 100644 python/tests/test_search/test_index.py delete mode 100644 python/tests/test_search/test_index_spec.py diff --git a/python/tests/test_base_install/test_graphdb/test_graphdb.py b/python/tests/test_base_install/test_graphdb/test_graphdb.py index fdd3a5ec12..59d438a43a 100644 --- a/python/tests/test_base_install/test_graphdb/test_graphdb.py +++ b/python/tests/test_base_install/test_graphdb/test_graphdb.py @@ -3015,30 +3015,3 @@ def datadir(tmpdir, request): raise e return tmpdir - -# def currently_broken_fuzzy_search(): #TODO: Fix fuzzy searching for properties -# g = Graph() -# g.add_edge(2,"haaroon","hamza", properties={"value":60,"value_f":31.3,"value_str":"abc123"}) -# g.add_edge(1,"ben","hamza", properties={"value":59,"value_f":11.4,"value_str":"test test test"}) -# g.add_edge(3,"ben","haaroon", properties={"value":199,"value_f":52.6,"value_str":"I gitgit awanna rock right now"}) -# g.add_edge(4,"hamza","naomi", properties={"value_str":"I wanna rock right now"}) -# assert len(index.fuzzy_search_edges("value_str:\"I wanna nock right now\"",levenshtein_distance=2)) == 2 - - -# def test_search_with_layers(): #TODO: Fix layer seearching -# g = Graph() -# g.add_edge(3,"haaroon","hamza",properties={"value":70,"value_f":11.3,"value_str":"abdsda2c123"},layer="1") -# g.add_edge(4,"ben","naomi",properties={"value":100,"value_f":22.3,"value_str":"ddddd"},layer="2") -# g.add_edge(5,"ben","naomi",properties={"value":100,"value_f":22.3,"value_str":"ddddd"},layer="3") -# index = g.index() - -# need to expose actual layer searching -# assert len(index.search_edges("layer:1")) == 1 - -# assert len(index.search_edges("value_str:ddddd")) == 1 -# assert len(index.search_edges("value:>60")) == 2 - -# l_g = g.layer(["1","3"]) -# l_index = l_g.index() -# assert len(index.search_edges("value_str:ddddd")) == 1 -# assert len(index.search_edges("value:>60")) == 2 diff --git a/python/tests/test_base_install/test_graphql/misc/test_index_off.py b/python/tests/test_base_install/test_graphql/misc/test_index_off.py deleted file mode 100644 index 4fc4848995..0000000000 --- a/python/tests/test_base_install/test_graphql/misc/test_index_off.py +++ /dev/null @@ -1,29 +0,0 @@ -import tempfile -import pytest -from raphtory import Graph -from raphtory.graphql import RaphtoryClient -from raphtory.graphql import GraphServer - - -@pytest.mark.skip(reason="Ignoring this test temporarily") -def test_latest_and_active(): - query = """ - { - graph(path: "graph") { - searchNodes(query: "a", limit: 1, offset: 0) { - name - } - } - } - """ - work_dir = tempfile.mkdtemp() - g = Graph() - g.save_to_file(work_dir + "/graph") - with GraphServer(work_dir).turn_off_index().start() as server: - client = server.get_client() - with pytest.raises(Exception) as excinfo: - client.query(query) - assert ( - "An operation tried to make use of the graph index but indexing has been turned off for the server" - in str(excinfo.value) - ) diff --git a/python/tests/test_base_install/test_graphql/test_gql_graph_surface.py b/python/tests/test_base_install/test_graphql/test_gql_graph_surface.py index 3b9815cbc2..eb1c7cbcb3 100644 --- a/python/tests/test_base_install/test_graphql/test_gql_graph_surface.py +++ b/python/tests/test_base_install/test_graphql/test_gql_graph_surface.py @@ -8,9 +8,6 @@ Each field is tested under a combination of base / window / layer views to exercise the composition plumbing, not just the field itself. - -`searchEdges` is left untested here — it's marked experimental in the schema -and requires an index-creation path that these fixtures don't exercise. """ from utils import run_group_graphql_test diff --git a/python/tests/test_search/test_gql_index_spec.py b/python/tests/test_search/test_gql_index_spec.py deleted file mode 100644 index 65811627b9..0000000000 --- a/python/tests/test_search/test_gql_index_spec.py +++ /dev/null @@ -1,169 +0,0 @@ -import pytest -from raphtory import Graph, PersistentGraph -from filters_setup import init_nodes_graph, init_edges_graph, create_test_graph -from utils import run_group_graphql_test -from raphtory.graphql import ( - GraphServer, - RaphtoryClient, - RemoteIndexSpec, - SomePropertySpec, - AllPropertySpec, - PropsInput, -) -import tempfile - -EVENT_GRAPH = init_nodes_graph(Graph()) -PERSISTENT_GRAPH = init_nodes_graph(PersistentGraph()) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_create_index_with_custom_spec(graph): - queries = [ - ( - """ - mutation { - createIndex( - path: "g" - indexSpec: { - nodeProps: { all: ALL } - edgeProps: { all: ALL_METADATA } - } - inRam: true - ) - } - """, - {"createIndex": True}, - ), - ( - """ - query { - graph(path: "g") { - getIndexSpec { - nodeMetadata - nodeProperties - edgeMetadata - edgeProperties - } - } - } - """, - { - "graph": { - "getIndexSpec": { - "nodeMetadata": ["p1"], - "nodeProperties": ["p1", "q1"], - "edgeMetadata": ["p1"], - "edgeProperties": [], - } - } - }, - ), - ] - - run_group_graphql_test(queries, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_create_index_with_default_spec(graph): - queries = [ - ( - """ - mutation { - createIndex( - path: "g" - inRam: true - ) - } - """, - {"createIndex": True}, - ), - ( - """ - query { - graph(path: "g") { - getIndexSpec { - nodeMetadata - nodeProperties - edgeMetadata - edgeProperties - } - } - } - """, - { - "graph": { - "getIndexSpec": { - "edgeMetadata": ["p1"], - "edgeProperties": ["p1", "q1"], - "nodeMetadata": ["p1"], - "nodeProperties": ["p1", "q1"], - } - } - }, - ), - ] - - run_group_graphql_test(queries, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_create_index_using_client(graph): - tmp_work_dir = tempfile.mkdtemp() - with GraphServer(tmp_work_dir).start() as server: - client = server.get_client() - client.send_graph(path="g", graph=graph) - - query = """{graph(path: "g") {nodes {list {name}}}}""" - assert client.query(query) == { - "graph": { - "nodes": { - "list": [ - {"name": "N1"}, - {"name": "N2"}, - {"name": "N3"}, - {"name": "N4"}, - {"name": "N5"}, - {"name": "N6"}, - {"name": "N7"}, - {"name": "N8"}, - {"name": "N9"}, - {"name": "N10"}, - {"name": "N11"}, - {"name": "N12"}, - {"name": "N13"}, - {"name": "N14"}, - {"name": "N15"}, - ] - } - } - } - - spec = RemoteIndexSpec( - node_props=PropsInput(all=AllPropertySpec.AllMetadata), - edge_props=PropsInput( - some=SomePropertySpec(metadata=["p1"], properties=["q1"]) - ), - ) - client.create_index("g", spec, in_ram=True) - - query = """query { - graph(path: "g") { - getIndexSpec { - nodeMetadata - nodeProperties - edgeMetadata - edgeProperties - } - } - } - """ - assert client.query(query) == { - "graph": { - "getIndexSpec": { - "edgeMetadata": ["p1"], - "edgeProperties": ["q1"], - "nodeMetadata": ["p1"], - "nodeProperties": [], - } - } - } diff --git a/python/tests/test_search/test_index.py b/python/tests/test_search/test_index.py deleted file mode 100644 index 92c44eda7d..0000000000 --- a/python/tests/test_search/test_index.py +++ /dev/null @@ -1,1532 +0,0 @@ -from raphtory import Graph, PersistentGraph -import pytest -from raphtory import filter - - -def init_graph(graph): - """Initializes the graph with nodes and properties.""" - - # Adding nodes with properties - graph.add_node(6, "N1", {"p1": 2}, "fire_nation") - graph.add_node(7, "N1", {"p1": 1}, "fire_nation") - graph.node("N1").add_metadata({"p1": 1}) - - graph.add_node(6, "N2", {"p1": 1}, "earth_kingdom") - graph.add_node(7, "N2", {"p1": 2}, "earth_kingdom") - - graph.add_node(8, "N3", {"p1": 1}, "water_tribe") - - graph.add_node(9, "N4", {"p1": 1}, "water_tribe") - graph.node("N4").add_metadata({"p1": 2}) - - graph.add_node(5, "N5", {"p1": 1}, "water_tribe") - graph.add_node(6, "N5", {"p1": 2}, "water_tribe") - - graph.add_node(5, "N6", {"p1": 1}, "fire_nation") - graph.add_node(6, "N6", {"p1": 1}, "fire_nation") - - graph.add_node(3, "N7", {"p1": 1}, "air_nomads") - graph.add_node(5, "N7", {"p1": 1}, "air_nomads") - - graph.add_node(3, "N8", {"p1": 1}, "air_nomads") - graph.add_node(4, "N8", {"p1": 2}, "air_nomads") - - graph.add_node(2, "N9", {"p1": 2}, "earth_kingdom") - graph.node("N9").add_metadata({"p1": 1}) - - graph.add_node(2, "N10", {"q1": 0}, "fire_nation") - graph.add_node(2, "N10", {"p1": 3}, "fire_nation") - graph.node("N10").add_metadata({"p1": 1}) - - graph.add_node(2, "N11", {"p1": 3}, "fire_nation") - graph.add_node(2, "N11", {"q1": 0}, "fire_nation") - graph.node("N11").add_metadata({"p1": 1}) - - graph.add_node(2, "N12", {"q1": 0}, "air_nomads") - graph.add_node(3, "N12", {"p1": 3}, "air_nomads") - graph.node("N12").add_metadata({"p1": 1}) - - graph.add_node(2, "N13", {"q1": 0}, "air_nomads") - graph.add_node(3, "N13", {"p1": 3}, "air_nomads") - graph.node("N13").add_metadata({"p1": 1}) - - graph.add_node(2, "N14", {"q1": 0}, "water_tribe") - graph.node("N14").add_metadata({"p1": 1}) - - graph.add_node(2, "N15", {}, "water_tribe") # NO_PROPS equivalent - graph.node("N15").add_metadata({"p1": 1}) - - return graph - - -def search_nodes(graph, filter_expr, limit=20, offset=0): - graph.create_index_in_ram() - return sorted( - [node.name for node in graph.search_nodes(filter_expr, limit, offset)] - ) - - -def test_search_nodes_for_node_name_eq(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.name() == "N1" - results = search_nodes(g, filter_expr) - assert ["N1"] == results - - -def test_search_nodes_for_node_name_ne(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.name() != "N1" - results = search_nodes(g, filter_expr) - assert [ - "N10", - "N11", - "N12", - "N13", - "N14", - "N15", - "N2", - "N3", - "N4", - "N5", - "N6", - "N7", - "N8", - "N9", - ] == results - - -def test_search_nodes_for_node_name_is_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.name().is_in(["N1", "N9"]) - results = search_nodes(g, filter_expr) - assert ["N1", "N9"] == results - - -def test_search_nodes_for_node_name_is_not_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.name().is_not_in(["N10", "N11", "N12", "N13", "N14"]) - results = search_nodes(g, filter_expr) - assert ["N1", "N15", "N2", "N3", "N4", "N5", "N6", "N7", "N8", "N9"] == results - - -def test_search_nodes_for_node_name_fuzzy_match(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.name().fuzzy_search("1", 1, False) - results = search_nodes(g, filter_expr) - assert ["N1"] == results - - -def test_search_nodes_for_node_type_eq(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.node_type() == "fire_nation" - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N6"] == results - - -def test_search_nodes_for_node_type_ne(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.node_type() != "water_tribe" - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N2", "N6", "N7", "N8", "N9"] == results - - -def test_search_nodes_for_node_type_is_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.node_type().is_in(["air_nomads", "fire_nation"]) - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N6", "N7", "N8"] == results - - -def test_search_nodes_for_node_type_is_not_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.node_type().is_not_in( - ["water_tribe", "air_nomads", "fire_nation"] - ) - results = search_nodes(g, filter_expr) - assert ["N2", "N9"] == results - - -def test_search_nodes_for_node_type_fuzzy_match(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.node_type().fuzzy_search("air_nomad", 1, False) - results = search_nodes(g, filter_expr) - assert ["N12", "N13", "N7", "N8"] == results - - -def test_search_nodes_for_property_eq(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1") == 1 - results = search_nodes(g, filter_expr) - assert ["N1", "N3", "N4", "N6", "N7"] == results - - -def test_search_nodes_for_property_ne(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1") != 2 - results = search_nodes(g, filter_expr) - assert [ - "N1", - "N10", - "N11", - "N12", - "N13", - "N3", - "N4", - "N6", - "N7", - ] == results - - -def test_search_nodes_for_property_lt(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("q1") < 2 - results = search_nodes(g, filter_expr) - assert ["N10", "N11", "N12", "N13", "N14"] == results - - -def test_search_nodes_for_property_le(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("q1") <= 3 - results = search_nodes(g, filter_expr) - assert ["N10", "N11", "N12", "N13", "N14"] == results - - -def test_search_nodes_for_property_gt(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1") > 2 - results = search_nodes(g, filter_expr) - assert ["N10", "N11", "N12", "N13"] == results - - -def test_search_nodes_for_property_ge(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1") >= 2 - results = search_nodes(g, filter_expr) - assert ["N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"] == results - - -def test_search_nodes_for_property_is_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").is_in([2]) - results = search_nodes(g, filter_expr) - assert ["N2", "N5", "N8", "N9"] == results - - -def test_search_nodes_for_property_is_not_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").is_not_in([2]) - results = search_nodes(g, filter_expr) - assert [ - "N1", - "N10", - "N11", - "N12", - "N13", - "N3", - "N4", - "N6", - "N7", - ] == results - - -def test_search_nodes_for_property_is_some(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").is_some() - results = search_nodes(g, filter_expr) - assert [ - "N1", - "N10", - "N11", - "N12", - "N13", - "N2", - "N3", - "N4", - "N5", - "N6", - "N7", - "N8", - "N9", - ] == results - - -@pytest.mark.skip(reason="Ignoring this test temporarily") -def test_search_nodes_for_property_is_none(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").is_none() - results = search_nodes(g, filter_expr) - assert [] == results - - -def test_search_nodes_for_metdata_eq(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1") == 1 - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N14", "N15", "N9"] == results - - -def test_search_nodes_for_metadata_ne(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1") != 2 - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N14", "N15", "N9"] == results - - -def test_search_nodes_for_metadata_lt(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1") < 2 - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N14", "N15", "N9"] == results - - -def test_search_nodes_for_metadata_le(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1") <= 3 - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N14", "N15", "N4", "N9"] == results - - -def test_search_nodes_for_metadata_gt(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1") > 1 - results = search_nodes(g, filter_expr) - assert ["N4"] == results - - -def test_search_nodes_for_metadata_ge(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1") >= 2 - results = search_nodes(g, filter_expr) - assert ["N4"] == results - - -def test_search_nodes_for_metadata_is_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1").is_in([2]) - results = search_nodes(g, filter_expr) - assert ["N4"] == results - - -def test_search_nodes_for_metadata_is_not_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1").is_not_in([2]) - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N14", "N15", "N9"] == results - - -def test_search_nodes_for_metadata_is_some(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1").is_some() - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N14", "N15", "N4", "N9"] == results - - -@pytest.mark.skip(reason="Ignoring this test temporarily") -def test_search_nodes_for_metadata_is_none(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.metadata("p1").is_none() - results = search_nodes(g, filter_expr) - assert [] == results - - -def test_search_nodes_for_property_temporal_any_eq(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any() == 1 - results = search_nodes(g, filter_expr) - assert ["N1", "N2", "N3", "N4", "N5", "N6", "N7", "N8"] == results - - -def test_search_nodes_for_property_temporal_any_ne(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any() != 2 - results = search_nodes(g, filter_expr) - assert [ - "N1", - "N10", - "N11", - "N12", - "N13", - "N2", - "N3", - "N4", - "N5", - "N6", - "N7", - "N8", - ] == results - - -def test_search_nodes_for_property_temporal_any_lt(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any() < 2 - results = search_nodes(g, filter_expr) - assert ["N1", "N2", "N3", "N4", "N5", "N6", "N7", "N8"] == results - - -def test_search_nodes_for_property_temporal_any_le(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any() <= 3 - results = search_nodes(g, filter_expr) - assert [ - "N1", - "N10", - "N11", - "N12", - "N13", - "N2", - "N3", - "N4", - "N5", - "N6", - "N7", - "N8", - "N9", - ] == results - - -def test_search_nodes_for_property_temporal_any_gt(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any() > 1 - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"] == results - - -def test_search_nodes_for_property_temporal_any_ge(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any() >= 2 - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"] == results - - -def test_search_nodes_for_property_temporal_any_is_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any().is_in([2]) - results = search_nodes(g, filter_expr) - assert ["N1", "N2", "N5", "N8", "N9"] == results - - -def test_search_nodes_for_property_temporal_any_is_not_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any().is_not_in([2]) - results = search_nodes(g, filter_expr) - assert [ - "N1", - "N10", - "N11", - "N12", - "N13", - "N2", - "N3", - "N4", - "N5", - "N6", - "N7", - "N8", - ] == results - - -def test_search_nodes_for_property_temporal_any_is_some(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any().is_some() - results = search_nodes(g, filter_expr) - assert [ - "N1", - "N10", - "N11", - "N12", - "N13", - "N2", - "N3", - "N4", - "N5", - "N6", - "N7", - "N8", - "N9", - ] == results - - -@pytest.mark.skip(reason="Ignoring this test temporarily") -def test_search_nodes_for_property_temporal_any_is_none(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().any().is_none() - results = search_nodes(g, filter_expr) - assert [] == results - - -def test_search_nodes_for_property_temporal_last_eq(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last() == 1 - results = search_nodes(g, filter_expr) - assert ["N1", "N3", "N4", "N6", "N7"] == results - - -def test_search_nodes_for_property_temporal_last_ne(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last() != 2 - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N3", "N4", "N6", "N7"] == results - - -def test_search_nodes_for_property_temporal_last_lt(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last() < 2 - results = search_nodes(g, filter_expr) - assert ["N1", "N3", "N4", "N6", "N7"] == results - - -def test_search_nodes_for_property_temporal_last_le(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last() <= 3 - results = search_nodes(g, filter_expr) - assert [ - "N1", - "N10", - "N11", - "N12", - "N13", - "N2", - "N3", - "N4", - "N5", - "N6", - "N7", - "N8", - "N9", - ] == results - - -def test_search_nodes_for_property_temporal_last_gt(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last() > 1 - results = search_nodes(g, filter_expr) - assert ["N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"] == results - - -def test_search_nodes_for_property_temporal_last_ge(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last() >= 2 - results = search_nodes(g, filter_expr) - assert ["N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"] == results - - -def test_search_nodes_for_property_temporal_last_is_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last().is_in([2]) - results = search_nodes(g, filter_expr) - assert ["N2", "N5", "N8", "N9"] == results - - -def test_search_nodes_for_property_temporal_last_is_not_in(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last().is_not_in([2]) - results = search_nodes(g, filter_expr) - assert ["N1", "N10", "N11", "N12", "N13", "N3", "N4", "N6", "N7"] == results - - -def test_search_nodes_for_property_temporal_last_is_some(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last().is_some() - results = search_nodes(g, filter_expr) - assert [ - "N1", - "N10", - "N11", - "N12", - "N13", - "N2", - "N3", - "N4", - "N5", - "N6", - "N7", - "N8", - "N9", - ] == results - - -def test_search_nodes_for_composite_filter(): - g = Graph() - g = init_graph(g) - - filter1 = filter.Node.node_type() == "fire_nation" - filter2 = filter.Node.metadata("p1") > 1 - results = search_nodes(g, filter1 | filter2) - assert ["N1", "N10", "N11", "N4", "N6"] == results - - -@pytest.mark.skip(reason="Ignoring this test temporarily") -def test_search_nodes_for_property_temporal_last_is_none(): - g = Graph() - g = init_graph(g) - - filter_expr = filter.Node.property("p1").temporal().last().is_none() - results = search_nodes(g, filter_expr) - assert [] == results - - -def init_edges_graph(graph): - """Initializes the graph with nodes and properties.""" - - # Adding nodes with properties - graph.add_edge(6, "N1", "N2", {"p1": 2}, "fire_nation") - graph.add_edge(7, "N1", "N2", {"p1": 1}, "fire_nation") - graph.edge("N1", "N2").add_metadata({"p1": 1}, layer="fire_nation") - - graph.add_edge(6, "N2", "N3", {"p1": 1}, "earth_kingdom") - graph.add_edge(7, "N2", "N3", {"p1": 2}, "earth_kingdom") - - graph.add_edge(8, "N3", "N4", {"p1": 1}, "water_tribe") - - graph.add_edge(9, "N4", "N5", {"p1": 1}, "water_tribe") - graph.edge("N4", "N5").add_metadata({"p1": 2}, layer="water_tribe") - - graph.add_edge(5, "N5", "N6", {"p1": 1}, "water_tribe") - graph.add_edge(6, "N5", "N6", {"p1": 2}, "water_tribe") - - graph.add_edge(5, "N6", "N7", {"p1": 1}, "fire_nation") - graph.add_edge(6, "N6", "N7", {"p1": 1}, "fire_nation") - - graph.add_edge(3, "N7", "N8", {"p1": 1}, "air_nomads") - graph.add_edge(5, "N7", "N8", {"p1": 1}, "air_nomads") - - graph.add_edge(3, "N8", "N9", {"p1": 1}, "air_nomads") - graph.add_edge(4, "N8", "N9", {"p1": 2}, "air_nomads") - - graph.add_edge(2, "N9", "N10", {"p1": 2}, "earth_kingdom") - graph.edge("N9", "N10").add_metadata({"p1": 1}, layer="earth_kingdom") - - graph.add_edge(2, "N10", "N11", {"q1": 0}, "fire_nation") - graph.add_edge(2, "N10", "N11", {"p1": 3}, "fire_nation") - graph.edge("N10", "N11").add_metadata({"p1": 1}, layer="fire_nation") - - graph.add_edge(2, "N11", "N12", {"p1": 3}, "fire_nation") - graph.add_edge(2, "N11", "N12", {"q1": 0}, "fire_nation") - graph.edge("N11", "N12").add_metadata({"p1": 1}, layer="fire_nation") - - graph.add_edge(2, "N12", "N13", {"q1": 0}, "air_nomads") - graph.add_edge(3, "N12", "N13", {"p1": 3}, "air_nomads") - graph.edge("N12", "N13").add_metadata({"p1": 1}, layer="air_nomads") - - graph.add_edge(2, "N13", "N14", {"q1": 0}, "air_nomads") - graph.add_edge(3, "N13", "N14", {"p1": 3}, "air_nomads") - graph.edge("N13", "N14").add_metadata({"p1": 1}, layer="air_nomads") - - graph.add_edge(2, "N14", "N15", {"q1": 0}, "water_tribe") - graph.edge("N14", "N15").add_metadata({"p1": 1}, layer="water_tribe") - - graph.add_edge(2, "N15", "N1", {}, "water_tribe") # NO_PROPS equivalent - graph.edge("N15", "N1").add_metadata({"p1": 1}, layer="water_tribe") - - return graph - - -def search_edges(graph, filter_expr, limit=20, offset=0): - graph.create_index_in_ram() - return sorted( - [ - (edge.src.name, edge.dst.name) - for edge in graph.search_edges(filter_expr, limit, offset) - ] - ) - - -def search_edges_layer(layer, graph, filter_expr, limit=20, offset=0): - graph.create_index_in_ram() - graph = graph.layer(layer) - return sorted( - [ - (edge.src.name, edge.dst.name) - for edge in graph.search_edges(filter_expr, limit, offset) - ] - ) - - -def test_search_edges_for_src_eq(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.src().name() == "N1" - results = search_edges(g, filter_expr) - assert [("N1", "N2")] == results - - -def test_search_edges_for_src_ne(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.src().name() != "N1" - results = search_edges(g, filter_expr) - assert [ - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N14", "N15"), - ("N15", "N1"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_src_is_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.src().name().is_in(["N1", "N9"]) - results = search_edges(g, filter_expr) - assert [("N1", "N2"), ("N9", "N10")] == results - - -def test_search_edges_for_src_is_not_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = ( - filter.Edge.src().name().is_not_in(["N10", "N11", "N12", "N13", "N14"]) - ) - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N15", "N1"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_src_fuzzy_match(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.src().name().fuzzy_search("1", 1, False) - results = search_edges(g, filter_expr) - assert [("N1", "N2")] == results - - -def test_search_edges_for_dst_eq(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.dst().name() == "N1" - results = search_edges(g, filter_expr) - assert [("N15", "N1")] == results - - -def test_search_edges_for_dst_ne(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.dst().name() != "N1" - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N14", "N15"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_dst_is_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.dst().name().is_in(["N1", "N9"]) - results = search_edges(g, filter_expr) - assert [("N15", "N1"), ("N8", "N9")] == results - - -def test_search_edges_for_dst_is_not_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.dst().name().is_not_in(["N1", "N9", "N10"]) - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N14", "N15"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ] == results - - -def test_search_edges_for_dst_fuzzy_match(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.dst().name().fuzzy_search("1", 1, False) - results = search_edges(g, filter_expr) - assert [("N15", "N1")] == results - - -def test_search_edges_for_property_eq(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1") == 1 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N3", "N4"), - ("N4", "N5"), - ("N6", "N7"), - ("N7", "N8"), - ] == results - - -def test_search_edges_for_property_ne(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1") != 2 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N3", "N4"), - ("N4", "N5"), - ("N6", "N7"), - ("N7", "N8"), - ] == results - - -def test_search_edges_for_property_lt(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("q1") < 2 - results = search_edges(g, filter_expr) - assert [ - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N14", "N15"), - ] == results - - -def test_search_edges_for_property_le(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("q1") <= 3 - results = search_edges(g, filter_expr) - assert [ - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N14", "N15"), - ] == results - - -def test_search_edges_for_property_gt(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1") > 2 - results = search_edges(g, filter_expr) - assert [("N10", "N11"), ("N11", "N12"), ("N12", "N13"), ("N13", "N14")] == results - - -def test_search_edges_for_property_ge(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1") >= 2 - results = search_edges(g, filter_expr) - assert [ - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N5", "N6"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_property_is_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").is_in([2]) - results = search_edges(g, filter_expr) - assert [("N2", "N3"), ("N5", "N6"), ("N8", "N9"), ("N9", "N10")] == results - - -def test_search_edges_for_property_is_not_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").is_not_in([2]) - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N3", "N4"), - ("N4", "N5"), - ("N6", "N7"), - ("N7", "N8"), - ] == results - - -def test_search_edges_for_property_is_some(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").is_some() - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -@pytest.mark.skip(reason="Ignoring this test temporarily") -def test_search_edges_for_property_is_none(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").is_none() - results = search_edges(g, filter_expr) - assert [] == results - - -def test_search_edges_for_metadata_eq(): - g = Graph() - g = init_edges_graph(g) - - # filter_expr = filter.Edge.metadata("p1") == {"fire_nation": 1} Should be supported by search or fallback - filter_expr = filter.Edge.metadata("p1") == 1 - - results = search_edges_layer("fire_nation", g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ] == results - - -def test_search_edges_for_metadata_ne(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.metadata("p1") != 2 - results = search_edges_layer("water_tribe", g, filter_expr) - assert [ - ("N14", "N15"), - ("N15", "N1"), - ] == results - - -def test_search_edges_for_metadata_lt(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.metadata("p1") < 2 - results = search_edges_layer("water_tribe", g, filter_expr) - assert [ - ("N14", "N15"), - ("N15", "N1"), - ] == results - - -def test_search_edges_for_metadata_le(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.metadata("p1") <= 3 - results = search_edges_layer("fire_nation", g, filter_expr) - assert [("N1", "N2"), ("N10", "N11"), ("N11", "N12")] == results - - -def test_search_edges_for_metadata_gt(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.metadata("p1") > 1 - results = search_edges_layer("water_tribe", g, filter_expr) - assert [("N4", "N5")] == results - - -def test_search_edges_for_metadata_ge(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.metadata("p1") >= 2 - results = search_edges_layer("water_tribe", g, filter_expr) - assert [("N4", "N5")] == results - - -def test_search_edges_for_metadata_is_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.metadata("p1").is_in([2]) - results = search_edges_layer("water_tribe", g, filter_expr) - assert [("N4", "N5")] == results - - -def test_search_edges_for_metadata_is_not_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.metadata("p1").is_not_in([2]) - results = search_edges_layer("water_tribe", g, filter_expr) - assert [ - ("N14", "N15"), - ("N15", "N1"), - ] == results - - -def test_search_edges_for_metadata_is_some(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.metadata("p1").is_some() - results = search_edges_layer("water_tribe", g, filter_expr) - assert [ - ("N14", "N15"), - ("N15", "N1"), - ("N4", "N5"), - ] == results - - -def test_search_edges_for_metadata_is_none(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.metadata("p1").is_none() - results = search_edges(g, filter_expr) - assert [ - ("N2", "N3"), - ("N3", "N4"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ] == results - - -def test_search_edges_for_property_temporal_any_eq(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any() == 1 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ] == results - - -def test_search_edges_for_property_temporal_any_ne(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any() != 2 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ] == results - - -def test_search_edges_for_property_temporal_any_lt(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any() < 2 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ] == results - - -def test_search_edges_for_property_temporal_any_le(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any() <= 3 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_property_temporal_any_gt(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any() > 1 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N5", "N6"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_property_temporal_any_ge(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any() >= 2 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N5", "N6"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_property_temporal_any_is_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any().is_in([2]) - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N2", "N3"), - ("N5", "N6"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_property_temporal_any_is_not_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any().is_not_in([2]) - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ] == results - - -def test_search_edges_for_property_temporal_any_is_some(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any().is_some() - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -@pytest.mark.skip(reason="Ignoring this test temporarily") -def test_search_edges_for_property_temporal_any_is_none(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().any().is_none() - results = search_edges(g, filter_expr) - assert [] == results - - -def test_search_edges_for_property_temporal_last_eq(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last() == 1 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N3", "N4"), - ("N4", "N5"), - ("N6", "N7"), - ("N7", "N8"), - ] == results - - -def test_search_edges_for_property_temporal_last_ne(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last() != 2 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N3", "N4"), - ("N4", "N5"), - ("N6", "N7"), - ("N7", "N8"), - ] == results - - -def test_search_edges_for_property_temporal_last_lt(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last() < 2 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N3", "N4"), - ("N4", "N5"), - ("N6", "N7"), - ("N7", "N8"), - ] == results - - -def test_search_edges_for_property_temporal_last_le(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last() <= 3 - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_property_temporal_last_gt(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last() > 1 - results = search_edges(g, filter_expr) - assert [ - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N5", "N6"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_property_temporal_last_ge(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last() >= 2 - results = search_edges(g, filter_expr) - assert [ - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N5", "N6"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -def test_search_edges_for_property_temporal_last_is_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last().is_in([2]) - results = search_edges(g, filter_expr) - assert [("N2", "N3"), ("N5", "N6"), ("N8", "N9"), ("N9", "N10")] == results - - -def test_search_edges_for_property_temporal_last_is_not_in(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last().is_not_in([2]) - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N3", "N4"), - ("N4", "N5"), - ("N6", "N7"), - ("N7", "N8"), - ] == results - - -def test_search_edges_for_property_temporal_last_is_some(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last().is_some() - results = search_edges(g, filter_expr) - assert [ - ("N1", "N2"), - ("N10", "N11"), - ("N11", "N12"), - ("N12", "N13"), - ("N13", "N14"), - ("N2", "N3"), - ("N3", "N4"), - ("N4", "N5"), - ("N5", "N6"), - ("N6", "N7"), - ("N7", "N8"), - ("N8", "N9"), - ("N9", "N10"), - ] == results - - -@pytest.mark.skip(reason="Ignoring this test temporarily") -def test_search_edges_for_property_temporal_last_is_none(): - g = Graph() - g = init_edges_graph(g) - - filter_expr = filter.Edge.property("p1").temporal().last().is_none() - results = search_edges(g, filter_expr) - assert [] == results - - -def test_search_edges_for_composite_filter(): - g = Graph() - g = init_edges_graph(g) - - filter1 = filter.Edge.src().name() == "N13" - filter2 = filter.Edge.property("p1").temporal().last() == 3 - results = search_edges(g, filter1 & filter2) - assert [("N13", "N14")] == results - - -def test_search_edges_for_composite_filter_pg(): - g = PersistentGraph() - g = init_edges_graph(g) - - filter1 = filter.Edge.src().name() == "N13" - filter2 = filter.Edge.property("p1").temporal().last() == 3 - results = search_edges(g, filter1 & filter2) - assert [("N13", "N14")] == results diff --git a/python/tests/test_search/test_index_spec.py b/python/tests/test_search/test_index_spec.py deleted file mode 100644 index ed132d8816..0000000000 --- a/python/tests/test_search/test_index_spec.py +++ /dev/null @@ -1,154 +0,0 @@ -from raphtory import Graph, IndexSpecBuilder -from raphtory import filter - - -def init_graph(graph): - nodes = [ - (1, "pometry", {"p1": 5, "p2": 50}, "fire_nation", {"x": True}), - (1, "raphtory", {"p1": 10, "p2": 100}, "water_tribe", {"y": False}), - ] - for t, name, props, group, metadata in nodes: - n = graph.add_node(t, name, props, group) - n.add_metadata(metadata) - - edges = [ - (1, "pometry", "raphtory", {"e_p1": 3.2, "e_p2": 10.0}, {"e_x": True}), - (1, "raphtory", "pometry", {"e_p1": 4.0, "e_p2": 20.0}, {"e_y": False}), - ] - for t, src, dst, props, metadata in edges: - e = graph.add_edge(t, src, dst, props) - e.add_metadata(metadata) - - return graph - - -def search_nodes(graph, filter_expr): - return sorted(n.name for n in graph.search_nodes(filter_expr, 10, 0)) - - -def search_edges(graph, filter_expr): - return sorted( - f"{e.src.name}->{e.dst.name}" for e in graph.search_edges(filter_expr, 10, 0) - ) - - -def test_with_all_props_index_spec(): - graph = init_graph(Graph()) - spec = ( - IndexSpecBuilder(graph) - .with_all_node_properties_and_metadata() - .with_all_edge_properties_and_metadata() - .build() - ) - - graph.create_index_in_ram_with_spec(spec) - - f1 = filter.Node.property("p1") == 5 - f2 = filter.Node.metadata("x") == True - assert search_nodes(graph, f1 & f2) == ["pometry"] - - f1 = filter.Edge.property("e_p1") < 5.0 - f2 = filter.Edge.metadata("e_y") == False - assert sorted(search_edges(graph, f1 & f2)) == sorted(["raphtory->pometry"]) - - -def test_with_selected_props_index_spec(): - graph = init_graph(Graph()) - spec = ( - IndexSpecBuilder(graph) - .with_node_metadata(["y"]) - .with_node_properties(["p1"]) - .with_edge_metadata(["e_y"]) - .with_edge_properties(["e_p1"]) - .build() - ) - - graph.create_index_in_ram_with_spec(spec) - - f1 = filter.Node.property("p1") == 5 - f2 = filter.Node.metadata("y") == False - assert sorted(search_nodes(graph, f1 | f2)) == sorted(["pometry", "raphtory"]) - - f = filter.Node.metadata("y") == False - assert search_nodes(graph, f) == ["raphtory"] - - f1 = filter.Edge.property("e_p1") < 5.0 - f2 = filter.Edge.metadata("e_y") == False - assert sorted(search_edges(graph, f1 | f2)) == sorted( - ["pometry->raphtory", "raphtory->pometry"] - ) - - -def test_with_invalid_property_returns_error(): - graph = init_graph(Graph()) - try: - IndexSpecBuilder(graph).with_node_metadata(["xyz"]) - assert False, "Expected error for unknown property" - except Exception as e: - assert "xyz" in str(e) - - -def test_build_empty_spec_by_default(): - graph = init_graph(Graph()) - spec = IndexSpecBuilder(graph).build() - - graph.create_index_in_ram_with_spec(spec) - - f1 = filter.Node.property("p1") == 5 - f2 = filter.Node.metadata("x") == True - assert sorted(search_nodes(graph, f1 & f2)) == ["pometry"] - - f1 = filter.Edge.property("e_p1") < 5.0 - f2 = filter.Edge.metadata("e_y") == False - assert sorted(search_edges(graph, f1 | f2)) == sorted( - ["pometry->raphtory", "raphtory->pometry"] - ) - - -def test_mixed_node_and_edge_props_index_spec(): - graph = init_graph(Graph()) - spec = ( - IndexSpecBuilder(graph) - .with_node_metadata(["x"]) - .with_node_metadata(["y"]) - .with_all_node_properties() - .with_all_edge_properties() - .build() - ) - - graph.create_index_in_ram_with_spec(spec) - - f1 = filter.Node.property("p1") == 5 - f2 = filter.Node.metadata("y") == False - assert sorted(search_nodes(graph, f1 | f2)) == sorted(["pometry", "raphtory"]) - - f1 = filter.Edge.property("e_p1") < 5.0 - f2 = filter.Edge.metadata("e_y") == False - assert sorted(search_edges(graph, f1 | f2)) == sorted( - ["pometry->raphtory", "raphtory->pometry"] - ) - - -def test_get_index_spec(): - graph = init_graph(Graph()) - spec = ( - IndexSpecBuilder(graph) - .with_node_metadata(["x"]) - .with_all_node_properties_and_metadata() - .with_all_edge_properties_and_metadata() - .build() - ) - - graph.create_index_in_ram_with_spec(spec) - - returned_spec = graph.get_index_spec() - - node_metadata_names = {name for name in returned_spec.node_metadata} - node_property_names = {name for name in returned_spec.node_properties} - edge_metadata_names = {name for name in returned_spec.edge_metadata} - edge_property_names = {name for name in returned_spec.edge_properties} - - assert "x" in node_metadata_names - assert "p1" in node_property_names and "p2" in node_property_names - assert "e_x" in edge_metadata_names and "e_y" in edge_metadata_names - assert "e_p1" in edge_property_names and "e_p2" in edge_property_names From db868ba5af35f79abbec04b85a3d110ff84000ce Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:37:31 +0100 Subject: [PATCH 063/155] rid search tests gql --- raphtory-graphql/src/lib.rs | 150 ------------------------------------ 1 file changed, 150 deletions(-) diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs index 1a3f2acfde..026c1fd7c7 100644 --- a/raphtory-graphql/src/lib.rs +++ b/raphtory-graphql/src/lib.rs @@ -105,156 +105,6 @@ mod graphql_test { assert_eq!(res.errors, []); } - #[tokio::test] - #[cfg(feature = "search")] - async fn test_search_nodes_gql() { - let graph = Graph::new(); - - let nodes = vec![ - (6, "N1", vec![("p1", Prop::U64(2u64))]), - (7, "N1", vec![("p1", Prop::U64(1u64))]), - (6, "N2", vec![("p1", Prop::U64(1u64))]), - (7, "N2", vec![("p1", Prop::U64(2u64))]), - (8, "N3", vec![("p1", Prop::U64(1u64))]), - (9, "N4", vec![("p1", Prop::U64(1u64))]), - (5, "N5", vec![("p1", Prop::U64(1u64))]), - (6, "N5", vec![("p1", Prop::U64(2u64))]), - (5, "N6", vec![("p1", Prop::U64(1u64))]), - (6, "N6", vec![("p1", Prop::U64(1u64))]), - (3, "N7", vec![("p1", Prop::U64(1u64))]), - (5, "N7", vec![("p1", Prop::U64(1u64))]), - (3, "N8", vec![("p1", Prop::U64(1u64))]), - (4, "N8", vec![("p1", Prop::U64(2u64))]), - (2, "N9", vec![("p1", Prop::U64(2u64))]), - (2, "N10", vec![("q1", Prop::U64(0u64))]), - (2, "N10", vec![("p1", Prop::U64(3u64))]), - (2, "N11", vec![("p1", Prop::U64(3u64))]), - (2, "N11", vec![("q1", Prop::U64(0u64))]), - (2, "N12", vec![("q1", Prop::U64(0u64))]), - (3, "N12", vec![("p1", Prop::U64(3u64))]), - (2, "N13", vec![("q1", Prop::U64(0u64))]), - (3, "N13", vec![("p1", Prop::U64(3u64))]), - (2, "N14", vec![("q1", Prop::U64(0u64))]), - (2, "N15", vec![]), - ]; - - for (id, name, props) in nodes { - graph.add_node(id, name, props, None, None).unwrap(); - } - - let metadata = vec![ - ("N1", vec![("p1", Prop::U64(1u64))]), - ("N4", vec![("p1", Prop::U64(2u64))]), - ("N9", vec![("p1", Prop::U64(1u64))]), - ("N10", vec![("p1", Prop::U64(1u64))]), - ("N11", vec![("p1", Prop::U64(1u64))]), - ("N12", vec![("p1", Prop::U64(1u64))]), - ("N13", vec![("p1", Prop::U64(1u64))]), - ("N14", vec![("p1", Prop::U64(1u64))]), - ("N15", vec![("p1", Prop::U64(1u64))]), - ]; - - for (name, props) in metadata { - graph.node(name).unwrap().add_metadata(props).unwrap(); - } - - let graph: MaterializedGraph = graph.into(); - - let graphs = HashMap::from([("master".to_string(), graph)]); - let tmp_dir = tempdir().unwrap(); - let config = AppConfigBuilder::new().with_create_index(true).build(); - let data = Data::new(tmp_dir.path(), &config, Config::default()); - save_graphs_to_work_dir(&data, &graphs).await.unwrap(); - - let schema = App::create_schema().data(data).finish().unwrap(); - - let query = r#" - { - graph(path: "master") { - searchNodes( - filter: { - or: [ - { - property: { - name: "p1", - where: { - gt: { - u64: 2 - } - } - } - }, - { - and: [ - { - node: { - field: NODE_NAME, - where: { - eq: { - str: "N1" - } - } - } - }, - { - node: { - field: NODE_TYPE, - where: { - ne: { - str: "air_nomads" - } - } - } - }, - { - property: { - name: "p1", - where: { - lt: { - u64: 5 - } - } - } - } - ] - } - ] - - - }, - limit: 20, - offset: 0 - ) { - name - } - } - } - "#; - let req = Request::new(query); - let res = schema.execute(req).await; - assert_eq!(res.errors, []); - let mut data = res.data.into_json().unwrap(); - - if let Some(nodes) = data["graph"]["searchNodes"].as_array_mut() { - nodes.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); - } - - assert_eq!( - data, - json!({ - "graph": { - "searchNodes": [ - { "name": "N1" }, - { "name": "N10" }, - { "name": "N11" }, - { "name": "N12" }, - { "name": "N13" } - ] - } - }), - ); - } - #[tokio::test] async fn basic_query() { let graph = PersistentGraph::new(); From 5b51c5d4b58f230c1ca51401eeb528a2a6d40450 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:52:37 +0100 Subject: [PATCH 064/155] impl. py expr --- .../views/filter/model/edge_expr/exprs.rs | 96 --- .../{edge_filter_builders.rs => edge_expr.rs} | 2 +- ...ilter_builder.rs => exploded_edge_expr.rs} | 2 +- .../{node_filter_builders.rs => node_expr.rs} | 2 +- raphtory/src/python/filter/property_expr.rs | 478 +++++++++++++ .../python/filter/property_filter_builders.rs | 669 ------------------ 6 files changed, 481 insertions(+), 768 deletions(-) delete mode 100644 raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs rename raphtory/src/python/filter/{edge_filter_builders.rs => edge_expr.rs} (99%) rename raphtory/src/python/filter/{exploded_edge_filter_builder.rs => exploded_edge_expr.rs} (99%) rename raphtory/src/python/filter/{node_filter_builders.rs => node_expr.rs} (99%) create mode 100644 raphtory/src/python/filter/property_expr.rs delete mode 100644 raphtory/src/python/filter/property_filter_builders.rs diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs deleted file mode 100644 index cbcd7b45bf..0000000000 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/exprs.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! EdgeExpr impls for the shared Property/Metadata structs and scalar types. - -use super::{ops::{EdgeMetaOp, EdgePropOp}, EdgeExpr, EdgeOp}; -use crate::{ - db::api::{state::ops::Const, view::internal::GraphView}, - db::graph::views::filter::model::{edge_filter::EdgeFilter, node_expr::EntityExpr, Metadata, Property}, - errors::GraphError, -}; -use raphtory_api::core::entities::properties::prop::Prop; -use std::sync::Arc; -// ───────────────────────────────────────────────────────────────────────────── -// Property / Metadata — EdgeExpr impls -// ───────────────────────────────────────────────────────────────────────────── - -impl EdgeExpr for Property { - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let prop_id = graph - .edge_meta() - .get_prop_id(&self.name, false) - .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; - Ok(Arc::new(EdgePropOp { graph, prop_id })) - } -} - -impl EdgeExpr for Metadata { - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let prop_id = graph - .edge_meta() - .get_prop_id(&self.name, true) - .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; - Ok(Arc::new(EdgeMetaOp { graph, prop_id })) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// Scalar EdgeExpr impls — literal RHS values -// ───────────────────────────────────────────────────────────────────────────── - -impl EdgeExpr for Prop { - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - _graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(self.clone())))) - } -} - -macro_rules! impl_edge_expr_for_numeric { - ($prim:ty, $variant:ident) => { - impl EdgeExpr for $prim { - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - _graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(Prop::$variant(*self))))) - } - } - }; -} - -impl_edge_expr_for_numeric!(i32, I32); -impl_edge_expr_for_numeric!(i64, I64); -impl_edge_expr_for_numeric!(u32, U32); -impl_edge_expr_for_numeric!(u64, U64); -impl_edge_expr_for_numeric!(f32, F32); -impl_edge_expr_for_numeric!(f64, F64); -impl_edge_expr_for_numeric!(bool, Bool); -impl_edge_expr_for_numeric!(u8, U8); -impl_edge_expr_for_numeric!(u16, U16); - -impl EdgeExpr for &'static str { - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - _graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(Prop::Str( - raphtory_api::core::storage::arc_str::ArcStr::from(*self), - ))))) - } -} - -impl EdgeExpr for usize { - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - _graph: G, - ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Const(Some(Prop::U64(*self as u64))))) - } -} - diff --git a/raphtory/src/python/filter/edge_filter_builders.rs b/raphtory/src/python/filter/edge_expr.rs similarity index 99% rename from raphtory/src/python/filter/edge_filter_builders.rs rename to raphtory/src/python/filter/edge_expr.rs index 086a473d28..ffac8a53ab 100644 --- a/raphtory/src/python/filter/edge_filter_builders.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -11,7 +11,7 @@ use crate::{ python::{ filter::{ filter_expr::PyFilterExpr, - property_filter_builders::{ + property_expr::{ PyEdgeViewPropsFilterBuilder, PyPropertyExprBuilder, PyPropertyFilterBuilder, }, }, diff --git a/raphtory/src/python/filter/exploded_edge_filter_builder.rs b/raphtory/src/python/filter/exploded_edge_expr.rs similarity index 99% rename from raphtory/src/python/filter/exploded_edge_filter_builder.rs rename to raphtory/src/python/filter/exploded_edge_expr.rs index 3766a9c044..c0f1b6b017 100644 --- a/raphtory/src/python/filter/exploded_edge_filter_builder.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -7,7 +7,7 @@ use crate::{ python::{ filter::{ filter_expr::PyFilterExpr, - property_filter_builders::{ + property_expr::{ PyEdgeViewPropsFilterBuilder, PyPropertyExprBuilder, PyPropertyFilterBuilder, }, }, diff --git a/raphtory/src/python/filter/node_filter_builders.rs b/raphtory/src/python/filter/node_expr.rs similarity index 99% rename from raphtory/src/python/filter/node_filter_builders.rs rename to raphtory/src/python/filter/node_expr.rs index 443736c660..1dcecc9039 100644 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -11,7 +11,7 @@ use crate::{ python::{ filter::{ filter_expr::PyFilterExpr, - property_filter_builders::{ + property_expr_builders::{ PyNodeViewPropsFilterBuilder, PyPropertyExprBuilder, PyPropertyFilterBuilder, }, }, diff --git a/raphtory/src/python/filter/property_expr.rs b/raphtory/src/python/filter/property_expr.rs new file mode 100644 index 0000000000..e619465b18 --- /dev/null +++ b/raphtory/src/python/filter/property_expr.rs @@ -0,0 +1,478 @@ +//! Python wrappers over the typed property-expression chain. +//! +//! Two classes: +//! - `PyPropertyExprBuilder` (`filter.FilterOps`) — wraps `Arc` +//! and exposes the comparator/string/set/aggregator/quantifier chain methods. +//! - `PyPropertyFilterBuilder` (`filter.PropertyFilterOps`) — extends `FilterOps` +//! with `.temporal()`. Wraps `Arc`. +//! +//! Plus three view-builder wrappers that combine view restrictions (window / +//! layer / snapshot) with property predicates: +//! - `PyViewFilterBuilder` (`filter.ViewFilterBuilder`) +//! - `PyNodeViewPropsFilterBuilder` (`filter.NodeViewPropsFilterBuilder`) +//! - `PyEdgeViewPropsFilterBuilder` (`filter.EdgeViewPropsFilterBuilder`) + +use crate::{ + db::graph::views::filter::model::{ + edge_filter::EdgeEndpointWrapper, + node_expr::{DynPropertyExpr, DynTemporalPropertyExpr, EntityExpr}, + DynEdgeViewFilterOps, DynEdgeViewProps, DynNodeViewProps, DynView, MetadataExpr, + PropertyExpr, ViewWrapOps, + }, + python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, +}; +use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; +use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; +use std::sync::Arc; + +/// Python wrapper over a chainable property expression. +/// +/// Represents a "property access" plus optional aggregator / quantifier +/// qualifiers (`first`, `len`, `sum`, `any`, …). Comparison operators +/// (`==`, `<`, `is_in`, …) terminate the chain by returning a +/// `filter.FilterExpr`. +/// +/// Combine `FilterExpr`s with `&`, `|`, `~`. +#[pyclass(frozen, name = "FilterOps", module = "raphtory.filter", subclass)] +pub struct PyPropertyExprBuilder(pub Arc); + +impl PyPropertyExprBuilder { + pub fn wrap(t: T) -> Self { + Self(Arc::new(t)) + } + + pub fn from_arc(inner: Arc) -> Self { + Self(inner) + } +} + +#[pymethods] +impl PyPropertyExprBuilder { + fn __eq__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_eq(value)) + } + + fn __ne__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_ne(value)) + } + + fn __lt__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_lt(value)) + } + + fn __le__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_le(value)) + } + + fn __gt__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_gt(value)) + } + + fn __ge__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_ge(value)) + } + + fn is_in(&self, values: FromIterable) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_in(values.into_iter().collect())) + } + + fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_not_in(values.into_iter().collect())) + } + + fn is_none(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_none()) + } + + fn is_some(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_some()) + } + + fn starts_with(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_starts_with(value)) + } + + fn ends_with(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_ends_with(value)) + } + + fn contains(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_contains(value)) + } + + fn not_contains(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_not_contains(value)) + } + + fn fuzzy_search( + &self, + prop_value: Prop, + levenshtein_distance: usize, + prefix_match: bool, + ) -> PyFilterExpr { + PyFilterExpr( + self.0 + .dyn_fuzzy_search(prop_value, levenshtein_distance, prefix_match), + ) + } + + pub fn first(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_first()) + } + + pub fn last(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_last()) + } + + pub fn any(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_any()) + } + + pub fn all(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_all()) + } + + fn len(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_len()) + } + + fn sum(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_sum()) + } + + fn avg(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_avg()) + } + + fn min(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_min()) + } + + fn max(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_max()) + } +} + +/// Python wrapper that adds `.temporal()` to the property-expression chain. +/// +/// Exported as: `filter.PropertyFilterOps`. +#[pyclass( + frozen, + name = "PropertyFilterOps", + module = "raphtory.filter", + extends = PyPropertyExprBuilder +)] +pub struct PyPropertyFilterBuilder(pub(crate) Arc); + +impl PyPropertyFilterBuilder { + pub(crate) fn from_arc(inner: Arc) -> Self { + Self(inner) + } +} + +#[pymethods] +impl PyPropertyFilterBuilder { + /// Switch to temporal evaluation — chain methods now operate over the + /// list of values at each timestamp. + fn temporal(&self) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(self.0.dyn_temporal()) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// IntoPyObject bridges — used by `Node.property(name)` / `Edge.property(name)` +// etc. to surface a `PyPropertyFilterBuilder` from the typed Rust expression. +// ───────────────────────────────────────────────────────────────────────────── + +impl<'py, E> IntoPyObject<'py> for PropertyExpr +where + E: EntityExpr + Clone + Send + Sync + 'static, + PropertyExpr: DynTemporalPropertyExpr, +{ + type Target = PyPropertyFilterBuilder; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let inner: Arc> = Arc::new(self); + let child = PyPropertyFilterBuilder::from_arc(inner.clone()); + let parent = PyPropertyExprBuilder::from_arc(inner); + Bound::new(py, (child, parent)) + } +} + +impl<'py, E> IntoPyObject<'py> for MetadataExpr +where + E: EntityExpr + Clone + Send + Sync + 'static, + MetadataExpr: DynPropertyExpr, +{ + type Target = PyPropertyExprBuilder; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + PyPropertyExprBuilder::wrap(self).into_pyobject(py) + } +} + +impl<'py, E> IntoPyObject<'py> for EdgeEndpointWrapper> +where + E: EntityExpr + Clone + Send + Sync + 'static, + EdgeEndpointWrapper>: DynTemporalPropertyExpr, +{ + type Target = PyPropertyFilterBuilder; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let inner: Arc>> = Arc::new(self); + let child = PyPropertyFilterBuilder::from_arc(inner.clone()); + let parent = PyPropertyExprBuilder::from_arc(inner); + Bound::new(py, (child, parent)) + } +} + +impl<'py, E> IntoPyObject<'py> for EdgeEndpointWrapper> +where + E: EntityExpr + Clone + Send + Sync + 'static, + EdgeEndpointWrapper>: DynPropertyExpr, +{ + type Target = PyPropertyExprBuilder; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let inner: Arc>> = Arc::new(self); + PyPropertyExprBuilder::from_arc(inner).into_pyobject(py) + } +} + +impl<'py> IntoPyObject<'py> for PyPropertyFilterBuilder { + type Target = PyPropertyFilterBuilder; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let parent = PyPropertyExprBuilder::from_arc(self.0.clone()); + Bound::new(py, (self, parent)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// View-builder wrappers (graph / node / edge views restricting when+where +// predicates are evaluated, then exposing `.property()` / `.metadata()` etc.) +// ───────────────────────────────────────────────────────────────────────────── + +/// Graph-level view filter — composes time / snapshot / layer restrictions +/// before applying node or edge predicates. +#[pyclass( + name = "ViewFilterBuilder", + module = "raphtory.filter", + extends = PyFilterExpr, + frozen +)] +pub struct PyViewFilterBuilder(pub(crate) DynView); + +#[pymethods] +impl PyViewFilterBuilder { + /// Restricts evaluation to events in the given time window. + /// + /// The window is inclusive of `start` and exclusive of `end`. + /// + /// Arguments: + /// start (int): Start time. + /// end (int): End time. + /// + /// Returns: + /// filter.ViewFilterBuilder: + fn window(&self, start: EventTime, end: EventTime) -> PyViewFilterBuilder { + PyViewFilterBuilder(self.0.clone().window(start, end)) + } + + /// Restricts evaluation to a single point in time. + /// + /// Arguments: + /// time (int): Event time. + /// + /// Returns: + /// filter.ViewFilterBuilder: + fn at(&self, time: EventTime) -> PyViewFilterBuilder { + PyViewFilterBuilder(self.0.clone().at(time)) + } + + /// Restricts evaluation to times strictly after the given time. + fn after(&self, time: EventTime) -> PyViewFilterBuilder { + PyViewFilterBuilder(self.0.clone().after(time)) + } + + /// Restricts evaluation to times strictly before the given time. + fn before(&self, time: EventTime) -> PyViewFilterBuilder { + PyViewFilterBuilder(self.0.clone().before(time)) + } + + /// Evaluates against the latest available state. + fn latest(&self) -> PyViewFilterBuilder { + PyViewFilterBuilder(Arc::new(self.0.clone().latest())) + } + + /// Evaluates against a snapshot of the graph at a specific time. + fn snapshot_at(&self, time: EventTime) -> PyViewFilterBuilder { + PyViewFilterBuilder(Arc::new(self.0.clone().snapshot_at(time))) + } + + /// Evaluates against the most recent snapshot of the graph. + fn snapshot_latest(&self) -> PyViewFilterBuilder { + PyViewFilterBuilder(Arc::new(self.0.clone().snapshot_latest())) + } + + /// Restricts evaluation to a single layer. + fn layer(&self, layer: String) -> PyViewFilterBuilder { + PyViewFilterBuilder(Arc::new(self.0.clone().layer(layer))) + } + + /// Restricts evaluation to any of the given layers. + fn layers(&self, layers: FromIterable) -> PyViewFilterBuilder { + PyViewFilterBuilder(Arc::new(self.0.clone().layer(layers))) + } +} + +impl<'py> IntoPyObject<'py> for PyViewFilterBuilder { + type Target = PyViewFilterBuilder; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let parent = PyFilterExpr(self.0.clone()); + Bound::new(py, (self, parent)) + } +} + +/// Node-side view + property predicate builder. +#[pyclass( + name = "NodeViewPropsFilterBuilder", + module = "raphtory.filter", + frozen +)] +pub struct PyNodeViewPropsFilterBuilder(pub(crate) DynNodeViewProps); + +#[pymethods] +impl PyNodeViewPropsFilterBuilder { + fn is_active(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_active()) + } + + fn property(&self, name: String) -> PyPropertyFilterBuilder { + PyPropertyFilterBuilder(Arc::new(self.0.property(name))) + } + + fn metadata(&self, name: String) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(Arc::new(self.0.metadata(name))) + } + + fn window(&self, start: EventTime, end: EventTime) -> PyNodeViewPropsFilterBuilder { + PyNodeViewPropsFilterBuilder(self.0.clone().window(start, end)) + } + + fn at(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { + PyNodeViewPropsFilterBuilder(self.0.clone().at(time)) + } + + fn after(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { + PyNodeViewPropsFilterBuilder(self.0.clone().after(time)) + } + + fn before(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { + PyNodeViewPropsFilterBuilder(self.0.clone().before(time)) + } + + fn latest(&self) -> PyNodeViewPropsFilterBuilder { + PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().latest())) + } + + fn snapshot_at(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { + PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_at(time))) + } + + fn snapshot_latest(&self) -> PyNodeViewPropsFilterBuilder { + PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_latest())) + } + + fn layer(&self, layer: String) -> PyNodeViewPropsFilterBuilder { + PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layer))) + } + + fn layers(&self, layers: FromIterable) -> PyNodeViewPropsFilterBuilder { + PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layers))) + } +} + +/// Edge-side view + property predicate builder. +#[pyclass( + name = "EdgeViewPropsFilterBuilder", + module = "raphtory.filter", + frozen +)] +pub struct PyEdgeViewPropsFilterBuilder(pub(crate) DynEdgeViewProps); + +#[pymethods] +impl PyEdgeViewPropsFilterBuilder { + fn is_active(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_active()) + } + + fn is_valid(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_valid()) + } + + fn is_deleted(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_deleted()) + } + + fn is_self_loop(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_self_loop()) + } + + fn property(&self, name: String) -> PyPropertyFilterBuilder { + PyPropertyFilterBuilder(Arc::new(self.0.property(name))) + } + + fn metadata(&self, name: String) -> PyPropertyExprBuilder { + PyPropertyExprBuilder(Arc::new(self.0.metadata(name))) + } + + fn window(&self, start: EventTime, end: EventTime) -> PyEdgeViewPropsFilterBuilder { + PyEdgeViewPropsFilterBuilder(self.0.clone().window(start, end)) + } + + fn at(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { + PyEdgeViewPropsFilterBuilder(self.0.clone().at(time)) + } + + fn after(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { + PyEdgeViewPropsFilterBuilder(self.0.clone().after(time)) + } + + fn before(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { + PyEdgeViewPropsFilterBuilder(self.0.clone().before(time)) + } + + fn latest(&self) -> PyEdgeViewPropsFilterBuilder { + PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().latest())) + } + + fn snapshot_at(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { + PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_at(time))) + } + + fn snapshot_latest(&self) -> PyEdgeViewPropsFilterBuilder { + PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_latest())) + } + + fn layer(&self, layer: String) -> PyEdgeViewPropsFilterBuilder { + PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layer))) + } + + fn layers(&self, layers: FromIterable) -> PyEdgeViewPropsFilterBuilder { + PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layers))) + } +} diff --git a/raphtory/src/python/filter/property_filter_builders.rs b/raphtory/src/python/filter/property_filter_builders.rs deleted file mode 100644 index aa86263137..0000000000 --- a/raphtory/src/python/filter/property_filter_builders.rs +++ /dev/null @@ -1,669 +0,0 @@ -use crate::{ - db::graph::views::filter::{ - model::{ - edge_filter::EdgeEndpointWrapper, - property_filter::{ - builders::{MetadataFilterBuilder, PropertyExprBuilder, PropertyFilterBuilder}, - ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - }, - DynEdgeViewFilterOps, DynEdgeViewProps, DynNodeViewProps, DynView, EntityMarker, - PropertyFilterFactory, TryAsCompositeFilter, ViewWrapOps, - }, - CreateFilter, - }, - prelude::PropertyFilter, - python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, -}; -use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; -use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; -use std::sync::Arc; - -/// Builds property filter expressions. -/// -/// This object represents “a property access” plus optional list/aggregate -/// qualifiers (e.g. `first`, `len`, `sum`) and can emit a `filter.FilterExpr` via -/// comparisons such as `==`, `<`, `is_in`, etc. -/// -/// Returned expressions can be combined with `&`, `|`, and `~` at the -/// `filter.FilterExpr` level (where supported). -#[pyclass(frozen, name = "FilterOps", module = "raphtory.filter", subclass)] -pub struct PyPropertyExprBuilder(pub Arc); - -impl PyPropertyExprBuilder { - pub fn wrap(t: T) -> Self { - Self(Arc::new(t)) - } - - pub fn from_arc(inner: Arc) -> Self { - Self(inner) - } -} - -#[pymethods] -impl PyPropertyExprBuilder { - /// Checks whether the property is equal to the given value. - /// - /// Arguments: - /// value (Prop): Property value to compare against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating equality. - fn __eq__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.eq(value)) - } - - /// Checks whether the property is not equal to the given value. - /// - /// Arguments: - /// value (Prop): Property value to compare against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating inequality. - fn __ne__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.ne(value)) - } - - /// Checks whether the property is less than the given value (exclusive). - /// - /// Arguments: - /// value (Prop): Upper bound (exclusive) for the property. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `<` comparison. - fn __lt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.lt(value)) - } - - /// Checks whether the property is less than or equal to the given value. - /// - /// Arguments: - /// value (Prop): Upper bound (inclusive) for the property. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `<=` comparison. - fn __le__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.le(value)) - } - - /// Checks whether the property is greater than the given value (exclusive). - /// - /// Arguments: - /// value (Prop): Lower bound (exclusive) for the property. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `>` comparison. - fn __gt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.gt(value)) - } - - /// Checks whether the property is greater than or equal to the given value. - /// - /// Arguments: - /// value (Prop): Lower bound (inclusive) for the property. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `>=` comparison. - fn __ge__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.ge(value)) - } - - /// Checks whether the property is contained within the specified iterable of values. - /// - /// Arguments: - /// values (list[Prop]): Iterable of property values to match against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating membership. - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(self.0.is_in(values)) - } - - /// Checks whether the property is **not** contained within the specified iterable of values. - /// - /// Arguments: - /// values (list[Prop]): Iterable of property values to exclude. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating non-membership. - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(self.0.is_not_in(values)) - } - - /// Checks whether the property value is `None` / missing. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating `value is None`. - fn is_none(&self) -> PyFilterExpr { - PyFilterExpr(self.0.is_none()) - } - - /// Checks whether the property value is present (not `None`). - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating `value is not None`. - fn is_some(&self) -> PyFilterExpr { - PyFilterExpr(self.0.is_some()) - } - - /// Checks whether the property's string representation starts with the given value. - /// - /// Arguments: - /// value (Prop): Prefix to check for. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating prefix matching. - fn starts_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.starts_with(value)) - } - - /// Checks whether the property's string representation ends with the given value. - /// - /// Arguments: - /// value (Prop): Suffix to check for. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating suffix matching. - fn ends_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.ends_with(value)) - } - - /// Checks whether the property's string representation contains the given value. - /// - /// Arguments: - /// value (Prop): Substring that must appear within the value. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating substring search. - fn contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.contains(value)) - } - - /// Checks whether the property's string representation **does not** contain the given value. - /// - /// Arguments: - /// value (Prop): Substring that must not appear within the value. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating substring exclusion. - fn not_contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.not_contains(value)) - } - - /// Performs fuzzy matching against the property's string value. - /// - /// Uses a specified Levenshtein distance and optional prefix matching. - /// - /// Arguments: - /// prop_value (str): String to approximately match against. - /// levenshtein_distance (int): Maximum allowed Levenshtein distance. - /// prefix_match (bool): Whether to require a matching prefix. - /// - /// Returns: - /// filter.FilterExpr: A filter expression performing approximate text matching. - fn fuzzy_search( - &self, - prop_value: String, - levenshtein_distance: usize, - prefix_match: bool, - ) -> PyFilterExpr { - PyFilterExpr( - self.0 - .fuzzy_search(prop_value, levenshtein_distance, prefix_match), - ) - } - - /// Selects the first element when the underlying property is list-like. - /// - /// Returns: - /// filter.PropertyFilterOps: - pub fn first(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.first()) - } - - /// Selects the last element when the underlying property is list-like. - /// - /// Returns: - /// filter.PropertyFilterOps: - pub fn last(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.last()) - } - - /// Requires that **any** element matches when the underlying property is list-like. - /// - /// Returns: - /// filter.PropertyFilterOps: - pub fn any(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.any()) - } - - /// Requires that **all** elements match when the underlying property is list-like. - /// - /// Returns: - /// filter.PropertyFilterOps: - pub fn all(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.all()) - } - - /// Returns the list length when the underlying property is list-like. - /// - /// Returns: - /// filter.PropertyFilterOps: - fn len(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.len()) - } - - /// Sums list elements when the underlying property is numeric and list-like. - /// - /// Returns: - /// filter.PropertyFilterOps: - fn sum(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.sum()) - } - - /// Averages list elements when the underlying property is numeric and list-like. - /// - /// Returns: - /// filter.PropertyFilterOps: - fn avg(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.avg()) - } - - /// Returns the minimum list element when the underlying property is list-like. - /// - /// Returns: - /// filter.PropertyFilterOps: - fn min(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.min()) - } - - /// Returns the maximum list element when the underlying property is list-like. - /// - /// Returns: - /// filter.PropertyFilterOps: - fn max(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.max()) - } -} - -/// Builds property filter expressions with access to temporal qualifiers. -/// -/// Exported as: `filter.PropertyFilterOps` -/// -/// This extends `FilterOps` and provides `.temporal()` to explicitly select -/// temporal property evaluation semantics (where supported by the query context). -#[pyclass( - frozen, - name = "PropertyFilterOps", - module = "raphtory.filter", - extends = PyPropertyExprBuilder -)] -pub struct PyPropertyFilterBuilder(pub(crate) Arc); - -impl PyPropertyFilterBuilder { - pub(crate) fn from_arc(inner: Arc) -> Self { - Self(inner) - } -} - -#[pymethods] -impl PyPropertyFilterBuilder { - /// Selects temporal evaluation for the property. - /// - /// Returns: - /// filter.FilterOps: A property expression builder operating on temporal values. - fn temporal(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.temporal()) - } -} - -impl<'py, M: Into + Clone + Send + Sync + 'static> IntoPyObject<'py> - for PropertyFilterBuilder -where - PropertyFilter: CreateFilter + TryAsCompositeFilter, - PropertyExprBuilder: InternalPropertyFilterBuilder, -{ - type Target = PyPropertyFilterBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let inner: Arc> = Arc::new(self); - let child = PyPropertyFilterBuilder::from_arc(inner.clone()); - let parent = PyPropertyExprBuilder::from_arc(inner); - Bound::new(py, (child, parent)) - } -} - -impl<'py, M: Into + Send + Sync + Clone + 'static> IntoPyObject<'py> - for MetadataFilterBuilder -where - PropertyFilter: CreateFilter + TryAsCompositeFilter, - PropertyExprBuilder: InternalPropertyFilterBuilder, -{ - type Target = PyPropertyExprBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - PyPropertyExprBuilder::wrap(self).into_pyobject(py) - } -} - -impl<'py, M> IntoPyObject<'py> for EdgeEndpointWrapper> -where - M: Into + Clone + Send + Sync + 'static, - PropertyFilter: CreateFilter + TryAsCompositeFilter, - PropertyExprBuilder: InternalPropertyFilterBuilder, -{ - type Target = PyPropertyFilterBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let inner: Arc>> = Arc::new(self); - let child = PyPropertyFilterBuilder::from_arc(inner.clone()); - let parent = PyPropertyExprBuilder::from_arc(inner); - Bound::new(py, (child, parent)) - } -} - -impl<'py, M> IntoPyObject<'py> for EdgeEndpointWrapper> -where - M: Into + Clone + Send + Sync + 'static, - PropertyFilter: CreateFilter + TryAsCompositeFilter, - PropertyExprBuilder: InternalPropertyFilterBuilder, -{ - type Target = PyPropertyExprBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let inner: Arc>> = Arc::new(self); - PyPropertyExprBuilder::from_arc(inner).into_pyobject(py) - } -} - -impl<'py> IntoPyObject<'py> for PyPropertyFilterBuilder { - type Target = PyPropertyFilterBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyPropertyExprBuilder::from_arc(self.0.clone()); - Bound::new(py, (self, parent)) - } -} - -/// Builds graph-level view filters. -/// -/// This builder restricts *when* and *where* filters are evaluated in time -/// and across layers. It operates at the **graph view** level and returns -/// new view builders that can be further refined. -/// -/// View filters can be composed before applying node or edge predicates. -/// -/// Examples: -/// Graph.window(0, 10) -/// Graph.at(5) -/// Graph.latest().layer("fire_nation") -#[pyclass( - name = "ViewFilterBuilder", - module = "raphtory.filter", - extends = PyFilterExpr, - frozen -)] -pub struct PyViewFilterBuilder(pub(crate) DynView); - -#[pymethods] -impl PyViewFilterBuilder { - /// Restricts evaluation to events in the given time window. - /// - /// The window is inclusive of `start` and exclusive of `end`. - /// - /// Arguments: - /// start (int): Start time. - /// end (int): End time. - /// - /// Returns: - /// filter.ViewFilterBuilder: - fn window(&self, start: EventTime, end: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(self.0.clone().window(start, end)) - } - - /// Restricts evaluation to a single point in time. - /// - /// Arguments: - /// time (int): Event time. - /// - /// Returns: - /// filter.ViewFilterBuilder: - fn at(&self, time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(self.0.clone().at(time)) - } - - /// Restricts evaluation to times strictly after the given time. - fn after(&self, time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(self.0.clone().after(time)) - } - - /// Restricts evaluation to times strictly before the given time. - fn before(&self, time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(self.0.clone().before(time)) - } - - /// Evaluates against the latest available state. - fn latest(&self) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().latest())) - } - - /// Evaluates against a snapshot of the graph at a specific time. - fn snapshot_at(&self, time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().snapshot_at(time))) - } - - /// Evaluates against the most recent snapshot of the graph. - fn snapshot_latest(&self) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().snapshot_latest())) - } - - /// Restricts evaluation to a single layer. - fn layer(&self, layer: String) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().layer(layer))) - } - - /// Restricts evaluation to any of the given layers. - fn layers(&self, layers: FromIterable) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().layer(layers))) - } -} - -impl<'py> IntoPyObject<'py> for PyViewFilterBuilder { - type Target = PyViewFilterBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyFilterExpr(self.0.clone()); - Bound::new(py, (self, parent)) - } -} - -/// Filters node properties within a restricted graph view. -/// -/// This builder combines **view restrictions** (time, layer, snapshot) -/// with **node-level predicates** such as properties, metadata, and state. -/// -/// Returned expressions evaluate node state *within the active view*. -/// -/// Examples: -/// Node.window(0, 10).property("age") > 18 -#[pyclass( - name = "NodeViewPropsFilterBuilder", - module = "raphtory.filter", - frozen -)] -pub struct PyNodeViewPropsFilterBuilder(pub(crate) DynNodeViewProps); - -#[pymethods] -impl PyNodeViewPropsFilterBuilder { - /// Matches nodes that have at least one event in the current view. - fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_active()) - } - - /// Selects a node property for filtering. - /// - /// Arguments: - /// name (str): Property key. - /// - /// Returns: - /// filter.PropertyFilterOps: - fn property(&self, name: String) -> PyPropertyFilterBuilder { - PyPropertyFilterBuilder(self.0.property(name)) - } - - /// Selects a node metadata field for filtering. - /// - /// Metadata is shared across all temporal versions of a node. - fn metadata(&self, name: String) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.metadata(name)) - } - - /// Restricts evaluation to a time window. - fn window(&self, start: EventTime, end: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(self.0.clone().window(start, end)) - } - - /// Restricts evaluation to a single point in time. - fn at(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(self.0.clone().at(time)) - } - - /// Restricts evaluation to times after the given time. - fn after(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(self.0.clone().after(time)) - } - - /// Restricts evaluation to times before the given time. - fn before(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(self.0.clone().before(time)) - } - - /// Evaluates node properties against the latest state. - fn latest(&self) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().latest())) - } - - /// Evaluates node properties against a snapshot at a given time. - fn snapshot_at(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_at(time))) - } - - /// Evaluates node properties against the most recent snapshot. - fn snapshot_latest(&self) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_latest())) - } - - /// Restricts evaluation to a single layer. - fn layer(&self, layer: String) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layer))) - } - - /// Restricts evaluation to any of the given layers. - fn layers(&self, layers: FromIterable) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layers))) - } -} - -/// Filters edge properties within a restricted graph view. -/// -/// Supports structural predicates (validity, deletion, self-loops), -/// property-based filters, and view restrictions. -/// -/// Examples: -/// Edge.is_valid() -/// Edge.window(0, 10).property("weight") > 0.5 -#[pyclass( - name = "EdgeViewPropsFilterBuilder", - module = "raphtory.filter", - frozen -)] -pub struct PyEdgeViewPropsFilterBuilder(pub(crate) DynEdgeViewProps); - -#[pymethods] -impl PyEdgeViewPropsFilterBuilder { - /// Matches edges active in the current view. - fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_active()) - } - - /// Matches edges that are not deleted. - fn is_valid(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_valid()) - } - - /// Matches edges that have been deleted. - fn is_deleted(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_deleted()) - } - - /// Matches self-loop edges. - fn is_self_loop(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_self_loop()) - } - - /// Selects an edge property for filtering. - fn property(&self, name: String) -> PyPropertyFilterBuilder { - PyPropertyFilterBuilder(self.0.property(name)) - } - - /// Selects an edge metadata field for filtering. - fn metadata(&self, name: String) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.metadata(name)) - } - - /// Restricts evaluation to a time window. - fn window(&self, start: EventTime, end: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(self.0.clone().window(start, end)) - } - - /// Restricts evaluation to a single time. - fn at(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(self.0.clone().at(time)) - } - - /// Restricts evaluation to times after the given time. - fn after(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(self.0.clone().after(time)) - } - - /// Restricts evaluation to times before the given time. - fn before(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(self.0.clone().before(time)) - } - - /// Evaluates against the latest edge state. - fn latest(&self) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().latest())) - } - - /// Evaluates against a snapshot at a given time. - fn snapshot_at(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_at(time))) - } - - /// Evaluates against the most recent snapshot. - fn snapshot_latest(&self) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_latest())) - } - - /// Restricts evaluation to a single layer. - fn layer(&self, layer: String) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layer))) - } - - /// Restricts evaluation to any of the given layers. - fn layers(&self, layers: FromIterable) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layers))) - } -} From 3322b3a4a17d31ef64ffe3b5899b0f2df6f92be6 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:52:46 +0100 Subject: [PATCH 065/155] impl py expr --- .../test_filters/test_node_filter.py | 392 +++++------ raphtory-tests/tests/cached_view.rs | 7 +- raphtory-tests/tests/edge_expr_filter.rs | 655 ++++++++++++++---- raphtory-tests/tests/subgraph_tests.rs | 7 +- raphtory-tests/tests/test_filters.rs | 78 +-- raphtory-tests/tests/test_layers.rs | 6 +- .../tests_node_type_filtered_subgraph.rs | 7 +- raphtory-tests/tests/views_test.rs | 18 +- .../algorithms/components/in_components.rs | 2 - .../algorithms/components/out_components.rs | 2 - .../views/filter/edge_expr_filtered_graph.rs | 19 +- .../exploded_edge_expr_filtered_graph.rs | 136 ++++ raphtory/src/db/graph/views/filter/mod.rs | 16 +- .../db/graph/views/filter/model/and_filter.rs | 30 +- .../graph/views/filter/model/degree_filter.rs | 6 - .../views/filter/model/edge_expr/filters.rs | 158 +++-- .../graph/views/filter/model/edge_expr/mod.rs | 32 +- .../graph/views/filter/model/edge_expr/ops.rs | 28 +- .../graph/views/filter/model/edge_filter.rs | 73 +- .../filter/model/exploded_edge_filter.rs | 10 - .../views/filter/model/filter_operator.rs | 10 +- .../graph/views/filter/model/graph_filter.rs | 6 - .../filter/model/is_active_edge_filter.rs | 6 - .../filter/model/is_active_node_filter.rs | 6 - .../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 | 20 +- .../views/filter/model/layered_filter.rs | 19 +- .../src/db/graph/views/filter/model/mod.rs | 132 ++-- .../views/filter/model/node_expr/dyn_expr.rs | 130 ++++ .../views/filter/model/node_expr/exprs.rs | 286 +++++--- .../views/filter/model/node_expr/filters.rs | 347 ++++++++-- .../graph/views/filter/model/node_expr/mod.rs | 24 +- .../graph/views/filter/model/node_expr/ops.rs | 63 +- .../views/filter/model/node_expr/tests.rs | 11 +- .../views/filter/model/node_filter/mod.rs | 38 +- .../db/graph/views/filter/model/not_filter.rs | 6 - .../db/graph/views/filter/model/or_filter.rs | 29 +- .../views/filter/model/property_filter/mod.rs | 17 - .../views/filter/model/snapshot_filter.rs | 26 +- .../views/filter/model/windowed_filter.rs | 25 +- raphtory/src/errors.rs | 5 +- raphtory/src/lib.rs | 3 +- raphtory/src/python/filter/edge_expr.rs | 2 +- raphtory/src/python/filter/filter_expr.rs | 6 - raphtory/src/python/filter/graph_filter.rs | 99 +-- raphtory/src/python/filter/mod.rs | 16 +- raphtory/src/python/filter/node_expr.rs | 420 ++--------- raphtory/src/python/filter/property_expr.rs | 252 +------ .../python/graph/properties/temporal_props.rs | 2 +- raphtory/src/python/packages/base_modules.rs | 4 +- 52 files changed, 1971 insertions(+), 1739 deletions(-) create mode 100644 raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs create mode 100644 raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index fb32637fe4..b16cdeb764 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -64,42 +64,42 @@ def test_degree_filter_both_direction_comparison(value): assert_filter( graph, - filter.Node.degree() < value, + filter.Node().degree() < value, "both", lambda d: d < value, f"BOTH < {value}", ) assert_filter( graph, - filter.Node.degree() <= value, + filter.Node().degree() <= value, "both", lambda d: d <= value, f"BOTH <= {value}", ) assert_filter( graph, - filter.Node.degree() == value, + filter.Node().degree() == value, "both", lambda d: d == value, f"BOTH == {value}", ) assert_filter( graph, - filter.Node.degree() != value, + filter.Node().degree() != value, "both", lambda d: d != value, f"BOTH != {value}", ) assert_filter( graph, - filter.Node.degree() >= value, + filter.Node().degree() >= value, "both", lambda d: d >= value, f"BOTH >= {value}", ) assert_filter( graph, - filter.Node.degree() > value, + filter.Node().degree() > value, "both", lambda d: d > value, f"BOTH > {value}", @@ -112,42 +112,42 @@ def test_degree_filter_in_direction_comparison(value): assert_filter( graph, - filter.Node.in_degree() < value, + filter.Node().in_degree() < value, "in", lambda d: d < value, f"IN < {value}", ) assert_filter( graph, - filter.Node.in_degree() <= value, + filter.Node().in_degree() <= value, "in", lambda d: d <= value, f"IN <= {value}", ) assert_filter( graph, - filter.Node.in_degree() == value, + filter.Node().in_degree() == value, "in", lambda d: d == value, f"IN == {value}", ) assert_filter( graph, - filter.Node.in_degree() != value, + filter.Node().in_degree() != value, "in", lambda d: d != value, f"IN != {value}", ) assert_filter( graph, - filter.Node.in_degree() >= value, + filter.Node().in_degree() >= value, "in", lambda d: d >= value, f"IN >= {value}", ) assert_filter( graph, - filter.Node.in_degree() > value, + filter.Node().in_degree() > value, "in", lambda d: d > value, f"IN > {value}", @@ -160,42 +160,42 @@ def test_degree_filter_out_direction_comparison(value): assert_filter( graph, - filter.Node.out_degree() < value, + filter.Node().out_degree() < value, "out", lambda d: d < value, f"OUT < {value}", ) assert_filter( graph, - filter.Node.out_degree() <= value, + filter.Node().out_degree() <= value, "out", lambda d: d <= value, f"OUT <= {value}", ) assert_filter( graph, - filter.Node.out_degree() == value, + filter.Node().out_degree() == value, "out", lambda d: d == value, f"OUT == {value}", ) assert_filter( graph, - filter.Node.out_degree() != value, + filter.Node().out_degree() != value, "out", lambda d: d != value, f"OUT != {value}", ) assert_filter( graph, - filter.Node.out_degree() >= value, + filter.Node().out_degree() >= value, "out", lambda d: d >= value, f"OUT >= {value}", ) assert_filter( graph, - filter.Node.out_degree() > value, + filter.Node().out_degree() > value, "out", lambda d: d > value, f"OUT > {value}", @@ -208,21 +208,21 @@ def test_degree_filter_and(value): assert_filter( graph, - (filter.Node.degree() > value) & (filter.Node.degree() < value + 5), + (filter.Node().degree() > value) & (filter.Node().degree() < value + 5), "both", lambda d: d > value and d < (value + 5), f"BOTH > {value} AND BOTH < {value + 5}", ) assert_filter( graph, - (filter.Node.in_degree() > value) & (filter.Node.in_degree() < value + 5), + (filter.Node().in_degree() > value) & (filter.Node().in_degree() < value + 5), "in", lambda d: d > value and d < (value + 5), f"IN > {value} AND IN < {value + 5}", ) assert_filter( graph, - (filter.Node.out_degree() > value) & (filter.Node.out_degree() < value + 5), + (filter.Node().out_degree() > value) & (filter.Node().out_degree() < value + 5), "out", lambda d: d > value and d < (value + 5), f"OUT > {value} AND OUT < {value + 5}", @@ -235,21 +235,21 @@ def test_degree_filter_or(value): assert_filter( graph, - (filter.Node.degree() < value) | (filter.Node.degree() > value + 5), + (filter.Node().degree() < value) | (filter.Node().degree() > value + 5), "both", lambda d: d < value or d > (value + 5), f"BOTH < {value} OR BOTH > {value + 5}", ) assert_filter( graph, - (filter.Node.in_degree() < value) | (filter.Node.in_degree() > value + 5), + (filter.Node().in_degree() < value) | (filter.Node().in_degree() > value + 5), "in", lambda d: d < value or d > (value + 5), f"IN < {value} OR IN > {value + 5}", ) assert_filter( graph, - (filter.Node.out_degree() < value) | (filter.Node.out_degree() > value + 5), + (filter.Node().out_degree() < value) | (filter.Node().out_degree() > value + 5), "out", lambda d: d < value or d > (value + 5), f"OUT < {value} OR OUT > {value + 5}", @@ -262,21 +262,21 @@ def test_degree_filter_not(value): assert_filter( graph, - (filter.Node.degree() < value) | (~(filter.Node.degree() > value + 5)), + (filter.Node().degree() < value) | (~(filter.Node().degree() > value + 5)), "both", lambda d: d < value or d <= (value + 5), f"BOTH < {value} OR BOTH > {value + 5}", ) assert_filter( graph, - (filter.Node.in_degree() < value) | (~(filter.Node.in_degree() > value + 5)), + (filter.Node().in_degree() < value) | (~(filter.Node().in_degree() > value + 5)), "in", lambda d: d < value or d <= (value + 5), f"IN < {value} OR IN > {value + 5}", ) assert_filter( graph, - (filter.Node.out_degree() < value) | (~(filter.Node.out_degree() > value + 5)), + (filter.Node().out_degree() < value) | (~(filter.Node().out_degree() > value + 5)), "out", lambda d: d < value or d <= (value + 5), f"OUT < {value} OR OUT > {value + 5}", @@ -290,21 +290,21 @@ def test_degree_filter_is_in(value): assert_filter( graph, - filter.Node.degree().is_in(set_values), + filter.Node().degree().is_in(set_values), "both", lambda d: d in set_values, f"BOTH is_in({value}, {value + 1})", ) assert_filter( graph, - filter.Node.in_degree().is_in(set_values), + filter.Node().in_degree().is_in(set_values), "in", lambda d: d in set_values, f"IN is_in({value}, {value + 1})", ) assert_filter( graph, - filter.Node.out_degree().is_in(set_values), + filter.Node().out_degree().is_in(set_values), "out", lambda d: d in set_values, f"OUT is_in({value}, {value + 1})", @@ -318,21 +318,21 @@ def test_degree_filter_is_not_in(value): assert_filter( graph, - filter.Node.degree().is_not_in(set_values), + filter.Node().degree().is_not_in(set_values), "both", lambda d: d not in set_values, f"BOTH is_not_in({value}, {value + 1})", ) assert_filter( graph, - filter.Node.in_degree().is_not_in(set_values), + filter.Node().in_degree().is_not_in(set_values), "in", lambda d: d not in set_values, f"IN is_not_in({value}, {value + 1})", ) assert_filter( graph, - filter.Node.out_degree().is_not_in(set_values), + filter.Node().out_degree().is_not_in(set_values), "out", lambda d: d not in set_values, f"OUT is_not_in({value}, {value + 1})", @@ -342,54 +342,54 @@ def test_degree_filter_is_not_in(value): def test_degree_filter_with_invalid_expressions(): graph = degree_graph_with_add_node_and_add_edge(Graph()) invalid_filters = [ - filter.Node.degree().is_none(), - filter.Node.degree().is_some(), - filter.Node.degree().starts_with("1"), - filter.Node.degree().ends_with("1"), - filter.Node.degree().contains("1"), - filter.Node.degree().not_contains("1"), - filter.Node.degree().fuzzy_search("1", 1, False), - filter.Node.in_degree().is_none(), - filter.Node.in_degree().is_some(), - filter.Node.in_degree().starts_with("1"), - filter.Node.in_degree().ends_with("1"), - filter.Node.in_degree().contains("1"), - filter.Node.in_degree().not_contains("1"), - filter.Node.in_degree().fuzzy_search("1", 1, False), - filter.Node.out_degree().is_none(), - filter.Node.out_degree().is_some(), - filter.Node.out_degree().starts_with("1"), - filter.Node.out_degree().ends_with("1"), - filter.Node.out_degree().contains("1"), - filter.Node.out_degree().not_contains("1"), - filter.Node.out_degree().fuzzy_search("1", 1, False), - filter.Node.degree().any() == 1, - filter.Node.degree().all() == 1, - filter.Node.degree().len() > 0, - filter.Node.degree().sum() == 1, - filter.Node.degree().avg() == 1, - filter.Node.degree().min() == 1, - filter.Node.degree().max() == 1, - filter.Node.degree().first() == 1, - filter.Node.degree().last() == 1, - filter.Node.in_degree().any() == 1, - filter.Node.in_degree().all() == 1, - filter.Node.in_degree().len() > 0, - filter.Node.in_degree().sum() == 1, - filter.Node.in_degree().avg() == 1, - filter.Node.in_degree().min() == 1, - filter.Node.in_degree().max() == 1, - filter.Node.in_degree().first() == 1, - filter.Node.in_degree().last() == 1, - filter.Node.out_degree().any() == 1, - filter.Node.out_degree().all() == 1, - filter.Node.out_degree().len() > 0, - filter.Node.out_degree().sum() == 1, - filter.Node.out_degree().avg() == 1, - filter.Node.out_degree().min() == 1, - filter.Node.out_degree().max() == 1, - filter.Node.out_degree().first() == 1, - filter.Node.out_degree().last() == 1, + filter.Node().degree().is_none(), + filter.Node().degree().is_some(), + filter.Node().degree().starts_with("1"), + filter.Node().degree().ends_with("1"), + filter.Node().degree().contains("1"), + filter.Node().degree().not_contains("1"), + filter.Node().degree().fuzzy_search("1", 1, False), + filter.Node().in_degree().is_none(), + filter.Node().in_degree().is_some(), + filter.Node().in_degree().starts_with("1"), + filter.Node().in_degree().ends_with("1"), + filter.Node().in_degree().contains("1"), + filter.Node().in_degree().not_contains("1"), + filter.Node().in_degree().fuzzy_search("1", 1, False), + filter.Node().out_degree().is_none(), + filter.Node().out_degree().is_some(), + filter.Node().out_degree().starts_with("1"), + filter.Node().out_degree().ends_with("1"), + filter.Node().out_degree().contains("1"), + filter.Node().out_degree().not_contains("1"), + filter.Node().out_degree().fuzzy_search("1", 1, False), + filter.Node().degree().any() == 1, + filter.Node().degree().all() == 1, + filter.Node().degree().len() > 0, + filter.Node().degree().sum() == 1, + filter.Node().degree().avg() == 1, + filter.Node().degree().min() == 1, + filter.Node().degree().max() == 1, + filter.Node().degree().first() == 1, + filter.Node().degree().last() == 1, + filter.Node().in_degree().any() == 1, + filter.Node().in_degree().all() == 1, + filter.Node().in_degree().len() > 0, + filter.Node().in_degree().sum() == 1, + filter.Node().in_degree().avg() == 1, + filter.Node().in_degree().min() == 1, + filter.Node().in_degree().max() == 1, + filter.Node().in_degree().first() == 1, + filter.Node().in_degree().last() == 1, + filter.Node().out_degree().any() == 1, + filter.Node().out_degree().all() == 1, + filter.Node().out_degree().len() > 0, + filter.Node().out_degree().sum() == 1, + filter.Node().out_degree().avg() == 1, + filter.Node().out_degree().min() == 1, + filter.Node().out_degree().max() == 1, + filter.Node().out_degree().first() == 1, + filter.Node().out_degree().last() == 1, ] for filter_expr in invalid_filters: @@ -401,30 +401,30 @@ def test_degree_filter_with_invalid_expressions(): def test_degree_filter_with_invalid_string_values(value_a, value_b): graph = degree_graph_with_add_node_and_add_edge(Graph()) invalid_filters = [ - filter.Node.degree() < value_a, - filter.Node.degree() <= value_a, - filter.Node.degree() == value_a, - filter.Node.degree() != value_a, - filter.Node.degree() >= value_a, - filter.Node.degree() > value_a, - filter.Node.in_degree() < value_a, - filter.Node.in_degree() <= value_a, - filter.Node.in_degree() == value_a, - filter.Node.in_degree() != value_a, - filter.Node.in_degree() >= value_a, - filter.Node.in_degree() > value_a, - filter.Node.out_degree() < value_a, - filter.Node.out_degree() <= value_a, - filter.Node.out_degree() == value_a, - filter.Node.out_degree() != value_a, - filter.Node.out_degree() >= value_a, - filter.Node.out_degree() > value_a, - filter.Node.degree().is_in([value_a, value_b]), - filter.Node.degree().is_not_in([value_a, value_b]), - filter.Node.in_degree().is_in([value_a, value_b]), - filter.Node.in_degree().is_not_in([value_a, value_b]), - filter.Node.out_degree().is_in([value_a, value_b]), - filter.Node.out_degree().is_not_in([value_a, value_b]), + filter.Node().degree() < value_a, + filter.Node().degree() <= value_a, + filter.Node().degree() == value_a, + filter.Node().degree() != value_a, + filter.Node().degree() >= value_a, + filter.Node().degree() > value_a, + filter.Node().in_degree() < value_a, + filter.Node().in_degree() <= value_a, + filter.Node().in_degree() == value_a, + filter.Node().in_degree() != value_a, + filter.Node().in_degree() >= value_a, + filter.Node().in_degree() > value_a, + filter.Node().out_degree() < value_a, + filter.Node().out_degree() <= value_a, + filter.Node().out_degree() == value_a, + filter.Node().out_degree() != value_a, + filter.Node().out_degree() >= value_a, + filter.Node().out_degree() > value_a, + filter.Node().degree().is_in([value_a, value_b]), + filter.Node().degree().is_not_in([value_a, value_b]), + filter.Node().in_degree().is_in([value_a, value_b]), + filter.Node().in_degree().is_not_in([value_a, value_b]), + filter.Node().out_degree().is_in([value_a, value_b]), + filter.Node().out_degree().is_not_in([value_a, value_b]), ] for filter_expr in invalid_filters: @@ -440,42 +440,42 @@ def test_degree_filter_with_string_threshold(value): assert_filter( graph, - filter.Node.degree() < threshold_str, + filter.Node().degree() < threshold_str, "both", lambda d: d < parsed_str, f"BOTH < string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.degree() <= threshold_str, + filter.Node().degree() <= threshold_str, "both", lambda d: d <= parsed_str, f"BOTH <= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.degree() == threshold_str, + filter.Node().degree() == threshold_str, "both", lambda d: d == parsed_str, f"BOTH == string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.degree() != threshold_str, + filter.Node().degree() != threshold_str, "both", lambda d: d != parsed_str, f"BOTH != string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.degree() >= threshold_str, + filter.Node().degree() >= threshold_str, "both", lambda d: d >= parsed_str, f"BOTH >= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.degree() > threshold_str, + filter.Node().degree() > threshold_str, "both", lambda d: d > parsed_str, f"BOTH > string threshold parsed to u64 ({threshold_str})", @@ -483,42 +483,42 @@ def test_degree_filter_with_string_threshold(value): assert_filter( graph, - filter.Node.in_degree() < threshold_str, + filter.Node().in_degree() < threshold_str, "in", lambda d: d < parsed_str, f"IN < string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.in_degree() <= threshold_str, + filter.Node().in_degree() <= threshold_str, "in", lambda d: d <= parsed_str, f"IN <= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.in_degree() == threshold_str, + filter.Node().in_degree() == threshold_str, "in", lambda d: d == parsed_str, f"IN == string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.in_degree() != threshold_str, + filter.Node().in_degree() != threshold_str, "in", lambda d: d != parsed_str, f"IN != string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.in_degree() >= threshold_str, + filter.Node().in_degree() >= threshold_str, "in", lambda d: d >= parsed_str, f"IN >= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.in_degree() > threshold_str, + filter.Node().in_degree() > threshold_str, "in", lambda d: d > parsed_str, f"IN > string threshold parsed to u64 ({threshold_str})", @@ -526,42 +526,42 @@ def test_degree_filter_with_string_threshold(value): assert_filter( graph, - filter.Node.out_degree() < threshold_str, + filter.Node().out_degree() < threshold_str, "out", lambda d: d < parsed_str, f"OUT < string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.out_degree() <= threshold_str, + filter.Node().out_degree() <= threshold_str, "out", lambda d: d <= parsed_str, f"OUT <= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.out_degree() == threshold_str, + filter.Node().out_degree() == threshold_str, "out", lambda d: d == parsed_str, f"OUT == string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.out_degree() != threshold_str, + filter.Node().out_degree() != threshold_str, "out", lambda d: d != parsed_str, f"OUT != string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.out_degree() >= threshold_str, + filter.Node().out_degree() >= threshold_str, "out", lambda d: d >= parsed_str, f"OUT >= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node.out_degree() > threshold_str, + filter.Node().out_degree() > threshold_str, "out", lambda d: d > parsed_str, f"OUT > string threshold parsed to u64 ({threshold_str})", @@ -577,21 +577,21 @@ def test_degree_filter_with_string_is_in(value): assert_filter( graph, - filter.Node.degree().is_in([threshold_a_str, threshold_b_str]), + filter.Node().degree().is_in([threshold_a_str, threshold_b_str]), "both", lambda d: d in set_values, f"BOTH is_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", ) assert_filter( graph, - filter.Node.in_degree().is_in([threshold_a_str, threshold_b_str]), + filter.Node().in_degree().is_in([threshold_a_str, threshold_b_str]), "in", lambda d: d in set_values, f"IN is_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", ) assert_filter( graph, - filter.Node.out_degree().is_in([threshold_a_str, threshold_b_str]), + filter.Node().out_degree().is_in([threshold_a_str, threshold_b_str]), "out", lambda d: d in set_values, f"OUT is_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", @@ -607,21 +607,21 @@ def test_degree_filter_with_string_is_not_in(value): assert_filter( graph, - filter.Node.degree().is_not_in([threshold_a_str, threshold_b_str]), + filter.Node().degree().is_not_in([threshold_a_str, threshold_b_str]), "both", lambda d: d not in set_values, f"BOTH is_not_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", ) assert_filter( graph, - filter.Node.in_degree().is_not_in([threshold_a_str, threshold_b_str]), + filter.Node().in_degree().is_not_in([threshold_a_str, threshold_b_str]), "in", lambda d: d not in set_values, f"IN is_not_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", ) assert_filter( graph, - filter.Node.out_degree().is_not_in([threshold_a_str, threshold_b_str]), + filter.Node().out_degree().is_not_in([threshold_a_str, threshold_b_str]), "out", lambda d: d not in set_values, f"OUT is_not_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", @@ -636,42 +636,42 @@ def test_degree_filter_with_float_threshold(value): assert_filter( graph, - filter.Node.degree() < threshold_float, + filter.Node().degree() < threshold_float, "both", lambda d: d < parsed_float, f"BOTH < float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.degree() <= threshold_float, + filter.Node().degree() <= threshold_float, "both", lambda d: d <= parsed_float, f"BOTH <= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.degree() == threshold_float, + filter.Node().degree() == threshold_float, "both", lambda d: d == parsed_float, f"BOTH == float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.degree() != threshold_float, + filter.Node().degree() != threshold_float, "both", lambda d: d != parsed_float, f"BOTH != float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.degree() >= threshold_float, + filter.Node().degree() >= threshold_float, "both", lambda d: d >= parsed_float, f"BOTH >= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.degree() > threshold_float, + filter.Node().degree() > threshold_float, "both", lambda d: d > parsed_float, f"BOTH > float threshold cast to u64 ({value})", @@ -679,42 +679,42 @@ def test_degree_filter_with_float_threshold(value): assert_filter( graph, - filter.Node.in_degree() < threshold_float, + filter.Node().in_degree() < threshold_float, "in", lambda d: d < parsed_float, f"IN < float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.in_degree() <= threshold_float, + filter.Node().in_degree() <= threshold_float, "in", lambda d: d <= parsed_float, f"IN <= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.in_degree() == threshold_float, + filter.Node().in_degree() == threshold_float, "in", lambda d: d == parsed_float, f"IN == float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.in_degree() != threshold_float, + filter.Node().in_degree() != threshold_float, "in", lambda d: d != parsed_float, f"IN != float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.in_degree() >= threshold_float, + filter.Node().in_degree() >= threshold_float, "in", lambda d: d >= parsed_float, f"IN >= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.in_degree() > threshold_float, + filter.Node().in_degree() > threshold_float, "in", lambda d: d > parsed_float, f"IN > float threshold cast to u64 ({value})", @@ -722,42 +722,42 @@ def test_degree_filter_with_float_threshold(value): assert_filter( graph, - filter.Node.out_degree() < threshold_float, + filter.Node().out_degree() < threshold_float, "out", lambda d: d < parsed_float, f"OUT < float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.out_degree() <= threshold_float, + filter.Node().out_degree() <= threshold_float, "out", lambda d: d <= parsed_float, f"OUT <= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.out_degree() == threshold_float, + filter.Node().out_degree() == threshold_float, "out", lambda d: d == parsed_float, f"OUT == float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.out_degree() != threshold_float, + filter.Node().out_degree() != threshold_float, "out", lambda d: d != parsed_float, f"OUT != float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.out_degree() >= threshold_float, + filter.Node().out_degree() >= threshold_float, "out", lambda d: d >= parsed_float, f"OUT >= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node.out_degree() > threshold_float, + filter.Node().out_degree() > threshold_float, "out", lambda d: d > parsed_float, f"OUT > float threshold cast to u64 ({value})", @@ -773,21 +773,21 @@ def test_degree_filter_with_float_is_in(value): assert_filter( graph, - filter.Node.degree().is_in([threshold_a, threshold_b]), + filter.Node().degree().is_in([threshold_a, threshold_b]), "both", lambda d: d in set_values, f"BOTH is_in(float thresholds cast to u64) ({value}, {value + 1})", ) assert_filter( graph, - filter.Node.in_degree().is_in([threshold_a, threshold_b]), + filter.Node().in_degree().is_in([threshold_a, threshold_b]), "in", lambda d: d in set_values, f"IN is_in(float thresholds cast to u64) ({value}, {value + 1})", ) assert_filter( graph, - filter.Node.out_degree().is_in([threshold_a, threshold_b]), + filter.Node().out_degree().is_in([threshold_a, threshold_b]), "out", lambda d: d in set_values, f"OUT is_in(float thresholds cast to u64) ({value}, {value + 1})", @@ -803,21 +803,21 @@ def test_degree_filter_with_float_is_not_in(value): assert_filter( graph, - filter.Node.degree().is_not_in([threshold_a, threshold_b]), + filter.Node().degree().is_not_in([threshold_a, threshold_b]), "both", lambda d: d not in set_values, f"BOTH is_not_in(float thresholds cast to u64) ({value}, {value + 1})", ) assert_filter( graph, - filter.Node.in_degree().is_not_in([threshold_a, threshold_b]), + filter.Node().in_degree().is_not_in([threshold_a, threshold_b]), "in", lambda d: d not in set_values, f"IN is_not_in(float thresholds cast to u64) ({value}, {value + 1})", ) assert_filter( graph, - filter.Node.out_degree().is_not_in([threshold_a, threshold_b]), + filter.Node().out_degree().is_not_in([threshold_a, threshold_b]), "out", lambda d: d not in set_values, f"OUT is_not_in(float thresholds cast to u64) ({value}, {value + 1})", @@ -827,7 +827,7 @@ def test_degree_filter_with_float_is_not_in(value): @with_variants(init_graph) def test_filter_nodes_for_node_name_eq(): def check(graph): - filter_expr = filter.Node.name() == "3" + filter_expr = filter.Node().name() == "3" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["3"] assert result_ids == expected_ids @@ -838,7 +838,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_name_ne(): def check(graph): - filter_expr = filter.Node.name() != "2" + filter_expr = filter.Node().name() != "2" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -849,12 +849,12 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_name_in(): def check(graph): - filter_expr = filter.Node.name().is_in(["1"]) + filter_expr = filter.Node().name().is_in(["1"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1"] assert result_ids == expected_ids - filter_expr = filter.Node.name().is_in(["2", "3"]) + filter_expr = filter.Node().name().is_in(["2", "3"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "3"] assert result_ids == expected_ids @@ -865,7 +865,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_name_not_in(): def check(graph): - filter_expr = filter.Node.name().is_not_in(["1"]) + filter_expr = filter.Node().name().is_not_in(["1"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -876,7 +876,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_eq(): def check(graph): - filter_expr = filter.Node.node_type() == "fire_nation" + filter_expr = filter.Node().node_type() == "fire_nation" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids @@ -887,7 +887,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_ne(): def check(graph): - filter_expr = filter.Node.node_type() != "fire_nation" + filter_expr = filter.Node().node_type() != "fire_nation" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -898,12 +898,12 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_in(): def check(graph): - filter_expr = filter.Node.node_type().is_in(["fire_nation"]) + filter_expr = filter.Node().node_type().is_in(["fire_nation"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.node_type().is_in(["fire_nation", "air_nomads"]) + filter_expr = filter.Node().node_type().is_in(["fire_nation", "air_nomads"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "2", "3"] assert result_ids == expected_ids @@ -914,7 +914,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_not_in(): def check(graph): - filter_expr = filter.Node.node_type().is_not_in(["fire_nation"]) + filter_expr = filter.Node().node_type().is_not_in(["fire_nation"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -925,12 +925,12 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_starts_with(): def check(graph): - filter_expr = filter.Node.node_type().starts_with("fire") + filter_expr = filter.Node().node_type().starts_with("fire") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.node_type().starts_with("Liar") + filter_expr = filter.Node().node_type().starts_with("Liar") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids @@ -941,12 +941,12 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_ends_with(): def check(graph): - filter_expr = filter.Node.node_type().ends_with("tion") + filter_expr = filter.Node().node_type().ends_with("tion") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.node_type().ends_with("station") + filter_expr = filter.Node().node_type().ends_with("station") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids @@ -957,7 +957,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_contains(): def check(graph): - filter_expr = filter.Node.node_type().contains("fire") + filter_expr = filter.Node().node_type().contains("fire") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids @@ -968,7 +968,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_not_contains(): def check(graph): - filter_expr = filter.Node.node_type().not_contains("fire") + filter_expr = filter.Node().node_type().not_contains("fire") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -979,17 +979,17 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_fuzzy_search(): def check(graph): - filter_expr = filter.Node.node_type().fuzzy_search("fire", 2, True) + filter_expr = filter.Node().node_type().fuzzy_search("fire", 2, True) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.node_type().fuzzy_search("fire", 2, False) + filter_expr = filter.Node().node_type().fuzzy_search("fire", 2, False) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids - filter_expr = filter.Node.node_type().fuzzy_search("air_noma", 2, False) + filter_expr = filter.Node().node_type().fuzzy_search("air_noma", 2, False) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2"] assert result_ids == expected_ids @@ -1000,7 +1000,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_not_node_type(): def check(graph): - filter_expr = filter.Node.node_type().is_not_in(["fire_nation"]) + filter_expr = filter.Node().node_type().is_not_in(["fire_nation"]) result_ids = sorted(graph.filter(~filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids @@ -1011,7 +1011,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_for_node_id_eq(): def check(graph): - filter_expr = filter.Node.id() == "3" + filter_expr = filter.Node().id() == "3" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["3"] assert result_ids == expected_ids @@ -1022,7 +1022,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_for_node_id_eq(): def check(graph): - filter_expr = filter.Node.id() == 3 + filter_expr = filter.Node().id() == 3 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [3] assert result_ids == expected_ids @@ -1033,7 +1033,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_for_node_id_ne(): def check(graph): - filter_expr = filter.Node.id() != "3" + filter_expr = filter.Node().id() != "3" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -1044,7 +1044,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_for_node_id_ne(): def check(graph): - filter_expr = filter.Node.id() != 3 + filter_expr = filter.Node().id() != 3 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [1, 2, 4] assert result_ids == expected_ids @@ -1055,7 +1055,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_for_node_id_is_in(): def check(graph): - filter_expr = filter.Node.id().is_in(["1"]) + filter_expr = filter.Node().id().is_in(["1"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1"] assert result_ids == expected_ids @@ -1066,7 +1066,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_for_node_id_is_in(): def check(graph): - filter_expr = filter.Node.id().is_in([1]) + filter_expr = filter.Node().id().is_in([1]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [1] assert result_ids == expected_ids @@ -1077,7 +1077,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_for_node_id_is_not_in(): def check(graph): - filter_expr = filter.Node.id().is_not_in(["1"]) + filter_expr = filter.Node().id().is_not_in(["1"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -1088,7 +1088,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_for_node_id_is_not_in(): def check(graph): - filter_expr = filter.Node.id().is_not_in([1]) + filter_expr = filter.Node().id().is_not_in([1]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [2, 3, 4] assert result_ids == expected_ids @@ -1099,7 +1099,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_error(): def check(graph): - filter_expr = filter.Node.id() == 3 + filter_expr = filter.Node().id() == 3 with pytest.raises( Exception, match='Invalid filter: Filter value type does not match node ID type. Expected Str but got "U64"', @@ -1112,7 +1112,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_error(): def check(graph): - filter_expr = filter.Node.id() == "3" + filter_expr = filter.Node().id() == "3" with pytest.raises( Exception, match='Invalid filter: Filter value type does not match node ID type. Expected U64 but got "Str"', @@ -1125,7 +1125,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_is_active(): def check(graph): - filter_expr = filter.Node.is_active() + filter_expr = filter.Node().is_active() result_ids = sorted(graph.window(1, 4).filter(filter_expr).nodes.id) expected_ids = sorted(["1", "2", "3", "4"]) assert result_ids == expected_ids @@ -1136,7 +1136,7 @@ def check(graph): @with_variants(init_graph) def test_select_nodes_is_active(): def check(graph): - filter_expr = filter.Node.is_active() + filter_expr = filter.Node().is_active() result_ids = sorted(graph.window(1, 4).nodes[filter_expr].id) expected_ids = sorted(["1", "2", "3", "4"]) assert result_ids == expected_ids @@ -1147,7 +1147,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_windowed_is_active(): def check(graph): - filter_expr = filter.Node.window(1, 2).is_active() + filter_expr = filter.Node().window(1, 2).is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(["1", "2"]) assert result_ids == expected_ids @@ -1158,7 +1158,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_windowed_is_active_not(): def check(graph): - filter_expr = filter.Node.window(1, 2).is_active() + filter_expr = filter.Node().window(1, 2).is_active() result_ids = sorted(graph.filter(~filter_expr).nodes.id) expected_ids = sorted([]) assert result_ids == expected_ids @@ -1169,7 +1169,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_latest_is_active(): def check(graph): - filter_expr = filter.Node.latest().is_active() + filter_expr = filter.Node().latest().is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(["1", "4", "David Gilmour", "Jimmy Page", "John Mayer"]) assert result_ids == expected_ids @@ -1180,7 +1180,7 @@ def check(graph): @with_variants(init_graph) def test_select_nodes_latest_is_active(): def check(graph): - filter_expr = filter.Node.latest().is_active() + filter_expr = filter.Node().latest().is_active() result_ids = sorted(graph.nodes[filter_expr].id) expected_ids = sorted(["1", "4", "David Gilmour", "Jimmy Page", "John Mayer"]) assert result_ids == expected_ids @@ -1191,7 +1191,7 @@ def check(graph): @with_variants(init_graph, variants=["graph"]) def test_filter_nodes_snapshot_latest_is_active(): def check(graph): - filter_expr = filter.Node.snapshot_latest().is_active() + filter_expr = filter.Node().snapshot_latest().is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted( ["1", "2", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] @@ -1204,7 +1204,7 @@ def check(graph): @with_variants(init_graph, variants=["persistent_graph"]) def test_filter_nodes_snapshot_latest_is_active_persistent(): def check(graph): - filter_expr = filter.Node.snapshot_latest().is_active() + filter_expr = filter.Node().snapshot_latest().is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(["1", "4", "David Gilmour", "Jimmy Page", "John Mayer"]) assert result_ids == expected_ids @@ -1215,7 +1215,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_at_is_active(): def check(graph): - filter_expr = filter.Node.at(2).is_active() + filter_expr = filter.Node().at(2).is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(["1", "2", "3"]) assert result_ids == expected_ids @@ -1226,7 +1226,7 @@ def check(graph): @with_variants(init_graph) def test_select_nodes_at_is_active(): def check(graph): - filter_expr = filter.Node.at(2).is_active() + filter_expr = filter.Node().at(2).is_active() result_ids = sorted(graph.nodes[filter_expr].id) expected_ids = sorted(["1", "2", "3"]) assert result_ids == expected_ids @@ -1260,7 +1260,7 @@ def test_filter_nodes_by_column(): actual = alternating_mask(graph) assert actual == expected - filter_expr = filter.Node.by_state_column(actual, "bool_col") + filter_expr = filter.Node().by_state_column(actual, "bool_col") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(i for i, v in expected.items() if v["bool_col"]) assert result_ids == expected_ids @@ -1273,7 +1273,7 @@ def test_filter_nodes_by_column(): def test_filter_nodes_for_node_name_all_is_invalid(): def check(graph): with pytest.raises(AttributeError, match=r"has no attribute 'all'"): - filter.Node.name().all() + filter.Node().name().all() return check @@ -1282,6 +1282,6 @@ def check(graph): def test_filter_nodes_for_node_name_len_is_invalid(): def check(graph): with pytest.raises(AttributeError, match=r"has no attribute 'len'"): - filter.Node.name().len() + filter.Node().name().len() return check diff --git a/raphtory-tests/tests/cached_view.rs b/raphtory-tests/tests/cached_view.rs index 79e4cf34a4..daea40b07b 100644 --- a/raphtory-tests/tests/cached_view.rs +++ b/raphtory-tests/tests/cached_view.rs @@ -170,8 +170,7 @@ mod test_filters_cached_view { }; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ - assert_filter_nodes_results, TestGraphVariants, - TestVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; use crate::test_filters_cached_view::{ @@ -252,9 +251,7 @@ mod test_filters_cached_view { prelude::{AdditionOps, EdgeFilter}, }; use raphtory_api::core::entities::properties::prop::Prop; - use raphtory_tests::assertions::{ - assert_filter_edges_results, TestVariants, - }; + use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; use crate::test_filters_cached_view::{ CachedGraphTransformer, WindowedCachedGraphTransformer, diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs index d10f14dd8f..6e4e2c418b 100644 --- a/raphtory-tests/tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -20,13 +20,18 @@ fn sorted_edges(g: impl GraphViewOps<'static>) -> Vec { fn test_edge_temporal_len_gt() { let g = Graph::new(); // A->B gets 3 temporal updates for "score" - g.add_edge(1, "A", "B", [("score", Prop::I64(10))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(20))], None).unwrap(); - g.add_edge(3, "A", "B", [("score", Prop::I64(30))], None).unwrap(); + g.add_edge(1, "A", "B", [("score", Prop::I64(10))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(20))], None) + .unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(30))], None) + .unwrap(); // C->D gets 1 temporal update for "score" - g.add_edge(1, "C", "D", [("score", Prop::I64(10))], None).unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(10))], None) + .unwrap(); // E->F gets no "score" update (zero temporal values) - g.add_edge(1, "E", "F", [("other", Prop::I64(1))], None).unwrap(); + g.add_edge(1, "E", "F", [("other", Prop::I64(1))], None) + .unwrap(); let filter = EdgeFilter.property("score").temporal().len().gt(1usize); let result = g.filter(filter).unwrap(); @@ -36,10 +41,14 @@ fn test_edge_temporal_len_gt() { #[test] fn test_edge_temporal_len_eq() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(10))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(20))], None).unwrap(); - g.add_edge(3, "A", "B", [("score", Prop::I64(30))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(10))], None).unwrap(); + g.add_edge(1, "A", "B", [("score", Prop::I64(10))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(20))], None) + .unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(30))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(10))], None) + .unwrap(); // exactly 1 temporal update let filter = EdgeFilter.property("score").temporal().len().eq(1usize); @@ -50,11 +59,16 @@ fn test_edge_temporal_len_eq() { #[test] fn test_edge_temporal_len_lt() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); - g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "C", "D", [("score", Prop::I64(2))], None).unwrap(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None) + .unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None) + .unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::I64(2))], None) + .unwrap(); // fewer than 3 updates let filter = EdgeFilter.property("score").temporal().len().lt(3usize); @@ -65,11 +79,16 @@ fn test_edge_temporal_len_lt() { #[test] fn test_edge_temporal_len_le() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); - g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "C", "D", [("score", Prop::I64(2))], None).unwrap(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None) + .unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None) + .unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::I64(2))], None) + .unwrap(); // at most 2 updates let filter = EdgeFilter.property("score").temporal().len().le(2usize); @@ -80,10 +99,14 @@ fn test_edge_temporal_len_le() { #[test] fn test_edge_temporal_len_ge() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); - g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None) + .unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None) + .unwrap(); // at least 2 updates let filter = EdgeFilter.property("score").temporal().len().ge(2usize); @@ -94,10 +117,14 @@ fn test_edge_temporal_len_ge() { #[test] fn test_edge_temporal_len_ne() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); - g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None) + .unwrap(); + g.add_edge(3, "A", "B", [("score", Prop::I64(3))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None) + .unwrap(); // not exactly 1 update let filter = EdgeFilter.property("score").temporal().len().ne(1usize); @@ -108,11 +135,16 @@ fn test_edge_temporal_len_ne() { #[test] fn test_edge_temporal_len_combined_with_and() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None).unwrap(); - g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None).unwrap(); - g.add_edge(2, "C", "D", [("score", Prop::I64(2))], None).unwrap(); - g.add_edge(3, "C", "D", [("score", Prop::I64(3))], None).unwrap(); + g.add_edge(1, "A", "B", [("score", Prop::I64(1))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("score", Prop::I64(2))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("score", Prop::I64(1))], None) + .unwrap(); + g.add_edge(2, "C", "D", [("score", Prop::I64(2))], None) + .unwrap(); + g.add_edge(3, "C", "D", [("score", Prop::I64(3))], None) + .unwrap(); // both have >= 2 updates; only A->B has exactly 2 let filter = EdgeFilter @@ -131,9 +163,24 @@ fn test_edge_temporal_len_combined_with_and() { fn band_graph() -> Graph { let g = Graph::new(); - g.add_edge(1, "Jimi", "John", [("band", Prop::str("Pink Floyd"))], None).unwrap(); - g.add_edge(1, "John", "David", [("band", Prop::str("Led Zeppelin"))], None).unwrap(); - g.add_edge(1, "David", "Robert", [("band", Prop::str("Deep Purple"))], None).unwrap(); + g.add_edge(1, "Jimi", "John", [("band", Prop::str("Pink Floyd"))], None) + .unwrap(); + g.add_edge( + 1, + "John", + "David", + [("band", Prop::str("Led Zeppelin"))], + None, + ) + .unwrap(); + g.add_edge( + 1, + "David", + "Robert", + [("band", Prop::str("Deep Purple"))], + None, + ) + .unwrap(); g } @@ -141,9 +188,7 @@ fn band_graph() -> Graph { fn test_edge_property_contains_via_expr_filter_ops() { let g = band_graph(); // generic form: PropertyExpr.contains(Prop::Str(...)) - let filter = EdgeFilter - .property("band") - .contains(Prop::str("Floyd")); + let filter = EdgeFilter.property("band").contains(Prop::str("Floyd")); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["Jimi->John"]); } @@ -151,9 +196,7 @@ fn test_edge_property_contains_via_expr_filter_ops() { #[test] fn test_edge_property_not_contains_via_expr_filter_ops() { let g = band_graph(); - let filter = EdgeFilter - .property("band") - .not_contains(Prop::str("Floyd")); + let filter = EdgeFilter.property("band").not_contains(Prop::str("Floyd")); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["David->Robert", "John->David"]); } @@ -161,9 +204,7 @@ fn test_edge_property_not_contains_via_expr_filter_ops() { #[test] fn test_edge_property_starts_with_via_expr_filter_ops() { let g = band_graph(); - let filter = EdgeFilter - .property("band") - .starts_with(Prop::str("Pink")); + let filter = EdgeFilter.property("band").starts_with(Prop::str("Pink")); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["Jimi->John"]); } @@ -171,9 +212,7 @@ fn test_edge_property_starts_with_via_expr_filter_ops() { #[test] fn test_edge_property_ends_with_via_expr_filter_ops() { let g = band_graph(); - let filter = EdgeFilter - .property("band") - .ends_with(Prop::str("Zeppelin")); + let filter = EdgeFilter.property("band").ends_with(Prop::str("Zeppelin")); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["John->David"]); } @@ -184,8 +223,10 @@ fn test_edge_property_fuzzy_search_via_expr_filter_ops() { // Use short values so whole-string Levenshtein is meaningful: // "Floyd" vs "Floid" = 1 substitution (y→i) // "Zeppelin" is much farther away - g.add_edge(1, "A", "B", [("tag", Prop::str("Floyd"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("Zeppelin"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("Floyd"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("Zeppelin"))], None) + .unwrap(); let filter = EdgeFilter .property("tag") @@ -197,12 +238,19 @@ fn test_edge_property_fuzzy_search_via_expr_filter_ops() { #[test] fn test_edge_aggregated_last_contains_str_convenience() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); // last temporal value of "tag": A->B = "metal", C->D = "jazz" - let filter = EdgeFilter.property("tag").temporal().last().contains("etal"); + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .contains("etal"); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -210,12 +258,19 @@ fn test_edge_aggregated_last_contains_str_convenience() { #[test] fn test_edge_aggregated_first_starts_with_str_convenience() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); // first temporal value: A->B = "rock", C->D = "jazz" - let filter = EdgeFilter.property("tag").temporal().first().starts_with("ro"); + let filter = EdgeFilter + .property("tag") + .temporal() + .first() + .starts_with("ro"); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -229,10 +284,9 @@ fn test_edge_aggregated_first_starts_with_str_convenience() { fn test_edge_property_is_in_prop_values() { // Path A: EdgePropertyExprOps::is_in — PropValueSetExpr let g = band_graph(); - let filter = EdgeFilter.property("band").is_in([ - Prop::str("Pink Floyd"), - Prop::str("Deep Purple"), - ]); + let filter = EdgeFilter + .property("band") + .is_in([Prop::str("Pink Floyd"), Prop::str("Deep Purple")]); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["David->Robert", "Jimi->John"]); } @@ -240,10 +294,9 @@ fn test_edge_property_is_in_prop_values() { #[test] fn test_edge_property_is_not_in_prop_values() { let g = band_graph(); - let filter = EdgeFilter.property("band").is_not_in([ - Prop::str("Pink Floyd"), - Prop::str("Deep Purple"), - ]); + let filter = EdgeFilter + .property("band") + .is_not_in([Prop::str("Pink Floyd"), Prop::str("Deep Purple")]); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["John->David"]); } @@ -252,9 +305,12 @@ fn test_edge_property_is_not_in_prop_values() { fn test_edge_aggregated_last_is_in_prop_values() { // Path A via EdgeAggregated convenience let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); // last value: A->B = "metal", C->D = "jazz" let filter = EdgeFilter @@ -269,9 +325,12 @@ fn test_edge_aggregated_last_is_in_prop_values() { #[test] fn test_edge_aggregated_last_is_not_in_prop_values() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); let filter = EdgeFilter .property("tag") @@ -289,12 +348,17 @@ fn test_edge_aggregated_last_is_not_in_prop_values() { fn genre_graph() -> Graph { let g = Graph::new(); // A->B has tags: "rock", "metal", "rock-n-roll" (3 updates) - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(3, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None) + .unwrap(); + g.add_edge(3, "A", "B", [("tag", Prop::str("rock-n-roll"))], None) + .unwrap(); // C->D has tags: "jazz", "blues" (2 updates) - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); - g.add_edge(2, "C", "D", [("tag", Prop::str("blues"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("blues"))], None) + .unwrap(); g } @@ -311,7 +375,11 @@ fn test_edge_quantified_any_contains() { #[test] fn test_edge_quantified_any_starts_with() { let g = genre_graph(); - let filter = EdgeFilter.property("tag").temporal().starts_with("rock").any(); + let filter = EdgeFilter + .property("tag") + .temporal() + .starts_with("rock") + .any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -319,7 +387,11 @@ fn test_edge_quantified_any_starts_with() { #[test] fn test_edge_quantified_any_ends_with() { let g = genre_graph(); - let filter = EdgeFilter.property("tag").temporal().ends_with("roll").any(); + let filter = EdgeFilter + .property("tag") + .temporal() + .ends_with("roll") + .any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -331,7 +403,11 @@ fn test_edge_quantified_any_not_contains() { // A->B: "metal" and "rock-n-roll" don't, but "rock" does → any not_contains is true for A->B // C->D: "jazz" and "blues" don't contain "rock" → any not_contains is true for C->D // Both edges pass (any value doesn't contain "rock") - let filter = EdgeFilter.property("tag").temporal().not_contains("rock").any(); + let filter = EdgeFilter + .property("tag") + .temporal() + .not_contains("rock") + .any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B", "C->D"]); } @@ -340,11 +416,15 @@ fn test_edge_quantified_any_not_contains() { fn test_edge_quantified_all_contains() { let g = Graph::new(); // A->B: all tags contain "rock" - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("rock-n-roll"))], None) + .unwrap(); // C->D: not all tags contain "rock" - g.add_edge(1, "C", "D", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); let filter = EdgeFilter.property("tag").temporal().contains("rock").all(); let result = g.filter(filter).unwrap(); @@ -354,12 +434,20 @@ fn test_edge_quantified_all_contains() { #[test] fn test_edge_quantified_all_starts_with() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); - g.add_edge(2, "C", "D", [("tag", Prop::str("rock-steady"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("rock-n-roll"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("rock-steady"))], None) + .unwrap(); - let filter = EdgeFilter.property("tag").temporal().starts_with("rock").all(); + let filter = EdgeFilter + .property("tag") + .temporal() + .starts_with("rock") + .all(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -400,10 +488,14 @@ fn test_edge_quantified_any_is_not_in() { #[test] fn test_edge_quantified_all_is_in() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); - g.add_edge(2, "C", "D", [("tag", Prop::str("metal"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("metal"))], None) + .unwrap(); // all temporal values in {"rock", "metal"} let filter = EdgeFilter @@ -419,10 +511,14 @@ fn test_edge_quantified_all_is_in() { #[test] fn test_edge_quantified_all_is_not_in() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); - g.add_edge(2, "C", "D", [("tag", Prop::str("blues"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); + g.add_edge(2, "C", "D", [("tag", Prop::str("blues"))], None) + .unwrap(); // all temporal values are NOT in {"rock", "metal"} let filter = EdgeFilter @@ -445,12 +541,44 @@ fn test_edge_aggregated_last_then_sum() { // .last() picks the last snapshot's list, .sum() reduces it to a scalar. let g = Graph::new(); // A->B: last snapshot = [3,4,5], sum = 12 (> 10) - g.add_edge(1, "A", "B", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2)].into()))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::List(vec![Prop::I64(3), Prop::I64(4), Prop::I64(5)].into()))], None).unwrap(); + g.add_edge( + 1, + "A", + "B", + [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2)].into()))], + None, + ) + .unwrap(); + g.add_edge( + 2, + "A", + "B", + [( + "score", + Prop::List(vec![Prop::I64(3), Prop::I64(4), Prop::I64(5)].into()), + )], + None, + ) + .unwrap(); // C->D: last (and only) snapshot = [1,2,3], sum = 6 (not > 10) - g.add_edge(1, "C", "D", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2), Prop::I64(3)].into()))], None).unwrap(); + g.add_edge( + 1, + "C", + "D", + [( + "score", + Prop::List(vec![Prop::I64(1), Prop::I64(2), Prop::I64(3)].into()), + )], + None, + ) + .unwrap(); - let filter = EdgeFilter.property("score").temporal().last().sum().gt(10i64); + let filter = EdgeFilter + .property("score") + .temporal() + .last() + .sum() + .gt(10i64); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -458,13 +586,20 @@ fn test_edge_aggregated_last_then_sum() { #[test] fn test_edge_aggregated_last_then_contains() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); // last value: A->B = "metal", C->D = "jazz" // re-chain: last().contains("metal") → A->B passes - let filter = EdgeFilter.property("tag").temporal().last().contains("metal"); + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .contains("metal"); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -472,12 +607,19 @@ fn test_edge_aggregated_last_then_contains() { #[test] fn test_edge_aggregated_first_then_ends_with() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock-n-roll"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("jazz"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("blues"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock-n-roll"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("jazz"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("blues"))], None) + .unwrap(); // first value: A->B = "rock-n-roll", C->D = "blues" - let filter = EdgeFilter.property("tag").temporal().first().ends_with("roll"); + let filter = EdgeFilter + .property("tag") + .temporal() + .first() + .ends_with("roll"); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -488,12 +630,44 @@ fn test_edge_aggregated_last_then_len() { // .last() picks the last snapshot's list, .len() returns its length. let g = Graph::new(); // A->B: last snapshot = [20, 30], len = 2 - g.add_edge(1, "A", "B", [("score", Prop::List(vec![Prop::I64(10)].into()))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::List(vec![Prop::I64(20), Prop::I64(30)].into()))], None).unwrap(); + g.add_edge( + 1, + "A", + "B", + [("score", Prop::List(vec![Prop::I64(10)].into()))], + None, + ) + .unwrap(); + g.add_edge( + 2, + "A", + "B", + [( + "score", + Prop::List(vec![Prop::I64(20), Prop::I64(30)].into()), + )], + None, + ) + .unwrap(); // C->D: last snapshot = [5, 10, 15], len = 3 - g.add_edge(1, "C", "D", [("score", Prop::List(vec![Prop::I64(5), Prop::I64(10), Prop::I64(15)].into()))], None).unwrap(); + g.add_edge( + 1, + "C", + "D", + [( + "score", + Prop::List(vec![Prop::I64(5), Prop::I64(10), Prop::I64(15)].into()), + )], + None, + ) + .unwrap(); - let filter = EdgeFilter.property("score").temporal().last().len().eq(2usize); + let filter = EdgeFilter + .property("score") + .temporal() + .last() + .len() + .eq(2usize); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -504,10 +678,40 @@ fn test_edge_aggregated_last_then_any_is_in() { // .last() picks the last snapshot's list, .is_in([...]).any() checks if any element is in the set. let g = Graph::new(); // A->B: last snapshot = ["folk","metal"] — "metal" ∈ {"metal","blues"} - g.add_edge(1, "A", "B", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("pop")].into()))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::List(vec![Prop::str("folk"), Prop::str("metal")].into()))], None).unwrap(); + g.add_edge( + 1, + "A", + "B", + [( + "tag", + Prop::List(vec![Prop::str("rock"), Prop::str("pop")].into()), + )], + None, + ) + .unwrap(); + g.add_edge( + 2, + "A", + "B", + [( + "tag", + Prop::List(vec![Prop::str("folk"), Prop::str("metal")].into()), + )], + None, + ) + .unwrap(); // C->D: last (and only) snapshot = ["jazz","pop"] — neither in {"metal","blues"} - g.add_edge(1, "C", "D", [("tag", Prop::List(vec![Prop::str("jazz"), Prop::str("pop")].into()))], None).unwrap(); + g.add_edge( + 1, + "C", + "D", + [( + "tag", + Prop::List(vec![Prop::str("jazz"), Prop::str("pop")].into()), + )], + None, + ) + .unwrap(); let filter = EdgeFilter .property("tag") @@ -525,12 +729,44 @@ fn test_edge_aggregated_last_then_all_contains() { // .last() picks the last snapshot's list, .contains("rock").all() checks all elements contain "rock". let g = Graph::new(); // A->B: last snapshot = ["rock","rock-n-roll"] — all contain "rock" - g.add_edge(1, "A", "B", [("tag", Prop::List(vec![Prop::str("jazz")].into()))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("rock-n-roll")].into()))], None).unwrap(); + g.add_edge( + 1, + "A", + "B", + [("tag", Prop::List(vec![Prop::str("jazz")].into()))], + None, + ) + .unwrap(); + g.add_edge( + 2, + "A", + "B", + [( + "tag", + Prop::List(vec![Prop::str("rock"), Prop::str("rock-n-roll")].into()), + )], + None, + ) + .unwrap(); // C->D: last (and only) snapshot = ["rock","jazz"] — "jazz" doesn't contain "rock" - g.add_edge(1, "C", "D", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("jazz")].into()))], None).unwrap(); + g.add_edge( + 1, + "C", + "D", + [( + "tag", + Prop::List(vec![Prop::str("rock"), Prop::str("jazz")].into()), + )], + None, + ) + .unwrap(); - let filter = EdgeFilter.property("tag").temporal().last().contains("rock").all(); + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .contains("rock") + .all(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -538,9 +774,12 @@ fn test_edge_aggregated_last_then_all_contains() { #[test] fn test_edge_aggregated_last_then_is_in() { let g = Graph::new(); - g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None).unwrap(); - g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None).unwrap(); + g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) + .unwrap(); + g.add_edge(2, "A", "B", [("tag", Prop::str("metal"))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("tag", Prop::str("jazz"))], None) + .unwrap(); let filter = EdgeFilter .property("tag") @@ -593,9 +832,12 @@ fn test_edge_property_not_contains_str_literal() { fn active_graph() -> Graph { let g = Graph::new(); - g.add_edge(1, "A", "B", [("active", Prop::Bool(true))], None).unwrap(); - g.add_edge(1, "C", "D", [("active", Prop::Bool(false))], None).unwrap(); - g.add_edge(1, "E", "F", [("active", Prop::Bool(true))], None).unwrap(); + g.add_edge(1, "A", "B", [("active", Prop::Bool(true))], None) + .unwrap(); + g.add_edge(1, "C", "D", [("active", Prop::Bool(false))], None) + .unwrap(); + g.add_edge(1, "E", "F", [("active", Prop::Bool(true))], None) + .unwrap(); g } @@ -623,11 +865,39 @@ fn test_edge_property_is_false() { fn test_edge_quantified_any_sum_gt() { let g = Graph::new(); // A->B: t=1 has a list whose sum = 11 (> 5); t=2 has a list whose sum = 3 - g.add_edge(1, "A", "B", [("score", Prop::List(vec![Prop::I64(3), Prop::I64(8)].into()))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2)].into()))], None).unwrap(); + g.add_edge( + 1, + "A", + "B", + [("score", Prop::List(vec![Prop::I64(3), Prop::I64(8)].into()))], + None, + ) + .unwrap(); + g.add_edge( + 2, + "A", + "B", + [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2)].into()))], + None, + ) + .unwrap(); // C->D: t=1 sum = 3, t=2 sum = 5 (neither > 5) - g.add_edge(1, "C", "D", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2)].into()))], None).unwrap(); - g.add_edge(2, "C", "D", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(4)].into()))], None).unwrap(); + g.add_edge( + 1, + "C", + "D", + [("score", Prop::List(vec![Prop::I64(1), Prop::I64(2)].into()))], + None, + ) + .unwrap(); + g.add_edge( + 2, + "C", + "D", + [("score", Prop::List(vec![Prop::I64(1), Prop::I64(4)].into()))], + None, + ) + .unwrap(); // any temporal snapshot where sum of list > 5 let filter = EdgeFilter.property("score").temporal().sum().gt(5i64).any(); @@ -639,11 +909,39 @@ fn test_edge_quantified_any_sum_gt() { fn test_edge_quantified_all_min_ge() { let g = Graph::new(); // A->B: t=1 min = 3, t=2 min = 5 (all ≥ 3) - g.add_edge(1, "A", "B", [("score", Prop::List(vec![Prop::I64(3), Prop::I64(8)].into()))], None).unwrap(); - g.add_edge(2, "A", "B", [("score", Prop::List(vec![Prop::I64(5), Prop::I64(9)].into()))], None).unwrap(); + g.add_edge( + 1, + "A", + "B", + [("score", Prop::List(vec![Prop::I64(3), Prop::I64(8)].into()))], + None, + ) + .unwrap(); + g.add_edge( + 2, + "A", + "B", + [("score", Prop::List(vec![Prop::I64(5), Prop::I64(9)].into()))], + None, + ) + .unwrap(); // C->D: t=1 min = 1 (< 3), so not all snapshots pass - g.add_edge(1, "C", "D", [("score", Prop::List(vec![Prop::I64(1), Prop::I64(9)].into()))], None).unwrap(); - g.add_edge(2, "C", "D", [("score", Prop::List(vec![Prop::I64(3), Prop::I64(5)].into()))], None).unwrap(); + g.add_edge( + 1, + "C", + "D", + [("score", Prop::List(vec![Prop::I64(1), Prop::I64(9)].into()))], + None, + ) + .unwrap(); + g.add_edge( + 2, + "C", + "D", + [("score", Prop::List(vec![Prop::I64(3), Prop::I64(5)].into()))], + None, + ) + .unwrap(); // all temporal snapshots where min of list >= 3 let filter = EdgeFilter.property("score").temporal().min().ge(3i64).all(); @@ -655,14 +953,59 @@ fn test_edge_quantified_all_min_ge() { fn test_edge_quantified_any_any_contains() { let g = Graph::new(); // A->B: t=1 list has "rock" (any element contains "rock") - g.add_edge(1, "A", "B", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("metal")].into()))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::List(vec![Prop::str("jazz"), Prop::str("blues")].into()))], None).unwrap(); + g.add_edge( + 1, + "A", + "B", + [( + "tag", + Prop::List(vec![Prop::str("rock"), Prop::str("metal")].into()), + )], + None, + ) + .unwrap(); + g.add_edge( + 2, + "A", + "B", + [( + "tag", + Prop::List(vec![Prop::str("jazz"), Prop::str("blues")].into()), + )], + None, + ) + .unwrap(); // C->D: no list has any element containing "rock" - g.add_edge(1, "C", "D", [("tag", Prop::List(vec![Prop::str("jazz"), Prop::str("blues")].into()))], None).unwrap(); - g.add_edge(2, "C", "D", [("tag", Prop::List(vec![Prop::str("folk"), Prop::str("pop")].into()))], None).unwrap(); + g.add_edge( + 1, + "C", + "D", + [( + "tag", + Prop::List(vec![Prop::str("jazz"), Prop::str("blues")].into()), + )], + None, + ) + .unwrap(); + g.add_edge( + 2, + "C", + "D", + [( + "tag", + Prop::List(vec![Prop::str("folk"), Prop::str("pop")].into()), + )], + None, + ) + .unwrap(); // any temporal snapshot where any list element contains "rock" - let filter = EdgeFilter.property("tag").temporal().contains("rock").any().any(); + let filter = EdgeFilter + .property("tag") + .temporal() + .contains("rock") + .any() + .any(); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B"]); } @@ -671,10 +1014,40 @@ fn test_edge_quantified_any_any_contains() { fn test_edge_quantified_any_last_is_in() { let g = Graph::new(); // A->B: last snapshot (t=2) has "metal" as an element - g.add_edge(1, "A", "B", [("tag", Prop::List(vec![Prop::str("rock"), Prop::str("folk")].into()))], None).unwrap(); - g.add_edge(2, "A", "B", [("tag", Prop::List(vec![Prop::str("pop"), Prop::str("metal")].into()))], None).unwrap(); + g.add_edge( + 1, + "A", + "B", + [( + "tag", + Prop::List(vec![Prop::str("rock"), Prop::str("folk")].into()), + )], + None, + ) + .unwrap(); + g.add_edge( + 2, + "A", + "B", + [( + "tag", + Prop::List(vec![Prop::str("pop"), Prop::str("metal")].into()), + )], + None, + ) + .unwrap(); // C->D: last (and only) snapshot has no element in {"metal"} - g.add_edge(1, "C", "D", [("tag", Prop::List(vec![Prop::str("jazz"), Prop::str("blues")].into()))], None).unwrap(); + g.add_edge( + 1, + "C", + "D", + [( + "tag", + Prop::List(vec![Prop::str("jazz"), Prop::str("blues")].into()), + )], + None, + ) + .unwrap(); // last temporal snapshot's list — any element is in {"metal"} let filter = EdgeFilter diff --git a/raphtory-tests/tests/subgraph_tests.rs b/raphtory-tests/tests/subgraph_tests.rs index 2f3cfb4955..3cc8eff24f 100644 --- a/raphtory-tests/tests/subgraph_tests.rs +++ b/raphtory-tests/tests/subgraph_tests.rs @@ -188,8 +188,7 @@ pub mod test_filters_node_subgraph { }; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ - assert_filter_nodes_results, TestGraphVariants, - TestVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; fn init_graph(graph: G) -> G { @@ -306,9 +305,7 @@ pub mod test_filters_node_subgraph { prelude::{AdditionOps, EdgeFilter}, }; use raphtory_api::core::entities::properties::prop::Prop; - use raphtory_tests::assertions::{ - assert_filter_edges_results, TestVariants, - }; + use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; use crate::test_filters_node_subgraph::{ NodeSubgraphTransformer, WindowedNodeSubgraphTransformer, diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 4914cdc98d..6bbb4ad41f 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -133,9 +133,7 @@ mod test_property_semantics { use raphtory_storage::mutation::{ addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, }; - use raphtory_tests::assertions::{ - assert_filter_nodes_results, TestVariants, - }; + use raphtory_tests::assertions::{assert_filter_nodes_results, TestVariants}; fn init_graph(graph: G) -> G { let nodes = [ @@ -425,8 +423,7 @@ mod test_property_semantics { addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, }; use raphtory_tests::assertions::{ - assert_filter_edges_results, TestGraphVariants, - TestVariants, WindowGraphTransformer, + assert_filter_edges_results, TestGraphVariants, TestVariants, WindowGraphTransformer, }; fn init_graph< @@ -1506,7 +1503,7 @@ mod test_node_filter { graph::views::filter::{ model::{ degree_filter::DegreeFilterFactory, - node_filter::ops::{NodeFilterOps}, + node_filter::ops::NodeFilterOps, not_filter::NotFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, ComposableFilter, CompositeNodeFilter, NodeViewFilterOps, @@ -1523,8 +1520,7 @@ mod test_node_filter { }; use raphtory_api::core::{entities::properties::prop::Prop, Direction}; use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_select_nodes_results, - TestVariants, + assert_filter_nodes_results, assert_select_nodes_results, TestVariants, }; fn sort_vids(mut vids: Vec) -> Vec { @@ -2350,7 +2346,9 @@ mod test_node_filter { TestVariants::All, ); - let filter = NodeFilter.node_type().is_in(vec!["fire_nation", "air_nomads"]); + let filter = NodeFilter + .node_type() + .is_in(vec!["fire_nation", "air_nomads"]); let expected_results = vec!["1", "2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2836,19 +2834,19 @@ mod test_node_filter { mod test_node_property_filter { use crate::{init_nodes_graph, IdentityGraphTransformer}; - use raphtory::db::graph::views::filter::model::{ - graph_filter::GraphFilter, - node_filter::NodeFilter, - not_filter::NotFilter, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - windowed_filter::Windowed, - ComposableFilter, PropertyFilterFactory, ViewWrapOps, + use raphtory::{ + db::graph::views::filter::model::{ + graph_filter::GraphFilter, + node_filter::NodeFilter, + not_filter::NotFilter, + property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, + windowed_filter::Windowed, + ComposableFilter, PropertyFilterFactory, ViewWrapOps, + }, + prelude::{EntityAggOps, EntityExprFilterOps}, }; use raphtory_api::core::entities::properties::prop::Prop; - use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; - use raphtory_tests::assertions::{ - assert_filter_nodes_results, TestVariants, - }; + use raphtory_tests::assertions::{assert_filter_nodes_results, TestVariants}; use std::vec; #[test] @@ -4314,8 +4312,7 @@ mod composite_node_filter_tests { prelude::NodeFilter, }; use raphtory_tests::assertions::{ - assert_filter_neighbours_results, assert_filter_nodes_results, - TestVariants, + assert_filter_neighbours_results, assert_filter_nodes_results, TestVariants, }; #[test] @@ -4428,7 +4425,8 @@ mod composite_node_filter_tests { TestVariants::All, ); - let filter = NodeFilter.node_type() + let filter = NodeFilter + .node_type() .eq("fire_nation") .and(NodeFilter.property("p1").eq("prop1")); let expected_results = Vec::<&str>::new(); @@ -4469,7 +4467,8 @@ mod composite_node_filter_tests { TestVariants::All, ); - let filter = NodeFilter.node_type() + let filter = NodeFilter + .node_type() .eq("fire_nation") .and(NodeFilter.property("p1").eq("shivam_kapoor")); let expected_results = vec!["1"]; @@ -4489,7 +4488,8 @@ mod composite_node_filter_tests { TestVariants::All, ); - let filter = NodeFilter.name() + let filter = NodeFilter + .name() .eq("2") .and(NodeFilter.property("p2").eq(2u64)); let expected_results = vec!["2"]; @@ -4509,7 +4509,8 @@ mod composite_node_filter_tests { TestVariants::All, ); - let filter = NodeFilter.name() + let filter = NodeFilter + .name() .eq("2") .and(NodeFilter.property("p2").eq(2u64)) .or(NodeFilter.property("p9").eq(5u64)); @@ -4564,7 +4565,8 @@ mod composite_node_filter_tests { #[test] fn test_out_neighbours_filter() { - let filter = NodeFilter.name() + let filter = NodeFilter + .name() .eq("2") .and(NodeFilter.property("p2").eq(2u64)); let expected_results = vec!["2"]; @@ -4644,7 +4646,9 @@ mod test_node_property_filter_agg { CreateFilter, }, }, - prelude::{AdditionOps, EntityAggOps, EntityExprFilterOps, GraphViewOps, PropertyAdditionOps}, + prelude::{ + AdditionOps, EntityAggOps, EntityExprFilterOps, GraphViewOps, PropertyAdditionOps, + }, }; use raphtory_api::core::{ entities::properties::prop::{IntoProp, Prop}, @@ -4654,8 +4658,7 @@ mod test_node_property_filter_agg { addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, }; use raphtory_tests::assertions::{ - assert_filter_nodes_err, assert_filter_nodes_results, - TestVariants::All, + assert_filter_nodes_err, assert_filter_nodes_results, TestVariants::All, }; fn list_u8(xs: &[u8]) -> Prop { @@ -4982,7 +4985,6 @@ mod test_node_property_filter_agg { &expected, All, ); - } fn apply_assertion_err( @@ -4996,7 +4998,6 @@ mod test_node_property_filter_agg { &expected, All, ); - } // ------ Property: SUM ---- @@ -8151,14 +8152,11 @@ mod test_edge_filter { init_edges_graph_with_str_ids_del, init_nodes_graph, IdentityGraphTransformer, }; use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, - node_expr::EntityExprFilterOps, - ComposableFilter, EdgeViewFilterOps, NodeFilterFactory, PropertyFilterFactory, - ViewWrapOps, + edge_filter::EdgeFilter, node_expr::EntityExprFilterOps, ComposableFilter, + EdgeViewFilterOps, NodeFilterFactory, PropertyFilterFactory, ViewWrapOps, }; use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_select_edges_results, - TestGraphVariants, TestVariants, + assert_filter_edges_results, assert_select_edges_results, TestGraphVariants, TestVariants, }; #[test] @@ -9172,8 +9170,8 @@ mod test_edge_property_filter { ComposableFilter, PropertyFilterFactory, ViewWrapOps, }; - use raphtory_api::core::entities::properties::prop::Prop; use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; + use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ assert_filter_edges_results, TestGraphVariants, TestVariants, }; @@ -10749,7 +10747,6 @@ mod composite_edge_filter_tests { TestVariants::EventOnly, ); - let filter = NotFilter( EdgeFilter::src() .name() @@ -10771,6 +10768,5 @@ mod composite_edge_filter_tests { &expected_results, TestVariants::EventOnly, ); - } } diff --git a/raphtory-tests/tests/test_layers.rs b/raphtory-tests/tests/test_layers.rs index 1b57c08c9b..1df1636b6d 100644 --- a/raphtory-tests/tests/test_layers.rs +++ b/raphtory-tests/tests/test_layers.rs @@ -254,8 +254,7 @@ pub mod test_filters_layer_graph { }; use raphtory_tests::assertions::{ - assert_filter_nodes_results, TestGraphVariants, - TestVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; fn init_graph(graph: G) -> G { let edges = vec![ @@ -453,8 +452,7 @@ pub mod test_filters_layer_graph { }; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_tests::assertions::{ - assert_filter_edges_results, TestGraphVariants, - TestVariants, + assert_filter_edges_results, TestGraphVariants, TestVariants, }; use crate::test_filters_layer_graph::{ diff --git a/raphtory-tests/tests/tests_node_type_filtered_subgraph.rs b/raphtory-tests/tests/tests_node_type_filtered_subgraph.rs index 5658892d6f..216f43fe56 100644 --- a/raphtory-tests/tests/tests_node_type_filtered_subgraph.rs +++ b/raphtory-tests/tests/tests_node_type_filtered_subgraph.rs @@ -254,8 +254,7 @@ mod test_filters_node_type_filtered_subgraph { db::graph::views::filter::model::PropertyFilterFactory, prelude::NodeFilter, }; use raphtory_tests::assertions::{ - assert_filter_nodes_results, TestGraphVariants, - TestVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; #[test] fn test_nodes_filters() { @@ -456,9 +455,7 @@ mod test_filters_node_type_filtered_subgraph { db::graph::views::filter::model::PropertyFilterFactory, prelude::EdgeFilter, }; - use raphtory_tests::assertions::{ - assert_filter_edges_results, TestVariants, - }; + use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; #[test] fn test_edges_filters() { let filter = EdgeFilter.property("p1").eq(1u64); diff --git a/raphtory-tests/tests/views_test.rs b/raphtory-tests/tests/views_test.rs index d12f477990..a6a980fe75 100644 --- a/raphtory-tests/tests/views_test.rs +++ b/raphtory-tests/tests/views_test.rs @@ -543,8 +543,7 @@ mod test_filters_window_graph { addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, }; use raphtory_tests::assertions::{ - assert_filter_nodes_results, TestGraphVariants, - TestVariants, + assert_filter_nodes_results, TestGraphVariants, TestVariants, }; use raphtory::{ @@ -552,9 +551,8 @@ mod test_filters_window_graph { api::view::filter_ops::Filter, graph::views::filter::model::{ node_filter::{ops::NodeFilterOps, NodeFilter}, - NodeFilterFactory, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, + NodeFilterFactory, PropertyFilterFactory, }, }, errors::GraphError, @@ -964,7 +962,9 @@ mod test_filters_window_graph { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter.node_type().is_in(vec!["fire_nation", "air_nomad"]); + let filter = NodeFilter + .node_type() + .is_in(vec!["fire_nation", "air_nomad"]); let expected_results = vec!["N1", "N3", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -987,7 +987,9 @@ mod test_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter.node_type().is_in(vec!["fire_nation", "air_nomad"]); + let filter = NodeFilter + .node_type() + .is_in(vec!["fire_nation", "air_nomad"]); let expected_results = vec!["N1", "N3", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -1984,8 +1986,7 @@ mod test_filters_window_graph { }; use raphtory_api::core::{entities::properties::prop::Prop, storage::arc_str::ArcStr}; use raphtory_tests::assertions::{ - assert_filter_edges_results, TestGraphVariants, - TestVariants, WindowGraphTransformer, + assert_filter_edges_results, TestGraphVariants, TestVariants, WindowGraphTransformer, }; fn init_graph(graph: G) -> G { @@ -3233,7 +3234,6 @@ mod test_filters_window_graph { &expected_results, TestVariants::PersistentOnly, ); - } #[test] diff --git a/raphtory/src/algorithms/components/in_components.rs b/raphtory/src/algorithms/components/in_components.rs index 1ef30872db..2406343438 100644 --- a/raphtory/src/algorithms/components/in_components.rs +++ b/raphtory/src/algorithms/components/in_components.rs @@ -91,7 +91,6 @@ pub fn in_components_filtered( where G: StaticGraphViewOps, F: CreateFilter + 'static, - F::EntityFiltered<'static, F::FilteredGraph<'static, G>>: StaticGraphViewOps, { let filtered = g.filter(filter)?; let ctx: Context<_, _> = (&filtered).into(); @@ -192,7 +191,6 @@ pub fn in_component_filtered<'graph, G, F>( where G: GraphViewOps<'graph>, F: CreateFilter + 'graph, - F::EntityFiltered<'graph, F::FilteredGraph<'graph, G>>: GraphViewOps<'graph>, { let mut in_components = HashMap::new(); let mut to_check_stack = VecDeque::new(); diff --git a/raphtory/src/algorithms/components/out_components.rs b/raphtory/src/algorithms/components/out_components.rs index 8596dbafb1..40ad543354 100644 --- a/raphtory/src/algorithms/components/out_components.rs +++ b/raphtory/src/algorithms/components/out_components.rs @@ -88,7 +88,6 @@ pub fn out_components_filtered( where G: StaticGraphViewOps, F: CreateFilter + 'static, - F::EntityFiltered<'static, F::FilteredGraph<'static, G>>: StaticGraphViewOps, { let filtered = g.filter(filter)?; let ctx: Context<_, _> = (&filtered).into(); @@ -189,7 +188,6 @@ pub fn out_component_filtered<'graph, G, F>( where G: GraphViewOps<'graph>, F: CreateFilter + 'graph, - F::EntityFiltered<'graph, F::FilteredGraph<'graph, G>>: GraphViewOps<'graph>, { let mut out_components = HashMap::new(); let mut to_check_stack = VecDeque::new(); diff --git a/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs index 90e5e6d9b9..e24b758a88 100644 --- a/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs @@ -16,11 +16,10 @@ use crate::{ }, prelude::GraphViewOps, }; -use raphtory_api::{ - core::entities::edges::edge_ref::EdgeRef, - inherit::Base, +use raphtory_api::{core::entities::edges::edge_ref::EdgeRef, inherit::Base}; +use raphtory_storage::{ + core_ops::InheritCoreGraphOps, graph::edges::edge_storage_ops::EdgeStorageOps, }; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_storage_ops::EdgeStorageOps}; use storage::EdgeEntryRef; /// Edge-filtered graph: hides edges that fail the predicate `filter`. @@ -78,12 +77,12 @@ impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritP for EdgeExprFilteredGraph { } -impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> - InheritNodePropertySchemaOps for EdgeExprFilteredGraph +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritNodePropertySchemaOps + for EdgeExprFilteredGraph { } -impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> - InheritEdgePropertySchemaOps for EdgeExprFilteredGraph +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgePropertySchemaOps + for EdgeExprFilteredGraph { } impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritTimeSemantics @@ -98,8 +97,8 @@ impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritE for EdgeExprFilteredGraph { } -impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> - InheritExplodedEdgeFilterOps for EdgeExprFilteredGraph +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritExplodedEdgeFilterOps + for EdgeExprFilteredGraph { } impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgeLayerFilterOps diff --git a/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs b/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs new file mode 100644 index 0000000000..ca3c1364d5 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs @@ -0,0 +1,136 @@ +use crate::{ + core::entities::LayerIds, + db::{ + api::{ + properties::internal::{ + InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, + }, + view::internal::{ + Immutable, InheritEdgeFilterOps, InheritEdgeHistoryFilter, + InheritEdgeLayerFilterOps, InheritLayerOps, + InheritListOps, InheritMaterialize, InheritNodeFilterOps, InheritNodeHistoryFilter, + InheritStorageOps, InheritTimeSemantics, + InternalExplodedEdgeFilterOps, Static, + }, + }, + graph::views::filter::model::edge_expr::EdgeOp, + }, + prelude::GraphViewOps, +}; +use either::Either; +use raphtory_api::{ + core::{ + entities::{edges::edge_ref::EdgeRef, ELID}, + storage::timeindex::EventTime, + }, + inherit::Base, +}; +use raphtory_storage::{ + core_ops::{CoreGraphOps, InheritCoreGraphOps}, + graph::edges::edge_storage_ops::EdgeStorageOps, +}; + +/// Edge-filtered graph: hides edges that fail the predicate `filter`. +/// +/// Parallel to `NodeFilteredGraph` but for edges: `internal_filter_edge` evaluates +/// `filter.apply(storage, edge_ref)` in O(1) after a single compile step. +#[derive(Clone)] +pub struct ExplodedEdgeExprFilteredGraph { + pub(crate) graph: G, + pub(crate) filter: F, +} + +impl ExplodedEdgeExprFilteredGraph { + pub fn new(graph: G, filter: F) -> Self { + Self { graph, filter } + } +} + +impl Base for ExplodedEdgeExprFilteredGraph { + type Base = G; + + fn base(&self) -> &Self::Base { + &self.graph + } +} + +impl Static for ExplodedEdgeExprFilteredGraph {} +impl Immutable for ExplodedEdgeExprFilteredGraph {} + +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritCoreGraphOps + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritStorageOps + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritLayerOps + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritListOps + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritMaterialize + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritNodeFilterOps + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritPropertiesOps + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritNodePropertySchemaOps + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgePropertySchemaOps + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritTimeSemantics + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritNodeHistoryFilter + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgeHistoryFilter + for ExplodedEdgeExprFilteredGraph +{ +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> + InternalExplodedEdgeFilterOps for ExplodedEdgeExprFilteredGraph +{ + fn internal_exploded_edge_filtered(&self) -> bool { + true + } + + fn internal_exploded_filter_edge_list_trusted(&self) -> bool { + false + } + + fn internal_filter_exploded_edge(&self, eid: ELID, t: EventTime, layer_ids: &LayerIds) -> bool { + if !self.graph.internal_filter_exploded_edge(eid, t, layer_ids) { + return false; + } + let edge_ref: EdgeRef = self.core_edge(Either::Left(eid.eid())).out_ref(); + self.filter + .apply(self.graph.core_graph(), edge_ref.at_layer(eid.layer()).at(t)) + } +} +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgeLayerFilterOps + for ExplodedEdgeExprFilteredGraph +{ +} + +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgeFilterOps + for ExplodedEdgeExprFilteredGraph +{ +} diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 57dc6b9f55..7cea62838b 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -20,6 +20,7 @@ pub mod model; pub mod node_filtered_graph; pub mod not_filtered_graph; pub mod or_filtered_graph; +mod exploded_edge_expr_filtered_graph; pub struct Unfiltered; @@ -34,11 +35,6 @@ impl CreateFilter for Unfiltered { where Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, @@ -66,11 +62,6 @@ pub trait CreateFilter: Sized { Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G>: GraphViewOps<'graph> - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -92,11 +83,6 @@ impl CreateFilter for T { = Self where Self: 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, 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 aec4089288..548d46a073 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -7,11 +7,12 @@ use crate::{ graph::views::filter::{ and_filtered_graph::AndFilteredGraph, model::{ - edge_expr::{EdgeExpr, EdgeOp, ops::AndBoolEdgeOp}, + edge_expr::{ops::AndBoolEdgeOp, EdgeOp}, edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, - node_expr::{EntityExpr, NodeExpr, ops::AndBoolNodeOp}, - node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, + node_expr::{ops::AndBoolNodeOp, CreateOp, EntityExpr}, + node_filter::CompositeNodeFilter, + ComposableFilter, TryAsCompositeFilter, }, CreateFilter, }, @@ -48,12 +49,6 @@ impl CreateFilter for AndFilter { where Self: 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -113,12 +108,16 @@ where R: EntityExpr, { type Marker = L::Marker; + + fn entity(&self) -> Self::Marker { + self.left.entity() + } } -impl NodeExpr for AndFilter +impl CreateOp for AndFilter where - L: NodeExpr, - R: NodeExpr, + L: CreateOp, + R: CreateOp, R: EntityExpr, { fn create_node_op<'g, G: GraphView + 'g>( @@ -129,14 +128,7 @@ where let right = self.right.create_node_op(graph)?; Ok(Arc::new(AndBoolNodeOp { left, right })) } -} -impl EdgeExpr for AndFilter -where - L: EdgeExpr, - R: EdgeExpr, - R: EntityExpr, -{ fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, diff --git a/raphtory/src/db/graph/views/filter/model/degree_filter.rs b/raphtory/src/db/graph/views/filter/model/degree_filter.rs index d81446f790..64a83197b5 100644 --- a/raphtory/src/db/graph/views/filter/model/degree_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/degree_filter.rs @@ -55,12 +55,6 @@ impl CreateFilter for DegreeFilter { type NodeFilter<'graph, G: GraphView + 'graph> = NodeDegreeFilterOp; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index bb73779b6c..a2b65e04e6 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -5,13 +5,11 @@ use super::{ ops::{ - BinaryCmpEdgeOp, ListAwareCmpEdgeOp, ListAwareSetEdgeOp, - ListAwareStringEdgeOp, PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, + BinaryCmpEdgeOp, ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, + PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, }, - EdgeExpr, EdgeOp, + EdgeOp, }; -use crate::db::graph::views::filter::model::edge_filter::EdgeFilter; -use crate::db::graph::views::filter::model::node_expr::filters::PropValueSetExpr; pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpExpr, StringExpr, UnaryExpr}; use crate::{ db::{ @@ -19,11 +17,11 @@ use crate::{ graph::views::filter::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, model::{ - edge_filter::CompositeEdgeFilter, - filter_operator::BinaryOp - - , CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateFilter, TryAsCompositeFilter, + edge_filter::{CompositeEdgeFilter, EdgeFilter}, + filter_operator::BinaryOp, + node_expr::{filters::PropValueSetExpr, CreateOp}, + CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, + TryAsCompositeFilter, }, }, }, @@ -32,13 +30,18 @@ use crate::{ }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; +use crate::db::graph::views::filter::exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph; +use crate::db::graph::views::filter::model::ExplodedEdgeFilter; // ───────────────────────────────────────────────────────────────────────────── // validate helpers // ───────────────────────────────────────────────────────────────────────────── 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) + && matches!( + op, + BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge + ) && *prop_type == PropType::Bool { return Err(GraphError::InvalidFilter(format!( @@ -52,10 +55,10 @@ fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), GraphEr // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpExpr // ───────────────────────────────────────────────────────────────────────────── -impl EdgeExpr for BinaryCmpExpr +impl CreateOp for BinaryCmpExpr where - L: EdgeExpr, - R: EdgeExpr, + L: CreateOp, + R: CreateOp, { fn create_edge_op<'g, G: GraphView + 'g>( &self, @@ -63,14 +66,18 @@ where ) -> Result> + 'g>, GraphError> { let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(ListAwareCmpEdgeOp { left, right, op: self.op })) + Ok(Arc::new(ListAwareCmpEdgeOp { + left, + right, + op: self.op, + })) } } impl TryAsCompositeFilter for BinaryCmpExpr where - L: EdgeExpr, - R: EdgeExpr, + L: CreateOp, + R: CreateOp, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -89,20 +96,14 @@ where impl CreateFilter for BinaryCmpExpr where - L: EdgeExpr, - R: EdgeExpr, + L: CreateOp, + R: CreateOp, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -110,8 +111,11 @@ where let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph.clone())?; validate_binary_op(&self.op, &left.prop_type())?; - let op: Arc + 'graph> = - Arc::new(BinaryCmpEdgeOp { left, right, op: self.op }); + let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { + left, + right, + op: self.op, + }); Ok(EdgeExprFilteredGraph::new(graph, op)) } @@ -123,13 +127,46 @@ where } } +impl CreateFilter for BinaryCmpExpr +where + L: CreateOp, + R: CreateOp, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + ExplodedEdgeExprFilteredGraph + 'graph>>; + + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph.clone())?; + validate_binary_op(&self.op, &left.prop_type())?; + let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { + left, + right, + op: self.op, + }); + Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + // ───────────────────────────────────────────────────────────────────────────── // UnaryExpr // ───────────────────────────────────────────────────────────────────────────── impl TryAsCompositeFilter for UnaryExpr where - E: EdgeExpr, + E: CreateOp, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -148,16 +185,11 @@ where impl CreateFilter for UnaryExpr where - E: EdgeExpr, + E: CreateOp, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, @@ -195,11 +227,10 @@ fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { // StringExpr — string expression filter for edges // ───────────────────────────────────────────────────────────────────────────── - -impl EdgeExpr for StringExpr +impl CreateOp for StringExpr where - L: EdgeExpr, - R: EdgeExpr, + L: CreateOp, + R: CreateOp, { fn create_edge_op<'g, G: GraphView + 'g>( &self, @@ -207,14 +238,18 @@ where ) -> Result> + 'g>, GraphError> { let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(ListAwareStringEdgeOp { left, right, op: self.op })) + Ok(Arc::new(ListAwareStringEdgeOp { + left, + right, + op: self.op, + })) } } impl TryAsCompositeFilter for StringExpr where - L: EdgeExpr, - R: EdgeExpr, + L: CreateOp, + R: CreateOp, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -231,17 +266,12 @@ where impl CreateFilter for StringExpr where - L: EdgeExpr, - R: EdgeExpr, + L: CreateOp, + R: CreateOp, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, @@ -250,8 +280,11 @@ where let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph.clone())?; validate_string_op(&left.prop_type())?; - let op: Arc + 'graph> = - Arc::new(StringEdgeOp { left, right, op: self.op }); + let op: Arc + 'graph> = Arc::new(StringEdgeOp { + left, + right, + op: self.op, + }); Ok(EdgeExprFilteredGraph::new(graph, op)) } @@ -267,18 +300,21 @@ where // PropValueSetExpr — is_in / is_not_in for edge-side exprs // ───────────────────────────────────────────────────────────────────────────── -impl EdgeExpr for PropValueSetExpr { +impl CreateOp for PropValueSetExpr { fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, ) -> Result> + 'g>, GraphError> { let inner = self.expr.create_edge_op(graph)?; - Ok(Arc::new(ListAwareSetEdgeOp { inner, values: self.values.clone(), op: self.op })) + Ok(Arc::new(ListAwareSetEdgeOp { + inner, + values: self.values.clone(), + op: self.op, + })) } } - -impl TryAsCompositeFilter for PropValueSetExpr { +impl TryAsCompositeFilter for PropValueSetExpr { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) } @@ -292,23 +328,21 @@ impl TryAsCompositeFilter for PropValueSetExpr { } } -impl CreateFilter for PropValueSetExpr { +impl CreateFilter for PropValueSetExpr { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, ) -> Result, GraphError> { let inner = self.expr.create_edge_op(graph.clone())?; - let op: Arc + 'graph> = - Arc::new(PropValueSetEdgeOp { inner, values: self.values, op: self.op }); + let op: Arc + 'graph> = Arc::new(PropValueSetEdgeOp { + inner, + values: self.values, + op: self.op, + }); Ok(EdgeExprFilteredGraph::new(graph, op)) } diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index 0d6b91ae0d..586c4b51e3 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -20,10 +20,7 @@ //! ``` use crate::{ - db::{ - api::view::internal::GraphView, - graph::views::filter::model::node_expr::EntityExpr, - }, + db::{api::view::internal::GraphView, graph::views::filter::model::node_expr::EntityExpr}, errors::GraphError, }; use raphtory_api::core::entities::{ @@ -37,9 +34,8 @@ pub mod exprs; pub mod filters; pub mod ops; -pub use exprs::*; -pub use filters::*; pub use super::{Metadata, Property}; +pub use filters::*; // ───────────────────────────────────────────────────────────────────────────── // EdgeOp — compiled evaluator: EdgeRef → typed value @@ -57,27 +53,3 @@ pub trait EdgeOp: Send + Sync { PropType::Empty } } - -// ───────────────────────────────────────────────────────────────────────────── -// EdgeExpr — typed expression describing what to compute per edge -// ───────────────────────────────────────────────────────────────────────────── - -/// A typed expression that produces a value per edge. -/// -/// Parallel to [`NodeExpr`] — all expressions produce `Option`; no associated output type. -/// -/// Usage: -/// ```rust,ignore -/// EdgeFilter.property("weight").gt(5.0f64) -/// EdgeFilter.property("tag").temporal().sum().gt(100i64) -/// EdgeFilter.property("label").temporal().into_expr().contains("foo").any() -/// ``` -pub(crate) trait EdgeExpr: EntityExpr + Clone + Send + Sync + 'static { - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError>; -} - - - diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index bdedd1dfb7..242a27a5f2 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -217,8 +217,14 @@ impl<'g> EdgeOp for PropValueSetEdgeOp<'g> { match self.inner.apply(storage, edge) { None => false, Some(v) => match self.op { - SetOp::IsIn => self.values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), - SetOp::IsNotIn => self.values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), + SetOp::IsIn => self + .values + .iter() + .any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), + SetOp::IsNotIn => self + .values + .iter() + .all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), }, } } @@ -318,8 +324,12 @@ impl<'g> EdgeOp for ListAwareSetEdgeOp<'g> { broadcast_unary(vals, |v| { let v = v?; Some(Prop::Bool(match op { - SetOp::IsIn => values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), - SetOp::IsNotIn => values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), + SetOp::IsIn => values + .iter() + .any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), + SetOp::IsNotIn => values + .iter() + .all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), })) }) } @@ -339,7 +349,10 @@ pub(crate) struct AndBoolEdgeOp<'g> { impl<'g> Clone for AndBoolEdgeOp<'g> { fn clone(&self) -> Self { - Self { left: self.left.clone(), right: self.right.clone() } + Self { + left: self.left.clone(), + right: self.right.clone(), + } } } @@ -364,7 +377,10 @@ pub(crate) struct OrBoolEdgeOp<'g> { impl<'g> Clone for OrBoolEdgeOp<'g> { fn clone(&self) -> Self { - Self { left: self.left.clone(), right: self.right.clone() } + Self { + left: self.left.clone(), + right: self.right.clone(), + } } } 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 0967dc15c3..9a7c9facdf 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -7,7 +7,7 @@ use crate::{ graph::views::filter::{ edge_node_filtered_graph::EdgeNodeFilteredGraph, model::{ - edge_expr::{ops::EdgeEndpointNodeOp, EdgeExpr, EdgeOp}, + edge_expr::{ops::EdgeEndpointNodeOp, EdgeOp}, exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, @@ -15,19 +15,18 @@ use crate::{ is_valid_filter::IsValidEdge, latest_filter::Latest, layered_filter::Layered, - node_expr::{EntityExpr, NodeExpr}, + node_expr::{CreateOp, EntityExpr}, node_filter::{ builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, }, property_filter::PropertyFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - AllExpr, AndFilter, AnyExpr, AvgExpr, CombinedFilter, ComposableFilter, - CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityAggOps, - EntityExprFilterOps, EntityMarker, FirstExpr, InternalViewWrapOps, - LastExpr, LenExpr, MaxExpr, MetadataExpr, MinExpr, NotFilter, OrFilter, - PropertyExpr, PropertyFilterFactory, SumExpr, TemporalExpr, - TryAsCompositeFilter, Wrap, + AllExpr, AndFilter, AnyExpr, AvgExpr, CombinedFilter, ComposableFilter, CreateView, + EdgeFilterFactory, EdgeViewFilterOps, EntityAggOps, + EntityMarker, FirstExpr, InternalViewWrapOps, LastExpr, LenExpr, MaxExpr, + MetadataExpr, MinExpr, NotFilter, OrFilter, PropertyExpr, PropertyFilterFactory, + SumExpr, TemporalPropExpr, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -36,7 +35,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::core::{ - entities::{properties::prop::Prop, GID}, + entities::properties::prop::Prop, storage::timeindex::EventTime, }; use std::{fmt, fmt::Display, sync::Arc}; @@ -152,66 +151,74 @@ impl EdgeEndpointWrapper { } #[inline] - pub fn property(&self, name: impl Into) -> EdgeEndpointWrapper> { + pub fn property( + &self, + name: impl Into, + ) -> EdgeEndpointWrapper> { EdgeEndpointWrapper::new(NodeFilter.property(name), self.endpoint) } #[inline] - pub fn metadata(&self, name: impl Into) -> EdgeEndpointWrapper> { + pub fn metadata( + &self, + name: impl Into, + ) -> EdgeEndpointWrapper> { EdgeEndpointWrapper::new(NodeFilter.metadata(name), self.endpoint) } } impl EdgeEndpointWrapper> { #[inline] - pub fn temporal(&self) -> EdgeEndpointWrapper> { + pub fn temporal(&self) -> EdgeEndpointWrapper> { EdgeEndpointWrapper::new(self.inner.temporal(), self.endpoint) } } -impl EdgeEndpointWrapper> { +impl + EdgeEndpointWrapper> +{ #[inline] - pub fn sum(self) -> EdgeEndpointWrapper>> { + pub fn sum(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; EdgeEndpointWrapper::new(self.inner.sum(), endpoint) } #[inline] - pub fn avg(self) -> EdgeEndpointWrapper>> { + pub fn avg(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; EdgeEndpointWrapper::new(self.inner.avg(), endpoint) } #[inline] - pub fn min(self) -> EdgeEndpointWrapper>> { + pub fn min(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; EdgeEndpointWrapper::new(self.inner.min(), endpoint) } #[inline] - pub fn max(self) -> EdgeEndpointWrapper>> { + pub fn max(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; EdgeEndpointWrapper::new(self.inner.max(), endpoint) } #[inline] - pub fn first(self) -> EdgeEndpointWrapper>> { + pub fn first(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; EdgeEndpointWrapper::new(self.inner.first(), endpoint) } #[inline] - pub fn last(self) -> EdgeEndpointWrapper>> { + pub fn last(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; EdgeEndpointWrapper::new(self.inner.last(), endpoint) } #[inline] - pub fn len(self) -> EdgeEndpointWrapper>> { + pub fn len(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; EdgeEndpointWrapper::new(self.inner.len(), endpoint) } #[inline] - pub fn any(self) -> EdgeEndpointWrapper>> { + pub fn any(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; EdgeEndpointWrapper::new(AnyExpr(self.inner), endpoint) } #[inline] - pub fn all(self) -> EdgeEndpointWrapper>> { + pub fn all(self) -> EdgeEndpointWrapper>> { let endpoint = self.endpoint; EdgeEndpointWrapper::new(AllExpr(self.inner), endpoint) } @@ -219,15 +226,21 @@ impl EdgeEndpointWra impl EntityExpr for EdgeEndpointWrapper { type Marker = EdgeFilter; + fn entity(&self) -> Self::Marker { + EdgeFilter + } } -impl EdgeExpr for EdgeEndpointWrapper { +impl CreateOp for EdgeEndpointWrapper { fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, ) -> Result> + 'g>, GraphError> { let node_op = self.inner.create_node_op(graph)?; - Ok(Arc::new(EdgeEndpointNodeOp { node_op, endpoint: self.endpoint })) + Ok(Arc::new(EdgeEndpointNodeOp { + node_op, + endpoint: self.endpoint, + })) } } @@ -264,12 +277,6 @@ impl CreateFilter for EdgeEndpointWrapper Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -362,12 +369,6 @@ impl CreateFilter for CompositeEdgeFilter { Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = Arc - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, 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 73cec17d8e..a73685f611 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 @@ -147,11 +147,6 @@ impl CreateFilter for ExplodedEdgeEndpointWra where Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, @@ -253,11 +248,6 @@ impl CreateFilter for CompositeExplodedEdgeFilter { where Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = Arc - where - Self: 'graph, - G: GraphViewOps<'graph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, 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 8e54c90575..c4b36e4d01 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -64,8 +64,14 @@ impl Comparable for Prop { 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::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), } } 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 2940454e89..2fb908732f 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -45,12 +45,6 @@ impl CreateFilter for GraphFilter { type NodeFilter<'graph, G: GraphView + 'graph> = NodeExistsOp; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, 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 db5f0bdaae..a90046cc62 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 @@ -39,12 +39,6 @@ impl CreateFilter for IsActiveEdge { Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, 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 564c6765e7..b422ca221e 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 @@ -39,12 +39,6 @@ impl CreateFilter for IsActiveNode { Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, 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 27c08e885e..a9cc891333 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 @@ -39,12 +39,6 @@ impl CreateFilter for IsDeletedEdge { Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, 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 d397dfdab4..c8a5503976 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 @@ -39,12 +39,6 @@ impl CreateFilter for IsSelfLoopEdge { Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, 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 a42ada47c3..70ad8f793d 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 @@ -39,12 +39,6 @@ impl CreateFilter for IsValidEdge { Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, 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 c325303813..f8098d45e2 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -4,15 +4,11 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, windowed_filter::Windowed, CombinedFilter, + ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, + EdgeViewFilterOps, InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -84,12 +80,6 @@ impl CreateFilter for Latest where G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + Clone + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G>( self, graph: G, 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 1d0af9196a..2d205591a3 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -4,14 +4,11 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, CombinedFilter, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, EdgeViewFilterOps, + InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -98,12 +95,6 @@ impl CreateFilter for Layered + 'graph, >::LayeredViewType: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G>( self, graph: G, diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index c2e6a020f3..e2ef8f8562 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -15,12 +15,9 @@ pub use crate::{ UnaryOp, }, node_expr::{ - AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, - EntityAggOps, EntityExprFilterOps, FirstExpr, - LastExpr, LenExpr, MaxExpr, MinExpr, - PropValueSetExpr, StringExpr, SumExpr, - TemporalExpr, - UnaryExpr, + AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, EntityAggOps, + EntityExprFilterOps, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, + PropValueSetExpr, StringExpr, SumExpr, TemporalPropExpr, UnaryExpr, }, node_filter::{NodeFilter, NodeFilterFactory}, not_filter::NotFilter, @@ -73,7 +70,10 @@ use crate::{ pub use node_filter::CompositeNodeFilter; use raphtory_api::core::{ entities::{properties::prop::Prop, Layer}, - storage::{arc_str::ArcStr, timeindex::{AsTime, EventTime}}, + storage::{ + arc_str::ArcStr, + timeindex::{AsTime, EventTime}, + }, utils::time::IntoTime, }; use std::{marker::PhantomData, ops::Deref, sync::Arc}; @@ -117,11 +117,6 @@ impl CreateFilter for NoFilter { where Self: 'graph, G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, @@ -222,12 +217,6 @@ impl CreateFilter for Arc { type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; - type FilteredGraph<'graph, G> - = Arc - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -290,12 +279,15 @@ pub struct PropertyExpr { impl EntityExpr for PropertyExpr { type Marker = E::Marker; + + fn entity(&self) -> Self::Marker { + self.view_expr.entity() + } } -impl NodeExpr +impl CreateOp for PropertyExpr { - fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -307,6 +299,18 @@ impl( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let prop_id = graph + .edge_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(EdgePropOp { graph, prop_id })) + } } #[derive(Clone)] @@ -317,9 +321,12 @@ pub struct MetadataExpr { impl EntityExpr for MetadataExpr { type Marker = E::Marker; + fn entity(&self) -> Self::Marker { + self.view_expr.entity() + } } -impl NodeExpr +impl CreateOp for MetadataExpr { fn create_node_op<'g, G: GraphView + 'g>( @@ -333,6 +340,18 @@ impl( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let prop_id = graph + .edge_meta() + .get_prop_id(&self.name, true) + .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; + let graph = self.view_expr.create_view(graph)?; + Ok(Arc::new(EdgeMetaOp { graph, prop_id })) + } } pub trait PropertyFilterFactory: Sized { @@ -433,8 +452,8 @@ where } impl PropertyExpr { - pub fn temporal(&self) -> TemporalExpr { - TemporalExpr { + pub fn temporal(&self) -> TemporalPropExpr { + TemporalPropExpr { view_expr: self.view_expr.clone(), name: self.name.clone(), } @@ -455,43 +474,11 @@ pub trait EdgeFilterFactory: PropertyFilterFactory + Clone {} // PropertyExpr / MetadataExpr — EdgeExpr impls // ───────────────────────────────────────────────────────────────────────────── -use edge_expr::{ - EdgeExpr, EdgeOp +use crate::db::graph::views::filter::model::{ + edge_expr::ops::{EdgeMetaOp, EdgePropOp}, + node_expr::{CreateOp, EntityExpr}, }; -use crate::db::graph::views::filter::model::edge_expr::ops::{EdgeMetaOp, EdgePropOp}; -use crate::db::graph::views::filter::model::node_expr::{EntityExpr, NodeExpr}; - -impl EdgeExpr - for PropertyExpr -{ - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let prop_id = graph - .edge_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(EdgePropOp { graph, prop_id })) - } -} - -impl EdgeExpr - for MetadataExpr -{ - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let prop_id = graph - .edge_meta() - .get_prop_id(&self.name, true) - .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; - let graph = self.view_expr.create_view(graph)?; - Ok(Arc::new(EdgeMetaOp { graph, prop_id })) - } -} +use edge_expr::EdgeOp; pub trait TryAsCompositeFilter: Send + Sync { fn try_as_composite_node_filter(&self) -> Result; @@ -667,6 +654,9 @@ impl CreateView for NodeFilter { impl EntityExpr for NodeFilter { type Marker = NodeFilter; + fn entity(&self) -> Self::Marker { + NodeFilter + } } impl CreateView for EdgeFilter { @@ -682,6 +672,9 @@ impl CreateView for EdgeFilter { impl EntityExpr for EdgeFilter { type Marker = EdgeFilter; + fn entity(&self) -> Self::Marker { + EdgeFilter + } } impl CreateView for ExplodedEdgeFilter { @@ -696,27 +689,45 @@ impl CreateView for ExplodedEdgeFilter { } impl EntityExpr for ExplodedEdgeFilter { - type Marker = EdgeFilter; + type Marker = ExplodedEdgeFilter; + fn entity(&self) -> Self::Marker { + ExplodedEdgeFilter + } } impl EntityExpr for Windowed { type Marker = T::Marker; + fn entity(&self) -> Self::Marker { + self.inner.entity() + } } impl EntityExpr for Layered { type Marker = T::Marker; + fn entity(&self) -> Self::Marker { + self.inner.entity() + } } impl EntityExpr for Latest { type Marker = T::Marker; + fn entity(&self) -> Self::Marker { + self.inner.entity() + } } impl EntityExpr for SnapshotAt { type Marker = T::Marker; + fn entity(&self) -> Self::Marker { + self.inner.entity() + } } impl EntityExpr for SnapshotLatest { type Marker = T::Marker; + fn entity(&self) -> Self::Marker { + self.inner.entity() + } } impl CreateView for Layered { @@ -936,4 +947,3 @@ impl EdgeViewFilterOps for DynEdgeViewProps { self.deref().dyn_is_self_loop() } } - diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs new file mode 100644 index 0000000000..6c226455ea --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -0,0 +1,130 @@ +//! Dyn-dispatch traits for property/aggregator/quantifier expression chains. +//! +//! `DynPropertyExpr` is the type-erased equivalent of the chained +//! `EntityExpr` → `EntityAggOps` → `EntityExprFilterOps` API. Each method either: +//! - terminates the chain by producing an `Arc` +//! (comparators / string ops / set ops / unary), or +//! - extends the chain by producing another `Arc` +//! (selectors / aggregators / quantifiers). +//! +//! Used by the Python `PyPropertyExprBuilder` and `PyPropertyFilterBuilder` +//! wrappers to dispatch chain calls at runtime through typed expressions. +//! +//! ## Chain methods are currently panic stubs +//! +//! Implementing the chain methods (`dyn_first`, `dyn_sum`, …) properly +//! requires distinguishing node-side vs edge-side expressions at the type +//! level. The blanket impl below only bounds `E: EntityExpr`, which is not +//! enough — `BinaryCmpExpr<_, _, M>: CreateFilter` requires `L: NodeExpr` +//! (or `L: EdgeExpr`) depending on `M`. Splitting the blanket into Node / +//! Edge versions creates coherence overlap because primitive types (`Prop`, +//! `u32`, …) impl both `NodeExpr` and `EdgeExpr`. +//! +//! Chain methods are left as panicking default impls until a working +//! resolution is in place. The Python `.sum()`, `.first()`, `.any()`, etc. +//! calls will panic at runtime. + +use crate::{ + db::{ + api::{ + state::{ops::GraphView, NodeOp}, + view::BoxableGraphView, + }, + graph::views::filter::model::{ + edge_expr::EdgeOp, + node_expr::{CreateOp, EntityExpr}, + EntityMarker, + }, + }, + errors::GraphError, +}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::{ops::Deref, sync::Arc}; + +trait DynEntityExpr: Send + Sync + 'static { + fn dyn_entity(&self) -> EntityMarker; + fn dyn_prop_type(&self) -> PropType; + fn dyn_nullable(&self) -> bool; +} + +impl>> DynEntityExpr for E { + fn dyn_entity(&self) -> EntityMarker { + self.entity().into() + } + + fn dyn_prop_type(&self) -> PropType { + self.prop_type() + } + + fn dyn_nullable(&self) -> bool { + self.nullable() + } +} + +impl EntityExpr for Arc { + type Marker = EntityMarker; + + fn entity(&self) -> Self::Marker { + self.deref().dyn_entity() + } + + fn prop_type(&self) -> PropType { + self.deref().dyn_prop_type() + } + + fn nullable(&self) -> bool { + self.deref().dyn_nullable() + } +} + +pub trait DynCreateOp: DynEntityExpr { + fn dyn_create_node_op<'g>( + &self, + graph: Arc, + ) -> Result> + 'g>, GraphError>; + + fn dyn_create_edge_op<'g>( + &self, + graph: Arc, + ) -> Result> + 'g>, GraphError>; +} + +impl>> DynCreateOp for E { + fn dyn_create_node_op<'g>( + &self, + graph: Arc, + ) -> Result> + 'g>, GraphError> { + self.create_node_op(graph) + } + + fn dyn_create_edge_op<'g>( + &self, + graph: Arc, + ) -> Result> + 'g>, GraphError> { + self.create_edge_op(graph) + } +} + +impl EntityExpr for Arc { + type Marker = EntityMarker; + + fn entity(&self) -> Self::Marker { + self.deref().dyn_entity() + } +} + +impl CreateOp for Arc { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.deref().dyn_create_node_op(Arc::new(graph)) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.deref().dyn_create_edge_op(Arc::new(graph)) + } +} 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 dfec15b821..5b380e937e 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 @@ -1,7 +1,7 @@ //! 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 +//! without computing it. Call [`CreateOp::create_node_op`] to compile it against a specific graph //! view, performing name→ID resolution once. //! //! # Field expressions @@ -61,10 +61,14 @@ //! // ConstExpr for custom comparable types not covered above //! ``` -use super::{ops::{ - AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NodeMetaOp, - NodePropOp, SumNodeOp, TemporalNodePropOp, -}, AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, NodeExpr, SumEdgeOp}; +use super::{ + ops::{ + AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, SumNodeOp, + TemporalNodePropOp, + }, + AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, CreateOp, EntityExpr, FirstEdgeOp, + LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, SumEdgeOp, +}; use crate::{ db::{ api::{ @@ -72,10 +76,10 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::model::{ - edge_expr::{EdgeExpr, EdgeOp, ops::TemporalEdgePropOp}, + edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, filter_operator::Comparable, node_filter::{NodeFilter, NodeFilterFactory}, - EdgeFilterFactory, CreateView, Metadata, Property, + CreateView, }, }, errors::GraphError, @@ -105,9 +109,13 @@ pub struct ConstFilter; impl EntityExpr for Id { type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } } -impl NodeExpr for Id { +impl CreateOp for Id { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, @@ -118,9 +126,13 @@ impl NodeExpr for Id { impl EntityExpr for GID { type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } } -impl NodeExpr for GID { +impl CreateOp for GID { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, @@ -131,12 +143,17 @@ impl NodeExpr for GID { impl EntityExpr for Name { type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } + fn prop_type(&self) -> PropType { PropType::Str } } -impl NodeExpr for Name { +impl CreateOp for Name { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, @@ -147,13 +164,17 @@ impl NodeExpr for Name { impl EntityExpr for Type { type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } + fn prop_type(&self) -> PropType { PropType::Str } } -impl NodeExpr for Type { - +impl CreateOp for Type { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, @@ -173,67 +194,114 @@ impl NodeExpr for Type { impl EntityExpr for usize { type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } + fn prop_type(&self) -> PropType { PropType::U64 } } -impl NodeExpr for usize { +impl CreateOp for usize { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Const(Some(Prop::U64(*self as u64))))) } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(Prop::U64(*self as u64))))) + } } impl EntityExpr for String { type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } + fn prop_type(&self) -> PropType { PropType::Str } } -impl NodeExpr for String { +impl CreateOp for String { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Const(Some(self.clone().into_prop())))) } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.clone().into_prop())))) + } } impl EntityExpr for ArcStr { type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } + fn prop_type(&self) -> PropType { PropType::Str } } -impl NodeExpr for ArcStr { +impl CreateOp for ArcStr { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Const(Some(self.clone().into_prop())))) } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.clone().into_prop())))) + } } -impl EntityExpr for &'static str -{ +impl EntityExpr for &'static str { type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } + fn prop_type(&self) -> PropType { PropType::Str } } -impl NodeExpr for &'static str { +impl CreateOp for &'static str { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Const(Some((*self).into_prop())))) } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(Prop::Str(ArcStr::from(*self)))))) + } } // ───────────────────────────────────────────────────────────────────────────── @@ -246,54 +314,75 @@ impl NodeExpr for &'static str { impl EntityExpr for Prop { type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } + fn prop_type(&self) -> PropType { self.dtype() } } -impl NodeExpr for Prop { +impl CreateOp for Prop { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Const(Some(self.clone())))) } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.clone())))) + } } -macro_rules! impl_node_expr_for_numeric { +macro_rules! impl_create_op_for_numeric { ($prim:ty, $variant:ident) => { impl EntityExpr for $prim { type Marker = ConstFilter; + fn entity(&self) -> Self::Marker { + ConstFilter + } fn prop_type(&self) -> PropType { PropType::$variant } } - impl NodeExpr for $prim { + impl CreateOp for $prim { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, ) -> Result> + 'g>, GraphError> { Ok(Arc::new(Const(Some(Prop::$variant(*self))))) } + fn create_edge_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); +impl_create_op_for_numeric!(i32, I32); +impl_create_op_for_numeric!(i64, I64); +impl_create_op_for_numeric!(u32, U32); +impl_create_op_for_numeric!(u64, U64); +impl_create_op_for_numeric!(f32, F32); +impl_create_op_for_numeric!(f64, F64); +impl_create_op_for_numeric!(bool, Bool); +impl_create_op_for_numeric!(u8, U8); +impl_create_op_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 +/// [`CreateOp`] directly and can be passed as-is. `ConstExpr` is only needed /// for custom comparable types. /// /// ```rust,ignore @@ -304,9 +393,13 @@ pub struct ConstExpr(pub T); impl EntityExpr for ConstExpr { type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } } -impl + Clone + Send + Sync + 'static> NodeExpr for ConstExpr { +impl + Clone + Send + Sync + 'static> CreateOp for ConstExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, _graph: G, @@ -337,6 +430,11 @@ pub struct DegreeExpr { impl EntityExpr for DegreeExpr { type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } + fn prop_type(&self) -> PropType { PropType::U64 } @@ -345,7 +443,7 @@ impl EntityExpr for DegreeExpr } } -impl NodeExpr for DegreeExpr { +impl CreateOp for DegreeExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -360,40 +458,6 @@ impl NodeExpr for DegreeExpr { } } -impl EntityExpr for Property { - type Marker = NodeFilter; -} - -impl NodeExpr for 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()))?; - Ok(Arc::new(NodePropOp { graph, prop_id })) - } -} - -impl EntityExpr for Metadata { - type Marker = NodeFilter; -} - -impl NodeExpr for Metadata { - 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::MetadataMissingError(self.name.clone()))?; - Ok(Arc::new(NodeMetaOp { graph, prop_id })) - } -} - // ───────────────────────────────────────────────────────────────────────────── // TemporalExpr — all temporal values of a property over the view window // @@ -409,32 +473,45 @@ impl NodeExpr for Metadata { /// are available, plus `EntityAggOps` for aggregators (`.sum()`, `.last()`, /// `.len()`, etc.). #[derive(Clone)] -pub struct TemporalExpr { +pub struct TemporalPropExpr { pub(crate) view_expr: E, pub(crate) name: String, } -impl TemporalExpr { - pub fn new(name: impl Into) -> Self { - Self { view_expr: NodeFilter, name: name.into() } - } -} - -impl EntityExpr for TemporalExpr { +impl EntityExpr for TemporalPropExpr { type Marker = E::Marker; + fn entity(&self) -> Self::Marker { + self.view_expr.entity() + } } -impl EntityAggOps for TemporalExpr { - 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 EntityAggOps for TemporalPropExpr { + 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 NodeExpr for TemporalExpr { +impl CreateOp + for TemporalPropExpr +{ fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -444,11 +521,11 @@ impl EdgeExpr for TemporalExpr { fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -502,19 +579,36 @@ macro_rules! impl_agg_expr { impl EntityExpr for $expr { type Marker = E::Marker; + fn entity(&self) -> Self::Marker { + self.0.entity() + } } impl EntityAggOps for $expr { - 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) } + 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 NodeExpr for $expr { + impl CreateOp for $expr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -522,9 +616,7 @@ macro_rules! impl_agg_expr { let inner = self.0.create_node_op(graph)?; Ok(Arc::new($node_op_ty { inner })) } - } - impl EdgeExpr for $expr { fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, 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 4b8eb56559..2d289ccc5a 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 @@ -45,24 +45,26 @@ use super::{ BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, - AllExpr, AnyExpr, EntityExpr, NodeExpr, + AllExpr, AnyExpr, CreateOp, EntityExpr, }; use crate::{ db::{ - api::{state::ops::NodeOp, view::internal::GraphView}, + api::{ + state::ops::NodeOp, + view::{internal::GraphView, BoxableGraphView}, + }, graph::views::filter::{ model::{ edge_filter::CompositeEdgeFilter, filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, - node_filter::NodeFilterFactory, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, - CreateView, MetadataExpr, PropertyExpr, TryAsCompositeFilter, + EntityMarker, ExplodedEdgeFilter, TryAsCompositeFilter, }, node_filtered_graph::NodeFilteredGraph, }, }, errors::GraphError, - prelude::{GraphViewOps, NodeFilter}, + prelude::{EdgeFilter, GraphViewOps, NodeFilter}, }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; @@ -70,7 +72,7 @@ use std::sync::Arc; // BinaryCmpExpr — binary expression filter // ───────────────────────────────────────────────────────────────────────────── -/// A node filter that compares two [`NodeExpr`] values using a [`BinaryOp`]. +/// A node filter that compares two [`CreateOp`] values using a [`BinaryOp`]. /// /// Both sides produce `Option` at runtime. Created by [`EntityExprFilterOps`] methods /// (`.gt`, `.lt`, `.eq`, `.ne`, `.ge`, `.le`). @@ -158,10 +160,7 @@ fn resolved_prop_type(expr_pt: PropType, op_pt: PropType) -> PropType { /// Only fires when both sides are known and the RHS is a literal/const. Defers /// to runtime when the LHS type is unknown (`PropType::Empty`) or the RHS isn't /// a const value. -fn validate_const_castable( - lhs_pt: &PropType, - rhs_const: Option<&Prop>, -) -> Result<(), GraphError> { +fn validate_const_castable(lhs_pt: &PropType, rhs_const: Option<&Prop>) -> Result<(), GraphError> { if *lhs_pt == PropType::Empty { return Ok(()); } @@ -208,20 +207,14 @@ fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result, impl CreateFilter for BinaryCmpExpr where - L: NodeExpr, - R: NodeExpr, + L: CreateOp, + R: CreateOp, { 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, @@ -239,7 +232,10 @@ where let right = self.right.create_node_op(graph)?; let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; - validate_const_castable(&lhs_pt, right.const_value().as_ref().and_then(|o| o.as_ref()))?; + validate_const_castable( + &lhs_pt, + right.const_value().as_ref().and_then(|o| o.as_ref()), + )?; Ok(Arc::new(BinaryCmpNodeOp { left, right, @@ -248,10 +244,54 @@ where } } +impl CreateFilter for BinaryCmpExpr +where + L: CreateOp, + R: CreateOp, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; + + type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + Ok(match self.entity { + EntityMarker::Node => Arc::new( + BinaryCmpExpr::new(self.left, self.op, self.right, NodeFilter) + .create_filter(graph)?, + ), + EntityMarker::Edge => Arc::new( + BinaryCmpExpr::new(self.left, self.op, self.right, EdgeFilter) + .create_filter(graph)?, + ), + EntityMarker::ExplodedEdge => Arc::new( + BinaryCmpExpr::new(self.left, self.op, self.right, ExplodedEdgeFilter) + .create_filter(graph)?, + ), + }) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + match self.entity { + EntityMarker::Node => Ok(BinaryCmpExpr::new( + self.left, self.op, self.right, NodeFilter, + ) + .create_node_filter(graph)?), + EntityMarker::Edge => Err(GraphError::NotEdgeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotExplodedEdgeFilter), + } + } +} + impl TryAsCompositeFilter for BinaryCmpExpr where - L: NodeExpr, - R: NodeExpr, + L: CreateOp, + R: CreateOp, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -289,23 +329,27 @@ pub struct UnaryExpr { pub entity: Entity, } +impl UnaryExpr { + fn with_entity(self, entity: T) -> UnaryExpr { + UnaryExpr { + expr: self.expr, + op: self.op, + entity, + } + } +} + impl ComposableFilter for UnaryExpr {} impl CreateFilter for UnaryExpr where - E: NodeExpr, + E: CreateOp, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, Prop>; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -329,9 +373,42 @@ where } } +impl CreateFilter for UnaryExpr +where + E: CreateOp, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; + + type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, Prop>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + Ok(match self.entity { + EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph)?), + EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph)?), + EntityMarker::ExplodedEdge => { + Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) + } + }) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + match self.entity { + EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), + EntityMarker::Edge => Err(GraphError::NotEdgeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotExplodedEdgeFilter), + } + } +} + impl TryAsCompositeFilter for UnaryExpr where - E: NodeExpr, + E: CreateOp, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -352,7 +429,7 @@ where // StringExpr — string expression filter // ───────────────────────────────────────────────────────────────────────────── -/// A node filter that applies a [`StringOp`] to two [`NodeExpr`] values. +/// A node filter that applies a [`StringOp`] to two [`CreateOp`] values. /// /// Both sides must produce the same string-comparable type (`L::Output: StringComparable`). /// Created by the string methods on [`EntityExprFilterOps`] (`.starts_with`, `.ends_with`, @@ -378,24 +455,32 @@ pub struct StringExpr { impl StringExpr { pub fn new(left: L, op: StringOp, right: R, entity: Entity) -> Self { - Self { left, op, right, entity } + Self { + left, + op, + right, + entity, + } + } + + fn with_entity(self, entity: T) -> StringExpr { + Self { + left: self.left, + op: self.op, + right: self.right, + entity, + } } } impl ComposableFilter for StringExpr {} -impl CreateFilter for StringExpr { +impl CreateFilter for StringExpr { 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, @@ -420,10 +505,39 @@ impl CreateFilter for StringExpr { } } +impl CreateFilter for StringExpr { + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; + + type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + Ok(match self.entity { + EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph)?), + EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph)?), + EntityMarker::ExplodedEdge => { + Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) + } + }) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + match self.entity { + EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), + EntityMarker::Edge => Err(GraphError::NotNodeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotExplodedEdgeFilter), + } + } +} impl TryAsCompositeFilter for StringExpr where - L: NodeExpr, - R: NodeExpr, + L: CreateOp, + R: CreateOp, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -456,17 +570,23 @@ pub struct PropValueSetExpr { pub(crate) entity: Entity, } +impl PropValueSetExpr { + fn with_entity(self, entity: T) -> PropValueSetExpr { + PropValueSetExpr { + expr: self.expr, + values: self.values, + op: self.op, + entity, + } + } +} + impl ComposableFilter for PropValueSetExpr {} -impl CreateFilter for PropValueSetExpr { +impl CreateFilter for PropValueSetExpr { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; type NodeFilter<'graph, G: GraphView + 'graph> = PropValueSetNodeOp<'graph>; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, @@ -492,7 +612,36 @@ impl CreateFilter for PropValueSetExpr { } } -impl TryAsCompositeFilter for PropValueSetExpr { +impl CreateFilter for PropValueSetExpr { + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; + type NodeFilter<'graph, G: GraphView + 'graph> = PropValueSetNodeOp<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + Ok(match self.entity { + EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph)?), + EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph)?), + EntityMarker::ExplodedEdge => { + Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) + } + }) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + match self.entity { + EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), + EntityMarker::Edge => Err(GraphError::NotEdgeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotExplodedEdgeFilter), + } + } +} + +impl TryAsCompositeFilter for PropValueSetExpr { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) } @@ -512,7 +661,7 @@ impl TryAsCompositeFilter for PropValueSetExpr { // EntityExprFilterOps — comparison and set operators on any EntityExpr // ───────────────────────────────────────────────────────────────────────────── -/// Comparison, string, set, and presence operators on any [`NodeExpr`]. +/// Comparison, string, set, and presence operators on any [`CreateOp`]. /// /// `.any()` / `.all()` are terminal: they wrap `self` in `AnyExpr`/`AllExpr` and compare the /// result to `Bool(true)`. For element-wise comparison before reduction, chain in order: @@ -527,44 +676,53 @@ impl TryAsCompositeFilter for PropValueSetExpr { /// ``` pub trait EntityExprFilterOps: EntityExpr + Sized { fn gt(self, rhs: R) -> BinaryCmpExpr { - // TODO: validate ops - BinaryCmpExpr::new(self, BinaryOp::Gt, rhs, Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Gt, rhs, entity) } fn ge(self, rhs: R) -> BinaryCmpExpr { - BinaryCmpExpr::new(self, BinaryOp::Ge, rhs, Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Ge, rhs, entity) } fn lt(self, rhs: R) -> BinaryCmpExpr { - BinaryCmpExpr::new(self, BinaryOp::Lt, rhs, Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Lt, rhs, entity) } fn le(self, rhs: R) -> BinaryCmpExpr { - BinaryCmpExpr::new(self, BinaryOp::Le, rhs, Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Le, rhs, entity) } fn eq(self, rhs: R) -> BinaryCmpExpr { - BinaryCmpExpr::new(self, BinaryOp::Eq, rhs, Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Eq, rhs, entity) } fn ne(self, rhs: R) -> BinaryCmpExpr { - BinaryCmpExpr::new(self, BinaryOp::Ne, rhs, Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Ne, rhs, entity) } fn starts_with(self, rhs: R) -> StringExpr { - StringExpr::new(self, StringOp::StartsWith, rhs, Self::Marker::default()) + let entity = self.entity(); + StringExpr::new(self, StringOp::StartsWith, rhs, entity) } fn ends_with(self, rhs: R) -> StringExpr { - StringExpr::new(self, StringOp::EndsWith, rhs, Self::Marker::default()) + let entity = self.entity(); + StringExpr::new(self, StringOp::EndsWith, rhs, entity) } fn contains(self, rhs: R) -> StringExpr { - StringExpr::new(self, StringOp::Contains, rhs, Self::Marker::default()) + let entity = self.entity(); + StringExpr::new(self, StringOp::Contains, rhs, entity) } fn not_contains(self, rhs: R) -> StringExpr { - StringExpr::new(self, StringOp::NotContains, rhs, Self::Marker::default()) + let entity = self.entity(); + StringExpr::new(self, StringOp::NotContains, rhs, entity) } fn fuzzy_search( @@ -573,6 +731,7 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { levenshtein_distance: usize, prefix_match: bool, ) -> StringExpr { + let entity = self.entity(); StringExpr::new( self, StringOp::FuzzySearch { @@ -580,50 +739,62 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { prefix_match, }, rhs, - Self::Marker::default(), + entity, ) } fn is_some(self) -> UnaryExpr { + let entity = self.entity(); UnaryExpr { expr: self, op: UnaryOp::IsSome, - entity: Self::Marker::default(), + entity, } } fn is_none(self) -> UnaryExpr { + let entity = self.entity(); UnaryExpr { expr: self, op: UnaryOp::IsNone, - entity: Self::Marker::default(), + entity, } } - fn is_in>(self, values: impl IntoIterator) -> PropValueSetExpr { + fn is_in>( + self, + values: impl IntoIterator, + ) -> PropValueSetExpr { + let entity = self.entity(); PropValueSetExpr { expr: self, values: values.into_iter().map(Into::into).collect(), op: SetOp::IsIn, - entity: Self::Marker::default(), + entity, } } - fn is_not_in>(self, values: impl IntoIterator) -> PropValueSetExpr { + fn is_not_in>( + self, + values: impl IntoIterator, + ) -> PropValueSetExpr { + let entity = self.entity(); PropValueSetExpr { expr: self, values: values.into_iter().map(Into::into).collect(), op: SetOp::IsNotIn, - entity: Self::Marker::default(), + entity, } } fn is_true(self) -> BinaryCmpExpr { - BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(true), entity) } fn is_false(self) -> BinaryCmpExpr { - BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(false), Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(false), entity) } fn not(self) -> BinaryCmpExpr { @@ -631,11 +802,13 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { } fn any(self) -> BinaryCmpExpr, Prop, Self::Marker> { - BinaryCmpExpr::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true), entity) } fn all(self) -> BinaryCmpExpr, Prop, Self::Marker> { - BinaryCmpExpr::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true), Self::Marker::default()) + let entity = self.entity(); + BinaryCmpExpr::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true), entity) } } @@ -649,15 +822,20 @@ impl EntityExprFilterOps for E {} // temporal().is_in([...]).any() // ───────────────────────────────────────────────────────────────────────────── -impl EntityExpr for BinaryCmpExpr { +impl EntityExpr + for BinaryCmpExpr +{ type Marker = E; + fn entity(&self) -> Self::Marker { + self.entity + } fn prop_type(&self) -> PropType { // TODO: depending on the types of left and right, we should figure out the type to return here PropType::Empty } } -impl NodeExpr for BinaryCmpExpr { +impl CreateOp for BinaryCmpExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -672,14 +850,19 @@ impl NodeExpr for BinaryCmpExpr { } } -impl EntityExpr for StringExpr { +impl EntityExpr + for StringExpr +{ type Marker = Entity; + fn entity(&self) -> Self::Marker { + self.entity + } fn prop_type(&self) -> PropType { PropType::Empty } } -impl NodeExpr for StringExpr { +impl CreateOp for StringExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -694,18 +877,28 @@ impl NodeExpr for StringExpr { } } -impl EntityExpr for PropValueSetExpr { +impl EntityExpr + for PropValueSetExpr +{ type Marker = Entity; + fn entity(&self) -> Self::Marker { + self.entity + } fn prop_type(&self) -> PropType { PropType::Empty } } -impl EntityExpr for UnaryExpr { +impl EntityExpr + for UnaryExpr +{ type Marker = Entity; + fn entity(&self) -> Self::Marker { + self.entity + } } -impl NodeExpr for PropValueSetExpr { +impl CreateOp for PropValueSetExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, 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 index ee08faab6f..85e2c6b11e 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -8,6 +8,7 @@ use crate::{ use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; +pub mod dyn_expr; pub mod exprs; pub mod filters; pub mod ops; @@ -17,6 +18,7 @@ mod tests; pub use super::{Metadata, Property}; use crate::db::graph::views::filter::model::{edge_expr::EdgeOp, node_filter::NodeFilter}; +pub use dyn_expr::*; pub use exprs::*; pub use filters::*; pub use ops::*; @@ -43,22 +45,29 @@ pub use ops::*; /// NodeFilter.property("score").temporal().gt(10i64).any() /// ``` /// -pub trait NodeExpr: EntityExpr + Clone + Send + Sync + 'static { +pub trait CreateOp: EntityExpr + 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>; + _graph: G, + ) -> Result> + 'g>, GraphError> { + Err(GraphError::NotNodeFilter) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Err(GraphError::NotEdgeFilter) + } } pub trait EntityExpr: Clone + Send + Sync + 'static { - type Marker: Copy + Default + 'static; + type Marker: Copy + 'static; - fn entity() -> Self::Marker { - Self::Marker::default() - } + fn entity(&self) -> Self::Marker; /// A priory known type (for early validation where possible) fn prop_type(&self) -> PropType { @@ -75,4 +84,3 @@ pub trait EntityExpr: Clone + Send + Sync + 'static { true } } - 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 index b7a9d3827d..601a876727 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -332,16 +332,6 @@ impl<'g> NodeOp for ListAwareCmpNodeOp<'g> { let lv = self.left.apply(storage, node); let rhs = self.right.apply(storage, node); let op = &self.op; - // aggregate_values(lv, &|pi| { - // let bools: Vec = pi - // .map(|v| Prop::Bool(Prop::binary_cmp(op, &v, &rhs))) - // .collect(); - // if bools.is_empty() { - // None - // } else { - // Some(Prop::List(PropArray::from(bools))) - // } - // }) broadcast_binary(lv, rhs, &|lv, rhs| { Some(Prop::Bool(Prop::binary_cmp(op, &lv?, &rhs?))) }) @@ -362,16 +352,6 @@ impl<'g> NodeOp for ListAwareStringNodeOp<'g> { let lv = self.left.apply(storage, node); let rhs = self.right.apply(storage, node); let op = &self.op; - // aggregate_values(lv, &|pi| { - // let bools: Vec = pi - // .map(|v| Prop::Bool(Option::::string_cmp(op, &Some(v), &rhs))) - // .collect(); - // if bools.is_empty() { - // None - // } else { - // Some(Prop::List(PropArray::from(bools))) - // } - // }) broadcast_binary(lv, rhs, &|lv, rhs| { Some(Prop::Bool(Option::::string_cmp(op, &lv, &rhs))) }) @@ -440,26 +420,15 @@ impl<'g> NodeOp for ListAwareSetNodeOp<'g> { let vals = self.inner.apply(storage, node); let values = &self.values; let op = &self.op; - // aggregate_values(vals, &|pi| { - // let bools: Vec = pi - // .map(|v| { - // Prop::Bool(match op { - // SetOp::IsIn => values.iter().any(|x| x == &v), - // SetOp::IsNotIn => values.iter().all(|x| x != &v), - // }) - // }) - // .collect(); - // if bools.is_empty() { - // None - // } else { - // Some(Prop::List(PropArray::from(bools))) - // } - // }) broadcast_unary(vals, |v| { let v = v?; Some(Prop::Bool(match op { - SetOp::IsIn => values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), - SetOp::IsNotIn => values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), + SetOp::IsIn => values + .iter() + .any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), + SetOp::IsNotIn => values + .iter() + .all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), })) }) } @@ -480,7 +449,10 @@ pub(crate) struct AndBoolNodeOp<'g> { impl<'g> Clone for AndBoolNodeOp<'g> { fn clone(&self) -> Self { - Self { left: self.left.clone(), right: self.right.clone() } + Self { + left: self.left.clone(), + right: self.right.clone(), + } } } @@ -505,7 +477,10 @@ pub(crate) struct OrBoolNodeOp<'g> { impl<'g> Clone for OrBoolNodeOp<'g> { fn clone(&self) -> Self { - Self { left: self.left.clone(), right: self.right.clone() } + Self { + left: self.left.clone(), + right: self.right.clone(), + } } } @@ -553,8 +528,14 @@ impl<'g> NodeOp for PropValueSetNodeOp<'g> { match self.inner.apply(storage, node) { None => false, Some(v) => match self.op { - SetOp::IsIn => self.values.iter().any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), - SetOp::IsNotIn => self.values.iter().all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), + SetOp::IsIn => self + .values + .iter() + .any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), + SetOp::IsNotIn => self + .values + .iter() + .all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), }, } } 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 fecac6af40..dab57a2ae1 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 @@ -148,7 +148,12 @@ fn total_gt_in_degree_selects_nodes_with_outgoing_edges() { #[test] fn const_expr_works() { - let filter = BinaryCmpExpr::new(ConstExpr(2usize), BinaryOp::Eq, ConstExpr(2usize), NodeFilter); + let filter = BinaryCmpExpr::new( + ConstExpr(2usize), + BinaryOp::Eq, + ConstExpr(2usize), + NodeFilter, + ); let g = build_test_graph(); assert_eq!(filtered_names(filter, g), vec!["a", "b", "c"]); } @@ -538,7 +543,9 @@ fn is_true_excludes_absent_property() { #[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 filter = NodeFilter + .property("score") + .starts_with(Prop::Str("x".into())); let result = filter.create_filter(g); assert!( result.is_err(), 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 0ed698a89f..2c67d7e786 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 @@ -22,15 +22,14 @@ use crate::{ is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, - node_expr::{DegreeExpr, Metadata, Property}, + node_expr::DegreeExpr, node_filter::validate::validate, - node_state_filter::NodeStateBoolColOp - , + node_state_filter::NodeStateBoolColOp, snapshot_filter::{SnapshotAt, SnapshotLatest}, - windowed_filter::Windowed - , CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CreateView, EntityMarker, InternalViewWrapOps, NodeViewFilterOps - , PropertyFilterFactory, TryAsCompositeFilter, Wrap, + windowed_filter::Windowed, + CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, CreateView, + EntityMarker, InternalViewWrapOps, NodeViewFilterOps, PropertyFilterFactory, + TryAsCompositeFilter, Wrap, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -86,6 +85,7 @@ pub trait NodeFilterFactory: PropertyFilterFactory + Clone { where V: NodeStateValue + 'graph, T: Clone + Send + Sync + 'graph, + Self: Sized, { state.bool_col_filter(col) } @@ -184,12 +184,6 @@ impl CreateFilter for NodeIdFilter { type NodeFilter<'graph, G: GraphView + 'graph> = NodeIdFilterOp; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -245,12 +239,6 @@ impl CreateFilter for NodeNameFilter { type NodeFilter<'graph, G: GraphView + 'graph> = NodeNameFilterOp; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -304,12 +292,6 @@ impl CreateFilter for NodeTypeFilter { type NodeFilter<'graph, G: GraphView + 'graph> = NodeTypeFilterOp; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -403,12 +385,6 @@ impl CreateFilter for CompositeNodeFilter { type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; - type FilteredGraph<'graph, G> - = Arc - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, 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 4f01eda422..061622a960 100644 --- a/raphtory/src/db/graph/views/filter/model/not_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/not_filter.rs @@ -41,12 +41,6 @@ impl CreateFilter for NotFilter { where Self: 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, 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 aed5b4723c..67abb92467 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -6,11 +6,12 @@ use crate::{ }, graph::views::filter::{ model::{ - edge_expr::{EdgeExpr, EdgeOp, ops::OrBoolEdgeOp}, + edge_expr::{ops::OrBoolEdgeOp, EdgeOp}, edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, - node_expr::{EntityExpr, NodeExpr, ops::OrBoolNodeOp}, - node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, + node_expr::{ops::OrBoolNodeOp, CreateOp, EntityExpr}, + node_filter::CompositeNodeFilter, + ComposableFilter, TryAsCompositeFilter, }, or_filtered_graph::OrFilteredGraph, CreateFilter, @@ -47,12 +48,6 @@ impl CreateFilter for OrFilter { where Self: 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -103,12 +98,15 @@ where R: EntityExpr, { type Marker = L::Marker; + fn entity(&self) -> Self::Marker { + self.left.entity() + } } -impl NodeExpr for OrFilter +impl CreateOp for OrFilter where - L: NodeExpr, - R: NodeExpr, + L: CreateOp, + R: CreateOp, R: EntityExpr, { fn create_node_op<'g, G: GraphView + 'g>( @@ -119,14 +117,7 @@ where let right = self.right.create_node_op(graph)?; Ok(Arc::new(OrBoolNodeOp { left, right })) } -} -impl EdgeExpr for OrFilter -where - L: EdgeExpr, - R: EdgeExpr, - R: EntityExpr, -{ fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, 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 8c416696d2..c89c690afa 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 @@ -385,12 +385,6 @@ impl CreateFilter for PropertyFilter { type NodeFilter<'graph, G: GraphView + 'graph> = NodePropertyFilterOp; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -413,12 +407,6 @@ impl CreateFilter for PropertyFilter { type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, @@ -438,11 +426,6 @@ impl CreateFilter for PropertyFilter { impl CreateFilter for PropertyFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = ExplodedEdgePropertyFilteredGraph; type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, 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 e147f9929a..25f47af814 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -4,15 +4,11 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, windowed_filter::Windowed, CombinedFilter, + ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, + EdgeViewFilterOps, InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -93,12 +89,6 @@ impl CreateFilter for SnapshotA where G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G>( self, graph: G, @@ -223,12 +213,6 @@ impl CreateFilter for SnapshotL where G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G>( self, graph: G, 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 8f824157a2..cfda2c3336 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -4,14 +4,11 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, CombinedFilter, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, + EdgeViewFilterOps, InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -109,12 +106,6 @@ impl CreateFilter for Windowed< where G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G>( self, graph: G, @@ -122,7 +113,8 @@ impl CreateFilter for Windowed< where G: GraphViewOps<'graph> + TimeOps<'graph, WindowedViewType = WindowedGraph>, { - self.inner.create_filter(graph.window(self.start.t(), self.end.t())) + self.inner + .create_filter(graph.window(self.start.t(), self.end.t())) } fn create_node_filter<'graph, G>( @@ -132,7 +124,8 @@ impl CreateFilter for Windowed< where G: GraphView + TimeOps<'graph, WindowedViewType = WindowedGraph> + 'graph, { - self.inner.create_node_filter(graph.window(self.start.t(), self.end.t())) + self.inner + .create_node_filter(graph.window(self.start.t(), self.end.t())) } } diff --git a/raphtory/src/errors.rs b/raphtory/src/errors.rs index e75edf6899..38210e94b3 100644 --- a/raphtory/src/errors.rs +++ b/raphtory/src/errors.rs @@ -358,9 +358,12 @@ pub enum GraphError { #[error("Not supported")] NotSupported, - #[error("Node filter expected")] + #[error("Edge filter expected")] NotNodeFilter, + #[error("Node filter expected")] + NotEdgeFilter, + #[error("Operator {0} requires a property value, but none was provided.")] InvalidFilterExpectSingleGotNone(FilterOperator), diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 627ef7d232..b6c4afce43 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -147,8 +147,7 @@ pub mod prelude { }; pub use crate::db::graph::views::filter::model::{ - filter::Filter, property_filter::PropertyFilter, - EntityAggOps, EntityExprFilterOps, + filter::Filter, property_filter::PropertyFilter, EntityAggOps, EntityExprFilterOps, }; pub use crate::db::graph::views::{ diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index ffac8a53ab..9c6028586f 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -12,7 +12,7 @@ use crate::{ filter::{ filter_expr::PyFilterExpr, property_expr::{ - PyEdgeViewPropsFilterBuilder, PyPropertyExprBuilder, PyPropertyFilterBuilder, + PyEdgeViewPropsFilterBuilder, }, }, types::iterable::FromIterable, diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 7fa8f83e94..d9d3ec7f0e 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -66,12 +66,6 @@ impl CreateFilter for PyFilterExpr { where Self: 'graph; - type FilteredGraph<'graph, G> - = Arc - where - Self: 'graph, - G: GraphViewOps<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index f3e47810ec..30dccc9301 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -1,10 +1,8 @@ use crate::{ - db::graph::views::filter::model::{graph_filter::GraphFilter, ViewWrapOps}, - python::{ - filter::property_filter_builders::PyViewFilterBuilder, types::iterable::FromIterable, - }, + db::graph::views::filter::model::{graph_filter::GraphFilter, DynView, ViewWrapOps}, + python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; -use pyo3::{pyclass, pymethods}; +use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; @@ -15,7 +13,7 @@ use std::sync::Arc; /// (windows, snapshots, latest state) and **layer scope** for subsequent /// node and edge filters. /// -/// All methods are static and return a `ViewFilterBuilder`, which can then +/// All methods are static and return a `Graph`, which can then /// be refined further or combined with node/edge predicates. /// /// Examples: @@ -23,12 +21,21 @@ use std::sync::Arc; /// Graph.at(5) /// Graph.latest().layer("fire_nation") /// Graph.layers(["A", "B"]).snapshot_latest() -#[pyclass(frozen, name = "Graph", module = "raphtory.filter", from_py_object)] -#[derive(Clone, Copy)] -pub struct PyGraphFilter; +#[pyclass( + name = "Graph", + module = "raphtory.filter", + extends = PyFilterExpr, + frozen +)] +pub struct PyGraphFilter(pub(crate) DynView); #[pymethods] impl PyGraphFilter { + #[new] + fn new() -> Self { + PyGraphFilter(Arc::new(GraphFilter)) + } + /// Restricts evaluation to events within a time window. /// /// The window is inclusive of `start` and exclusive of `end`. @@ -38,10 +45,9 @@ impl PyGraphFilter { /// end (int): End time. /// /// Returns: - /// filter.ViewFilterBuilder: - #[staticmethod] - fn window(start: EventTime, end: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.window(start, end))) + /// filter.Graph: + fn window(&self, start: EventTime, end: EventTime) -> PyGraphFilter { + PyGraphFilter(self.0.clone().window(start, end)) } /// Restricts evaluation to a single point in time. @@ -50,10 +56,9 @@ impl PyGraphFilter { /// time (int): Event time. /// /// Returns: - /// filter.ViewFilterBuilder: - #[staticmethod] - fn at(time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.at(time))) + /// filter.Graph: + fn at(&self, time: EventTime) -> PyGraphFilter { + PyGraphFilter(self.0.clone().at(time)) } /// Restricts evaluation to times strictly after the given time. @@ -62,10 +67,9 @@ impl PyGraphFilter { /// time (int): Lower time bound. /// /// Returns: - /// filter.ViewFilterBuilder: - #[staticmethod] - fn after(time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.after(time))) + /// filter.Graph: + fn after(&self, time: EventTime) -> PyGraphFilter { + PyGraphFilter(self.0.clone().after(time)) } /// Restricts evaluation to times strictly before the given time. @@ -74,19 +78,17 @@ impl PyGraphFilter { /// time (int): Upper time bound. /// /// Returns: - /// filter.ViewFilterBuilder: - #[staticmethod] - fn before(time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.before(time))) + /// filter.Graph: + fn before(&self, time: EventTime) -> PyGraphFilter { + PyGraphFilter(self.0.clone().before(time)) } /// Evaluates filters against the latest available state of the graph. /// /// Returns: - /// filter.ViewFilterBuilder: - #[staticmethod] - fn latest() -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.latest())) + /// filter.Graph: + fn latest(&self) -> PyGraphFilter { + PyGraphFilter(Arc::new(self.0.clone().latest())) } /// Evaluates filters against a snapshot of the graph at a given time. @@ -95,19 +97,17 @@ impl PyGraphFilter { /// time (int): Snapshot time. /// /// Returns: - /// filter.ViewFilterBuilder: - #[staticmethod] - fn snapshot_at(time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.snapshot_at(time))) + /// filter.Graph: + fn snapshot_at(&self, time: EventTime) -> PyGraphFilter { + PyGraphFilter(Arc::new(self.0.clone().snapshot_at(time))) } /// Evaluates filters against the most recent snapshot of the graph. /// /// Returns: - /// filter.ViewFilterBuilder: - #[staticmethod] - fn snapshot_latest() -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.snapshot_latest())) + /// filter.Graph: + fn snapshot_latest(&self) -> PyGraphFilter { + PyGraphFilter(Arc::new(self.0.clone().snapshot_latest())) } /// Restricts evaluation to a single layer. @@ -116,10 +116,9 @@ impl PyGraphFilter { /// layer (str): Layer name. /// /// Returns: - /// filter.ViewFilterBuilder: - #[staticmethod] - fn layer(layer: String) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.layer(layer))) + /// filter.Graph: + fn layer(&self, layer: String) -> PyGraphFilter { + PyGraphFilter(Arc::new(self.0.clone().layer(layer))) } /// Restricts evaluation to any of the given layers. @@ -128,9 +127,19 @@ impl PyGraphFilter { /// layers (list[str]): Layer names. /// /// Returns: - /// filter.ViewFilterBuilder: - #[staticmethod] - fn layers(layers: FromIterable) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.layer(layers))) + /// filter.Graph: + fn layers(&self, layers: FromIterable) -> PyGraphFilter { + PyGraphFilter(Arc::new(self.0.clone().layer(layers))) + } +} + +impl<'py> IntoPyObject<'py> for PyGraphFilter { + type Target = PyGraphFilter; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let parent = PyFilterExpr(self.0.clone()); + Bound::new(py, (self, parent)) } } diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index f585a12036..9c5330d737 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -1,16 +1,16 @@ use crate::python::{ filter::{ - edge_filter_builders::{ + edge_expr::{ PyEdgeEndpoint, PyEdgeEndpointIdFilterBuilder, PyEdgeEndpointNameFilterBuilder, PyEdgeEndpointTypeFilterBuilder, PyEdgeFilter, }, - exploded_edge_filter_builder::PyExplodedEdgeFilter, + exploded_edge_expr::PyExplodedEdgeFilter, filter_expr::PyFilterExpr, graph_filter::PyGraphFilter, - node_filter_builders::{ + node_expr::{ PyNodeFilter, PyNodeIdFilterBuilder, PyNodeNameFilterBuilder, PyNodeTypeFilterBuilder, }, - property_filter_builders::{PyPropertyExprBuilder, PyPropertyFilterBuilder}, + property_expr::{PyPropertyExprBuilder, PyPropertyFilterBuilder}, }, types::iterable::FromIterable, }; @@ -20,12 +20,12 @@ use pyo3::{ }; use raphtory_api::core::entities::Layer; -pub mod edge_filter_builders; -pub mod exploded_edge_filter_builder; +pub mod edge_expr; +pub mod exploded_edge_expr; pub mod filter_expr; pub mod graph_filter; -pub mod node_filter_builders; -pub mod property_filter_builders; +pub mod node_expr; +pub mod property_expr; impl From> for Layer { fn from(iter: FromIterable) -> Self { diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 1dcecc9039..c4c15e297f 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,305 +1,32 @@ use crate::{ - db::{ - api::state::ops::{Id, Name, Type}, - graph::views::filter::model::{ - degree_filter::DegreeFilterFactory,node_filter::{ops::NodeFilterOps, NodeFilter}, - node_state_filter::NodeStateBoolColOp, - property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, - NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, - }, + db::graph::views::filter::model::{ + degree_filter::DegreeFilterFactory, + node_expr::{CreateOp}, + node_state_filter::NodeStateBoolColOp, + NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, - python::{ - filter::{ - filter_expr::PyFilterExpr, - property_expr_builders::{ - PyNodeViewPropsFilterBuilder, PyPropertyExprBuilder, PyPropertyFilterBuilder, - }, - }, - graph::node_state::PyOutputNodeState, - types::iterable::FromIterable, - }, -}; -use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyResult, Python}; -use raphtory_api::core::{ - entities::GID, - storage::{arc_str::ArcStr, timeindex::EventTime}, + prelude::{EntityExprFilterOps, NodeFilter, NodeFilterFactory}, + python::{graph::node_state::PyOutputNodeState, types::iterable::FromIterable}, }; +use pyo3::{pyclass, pymethods, IntoPyObject, PyResult}; +use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; +use crate::db::graph::views::filter::model::node_expr::DynCreateOp; -/// Filters nodes by their ID value. -/// -/// Supports numeric and string IDs and produces a `FilterExpr` -/// that can be used in node queries. -/// -/// Examples: -/// Node.id() == 1 -/// Node.id().is_in([1, 2, 3]) -/// Node.id().starts_with("user:") -#[pyclass(frozen, name = "NodeIdFilterBuilder", module = "raphtory.filter")] -pub struct PyNodeIdFilterBuilder(Id); - -#[pymethods] -impl PyNodeIdFilterBuilder { - /// Returns a filter expression that checks whether the node ID - /// is equal to the given value. - /// Arguments: - /// value (int): Node ID to compare against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating equality. - fn __eq__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.eq(value))) - } - - /// Returns a filter expression that checks whether the node ID - /// is not equal to the given value. - /// - /// Arguments: - /// value (int): Node ID to compare against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating inequality. - fn __ne__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.ne(value))) - } - - /// Returns a filter expression that checks whether the node ID - /// is less than the given value. - /// - /// Arguments: - /// value (int): Upper bound (exclusive) for the node ID. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `<` comparison. - fn __lt__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.lt(value))) - } - - /// Returns a filter expression that checks whether the node ID - /// is less than or equal to the given value. - /// - /// Arguments: - /// value (int): Upper bound (inclusive) for the node ID. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `<=` comparison. - fn __le__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.le(value))) - } - - /// Returns a filter expression that checks whether the node ID - /// is greater than the given value. - /// - /// Arguments: - /// value (int): Lower bound (exclusive) for the node ID. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `>` comparison. - fn __gt__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.gt(value))) - } +#[pyclass(frozen, name = "Expr", module = "raphtory.filter")] +#[derive(Clone)] +pub struct PyExpr(Arc); - /// Returns a filter expression that checks whether the node ID - /// is greater than or equal to the given value. - /// - /// Arguments: - /// value (int): Lower bound (inclusive) for the node ID. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `>=` comparison. - fn __ge__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.ge(value))) - } - - /// Returns a filter expression that checks whether the node ID - /// is contained within the specified iterable of IDs. - /// - /// Arguments: - /// values (list[int]): Iterable of node IDs to match against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating membership. - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.is_in(values))) - } - - /// Returns a filter expression that checks whether the node ID - /// is **not** contained within the specified iterable of IDs. - /// - /// Arguments: - /// values (list[int]): Iterable of node IDs to exclude. - /// - /// 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))) - } - - /// Returns a filter expression that checks whether the string - /// representation of the node ID starts with the given 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))) - } - - /// Returns a filter expression that checks whether the string - /// representation of the node ID ends with the given 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))) - } - - /// Returns a filter expression that checks whether the string - /// representation of the node ID 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))) - } - - /// Returns a filter expression that checks whether the string - /// representation of the node ID **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))) - } - - /// Returns a filter expression that performs fuzzy matching - /// against the string representation of the node ID. - /// - /// 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( - value, - levenshtein_distance, - prefix_match, - ))) +impl From for PyExpr { + fn from(value: E) -> Self { + PyExpr(Arc::new(value)) } } -/// Filters nodes by their name. -/// -/// Comparisons are performed on the node's string name. -/// -/// Examples: -/// Node.name() == "alice" -/// Node.name().contains("ali") -#[pyclass(frozen, name = "NodeNameFilterBuilder", module = "raphtory.filter")] -pub struct PyNodeNameFilterBuilder; - -/// Filters nodes by their node type. -/// -/// The node type corresponds to the optional type assigned at node creation. -/// -/// Examples: -/// Node.node_type() == "fire_nation" -/// Node.node_type().is_not_in(["air_nomads"]) -#[pyclass(frozen, name = "NodeTypeFilterBuilder", module = "raphtory.filter")] -pub struct PyNodeTypeFilterBuilder; - -macro_rules! impl_node_text_filter_builder { - ($py_ty:ident, $expr:expr) => { - #[pymethods] - impl $py_ty { - fn __eq__(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new($expr.eq(value))) - } - - fn __ne__(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new($expr.ne(value))) - } - - fn starts_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new($expr.starts_with(value))) - } - - fn ends_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new($expr.ends_with(value))) - } - - fn contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new($expr.contains(value))) - } - - fn not_contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new($expr.not_contains(value))) - } - - fn fuzzy_search( - &self, - value: String, - levenshtein_distance: usize, - prefix_match: bool, - ) -> PyFilterExpr { - PyFilterExpr(Arc::new($expr.fuzzy_search( - value, - levenshtein_distance, - prefix_match, - ))) - } - } - }; -} - -impl_node_text_filter_builder!(PyNodeNameFilterBuilder, Name); - #[pymethods] -impl PyNodeNameFilterBuilder { - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new(Name.is_in(vals))) - } - - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new(Name.is_not_in(vals))) - } -} - -impl_node_text_filter_builder!(PyNodeTypeFilterBuilder, Type); - -#[pymethods] -impl PyNodeTypeFilterBuilder { - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().map(ArcStr::from).collect(); - PyFilterExpr(Arc::new(Type.is_in(vals))) - } - - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().map(ArcStr::from).collect(); - PyFilterExpr(Arc::new(Type.is_not_in(vals))) +impl PyExpr { + fn __eq__(&self, other: &Self) -> Self { + self.0.eq(&other.0).into() } } @@ -310,62 +37,61 @@ impl PyNodeTypeFilterBuilder { /// - a view-restricted filter context, or /// - a boolean predicate over node state. #[pyclass(frozen, name = "Node", module = "raphtory.filter")] -pub struct PyNodeFilter; +pub struct PyNodeFilter(Arc); #[pymethods] impl PyNodeFilter { + #[new] + fn new() -> PyNodeFilter { + PyNodeFilter(Arc::new(NodeFilter)) + } + /// Selects the node ID field for filtering. /// /// Returns: /// filter.NodeIdFilterBuilder: - #[staticmethod] - fn id() -> PyNodeIdFilterBuilder { - PyNodeIdFilterBuilder(NodeFilter::id()) + fn id(&self) -> PyExpr { + self.0.id().into() } /// Selects the node name field for filtering. /// /// Returns: /// filter.NodeNameFilterBuilder: - #[staticmethod] - fn name() -> PyNodeNameFilterBuilder { - PyNodeNameFilterBuilder + fn name(&self) -> PyExpr { + self.0.name().into() } /// Selects the node type field for filtering. /// /// Returns: /// filter.NodeTypeFilterBuilder: - #[staticmethod] - fn node_type() -> PyNodeTypeFilterBuilder { - PyNodeTypeFilterBuilder + fn node_type(&self) -> PyExpr { + self.0.node_type().into() } /// Selects incoming node degree for filtering. /// /// Returns: /// filter.FilterOps - #[staticmethod] - fn in_degree<'py>(py: Python<'py>) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(Arc::new(NodeFilter.in_degree())) + fn in_degree(&self) -> PyExpr { + self.0.in_degree().into() } /// Selects total node degree for filtering. /// /// Returns: /// filter.FilterOps - #[staticmethod] - fn degree<'py>(py: Python<'py>) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(Arc::new(NodeFilter.degree())) + fn degree(&self) -> PyExpr { + self.0.degree().into() } /// Selects outgoing node degree for filtering. /// /// Returns: /// filter.FilterOps - #[staticmethod] - fn out_degree<'py>(py: Python<'py>) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(Arc::new(NodeFilter.out_degree())) + fn out_degree(&self) -> PyExpr { + self.0.out_degree().into() } /// Filters a node property by name. @@ -377,14 +103,8 @@ impl PyNodeFilter { /// /// Returns: /// filter.PropertyFilterOps: - #[staticmethod] - fn property<'py>( - py: Python<'py>, - name: String, - ) -> PyResult> { - let b: PropertyFilterBuilder = - PropertyFilterFactory::property(&NodeFilter, name); - b.into_pyobject(py) + fn property(&self, name: String) -> PyExpr { + self.0.property(name).into() } /// Filters a node metadata field by name. @@ -396,11 +116,8 @@ impl PyNodeFilter { /// /// Returns: /// filter.FilterOps: - #[staticmethod] - fn metadata<'py>(py: Python<'py>, name: String) -> PyResult> { - let b: MetadataFilterBuilder = - PropertyFilterFactory::metadata(&NodeFilter, name); - b.into_pyobject(py) + fn metadata(&self, name: String) -> PyExpr { + self.0.metadata(name).into() } /// Restricts node evaluation to the given time window. @@ -413,9 +130,8 @@ impl PyNodeFilter { /// /// Returns: /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn window(start: EventTime, end: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.window(start, end))) + fn window(&self, start: EventTime, end: EventTime) -> PyNodeFilter { + self.0.window(start, end).into() } /// Restricts node evaluation to a single point in time. @@ -425,9 +141,8 @@ impl PyNodeFilter { /// /// Returns: /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn at(time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.at(time))) + fn at(&self, time: EventTime) -> PyNodeFilter { + self.0.at(time).into() } /// Restricts node evaluation to times strictly after the given time. @@ -437,9 +152,8 @@ impl PyNodeFilter { /// /// Returns: /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn after(time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.after(time))) + fn after(&self, time: EventTime) -> PyNodeFilter { + self.0.after(time).into() } /// Restricts node evaluation to times strictly before the given time. @@ -449,18 +163,16 @@ impl PyNodeFilter { /// /// Returns: /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn before(time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.before(time))) + fn before(&self, time: EventTime) -> PyNodeFilter { + self.0.before(time).into() } /// Evaluates filters against the latest available state of each node. /// /// Returns: /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn latest() -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.latest())) + fn latest(&self) -> PyNodeFilter { + self.0.latest().into() } /// Evaluates filters against a snapshot of the graph at a given time. @@ -470,18 +182,16 @@ impl PyNodeFilter { /// /// Returns: /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn snapshot_at(time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.snapshot_at(time))) + fn snapshot_at(&self, time: EventTime) -> PyNodeFilter { + self.0.snapshot_at(time).into() } /// Evaluates filters against the most recent snapshot of the graph. /// /// Returns: /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn snapshot_latest() -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.snapshot_latest())) + fn snapshot_latest(&self) -> PyNodeFilter { + self.0.snapshot_latest().into() } /// Restricts evaluation to nodes belonging to the given layer. @@ -491,9 +201,8 @@ impl PyNodeFilter { /// /// Returns: /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn layer(layer: String) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.layer(layer))) + fn layer(&self, layer: String) -> PyNodeFilter { + self.0.layer(layer).into() } /// Restricts evaluation to nodes belonging to any of the given layers. @@ -503,18 +212,16 @@ impl PyNodeFilter { /// /// Returns: /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn layers(layers: FromIterable) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.layer(layers))) + fn layers(&self, layers: FromIterable) -> PyNodeFilter { + self.0.layer(layers).into() } /// Matches nodes that have at least one event in the current view. /// /// Returns: /// filter.FilterExpr: - #[staticmethod] - fn is_active() -> PyFilterExpr { - PyFilterExpr(Arc::new(NodeFilter.is_active())) + fn is_active(&self) -> PyNodeFilter { + self.0.is_active().into() } /// Build a node filter from a boolean column of an existing node-state result. @@ -525,10 +232,9 @@ impl PyNodeFilter { /// /// Returns: /// filter.FilterExpr: - #[staticmethod] - fn by_state_column(state: &PyOutputNodeState, col: String) -> PyResult { + fn by_state_column(&self, state: &PyOutputNodeState, col: String) -> PyResult { let op = NodeStateBoolColOp::new(&state.inner, &col) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; - Ok(PyFilterExpr(Arc::new(op))) + Ok(PyExpr(Arc::new(op))) } } diff --git a/raphtory/src/python/filter/property_expr.rs b/raphtory/src/python/filter/property_expr.rs index e619465b18..8a334bac69 100644 --- a/raphtory/src/python/filter/property_expr.rs +++ b/raphtory/src/python/filter/property_expr.rs @@ -14,171 +14,14 @@ use crate::{ db::graph::views::filter::model::{ - edge_filter::EdgeEndpointWrapper, - node_expr::{DynPropertyExpr, DynTemporalPropertyExpr, EntityExpr}, - DynEdgeViewFilterOps, DynEdgeViewProps, DynNodeViewProps, DynView, MetadataExpr, - PropertyExpr, ViewWrapOps, + edge_filter::EdgeEndpointWrapper, node_expr::EntityExpr, DynEdgeViewFilterOps, + DynEdgeViewProps, DynNodeViewProps, MetadataExpr, PropertyExpr, ViewWrapOps, }, python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; -use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; +use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; - -/// Python wrapper over a chainable property expression. -/// -/// Represents a "property access" plus optional aggregator / quantifier -/// qualifiers (`first`, `len`, `sum`, `any`, …). Comparison operators -/// (`==`, `<`, `is_in`, …) terminate the chain by returning a -/// `filter.FilterExpr`. -/// -/// Combine `FilterExpr`s with `&`, `|`, `~`. -#[pyclass(frozen, name = "FilterOps", module = "raphtory.filter", subclass)] -pub struct PyPropertyExprBuilder(pub Arc); - -impl PyPropertyExprBuilder { - pub fn wrap(t: T) -> Self { - Self(Arc::new(t)) - } - - pub fn from_arc(inner: Arc) -> Self { - Self(inner) - } -} - -#[pymethods] -impl PyPropertyExprBuilder { - fn __eq__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_eq(value)) - } - - fn __ne__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_ne(value)) - } - - fn __lt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_lt(value)) - } - - fn __le__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_le(value)) - } - - fn __gt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_gt(value)) - } - - fn __ge__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_ge(value)) - } - - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_in(values.into_iter().collect())) - } - - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_not_in(values.into_iter().collect())) - } - - fn is_none(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_none()) - } - - fn is_some(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_some()) - } - - fn starts_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_starts_with(value)) - } - - fn ends_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_ends_with(value)) - } - - fn contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_contains(value)) - } - - fn not_contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_not_contains(value)) - } - - fn fuzzy_search( - &self, - prop_value: Prop, - levenshtein_distance: usize, - prefix_match: bool, - ) -> PyFilterExpr { - PyFilterExpr( - self.0 - .dyn_fuzzy_search(prop_value, levenshtein_distance, prefix_match), - ) - } - - pub fn first(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_first()) - } - - pub fn last(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_last()) - } - - pub fn any(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_any()) - } - - pub fn all(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_all()) - } - - fn len(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_len()) - } - - fn sum(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_sum()) - } - - fn avg(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_avg()) - } - - fn min(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_min()) - } - - fn max(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_max()) - } -} - -/// Python wrapper that adds `.temporal()` to the property-expression chain. -/// -/// Exported as: `filter.PropertyFilterOps`. -#[pyclass( - frozen, - name = "PropertyFilterOps", - module = "raphtory.filter", - extends = PyPropertyExprBuilder -)] -pub struct PyPropertyFilterBuilder(pub(crate) Arc); - -impl PyPropertyFilterBuilder { - pub(crate) fn from_arc(inner: Arc) -> Self { - Self(inner) - } -} - -#[pymethods] -impl PyPropertyFilterBuilder { - /// Switch to temporal evaluation — chain methods now operate over the - /// list of values at each timestamp. - fn temporal(&self) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(self.0.dyn_temporal()) - } -} - // ───────────────────────────────────────────────────────────────────────────── // IntoPyObject bridges — used by `Node.property(name)` / `Edge.property(name)` // etc. to surface a `PyPropertyFilterBuilder` from the typed Rust expression. @@ -258,95 +101,6 @@ impl<'py> IntoPyObject<'py> for PyPropertyFilterBuilder { } } -// ───────────────────────────────────────────────────────────────────────────── -// View-builder wrappers (graph / node / edge views restricting when+where -// predicates are evaluated, then exposing `.property()` / `.metadata()` etc.) -// ───────────────────────────────────────────────────────────────────────────── - -/// Graph-level view filter — composes time / snapshot / layer restrictions -/// before applying node or edge predicates. -#[pyclass( - name = "ViewFilterBuilder", - module = "raphtory.filter", - extends = PyFilterExpr, - frozen -)] -pub struct PyViewFilterBuilder(pub(crate) DynView); - -#[pymethods] -impl PyViewFilterBuilder { - /// Restricts evaluation to events in the given time window. - /// - /// The window is inclusive of `start` and exclusive of `end`. - /// - /// Arguments: - /// start (int): Start time. - /// end (int): End time. - /// - /// Returns: - /// filter.ViewFilterBuilder: - fn window(&self, start: EventTime, end: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(self.0.clone().window(start, end)) - } - - /// Restricts evaluation to a single point in time. - /// - /// Arguments: - /// time (int): Event time. - /// - /// Returns: - /// filter.ViewFilterBuilder: - fn at(&self, time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(self.0.clone().at(time)) - } - - /// Restricts evaluation to times strictly after the given time. - fn after(&self, time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(self.0.clone().after(time)) - } - - /// Restricts evaluation to times strictly before the given time. - fn before(&self, time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(self.0.clone().before(time)) - } - - /// Evaluates against the latest available state. - fn latest(&self) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().latest())) - } - - /// Evaluates against a snapshot of the graph at a specific time. - fn snapshot_at(&self, time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().snapshot_at(time))) - } - - /// Evaluates against the most recent snapshot of the graph. - fn snapshot_latest(&self) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().snapshot_latest())) - } - - /// Restricts evaluation to a single layer. - fn layer(&self, layer: String) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().layer(layer))) - } - - /// Restricts evaluation to any of the given layers. - fn layers(&self, layers: FromIterable) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(self.0.clone().layer(layers))) - } -} - -impl<'py> IntoPyObject<'py> for PyViewFilterBuilder { - type Target = PyViewFilterBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyFilterExpr(self.0.clone()); - Bound::new(py, (self, parent)) - } -} - /// Node-side view + property predicate builder. #[pyclass( name = "NodeViewPropsFilterBuilder", diff --git a/raphtory/src/python/graph/properties/temporal_props.rs b/raphtory/src/python/graph/properties/temporal_props.rs index bde0abff85..27c1a1d879 100644 --- a/raphtory/src/python/graph/properties/temporal_props.rs +++ b/raphtory/src/python/graph/properties/temporal_props.rs @@ -225,7 +225,7 @@ impl From for PyTemporalPropCmp { } } -py_eq!(Py PyTemporalPropCmp); +py_eq!(PyTemporalProp, PyTemporalPropCmp); #[pymethods] impl PyTemporalProp { diff --git a/raphtory/src/python/packages/base_modules.rs b/raphtory/src/python/packages/base_modules.rs index 24650ed949..23e183b73f 100644 --- a/raphtory/src/python/packages/base_modules.rs +++ b/raphtory/src/python/packages/base_modules.rs @@ -19,7 +19,7 @@ use crate::{ properties::{ MetadataView, PropertiesView, PyMetadata, PyPropHistItemsList, PyPropHistItemsListList, PyPropHistValueList, PyPropHistValueListList, - PyPropValueList, PyPropValueListList, PyProperties, Py + PyPropValueList, PyPropValueListList, PyProperties, PyTemporalProp, PyTemporalPropList, PyTemporalPropListList, PyTemporalProperties, PyTemporalPropsList, PyTemporalPropsListList, }, @@ -93,7 +93,7 @@ pub fn add_raphtory_classes(m: &Bound) -> PyResult<()> { MetadataView, PyTemporalProperties, PropertiesView, - Py + PyTemporalProp, PyEventTime, PyOptionalEventTime, PyHistory, From 4dea8f834cc541bdc0e4a13727448ef675b864a3 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:09:58 +0100 Subject: [PATCH 066/155] ref --- .../views/filter/model/edge_expr/filters.rs | 36 +- .../graph/views/filter/model/edge_expr/mod.rs | 1 - .../src/db/graph/views/filter/model/mod.rs | 261 ++++++++++- .../views/filter/model/node_expr/filters.rs | 423 ++++-------------- 4 files changed, 340 insertions(+), 381 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index a2b65e04e6..a6585a2bbd 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -31,27 +31,7 @@ use crate::{ use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; use crate::db::graph::views::filter::exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph; -use crate::db::graph::views::filter::model::ExplodedEdgeFilter; -// ───────────────────────────────────────────────────────────────────────────── -// validate helpers -// ───────────────────────────────────────────────────────────────────────────── - -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(()) -} - +use crate::db::graph::views::filter::model::{validate_binary_op, validate_string_op, ExplodedEdgeFilter}; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpExpr // ───────────────────────────────────────────────────────────────────────────── @@ -209,20 +189,6 @@ where } } -// ───────────────────────────────────────────────────────────────────────────── -// validate_string_op — reject non-string prop types at compile time -// ───────────────────────────────────────────────────────────────────────────── - -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(()) -} - // ───────────────────────────────────────────────────────────────────────────── // StringExpr — string expression filter for edges // ───────────────────────────────────────────────────────────────────────────── diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index 586c4b51e3..f8eec08b8a 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -30,7 +30,6 @@ use raphtory_api::core::entities::{ use raphtory_storage::graph::graph::GraphStorage; use std::sync::Arc; -pub mod exprs; pub mod filters; pub mod ops; diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index e2ef8f8562..dbc461e5bc 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -34,7 +34,6 @@ pub use crate::{ use crate::{ db::{ api::{ - properties::TemporalPropertyView, state::{ ops::{filter::NO_FILTER, Const}, NodeOp, @@ -70,13 +69,10 @@ use crate::{ pub use node_filter::CompositeNodeFilter; use raphtory_api::core::{ entities::{properties::prop::Prop, Layer}, - storage::{ - arc_str::ArcStr, - timeindex::{AsTime, EventTime}, - }, + storage::timeindex::{AsTime, EventTime}, utils::time::IntoTime, }; -use std::{marker::PhantomData, ops::Deref, sync::Arc}; +use std::{ops::Deref, sync::Arc}; pub mod and_filter; pub mod degree_filter; @@ -479,6 +475,7 @@ use crate::db::graph::views::filter::model::{ node_expr::{CreateOp, EntityExpr}, }; use edge_expr::EdgeOp; +use raphtory_api::core::entities::properties::prop::PropType; pub trait TryAsCompositeFilter: Send + Sync { fn try_as_composite_node_filter(&self) -> Result; @@ -947,3 +944,255 @@ impl EdgeViewFilterOps for DynEdgeViewProps { self.deref().dyn_is_self_loop() } } + +// ───────────────────────────────────────────────────────────────────────────── +// EntityExprFilterOps — comparison and set operators on any EntityExpr +// ───────────────────────────────────────────────────────────────────────────── + +/// Comparison, string, set, and presence operators on any [`CreateOp`]. +/// +/// `.any()` / `.all()` are terminal: they wrap `self` in `AnyExpr`/`AllExpr` and compare the +/// result to `Bool(true)`. For element-wise comparison before reduction, chain in order: +/// `.gt(10i64).any()` not `.any().gt(10i64)`. +/// +/// ```rust,ignore +/// NodeFilter.degree().gt(2usize) +/// NodeFilter.degree().sum() // TODO: Throw an error +/// NodeFilter.out_degree().gt(NodeFilter.in_degree()) +/// NodeFilter.property("age").gt(30i64) +/// NodeFilter.property("score").temporal().gt(10i64).any() +/// ``` +pub trait EntityExprFilterOps: EntityExpr + Sized { + fn gt(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Gt, rhs, entity) + } + + fn ge(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Ge, rhs, entity) + } + + fn lt(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Lt, rhs, entity) + } + + fn le(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Le, rhs, entity) + } + + fn eq(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Eq, rhs, entity) + } + + fn ne(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Ne, rhs, entity) + } + + fn starts_with(self, rhs: R) -> StringExpr { + let entity = self.entity(); + StringExpr::new(self, StringOp::StartsWith, rhs, entity) + } + + fn ends_with(self, rhs: R) -> StringExpr { + let entity = self.entity(); + StringExpr::new(self, StringOp::EndsWith, rhs, entity) + } + + fn contains(self, rhs: R) -> StringExpr { + let entity = self.entity(); + StringExpr::new(self, StringOp::Contains, rhs, entity) + } + + fn not_contains(self, rhs: R) -> StringExpr { + let entity = self.entity(); + StringExpr::new(self, StringOp::NotContains, rhs, entity) + } + + fn fuzzy_search( + self, + rhs: R, + levenshtein_distance: usize, + prefix_match: bool, + ) -> StringExpr { + let entity = self.entity(); + StringExpr::new( + self, + StringOp::FuzzySearch { + levenshtein_distance, + prefix_match, + }, + rhs, + entity, + ) + } + + fn is_some(self) -> UnaryExpr { + let entity = self.entity(); + UnaryExpr { + expr: self, + op: UnaryOp::IsSome, + entity, + } + } + + fn is_none(self) -> UnaryExpr { + let entity = self.entity(); + UnaryExpr { + expr: self, + op: UnaryOp::IsNone, + entity, + } + } + + fn is_in>( + self, + values: impl IntoIterator, + ) -> PropValueSetExpr { + let entity = self.entity(); + PropValueSetExpr { + expr: self, + values: values.into_iter().map(Into::into).collect(), + op: SetOp::IsIn, + entity, + } + } + + fn is_not_in>( + self, + values: impl IntoIterator, + ) -> PropValueSetExpr { + let entity = self.entity(); + PropValueSetExpr { + expr: self, + values: values.into_iter().map(Into::into).collect(), + op: SetOp::IsNotIn, + entity, + } + } + + fn is_true(self) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(true), entity) + } + + fn is_false(self) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(false), entity) + } + + fn not(self) -> BinaryCmpExpr { + self.eq(Prop::Bool(false)) + } + + fn any(self) -> BinaryCmpExpr, Prop, Self::Marker> { + let entity = self.entity(); + BinaryCmpExpr::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true), entity) + } + + fn all(self) -> BinaryCmpExpr, Prop, Self::Marker> { + let entity = self.entity(); + BinaryCmpExpr::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true), entity) + } +} + +impl EntityExprFilterOps for E {} + +/// Reject ordering operators on boolean properties. +//. TODO: Also check if both the types are comparable. +pub 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`). +pub 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(()) +} + +/// Pick the more specific of the two known prop types. +/// +/// Compiled `NodeOp`s and `EntityExpr`s may both have a known prop type, but +/// expression-level info (e.g. `DegreeExpr::prop_type()` → U64) is not always +/// propagated through generic wrappers like `Map`. Prefer whichever side +/// has a concrete type so validation can fire early. +pub fn resolved_prop_type(expr_pt: PropType, op_pt: PropType) -> PropType { + if expr_pt != PropType::Empty { + expr_pt + } else { + op_pt + } +} + +/// Reject a constant RHS value whose type cannot be coerced to the LHS type. +/// +/// Only fires when both sides are known and the RHS is a literal/const. Defers +/// to runtime when the LHS type is unknown (`PropType::Empty`) or the RHS isn't +/// a const value. +pub fn validate_const_castable(lhs_pt: &PropType, rhs_const: Option<&Prop>) -> Result<(), GraphError> { + if *lhs_pt == PropType::Empty { + return Ok(()); + } + if let Some(rhs) = rhs_const { + if rhs.dtype() != *lhs_pt && rhs.clone().try_cast(lhs_pt.clone()).is_none() { + return Err(GraphError::InvalidFilter(format!( + "value {:?} of type {} cannot be coerced to {}", + rhs, + rhs.dtype(), + lhs_pt + ))); + } + } + Ok(()) +} + +/// Cast every value in an `is_in`/`is_not_in` set to the LHS type. +/// +/// If the LHS type is unknown (`PropType::Empty`), the values are returned +/// unchanged and coercion is deferred to runtime. Otherwise, any value whose +/// type cannot be coerced produces `Err(InvalidFilter)`. Successful casts are +/// substituted so the runtime set comparison sees same-typed values. +pub fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result, GraphError> { + if *lhs_pt == PropType::Empty { + return Ok(values); + } + values + .into_iter() + .map(|v| { + if v.dtype() == *lhs_pt { + Ok(v) + } else { + let original_dtype = v.dtype(); + v.clone().try_cast(lhs_pt.clone()).ok_or_else(|| { + GraphError::InvalidFilter(format!( + "value {:?} of type {} cannot be coerced to {}", + v, original_dtype, lhs_pt + )) + }) + } + }) + .collect() +} 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 2d289ccc5a..0338f83d38 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 @@ -68,6 +68,7 @@ use crate::{ }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; +use crate::db::graph::views::filter::model::{coerce_set_values, resolved_prop_type, validate_binary_op, validate_const_castable, validate_string_op}; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpExpr — binary expression filter // ───────────────────────────────────────────────────────────────────────────── @@ -110,101 +111,6 @@ impl BinaryCmpExpr { impl ComposableFilter for BinaryCmpExpr {} -/// Reject ordering operators on boolean properties. -//. TODO: Also check if both the types are comparable. -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(()) -} - -/// Pick the more specific of the two known prop types. -/// -/// Compiled `NodeOp`s and `EntityExpr`s may both have a known prop type, but -/// expression-level info (e.g. `DegreeExpr::prop_type()` → U64) is not always -/// propagated through generic wrappers like `Map`. Prefer whichever side -/// has a concrete type so validation can fire early. -fn resolved_prop_type(expr_pt: PropType, op_pt: PropType) -> PropType { - if expr_pt != PropType::Empty { - expr_pt - } else { - op_pt - } -} - -/// Reject a constant RHS value whose type cannot be coerced to the LHS type. -/// -/// Only fires when both sides are known and the RHS is a literal/const. Defers -/// to runtime when the LHS type is unknown (`PropType::Empty`) or the RHS isn't -/// a const value. -fn validate_const_castable(lhs_pt: &PropType, rhs_const: Option<&Prop>) -> Result<(), GraphError> { - if *lhs_pt == PropType::Empty { - return Ok(()); - } - if let Some(rhs) = rhs_const { - if rhs.dtype() != *lhs_pt && rhs.clone().try_cast(lhs_pt.clone()).is_none() { - return Err(GraphError::InvalidFilter(format!( - "value {:?} of type {} cannot be coerced to {}", - rhs, - rhs.dtype(), - lhs_pt - ))); - } - } - Ok(()) -} - -/// Cast every value in an `is_in`/`is_not_in` set to the LHS type. -/// -/// If the LHS type is unknown (`PropType::Empty`), the values are returned -/// unchanged and coercion is deferred to runtime. Otherwise, any value whose -/// type cannot be coerced produces `Err(InvalidFilter)`. Successful casts are -/// substituted so the runtime set comparison sees same-typed values. -fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result, GraphError> { - if *lhs_pt == PropType::Empty { - return Ok(values); - } - values - .into_iter() - .map(|v| { - if v.dtype() == *lhs_pt { - Ok(v) - } else { - let original_dtype = v.dtype(); - v.clone().try_cast(lhs_pt.clone()).ok_or_else(|| { - GraphError::InvalidFilter(format!( - "value {:?} of type {} cannot be coerced to {}", - v, original_dtype, lhs_pt - )) - }) - } - }) - .collect() -} - impl CreateFilter for BinaryCmpExpr where L: CreateOp, @@ -288,6 +194,34 @@ where } } +impl EntityExpr +for BinaryCmpExpr +{ + type Marker = E; + fn entity(&self) -> Self::Marker { + self.entity + } + fn prop_type(&self) -> PropType { + // TODO: depending on the types of left and right, we should figure out the type to return here + PropType::Empty + } +} + +impl CreateOp for BinaryCmpExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + Ok(Arc::new(ListAwareCmpNodeOp { + left, + right, + op: self.op, + })) + } +} + impl TryAsCompositeFilter for BinaryCmpExpr where L: CreateOp, @@ -406,6 +340,16 @@ where } } +impl EntityExpr +for UnaryExpr +{ + type Marker = Entity; + fn entity(&self) -> Self::Marker { + self.entity + } +} + + impl TryAsCompositeFilter for UnaryExpr where E: CreateOp, @@ -534,6 +478,34 @@ impl CreateFilter for StringExpr { } } } + +impl EntityExpr +for StringExpr +{ + type Marker = Entity; + fn entity(&self) -> Self::Marker { + self.entity + } + fn prop_type(&self) -> PropType { + PropType::Empty + } +} + +impl CreateOp for StringExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + Ok(Arc::new(ListAwareStringNodeOp { + left, + right, + op: self.op, + })) + } +} + impl TryAsCompositeFilter for StringExpr where L: CreateOp, @@ -641,217 +613,8 @@ impl CreateFilter for PropValueSetExpr { } } -impl TryAsCompositeFilter for PropValueSetExpr { - 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) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// EntityExprFilterOps — comparison and set operators on any EntityExpr -// ───────────────────────────────────────────────────────────────────────────── - -/// Comparison, string, set, and presence operators on any [`CreateOp`]. -/// -/// `.any()` / `.all()` are terminal: they wrap `self` in `AnyExpr`/`AllExpr` and compare the -/// result to `Bool(true)`. For element-wise comparison before reduction, chain in order: -/// `.gt(10i64).any()` not `.any().gt(10i64)`. -/// -/// ```rust,ignore -/// NodeFilter.degree().gt(2usize) -/// NodeFilter.degree().sum() // TODO: Throw an error -/// NodeFilter.out_degree().gt(NodeFilter.in_degree()) -/// NodeFilter.property("age").gt(30i64) -/// NodeFilter.property("score").temporal().gt(10i64).any() -/// ``` -pub trait EntityExprFilterOps: EntityExpr + Sized { - fn gt(self, rhs: R) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Gt, rhs, entity) - } - - fn ge(self, rhs: R) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Ge, rhs, entity) - } - - fn lt(self, rhs: R) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Lt, rhs, entity) - } - - fn le(self, rhs: R) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Le, rhs, entity) - } - - fn eq(self, rhs: R) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Eq, rhs, entity) - } - - fn ne(self, rhs: R) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Ne, rhs, entity) - } - - fn starts_with(self, rhs: R) -> StringExpr { - let entity = self.entity(); - StringExpr::new(self, StringOp::StartsWith, rhs, entity) - } - - fn ends_with(self, rhs: R) -> StringExpr { - let entity = self.entity(); - StringExpr::new(self, StringOp::EndsWith, rhs, entity) - } - - fn contains(self, rhs: R) -> StringExpr { - let entity = self.entity(); - StringExpr::new(self, StringOp::Contains, rhs, entity) - } - - fn not_contains(self, rhs: R) -> StringExpr { - let entity = self.entity(); - StringExpr::new(self, StringOp::NotContains, rhs, entity) - } - - fn fuzzy_search( - self, - rhs: R, - levenshtein_distance: usize, - prefix_match: bool, - ) -> StringExpr { - let entity = self.entity(); - StringExpr::new( - self, - StringOp::FuzzySearch { - levenshtein_distance, - prefix_match, - }, - rhs, - entity, - ) - } - - fn is_some(self) -> UnaryExpr { - let entity = self.entity(); - UnaryExpr { - expr: self, - op: UnaryOp::IsSome, - entity, - } - } - - fn is_none(self) -> UnaryExpr { - let entity = self.entity(); - UnaryExpr { - expr: self, - op: UnaryOp::IsNone, - entity, - } - } - - fn is_in>( - self, - values: impl IntoIterator, - ) -> PropValueSetExpr { - let entity = self.entity(); - PropValueSetExpr { - expr: self, - values: values.into_iter().map(Into::into).collect(), - op: SetOp::IsIn, - entity, - } - } - - fn is_not_in>( - self, - values: impl IntoIterator, - ) -> PropValueSetExpr { - let entity = self.entity(); - PropValueSetExpr { - expr: self, - values: values.into_iter().map(Into::into).collect(), - op: SetOp::IsNotIn, - entity, - } - } - - fn is_true(self) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(true), entity) - } - - fn is_false(self) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(false), entity) - } - - fn not(self) -> BinaryCmpExpr { - self.eq(Prop::Bool(false)) - } - - fn any(self) -> BinaryCmpExpr, Prop, Self::Marker> { - let entity = self.entity(); - BinaryCmpExpr::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true), entity) - } - - fn all(self) -> BinaryCmpExpr, Prop, Self::Marker> { - let entity = self.entity(); - BinaryCmpExpr::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true), entity) - } -} - -impl EntityExprFilterOps for E {} - -// ───────────────────────────────────────────────────────────────────────────── -// NodeExpr impls for filter types — enables mid-chain use before .any()/.all() -// -// e.g. temporal().sum().gt(5).any() -// temporal().contains("rock").all() -// temporal().is_in([...]).any() -// ───────────────────────────────────────────────────────────────────────────── - -impl EntityExpr - for BinaryCmpExpr -{ - type Marker = E; - fn entity(&self) -> Self::Marker { - self.entity - } - fn prop_type(&self) -> PropType { - // TODO: depending on the types of left and right, we should figure out the type to return here - PropType::Empty - } -} - -impl CreateOp for BinaryCmpExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; - Ok(Arc::new(ListAwareCmpNodeOp { - left, - right, - op: self.op, - })) - } -} - -impl EntityExpr - for StringExpr +impl EntityExpr +for PropValueSetExpr { type Marker = Entity; fn entity(&self) -> Self::Marker { @@ -862,52 +625,34 @@ impl CreateOp for StringExpr { + +impl CreateOp for PropValueSetExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, ) -> Result> + 'g>, GraphError> { - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; - Ok(Arc::new(ListAwareStringNodeOp { - left, - right, + let inner = self.expr.create_node_op(graph)?; + Ok(Arc::new(ListAwareSetNodeOp { + inner, + values: self.values.clone(), op: self.op, })) } } -impl EntityExpr - for PropValueSetExpr -{ - type Marker = Entity; - fn entity(&self) -> Self::Marker { - self.entity - } - fn prop_type(&self) -> PropType { - PropType::Empty + +impl TryAsCompositeFilter for PropValueSetExpr { + fn try_as_composite_node_filter(&self) -> Result { + Err(GraphError::NotSupported) } -} -impl EntityExpr - for UnaryExpr -{ - type Marker = Entity; - fn entity(&self) -> Self::Marker { - self.entity + fn try_as_composite_edge_filter(&self) -> Result { + Err(GraphError::NotSupported) } -} -impl CreateOp for PropValueSetExpr { - fn create_node_op<'g, G: GraphView + 'g>( + fn try_as_composite_exploded_edge_filter( &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_node_op(graph)?; - Ok(Arc::new(ListAwareSetNodeOp { - inner, - values: self.values.clone(), - op: self.op, - })) + ) -> Result { + Err(GraphError::NotSupported) } } From b305f1b3db172e208bb0557b88ed4a8fdee281ea Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:13:34 +0100 Subject: [PATCH 067/155] ref --- .../db/graph/views/filter/model/and_filter.rs | 50 +++++++++---------- .../views/filter/model/edge_expr/filters.rs | 10 ++-- .../db/graph/views/filter/model/or_filter.rs | 50 +++++++++---------- 3 files changed, 55 insertions(+), 55 deletions(-) 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 548d46a073..81ca7a064b 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -77,31 +77,6 @@ impl CreateFilter for AndFilter { } } -impl TryAsCompositeFilter for AndFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::And( - Box::new(self.left.try_as_composite_node_filter()?), - Box::new(self.right.try_as_composite_node_filter()?), - )) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::And( - Box::new(self.left.try_as_composite_edge_filter()?), - Box::new(self.right.try_as_composite_edge_filter()?), - )) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::And( - Box::new(self.left.try_as_composite_exploded_edge_filter()?), - Box::new(self.right.try_as_composite_exploded_edge_filter()?), - )) - } -} - impl EntityExpr for AndFilter where L: EntityExpr, @@ -138,3 +113,28 @@ where Ok(Arc::new(AndBoolEdgeOp { left, right })) } } + +impl TryAsCompositeFilter for AndFilter { + fn try_as_composite_node_filter(&self) -> Result { + Ok(CompositeNodeFilter::And( + Box::new(self.left.try_as_composite_node_filter()?), + Box::new(self.right.try_as_composite_node_filter()?), + )) + } + + fn try_as_composite_edge_filter(&self) -> Result { + Ok(CompositeEdgeFilter::And( + Box::new(self.left.try_as_composite_edge_filter()?), + Box::new(self.right.try_as_composite_edge_filter()?), + )) + } + + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + Ok(CompositeExplodedEdgeFilter::And( + Box::new(self.left.try_as_composite_exploded_edge_filter()?), + Box::new(self.right.try_as_composite_exploded_edge_filter()?), + )) + } +} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index a6585a2bbd..a7de91d3ba 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -10,6 +10,8 @@ use super::{ }, EdgeOp, }; +use crate::db::graph::views::filter::exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph; +use crate::db::graph::views::filter::model::{validate_binary_op, validate_string_op, ExplodedEdgeFilter}; pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpExpr, StringExpr, UnaryExpr}; use crate::{ db::{ @@ -17,8 +19,8 @@ use crate::{ graph::views::filter::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, model::{ - edge_filter::{CompositeEdgeFilter, EdgeFilter}, - filter_operator::BinaryOp, + edge_filter::{CompositeEdgeFilter, EdgeFilter} + , node_expr::{filters::PropValueSetExpr, CreateOp}, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, TryAsCompositeFilter, @@ -28,10 +30,8 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use raphtory_api::core::entities::properties::prop::Prop; use std::sync::Arc; -use crate::db::graph::views::filter::exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph; -use crate::db::graph::views::filter::model::{validate_binary_op, validate_string_op, ExplodedEdgeFilter}; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpExpr // ───────────────────────────────────────────────────────────────────────────── 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 67abb92467..b08ff10176 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -67,31 +67,6 @@ impl CreateFilter for OrFilter { } } -impl TryAsCompositeFilter for OrFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Or( - Box::new(self.left.try_as_composite_node_filter()?), - Box::new(self.right.try_as_composite_node_filter()?), - )) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::Or( - Box::new(self.left.try_as_composite_edge_filter()?), - Box::new(self.right.try_as_composite_edge_filter()?), - )) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::Or( - Box::new(self.left.try_as_composite_exploded_edge_filter()?), - Box::new(self.right.try_as_composite_exploded_edge_filter()?), - )) - } -} - impl EntityExpr for OrFilter where L: EntityExpr, @@ -127,3 +102,28 @@ where Ok(Arc::new(OrBoolEdgeOp { left, right })) } } + +impl TryAsCompositeFilter for OrFilter { + fn try_as_composite_node_filter(&self) -> Result { + Ok(CompositeNodeFilter::Or( + Box::new(self.left.try_as_composite_node_filter()?), + Box::new(self.right.try_as_composite_node_filter()?), + )) + } + + fn try_as_composite_edge_filter(&self) -> Result { + Ok(CompositeEdgeFilter::Or( + Box::new(self.left.try_as_composite_edge_filter()?), + Box::new(self.right.try_as_composite_edge_filter()?), + )) + } + + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + Ok(CompositeExplodedEdgeFilter::Or( + Box::new(self.left.try_as_composite_exploded_edge_filter()?), + Box::new(self.right.try_as_composite_exploded_edge_filter()?), + )) + } +} From a95cb118f32db1ab1e628f91d1b83bbeb06b4631 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:30:19 +0100 Subject: [PATCH 068/155] composable filters need not be exprs --- .../db/graph/views/filter/model/and_filter.rs | 44 +------------------ .../views/filter/model/node_expr/filters.rs | 6 +-- .../db/graph/views/filter/model/or_filter.rs | 43 +----------------- 3 files changed, 7 insertions(+), 86 deletions(-) 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 81ca7a064b..39116f378a 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -1,16 +1,14 @@ use crate::{ db::{ api::{ - state::ops::{filter::AndOp, NodeFilterOp, NodeOp}, + state::ops::{filter::AndOp, NodeFilterOp}, view::internal::GraphView, }, graph::views::filter::{ and_filtered_graph::AndFilteredGraph, model::{ - edge_expr::{ops::AndBoolEdgeOp, EdgeOp}, edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, - node_expr::{ops::AndBoolNodeOp, CreateOp, EntityExpr}, node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, }, @@ -20,9 +18,8 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use raphtory_api::core::entities::properties::prop::Prop; use raphtory_storage::layer_ops::InternalLayerOps; -use std::{fmt, fmt::Display, sync::Arc}; +use std::{fmt, fmt::Display}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AndFilter { @@ -77,43 +74,6 @@ impl CreateFilter for AndFilter { } } -impl EntityExpr for AndFilter -where - L: EntityExpr, - R: EntityExpr, -{ - type Marker = L::Marker; - - fn entity(&self) -> Self::Marker { - self.left.entity() - } -} - -impl CreateOp for AndFilter -where - L: CreateOp, - R: CreateOp, - R: EntityExpr, -{ - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; - Ok(Arc::new(AndBoolNodeOp { left, right })) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(AndBoolEdgeOp { left, right })) - } -} - impl TryAsCompositeFilter for AndFilter { fn try_as_composite_node_filter(&self) -> Result { Ok(CompositeNodeFilter::And( 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 0338f83d38..47e2ee7c57 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 @@ -44,9 +44,10 @@ use super::{ ops::{ BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, - }, - AllExpr, AnyExpr, CreateOp, EntityExpr, + } + , CreateOp, EntityExpr, }; +use crate::db::graph::views::filter::model::{coerce_set_values, resolved_prop_type, validate_binary_op, validate_const_castable, validate_string_op}; use crate::{ db::{ api::{ @@ -68,7 +69,6 @@ use crate::{ }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; -use crate::db::graph::views::filter::model::{coerce_set_values, resolved_prop_type, validate_binary_op, validate_const_castable, validate_string_op}; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpExpr — binary expression filter // ───────────────────────────────────────────────────────────────────────────── 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 b08ff10176..5b14909818 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -1,15 +1,13 @@ use crate::{ db::{ api::{ - state::ops::{filter::OrOp, NodeFilterOp, NodeOp}, + state::ops::{filter::OrOp, NodeFilterOp}, view::internal::GraphView, }, graph::views::filter::{ model::{ - edge_expr::{ops::OrBoolEdgeOp, EdgeOp}, edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, - node_expr::{ops::OrBoolNodeOp, CreateOp, EntityExpr}, node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, }, @@ -20,8 +18,7 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use raphtory_api::core::entities::properties::prop::Prop; -use std::{fmt, fmt::Display, sync::Arc}; +use std::{fmt, fmt::Display}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct OrFilter { @@ -67,42 +64,6 @@ impl CreateFilter for OrFilter { } } -impl EntityExpr for OrFilter -where - L: EntityExpr, - R: EntityExpr, -{ - type Marker = L::Marker; - fn entity(&self) -> Self::Marker { - self.left.entity() - } -} - -impl CreateOp for OrFilter -where - L: CreateOp, - R: CreateOp, - R: EntityExpr, -{ - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; - Ok(Arc::new(OrBoolNodeOp { left, right })) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(OrBoolEdgeOp { left, right })) - } -} - impl TryAsCompositeFilter for OrFilter { fn try_as_composite_node_filter(&self) -> Result { Ok(CompositeNodeFilter::Or( From b53c47321c237b9c6d48cc04cfcaebf3284ce0e8 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:32:42 +0100 Subject: [PATCH 069/155] ref --- raphtory/src/db/graph/views/filter/model/mod.rs | 11 +++++++---- raphtory/src/python/filter/node_expr.rs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index dbc461e5bc..2318ed3e79 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -15,9 +15,9 @@ pub use crate::{ UnaryOp, }, node_expr::{ - AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, EntityAggOps, - EntityExprFilterOps, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, - PropValueSetExpr, StringExpr, SumExpr, TemporalPropExpr, UnaryExpr, + AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, EntityAggOps, FirstExpr, + LastExpr, LenExpr, MaxExpr, MinExpr, PropValueSetExpr, StringExpr, SumExpr, + TemporalPropExpr, UnaryExpr, }, node_filter::{NodeFilter, NodeFilterFactory}, not_filter::NotFilter, @@ -1152,7 +1152,10 @@ pub fn resolved_prop_type(expr_pt: PropType, op_pt: PropType) -> PropType { /// Only fires when both sides are known and the RHS is a literal/const. Defers /// to runtime when the LHS type is unknown (`PropType::Empty`) or the RHS isn't /// a const value. -pub fn validate_const_castable(lhs_pt: &PropType, rhs_const: Option<&Prop>) -> Result<(), GraphError> { +pub fn validate_const_castable( + lhs_pt: &PropType, + rhs_const: Option<&Prop>, +) -> Result<(), GraphError> { if *lhs_pt == PropType::Empty { return Ok(()); } diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index c4c15e297f..299fbdbab4 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -8,7 +8,7 @@ use crate::{ prelude::{EntityExprFilterOps, NodeFilter, NodeFilterFactory}, python::{graph::node_state::PyOutputNodeState, types::iterable::FromIterable}, }; -use pyo3::{pyclass, pymethods, IntoPyObject, PyResult}; +use pyo3::{pyclass, pymethods, PyResult}; use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; use crate::db::graph::views::filter::model::node_expr::DynCreateOp; From 7d3172c9cc782ee90776822c3a5efd8c6e9518ca Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:51:36 +0100 Subject: [PATCH 070/155] expose EntityExprFilterOps surface on PyExpr --- raphtory/src/python/filter/node_expr.rs | 88 ++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 299fbdbab4..ea513e0389 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -9,7 +9,7 @@ use crate::{ python::{graph::node_state::PyOutputNodeState, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods, PyResult}; -use raphtory_api::core::storage::timeindex::EventTime; +use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; use std::sync::Arc; use crate::db::graph::views::filter::model::node_expr::DynCreateOp; @@ -26,8 +26,92 @@ impl From for PyExpr { #[pymethods] impl PyExpr { fn __eq__(&self, other: &Self) -> Self { - self.0.eq(&other.0).into() + self.0.clone().eq(other.0.clone()).into() } + fn __ne__(&self, other: &Self) -> Self { + self.0.clone().ne(other.0.clone()).into() + } + fn __lt__(&self, other: &Self) -> Self { + self.0.clone().lt(other.0.clone()).into() + } + fn __le__(&self, other: &Self) -> Self { + self.0.clone().le(other.0.clone()).into() + } + fn __gt__(&self, other: &Self) -> Self { + self.0.clone().gt(other.0.clone()).into() + } + fn __ge__(&self, other: &Self) -> Self { + self.0.clone().ge(other.0.clone()).into() + } + + fn starts_with(&self, other: &Self) -> Self { + self.0.clone().starts_with(other.0.clone()).into() + } + fn ends_with(&self, other: &Self) -> Self { + self.0.clone().ends_with(other.0.clone()).into() + } + fn contains(&self, other: &Self) -> Self { + self.0.clone().contains(other.0.clone()).into() + } + fn not_contains(&self, other: &Self) -> Self { + self.0.clone().not_contains(other.0.clone()).into() + } + fn fuzzy_search( + &self, + other: &Self, + levenshtein_distance: usize, + prefix_match: bool, + ) -> Self { + self.0 + .clone() + .fuzzy_search(other.0.clone(), levenshtein_distance, prefix_match) + .into() + } + + fn is_in(&self, values: FromIterable) -> Self { + self.0.clone().is_in(values).into() + } + fn is_not_in(&self, values: FromIterable) -> Self { + self.0.clone().is_not_in(values).into() + } + + fn is_some(&self) -> Self { + self.0.clone().is_some().into() + } + fn is_none(&self) -> Self { + self.0.clone().is_none().into() + } + + fn __invert__(&self) -> Self { + self.0.clone().not().into() + } + + fn any(&self) -> Self { + self.0.clone().any().into() + } + fn all(&self) -> Self { + self.0.clone().all().into() + } + + // ── Aggregators ───────────────────────────────────────────────────── + // Require `Arc: EntityAggOps`, which isn't impl'd yet. + // Add an `impl EntityAggOps for Arc` in `dyn_expr.rs` + // (mirroring the EntityExpr / CreateOp impls there) before these will compile. + // + // fn sum(&self) -> Self { self.0.clone().sum().into() } + // fn avg(&self) -> Self { self.0.clone().avg().into() } + // fn min(&self) -> Self { self.0.clone().min().into() } + // fn max(&self) -> Self { self.0.clone().max().into() } + // fn first(&self) -> Self { self.0.clone().first().into() } + // fn last(&self) -> Self { self.0.clone().last().into() } + // fn len(&self) -> Self { self.0.clone().len().into() } + + // ── Temporal ──────────────────────────────────────────────────────── + // `.temporal()` only exists on `PropertyExpr`. To expose it on PyExpr + // you need either a separate `PyPropertyExpr` subtype, or a `dyn_temporal` + // method on `DynCreateOp` that downcasts/dispatches. + // + // fn temporal(&self) -> Self { … } } /// Constructs node filter expressions. From 1207c202b858e7e899b265260e4de3a279a474f6 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:20:36 +0100 Subject: [PATCH 071/155] expose aggregator methods on PyExpr --- .../views/filter/model/node_expr/dyn_expr.rs | 29 +++++++++++++++- raphtory/src/python/filter/node_expr.rs | 34 ++++++++++++------- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 6c226455ea..1ae281b726 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -32,7 +32,10 @@ use crate::{ }, graph::views::filter::model::{ edge_expr::EdgeOp, - node_expr::{CreateOp, EntityExpr}, + node_expr::{ + AvgExpr, CreateOp, EntityAggOps, EntityExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, + MinExpr, SumExpr, + }, EntityMarker, }, }, @@ -128,3 +131,27 @@ impl CreateOp for Arc { self.deref().dyn_create_edge_op(Arc::new(graph)) } } + +impl EntityAggOps for Arc { + 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) + } +} diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index ea513e0389..068127319d 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -12,6 +12,7 @@ use pyo3::{pyclass, pymethods, PyResult}; use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; use std::sync::Arc; use crate::db::graph::views::filter::model::node_expr::DynCreateOp; +use crate::prelude::EntityAggOps; #[pyclass(frozen, name = "Expr", module = "raphtory.filter")] #[derive(Clone)] @@ -93,18 +94,27 @@ impl PyExpr { self.0.clone().all().into() } - // ── Aggregators ───────────────────────────────────────────────────── - // Require `Arc: EntityAggOps`, which isn't impl'd yet. - // Add an `impl EntityAggOps for Arc` in `dyn_expr.rs` - // (mirroring the EntityExpr / CreateOp impls there) before these will compile. - // - // fn sum(&self) -> Self { self.0.clone().sum().into() } - // fn avg(&self) -> Self { self.0.clone().avg().into() } - // fn min(&self) -> Self { self.0.clone().min().into() } - // fn max(&self) -> Self { self.0.clone().max().into() } - // fn first(&self) -> Self { self.0.clone().first().into() } - // fn last(&self) -> Self { self.0.clone().last().into() } - // fn len(&self) -> Self { self.0.clone().len().into() } + fn sum(&self) -> Self { + self.0.clone().sum().into() + } + fn avg(&self) -> Self { + self.0.clone().avg().into() + } + fn min(&self) -> Self { + self.0.clone().min().into() + } + fn max(&self) -> Self { + self.0.clone().max().into() + } + fn first(&self) -> Self { + self.0.clone().first().into() + } + fn last(&self) -> Self { + self.0.clone().last().into() + } + fn len(&self) -> Self { + self.0.clone().len().into() + } // ── Temporal ──────────────────────────────────────────────────────── // `.temporal()` only exists on `PropertyExpr`. To expose it on PyExpr From 7b464ee67f77054d69a7134ee75d2fcd5bc1207a Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:09:43 +0100 Subject: [PATCH 072/155] wip --- .../src/db/graph/views/filter/model/mod.rs | 4 +- .../views/filter/model/node_expr/filters.rs | 2 +- raphtory/src/python/filter/edge_expr.rs | 498 ++---------------- .../src/python/filter/exploded_edge_expr.rs | 176 ++----- raphtory/src/python/filter/mod.rs | 19 +- raphtory/src/python/filter/node_expr.rs | 178 +++++-- raphtory/src/python/filter/property_expr.rs | 232 -------- 7 files changed, 247 insertions(+), 862 deletions(-) delete mode 100644 raphtory/src/python/filter/property_expr.rs diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 2318ed3e79..c7cbc93012 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -269,8 +269,8 @@ impl Metadata { #[derive(Clone)] pub struct PropertyExpr { - view_expr: E, - name: String, + pub(crate) view_expr: E, + pub(crate) name: String, } impl EntityExpr for PropertyExpr { 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 47e2ee7c57..849080d365 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 @@ -408,7 +408,7 @@ impl StringExpr { } fn with_entity(self, entity: T) -> StringExpr { - Self { + StringExpr { left: self.left, op: self.op, right: self.right, diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 9c6028586f..1d07a0c0ae 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -1,274 +1,15 @@ use crate::{ - db::{ - api::state::ops::{Id, Name, Type}, - graph::views::filter::model::{ - edge_filter::{EdgeEndpointWrapper, EdgeFilter}, - node_filter::{ops::NodeFilterOps, NodeFilter}, - property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, - EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, - }, - }, - python::{ - filter::{ - filter_expr::PyFilterExpr, - property_expr::{ - PyEdgeViewPropsFilterBuilder, - }, - }, - types::iterable::FromIterable, + db::graph::views::filter::model::{ + edge_filter::{EdgeEndpointWrapper, EdgeFilter}, + node_filter::NodeFilter, + EdgeFilterFactory, EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, + python::{filter::node_expr::PyExpr, types::iterable::FromIterable}, }; -use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyResult, Python}; -use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; +use pyo3::{pyclass, pymethods}; +use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; - -/// Filters an edge endpoint by its node ID. -/// -/// This builder produces `FilterExpr` predicates over the **source** or -/// **destination** endpoint of an edge (depending on where it was obtained). -/// -/// Examples: -/// Edge.src().id() == 1 -/// Edge.dst().id().is_in([1, 2, 3]) -/// Edge.src().id().starts_with("user:") -#[pyclass( - frozen, - name = "EdgeEndpointIdFilter", - module = "raphtory.filter", - from_py_object -)] -#[derive(Clone)] -pub struct PyEdgeEndpointIdFilterBuilder(pub EdgeEndpointWrapper); - -#[pymethods] -impl PyEdgeEndpointIdFilterBuilder { - /// Checks whether the endpoint ID is equal to the given value. - /// - /// Arguments: - /// value (int): Node ID to compare against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating equality. - fn __eq__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().eq(value))) - } - - /// Checks whether the endpoint ID is not equal to the given value. - /// - /// Arguments: - /// value (int): Node ID to compare against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating inequality. - fn __ne__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().ne(value))) - } - - /// Checks whether the endpoint ID is less than the given value (exclusive). - /// - /// Arguments: - /// value (int): Upper bound (exclusive) for the node ID. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `<` comparison. - fn __lt__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().lt(value))) - } - - /// Checks whether the endpoint ID is less than or equal to the given value. - /// - /// Arguments: - /// value (int): Upper bound (inclusive) for the node ID. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `<=` comparison. - fn __le__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().le(value))) - } - - /// Checks whether the endpoint ID is greater than the given value (exclusive). - /// - /// Arguments: - /// value (int): Lower bound (exclusive) for the node ID. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `>` comparison. - fn __gt__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().gt(value))) - } - - /// Checks whether the endpoint ID is greater than or equal to the given value. - /// - /// Arguments: - /// value (int): Lower bound (inclusive) for the node ID. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating a `>=` comparison. - fn __ge__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().ge(value))) - } - - /// Checks whether the endpoint ID is contained within the specified iterable of IDs. - /// - /// Arguments: - /// values (list[int]): Iterable of node IDs to match against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating membership. - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().is_in(values))) - } - - /// Checks whether the endpoint ID is **not** contained within the specified iterable of IDs. - /// - /// Arguments: - /// values (list[int]): Iterable of node IDs to exclude. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating non-membership. - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().is_not_in(values))) - } - - /// Checks whether the string representation of the endpoint ID starts with the given 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.clone().starts_with(value))) - } - - /// Checks whether the string representation of the endpoint ID ends with the given 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.clone().ends_with(value))) - } - - /// Checks whether the string representation of the endpoint ID contains the given substring. - /// - /// Arguments: - /// value (str): Substring to search for. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating substring search. - fn contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().contains(value))) - } - - /// Checks whether the string representation of the endpoint ID **does not** contain the given substring. - /// - /// Arguments: - /// value (str): Substring to exclude. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating substring exclusion. - fn not_contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().not_contains(value))) - } - - /// Performs fuzzy matching against the string representation of the endpoint ID. - /// - /// Uses a specified Levenshtein distance and optional prefix matching. - /// - /// Arguments: - /// value (str): String to approximately match against. - /// levenshtein_distance (int): Maximum allowed Levenshtein distance. - /// prefix_match (bool): Whether to require a matching 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.clone().fuzzy_search( - value, - levenshtein_distance, - prefix_match, - ))) - } -} - -/// Filters an edge endpoint by its node name. -/// -/// This builder produces `FilterExpr` predicates over the **source** or -/// **destination** endpoint node name. -/// -/// Examples: -/// Edge.src().name() == "alice" -/// Edge.dst().name().contains("ali") -#[pyclass(frozen, name = "EdgeEndpointNameFilter", module = "raphtory.filter")] -pub struct PyEdgeEndpointNameFilterBuilder(pub EdgeEndpointWrapper); - -/// Filters an edge endpoint by its node type. -/// -/// This builder produces `FilterExpr` predicates over the **source** or -/// **destination** endpoint node type. -/// -/// Examples: -/// Edge.src().node_type() == "fire_nation" -/// Edge.dst().node_type().is_not_in(["air_nomads"]) -#[pyclass(frozen, name = "EdgeEndpointTypeFilter", module = "raphtory.filter")] -pub struct PyEdgeEndpointTypeFilterBuilder(pub EdgeEndpointWrapper); - -macro_rules! impl_edge_text_filter_builder { - ($py_ty:ident) => { - #[pymethods] - impl $py_ty { - fn __eq__(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.eq(value))) - } - fn __ne__(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.ne(value))) - } - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new(self.0.is_in(vals))) - } - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new(self.0.is_not_in(vals))) - } - fn starts_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.starts_with(value))) - } - fn ends_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.ends_with(value))) - } - fn contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.contains(value))) - } - fn not_contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.not_contains(value))) - } - fn fuzzy_search( - &self, - value: String, - levenshtein_distance: usize, - prefix_match: bool, - ) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.fuzzy_search( - value, - levenshtein_distance, - prefix_match, - ))) - } - } - }; -} - -impl_edge_text_filter_builder!(PyEdgeEndpointNameFilterBuilder); -impl_edge_text_filter_builder!(PyEdgeEndpointTypeFilterBuilder); +use crate::prelude::EdgeViewOps; /// Entry point for filtering an edge endpoint (source or destination). /// @@ -286,63 +27,34 @@ pub struct PyEdgeEndpoint(pub EdgeEndpointWrapper); #[pymethods] impl PyEdgeEndpoint { /// Selects the endpoint node ID field for filtering. - /// - /// Returns: - /// filter.EdgeEndpointIdFilter: - fn id(&self) -> PyEdgeEndpointIdFilterBuilder { - PyEdgeEndpointIdFilterBuilder(self.0.id()) + fn id(&self) -> PyExpr { + self.0.clone().id().into() } /// Selects the endpoint node name field for filtering. - /// - /// Returns: - /// filter.EdgeEndpointNameFilter: - fn name(&self) -> PyEdgeEndpointNameFilterBuilder { - PyEdgeEndpointNameFilterBuilder(self.0.name()) + fn name(&self) -> PyExpr { + self.0.clone().name().into() } /// Selects the endpoint node type field for filtering. - /// - /// Returns: - /// filter.EdgeEndpointTypeFilter: - fn node_type(&self) -> PyEdgeEndpointTypeFilterBuilder { - PyEdgeEndpointTypeFilterBuilder(self.0.node_type()) + fn node_type(&self) -> PyExpr { + self.0.clone().node_type().into() } /// Filters an endpoint node property by name. /// - /// The property may be static or temporal depending on the query context. - /// /// Arguments: /// name (str): Property key. - /// - /// Returns: - /// filter.PropertyFilterOps: - fn property<'py>( - &self, - py: Python<'py>, - name: String, - ) -> PyResult> { - let b = PropertyFilterFactory::property(&self.0, name); - b.into_pyobject(py) + fn property(&self, name: String) -> PyExpr { + self.0.clone().property(name).into() } /// Filters an endpoint node metadata field by name. /// - /// Metadata is shared across all temporal versions of a node. - /// /// Arguments: /// name (str): Metadata key. - /// - /// Returns: - /// filter.FilterOps: - fn metadata<'py>( - &self, - py: Python<'py>, - name: String, - ) -> PyResult> { - let b = PropertyFilterFactory::metadata(&self.0, name); - b.into_pyobject(py) + fn metadata(&self, name: String) -> PyExpr { + self.0.clone().metadata(name).into() } } @@ -360,201 +72,103 @@ impl PyEdgeEndpoint { /// Edge.window(0, 10).is_active() /// Edge.layer("fire_nation").is_valid() #[pyclass(frozen, name = "Edge", module = "raphtory.filter")] -pub struct PyEdgeFilter; +pub struct PyEdgeFilter(Arc); #[pymethods] impl PyEdgeFilter { + #[new] + fn new() -> PyEdgeFilter { + PyEdgeFilter(Arc::new(EdgeFilter)) + } + /// Selects the edge **source endpoint** for filtering. - /// - /// Returns: - /// filter.EdgeEndpoint: - #[staticmethod] - fn src() -> PyEdgeEndpoint { + fn src(&self) -> PyEdgeEndpoint { PyEdgeEndpoint(EdgeFilter::src()) } /// Selects the edge **destination endpoint** for filtering. - /// - /// Returns: - /// filter.EdgeEndpoint: - #[staticmethod] - fn dst() -> PyEdgeEndpoint { + fn dst(&self) -> PyEdgeEndpoint { PyEdgeEndpoint(EdgeFilter::dst()) } /// Filters an edge property by name. /// - /// The property may be static or temporal depending on the query context. - /// /// Arguments: /// name (str): Property key. - /// - /// Returns: - /// filter.PropertyFilterOps: - #[staticmethod] - fn property<'py>( - py: Python<'py>, - name: String, - ) -> PyResult> { - let b: PropertyFilterBuilder = - PropertyFilterFactory::property(&EdgeFilter, name); - b.into_pyobject(py) + fn property(&self, name: String) -> PyExpr { + self.0.property(name).into() } /// Filters an edge metadata field by name. /// - /// Metadata is shared across all temporal versions of an edge. - /// /// Arguments: /// name (str): Metadata key. - /// - /// Returns: - /// filter.FilterOps: - #[staticmethod] - fn metadata<'py>(py: Python<'py>, name: String) -> PyResult> { - let b: MetadataFilterBuilder = - PropertyFilterFactory::metadata(&EdgeFilter, name); - b.into_pyobject(py) + fn metadata(&self, name: String) -> PyExpr { + self.0.metadata(name).into() } /// Restricts edge evaluation to the given time window. - /// - /// The window is inclusive of `start` and exclusive of `end`. - /// - /// Arguments: - /// start (int): Start time. - /// end (int): End time. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn window(start: EventTime, end: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.window(start, end))) + fn window(&self, start: EventTime, end: EventTime) -> PyEdgeFilter { + self.0.clone().window(start, end).into() } /// Restricts edge evaluation to a single point in time. - /// - /// Arguments: - /// time (int): Event time. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn at(time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.at(time))) + fn at(&self, time: EventTime) -> PyEdgeFilter { + self.0.clone().at(time).into() } /// Restricts edge evaluation to times strictly after the given time. - /// - /// Arguments: - /// time (int): Lower time bound. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn after(time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.after(time))) + fn after(&self, time: EventTime) -> PyEdgeFilter { + self.0.clone().after(time).into() } /// Restricts edge evaluation to times strictly before the given time. - /// - /// Arguments: - /// time (int): Upper time bound. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn before(time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.before(time))) + fn before(&self, time: EventTime) -> PyEdgeFilter { + self.0.clone().before(time).into() } /// Evaluates edge predicates against the latest available edge state. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn latest() -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.latest())) + fn latest(&self) -> PyEdgeFilter { + self.0.clone().latest().into() } /// Evaluates edge predicates against a snapshot of the graph at a given time. - /// - /// Arguments: - /// time (int): Snapshot time. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn snapshot_at(time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.snapshot_at(time))) + fn snapshot_at(&self, time: EventTime) -> PyEdgeFilter { + self.0.clone().snapshot_at(time).into() } /// Evaluates edge predicates against the most recent snapshot of the graph. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn snapshot_latest() -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.snapshot_latest())) + fn snapshot_latest(&self) -> PyEdgeFilter { + self.0.clone().snapshot_latest().into() } /// Restricts evaluation to edges belonging to the given layer. - /// - /// Arguments: - /// layer (str): Layer name. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn layer(layer: String) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.layer(layer))) + fn layer(&self, layer: String) -> PyEdgeFilter { + self.0.clone().layer(layer).into() } /// Restricts evaluation to edges belonging to any of the given layers. - /// - /// Arguments: - /// layers (list[str]): Layer names. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn layers(layers: FromIterable) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.layer(layers))) + fn layers(&self, layers: FromIterable) -> PyEdgeFilter { + self.0.clone().layer(layers).into() } /// Matches edges that have at least one event in the current view. - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_active() -> PyFilterExpr { - PyFilterExpr(Arc::new(EdgeFilter.is_active())) + fn is_active(&self) -> PyEdgeFilter { + self.0.is_active().into() } /// Matches edges that are structurally valid in the current view. - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_valid() -> PyFilterExpr { - PyFilterExpr(Arc::new(EdgeFilter.is_valid())) + fn is_valid(&self) -> PyEdgeFilter { + self.0.is_valid().into() } /// Matches edges that have been deleted. - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_deleted() -> PyFilterExpr { - PyFilterExpr(Arc::new(EdgeFilter.is_deleted())) + fn is_deleted(&self) -> PyEdgeFilter { + self.0.is_deleted().into() } /// Matches edges that are self-loops (source == destination). - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_self_loop() -> PyFilterExpr { - PyFilterExpr(Arc::new(EdgeFilter.is_self_loop())) + fn is_self_loop(&self) -> PyEdgeFilter { + self.0.is_self_loop().into() } } diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index c0f1b6b017..f6c1d76706 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -1,27 +1,19 @@ use crate::{ db::graph::views::filter::model::{ - exploded_edge_filter::ExplodedEdgeFilter, - property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, - EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, - }, - python::{ - filter::{ - filter_expr::PyFilterExpr, - property_expr::{ - PyEdgeViewPropsFilterBuilder, PyPropertyExprBuilder, PyPropertyFilterBuilder, - }, - }, - types::iterable::FromIterable, + exploded_edge_filter::ExplodedEdgeFilter, EdgeFilterFactory, EdgeViewFilterOps, + PropertyFilterFactory, ViewWrapOps, }, + python::{filter::node_expr::PyExpr, types::iterable::FromIterable}, }; -use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyResult, Python}; +use pyo3::{pyclass, pymethods}; use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; +use crate::prelude::EdgeViewOps; /// Entry point for constructing **exploded edge** filter expressions. /// /// An **exploded edge** represents an edge view where temporal events are treated -/// as individually addressable edge instances (i.e. “event-level” edges), rather +/// as individually addressable edge instances (i.e. "event-level" edges), rather /// than a single aggregated edge across time. /// /// This filter provides: @@ -34,27 +26,23 @@ use std::sync::Arc; /// ExplodedEdge.window(0, 10).is_active() /// ExplodedEdge.layer("fire_nation").is_valid() #[pyclass(frozen, name = "ExplodedEdge", module = "raphtory.filter")] -pub struct PyExplodedEdgeFilter; +pub struct PyExplodedEdgeFilter(Arc); #[pymethods] impl PyExplodedEdgeFilter { + #[new] + fn new() -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter(Arc::new(ExplodedEdgeFilter)) + } + /// Filters an exploded edge property by name. /// /// The property may be static or temporal depending on the query context. /// /// Arguments: /// name (str): Property key. - /// - /// Returns: - /// filter.PropertyFilterOps: - #[staticmethod] - fn property<'py>( - py: Python<'py>, - name: String, - ) -> PyResult> { - let b: PropertyFilterBuilder = - PropertyFilterFactory::property(&ExplodedEdgeFilter, name); - b.into_pyobject(py) + fn property(&self, name: String) -> PyExpr { + self.0.property(name).into() } /// Filters an exploded edge metadata field by name. @@ -63,154 +51,74 @@ impl PyExplodedEdgeFilter { /// /// Arguments: /// name (str): Metadata key. - /// - /// Returns: - /// filter.FilterOps: - #[staticmethod] - fn metadata<'py>(py: Python<'py>, name: String) -> PyResult> { - let b: MetadataFilterBuilder = - PropertyFilterFactory::metadata(&ExplodedEdgeFilter, name); - b.into_pyobject(py) + fn metadata(&self, name: String) -> PyExpr { + self.0.metadata(name).into() } /// Restricts exploded edge evaluation to the given time window. /// /// The window is inclusive of `start` and exclusive of `end`. - /// - /// Arguments: - /// start (int): Start time. - /// end (int): End time. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn window(start: EventTime, end: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.window(start, end))) + fn window(&self, start: EventTime, end: EventTime) -> PyExplodedEdgeFilter { + self.0.clone().window(start, end).into() } /// Restricts exploded edge evaluation to a single point in time. - /// - /// Arguments: - /// time (int): Event time. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn at(time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.at(time))) + fn at(&self, time: EventTime) -> PyExplodedEdgeFilter { + self.0.clone().at(time).into() } /// Restricts exploded edge evaluation to times strictly after the given time. - /// - /// Arguments: - /// time (int): Lower time bound. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn after(time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.after(time))) + fn after(&self, time: EventTime) -> PyExplodedEdgeFilter { + self.0.clone().after(time).into() } /// Restricts exploded edge evaluation to times strictly before the given time. - /// - /// Arguments: - /// time (int): Upper time bound. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn before(time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.before(time))) + fn before(&self, time: EventTime) -> PyExplodedEdgeFilter { + self.0.clone().before(time).into() } /// Evaluates exploded edge predicates against the latest available state. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn latest() -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.latest())) + fn latest(&self) -> PyExplodedEdgeFilter { + self.0.clone().latest().into() } /// Evaluates exploded edge predicates against a snapshot of the graph at a given time. - /// - /// Arguments: - /// time (int): Snapshot time. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn snapshot_at(time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.snapshot_at(time))) + fn snapshot_at(&self, time: EventTime) -> PyExplodedEdgeFilter { + self.0.clone().snapshot_at(time).into() } /// Evaluates exploded edge predicates against the most recent snapshot of the graph. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn snapshot_latest() -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.snapshot_latest())) + fn snapshot_latest(&self) -> PyExplodedEdgeFilter { + self.0.clone().snapshot_latest().into() } /// Restricts evaluation to exploded edges belonging to the given layer. - /// - /// Arguments: - /// layer (str): Layer name. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn layer(layer: String) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.layer(layer))) + fn layer(&self, layer: String) -> PyExplodedEdgeFilter { + self.0.clone().layer(layer).into() } /// Restricts evaluation to exploded edges belonging to any of the given layers. - /// - /// Arguments: - /// layers (list[str]): Layer names. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn layers(layers: FromIterable) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.layer(layers))) + fn layers(&self, layers: FromIterable) -> PyExplodedEdgeFilter { + self.0.clone().layer(layers).into() } /// Matches exploded edges that have at least one event in the current view. - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_active() -> PyFilterExpr { - PyFilterExpr(Arc::new(ExplodedEdgeFilter.is_active())) + fn is_active(&self) -> PyExplodedEdgeFilter { + self.0.is_active().into() } /// Matches exploded edges that are structurally valid in the current view. - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_valid() -> PyFilterExpr { - PyFilterExpr(Arc::new(ExplodedEdgeFilter.is_valid())) + fn is_valid(&self) -> PyExplodedEdgeFilter { + self.0.is_valid().into() } /// Matches exploded edges that have been deleted. - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_deleted() -> PyFilterExpr { - PyFilterExpr(Arc::new(ExplodedEdgeFilter.is_deleted())) + fn is_deleted(&self) -> PyExplodedEdgeFilter { + self.0.is_deleted().into() } /// Matches exploded edges that are self-loops (source == destination). - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_self_loop() -> PyFilterExpr { - PyFilterExpr(Arc::new(ExplodedEdgeFilter.is_self_loop())) + fn is_self_loop(&self) -> PyExplodedEdgeFilter { + self.0.is_self_loop().into() } } diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 9c5330d737..5ae708d44a 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -1,16 +1,10 @@ use crate::python::{ filter::{ - edge_expr::{ - PyEdgeEndpoint, PyEdgeEndpointIdFilterBuilder, PyEdgeEndpointNameFilterBuilder, - PyEdgeEndpointTypeFilterBuilder, PyEdgeFilter, - }, + edge_expr::{PyEdgeEndpoint, PyEdgeFilter}, exploded_edge_expr::PyExplodedEdgeFilter, filter_expr::PyFilterExpr, graph_filter::PyGraphFilter, - node_expr::{ - PyNodeFilter, PyNodeIdFilterBuilder, PyNodeNameFilterBuilder, PyNodeTypeFilterBuilder, - }, - property_expr::{PyPropertyExprBuilder, PyPropertyFilterBuilder}, + node_expr::PyNodeFilter, }, types::iterable::FromIterable, }; @@ -25,7 +19,6 @@ pub mod exploded_edge_expr; pub mod filter_expr; pub mod graph_filter; pub mod node_expr; -pub mod property_expr; impl From> for Layer { fn from(iter: FromIterable) -> Self { @@ -37,19 +30,11 @@ pub fn base_filter_module(py: Python<'_>) -> Result, PyErr> let filter_module = PyModule::new(py, "filter")?; filter_module.add_class::()?; - filter_module.add_class::()?; - filter_module.add_class::()?; filter_module.add_class::()?; - filter_module.add_class::()?; - filter_module.add_class::()?; - filter_module.add_class::()?; filter_module.add_class::()?; filter_module.add_class::()?; - filter_module.add_class::()?; - filter_module.add_class::()?; - filter_module.add_class::()?; filter_module.add_class::()?; filter_module.add_class::()?; diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 068127319d..c2d68becb8 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,24 +1,57 @@ use crate::{ db::graph::views::filter::model::{ degree_filter::DegreeFilterFactory, - node_expr::{CreateOp}, + node_expr::{CreateOp, DynCreateOp, EntityExpr}, node_state_filter::NodeStateBoolColOp, - NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, + CreateView, DynCreateView, EntityMarker, NodeViewFilterOps, PropertyExpr, + PropertyFilterFactory, ViewWrapOps, }, - prelude::{EntityExprFilterOps, NodeFilter, NodeFilterFactory}, + prelude::{EntityAggOps, EntityExprFilterOps, NodeFilter, NodeFilterFactory}, python::{graph::node_state::PyOutputNodeState, types::iterable::FromIterable}, }; -use pyo3::{pyclass, pymethods, PyResult}; +use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, PyResult, Python}; use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; use std::sync::Arc; -use crate::db::graph::views::filter::model::node_expr::DynCreateOp; -use crate::prelude::EntityAggOps; -#[pyclass(frozen, name = "Expr", module = "raphtory.filter")] +trait DynTemporal: DynCreateOp { + fn temporal(&self) -> Arc; +} + +impl< + E: EntityExpr> + + CreateView + + NodeFilterFactory + + Send + + Sync + + 'static, + > DynTemporal for PropertyExpr +{ + fn temporal(&self) -> Arc { + Arc::new(self.temporal()) + } +} + +#[pyclass(frozen, subclass, name = "Expr", module = "raphtory.filter")] #[derive(Clone)] pub struct PyExpr(Arc); -impl From for PyExpr { +#[pyclass(frozen, extends = PyExpr, name = "PropertyExpr", module = "raphtory.filter")] +#[derive(Clone)] +pub struct PyPropertyExpr(Arc); + +impl<'py> IntoPyObject<'py> for PyPropertyExpr { + type Target = PyPropertyExpr; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let parent = PyExpr(self.0.clone()); + let child = self; + Bound::new(py, (child, parent)) + } +} + +impl>> From for PyExpr { fn from(value: E) -> Self { PyExpr(Arc::new(value)) } @@ -57,12 +90,7 @@ impl PyExpr { fn not_contains(&self, other: &Self) -> Self { self.0.clone().not_contains(other.0.clone()).into() } - fn fuzzy_search( - &self, - other: &Self, - levenshtein_distance: usize, - prefix_match: bool, - ) -> Self { + fn fuzzy_search(&self, other: &Self, levenshtein_distance: usize, prefix_match: bool) -> Self { self.0 .clone() .fuzzy_search(other.0.clone(), levenshtein_distance, prefix_match) @@ -124,6 +152,88 @@ impl PyExpr { // fn temporal(&self) -> Self { … } } +pub trait DynNodeFilterFactory: Send + Sync + 'static { + fn dyn_id(&self) -> Arc; + fn dyn_name(&self) -> Arc; + fn dyn_node_type(&self) -> Arc; + fn dyn_degree(&self) -> Arc; + fn dyn_in_degree(&self) -> Arc; + fn dyn_out_degree(&self) -> Arc; + fn dyn_is_active(&self) -> Arc; + fn dyn_property(&self, name: String) -> Arc; + fn dyn_metadata(&self, name: String) -> Arc; + + fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc; + fn dyn_at(&self, time: EventTime) -> Arc; + fn dyn_after(&self, time: EventTime) -> Arc; + fn dyn_before(&self, time: EventTime) -> Arc; + fn dyn_latest(&self) -> Arc; + fn dyn_snapshot_at(&self, time: EventTime) -> Arc; + fn dyn_snapshot_latest(&self) -> Arc; + fn dyn_layer(&self, layers: Vec) -> Arc; +} + +impl DynNodeFilterFactory for T +where + T: NodeFilterFactory + ViewWrapOps + CreateView + Clone + Send + Sync + 'static, +{ + fn dyn_id(&self) -> Arc { + Arc::new(self.id()) + } + fn dyn_name(&self) -> Arc { + Arc::new(self.name()) + } + fn dyn_node_type(&self) -> Arc { + Arc::new(self.node_type()) + } + + fn dyn_degree(&self) -> Arc { + Arc::new(self.degree()) + } + fn dyn_in_degree(&self) -> Arc { + Arc::new(self.in_degree()) + } + fn dyn_out_degree(&self) -> Arc { + Arc::new(self.out_degree()) + } + + fn dyn_is_active(&self) -> Arc { + Arc::new(self.is_active()) + } + + fn dyn_property(&self, name: String) -> Arc { + Arc::new(self.property(name)) + } + fn dyn_metadata(&self, name: String) -> Arc { + Arc::new(PropertyFilterFactory::metadata(self, name)) + } + + fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { + Arc::new(self.clone().window(start, end)) + } + fn dyn_at(&self, time: EventTime) -> Arc { + Arc::new(self.clone().at(time)) + } + fn dyn_after(&self, time: EventTime) -> Arc { + Arc::new(self.clone().after(time)) + } + fn dyn_before(&self, time: EventTime) -> Arc { + Arc::new(self.clone().before(time)) + } + fn dyn_latest(&self) -> Arc { + Arc::new(self.clone().latest()) + } + fn dyn_snapshot_at(&self, time: EventTime) -> Arc { + Arc::new(self.clone().snapshot_at(time)) + } + fn dyn_snapshot_latest(&self) -> Arc { + Arc::new(self.clone().snapshot_latest()) + } + fn dyn_layer(&self, layers: Vec) -> Arc { + Arc::new(self.clone().layer(layers)) + } +} + /// Constructs node filter expressions. /// /// Each method returns either: @@ -131,7 +241,7 @@ impl PyExpr { /// - a view-restricted filter context, or /// - a boolean predicate over node state. #[pyclass(frozen, name = "Node", module = "raphtory.filter")] -pub struct PyNodeFilter(Arc); +pub struct PyNodeFilter(Arc); #[pymethods] impl PyNodeFilter { @@ -145,7 +255,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeIdFilterBuilder: fn id(&self) -> PyExpr { - self.0.id().into() + self.0.dyn_id().into() } /// Selects the node name field for filtering. @@ -153,7 +263,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeNameFilterBuilder: fn name(&self) -> PyExpr { - self.0.name().into() + self.0.dyn_name().into() } /// Selects the node type field for filtering. @@ -161,7 +271,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeTypeFilterBuilder: fn node_type(&self) -> PyExpr { - self.0.node_type().into() + self.0.dyn_node_type().into() } /// Selects incoming node degree for filtering. @@ -169,7 +279,7 @@ impl PyNodeFilter { /// Returns: /// filter.FilterOps fn in_degree(&self) -> PyExpr { - self.0.in_degree().into() + self.0.dyn_in_degree().into() } /// Selects total node degree for filtering. @@ -177,7 +287,7 @@ impl PyNodeFilter { /// Returns: /// filter.FilterOps fn degree(&self) -> PyExpr { - self.0.degree().into() + self.0.dyn_degree().into() } /// Selects outgoing node degree for filtering. @@ -185,7 +295,7 @@ impl PyNodeFilter { /// Returns: /// filter.FilterOps fn out_degree(&self) -> PyExpr { - self.0.out_degree().into() + self.0.dyn_out_degree().into() } /// Filters a node property by name. @@ -197,8 +307,8 @@ impl PyNodeFilter { /// /// Returns: /// filter.PropertyFilterOps: - fn property(&self, name: String) -> PyExpr { - self.0.property(name).into() + fn property(&self, name: String) -> PyPropertyExpr { + self.0.dyn_property(name).into() } /// Filters a node metadata field by name. @@ -211,7 +321,7 @@ impl PyNodeFilter { /// Returns: /// filter.FilterOps: fn metadata(&self, name: String) -> PyExpr { - self.0.metadata(name).into() + self.0.dyn_metadata(name).into() } /// Restricts node evaluation to the given time window. @@ -225,7 +335,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn window(&self, start: EventTime, end: EventTime) -> PyNodeFilter { - self.0.window(start, end).into() + self.0.dyn_window(start, end).into() } /// Restricts node evaluation to a single point in time. @@ -236,7 +346,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn at(&self, time: EventTime) -> PyNodeFilter { - self.0.at(time).into() + self.0.dyn_at(time).into() } /// Restricts node evaluation to times strictly after the given time. @@ -247,7 +357,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn after(&self, time: EventTime) -> PyNodeFilter { - self.0.after(time).into() + self.0.dyn_after(time).into() } /// Restricts node evaluation to times strictly before the given time. @@ -258,7 +368,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn before(&self, time: EventTime) -> PyNodeFilter { - self.0.before(time).into() + self.0.dyn_before(time).into() } /// Evaluates filters against the latest available state of each node. @@ -266,7 +376,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn latest(&self) -> PyNodeFilter { - self.0.latest().into() + self.0.dyn_latest().into() } /// Evaluates filters against a snapshot of the graph at a given time. @@ -277,7 +387,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn snapshot_at(&self, time: EventTime) -> PyNodeFilter { - self.0.snapshot_at(time).into() + self.0.dyn_snapshot_at(time).into() } /// Evaluates filters against the most recent snapshot of the graph. @@ -285,7 +395,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn snapshot_latest(&self) -> PyNodeFilter { - self.0.snapshot_latest().into() + self.0.dyn_snapshot_latest().into() } /// Restricts evaluation to nodes belonging to the given layer. @@ -296,7 +406,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn layer(&self, layer: String) -> PyNodeFilter { - self.0.layer(layer).into() + self.0.dyn_layer(vec![layer]).into() } /// Restricts evaluation to nodes belonging to any of the given layers. @@ -307,7 +417,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn layers(&self, layers: FromIterable) -> PyNodeFilter { - self.0.layer(layers).into() + self.0.dyn_layer(layers.into_vec()).into() } /// Matches nodes that have at least one event in the current view. @@ -315,7 +425,7 @@ impl PyNodeFilter { /// Returns: /// filter.FilterExpr: fn is_active(&self) -> PyNodeFilter { - self.0.is_active().into() + self.0.dyn_is_active().into() } /// Build a node filter from a boolean column of an existing node-state result. diff --git a/raphtory/src/python/filter/property_expr.rs b/raphtory/src/python/filter/property_expr.rs deleted file mode 100644 index 8a334bac69..0000000000 --- a/raphtory/src/python/filter/property_expr.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Python wrappers over the typed property-expression chain. -//! -//! Two classes: -//! - `PyPropertyExprBuilder` (`filter.FilterOps`) — wraps `Arc` -//! and exposes the comparator/string/set/aggregator/quantifier chain methods. -//! - `PyPropertyFilterBuilder` (`filter.PropertyFilterOps`) — extends `FilterOps` -//! with `.temporal()`. Wraps `Arc`. -//! -//! Plus three view-builder wrappers that combine view restrictions (window / -//! layer / snapshot) with property predicates: -//! - `PyViewFilterBuilder` (`filter.ViewFilterBuilder`) -//! - `PyNodeViewPropsFilterBuilder` (`filter.NodeViewPropsFilterBuilder`) -//! - `PyEdgeViewPropsFilterBuilder` (`filter.EdgeViewPropsFilterBuilder`) - -use crate::{ - db::graph::views::filter::model::{ - edge_filter::EdgeEndpointWrapper, node_expr::EntityExpr, DynEdgeViewFilterOps, - DynEdgeViewProps, DynNodeViewProps, MetadataExpr, PropertyExpr, ViewWrapOps, - }, - python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, -}; -use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; -use raphtory_api::core::storage::timeindex::EventTime; -use std::sync::Arc; -// ───────────────────────────────────────────────────────────────────────────── -// IntoPyObject bridges — used by `Node.property(name)` / `Edge.property(name)` -// etc. to surface a `PyPropertyFilterBuilder` from the typed Rust expression. -// ───────────────────────────────────────────────────────────────────────────── - -impl<'py, E> IntoPyObject<'py> for PropertyExpr -where - E: EntityExpr + Clone + Send + Sync + 'static, - PropertyExpr: DynTemporalPropertyExpr, -{ - type Target = PyPropertyFilterBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let inner: Arc> = Arc::new(self); - let child = PyPropertyFilterBuilder::from_arc(inner.clone()); - let parent = PyPropertyExprBuilder::from_arc(inner); - Bound::new(py, (child, parent)) - } -} - -impl<'py, E> IntoPyObject<'py> for MetadataExpr -where - E: EntityExpr + Clone + Send + Sync + 'static, - MetadataExpr: DynPropertyExpr, -{ - type Target = PyPropertyExprBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - PyPropertyExprBuilder::wrap(self).into_pyobject(py) - } -} - -impl<'py, E> IntoPyObject<'py> for EdgeEndpointWrapper> -where - E: EntityExpr + Clone + Send + Sync + 'static, - EdgeEndpointWrapper>: DynTemporalPropertyExpr, -{ - type Target = PyPropertyFilterBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let inner: Arc>> = Arc::new(self); - let child = PyPropertyFilterBuilder::from_arc(inner.clone()); - let parent = PyPropertyExprBuilder::from_arc(inner); - Bound::new(py, (child, parent)) - } -} - -impl<'py, E> IntoPyObject<'py> for EdgeEndpointWrapper> -where - E: EntityExpr + Clone + Send + Sync + 'static, - EdgeEndpointWrapper>: DynPropertyExpr, -{ - type Target = PyPropertyExprBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let inner: Arc>> = Arc::new(self); - PyPropertyExprBuilder::from_arc(inner).into_pyobject(py) - } -} - -impl<'py> IntoPyObject<'py> for PyPropertyFilterBuilder { - type Target = PyPropertyFilterBuilder; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyPropertyExprBuilder::from_arc(self.0.clone()); - Bound::new(py, (self, parent)) - } -} - -/// Node-side view + property predicate builder. -#[pyclass( - name = "NodeViewPropsFilterBuilder", - module = "raphtory.filter", - frozen -)] -pub struct PyNodeViewPropsFilterBuilder(pub(crate) DynNodeViewProps); - -#[pymethods] -impl PyNodeViewPropsFilterBuilder { - fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_active()) - } - - fn property(&self, name: String) -> PyPropertyFilterBuilder { - PyPropertyFilterBuilder(Arc::new(self.0.property(name))) - } - - fn metadata(&self, name: String) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(Arc::new(self.0.metadata(name))) - } - - fn window(&self, start: EventTime, end: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(self.0.clone().window(start, end)) - } - - fn at(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(self.0.clone().at(time)) - } - - fn after(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(self.0.clone().after(time)) - } - - fn before(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(self.0.clone().before(time)) - } - - fn latest(&self) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().latest())) - } - - fn snapshot_at(&self, time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_at(time))) - } - - fn snapshot_latest(&self) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_latest())) - } - - fn layer(&self, layer: String) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layer))) - } - - fn layers(&self, layers: FromIterable) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layers))) - } -} - -/// Edge-side view + property predicate builder. -#[pyclass( - name = "EdgeViewPropsFilterBuilder", - module = "raphtory.filter", - frozen -)] -pub struct PyEdgeViewPropsFilterBuilder(pub(crate) DynEdgeViewProps); - -#[pymethods] -impl PyEdgeViewPropsFilterBuilder { - fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_active()) - } - - fn is_valid(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_valid()) - } - - fn is_deleted(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_deleted()) - } - - fn is_self_loop(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_self_loop()) - } - - fn property(&self, name: String) -> PyPropertyFilterBuilder { - PyPropertyFilterBuilder(Arc::new(self.0.property(name))) - } - - fn metadata(&self, name: String) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(Arc::new(self.0.metadata(name))) - } - - fn window(&self, start: EventTime, end: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(self.0.clone().window(start, end)) - } - - fn at(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(self.0.clone().at(time)) - } - - fn after(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(self.0.clone().after(time)) - } - - fn before(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(self.0.clone().before(time)) - } - - fn latest(&self) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().latest())) - } - - fn snapshot_at(&self, time: EventTime) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_at(time))) - } - - fn snapshot_latest(&self) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().snapshot_latest())) - } - - fn layer(&self, layer: String) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layer))) - } - - fn layers(&self, layers: FromIterable) -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(self.0.clone().layer(layers))) - } -} From 46af8573426d0dde6787a63bcacc44d73cd73698 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:54:13 +0100 Subject: [PATCH 073/155] impl missing traits --- .../views/filter/model/edge_expr/filters.rs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index a7de91d3ba..721b951ea2 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -54,6 +54,25 @@ where } } +impl CreateOp for BinaryCmpExpr +where + L: CreateOp, + R: CreateOp, +{ + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph)?; + Ok(Arc::new(ListAwareCmpEdgeOp { + left, + right, + op: self.op, + })) + } +} + impl TryAsCompositeFilter for BinaryCmpExpr where L: CreateOp, @@ -189,6 +208,32 @@ where } } +impl CreateFilter for UnaryExpr +where + E: CreateOp, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + ExplodedEdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let inner = self.expr.create_edge_op(graph.clone())?; + let op: Arc + 'graph> = + Arc::new(UnaryEdgeOp { inner, op: self.op }); + Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + // ───────────────────────────────────────────────────────────────────────────── // StringExpr — string expression filter for edges // ───────────────────────────────────────────────────────────────────────────── @@ -212,6 +257,25 @@ where } } +impl CreateOp for StringExpr +where + L: CreateOp, + R: CreateOp, +{ + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph)?; + Ok(Arc::new(ListAwareStringEdgeOp { + left, + right, + op: self.op, + })) + } +} + impl TryAsCompositeFilter for StringExpr where L: CreateOp, @@ -262,6 +326,38 @@ where } } +impl CreateFilter for StringExpr +where + L: CreateOp, + R: CreateOp, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + ExplodedEdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph.clone())?; + validate_string_op(&left.prop_type())?; + let op: Arc + 'graph> = Arc::new(StringEdgeOp { + left, + right, + op: self.op, + }); + Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} + // ───────────────────────────────────────────────────────────────────────────── // PropValueSetExpr — is_in / is_not_in for edge-side exprs // ───────────────────────────────────────────────────────────────────────────── @@ -280,6 +376,20 @@ impl CreateOp for PropValueSetExpr { } } +impl CreateOp for PropValueSetExpr { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.expr.create_edge_op(graph)?; + Ok(Arc::new(ListAwareSetEdgeOp { + inner, + values: self.values.clone(), + op: self.op, + })) + } +} + impl TryAsCompositeFilter for PropValueSetExpr { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -319,3 +429,29 @@ impl CreateFilter for PropValueSetExpr { Err(GraphError::NotNodeFilter) } } + +impl CreateFilter for PropValueSetExpr { + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + ExplodedEdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let inner = self.expr.create_edge_op(graph.clone())?; + let op: Arc + 'graph> = Arc::new(PropValueSetEdgeOp { + inner, + values: self.values, + op: self.op, + }); + Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + _graph: G, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } +} From 6a26411c62bb6d9d810deaa9774538087f01ebc3 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:58:37 +0100 Subject: [PATCH 074/155] ref --- .../views/filter/model/edge_expr/filters.rs | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index 721b951ea2..b782ed7744 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -73,26 +73,6 @@ where } } -impl TryAsCompositeFilter for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - 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 CreateFilter for BinaryCmpExpr where L: CreateOp, @@ -159,13 +139,10 @@ where } } -// ───────────────────────────────────────────────────────────────────────────── -// UnaryExpr -// ───────────────────────────────────────────────────────────────────────────── - -impl TryAsCompositeFilter for UnaryExpr +impl TryAsCompositeFilter for BinaryCmpExpr where - E: CreateOp, + L: CreateOp, + R: CreateOp, { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) @@ -182,6 +159,10 @@ where } } +// ───────────────────────────────────────────────────────────────────────────── +// UnaryExpr +// ───────────────────────────────────────────────────────────────────────────── + impl CreateFilter for UnaryExpr where E: CreateOp, @@ -234,6 +215,25 @@ where } } +impl TryAsCompositeFilter for UnaryExpr +where + E: CreateOp, +{ + 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) + } +} + // ───────────────────────────────────────────────────────────────────────────── // StringExpr — string expression filter for edges // ───────────────────────────────────────────────────────────────────────────── @@ -276,24 +276,6 @@ where } } -impl TryAsCompositeFilter for StringExpr -where - L: CreateOp, - R: CreateOp, -{ - 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 CreateFilter for StringExpr where L: CreateOp, @@ -358,6 +340,24 @@ where } } +impl TryAsCompositeFilter for StringExpr +where + L: CreateOp, + R: CreateOp, +{ + 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) + } +} + // ───────────────────────────────────────────────────────────────────────────── // PropValueSetExpr — is_in / is_not_in for edge-side exprs // ───────────────────────────────────────────────────────────────────────────── @@ -390,20 +390,6 @@ impl CreateOp for PropValueSetExpr { } } -impl TryAsCompositeFilter for PropValueSetExpr { - 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 CreateFilter for PropValueSetExpr { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; @@ -455,3 +441,17 @@ impl CreateFilter for PropValueSetExpr { Err(GraphError::NotNodeFilter) } } + +impl TryAsCompositeFilter for PropValueSetExpr { + 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) + } +} From 089eff5e7007f12e52f21a01aa6f12259f90d867 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:21:26 +0100 Subject: [PATCH 075/155] impl CreateOp for UnaryExpr and missing ExplodedEdgeFilter variants --- .../views/filter/model/edge_expr/filters.rs | 27 ++- .../graph/views/filter/model/edge_expr/ops.rs | 41 +++- .../views/filter/model/node_expr/filters.rs | 199 +++++++++--------- .../graph/views/filter/model/node_expr/ops.rs | 30 +++ 4 files changed, 196 insertions(+), 101 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index b782ed7744..b221ad0b7a 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -6,7 +6,7 @@ use super::{ ops::{ BinaryCmpEdgeOp, ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, - PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, + ListAwareUnaryEdgeOp, PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, }, EdgeOp, }; @@ -162,6 +162,31 @@ where // ───────────────────────────────────────────────────────────────────────────── // UnaryExpr // ───────────────────────────────────────────────────────────────────────────── +impl CreateOp for UnaryExpr +where + E: CreateOp, +{ + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.expr.create_edge_op(graph)?; + Ok(Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op })) + } +} + +impl CreateOp for UnaryExpr +where + E: CreateOp, +{ + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.expr.create_edge_op(graph)?; + Ok(Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op })) + } +} impl CreateFilter for UnaryExpr where diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 242a27a5f2..e72de604f4 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -27,6 +27,11 @@ use crate::db::{ }; use raphtory_api::core::entities::properties::prop::PropArray; use std::sync::Arc; +use crate::db::graph::views::filter::model::filter_operator::{BinaryOp, Comparable}; + +use crate::db::graph::views::filter::model::{ + filter_operator::UnaryOp, SetOp, StringComparable, StringOp, +}; // ───────────────────────────────────────────────────────────────────────────── // Arc — blanket impl so Arc-boxed ops satisfy EdgeOp // ───────────────────────────────────────────────────────────────────────────── @@ -129,8 +134,6 @@ impl EdgeOp for TemporalEdgePropOp { // BinaryCmpEdgeOp<'g> — compares two EdgeOp outputs, returns bool // ───────────────────────────────────────────────────────────────────────────── -use crate::db::graph::views::filter::model::filter_operator::{BinaryOp, Comparable}; - #[derive(Clone)] pub(crate) struct BinaryCmpEdgeOp<'g, L> { pub(crate) left: Arc + 'g>, @@ -152,10 +155,6 @@ impl<'g, L: Comparable + Clone + Send + Sync + 'static> EdgeOp for BinaryCmpEdge // UnaryEdgeOp<'g, I> — is_some / is_none on Option-valued expressions // ───────────────────────────────────────────────────────────────────────────── -use crate::db::graph::views::filter::model::{ - filter_operator::UnaryOp, SetOp, StringComparable, StringOp, -}; - #[derive(Clone)] pub(crate) struct UnaryEdgeOp<'g, I: Clone + Send + Sync + 'static> { pub(crate) inner: Arc> + 'g>, @@ -335,6 +334,36 @@ impl<'g> EdgeOp for ListAwareSetEdgeOp<'g> { } } +// ───────────────────────────────────────────────────────────────────────────── +// ListAwareUnaryEdgeOp — element-wise is_some / is_none via broadcast_unary +// +// Unlike `UnaryEdgeOp` (which returns `bool` for use in `CreateFilter`), this +// op returns `Option` so it can plug into the expression chain via +// `CreateOp`. The closure intentionally does NOT `?`-propagate the inner +// `None` — the whole purpose of `is_some`/`is_none` is to test that case. +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct ListAwareUnaryEdgeOp<'g> { + pub(crate) inner: Arc> + 'g>, + pub(crate) op: UnaryOp, +} + +impl<'g> EdgeOp for ListAwareUnaryEdgeOp<'g> { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + let vals = self.inner.apply(storage, edge); + let op = &self.op; + broadcast_unary(vals, |v| { + Some(Prop::Bool(match op { + UnaryOp::IsSome => v.is_some(), + UnaryOp::IsNone => v.is_none(), + })) + }) + } +} + // ───────────────────────────────────────────────────────────────────────────── // AndBoolEdgeOp / OrBoolEdgeOp — boolean AND/OR over two Option edge ops // 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 849080d365..a27aecb9ab 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 @@ -43,7 +43,7 @@ use super::{ ops::{ BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, - PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, + ListAwareUnaryNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, } , CreateOp, EntityExpr, }; @@ -111,6 +111,34 @@ impl BinaryCmpExpr { impl ComposableFilter for BinaryCmpExpr {} +impl EntityExpr +for BinaryCmpExpr +{ + type Marker = E; + fn entity(&self) -> Self::Marker { + self.entity + } + fn prop_type(&self) -> PropType { + // TODO: depending on the types of left and right, we should figure out the type to return here + PropType::Empty + } +} + +impl CreateOp for BinaryCmpExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + Ok(Arc::new(ListAwareCmpNodeOp { + left, + right, + op: self.op, + })) + } +} + impl CreateFilter for BinaryCmpExpr where L: CreateOp, @@ -194,34 +222,6 @@ where } } -impl EntityExpr -for BinaryCmpExpr -{ - type Marker = E; - fn entity(&self) -> Self::Marker { - self.entity - } - fn prop_type(&self) -> PropType { - // TODO: depending on the types of left and right, we should figure out the type to return here - PropType::Empty - } -} - -impl CreateOp for BinaryCmpExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; - Ok(Arc::new(ListAwareCmpNodeOp { - left, - right, - op: self.op, - })) - } -} - impl TryAsCompositeFilter for BinaryCmpExpr where L: CreateOp, @@ -275,6 +275,28 @@ impl UnaryExpr { impl ComposableFilter for UnaryExpr {} +impl EntityExpr +for UnaryExpr +{ + type Marker = Entity; + fn entity(&self) -> Self::Marker { + self.entity + } +} + +impl CreateOp for UnaryExpr +where + E: CreateOp, +{ + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.expr.create_node_op(graph)?; + Ok(Arc::new(ListAwareUnaryNodeOp { inner, op: self.op })) + } +} + impl CreateFilter for UnaryExpr where E: CreateOp, @@ -340,16 +362,6 @@ where } } -impl EntityExpr -for UnaryExpr -{ - type Marker = Entity; - fn entity(&self) -> Self::Marker { - self.entity - } -} - - impl TryAsCompositeFilter for UnaryExpr where E: CreateOp, @@ -419,6 +431,33 @@ impl StringExpr { impl ComposableFilter for StringExpr {} +impl EntityExpr +for StringExpr +{ + type Marker = Entity; + fn entity(&self) -> Self::Marker { + self.entity + } + fn prop_type(&self) -> PropType { + PropType::Empty + } +} + +impl CreateOp for StringExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + Ok(Arc::new(ListAwareStringNodeOp { + left, + right, + op: self.op, + })) + } +} + impl CreateFilter for StringExpr { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; @@ -479,33 +518,6 @@ impl CreateFilter for StringExpr { } } -impl EntityExpr -for StringExpr -{ - type Marker = Entity; - fn entity(&self) -> Self::Marker { - self.entity - } - fn prop_type(&self) -> PropType { - PropType::Empty - } -} - -impl CreateOp for StringExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; - Ok(Arc::new(ListAwareStringNodeOp { - left, - right, - op: self.op, - })) - } -} - impl TryAsCompositeFilter for StringExpr where L: CreateOp, @@ -555,6 +567,33 @@ impl PropValueSetExpr { impl ComposableFilter for PropValueSetExpr {} +impl EntityExpr +for PropValueSetExpr +{ + type Marker = Entity; + fn entity(&self) -> Self::Marker { + self.entity + } + fn prop_type(&self) -> PropType { + PropType::Empty + } +} + + +impl CreateOp for PropValueSetExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.expr.create_node_op(graph)?; + Ok(Arc::new(ListAwareSetNodeOp { + inner, + values: self.values.clone(), + op: self.op, + })) + } +} + impl CreateFilter for PropValueSetExpr { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; @@ -613,34 +652,6 @@ impl CreateFilter for PropValueSetExpr { } } -impl EntityExpr -for PropValueSetExpr -{ - type Marker = Entity; - fn entity(&self) -> Self::Marker { - self.entity - } - fn prop_type(&self) -> PropType { - PropType::Empty - } -} - - -impl CreateOp for PropValueSetExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_node_op(graph)?; - Ok(Arc::new(ListAwareSetNodeOp { - inner, - values: self.values.clone(), - op: self.op, - })) - } -} - - impl TryAsCompositeFilter for PropValueSetExpr { fn try_as_composite_node_filter(&self) -> Result { Err(GraphError::NotSupported) 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 index 601a876727..700a18b039 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -434,6 +434,36 @@ impl<'g> NodeOp for ListAwareSetNodeOp<'g> { } } +// ───────────────────────────────────────────────────────────────────────────── +// ListAwareUnaryNodeOp — element-wise is_some / is_none via broadcast_unary +// +// Unlike `UnaryNodeOp` (which returns `bool` for use in `CreateFilter`), this +// op returns `Option` so it can plug into the expression chain via +// `CreateOp`. The closure intentionally does NOT `?`-propagate the inner +// `None` — the whole purpose of `is_some`/`is_none` is to test that case. +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct ListAwareUnaryNodeOp<'g> { + pub(crate) inner: Arc> + 'g>, + pub(crate) op: UnaryOp, +} + +impl<'g> NodeOp for ListAwareUnaryNodeOp<'g> { + type Output = Option; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + let vals = self.inner.apply(storage, node); + let op = &self.op; + broadcast_unary(vals, |v| { + Some(Prop::Bool(match op { + UnaryOp::IsSome => v.is_some(), + UnaryOp::IsNone => v.is_none(), + })) + }) + } +} + // ───────────────────────────────────────────────────────────────────────────── // AndBoolNodeOp / OrBoolNodeOp — boolean AND/OR over two Option node ops // From 51ecb594ba885d622196fb2e3c3a17e4b5098f0b Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:39:56 +0100 Subject: [PATCH 076/155] fix error messages --- raphtory/src/errors.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/raphtory/src/errors.rs b/raphtory/src/errors.rs index 38210e94b3..cd430101c5 100644 --- a/raphtory/src/errors.rs +++ b/raphtory/src/errors.rs @@ -358,10 +358,10 @@ pub enum GraphError { #[error("Not supported")] NotSupported, - #[error("Edge filter expected")] + #[error("Expression is not a node filter")] NotNodeFilter, - #[error("Node filter expected")] + #[error("Expression is not an edge filter")] NotEdgeFilter, #[error("Operator {0} requires a property value, but none was provided.")] @@ -421,7 +421,7 @@ pub enum GraphError { #[error("Node filter is not supported for edge filtering")] NodeFilterIsNotEdgeFilter, - #[error("Only property filters are supported for exploded edge filtering")] + #[error("Expression is not an exploded edge filter")] NotExplodedEdgeFilter, #[error("Indexing not supported")] From 08d7bfc8eabbcbc9f818b35a9635e8c01827ad74 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:52:48 +0100 Subject: [PATCH 077/155] align CreateOp/error-code dispatch for EntityMarker variants --- .../views/filter/model/node_expr/filters.rs | 141 ++++++++++++++++-- raphtory/src/errors.rs | 3 - 2 files changed, 130 insertions(+), 14 deletions(-) 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 a27aecb9ab..bf20654b57 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 @@ -56,6 +56,7 @@ use crate::{ }, graph::views::filter::{ model::{ + edge_expr::EdgeOp, edge_filter::CompositeEdgeFilter, filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, @@ -111,7 +112,7 @@ impl BinaryCmpExpr { impl ComposableFilter for BinaryCmpExpr {} -impl EntityExpr +impl EntityExpr for BinaryCmpExpr { type Marker = E; @@ -216,8 +217,45 @@ where self.left, self.op, self.right, NodeFilter, ) .create_node_filter(graph)?), - EntityMarker::Edge => Err(GraphError::NotEdgeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotExplodedEdgeFilter), + EntityMarker::Edge => Err(GraphError::NotNodeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), + } + } +} + +impl CreateOp for BinaryCmpExpr +where + L: CreateOp, + R: CreateOp, +{ + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + match self.entity { + EntityMarker::Node => BinaryCmpExpr::new( + self.left.clone(), self.op, self.right.clone(), NodeFilter, + ) + .create_node_op(graph), + EntityMarker::Edge => Err(GraphError::NotNodeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), + } + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + match self.entity { + EntityMarker::Node => Err(GraphError::NotEdgeFilter), + EntityMarker::Edge => BinaryCmpExpr::new( + self.left.clone(), self.op, self.right.clone(), EdgeFilter, + ) + .create_edge_op(graph), + EntityMarker::ExplodedEdge => BinaryCmpExpr::new( + self.left.clone(), self.op, self.right.clone(), ExplodedEdgeFilter, + ) + .create_edge_op(graph), } } } @@ -275,7 +313,7 @@ impl UnaryExpr { impl ComposableFilter for UnaryExpr {} -impl EntityExpr +impl EntityExpr for UnaryExpr { type Marker = Entity; @@ -356,8 +394,37 @@ where ) -> Result, GraphError> { match self.entity { EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), - EntityMarker::Edge => Err(GraphError::NotEdgeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotExplodedEdgeFilter), + EntityMarker::Edge => Err(GraphError::NotNodeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), + } + } +} + +impl CreateOp for UnaryExpr +where + E: CreateOp, +{ + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + match self.entity { + EntityMarker::Node => self.clone().with_entity(NodeFilter).create_node_op(graph), + EntityMarker::Edge => Err(GraphError::NotNodeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), + } + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + match self.entity { + EntityMarker::Node => Err(GraphError::NotEdgeFilter), + EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), + EntityMarker::ExplodedEdge => { + self.clone().with_entity(ExplodedEdgeFilter).create_edge_op(graph) + } } } } @@ -431,7 +498,7 @@ impl StringExpr { impl ComposableFilter for StringExpr {} -impl EntityExpr +impl EntityExpr for StringExpr { type Marker = Entity; @@ -513,7 +580,33 @@ impl CreateFilter for StringExpr { match self.entity { EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotExplodedEdgeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), + } + } +} + +impl CreateOp for StringExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + match self.entity { + EntityMarker::Node => self.clone().with_entity(NodeFilter).create_node_op(graph), + EntityMarker::Edge => Err(GraphError::NotNodeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), + } + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + match self.entity { + EntityMarker::Node => Err(GraphError::NotEdgeFilter), + EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), + EntityMarker::ExplodedEdge => { + self.clone().with_entity(ExplodedEdgeFilter).create_edge_op(graph) + } } } } @@ -567,7 +660,7 @@ impl PropValueSetExpr { impl ComposableFilter for PropValueSetExpr {} -impl EntityExpr +impl EntityExpr for PropValueSetExpr { type Marker = Entity; @@ -646,8 +739,34 @@ impl CreateFilter for PropValueSetExpr { ) -> Result, GraphError> { match self.entity { EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), - EntityMarker::Edge => Err(GraphError::NotEdgeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotExplodedEdgeFilter), + EntityMarker::Edge => Err(GraphError::NotNodeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), + } + } +} + +impl CreateOp for PropValueSetExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + match self.entity { + EntityMarker::Node => self.clone().with_entity(NodeFilter).create_node_op(graph), + EntityMarker::Edge => Err(GraphError::NotNodeFilter), + EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), + } + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + match self.entity { + EntityMarker::Node => Err(GraphError::NotEdgeFilter), + EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), + EntityMarker::ExplodedEdge => { + self.clone().with_entity(ExplodedEdgeFilter).create_edge_op(graph) + } } } } diff --git a/raphtory/src/errors.rs b/raphtory/src/errors.rs index cd430101c5..45917e7cd9 100644 --- a/raphtory/src/errors.rs +++ b/raphtory/src/errors.rs @@ -421,9 +421,6 @@ pub enum GraphError { #[error("Node filter is not supported for edge filtering")] NodeFilterIsNotEdgeFilter, - #[error("Expression is not an exploded edge filter")] - NotExplodedEdgeFilter, - #[error("Indexing not supported")] IndexingNotSupported, From 71389623305f08f1860abf4e6c20774f6823994f Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:26:58 +0100 Subject: [PATCH 078/155] fix node filter Python wrappers: add From> and make is_active return PyExpr --- raphtory/src/python/filter/node_expr.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index c2d68becb8..5ea709c7f7 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -57,6 +57,18 @@ impl>> From for PyExpr { } } +impl From> for PyPropertyExpr { + fn from(value: Arc) -> Self { + PyPropertyExpr(value) + } +} + +impl From> for PyNodeFilter { + fn from(value: Arc) -> Self { + PyNodeFilter(value) + } +} + #[pymethods] impl PyExpr { fn __eq__(&self, other: &Self) -> Self { @@ -417,14 +429,14 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn layers(&self, layers: FromIterable) -> PyNodeFilter { - self.0.dyn_layer(layers.into_vec()).into() + self.0.dyn_layer(layers.to_vec()).into() } /// Matches nodes that have at least one event in the current view. /// /// Returns: /// filter.FilterExpr: - fn is_active(&self) -> PyNodeFilter { + fn is_active(&self) -> PyExpr { self.0.dyn_is_active().into() } From 9f45b9dd2f8e852b53fb0aaed08cea8e63464e52 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:07:28 +0100 Subject: [PATCH 079/155] mirror DynNodeFilterFactory pattern on edge side: add DynEdgeFilterFactory + migrate PyEdgeFilter/PyExplodedEdgeFilter --- raphtory/src/python/filter/edge_expr.rs | 120 ++++++++++++++---- .../src/python/filter/exploded_edge_expr.rs | 55 ++++---- 2 files changed, 128 insertions(+), 47 deletions(-) diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 1d07a0c0ae..2f93eba342 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -1,15 +1,16 @@ use crate::{ db::graph::views::filter::model::{ edge_filter::{EdgeEndpointWrapper, EdgeFilter}, + node_expr::DynCreateOp, node_filter::NodeFilter, - EdgeFilterFactory, EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, + CreateView, EdgeFilterFactory, EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, + prelude::EdgeViewOps, python::{filter::node_expr::PyExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods}; use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; -use crate::prelude::EdgeViewOps; /// Entry point for filtering an edge endpoint (source or destination). /// @@ -58,6 +59,81 @@ impl PyEdgeEndpoint { } } +pub trait DynEdgeFilterFactory: Send + Sync + 'static { + fn dyn_property(&self, name: String) -> Arc; + fn dyn_metadata(&self, name: String) -> Arc; + + fn dyn_is_active(&self) -> Arc; + fn dyn_is_valid(&self) -> Arc; + fn dyn_is_deleted(&self) -> Arc; + fn dyn_is_self_loop(&self) -> Arc; + + fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc; + fn dyn_at(&self, time: EventTime) -> Arc; + fn dyn_after(&self, time: EventTime) -> Arc; + fn dyn_before(&self, time: EventTime) -> Arc; + fn dyn_latest(&self) -> Arc; + fn dyn_snapshot_at(&self, time: EventTime) -> Arc; + fn dyn_snapshot_latest(&self) -> Arc; + fn dyn_layer(&self, layers: Vec) -> Arc; +} + +impl DynEdgeFilterFactory for T +where + T: EdgeFilterFactory + EdgeViewFilterOps + ViewWrapOps + CreateView + Clone + Send + Sync + 'static, +{ + fn dyn_property(&self, name: String) -> Arc { + Arc::new(PropertyFilterFactory::property(self, name)) + } + fn dyn_metadata(&self, name: String) -> Arc { + Arc::new(PropertyFilterFactory::metadata(self, name)) + } + + fn dyn_is_active(&self) -> Arc { + Arc::new(self.is_active()) + } + fn dyn_is_valid(&self) -> Arc { + Arc::new(self.is_valid()) + } + fn dyn_is_deleted(&self) -> Arc { + Arc::new(self.is_deleted()) + } + fn dyn_is_self_loop(&self) -> Arc { + Arc::new(self.is_self_loop()) + } + + fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { + Arc::new(self.clone().window(start, end)) + } + fn dyn_at(&self, time: EventTime) -> Arc { + Arc::new(self.clone().at(time)) + } + fn dyn_after(&self, time: EventTime) -> Arc { + Arc::new(self.clone().after(time)) + } + fn dyn_before(&self, time: EventTime) -> Arc { + Arc::new(self.clone().before(time)) + } + fn dyn_latest(&self) -> Arc { + Arc::new(self.clone().latest()) + } + fn dyn_snapshot_at(&self, time: EventTime) -> Arc { + Arc::new(self.clone().snapshot_at(time)) + } + fn dyn_snapshot_latest(&self) -> Arc { + Arc::new(self.clone().snapshot_latest()) + } + fn dyn_layer(&self, layers: Vec) -> Arc { + Arc::new(self.clone().layer(layers)) + } +} + +impl From> for PyEdgeFilter { + fn from(value: Arc) -> Self { + PyEdgeFilter(value) + } +} + /// Entry point for constructing edge filter expressions. /// /// The `Edge` filter provides: @@ -72,7 +148,7 @@ impl PyEdgeEndpoint { /// Edge.window(0, 10).is_active() /// Edge.layer("fire_nation").is_valid() #[pyclass(frozen, name = "Edge", module = "raphtory.filter")] -pub struct PyEdgeFilter(Arc); +pub struct PyEdgeFilter(Arc); #[pymethods] impl PyEdgeFilter { @@ -96,7 +172,7 @@ impl PyEdgeFilter { /// Arguments: /// name (str): Property key. fn property(&self, name: String) -> PyExpr { - self.0.property(name).into() + self.0.dyn_property(name).into() } /// Filters an edge metadata field by name. @@ -104,71 +180,71 @@ impl PyEdgeFilter { /// Arguments: /// name (str): Metadata key. fn metadata(&self, name: String) -> PyExpr { - self.0.metadata(name).into() + self.0.dyn_metadata(name).into() } /// Restricts edge evaluation to the given time window. fn window(&self, start: EventTime, end: EventTime) -> PyEdgeFilter { - self.0.clone().window(start, end).into() + self.0.dyn_window(start, end).into() } /// Restricts edge evaluation to a single point in time. fn at(&self, time: EventTime) -> PyEdgeFilter { - self.0.clone().at(time).into() + self.0.dyn_at(time).into() } /// Restricts edge evaluation to times strictly after the given time. fn after(&self, time: EventTime) -> PyEdgeFilter { - self.0.clone().after(time).into() + self.0.dyn_after(time).into() } /// Restricts edge evaluation to times strictly before the given time. fn before(&self, time: EventTime) -> PyEdgeFilter { - self.0.clone().before(time).into() + self.0.dyn_before(time).into() } /// Evaluates edge predicates against the latest available edge state. fn latest(&self) -> PyEdgeFilter { - self.0.clone().latest().into() + self.0.dyn_latest().into() } /// Evaluates edge predicates against a snapshot of the graph at a given time. fn snapshot_at(&self, time: EventTime) -> PyEdgeFilter { - self.0.clone().snapshot_at(time).into() + self.0.dyn_snapshot_at(time).into() } /// Evaluates edge predicates against the most recent snapshot of the graph. fn snapshot_latest(&self) -> PyEdgeFilter { - self.0.clone().snapshot_latest().into() + self.0.dyn_snapshot_latest().into() } /// Restricts evaluation to edges belonging to the given layer. fn layer(&self, layer: String) -> PyEdgeFilter { - self.0.clone().layer(layer).into() + self.0.dyn_layer(vec![layer]).into() } /// Restricts evaluation to edges belonging to any of the given layers. fn layers(&self, layers: FromIterable) -> PyEdgeFilter { - self.0.clone().layer(layers).into() + self.0.dyn_layer(layers.to_vec()).into() } /// Matches edges that have at least one event in the current view. - fn is_active(&self) -> PyEdgeFilter { - self.0.is_active().into() + fn is_active(&self) -> PyExpr { + self.0.dyn_is_active().into() } /// Matches edges that are structurally valid in the current view. - fn is_valid(&self) -> PyEdgeFilter { - self.0.is_valid().into() + fn is_valid(&self) -> PyExpr { + self.0.dyn_is_valid().into() } /// Matches edges that have been deleted. - fn is_deleted(&self) -> PyEdgeFilter { - self.0.is_deleted().into() + fn is_deleted(&self) -> PyExpr { + self.0.dyn_is_deleted().into() } /// Matches edges that are self-loops (source == destination). - fn is_self_loop(&self) -> PyEdgeFilter { - self.0.is_self_loop().into() + fn is_self_loop(&self) -> PyExpr { + self.0.dyn_is_self_loop().into() } } diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index f6c1d76706..87a33a6bd6 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -1,14 +1,13 @@ use crate::{ - db::graph::views::filter::model::{ - exploded_edge_filter::ExplodedEdgeFilter, EdgeFilterFactory, EdgeViewFilterOps, - PropertyFilterFactory, ViewWrapOps, + db::graph::views::filter::model::exploded_edge_filter::ExplodedEdgeFilter, + python::{ + filter::{edge_expr::DynEdgeFilterFactory, node_expr::PyExpr}, + types::iterable::FromIterable, }, - python::{filter::node_expr::PyExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods}; use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; -use crate::prelude::EdgeViewOps; /// Entry point for constructing **exploded edge** filter expressions. /// @@ -26,7 +25,13 @@ use crate::prelude::EdgeViewOps; /// ExplodedEdge.window(0, 10).is_active() /// ExplodedEdge.layer("fire_nation").is_valid() #[pyclass(frozen, name = "ExplodedEdge", module = "raphtory.filter")] -pub struct PyExplodedEdgeFilter(Arc); +pub struct PyExplodedEdgeFilter(Arc); + +impl From> for PyExplodedEdgeFilter { + fn from(value: Arc) -> Self { + PyExplodedEdgeFilter(value) + } +} #[pymethods] impl PyExplodedEdgeFilter { @@ -42,7 +47,7 @@ impl PyExplodedEdgeFilter { /// Arguments: /// name (str): Property key. fn property(&self, name: String) -> PyExpr { - self.0.property(name).into() + self.0.dyn_property(name).into() } /// Filters an exploded edge metadata field by name. @@ -52,73 +57,73 @@ impl PyExplodedEdgeFilter { /// Arguments: /// name (str): Metadata key. fn metadata(&self, name: String) -> PyExpr { - self.0.metadata(name).into() + self.0.dyn_metadata(name).into() } /// Restricts exploded edge evaluation to the given time window. /// /// The window is inclusive of `start` and exclusive of `end`. fn window(&self, start: EventTime, end: EventTime) -> PyExplodedEdgeFilter { - self.0.clone().window(start, end).into() + self.0.dyn_window(start, end).into() } /// Restricts exploded edge evaluation to a single point in time. fn at(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.0.clone().at(time).into() + self.0.dyn_at(time).into() } /// Restricts exploded edge evaluation to times strictly after the given time. fn after(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.0.clone().after(time).into() + self.0.dyn_after(time).into() } /// Restricts exploded edge evaluation to times strictly before the given time. fn before(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.0.clone().before(time).into() + self.0.dyn_before(time).into() } /// Evaluates exploded edge predicates against the latest available state. fn latest(&self) -> PyExplodedEdgeFilter { - self.0.clone().latest().into() + self.0.dyn_latest().into() } /// Evaluates exploded edge predicates against a snapshot of the graph at a given time. fn snapshot_at(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.0.clone().snapshot_at(time).into() + self.0.dyn_snapshot_at(time).into() } /// Evaluates exploded edge predicates against the most recent snapshot of the graph. fn snapshot_latest(&self) -> PyExplodedEdgeFilter { - self.0.clone().snapshot_latest().into() + self.0.dyn_snapshot_latest().into() } /// Restricts evaluation to exploded edges belonging to the given layer. fn layer(&self, layer: String) -> PyExplodedEdgeFilter { - self.0.clone().layer(layer).into() + self.0.dyn_layer(vec![layer]).into() } /// Restricts evaluation to exploded edges belonging to any of the given layers. fn layers(&self, layers: FromIterable) -> PyExplodedEdgeFilter { - self.0.clone().layer(layers).into() + self.0.dyn_layer(layers.to_vec()).into() } /// Matches exploded edges that have at least one event in the current view. - fn is_active(&self) -> PyExplodedEdgeFilter { - self.0.is_active().into() + fn is_active(&self) -> PyExpr { + self.0.dyn_is_active().into() } /// Matches exploded edges that are structurally valid in the current view. - fn is_valid(&self) -> PyExplodedEdgeFilter { - self.0.is_valid().into() + fn is_valid(&self) -> PyExpr { + self.0.dyn_is_valid().into() } /// Matches exploded edges that have been deleted. - fn is_deleted(&self) -> PyExplodedEdgeFilter { - self.0.is_deleted().into() + fn is_deleted(&self) -> PyExpr { + self.0.dyn_is_deleted().into() } /// Matches exploded edges that are self-loops (source == destination). - fn is_self_loop(&self) -> PyExplodedEdgeFilter { - self.0.is_self_loop().into() + fn is_self_loop(&self) -> PyExpr { + self.0.dyn_is_self_loop().into() } } From 36c77b75b220e5c9728b46b7bda3243ca9aca020 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:26:39 +0100 Subject: [PATCH 080/155] relax CreateOp bound on PropertyExpr/MetadataExpr to allow edge-side use --- raphtory/src/db/graph/views/filter/model/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index c7cbc93012..fb92b749ed 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -281,7 +281,7 @@ impl EntityExpr for PropertyExpr { } } -impl CreateOp +impl CreateOp for PropertyExpr { fn create_node_op<'g, G: GraphView + 'g>( @@ -322,7 +322,7 @@ impl EntityExpr for MetadataExpr { } } -impl CreateOp +impl CreateOp for MetadataExpr { fn create_node_op<'g, G: GraphView + 'g>( From e694c0e731b7afcf5241571a539b1281fa557af8 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:55:22 +0100 Subject: [PATCH 081/155] add with_entity to BinaryCmpExpr and simplify EntityMarker dispatchers; drop dead PyGraphFilter::new --- .../views/filter/model/node_expr/filters.rs | 48 ++++++++----------- raphtory/src/python/filter/graph_filter.rs | 7 +-- 2 files changed, 21 insertions(+), 34 deletions(-) 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 bf20654b57..43e86366f8 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 @@ -108,6 +108,15 @@ impl BinaryCmpExpr { entity, } } + + fn with_entity(self, entity: T) -> BinaryCmpExpr { + BinaryCmpExpr { + left: self.left, + op: self.op, + right: self.right, + entity, + } + } } impl ComposableFilter for BinaryCmpExpr {} @@ -193,18 +202,11 @@ where graph: G, ) -> Result, GraphError> { Ok(match self.entity { - EntityMarker::Node => Arc::new( - BinaryCmpExpr::new(self.left, self.op, self.right, NodeFilter) - .create_filter(graph)?, - ), - EntityMarker::Edge => Arc::new( - BinaryCmpExpr::new(self.left, self.op, self.right, EdgeFilter) - .create_filter(graph)?, - ), - EntityMarker::ExplodedEdge => Arc::new( - BinaryCmpExpr::new(self.left, self.op, self.right, ExplodedEdgeFilter) - .create_filter(graph)?, - ), + EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph)?), + EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph)?), + EntityMarker::ExplodedEdge => { + Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) + } }) } @@ -213,10 +215,7 @@ where graph: G, ) -> Result, GraphError> { match self.entity { - EntityMarker::Node => Ok(BinaryCmpExpr::new( - self.left, self.op, self.right, NodeFilter, - ) - .create_node_filter(graph)?), + EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), } @@ -233,10 +232,7 @@ where graph: G, ) -> Result> + 'g>, GraphError> { match self.entity { - EntityMarker::Node => BinaryCmpExpr::new( - self.left.clone(), self.op, self.right.clone(), NodeFilter, - ) - .create_node_op(graph), + EntityMarker::Node => self.clone().with_entity(NodeFilter).create_node_op(graph), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), } @@ -248,14 +244,10 @@ where ) -> Result> + 'g>, GraphError> { match self.entity { EntityMarker::Node => Err(GraphError::NotEdgeFilter), - EntityMarker::Edge => BinaryCmpExpr::new( - self.left.clone(), self.op, self.right.clone(), EdgeFilter, - ) - .create_edge_op(graph), - EntityMarker::ExplodedEdge => BinaryCmpExpr::new( - self.left.clone(), self.op, self.right.clone(), ExplodedEdgeFilter, - ) - .create_edge_op(graph), + EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), + EntityMarker::ExplodedEdge => { + self.clone().with_entity(ExplodedEdgeFilter).create_edge_op(graph) + } } } } diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index 30dccc9301..24c8f766a6 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -1,5 +1,5 @@ use crate::{ - db::graph::views::filter::model::{graph_filter::GraphFilter, DynView, ViewWrapOps}, + db::graph::views::filter::model::{DynView, ViewWrapOps}, python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; @@ -31,11 +31,6 @@ pub struct PyGraphFilter(pub(crate) DynView); #[pymethods] impl PyGraphFilter { - #[new] - fn new() -> Self { - PyGraphFilter(Arc::new(GraphFilter)) - } - /// Restricts evaluation to events within a time window. /// /// The window is inclusive of `start` and exclusive of `end`. From b26c29fb06b56d2228fd8546047863ce728901fc Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:02:18 +0100 Subject: [PATCH 082/155] drop search_nodes/search_edges from Python API and remove composite-path helpers from PyFilterExpr, ref --- .../exploded_edge_expr_filtered_graph.rs | 13 ++--- raphtory/src/db/graph/views/filter/mod.rs | 2 +- .../db/graph/views/filter/model/and_filter.rs | 3 +- .../views/filter/model/edge_expr/filters.rs | 12 ++--- .../graph/views/filter/model/edge_expr/ops.rs | 2 +- .../graph/views/filter/model/edge_filter.rs | 13 ++--- .../src/db/graph/views/filter/model/mod.rs | 8 +-- .../views/filter/model/node_expr/dyn_expr.rs | 2 +- .../views/filter/model/node_expr/filters.rs | 51 ++++++++++--------- .../db/graph/views/filter/model/or_filter.rs | 3 +- raphtory/src/python/filter/edge_expr.rs | 9 +++- raphtory/src/python/filter/filter_expr.rs | 16 +----- raphtory/src/python/graph/index.rs | 51 ++----------------- 13 files changed, 64 insertions(+), 121 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs b/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs index ca3c1364d5..7fe3768542 100644 --- a/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs @@ -7,10 +7,9 @@ use crate::{ }, view::internal::{ Immutable, InheritEdgeFilterOps, InheritEdgeHistoryFilter, - InheritEdgeLayerFilterOps, InheritLayerOps, - InheritListOps, InheritMaterialize, InheritNodeFilterOps, InheritNodeHistoryFilter, - InheritStorageOps, InheritTimeSemantics, - InternalExplodedEdgeFilterOps, Static, + InheritEdgeLayerFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, + InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, + InheritTimeSemantics, InternalExplodedEdgeFilterOps, Static, }, }, graph::views::filter::model::edge_expr::EdgeOp, @@ -121,8 +120,10 @@ impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> return false; } let edge_ref: EdgeRef = self.core_edge(Either::Left(eid.eid())).out_ref(); - self.filter - .apply(self.graph.core_graph(), edge_ref.at_layer(eid.layer()).at(t)) + self.filter.apply( + self.graph.core_graph(), + edge_ref.at_layer(eid.layer()).at(t), + ) } } impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgeLayerFilterOps diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 7cea62838b..f5c619099d 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -14,13 +14,13 @@ pub mod and_filtered_graph; pub mod edge_expr_filtered_graph; pub mod edge_node_filtered_graph; pub mod edge_property_filtered_graph; +mod exploded_edge_expr_filtered_graph; pub mod exploded_edge_node_filtered_graph; pub mod exploded_edge_property_filter; pub mod model; pub mod node_filtered_graph; pub mod not_filtered_graph; pub mod or_filtered_graph; -mod exploded_edge_expr_filtered_graph; pub struct Unfiltered; 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 39116f378a..47bdb01dc8 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -9,8 +9,7 @@ use crate::{ model::{ edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, - ComposableFilter, TryAsCompositeFilter, + node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, }, CreateFilter, }, diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index b221ad0b7a..fcfdb4d347 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -10,20 +10,18 @@ use super::{ }, EdgeOp, }; -use crate::db::graph::views::filter::exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph; -use crate::db::graph::views::filter::model::{validate_binary_op, validate_string_op, ExplodedEdgeFilter}; pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpExpr, StringExpr, UnaryExpr}; use crate::{ db::{ api::{state::ops::NotANodeFilter, view::internal::GraphView}, graph::views::filter::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, + exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, model::{ - edge_filter::{CompositeEdgeFilter, EdgeFilter} - , + edge_filter::{CompositeEdgeFilter, EdgeFilter}, node_expr::{filters::PropValueSetExpr, CreateOp}, - CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, - TryAsCompositeFilter, + validate_binary_op, validate_string_op, CompositeExplodedEdgeFilter, + CompositeNodeFilter, CreateFilter, ExplodedEdgeFilter, TryAsCompositeFilter, }, }, }, @@ -112,7 +110,7 @@ where R: CreateOp, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - ExplodedEdgeExprFilteredGraph + 'graph>>; + ExplodedEdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index e72de604f4..e0f21f1c17 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -22,12 +22,12 @@ use crate::db::{ api::state::ops::NodeOp, graph::views::filter::model::{ edge_filter::Endpoint, + filter_operator::{BinaryOp, Comparable}, node_expr::ops::{broadcast_binary, broadcast_unary}, }, }; use raphtory_api::core::entities::properties::prop::PropArray; use std::sync::Arc; -use crate::db::graph::views::filter::model::filter_operator::{BinaryOp, Comparable}; use crate::db::graph::views::filter::model::{ filter_operator::UnaryOp, SetOp, StringComparable, StringOp, 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 9a7c9facdf..9df4ef0377 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -23,10 +23,10 @@ use crate::{ snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, AllExpr, AndFilter, AnyExpr, AvgExpr, CombinedFilter, ComposableFilter, CreateView, - EdgeFilterFactory, EdgeViewFilterOps, EntityAggOps, - EntityMarker, FirstExpr, InternalViewWrapOps, LastExpr, LenExpr, MaxExpr, - MetadataExpr, MinExpr, NotFilter, OrFilter, PropertyExpr, PropertyFilterFactory, - SumExpr, TemporalPropExpr, TryAsCompositeFilter, Wrap, + EdgeFilterFactory, EdgeViewFilterOps, EntityAggOps, EntityMarker, FirstExpr, + InternalViewWrapOps, LastExpr, LenExpr, MaxExpr, MetadataExpr, MinExpr, NotFilter, + OrFilter, PropertyExpr, PropertyFilterFactory, SumExpr, TemporalPropExpr, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -34,10 +34,7 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use raphtory_api::core::{ - entities::properties::prop::Prop, - storage::timeindex::EventTime, -}; +use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; use std::{fmt, fmt::Display, sync::Arc}; // User facing entry for building edge filters. diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index fb92b749ed..dd4f047551 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -281,9 +281,7 @@ impl EntityExpr for PropertyExpr { } } -impl CreateOp - for PropertyExpr -{ +impl CreateOp for PropertyExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -322,9 +320,7 @@ impl EntityExpr for MetadataExpr { } } -impl CreateOp - for MetadataExpr -{ +impl CreateOp for MetadataExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 1ae281b726..a4e68613ef 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -102,7 +102,7 @@ impl>> DynCreateOp for E { fn dyn_create_edge_op<'g>( &self, - graph: Arc, + graph: Arc, ) -> Result> + 'g>, GraphError> { self.create_edge_op(graph) } 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 43e86366f8..a56ef2b59c 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 @@ -44,10 +44,9 @@ use super::{ ops::{ BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, ListAwareUnaryNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, - } - , CreateOp, EntityExpr, + }, + CreateOp, EntityExpr, }; -use crate::db::graph::views::filter::model::{coerce_set_values, resolved_prop_type, validate_binary_op, validate_const_castable, validate_string_op}; use crate::{ db::{ api::{ @@ -56,11 +55,14 @@ use crate::{ }, graph::views::filter::{ model::{ + coerce_set_values, edge_expr::EdgeOp, edge_filter::CompositeEdgeFilter, filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, - ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, - EntityMarker, ExplodedEdgeFilter, TryAsCompositeFilter, + resolved_prop_type, validate_binary_op, validate_const_castable, + validate_string_op, ComposableFilter, CompositeExplodedEdgeFilter, + CompositeNodeFilter, CreateFilter, EntityMarker, ExplodedEdgeFilter, + TryAsCompositeFilter, }, node_filtered_graph::NodeFilteredGraph, }, @@ -122,7 +124,7 @@ impl BinaryCmpExpr { impl ComposableFilter for BinaryCmpExpr {} impl EntityExpr -for BinaryCmpExpr + for BinaryCmpExpr { type Marker = E; fn entity(&self) -> Self::Marker { @@ -245,9 +247,10 @@ where match self.entity { EntityMarker::Node => Err(GraphError::NotEdgeFilter), EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), - EntityMarker::ExplodedEdge => { - self.clone().with_entity(ExplodedEdgeFilter).create_edge_op(graph) - } + EntityMarker::ExplodedEdge => self + .clone() + .with_entity(ExplodedEdgeFilter) + .create_edge_op(graph), } } } @@ -305,9 +308,7 @@ impl UnaryExpr { impl ComposableFilter for UnaryExpr {} -impl EntityExpr -for UnaryExpr -{ +impl EntityExpr for UnaryExpr { type Marker = Entity; fn entity(&self) -> Self::Marker { self.entity @@ -414,9 +415,10 @@ where match self.entity { EntityMarker::Node => Err(GraphError::NotEdgeFilter), EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), - EntityMarker::ExplodedEdge => { - self.clone().with_entity(ExplodedEdgeFilter).create_edge_op(graph) - } + EntityMarker::ExplodedEdge => self + .clone() + .with_entity(ExplodedEdgeFilter) + .create_edge_op(graph), } } } @@ -491,7 +493,7 @@ impl StringExpr { impl ComposableFilter for StringExpr {} impl EntityExpr -for StringExpr + for StringExpr { type Marker = Entity; fn entity(&self) -> Self::Marker { @@ -596,9 +598,10 @@ impl CreateOp for StringExpr { match self.entity { EntityMarker::Node => Err(GraphError::NotEdgeFilter), EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), - EntityMarker::ExplodedEdge => { - self.clone().with_entity(ExplodedEdgeFilter).create_edge_op(graph) - } + EntityMarker::ExplodedEdge => self + .clone() + .with_entity(ExplodedEdgeFilter) + .create_edge_op(graph), } } } @@ -653,7 +656,7 @@ impl PropValueSetExpr { impl ComposableFilter for PropValueSetExpr {} impl EntityExpr -for PropValueSetExpr + for PropValueSetExpr { type Marker = Entity; fn entity(&self) -> Self::Marker { @@ -664,7 +667,6 @@ for PropValueSetExpr } } - impl CreateOp for PropValueSetExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, @@ -756,9 +758,10 @@ impl CreateOp for PropValueSetExpr { match self.entity { EntityMarker::Node => Err(GraphError::NotEdgeFilter), EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), - EntityMarker::ExplodedEdge => { - self.clone().with_entity(ExplodedEdgeFilter).create_edge_op(graph) - } + EntityMarker::ExplodedEdge => self + .clone() + .with_entity(ExplodedEdgeFilter) + .create_edge_op(graph), } } } 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 5b14909818..674efb7175 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -8,8 +8,7 @@ use crate::{ model::{ edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, - ComposableFilter, TryAsCompositeFilter, + node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, }, or_filtered_graph::OrFilteredGraph, CreateFilter, diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 2f93eba342..d5496d96b3 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -80,7 +80,14 @@ pub trait DynEdgeFilterFactory: Send + Sync + 'static { impl DynEdgeFilterFactory for T where - T: EdgeFilterFactory + EdgeViewFilterOps + ViewWrapOps + CreateView + Clone + Send + Sync + 'static, + T: EdgeFilterFactory + + EdgeViewFilterOps + + ViewWrapOps + + CreateView + + Clone + + Send + + Sync + + 'static, { fn dyn_property(&self, name: String) -> Arc { Arc::new(PropertyFilterFactory::property(self, name)) diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index d9d3ec7f0e..c53a5f6d09 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -5,11 +5,7 @@ use crate::{ view::{internal::GraphView, BoxableGraphView}, }, graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, - not_filter::NotFilter, or_filter::OrFilter, AndFilter, DynCreateFilter, - TryAsCompositeFilter, - }, + model::{not_filter::NotFilter, or_filter::OrFilter, AndFilter, DynCreateFilter}, CreateFilter, }, }, @@ -29,16 +25,6 @@ use std::sync::Arc; #[derive(Clone)] pub struct PyFilterExpr(pub Arc); -impl PyFilterExpr { - pub fn try_as_node_filter(&self) -> Result { - self.0.try_as_composite_node_filter() - } - - pub fn try_as_edge_filter(&self) -> Result { - self.0.try_as_composite_edge_filter() - } -} - #[pymethods] impl PyFilterExpr { pub fn __and__(&self, other: &Self) -> Self { diff --git a/raphtory/src/python/graph/index.rs b/raphtory/src/python/graph/index.rs index a6e23e3173..0350a83598 100644 --- a/raphtory/src/python/graph/index.rs +++ b/raphtory/src/python/graph/index.rs @@ -1,14 +1,11 @@ use crate::{ - db::{ - api::view::{ - internal::{CoreGraphOps, DynamicGraph}, - IndexSpec, IndexSpecBuilder, IntoDynamic, MaterializedGraph, - }, - graph::{edge::EdgeView, node::NodeView}, + db::api::view::{ + internal::{CoreGraphOps, DynamicGraph}, + IndexSpec, IndexSpecBuilder, IntoDynamic, MaterializedGraph, }, errors::GraphError, prelude::SearchableGraphOps, - python::{filter::filter_expr::PyFilterExpr, graph::views::graph_view::PyGraphView}, + python::graph::views::graph_view::PyGraphView, }; use ahash::HashSet; use pyo3::prelude::*; @@ -256,44 +253,4 @@ impl PyGraphView { spec, }) } - - /// Searches for nodes which match the given filter expression. This uses Tantivy's exact search. - /// - /// Arguments: - /// filter: The filter expression to search for. - /// limit(int): The maximum number of results to return. Defaults to 25. - /// offset(int): The number of results to skip. This is useful for pagination. Defaults to 0. - /// - /// Returns: - /// list[Node]: A list of nodes which match the filter expression. The list will be empty if no nodes match. - #[pyo3(signature = (filter, limit=25, offset=0))] - fn search_nodes( - &self, - filter: PyFilterExpr, - limit: usize, - offset: usize, - ) -> Result>, GraphError> { - let filter = filter.try_as_node_filter()?; - self.graph.search_nodes(filter, limit, offset) - } - - /// Searches for edges which match the given filter expression. This uses Tantivy's exact search. - /// - /// Arguments: - /// filter: The filter expression to search for. - /// limit(int): The maximum number of results to return. Defaults to 25. - /// offset(int): The number of results to skip. This is useful for pagination. Defaults to 0. - /// - /// Returns: - /// list[Edge]: A list of edges which match the filter expression. The list will be empty if no edges match the query. - #[pyo3(signature = (filter, limit=25, offset=0))] - fn search_edges( - &self, - filter: PyFilterExpr, - limit: usize, - offset: usize, - ) -> Result>, GraphError> { - let filter = filter.try_as_edge_filter()?; - self.graph.search_edges(filter, limit, offset) - } } From 29cdf2805c0bcce282fb77c68b10704dcd3ca253 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:21:00 +0100 Subject: [PATCH 083/155] delete composite-path test invocations and trait bounds from raphtory-tests --- raphtory-tests/src/assertions.rs | 34 ++--- raphtory-tests/tests/algo_tests/components.rs | 4 +- raphtory-tests/tests/test_filters.rs | 128 ------------------ 3 files changed, 19 insertions(+), 147 deletions(-) diff --git a/raphtory-tests/src/assertions.rs b/raphtory-tests/src/assertions.rs index 614090960a..8543e93e09 100644 --- a/raphtory-tests/src/assertions.rs +++ b/raphtory-tests/src/assertions.rs @@ -8,7 +8,7 @@ use raphtory::{ db::{ api::view::filter_ops::{EdgeSelect, NodeSelect}, graph::views::{ - filter::{model::TryAsCompositeFilter, CreateFilter}, + filter::CreateFilter, window_graph::WindowedGraph, }, }, @@ -65,9 +65,9 @@ pub trait ApplyFilter { fn apply(&self, graph: G) -> Vec; } -pub struct FilterNodes(F); +pub struct FilterNodes(F); -impl ApplyFilter for FilterNodes { +impl ApplyFilter for FilterNodes { fn apply(&self, graph: G) -> Vec { let mut results = graph .filter(self.0.clone()) @@ -81,9 +81,9 @@ impl ApplyFilter for FilterNodes } } -pub struct SelectNodes(F); +pub struct SelectNodes(F); -impl ApplyFilter for SelectNodes { +impl ApplyFilter for SelectNodes { fn apply(&self, graph: G) -> Vec { let mut results = graph .nodes() @@ -97,9 +97,9 @@ impl ApplyFilter for SelectNodes } } -pub struct FilterNeighbours(F, String, Direction); +pub struct FilterNeighbours(F, String, Direction); -impl ApplyFilter for FilterNeighbours { +impl ApplyFilter for FilterNeighbours { fn apply(&self, graph: G) -> Vec { let filter_applied = graph .node(self.1.clone()) @@ -120,9 +120,9 @@ impl ApplyFilter for FilterNeigh } } -pub struct FilterEdges(F); +pub struct FilterEdges(F); -impl ApplyFilter for FilterEdges { +impl ApplyFilter for FilterEdges { fn apply(&self, graph: G) -> Vec { let mut results = graph .filter(self.0.clone()) @@ -136,9 +136,9 @@ impl ApplyFilter for FilterEdges } } -pub struct SelectEdges(F); +pub struct SelectEdges(F); -impl ApplyFilter for SelectEdges { +impl ApplyFilter for SelectEdges { fn apply(&self, graph: G) -> Vec { let mut results = graph .edges() @@ -156,7 +156,7 @@ impl ApplyFilter for SelectEdges pub fn assert_filter_nodes_results( init_graph: impl FnOnce(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { @@ -173,7 +173,7 @@ pub fn assert_filter_nodes_results( pub fn assert_select_nodes_results( init_graph: impl FnOnce(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { @@ -207,7 +207,7 @@ where pub fn assert_filter_nodes_err( init_graph: fn(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &str, variants: impl Into>, ) { @@ -239,7 +239,7 @@ pub fn assert_filter_neighbours_results( transform: impl GraphTransformer, node_name: impl AsRef, direction: Direction, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { @@ -257,7 +257,7 @@ pub fn assert_filter_neighbours_results( pub fn assert_filter_edges_results( init_graph: impl FnOnce(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { @@ -275,7 +275,7 @@ pub fn assert_filter_edges_results( pub fn assert_select_edges_results( init_graph: impl FnOnce(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { diff --git a/raphtory-tests/tests/algo_tests/components.rs b/raphtory-tests/tests/algo_tests/components.rs index b247462e8b..bc661fd507 100644 --- a/raphtory-tests/tests/algo_tests/components.rs +++ b/raphtory-tests/tests/algo_tests/components.rs @@ -264,7 +264,7 @@ mod in_component_test { graph::views::filter::{ model::{ graph_filter::GraphFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, TryAsCompositeFilter, ViewWrapOps, + PropertyFilterFactory, ViewWrapOps, }, CreateFilter, }, @@ -284,7 +284,7 @@ mod in_component_test { assert_eq!(results, correct); } - fn check_node_filtered( + fn check_node_filtered( graph: &Graph, node_id: u64, filter: F, diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 6bbb4ad41f..c93600685f 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -4374,14 +4374,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p2") @@ -4395,14 +4387,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter.property("p1").eq("pometry").or(NodeFilter .property("p2") @@ -4416,14 +4400,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .node_type() @@ -4437,14 +4413,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p9") @@ -4458,14 +4426,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .node_type() @@ -4479,14 +4439,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .name() @@ -4500,14 +4452,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .name() @@ -4522,14 +4466,6 @@ mod composite_node_filter_tests { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -10558,14 +10494,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = EdgeFilter .property("p2") @@ -10579,14 +10507,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = EdgeFilter.property("p1").eq("pometry").or(EdgeFilter .property("p2") @@ -10605,14 +10525,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = EdgeFilter::src() .name() @@ -10626,14 +10538,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = EdgeFilter .property("p2") @@ -10647,14 +10551,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = EdgeFilter::src() .name() @@ -10668,14 +10564,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - vec![TestGraphVariants::Graph], - ); let filter = EdgeFilter::dst() .name() @@ -10689,14 +10577,6 @@ mod composite_edge_filter_tests { &expected_results, TestVariants::EventOnly, ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); let filter = EdgeFilter::src() .name() @@ -10712,14 +10592,6 @@ mod composite_edge_filter_tests { vec![TestGraphVariants::Graph], ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - vec![TestGraphVariants::Graph], - ); } #[test] From 080da9c96ee9b832e9c76ef037d41ff1b6a80b24 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:27:41 +0100 Subject: [PATCH 084/155] drop remaining TryAsCompositeFilter trait bounds and imports from test_filters.rs --- raphtory-tests/src/assertions.rs | 5 +---- raphtory-tests/tests/test_filters.rs | 22 +++++++--------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/raphtory-tests/src/assertions.rs b/raphtory-tests/src/assertions.rs index 8543e93e09..dd517d978e 100644 --- a/raphtory-tests/src/assertions.rs +++ b/raphtory-tests/src/assertions.rs @@ -7,10 +7,7 @@ use std::ops::Range; use raphtory::{ db::{ api::view::filter_ops::{EdgeSelect, NodeSelect}, - graph::views::{ - filter::CreateFilter, - window_graph::WindowedGraph, - }, + graph::views::{filter::CreateFilter, window_graph::WindowedGraph}, }, errors::GraphError, prelude::TimeOps, diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index c93600685f..8bad031098 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -1506,8 +1506,7 @@ mod test_node_filter { node_filter::ops::NodeFilterOps, not_filter::NotFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - ComposableFilter, CompositeNodeFilter, NodeViewFilterOps, - PropertyFilterFactory, TryAsCompositeFilter, ViewWrapOps, + ComposableFilter, NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, CreateFilter, }, @@ -1550,7 +1549,7 @@ mod test_node_filter { manual_expr: F, context: &str, ) where - CF: CreateFilter + TryAsCompositeFilter + Clone, + CF: CreateFilter + Clone, F: Fn(usize) -> bool + Copy, { let expected_select_nodes = graph @@ -4307,7 +4306,7 @@ mod composite_node_filter_tests { db::graph::views::filter::model::{ node_filter::ops::NodeFilterOps, not_filter::NotFilter, property_filter::ops::PropertyFilterOps, ComposableFilter, NodeFilterFactory, - PropertyFilterFactory, TryAsCompositeFilter, + PropertyFilterFactory, }, prelude::NodeFilter, }; @@ -4577,7 +4576,7 @@ mod test_node_property_filter_agg { model::{ node_filter::NodeFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - PropertyFilterFactory, TryAsCompositeFilter, + PropertyFilterFactory, }, CreateFilter, }, @@ -4910,10 +4909,7 @@ mod test_node_property_filter_agg { graph } - fn apply_assertion( - filter: impl TryAsCompositeFilter + CreateFilter + Clone, - expected: &[&str], - ) { + fn apply_assertion(filter: impl CreateFilter + Clone, expected: &[&str]) { assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -4923,10 +4919,7 @@ mod test_node_property_filter_agg { ); } - fn apply_assertion_err( - filter: impl TryAsCompositeFilter + CreateFilter + Clone, - expected: &str, - ) { + fn apply_assertion_err(filter: impl CreateFilter + Clone, expected: &str) { assert_filter_nodes_err( init_nodes_graph, IdentityGraphTransformer, @@ -10409,7 +10402,7 @@ mod composite_edge_filter_tests { use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, not_filter::NotFilter, property_filter::ops::PropertyFilterOps, ComposableFilter, NodeFilterFactory, - PropertyFilterFactory, TryAsCompositeFilter, + PropertyFilterFactory, }; use raphtory_tests::assertions::{ assert_filter_edges_results, TestGraphVariants, TestVariants, @@ -10591,7 +10584,6 @@ mod composite_edge_filter_tests { &expected_results, vec![TestGraphVariants::Graph], ); - } #[test] From 1cf86fbba90e3a854b251722b71fdb4a61645839 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:43:42 +0100 Subject: [PATCH 085/155] remove gql search apis --- raphtory-graphql/src/model/graph/graph.rs | 56 ----------------------- 1 file changed, 56 deletions(-) diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index 25bf609365..bb00079073 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -794,62 +794,6 @@ impl GqlGraph { } } - /// (Experimental) Searches for nodes which match the given filter - /// expression. Uses Tantivy's exact search; requires the graph to have - /// been indexed. - - async fn search_nodes( - &self, - #[graphql(desc = "Composite node filter (by name, property, type, etc.).")] - filter: GqlNodeFilter, - #[graphql(desc = "Maximum number of nodes to return.")] limit: usize, - #[graphql(desc = "Number of matches to skip before returning results.")] offset: usize, - ) -> Result> { - #[cfg(feature = "search")] - { - let self_clone = self.clone(); - blocking_compute(move || { - let f: CompositeNodeFilter = filter.try_into()?; - let nodes = self_clone.graph.search_nodes(f, limit, offset)?; - let result = nodes.into_iter().map(|vv| vv.into()).collect(); - Ok(result) - }) - .await - } - #[cfg(not(feature = "search"))] - { - Err(GraphError::IndexingNotSupported.into()) - } - } - - /// (Experimental) Searches the index for edges which match the given - /// filter expression. Uses Tantivy's exact search; requires the graph to - /// have been indexed. - - async fn search_edges( - &self, - #[graphql(desc = "Composite edge filter (by property, layer, src/dst, etc.).")] - filter: GqlEdgeFilter, - #[graphql(desc = "Maximum number of edges to return.")] limit: usize, - #[graphql(desc = "Number of matches to skip before returning results.")] offset: usize, - ) -> Result> { - #[cfg(feature = "search")] - { - let self_clone = self.clone(); - blocking_compute(move || { - let f: CompositeEdgeFilter = filter.try_into()?; - let edges = self_clone.graph.search_edges(f, limit, offset)?; - let result = edges.into_iter().map(|vv| vv.into()).collect(); - Ok(result) - }) - .await - } - #[cfg(not(feature = "search"))] - { - Err(GraphError::IndexingNotSupported.into()) - } - } - /// Apply a list of view operations in the given order and return the /// resulting graph view. Lets callers compose multiple view transforms /// (window, layer, filter, snapshot, ...) in a single call. From 274d04f46b78dea828b4ea53a4a3cf526b553ae4 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:42:24 +0100 Subject: [PATCH 086/155] fix isactivenode --- .../filter/model/is_active_node_filter.rs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) 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 b422ca221e..040e76cce2 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 @@ -3,17 +3,21 @@ use crate::{ api::state::ops::{GraphView, HistoryOp, Map, NodeOp}, graph::views::filter::{ model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateView, TryAsCompositeFilter, + edge_expr::EdgeOp, + edge_filter::CompositeEdgeFilter, + node_expr::{CreateOp, EntityExpr}, + ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, + TryAsCompositeFilter, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, }, }, errors::GraphError, - prelude::GraphViewOps, + prelude::{GraphViewOps, NodeFilter}, }; -use std::fmt; +use raphtory_api::core::entities::properties::prop::Prop; +use std::{fmt, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IsActiveNode { @@ -26,6 +30,25 @@ impl fmt::Display for IsActiveNode { } } +impl EntityExpr for IsActiveNode { + type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } +} + +impl CreateOp for IsActiveNode { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new( + HistoryOp::new(self.view_expr.create_view(graph)?).map(|h| !h.is_empty()), + )) + } +} + impl CreateFilter for IsActiveNode { type EntityFiltered<'graph, G> = NodeFilteredGraph> From 7d89dda15cb8cdca4e067d474ff684a71b09e279 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:57:45 +0100 Subject: [PATCH 087/155] break recursive Window bound on Dyn*FilterFactory blanket impls via go-dynamic-first pattern --- raphtory/src/python/filter/edge_expr.rs | 29 ++++++++++++++++++++----- raphtory/src/python/filter/node_expr.rs | 29 +++++++++++++++++++------ 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index d5496d96b3..0b5c614102 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -1,9 +1,10 @@ use crate::{ db::graph::views::filter::model::{ edge_filter::{EdgeEndpointWrapper, EdgeFilter}, - node_expr::DynCreateOp, + node_expr::{DynCreateOp, EntityExpr}, node_filter::NodeFilter, - CreateView, EdgeFilterFactory, EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, + CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, + PropertyFilterFactory, ViewWrapOps, }, prelude::EdgeViewOps, python::{filter::node_expr::PyExpr, types::iterable::FromIterable}, @@ -78,16 +79,26 @@ pub trait DynEdgeFilterFactory: Send + Sync + 'static { fn dyn_layer(&self, layers: Vec) -> Arc; } +impl InternalViewWrapOps for Arc { + type Window = Arc; + + fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { + self.dyn_window(start, end) + } +} + impl DynEdgeFilterFactory for T where T: EdgeFilterFactory + EdgeViewFilterOps + ViewWrapOps + CreateView + + EntityExpr + Clone + Send + Sync + 'static, + ::Marker: Into, { fn dyn_property(&self, name: String) -> Arc { Arc::new(PropertyFilterFactory::property(self, name)) @@ -109,17 +120,23 @@ where Arc::new(self.is_self_loop()) } + // Go dynamic before calling window — the Arc impl + // has Window = Self, which terminates the recursive bound resolution. fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { - Arc::new(self.clone().window(start, end)) + let dyn_self: Arc = Arc::new(self.clone()); + dyn_self.window(start, end) } fn dyn_at(&self, time: EventTime) -> Arc { - Arc::new(self.clone().at(time)) + let dyn_self: Arc = Arc::new(self.clone()); + dyn_self.at(time) } fn dyn_after(&self, time: EventTime) -> Arc { - Arc::new(self.clone().after(time)) + let dyn_self: Arc = Arc::new(self.clone()); + dyn_self.after(time) } fn dyn_before(&self, time: EventTime) -> Arc { - Arc::new(self.clone().before(time)) + let dyn_self: Arc = Arc::new(self.clone()); + dyn_self.before(time) } fn dyn_latest(&self) -> Arc { Arc::new(self.clone().latest()) diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 5ea709c7f7..366a8fb887 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -3,7 +3,7 @@ use crate::{ degree_filter::DegreeFilterFactory, node_expr::{CreateOp, DynCreateOp, EntityExpr}, node_state_filter::NodeStateBoolColOp, - CreateView, DynCreateView, EntityMarker, NodeViewFilterOps, PropertyExpr, + CreateView, EntityMarker, InternalViewWrapOps, NodeViewFilterOps, PropertyExpr, PropertyFilterFactory, ViewWrapOps, }, prelude::{EntityAggOps, EntityExprFilterOps, NodeFilter, NodeFilterFactory}, @@ -30,7 +30,7 @@ impl< Arc::new(self.temporal()) } } - +// filter.Node.neighbours.is_active.all #[pyclass(frozen, subclass, name = "Expr", module = "raphtory.filter")] #[derive(Clone)] pub struct PyExpr(Arc); @@ -185,9 +185,18 @@ pub trait DynNodeFilterFactory: Send + Sync + 'static { fn dyn_layer(&self, layers: Vec) -> Arc; } +impl InternalViewWrapOps for Arc { + type Window = Arc; + + fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { + self.dyn_window(start, end) + } +} + impl DynNodeFilterFactory for T where - T: NodeFilterFactory + ViewWrapOps + CreateView + Clone + Send + Sync + 'static, + T: NodeFilterFactory + ViewWrapOps + CreateView + EntityExpr + Clone + Send + Sync + 'static, + ::Marker: Into, { fn dyn_id(&self) -> Arc { Arc::new(self.id()) @@ -220,17 +229,23 @@ where Arc::new(PropertyFilterFactory::metadata(self, name)) } + // Go dynamic before calling window — the Arc impl + // has Window = Self, which terminates the recursive bound resolution. fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { - Arc::new(self.clone().window(start, end)) + let dyn_self: Arc = Arc::new(self.clone()); + dyn_self.window(start, end) } fn dyn_at(&self, time: EventTime) -> Arc { - Arc::new(self.clone().at(time)) + let dyn_self: Arc = Arc::new(self.clone()); + dyn_self.at(time) } fn dyn_after(&self, time: EventTime) -> Arc { - Arc::new(self.clone().after(time)) + let dyn_self: Arc = Arc::new(self.clone()); + dyn_self.after(time) } fn dyn_before(&self, time: EventTime) -> Arc { - Arc::new(self.clone().before(time)) + let dyn_self: Arc = Arc::new(self.clone()); + dyn_self.before(time) } fn dyn_latest(&self) -> Arc { Arc::new(self.clone().latest()) From 6c864cd8d5aca2facb4de1f42dd7c1d28a7336dd Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:13:34 +0100 Subject: [PATCH 088/155] fix isactivenode --- .../views/filter/model/is_active_node_filter.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 040e76cce2..59789ce52b 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 @@ -16,7 +16,7 @@ use crate::{ errors::GraphError, prelude::{GraphViewOps, NodeFilter}, }; -use raphtory_api::core::entities::properties::prop::Prop; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::{fmt, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -36,6 +36,14 @@ impl EntityExpr for IsActiveNode { fn entity(&self) -> Self::Marker { NodeFilter } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } } impl CreateOp for IsActiveNode { @@ -44,7 +52,8 @@ impl CreateOp for IsActiveNode { graph: G, ) -> Result> + 'g>, GraphError> { Ok(Arc::new( - HistoryOp::new(self.view_expr.create_view(graph)?).map(|h| !h.is_empty()), + HistoryOp::new(self.view_expr.create_view(graph)?) + .map(|h| Some(Prop::Bool(!h.is_empty()))), )) } } From bb675356d162616e3f5e0abccae7597b99f8b588 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:37:51 +0100 Subject: [PATCH 089/155] add prop-type aware validation for IsActiveNode: prop_type/nullable overrides, non-const RHS type check, list-aware aggregator gate --- .../views/filter/model/edge_expr/filters.rs | 17 +++-- .../src/db/graph/views/filter/model/mod.rs | 66 +++++++++++++++++++ .../views/filter/model/node_expr/exprs.rs | 6 +- .../views/filter/model/node_expr/filters.rs | 8 ++- 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index fcfdb4d347..26557bbd99 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -20,8 +20,9 @@ use crate::{ model::{ edge_filter::{CompositeEdgeFilter, EdgeFilter}, node_expr::{filters::PropValueSetExpr, CreateOp}, - validate_binary_op, validate_string_op, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateFilter, ExplodedEdgeFilter, TryAsCompositeFilter, + resolved_prop_type, validate_binary_op, validate_string_op, + validate_types_compatible, CompositeExplodedEdgeFilter, CompositeNodeFilter, + CreateFilter, ExplodedEdgeFilter, TryAsCompositeFilter, }, }, }, @@ -85,9 +86,13 @@ where self, graph: G, ) -> Result, GraphError> { + let expr_pt = self.left.prop_type(); let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph.clone())?; - validate_binary_op(&self.op, &left.prop_type())?; + let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); + let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); + validate_binary_op(&self.op, &lhs_pt)?; + validate_types_compatible(&lhs_pt, &rhs_pt)?; let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { left, right, @@ -118,9 +123,13 @@ where self, graph: G, ) -> Result, GraphError> { + let expr_pt = self.left.prop_type(); let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph.clone())?; - validate_binary_op(&self.op, &left.prop_type())?; + let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); + let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); + validate_binary_op(&self.op, &lhs_pt)?; + validate_types_compatible(&lhs_pt, &rhs_pt)?; let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { left, right, diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index dd4f047551..53f19f642c 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -1168,6 +1168,72 @@ pub fn validate_const_castable( Ok(()) } +/// A representative value for a given `PropType` — used to check type-level +/// compatibility via the value-based `Prop::try_cast` matrix. Returns `None` +/// for composite types (List, Map) where no canonical scalar default exists. +fn representative_prop(pt: &PropType) -> Option { + Some(match pt { + PropType::Str => Prop::Str("".into()), + PropType::U8 => Prop::U8(0), + PropType::U16 => Prop::U16(0), + PropType::U32 => Prop::U32(0), + PropType::U64 => Prop::U64(0), + PropType::I32 => Prop::I32(0), + PropType::I64 => Prop::I64(0), + PropType::F32 => Prop::F32(0.0), + PropType::F64 => Prop::F64(0.0), + PropType::Bool => Prop::Bool(false), + PropType::Empty + | PropType::List(_) + | PropType::Map(_) + | PropType::NDTime + | PropType::DTime + | PropType::Decimal { .. } => return None, + }) +} + +/// Reject a binary comparison where LHS and RHS types are known but incompatible. +/// +/// Complements `validate_const_castable` (which only checks const RHS) by also +/// catching mismatches when the RHS is another expression with a declared +/// `prop_type`. Uses the same `Prop::try_cast` matrix for coercion checks via +/// a representative value, so the numeric family (U/I/F) is considered +/// compatible while cross-domain (Bool vs U64, Str vs I64) is rejected. +/// +/// Both sides being `Empty` defers to runtime (no-op). +pub fn validate_types_compatible(lhs_pt: &PropType, rhs_pt: &PropType) -> Result<(), GraphError> { + if *lhs_pt == PropType::Empty || *rhs_pt == PropType::Empty || lhs_pt == rhs_pt { + return Ok(()); + } + let castable = representative_prop(rhs_pt) + .and_then(|v| v.try_cast(lhs_pt.clone())) + .is_some(); + if !castable { + return Err(GraphError::InvalidFilter(format!( + "type mismatch: lhs is {}, rhs is {}", + lhs_pt, rhs_pt + ))); + } + Ok(()) +} + +/// Reject aggregators called on a declared scalar expression. +/// +/// Lists and unresolved (`PropType::Empty`) types pass through — unresolved +/// is the case where a property name hasn't been looked up yet at expression- +/// build time, so we defer to filter-build / runtime to catch scalar/list +/// mismatches there. Anything declaring a scalar type up front (e.g. +/// `IsActiveNode` → `Bool`, `DegreeExpr` → `U64`) is rejected. +pub fn require_aggregable(pt: &PropType, op: &str) -> Result<(), GraphError> { + match pt { + PropType::List(_) | PropType::Empty => Ok(()), + _ => Err(GraphError::InvalidFilter(format!( + "{} is not valid on a scalar expression of type {}", + op, pt + ))), + } +} + /// Cast every value in an `is_in`/`is_not_in` set to the LHS type. /// /// If the LHS type is unknown (`PropType::Empty`), the values are returned 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 5b380e937e..88a63c3719 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 @@ -79,7 +79,7 @@ use crate::{ edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, filter_operator::Comparable, node_filter::{NodeFilter, NodeFilterFactory}, - CreateView, + require_aggregable, resolved_prop_type, CreateView, }, }, errors::GraphError, @@ -614,6 +614,8 @@ macro_rules! impl_agg_expr { graph: G, ) -> Result> + 'g>, GraphError> { let inner = self.0.create_node_op(graph)?; + let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); + require_aggregable(&pt, stringify!($expr))?; Ok(Arc::new($node_op_ty { inner })) } @@ -622,6 +624,8 @@ macro_rules! impl_agg_expr { graph: G, ) -> Result> + 'g>, GraphError> { let inner = self.0.create_edge_op(graph)?; + let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); + require_aggregable(&pt, stringify!($expr))?; Ok(Arc::new($edge_op_ty { inner })) } } 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 a56ef2b59c..2830f8c1fc 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 @@ -60,9 +60,9 @@ use crate::{ edge_filter::CompositeEdgeFilter, filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, resolved_prop_type, validate_binary_op, validate_const_castable, - validate_string_op, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateFilter, EntityMarker, ExplodedEdgeFilter, - TryAsCompositeFilter, + validate_string_op, validate_types_compatible, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, EntityMarker, + ExplodedEdgeFilter, TryAsCompositeFilter, }, node_filtered_graph::NodeFilteredGraph, }, @@ -177,11 +177,13 @@ where let left = self.left.create_node_op(graph.clone())?; let right = self.right.create_node_op(graph)?; let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); + let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; validate_const_castable( &lhs_pt, right.const_value().as_ref().and_then(|o| o.as_ref()), )?; + validate_types_compatible(&lhs_pt, &rhs_pt)?; Ok(Arc::new(BinaryCmpNodeOp { left, right, From d29a7219f9fb3b93737e416f479aa3165100c0c5 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:57:58 +0100 Subject: [PATCH 090/155] migrate structural edge predicates (IsActive/IsValid/IsDeleted/IsSelfLoop) to generic view_expr shape --- .../src/db/api/state/generic_node_state.rs | 3 +- raphtory/src/db/api/state/ops/mod.rs | 21 ++ raphtory/src/db/graph/views/filter/mod.rs | 3 +- .../db/graph/views/filter/model/and_filter.rs | 32 +-- .../graph/views/filter/model/degree_filter.rs | 22 +- .../views/filter/model/edge_expr/filters.rs | 76 +------ .../graph/views/filter/model/edge_expr/mod.rs | 6 +- .../graph/views/filter/model/edge_filter.rs | 203 +---------------- .../filter/model/exploded_edge_filter.rs | 205 +----------------- .../graph/views/filter/model/graph_filter.rs | 21 +- .../filter/model/is_active_edge_filter.rs | 32 +-- .../filter/model/is_active_node_filter.rs | 23 +- .../views/filter/model/is_deleted_filter.rs | 31 +-- .../views/filter/model/is_self_loop_filter.rs | 35 +-- .../views/filter/model/is_valid_filter.rs | 35 +-- .../graph/views/filter/model/latest_filter.rs | 50 +---- .../views/filter/model/layered_filter.rs | 58 +---- .../src/db/graph/views/filter/model/mod.rs | 128 +++-------- .../views/filter/model/node_expr/filters.rs | 80 +------ .../views/filter/model/node_filter/mod.rs | 199 +---------------- .../views/filter/model/node_state_filter.rs | 24 +- .../db/graph/views/filter/model/not_filter.rs | 29 +-- .../db/graph/views/filter/model/or_filter.rs | 32 +-- .../filter/model/property_filter/builders.rs | 9 +- .../views/filter/model/property_filter/mod.rs | 53 +---- .../views/filter/model/snapshot_filter.rs | 97 +-------- .../views/filter/model/windowed_filter.rs | 51 +---- raphtory/src/python/filter/node_expr.rs | 5 + 28 files changed, 151 insertions(+), 1412 deletions(-) diff --git a/raphtory/src/db/api/state/generic_node_state.rs b/raphtory/src/db/api/state/generic_node_state.rs index b2f2a5cd45..f41edf07db 100644 --- a/raphtory/src/db/api/state/generic_node_state.rs +++ b/raphtory/src/db/api/state/generic_node_state.rs @@ -8,8 +8,7 @@ use crate::{ Index, }, view::{ - internal::{GraphView, InnerFilterOps}, - BoxableGraphView, DynamicGraph, IntoDynBoxed, IntoDynamic, + internal::GraphView, BoxableGraphView, DynamicGraph, IntoDynBoxed, IntoDynamic, }, }, graph::{ diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 361dbf2abb..db89330910 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -17,6 +17,10 @@ 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::Prop; +use crate::db::graph::views::filter::model::node_expr::{CreateOp, EntityExpr}; +use crate::errors::GraphError; +use crate::prelude::NodeFilter; // this probably needs the 'graph lifetime to make bin_cmp work with ops that capture the graph pub trait NodeOp: Send + Sync { @@ -70,6 +74,23 @@ pub trait NodeOp: Send + Sync { } } +// impl EntityExpr for T { +// type Marker = NodeFilter; +// +// fn entity(&self) -> Self::Marker { +// NodeFilter +// } +// } +// +// impl CreateOp for T { +// fn create_node_op<'g, G: GraphView + 'g>( +// &self, +// _graph: G, +// ) -> Result> + 'g>, GraphError> { +// Ok(Arc::new(self.clone())) +// } +// } + pub trait IntoArrowNodeOp: NodeOp + Sized { fn into_arrow_node_op>(self) -> ArrowMap { ArrowMap { diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index f5c619099d..6877d8f918 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -22,6 +22,7 @@ pub mod node_filtered_graph; pub mod not_filtered_graph; pub mod or_filtered_graph; +#[derive(Copy, Clone, Debug)] pub struct Unfiltered; impl CreateFilter for Unfiltered { @@ -51,7 +52,7 @@ impl CreateFilter for Unfiltered { } } -pub trait CreateFilter: Sized { +pub trait CreateFilter: Clone + Sized + Send + Sync { type EntityFiltered<'graph, G>: GraphViewOps<'graph> where Self: 'graph, diff --git a/raphtory/src/db/graph/views/filter/model/and_filter.rs b/raphtory/src/db/graph/views/filter/model/and_filter.rs index 47bdb01dc8..3100867380 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -5,13 +5,7 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - and_filtered_graph::AndFilteredGraph, - model::{ - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, - }, - CreateFilter, + and_filtered_graph::AndFilteredGraph, model::ComposableFilter, CreateFilter, }, }, errors::GraphError, @@ -73,27 +67,3 @@ impl CreateFilter for AndFilter { } } -impl TryAsCompositeFilter for AndFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::And( - Box::new(self.left.try_as_composite_node_filter()?), - Box::new(self.right.try_as_composite_node_filter()?), - )) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::And( - Box::new(self.left.try_as_composite_edge_filter()?), - Box::new(self.right.try_as_composite_edge_filter()?), - )) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::And( - Box::new(self.left.try_as_composite_exploded_edge_filter()?), - Box::new(self.right.try_as_composite_exploded_edge_filter()?), - )) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/degree_filter.rs b/raphtory/src/db/graph/views/filter/model/degree_filter.rs index 64a83197b5..8797f48fcb 100644 --- a/raphtory/src/db/graph/views/filter/model/degree_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/degree_filter.rs @@ -5,14 +5,12 @@ use crate::{ view::GraphViewOps, }, graph::views::filter::{ - model, model::{ property_filter::{ builders::PropertyExprBuilderInput, Op, PropertyFilter, PropertyFilterInput, PropertyFilterValue, PropertyRef, }, - CombinedFilter, ComposableFilter, CompositeNodeFilter, FilterOperator, - InternalPropertyFilterBuilder, NodeFilter, TryAsCompositeFilter, + ComposableFilter, FilterOperator, InternalPropertyFilterBuilder, NodeFilter, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -126,22 +124,6 @@ impl CreateFilter for DegreeFilter { } } -impl TryAsCompositeFilter for DegreeFilter { - fn try_as_composite_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Degree(self.clone())) - } -} - fn property_ref(direction: &Direction) -> PropertyRef { match direction { Direction::IN => PropertyRef::Property("in_degree".to_string()), @@ -152,7 +134,7 @@ fn property_ref(direction: &Direction) -> PropertyRef { impl InternalPropertyFilterBuilder for DegreeFilterBuilder where - DegreeFilter: CombinedFilter, + DegreeFilter: CreateFilter, { type Filter = DegreeFilter; type ExprBuilder = DegreeFilterBuilder; diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index 26557bbd99..455c7a3bb0 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -18,11 +18,10 @@ use crate::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, model::{ - edge_filter::{CompositeEdgeFilter, EdgeFilter}, + edge_filter::EdgeFilter, node_expr::{filters::PropValueSetExpr, CreateOp}, resolved_prop_type, validate_binary_op, validate_string_op, - validate_types_compatible, CompositeExplodedEdgeFilter, CompositeNodeFilter, - CreateFilter, ExplodedEdgeFilter, TryAsCompositeFilter, + validate_types_compatible, CreateFilter, ExplodedEdgeFilter, }, }, }, @@ -146,26 +145,6 @@ where } } -impl TryAsCompositeFilter for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - 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) - } -} - // ───────────────────────────────────────────────────────────────────────────── // UnaryExpr // ───────────────────────────────────────────────────────────────────────────── @@ -247,25 +226,6 @@ where } } -impl TryAsCompositeFilter for UnaryExpr -where - E: CreateOp, -{ - 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) - } -} - // ───────────────────────────────────────────────────────────────────────────── // StringExpr — string expression filter for edges // ───────────────────────────────────────────────────────────────────────────── @@ -372,24 +332,6 @@ where } } -impl TryAsCompositeFilter for StringExpr -where - L: CreateOp, - R: CreateOp, -{ - 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) - } -} - // ───────────────────────────────────────────────────────────────────────────── // PropValueSetExpr — is_in / is_not_in for edge-side exprs // ───────────────────────────────────────────────────────────────────────────── @@ -473,17 +415,3 @@ impl CreateFilter for PropValueSetExpr { Err(GraphError::NotNodeFilter) } } - -impl TryAsCompositeFilter for PropValueSetExpr { - 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) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index f8eec08b8a..6306d7da9a 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -20,15 +20,13 @@ //! ``` use crate::{ - db::{api::view::internal::GraphView, graph::views::filter::model::node_expr::EntityExpr}, - errors::GraphError, + db::{ graph::views::filter::model::node_expr::EntityExpr}, }; use raphtory_api::core::entities::{ edges::edge_ref::EdgeRef, - properties::prop::{Prop, PropType}, + properties::prop::{ PropType}, }; use raphtory_storage::graph::graph::GraphStorage; -use std::sync::Arc; pub mod filters; pub mod ops; 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 9df4ef0377..cf07c32927 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -2,31 +2,23 @@ use crate::{ db::{ api::{ state::ops::{Id, Name, NotANodeFilter, Type}, - view::{internal::GraphView, BoxableGraphView}, + view::internal::GraphView, }, graph::views::filter::{ edge_node_filtered_graph::EdgeNodeFilteredGraph, model::{ edge_expr::{ops::EdgeEndpointNodeOp, EdgeOp}, - exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, + exploded_edge_filter::ExplodedEdgeFilter, latest_filter::Latest, layered_filter::Layered, node_expr::{CreateOp, EntityExpr}, - node_filter::{ - builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, - }, - property_filter::PropertyFilter, + node_filter::{builders::InternalNodeFilterBuilder, NodeFilter}, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - AllExpr, AndFilter, AnyExpr, AvgExpr, CombinedFilter, ComposableFilter, CreateView, - EdgeFilterFactory, EdgeViewFilterOps, EntityAggOps, EntityMarker, FirstExpr, - InternalViewWrapOps, LastExpr, LenExpr, MaxExpr, MetadataExpr, MinExpr, NotFilter, - OrFilter, PropertyExpr, PropertyFilterFactory, SumExpr, TemporalPropExpr, - TryAsCompositeFilter, Wrap, + AllExpr, AnyExpr, AvgExpr, ComposableFilter, CreateView, EdgeFilterFactory, + EntityAggOps, EntityMarker, FirstExpr, InternalViewWrapOps, LastExpr, LenExpr, + MaxExpr, MetadataExpr, MinExpr, PropertyExpr, PropertyFilterFactory, SumExpr, + TemporalPropExpr, Wrap, }, CreateFilter, }, @@ -75,26 +67,6 @@ impl InternalViewWrapOps for EdgeFilter { } } -impl EdgeViewFilterOps for EdgeFilter { - type Output = T; - - fn is_active(&self) -> Self::Output { - IsActiveEdge - } - - fn is_valid(&self) -> Self::Output { - IsValidEdge - } - - fn is_deleted(&self) -> Self::Output { - IsDeletedEdge - } - - fn is_self_loop(&self) -> Self::Output { - IsSelfLoopEdge - } -} - #[derive(Clone, Debug, Copy, PartialEq, Eq)] pub enum Endpoint { Src, @@ -252,7 +224,7 @@ impl Wrap for EdgeEndpointWrapper { } } -impl ComposableFilter for EdgeEndpointWrapper where T: TryAsCompositeFilter + Clone {} +impl ComposableFilter for EdgeEndpointWrapper where T: Clone {} impl InternalNodeFilterBuilder for EdgeEndpointWrapper { type FilterType = T::FilterType; @@ -290,165 +262,6 @@ impl CreateFilter for EdgeEndpointWrapper } } -impl TryAsCompositeFilter for EdgeEndpointWrapper { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotNodeFilter) - } - - fn try_as_composite_edge_filter(&self) -> Result { - let filter = self.inner.try_as_composite_node_filter()?; - let filter = match self.endpoint { - Endpoint::Src => CompositeEdgeFilter::Src(filter), - Endpoint::Dst => CompositeEdgeFilter::Dst(filter), - }; - Ok(filter) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - let filter = self.inner.try_as_composite_node_filter()?; - let filter = match self.endpoint { - Endpoint::Src => CompositeExplodedEdgeFilter::Src(filter), - Endpoint::Dst => CompositeExplodedEdgeFilter::Dst(filter), - }; - Ok(filter) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CompositeEdgeFilter { - Src(CompositeNodeFilter), - Dst(CompositeNodeFilter), - Property(PropertyFilter), - Windowed(Box>), - Latest(Box>), - SnapshotAt(Box>), - SnapshotLatest(Box>), - IsActiveEdge(IsActiveEdge), - IsValidEdge(IsValidEdge), - IsDeletedEdge(IsDeletedEdge), - IsSelfLoopEdge(IsSelfLoopEdge), - Layered(Box>), - And(Box, Box), - Or(Box, Box), - Not(Box), -} - -impl Display for CompositeEdgeFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CompositeEdgeFilter::Src(filter) => write!(f, "SRC({})", filter), - CompositeEdgeFilter::Dst(filter) => write!(f, "DST({})", filter), - CompositeEdgeFilter::Property(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::Windowed(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::Latest(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::SnapshotAt(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::SnapshotLatest(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::IsActiveEdge(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::IsValidEdge(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::IsDeletedEdge(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::IsSelfLoopEdge(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::Layered(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::And(left, right) => write!(f, "({} AND {})", left, right), - CompositeEdgeFilter::Or(left, right) => write!(f, "({} OR {})", left, right), - CompositeEdgeFilter::Not(filter) => write!(f, "(NOT {})", filter), - } - } -} - -impl CreateFilter for CompositeEdgeFilter { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; - - type NodeFilter<'graph, G> - = NotANodeFilter - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphViewOps<'graph>>( - self, - graph: G, - ) -> Result, GraphError> { - match self { - CompositeEdgeFilter::Src(filter) => { - let wrapped = EdgeEndpointWrapper::new(filter, Endpoint::Src); - let filtered_graph = wrapped.create_filter(graph)?; - Ok(Arc::new(filtered_graph)) - } - CompositeEdgeFilter::Dst(filter) => { - let wrapped = EdgeEndpointWrapper::new(filter, Endpoint::Dst); - let filtered_graph = wrapped.create_filter(graph)?; - Ok(Arc::new(filtered_graph)) - } - CompositeEdgeFilter::Property(i) => Ok(Arc::new(i.create_filter(graph)?)), - CompositeEdgeFilter::Windowed(i) => { - let dyn_graph: Arc = Arc::new(graph); - i.create_filter(dyn_graph) - } - CompositeEdgeFilter::Latest(i) => { - let dyn_graph: Arc = Arc::new(graph); - i.create_filter(dyn_graph) - } - CompositeEdgeFilter::SnapshotAt(i) => { - let dyn_graph: Arc = Arc::new(graph); - i.create_filter(dyn_graph) - } - CompositeEdgeFilter::SnapshotLatest(i) => { - let dyn_graph: Arc = Arc::new(graph); - i.create_filter(dyn_graph) - } - CompositeEdgeFilter::IsActiveEdge(i) => Ok(Arc::new(i.create_filter(graph)?)), - CompositeEdgeFilter::IsValidEdge(i) => Ok(Arc::new(i.create_filter(graph)?)), - CompositeEdgeFilter::IsDeletedEdge(i) => Ok(Arc::new(i.create_filter(graph)?)), - CompositeEdgeFilter::IsSelfLoopEdge(i) => Ok(Arc::new(i.create_filter(graph)?)), - CompositeEdgeFilter::Layered(i) => { - let dyn_graph: Arc = Arc::new(graph); - i.create_filter(dyn_graph) - } - CompositeEdgeFilter::And(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - AndFilter { left: l, right: r }.create_filter(graph)?, - )) - } - CompositeEdgeFilter::Or(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - OrFilter { left: l, right: r }.create_filter(graph)?, - )) - } - CompositeEdgeFilter::Not(f) => { - let base = *f; - Ok(Arc::new(NotFilter(base).create_filter(graph)?)) - } - } - } - - fn create_node_filter<'graph, G: GraphView + 'graph>( - self, - _graph: G, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } -} - -impl TryAsCompositeFilter for CompositeEdgeFilter { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(self.clone()) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - // ───────────────────────────────────────────────────────────────────────────── // EdgeFilterFactory impls // ───────────────────────────────────────────────────────────────────────────── 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 a73685f611..bf4e226bda 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 @@ -1,27 +1,13 @@ use crate::{ db::{ - api::{ - state::ops::NotANodeFilter, - view::{internal::GraphView, BoxableGraphView}, - }, + api::{state::ops::NotANodeFilter, view::internal::GraphView}, graph::views::filter::{ exploded_edge_node_filtered_graph::ExplodedEdgeNodeFilteredGraph, model::{ - edge_filter::{CompositeEdgeFilter, Endpoint}, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - latest_filter::Latest, - layered_filter::Layered, - node_filter::{ - builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, - }, - property_filter::PropertyFilter, - snapshot_filter::{SnapshotAt, SnapshotLatest}, + edge_filter::Endpoint, + node_filter::{builders::InternalNodeFilterBuilder, NodeFilter}, windowed_filter::Windowed, - AndFilter, CombinedFilter, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, - NotFilter, OrFilter, TryAsCompositeFilter, Wrap, + EntityMarker, InternalViewWrapOps, Wrap, }, CreateFilter, }, @@ -30,7 +16,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::core::storage::timeindex::EventTime; -use std::{fmt, fmt::Display, sync::Arc}; +use std::{fmt, fmt::Display}; #[derive(Clone, Debug, Copy, Default, PartialEq, Eq)] pub struct ExplodedEdgeFilter; @@ -69,26 +55,6 @@ impl InternalViewWrapOps for ExplodedEdgeFilter { } } -impl EdgeViewFilterOps for ExplodedEdgeFilter { - type Output = T; - - fn is_active(&self) -> Self::Output { - IsActiveEdge - } - - fn is_valid(&self) -> Self::Output { - IsValidEdge - } - - fn is_deleted(&self) -> Self::Output { - IsDeletedEdge - } - - fn is_self_loop(&self) -> Self::Output { - IsSelfLoopEdge - } -} - #[derive(Debug, Clone)] pub struct ExplodedEdgeEndpointWrapper { pub(crate) inner: T, @@ -170,164 +136,3 @@ impl CreateFilter for ExplodedEdgeEndpointWra Err(GraphError::NotNodeFilter) } } - -impl TryAsCompositeFilter for ExplodedEdgeEndpointWrapper -where - T: TryAsCompositeFilter + Clone, -{ - 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 { - let nf = self.inner.try_as_composite_node_filter()?; - Ok(match self.endpoint { - Endpoint::Src => CompositeExplodedEdgeFilter::Src(nf), - Endpoint::Dst => CompositeExplodedEdgeFilter::Dst(nf), - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CompositeExplodedEdgeFilter { - Src(CompositeNodeFilter), - Dst(CompositeNodeFilter), - Property(PropertyFilter), - Windowed(Box>), - Latest(Box>), - SnapshotAt(Box>), - SnapshotLatest(Box>), - Layered(Box>), - IsActiveEdge(IsActiveEdge), - IsValidEdge(IsValidEdge), - IsDeletedEdge(IsDeletedEdge), - IsSelfLoopEdge(IsSelfLoopEdge), - And( - Box, - Box, - ), - Or( - Box, - Box, - ), - Not(Box), -} - -impl Display for CompositeExplodedEdgeFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CompositeExplodedEdgeFilter::Src(filter) => write!(f, "SRC({})", filter), - CompositeExplodedEdgeFilter::Dst(filter) => write!(f, "DST({})", filter), - CompositeExplodedEdgeFilter::Property(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::Windowed(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::Latest(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::SnapshotAt(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::SnapshotLatest(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::IsActiveEdge(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::IsValidEdge(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::IsDeletedEdge(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::IsSelfLoopEdge(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::Layered(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::And(left, right) => write!(f, "({} AND {})", left, right), - CompositeExplodedEdgeFilter::Or(left, right) => write!(f, "({} OR {})", left, right), - CompositeExplodedEdgeFilter::Not(filter) => write!(f, "(NOT {})", filter), - } - } -} - -impl CreateFilter for CompositeExplodedEdgeFilter { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; - type NodeFilter<'graph, G> - = NotANodeFilter - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphViewOps<'graph>>( - self, - graph: G, - ) -> Result, GraphError> { - match self { - Self::Src(filter) => { - let wrapped = ExplodedEdgeEndpointWrapper::new(filter, Endpoint::Src); - let filtered_graph = wrapped.create_filter(graph)?; - Ok(Arc::new(filtered_graph)) - } - Self::Dst(filter) => { - let wrapped = ExplodedEdgeEndpointWrapper::new(filter, Endpoint::Dst); - let filtered_graph = wrapped.create_filter(graph)?; - Ok(Arc::new(filtered_graph)) - } - Self::Property(p) => Ok(Arc::new(p.create_filter(graph)?)), - Self::Windowed(pw) => { - let dyn_graph: Arc = Arc::new(graph); - pw.create_filter(dyn_graph) - } - Self::Latest(pw) => { - let dyn_graph: Arc = Arc::new(graph); - pw.create_filter(dyn_graph) - } - Self::SnapshotAt(pw) => { - let dyn_graph: Arc = Arc::new(graph); - pw.create_filter(dyn_graph) - } - Self::SnapshotLatest(pw) => { - let dyn_graph: Arc = Arc::new(graph); - pw.create_filter(dyn_graph) - } - Self::Layered(pw) => { - let dyn_graph: Arc = Arc::new(graph); - pw.create_filter(dyn_graph) - } - Self::IsActiveEdge(pw) => Ok(Arc::new(pw.create_filter(graph)?)), - Self::IsValidEdge(pw) => Ok(Arc::new(pw.create_filter(graph)?)), - Self::IsDeletedEdge(pw) => Ok(Arc::new(pw.create_filter(graph)?)), - Self::IsSelfLoopEdge(pw) => Ok(Arc::new(pw.create_filter(graph)?)), - Self::And(l, r) => { - let (l, r) = (*l, *r); // move out, no clone - Ok(Arc::new( - AndFilter { left: l, right: r }.create_filter(graph)?, - )) - } - Self::Or(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - OrFilter { left: l, right: r }.create_filter(graph)?, - )) - } - Self::Not(f) => { - let base = *f; - Ok(Arc::new(NotFilter(base).create_filter(graph)?)) - } - } - } - - fn create_node_filter<'graph, G: GraphView + 'graph>( - self, - _graph: G, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } -} - -impl TryAsCompositeFilter for CompositeExplodedEdgeFilter { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(self.clone()) - } -} 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 2fb908732f..afb7cb91ba 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -2,11 +2,7 @@ use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, - CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, - }, + model::{windowed_filter::Windowed, InternalViewWrapOps, Wrap}, CreateFilter, }, }, @@ -60,18 +56,3 @@ impl CreateFilter for GraphFilter { } } -impl TryAsCompositeFilter for GraphFilter { - 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) - } -} 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 a90046cc62..8a1f63c105 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 @@ -1,12 +1,10 @@ +use crate::db::graph::views::filter::model::CreateView; use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, TryAsCompositeFilter, - }, + model::ComposableFilter, CreateFilter, }, is_active_graph::IsActiveGraph, @@ -18,15 +16,17 @@ use crate::{ use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct IsActiveEdge; +pub struct IsActiveEdge { + pub(crate) view_expr: E, +} -impl fmt::Display for IsActiveEdge { +impl fmt::Display for IsActiveEdge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_ACTIVE_EDGE") } } -impl CreateFilter for IsActiveEdge { +impl CreateFilter for IsActiveEdge { type EntityFiltered<'graph, G> = IsActiveGraph where @@ -54,20 +54,4 @@ impl CreateFilter for IsActiveEdge { } } -impl ComposableFilter for IsActiveEdge {} - -impl TryAsCompositeFilter for IsActiveEdge { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsActiveEdge(IsActiveEdge)) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsActiveEdge(IsActiveEdge)) - } -} +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 59789ce52b..7ca6c09f45 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 @@ -3,11 +3,8 @@ use crate::{ api::state::ops::{GraphView, HistoryOp, Map, NodeOp}, graph::views::filter::{ model::{ - edge_expr::EdgeOp, - edge_filter::CompositeEdgeFilter, node_expr::{CreateOp, EntityExpr}, - ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, - TryAsCompositeFilter, + ComposableFilter, CreateView, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -88,21 +85,3 @@ impl CreateFilter for IsActiveNode { } impl ComposableFilter for IsActiveNode {} - -impl TryAsCompositeFilter for IsActiveNode { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::IsActiveNode(Box::new( - self.view_expr.try_as_composite_node_filter()?, - ))) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} 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 a9cc891333..87a7c160ca 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 @@ -3,10 +3,7 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, TryAsCompositeFilter, - }, + model::{ComposableFilter, CreateView}, CreateFilter, }, is_deleted_graph::IsDeletedGraph, @@ -18,15 +15,17 @@ use crate::{ use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct IsDeletedEdge; +pub struct IsDeletedEdge { + pub(crate) view_expr: E, +} -impl fmt::Display for IsDeletedEdge { +impl fmt::Display for IsDeletedEdge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_DELETED_EDGE") } } -impl CreateFilter for IsDeletedEdge { +impl CreateFilter for IsDeletedEdge { type EntityFiltered<'graph, G> = IsDeletedGraph where @@ -54,20 +53,4 @@ impl CreateFilter for IsDeletedEdge { } } -impl ComposableFilter for IsDeletedEdge {} - -impl TryAsCompositeFilter for IsDeletedEdge { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge)) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsDeletedEdge(IsDeletedEdge)) - } -} +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 c8a5503976..69cda7cb19 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 @@ -2,13 +2,7 @@ use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ - filter::{ - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, TryAsCompositeFilter, - }, - CreateFilter, - }, + filter::{model::ComposableFilter, CreateFilter}, is_self_loop_graph::IsSelfLoopGraph, }, }, @@ -16,17 +10,20 @@ use crate::{ prelude::GraphViewOps, }; use std::fmt; +use crate::db::graph::views::filter::model::CreateView; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct IsSelfLoopEdge; +pub struct IsSelfLoopEdge { + pub(crate) view_expr: E, +} -impl fmt::Display for IsSelfLoopEdge { +impl fmt::Display for IsSelfLoopEdge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_SELF_LOOP_EDGE") } } -impl CreateFilter for IsSelfLoopEdge { +impl CreateFilter for IsSelfLoopEdge { type EntityFiltered<'graph, G> = IsSelfLoopGraph where @@ -54,20 +51,4 @@ impl CreateFilter for IsSelfLoopEdge { } } -impl ComposableFilter for IsSelfLoopEdge {} - -impl TryAsCompositeFilter for IsSelfLoopEdge { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) - } -} +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 70ad8f793d..bca1458efa 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 @@ -2,13 +2,7 @@ use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ - filter::{ - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, TryAsCompositeFilter, - }, - CreateFilter, - }, + filter::{model::ComposableFilter, CreateFilter}, valid_graph::ValidGraph, }, }, @@ -16,17 +10,20 @@ use crate::{ prelude::GraphViewOps, }; use std::fmt; +use crate::db::graph::views::filter::model::CreateView; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct IsValidEdge; +pub struct IsValidEdge { + pub(crate) view_expr: E, +} -impl fmt::Display for IsValidEdge { +impl fmt::Display for IsValidEdge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_VALID_EDGE") } } -impl CreateFilter for IsValidEdge { +impl CreateFilter for IsValidEdge { type EntityFiltered<'graph, G> = ValidGraph where @@ -54,20 +51,4 @@ impl CreateFilter for IsValidEdge { } } -impl ComposableFilter for IsValidEdge {} - -impl TryAsCompositeFilter for IsValidEdge { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsValidEdge(IsValidEdge)) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge)) - } -} +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 f8098d45e2..eed3f5312a 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -4,11 +4,8 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, windowed_filter::Windowed, CombinedFilter, - ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, - EdgeViewFilterOps, InternalViewWrapOps, TryAsCompositeFilter, Wrap, + windowed_filter::Windowed, ComposableFilter, CreateView, InternalViewWrapOps, + Wrap, }, CreateFilter, }, @@ -20,6 +17,7 @@ use crate::{ }; use raphtory_api::core::storage::timeindex::EventTime; use std::{fmt, fmt::Display}; +use crate::db::graph::views::filter::model::EdgeViewFilterOps; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Latest { @@ -47,28 +45,6 @@ impl InternalViewWrapOps for Latest { } } -impl TryAsCompositeFilter for Latest { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Latest(Box::new(Latest::new( - self.inner.try_as_composite_node_filter()?, - )))) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::Latest(Box::new(Latest::new( - self.inner.try_as_composite_edge_filter()?, - )))) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::Latest(Box::new(Latest::new( - self.inner.try_as_composite_exploded_edge_filter()?, - )))) - } -} - impl CreateFilter for Latest { type EntityFiltered<'graph, G> = T::EntityFiltered<'graph, WindowedGraph> @@ -122,22 +98,4 @@ impl Wrap for Latest { } } -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()) - } -} +impl EdgeViewFilterOps for 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 2d205591a3..b834dd5e13 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -1,18 +1,9 @@ use crate::{ db::{ api::view::internal::{GraphView, InternalFilter}, - graph::views::{ - filter::{ - model::{ - edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, CombinedFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, EdgeViewFilterOps, - InternalViewWrapOps, TryAsCompositeFilter, Wrap, - }, - CreateFilter, - }, - layer_graph::LayeredGraph, + graph::views::filter::{ + model::{ComposableFilter, InternalViewWrapOps, Wrap}, + CreateFilter, }, }, errors::GraphError, @@ -20,6 +11,7 @@ use crate::{ }; use raphtory_api::core::{entities::Layer, storage::timeindex::EventTime}; use std::{fmt, fmt::Display}; +use crate::db::graph::views::filter::model::EdgeViewFilterOps; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Layered { @@ -60,28 +52,6 @@ impl InternalViewWrapOps for Layered { } } -impl TryAsCompositeFilter for Layered { - fn try_as_composite_node_filter(&self) -> Result { - let filter = self.inner.try_as_composite_node_filter()?; - let filter = CompositeNodeFilter::Layered(Box::new(self.wrap(filter))); - Ok(filter) - } - - fn try_as_composite_edge_filter(&self) -> Result { - let filter = self.inner.try_as_composite_edge_filter()?; - let filter = CompositeEdgeFilter::Layered(Box::new(self.wrap(filter))); - Ok(filter) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - let filter = self.inner.try_as_composite_exploded_edge_filter()?; - let filter = CompositeExplodedEdgeFilter::Layered(Box::new(self.wrap(filter))); - Ok(filter) - } -} - impl CreateFilter for Layered { type EntityFiltered<'graph, G> = T::EntityFiltered<'graph, >::LayeredViewType> @@ -128,22 +98,4 @@ impl Wrap for Layered { } } -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()) - } -} +impl EdgeViewFilterOps for Layered {} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 53f19f642c..480dbb7e7b 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -6,10 +6,7 @@ pub use crate::{ filter::{ model::{ edge_filter::{EdgeEndpointWrapper, EdgeFilter}, - exploded_edge_filter::{ - CompositeExplodedEdgeFilter, ExplodedEdgeEndpointWrapper, - ExplodedEdgeFilter, - }, + exploded_edge_filter::{ExplodedEdgeEndpointWrapper, ExplodedEdgeFilter}, filter_operator::{ BinaryOp, Comparable, FilterOperator, SetOp, StringComparable, StringOp, UnaryOp, @@ -42,7 +39,6 @@ use crate::{ }, graph::views::{ filter::model::{ - edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, @@ -66,7 +62,6 @@ use crate::{ }, prelude::LayerOps, }; -pub use node_filter::CompositeNodeFilter; use raphtory_api::core::{ entities::{properties::prop::Prop, Layer}, storage::timeindex::{AsTime, EventTime}, @@ -129,22 +124,6 @@ impl CreateFilter for NoFilter { } } -impl TryAsCompositeFilter for NoFilter { - 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) - } -} - pub trait Wrap { type Wrapped; @@ -174,7 +153,7 @@ pub trait ComposableFilter: Sized { } } -pub trait DynCreateFilter: TryAsCompositeFilter + Send + Sync + 'static { +pub trait DynCreateFilter: Send + Sync + 'static { fn create_dyn_filter<'graph>( &self, graph: Arc, @@ -186,10 +165,7 @@ pub trait DynCreateFilter: TryAsCompositeFilter + Send + Sync + 'static { ) -> Result + 'graph>, GraphError>; } -impl DynCreateFilter for T -where - T: CombinedFilter, -{ +impl DynCreateFilter for T { fn create_dyn_filter<'graph>( &self, graph: Arc, @@ -384,7 +360,7 @@ impl DynPropertyFilterFactory for T { impl InternalPropertyFilterBuilder for PropertyExpr where E: Into + Send + Sync + Clone + 'static, - crate::prelude::PropertyFilter: CombinedFilter, + crate::prelude::PropertyFilter: CreateFilter, PropertyExprBuilder: InternalPropertyFilterBuilder, { type Filter = crate::prelude::PropertyFilter; @@ -415,7 +391,7 @@ where impl InternalPropertyFilterBuilder for MetadataExpr where E: Into + Send + Sync + Clone + 'static, - crate::prelude::PropertyFilter: CombinedFilter, + crate::prelude::PropertyFilter: CreateFilter, PropertyExprBuilder: InternalPropertyFilterBuilder, { type Filter = crate::prelude::PropertyFilter; @@ -473,36 +449,6 @@ use crate::db::graph::views::filter::model::{ use edge_expr::EdgeOp; use raphtory_api::core::entities::properties::prop::PropType; -pub trait TryAsCompositeFilter: Send + Sync { - fn try_as_composite_node_filter(&self) -> Result; - - fn try_as_composite_edge_filter(&self) -> Result; - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result; -} - -impl TryAsCompositeFilter for Arc { - fn try_as_composite_node_filter(&self) -> Result { - self.deref().try_as_composite_node_filter() - } - - fn try_as_composite_edge_filter(&self) -> Result { - self.deref().try_as_composite_edge_filter() - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - self.deref().try_as_composite_exploded_edge_filter() - } -} - -pub trait CombinedFilter: CreateFilter + TryAsCompositeFilter + Clone + 'static {} - -impl CombinedFilter for T {} - // This is implemented to avoid infinite recursive windowing. pub trait InternalViewWrapOps: Send + Sync + Clone + 'static { type Window: InternalViewWrapOps; @@ -796,36 +742,49 @@ impl InternalViewWrapOps for DynView { } pub trait NodeViewFilterOps: ViewWrapOps { - type Output: CombinedFilter; + type Output: CreateFilter; fn is_active(&self) -> Self::Output>; } -pub trait DynNodeViewFilterOps: DynInternalViewWrapPropOps + TryAsCompositeFilter { +pub trait DynNodeViewFilterOps: DynInternalViewWrapPropOps { 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()) } } pub trait EdgeViewFilterOps: ViewWrapOps { - type Output: CombinedFilter; - - fn is_active(&self) -> Self::Output; + fn is_active(&self) -> IsActiveEdge { + IsActiveEdge{ + view_expr: self.clone() + } + } - fn is_valid(&self) -> Self::Output; + fn is_valid(&self) -> IsValidEdge { + IsValidEdge { + view_expr: self.clone() + } + } - fn is_deleted(&self) -> Self::Output; + fn is_deleted(&self) -> IsDeletedEdge { + IsDeletedEdge { + view_expr: self.clone() + } + } - fn is_self_loop(&self) -> Self::Output; + fn is_self_loop(&self) -> IsSelfLoopEdge { + IsSelfLoopEdge { + view_expr: self.clone() + } + } } -pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps + TryAsCompositeFilter { +impl EdgeViewFilterOps for EdgeFilter {} +pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps { fn dyn_is_active(&self) -> Arc; fn dyn_is_valid(&self) -> Arc; @@ -835,9 +794,7 @@ 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()) } @@ -881,7 +838,7 @@ impl InternalViewWrapOps for DynNodeViewProps { } impl NodeViewFilterOps for DynNodeViewProps { - type Output = Arc; + type Output = Arc; fn is_active(&self) -> Self::Output> { self.deref().dyn_is_active() @@ -921,25 +878,8 @@ impl InternalViewWrapOps for DynEdgeViewProps { } } -impl EdgeViewFilterOps for DynEdgeViewProps { - type Output = Arc; - - fn is_active(&self) -> Self::Output { - self.deref().dyn_is_active() - } - - fn is_valid(&self) -> Self::Output { - self.deref().dyn_is_valid() - } - - fn is_deleted(&self) -> Self::Output { - self.deref().dyn_is_deleted() - } - - fn is_self_loop(&self) -> Self::Output { - self.deref().dyn_is_self_loop() - } -} +// TODO: Do we need this? +impl EdgeViewFilterOps for DynEdgeViewProps {} // ───────────────────────────────────────────────────────────────────────────── // EntityExprFilterOps — comparison and set operators on any EntityExpr 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 2830f8c1fc..ffa3227be2 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 @@ -57,12 +57,10 @@ use crate::{ model::{ coerce_set_values, edge_expr::EdgeOp, - edge_filter::CompositeEdgeFilter, filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, resolved_prop_type, validate_binary_op, validate_const_castable, - validate_string_op, validate_types_compatible, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, EntityMarker, - ExplodedEdgeFilter, TryAsCompositeFilter, + validate_string_op, validate_types_compatible, ComposableFilter, CreateFilter, + EntityMarker, ExplodedEdgeFilter, }, node_filtered_graph::NodeFilteredGraph, }, @@ -257,26 +255,6 @@ where } } -impl TryAsCompositeFilter for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - 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) - } -} - // ───────────────────────────────────────────────────────────────────────────── // UnaryExpr — is_some / is_none on nullable expressions // ───────────────────────────────────────────────────────────────────────────── @@ -425,25 +403,6 @@ where } } -impl TryAsCompositeFilter for UnaryExpr -where - E: CreateOp, -{ - 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) - } -} - // ───────────────────────────────────────────────────────────────────────────── // StringExpr — string expression filter // ───────────────────────────────────────────────────────────────────────────── @@ -608,26 +567,6 @@ impl CreateOp for StringExpr { } } -impl TryAsCompositeFilter for StringExpr -where - L: CreateOp, - R: CreateOp, -{ - 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) - } -} - // ───────────────────────────────────────────────────────────────────────────── // PropValueSetExpr — is_in / is_not_in for aggregated Option values // ───────────────────────────────────────────────────────────────────────────── @@ -768,18 +707,3 @@ impl CreateOp for PropValueSetExpr { } } -impl TryAsCompositeFilter for PropValueSetExpr { - 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) - } -} 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 2c67d7e786..b333e9ca84 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 @@ -4,10 +4,7 @@ use crate::{ api::{ state::{ ops::{ - filter::{ - AndOp, MaskOp, NodeIdFilterOp, NodeNameFilterOp, NodeTypeFilterOp, NotOp, - OrOp, - }, + filter::{MaskOp, NodeIdFilterOp, NodeNameFilterOp, NodeTypeFilterOp}, Id, Name, NodeOp, Type, TypeId, }, NodeStateValue, TypedNodeState, @@ -16,8 +13,6 @@ use crate::{ }, graph::views::filter::{ model::{ - degree_filter::{DegreeFilter, DegreeFilterFactory}, - edge_filter::CompositeEdgeFilter, filter::Filter, is_active_node_filter::IsActiveNode, latest_filter::Latest, @@ -27,19 +22,18 @@ use crate::{ node_state_filter::NodeStateBoolColOp, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, CreateView, - EntityMarker, InternalViewWrapOps, NodeViewFilterOps, PropertyFilterFactory, - TryAsCompositeFilter, Wrap, + ComposableFilter, CreateView, EntityMarker, InternalViewWrapOps, + NodeViewFilterOps, PropertyFilterFactory, Wrap, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, }, }, errors::GraphError, - prelude::{GraphViewOps, PropertyFilter}, + prelude::GraphViewOps, }; use raphtory_api::core::storage::timeindex::EventTime; -use std::{fmt, fmt::Display, sync::Arc}; +use std::{fmt, fmt::Display}; pub mod builders; pub mod ops; @@ -124,22 +118,6 @@ pub trait NodeFilterFactory: PropertyFilterFactory + Clone { 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 Wrap for NodeFilter { type Wrapped = T; @@ -201,22 +179,6 @@ impl CreateFilter for NodeIdFilter { } } -impl TryAsCompositeFilter for NodeIdFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Id(self.0.clone())) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - #[derive(Debug, Clone)] pub struct NodeNameFilter(pub Filter); @@ -254,22 +216,6 @@ impl CreateFilter for NodeNameFilter { } } -impl TryAsCompositeFilter for NodeNameFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Name(self.0.clone())) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - #[derive(Debug, Clone)] pub struct NodeTypeFilter(pub Filter); @@ -323,138 +269,3 @@ impl CreateFilter for NodeTypeFilter { Ok(TypeId.mask(node_types_filter.into())) } } - -impl TryAsCompositeFilter for NodeTypeFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Type(self.0.clone())) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CompositeNodeFilter { - Id(Filter), - Name(Filter), - Type(Filter), - Property(PropertyFilter), - Degree(DegreeFilter), - Windowed(Box>), - Latest(Box>), - SnapshotAt(Box>), - SnapshotLatest(Box>), - Layered(Box>), - IsActiveNode(Box), - And(Box, Box), - Or(Box, Box), - Not(Box), -} - -impl Display for CompositeNodeFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CompositeNodeFilter::Property(filter) => write!(f, "{}", filter), - CompositeNodeFilter::Windowed(filter) => write!(f, "{}", filter), - CompositeNodeFilter::Degree(filter) => write!(f, "{}", filter), - CompositeNodeFilter::Layered(filter) => write!(f, "{}", filter), - CompositeNodeFilter::Latest(filter) => write!(f, "{}", filter), - CompositeNodeFilter::SnapshotAt(filter) => write!(f, "{}", filter), - CompositeNodeFilter::SnapshotLatest(filter) => write!(f, "{}", filter), - CompositeNodeFilter::IsActiveNode(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), - } - } -} - -impl CreateFilter for CompositeNodeFilter { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - NodeFilteredGraph>; - - type NodeFilter<'graph, G: GraphView + 'graph> = Arc + '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> { - match self { - CompositeNodeFilter::Degree(i) => Ok(Arc::new(i.create_node_filter(graph)?)), - 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); - i.create_node_filter(dyn_graph) - } - CompositeNodeFilter::Layered(i) => { - let dyn_graph: Arc = Arc::new(graph); - i.create_node_filter(dyn_graph) - } - CompositeNodeFilter::Latest(i) => { - let dyn_graph: Arc = Arc::new(graph); - i.create_node_filter(dyn_graph) - } - CompositeNodeFilter::SnapshotAt(i) => { - let dyn_graph: Arc = Arc::new(graph); - i.create_node_filter(dyn_graph) - } - CompositeNodeFilter::SnapshotLatest(i) => { - let dyn_graph: Arc = Arc::new(graph); - i.create_node_filter(dyn_graph) - } - CompositeNodeFilter::IsActiveNode(i) => Ok(Arc::new(i.create_node_filter(graph)?)), - CompositeNodeFilter::And(l, r) => Ok(Arc::new(AndOp { - left: l.clone().create_node_filter(graph.clone())?, - right: r.clone().create_node_filter(graph.clone())?, - })), - CompositeNodeFilter::Or(l, r) => Ok(Arc::new(OrOp { - left: l.clone().create_node_filter(graph.clone())?, - right: r.clone().create_node_filter(graph.clone())?, - })), - CompositeNodeFilter::Not(filter) => { - Ok(Arc::new(NotOp(filter.clone().create_node_filter(graph)?))) - } - } - } -} - -impl TryAsCompositeFilter for CompositeNodeFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(self.clone()) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/node_state_filter.rs b/raphtory/src/db/graph/views/filter/model/node_state_filter.rs index f796de3d9f..d63a3d462a 100644 --- a/raphtory/src/db/graph/views/filter/model/node_state_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/node_state_filter.rs @@ -1,11 +1,5 @@ use crate::{ - db::{ - api::state::{ops::NodeOp, Index, NodeStateValue, TypedNodeState}, - graph::views::filter::model::{ - edge_filter::CompositeEdgeFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, - TryAsCompositeFilter, - }, - }, + db::api::state::{ops::NodeOp, Index, NodeStateValue, TypedNodeState}, errors::GraphError, }; use arrow_array::{cast::AsArray, Array, BooleanArray}; @@ -72,19 +66,3 @@ impl NodeOp for NodeStateBoolColOp { self.bool_at_row(row) } } - -impl TryAsCompositeFilter for NodeStateBoolColOp { - 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) - } -} 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 061622a960..b0859f1d70 100644 --- a/raphtory/src/db/graph/views/filter/model/not_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/not_filter.rs @@ -5,13 +5,7 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, - }, - not_filtered_graph::NotFilteredGraph, - CreateFilter, + model::ComposableFilter, not_filtered_graph::NotFilteredGraph, CreateFilter, }, }, errors::GraphError, @@ -60,24 +54,3 @@ impl CreateFilter for NotFilter { } } -impl TryAsCompositeFilter for NotFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Not(Box::new( - self.0.try_as_composite_node_filter()?, - ))) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::Not(Box::new( - self.0.try_as_composite_edge_filter()?, - ))) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::Not(Box::new( - self.0.try_as_composite_exploded_edge_filter()?, - ))) - } -} 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 674efb7175..807de88259 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -5,13 +5,7 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, ComposableFilter, TryAsCompositeFilter, - }, - or_filtered_graph::OrFilteredGraph, - CreateFilter, + model::ComposableFilter, or_filtered_graph::OrFilteredGraph, CreateFilter, }, }, errors::GraphError, @@ -63,27 +57,3 @@ impl CreateFilter for OrFilter { } } -impl TryAsCompositeFilter for OrFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Or( - Box::new(self.left.try_as_composite_node_filter()?), - Box::new(self.right.try_as_composite_node_filter()?), - )) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::Or( - Box::new(self.left.try_as_composite_edge_filter()?), - Box::new(self.right.try_as_composite_edge_filter()?), - )) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::Or( - Box::new(self.left.try_as_composite_exploded_edge_filter()?), - Box::new(self.right.try_as_composite_exploded_edge_filter()?), - )) - } -} 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 b9d639d8ee..63cfe7de47 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,6 +1,7 @@ +use crate::db::graph::views::filter::CreateFilter; use crate::db::graph::views::filter::model::{ property_filter::{Op, PropertyFilter, PropertyFilterInput, PropertyRef}, - CombinedFilter, EntityMarker, Wrap, + EntityMarker, Wrap, }; pub trait InternalPropertyFilterBuilder { @@ -35,7 +36,7 @@ impl Wrap for PropertyFilterBuilder { impl InternalPropertyFilterBuilder for PropertyFilterBuilder where M: Into + Send + Sync + Clone + 'static, - PropertyFilter: CombinedFilter, + PropertyFilter: CreateFilter, PropertyExprBuilder: InternalPropertyFilterBuilder, { type Filter = PropertyFilter; @@ -83,7 +84,7 @@ impl Wrap for MetadataFilterBuilder { impl InternalPropertyFilterBuilder for MetadataFilterBuilder where M: Into + Send + Sync + Clone + 'static, - PropertyFilter: CombinedFilter, + PropertyFilter: CreateFilter, PropertyExprBuilder: InternalPropertyFilterBuilder, { type Filter = PropertyFilter; @@ -192,7 +193,7 @@ impl Wrap for PropertyExprBuilder { impl InternalPropertyFilterBuilder for PropertyExprBuilder where M: Into + Send + Sync + Clone + 'static, - PropertyFilter: CombinedFilter, + PropertyFilter: CreateFilter, { type Filter = PropertyFilter; type ExprBuilder = PropertyExprBuilder; 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 c89c690afa..300a831d38 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,9 +12,9 @@ use crate::{ edge_property_filtered_graph::EdgePropertyFilteredGraph, exploded_edge_property_filter::ExplodedEdgePropertyFilteredGraph, model::{ - edge_filter::CompositeEdgeFilter, filter_value::FilterValue, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, ExplodedEdgeFilter, - FilterOperator, TryAsCompositeFilter, + filter_value::FilterValue, ComposableFilter, + ExplodedEdgeFilter, + FilterOperator, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -445,50 +445,3 @@ impl CreateFilter for PropertyFilter { impl ComposableFilter for PropertyFilter {} -impl TryAsCompositeFilter for PropertyFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Property(self.clone())) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - -impl TryAsCompositeFilter for PropertyFilter { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::Property(self.clone())) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - -impl TryAsCompositeFilter for PropertyFilter { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::Property(self.clone())) - } -} 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 25f47af814..46ed98e1bc 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -4,11 +4,8 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, windowed_filter::Windowed, CombinedFilter, - ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, - EdgeViewFilterOps, InternalViewWrapOps, TryAsCompositeFilter, Wrap, + windowed_filter::Windowed, ComposableFilter, CreateView, + InternalViewWrapOps, Wrap, }, CreateFilter, }, @@ -20,6 +17,7 @@ use crate::{ }; use raphtory_api::core::{storage::timeindex::EventTime, utils::time::IntoTime}; use std::{fmt, fmt::Display}; +use crate::db::graph::views::filter::model::EdgeViewFilterOps; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SnapshotAt { @@ -51,33 +49,6 @@ impl InternalViewWrapOps for SnapshotAt { } } -impl TryAsCompositeFilter for SnapshotAt { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::SnapshotAt(Box::new(SnapshotAt { - time: self.time, - inner: self.inner.try_as_composite_node_filter()?, - }))) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::SnapshotAt(Box::new(SnapshotAt::new( - self.time, - self.inner.try_as_composite_edge_filter()?, - )))) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::SnapshotAt(Box::new( - SnapshotAt::new( - self.time, - self.inner.try_as_composite_exploded_edge_filter()?, - ), - ))) - } -} - impl CreateFilter for SnapshotAt { type EntityFiltered<'graph, G> = T::EntityFiltered<'graph, WindowedGraph> @@ -134,25 +105,7 @@ impl Wrap for SnapshotAt { } } -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()) - } -} +impl EdgeViewFilterOps for SnapshotAt {} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SnapshotLatest { @@ -180,28 +133,6 @@ impl InternalViewWrapOps for SnapshotLatest { } } -impl TryAsCompositeFilter for SnapshotLatest { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::SnapshotLatest(Box::new( - SnapshotLatest::new(self.inner.try_as_composite_node_filter()?), - ))) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::SnapshotLatest(Box::new( - SnapshotLatest::new(self.inner.try_as_composite_edge_filter()?), - ))) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::SnapshotLatest(Box::new( - SnapshotLatest::new(self.inner.try_as_composite_exploded_edge_filter()?), - ))) - } -} - impl CreateFilter for SnapshotLatest { type EntityFiltered<'graph, G> = T::EntityFiltered<'graph, WindowedGraph> @@ -255,22 +186,4 @@ impl Wrap for SnapshotLatest { } } -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()) - } -} +impl EdgeViewFilterOps for SnapshotLatest {} 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 cfda2c3336..4203584d3d 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -3,13 +3,7 @@ use crate::{ api::view::internal::GraphView, graph::views::{ filter::{ - model::{ - edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, CombinedFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, - EdgeViewFilterOps, InternalViewWrapOps, TryAsCompositeFilter, Wrap, - }, + model::{ComposableFilter, CreateView, InternalViewWrapOps, Wrap}, CreateFilter, }, window_graph::WindowedGraph, @@ -23,6 +17,7 @@ use raphtory_api::core::{ utils::time::IntoTime, }; use std::{fmt, fmt::Display}; +use crate::db::graph::views::filter::model::EdgeViewFilterOps; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Windowed { @@ -73,28 +68,6 @@ impl InternalViewWrapOps for Windowed { } } -impl TryAsCompositeFilter for Windowed { - fn try_as_composite_node_filter(&self) -> Result { - let filter = self.inner.try_as_composite_node_filter()?; - let filter = CompositeNodeFilter::Windowed(Box::new(self.wrap(filter))); - Ok(filter) - } - - fn try_as_composite_edge_filter(&self) -> Result { - let filter = self.inner.try_as_composite_edge_filter()?; - let filter = CompositeEdgeFilter::Windowed(Box::new(self.wrap(filter))); - Ok(filter) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - let filter = self.inner.try_as_composite_exploded_edge_filter()?; - let filter = CompositeExplodedEdgeFilter::Windowed(Box::new(self.wrap(filter))); - Ok(filter) - } -} - impl CreateFilter for Windowed { type EntityFiltered<'graph, G> = T::EntityFiltered<'graph, WindowedGraph> @@ -151,22 +124,4 @@ impl CreateView for Windowed { } } -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()) - } -} +impl EdgeViewFilterOps for Windowed {} diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 366a8fb887..3315e43a94 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -469,3 +469,8 @@ impl PyNodeFilter { Ok(PyExpr(Arc::new(op))) } } + +// TODO: +// We need a wrapper for Arc +// Py Expr needs to extend this wrapper (PyFilter) +// by_state_column can return pyfilter From bcbc358029767c99f63e518a0f6850c73f6ae4fc Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:09:21 +0100 Subject: [PATCH 091/155] ref --- raphtory/src/db/api/state/ops/mod.rs | 26 ++++++++++++------- .../db/graph/views/filter/model/and_filter.rs | 1 - .../graph/views/filter/model/edge_expr/mod.rs | 9 ++----- .../graph/views/filter/model/graph_filter.rs | 1 - .../filter/model/is_active_edge_filter.rs | 3 +-- .../views/filter/model/is_self_loop_filter.rs | 6 +++-- .../views/filter/model/is_valid_filter.rs | 6 +++-- .../graph/views/filter/model/latest_filter.rs | 5 ++-- .../views/filter/model/layered_filter.rs | 3 +-- .../src/db/graph/views/filter/model/mod.rs | 14 +++++----- .../views/filter/model/node_expr/filters.rs | 1 - .../views/filter/model/node_filter/mod.rs | 4 +-- .../db/graph/views/filter/model/not_filter.rs | 1 - .../db/graph/views/filter/model/or_filter.rs | 1 - .../filter/model/property_filter/builders.rs | 10 ++++--- .../views/filter/model/property_filter/mod.rs | 5 +--- .../views/filter/model/snapshot_filter.rs | 5 ++-- .../views/filter/model/windowed_filter.rs | 5 ++-- 18 files changed, 52 insertions(+), 54 deletions(-) diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index db89330910..2070dcb9fc 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -3,24 +3,30 @@ pub mod history; pub mod node; pub mod properties; -use crate::db::{ - api::{ - state::ops::filter::{AndOp, NotOp, OrOp}, - view::internal::NodeList, +use crate::{ + db::{ + api::{ + state::ops::filter::{AndOp, NotOp, OrOp}, + view::internal::NodeList, + }, + graph::views::filter::model::{ + node_expr::{BinaryCmpNodeOp, CreateOp, EntityExpr}, + BinaryOp, Comparable, + }, }, - graph::views::filter::model::{node_expr::BinaryCmpNodeOp, BinaryOp, Comparable}, + errors::GraphError, + prelude::NodeFilter, }; pub use history::*; pub use node::*; pub use properties::*; -use raphtory_api::core::entities::{properties::prop::PropType, VID}; +use raphtory_api::core::entities::{ + properties::prop::{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::Prop; -use crate::db::graph::views::filter::model::node_expr::{CreateOp, EntityExpr}; -use crate::errors::GraphError; -use crate::prelude::NodeFilter; // this probably needs the 'graph lifetime to make bin_cmp work with ops that capture the graph pub trait NodeOp: Send + Sync { 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 3100867380..69776acfb6 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -66,4 +66,3 @@ impl CreateFilter for AndFilter { Ok(left.and(right)) } } - diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index 6306d7da9a..fc615e202c 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -19,13 +19,8 @@ //! └──────────────────────────────────────────────────────────┘ //! ``` -use crate::{ - db::{ graph::views::filter::model::node_expr::EntityExpr}, -}; -use raphtory_api::core::entities::{ - edges::edge_ref::EdgeRef, - properties::prop::{ PropType}, -}; +use crate::db::graph::views::filter::model::node_expr::EntityExpr; +use raphtory_api::core::entities::{edges::edge_ref::EdgeRef, properties::prop::PropType}; use raphtory_storage::graph::graph::GraphStorage; pub mod filters; 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 afb7cb91ba..67c998fc5c 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -55,4 +55,3 @@ impl CreateFilter for GraphFilter { Ok(NodeExistsOp::new(graph)) } } - 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 8a1f63c105..55781e89a3 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 @@ -1,10 +1,9 @@ -use crate::db::graph::views::filter::model::CreateView; use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - model::ComposableFilter, + model::{ComposableFilter, CreateView}, CreateFilter, }, is_active_graph::IsActiveGraph, 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 69cda7cb19..40fc05d767 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 @@ -2,7 +2,10 @@ use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ - filter::{model::ComposableFilter, CreateFilter}, + filter::{ + model::{ComposableFilter, CreateView}, + CreateFilter, + }, is_self_loop_graph::IsSelfLoopGraph, }, }, @@ -10,7 +13,6 @@ use crate::{ prelude::GraphViewOps, }; use std::fmt; -use crate::db::graph::views::filter::model::CreateView; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct 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 bca1458efa..2e58b6c693 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 @@ -2,7 +2,10 @@ use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ - filter::{model::ComposableFilter, CreateFilter}, + filter::{ + model::{ComposableFilter, CreateView}, + CreateFilter, + }, valid_graph::ValidGraph, }, }, @@ -10,7 +13,6 @@ use crate::{ prelude::GraphViewOps, }; use std::fmt; -use crate::db::graph::views::filter::model::CreateView; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct 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 eed3f5312a..d84f743bf0 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -4,8 +4,8 @@ use crate::{ graph::views::{ filter::{ model::{ - windowed_filter::Windowed, ComposableFilter, CreateView, InternalViewWrapOps, - Wrap, + windowed_filter::Windowed, ComposableFilter, CreateView, EdgeViewFilterOps, + InternalViewWrapOps, Wrap, }, CreateFilter, }, @@ -17,7 +17,6 @@ use crate::{ }; use raphtory_api::core::storage::timeindex::EventTime; use std::{fmt, fmt::Display}; -use crate::db::graph::views::filter::model::EdgeViewFilterOps; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct 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 b834dd5e13..b12d64a2f0 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -2,7 +2,7 @@ use crate::{ db::{ api::view::internal::{GraphView, InternalFilter}, graph::views::filter::{ - model::{ComposableFilter, InternalViewWrapOps, Wrap}, + model::{ComposableFilter, EdgeViewFilterOps, InternalViewWrapOps, Wrap}, CreateFilter, }, }, @@ -11,7 +11,6 @@ use crate::{ }; use raphtory_api::core::{entities::Layer, storage::timeindex::EventTime}; use std::{fmt, fmt::Display}; -use crate::db::graph::views::filter::model::EdgeViewFilterOps; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Layered { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 480dbb7e7b..988020a5b5 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -759,31 +759,33 @@ impl DynNodeViewFilterOps for pub trait EdgeViewFilterOps: ViewWrapOps { fn is_active(&self) -> IsActiveEdge { - IsActiveEdge{ - view_expr: self.clone() + IsActiveEdge { + view_expr: self.clone(), } } fn is_valid(&self) -> IsValidEdge { IsValidEdge { - view_expr: self.clone() + view_expr: self.clone(), } } fn is_deleted(&self) -> IsDeletedEdge { IsDeletedEdge { - view_expr: self.clone() + view_expr: self.clone(), } } fn is_self_loop(&self) -> IsSelfLoopEdge { IsSelfLoopEdge { - view_expr: self.clone() + view_expr: self.clone(), } } } impl EdgeViewFilterOps for EdgeFilter {} +impl EdgeViewFilterOps for ExplodedEdgeFilter {} + pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps { fn dyn_is_active(&self) -> Arc; @@ -879,7 +881,7 @@ impl InternalViewWrapOps for DynEdgeViewProps { } // TODO: Do we need this? -impl EdgeViewFilterOps for DynEdgeViewProps {} +// impl EdgeViewFilterOps for DynEdgeViewProps {} // ───────────────────────────────────────────────────────────────────────────── // EntityExprFilterOps — comparison and set operators on any EntityExpr 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 ffa3227be2..b8d0583411 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 @@ -706,4 +706,3 @@ impl CreateOp for PropValueSetExpr { } } } - 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 b333e9ca84..3764c01804 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 @@ -22,8 +22,8 @@ use crate::{ node_state_filter::NodeStateBoolColOp, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - ComposableFilter, CreateView, EntityMarker, InternalViewWrapOps, - NodeViewFilterOps, PropertyFilterFactory, Wrap, + ComposableFilter, CreateView, EntityMarker, InternalViewWrapOps, NodeViewFilterOps, + PropertyFilterFactory, Wrap, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, 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 b0859f1d70..014eeb5eb4 100644 --- a/raphtory/src/db/graph/views/filter/model/not_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/not_filter.rs @@ -53,4 +53,3 @@ impl CreateFilter for NotFilter { 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 807de88259..6f9aa6f26c 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -56,4 +56,3 @@ impl CreateFilter for OrFilter { Ok(left.or(right)) } } - 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 63cfe7de47..0fb1ff301e 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,7 +1,9 @@ -use crate::db::graph::views::filter::CreateFilter; -use crate::db::graph::views::filter::model::{ - property_filter::{Op, PropertyFilter, PropertyFilterInput, PropertyRef}, - EntityMarker, Wrap, +use crate::db::graph::views::filter::{ + model::{ + property_filter::{Op, PropertyFilter, PropertyFilterInput, PropertyRef}, + EntityMarker, Wrap, + }, + CreateFilter, }; pub trait InternalPropertyFilterBuilder { 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 300a831d38..7cad07085b 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,9 +12,7 @@ use crate::{ edge_property_filtered_graph::EdgePropertyFilteredGraph, exploded_edge_property_filter::ExplodedEdgePropertyFilteredGraph, model::{ - filter_value::FilterValue, ComposableFilter, - ExplodedEdgeFilter, - FilterOperator, + filter_value::FilterValue, ComposableFilter, ExplodedEdgeFilter, FilterOperator, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -444,4 +442,3 @@ impl CreateFilter for PropertyFilter { } impl ComposableFilter for PropertyFilter {} - 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 46ed98e1bc..8416bf6861 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -4,8 +4,8 @@ use crate::{ graph::views::{ filter::{ model::{ - windowed_filter::Windowed, ComposableFilter, CreateView, - InternalViewWrapOps, Wrap, + windowed_filter::Windowed, ComposableFilter, CreateView, EdgeViewFilterOps, + InternalViewWrapOps, Wrap, }, CreateFilter, }, @@ -17,7 +17,6 @@ use crate::{ }; use raphtory_api::core::{storage::timeindex::EventTime, utils::time::IntoTime}; use std::{fmt, fmt::Display}; -use crate::db::graph::views::filter::model::EdgeViewFilterOps; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SnapshotAt { 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 4203584d3d..4e3c3fa5ac 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -3,7 +3,9 @@ use crate::{ api::view::internal::GraphView, graph::views::{ filter::{ - model::{ComposableFilter, CreateView, InternalViewWrapOps, Wrap}, + model::{ + ComposableFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, Wrap, + }, CreateFilter, }, window_graph::WindowedGraph, @@ -17,7 +19,6 @@ use raphtory_api::core::{ utils::time::IntoTime, }; use std::{fmt, fmt::Display}; -use crate::db::graph::views::filter::model::EdgeViewFilterOps; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Windowed { From 288bdbbf5beb77218e7e0aab6e2ed4c65c6a5d1a Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:17:05 +0100 Subject: [PATCH 092/155] fix bounds --- raphtory/src/db/graph/views/filter/model/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 988020a5b5..ab7a4c04fe 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -796,7 +796,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()) } @@ -880,8 +880,7 @@ impl InternalViewWrapOps for DynEdgeViewProps { } } -// TODO: Do we need this? -// impl EdgeViewFilterOps for DynEdgeViewProps {} +impl EdgeViewFilterOps for DynEdgeViewProps {} // ───────────────────────────────────────────────────────────────────────────── // EntityExprFilterOps — comparison and set operators on any EntityExpr From 0ef95b0f9689d5f94e444e6ebe4e231dddc8baca Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:28:48 +0100 Subject: [PATCH 093/155] add EntityExpr + CreateOp impls to IsActive/IsValid/IsDeleted/IsSelfLoop edge predicates with per-edge prop ops --- .../graph/views/filter/model/edge_expr/ops.rs | 90 +++++++++++++++++-- .../filter/model/is_active_edge_filter.rs | 36 +++++++- .../views/filter/model/is_deleted_filter.rs | 36 +++++++- .../views/filter/model/is_self_loop_filter.rs | 36 +++++++- .../views/filter/model/is_valid_filter.rs | 36 +++++++- 5 files changed, 220 insertions(+), 14 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index e0f21f1c17..59c551cced 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -2,13 +2,16 @@ //! //! Parallel to `node_expr/ops.rs` — same design, different subject. -use crate::db::{ - api::{ - properties::internal::{InternalMetadataOps, InternalTemporalPropertyViewOps}, - state::ops::Const, - view::internal::GraphView, +use crate::{ + db::{ + api::{ + properties::internal::{InternalMetadataOps, InternalTemporalPropertyViewOps}, + state::ops::Const, + view::internal::GraphView, + }, + graph::edge::EdgeView, }, - graph::edge::EdgeView, + prelude::EdgeViewOps, }; use raphtory_api::core::entities::{ edges::edge_ref::EdgeRef, @@ -452,3 +455,78 @@ impl<'g> EdgeOp for EdgeEndpointNodeOp<'g> { self.node_op.apply(storage, vid) } } + +// ───────────────────────────────────────────────────────────────────────────── +// Per-edge predicate ops — produce Some(Prop::Bool(...)) per edge. +// Used by `CreateOp::create_edge_op` for the expression-mode path of the +// structural edge predicates (IsActiveEdge, IsValidEdge, IsDeletedEdge, +// IsSelfLoopEdge). +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct IsActiveEdgePropOp { + pub(crate) graph: G, +} + +impl EdgeOp for IsActiveEdgePropOp { + type Output = Option; + + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { + Some(Prop::Bool(EdgeView::new(&self.graph, edge).is_active())) + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } +} + +#[derive(Clone)] +pub(crate) struct IsValidEdgePropOp { + pub(crate) graph: G, +} + +impl EdgeOp for IsValidEdgePropOp { + type Output = Option; + + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { + Some(Prop::Bool(EdgeView::new(&self.graph, edge).is_valid())) + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } +} + +#[derive(Clone)] +pub(crate) struct IsDeletedEdgePropOp { + pub(crate) graph: G, +} + +impl EdgeOp for IsDeletedEdgePropOp { + type Output = Option; + + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { + Some(Prop::Bool(EdgeView::new(&self.graph, edge).is_deleted())) + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } +} + +#[derive(Clone)] +pub(crate) struct IsSelfLoopEdgePropOp { + pub(crate) graph: G, +} + +impl EdgeOp for IsSelfLoopEdgePropOp { + type Output = Option; + + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { + Some(Prop::Bool(EdgeView::new(&self.graph, edge).is_self_loop())) + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } +} 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 55781e89a3..6670691fc7 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 @@ -3,7 +3,12 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - model::{ComposableFilter, CreateView}, + model::{ + edge_expr::{ops::IsActiveEdgePropOp, EdgeOp}, + edge_filter::EdgeFilter, + node_expr::{CreateOp, EntityExpr}, + ComposableFilter, CreateView, + }, CreateFilter, }, is_active_graph::IsActiveGraph, @@ -12,7 +17,8 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use std::fmt; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::{fmt, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IsActiveEdge { @@ -25,6 +31,32 @@ impl fmt::Display for IsActiveEdge { } } +impl EntityExpr for IsActiveEdge { + type Marker = EdgeFilter; + + fn entity(&self) -> Self::Marker { + EdgeFilter + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsActiveEdge { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let view = self.view_expr.create_view(graph)?; + Ok(Arc::new(IsActiveEdgePropOp { graph: view })) + } +} + impl CreateFilter for IsActiveEdge { type EntityFiltered<'graph, G> = IsActiveGraph 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 87a7c160ca..510a3c8406 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 @@ -3,7 +3,12 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - model::{ComposableFilter, CreateView}, + model::{ + edge_expr::{ops::IsDeletedEdgePropOp, EdgeOp}, + edge_filter::EdgeFilter, + node_expr::{CreateOp, EntityExpr}, + ComposableFilter, CreateView, + }, CreateFilter, }, is_deleted_graph::IsDeletedGraph, @@ -12,7 +17,8 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use std::fmt; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::{fmt, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IsDeletedEdge { @@ -25,6 +31,32 @@ impl fmt::Display for IsDeletedEdge { } } +impl EntityExpr for IsDeletedEdge { + type Marker = EdgeFilter; + + fn entity(&self) -> Self::Marker { + EdgeFilter + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsDeletedEdge { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let view = self.view_expr.create_view(graph)?; + Ok(Arc::new(IsDeletedEdgePropOp { graph: view })) + } +} + impl CreateFilter for IsDeletedEdge { type EntityFiltered<'graph, G> = IsDeletedGraph 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 40fc05d767..37d062ddf0 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 @@ -3,7 +3,12 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - model::{ComposableFilter, CreateView}, + model::{ + edge_expr::{ops::IsSelfLoopEdgePropOp, EdgeOp}, + edge_filter::EdgeFilter, + node_expr::{CreateOp, EntityExpr}, + ComposableFilter, CreateView, + }, CreateFilter, }, is_self_loop_graph::IsSelfLoopGraph, @@ -12,7 +17,8 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use std::fmt; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::{fmt, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IsSelfLoopEdge { @@ -25,6 +31,32 @@ impl fmt::Display for IsSelfLoopEdge { } } +impl EntityExpr for IsSelfLoopEdge { + type Marker = EdgeFilter; + + fn entity(&self) -> Self::Marker { + EdgeFilter + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsSelfLoopEdge { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let view = self.view_expr.create_view(graph)?; + Ok(Arc::new(IsSelfLoopEdgePropOp { graph: view })) + } +} + impl CreateFilter for IsSelfLoopEdge { type EntityFiltered<'graph, G> = IsSelfLoopGraph 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 2e58b6c693..ef0b0ef7b4 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 @@ -3,7 +3,12 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - model::{ComposableFilter, CreateView}, + model::{ + edge_expr::{ops::IsValidEdgePropOp, EdgeOp}, + edge_filter::EdgeFilter, + node_expr::{CreateOp, EntityExpr}, + ComposableFilter, CreateView, + }, CreateFilter, }, valid_graph::ValidGraph, @@ -12,7 +17,8 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use std::fmt; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::{fmt, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IsValidEdge { @@ -25,6 +31,32 @@ impl fmt::Display for IsValidEdge { } } +impl EntityExpr for IsValidEdge { + type Marker = EdgeFilter; + + fn entity(&self) -> Self::Marker { + EdgeFilter + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsValidEdge { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let view = self.view_expr.create_view(graph)?; + Ok(Arc::new(IsValidEdgePropOp { graph: view })) + } +} + impl CreateFilter for IsValidEdge { type EntityFiltered<'graph, G> = ValidGraph From a47b471830162997b846b3ca943fdfd89b47901c Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:46:49 +0100 Subject: [PATCH 094/155] fix by_state_column, ref --- .../db/graph/views/filter/model/node_expr/ops.rs | 4 ++-- raphtory/src/db/graph/views/window_graph.rs | 2 +- raphtory/src/python/filter/node_expr.rs | 14 ++++++-------- 3 files changed, 9 insertions(+), 11 deletions(-) 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 index 700a18b039..784add51fc 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -201,7 +201,7 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { if vals.peek().is_none() { return None; } - let inner = vals.peek().unwrap().borrow().dtype(); + let inner = vals.peek().unwrap().dtype(); match inner { PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { let (promoted, s64, s128, _) = scan_u64_sum(vals)?; @@ -233,7 +233,7 @@ impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { if vals.peek().is_none() { return None; } - let inner = vals.peek().unwrap().borrow().dtype(); + let inner = vals.peek().unwrap().dtype(); match inner { PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { let (promoted, s64, s128, count) = scan_u64_sum(vals)?; diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index 21e2c18916..fbc1feef2d 100644 --- a/raphtory/src/db/graph/views/window_graph.rs +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -113,7 +113,7 @@ impl<'graph1, 'graph2, G1: GraphViewOps<'graph1>, G2: GraphViewOps<'graph2>> Par } } -impl<'graph, G: GraphViewOps<'graph>> Base for WindowedGraph { +impl<'graph, G> Base for WindowedGraph { type Base = G; #[inline(always)] fn base(&self) -> &Self::Base { diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 3315e43a94..728a000dfe 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -7,7 +7,10 @@ use crate::{ PropertyFilterFactory, ViewWrapOps, }, prelude::{EntityAggOps, EntityExprFilterOps, NodeFilter, NodeFilterFactory}, - python::{graph::node_state::PyOutputNodeState, types::iterable::FromIterable}, + python::{ + filter::filter_expr::PyFilterExpr, graph::node_state::PyOutputNodeState, + types::iterable::FromIterable, + }, }; use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, PyResult, Python}; use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; @@ -463,14 +466,9 @@ impl PyNodeFilter { /// /// Returns: /// filter.FilterExpr: - fn by_state_column(&self, state: &PyOutputNodeState, col: String) -> PyResult { + fn by_state_column(&self, state: &PyOutputNodeState, col: String) -> PyResult { let op = NodeStateBoolColOp::new(&state.inner, &col) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; - Ok(PyExpr(Arc::new(op))) + Ok(PyFilterExpr(Arc::new(op))) } } - -// TODO: -// We need a wrapper for Arc -// Py Expr needs to extend this wrapper (PyFilter) -// by_state_column can return pyfilter From f7de3ea9ad5e1c6b4251ec652fb26f7a100402cc Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Mon, 29 Jun 2026 08:23:31 +0200 Subject: [PATCH 095/155] simplify some trait implementations --- raphtory/src/db/api/view/graph.rs | 7 +- raphtory/src/db/api/view/internal/filter.rs | 22 ++-- .../internal/time_semantics/filtered_edge.rs | 2 +- .../src/db/graph/views/filter/model/mod.rs | 121 +----------------- .../views/filter/model/node_expr/tests.rs | 2 +- .../views/filter/model/node_filter/mod.rs | 2 +- 6 files changed, 19 insertions(+), 137 deletions(-) diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index d21868f439..13a72561ec 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -1292,16 +1292,13 @@ where G: GraphView + 'graph, { type Graph = G; - type Filtered + 'graph> = Next; + type Filtered = Next; fn base_graph(&self) -> &Self::Graph { self } - fn apply_filter + 'graph>( - &self, - filtered_graph: Next, - ) -> Self::Filtered { + fn apply_filter(&self, filtered_graph: Next) -> Self::Filtered { filtered_graph } } diff --git a/raphtory/src/db/api/view/internal/filter.rs b/raphtory/src/db/api/view/internal/filter.rs index cb7be9fe87..17379caef4 100644 --- a/raphtory/src/db/api/view/internal/filter.rs +++ b/raphtory/src/db/api/view/internal/filter.rs @@ -1,23 +1,23 @@ -use crate::{db::api::state::ops::NodeFilterOp, prelude::GraphViewOps}; +use crate::{ + db::api::{state::ops::NodeFilterOp, view::internal::GraphView}, + prelude::GraphViewOps, +}; pub trait InternalFilter<'graph> { - type Graph: GraphViewOps<'graph> + 'graph; + type Graph: GraphView + 'graph; - type Filtered + 'graph>: InternalFilter< - 'graph, - Graph = FilteredGraph, - >; + type Filtered: InternalFilter<'graph, Graph = FilteredGraph>; fn base_graph(&self) -> &Self::Graph; - fn apply_filter + 'graph>( + fn apply_filter( &self, filtered_graph: FilteredGraph, ) -> Self::Filtered; } pub trait InternalNodeSelect<'graph> { - type IterGraph: GraphViewOps<'graph> + 'graph; + type IterGraph: GraphView + 'graph; type IterFiltered: InternalNodeSelect< 'graph, @@ -33,16 +33,16 @@ pub trait InternalNodeSelect<'graph> { } pub trait InternalEdgeSelect<'graph> { - type IterGraph: GraphViewOps<'graph> + 'graph; + type IterGraph: GraphView + 'graph; - type IterFiltered + 'graph>: InternalEdgeSelect< + type IterFiltered: InternalEdgeSelect< 'graph, IterGraph = Self::IterGraph, >; fn iter_graph(&self) -> &Self::IterGraph; - fn apply_iter_filter + 'graph>( + fn apply_iter_filter( &self, filtered_graph: FilteredGraph, ) -> Self::IterFiltered; diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index a97d4e4fa5..f652274c66 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs @@ -208,7 +208,7 @@ pub struct FilteredEdgeTProp { props: P, } -impl<'graph, G: GraphViewOps<'graph>, P: TPropOps<'graph>> TPropOps<'graph> +impl<'graph, G: GraphView + 'graph, P: TPropOps<'graph>> TPropOps<'graph> for FilteredEdgeTProp { fn last(&self) -> Option<(EventTime, Prop)> { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index ab7a4c04fe..86160189c3 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -481,7 +481,7 @@ impl DynInternalViewWrapOps for T { } } -impl InternalViewWrapOps for Arc { +impl InternalViewWrapOps for Arc { type Window = Arc; fn bounds(&self) -> (EventTime, EventTime) { @@ -553,7 +553,7 @@ pub trait CreateView: Clone + Send + Sync + 'static { ) -> Result, GraphError>; } -pub trait DynCreateView: Send + Sync { +pub trait DynCreateView: Send + Sync + 'static { fn dyn_create_view<'graph>( &self, view: Arc, @@ -569,7 +569,7 @@ impl DynCreateView for T { } } -impl CreateView for Arc { +impl CreateView for Arc { type View<'graph, G: GraphView + 'graph> = Arc; fn create_view<'graph, G: GraphView + 'graph>( @@ -695,29 +695,6 @@ impl DynInternalViewWrapPropOps for T where { } -impl InternalViewWrapOps for Arc { - type Window = Arc; - - fn bounds(&self) -> (EventTime, EventTime) { - self.deref().dyn_bounds() - } - - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - Arc::new(Windowed::new(start, end, self)) - } -} - -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 {} @@ -729,34 +706,6 @@ pub type DynFilter = Arc; impl ComposableFilter for DynFilter {} impl ComposableFilter for DynView {} -impl InternalViewWrapOps for DynView { - type Window = DynView; - - fn bounds(&self) -> (EventTime, EventTime) { - self.deref().dyn_bounds() - } - - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - Arc::new(Windowed::new(start, end, self)) - } -} - -pub trait NodeViewFilterOps: ViewWrapOps { - type Output: CreateFilter; - - fn is_active(&self) -> Self::Output>; -} - -pub trait DynNodeViewFilterOps: DynInternalViewWrapPropOps { - fn dyn_is_active(&self) -> Arc; -} - -impl DynNodeViewFilterOps for T { - fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) - } -} - pub trait EdgeViewFilterOps: ViewWrapOps { fn is_active(&self) -> IsActiveEdge { IsActiveEdge { @@ -814,72 +763,8 @@ impl DynEdgeView } } -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; - - fn bounds(&self) -> (EventTime, EventTime) { - self.deref().dyn_bounds() - } - - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - Arc::new(Windowed::new(start, end, self)) - } -} - -impl NodeViewFilterOps for DynNodeViewProps { - type Output = Arc; - - 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; - - fn bounds(&self) -> (EventTime, EventTime) { - self.deref().dyn_bounds() - } - - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - Arc::new(Windowed::new(start, end, self)) - } -} - impl EdgeViewFilterOps for DynEdgeViewProps {} // ───────────────────────────────────────────────────────────────────────────── 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 dab57a2ae1..00674a4c87 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 @@ -10,7 +10,7 @@ use crate::{ CreateFilter, }, }, - prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, + prelude::{AdditionOps, EntityExprFilterOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, }; use raphtory_api::core::{ entities::{ 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 3764c01804..9a9ff234cd 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 @@ -22,7 +22,7 @@ use crate::{ node_state_filter::NodeStateBoolColOp, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - ComposableFilter, CreateView, EntityMarker, InternalViewWrapOps, NodeViewFilterOps, + ComposableFilter, CreateView, EntityMarker, InternalViewWrapOps, PropertyFilterFactory, Wrap, }, node_filtered_graph::NodeFilteredGraph, From a0aac86b06cb19a417ad60de4a8e2163039b49a0 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Mon, 29 Jun 2026 11:04:26 +0200 Subject: [PATCH 096/155] fix the trait implementations and clean up redundant implementations of CreateOp --- .../views/filter/model/edge_expr/filters.rs | 128 ----------- .../src/db/graph/views/filter/model/mod.rs | 18 +- .../views/filter/model/node_expr/dyn_expr.rs | 30 +-- .../views/filter/model/node_expr/exprs.rs | 12 +- .../views/filter/model/node_expr/filters.rs | 213 ++++++------------ .../graph/views/filter/model/node_expr/mod.rs | 10 +- .../views/filter/model/node_filter/mod.rs | 35 ++- raphtory/src/python/filter/node_expr.rs | 113 ++++------ 8 files changed, 177 insertions(+), 382 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index 455c7a3bb0..1434636b3a 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -33,43 +33,6 @@ use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpExpr // ───────────────────────────────────────────────────────────────────────────── -impl CreateOp for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(ListAwareCmpEdgeOp { - left, - right, - op: self.op, - })) - } -} - -impl CreateOp for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(ListAwareCmpEdgeOp { - left, - right, - op: self.op, - })) - } -} impl CreateFilter for BinaryCmpExpr where @@ -148,31 +111,6 @@ where // ───────────────────────────────────────────────────────────────────────────── // UnaryExpr // ───────────────────────────────────────────────────────────────────────────── -impl CreateOp for UnaryExpr -where - E: CreateOp, -{ - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_edge_op(graph)?; - Ok(Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op })) - } -} - -impl CreateOp for UnaryExpr -where - E: CreateOp, -{ - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_edge_op(graph)?; - Ok(Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op })) - } -} impl CreateFilter for UnaryExpr where @@ -230,44 +168,6 @@ where // StringExpr — string expression filter for edges // ───────────────────────────────────────────────────────────────────────────── -impl CreateOp for StringExpr -where - L: CreateOp, - R: CreateOp, -{ - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(ListAwareStringEdgeOp { - left, - right, - op: self.op, - })) - } -} - -impl CreateOp for StringExpr -where - L: CreateOp, - R: CreateOp, -{ - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(ListAwareStringEdgeOp { - left, - right, - op: self.op, - })) - } -} - impl CreateFilter for StringExpr where L: CreateOp, @@ -336,34 +236,6 @@ where // PropValueSetExpr — is_in / is_not_in for edge-side exprs // ───────────────────────────────────────────────────────────────────────────── -impl CreateOp for PropValueSetExpr { - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_edge_op(graph)?; - Ok(Arc::new(ListAwareSetEdgeOp { - inner, - values: self.values.clone(), - op: self.op, - })) - } -} - -impl CreateOp for PropValueSetExpr { - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_edge_op(graph)?; - Ok(Arc::new(ListAwareSetEdgeOp { - inner, - values: self.values.clone(), - op: self.op, - })) - } -} - impl CreateFilter for PropValueSetExpr { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgeExprFilteredGraph + 'graph>>; diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 86160189c3..093c4cc77c 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -209,6 +209,7 @@ pub enum EntityMarker { Node, Edge, ExplodedEdge, + Const, } // ───────────────────────────────────────────────────────────────────────────── @@ -322,13 +323,13 @@ impl CreateOp for Me } } -pub trait PropertyFilterFactory: Sized { +pub trait PropertyFilterFactory: CreateView + EntityExpr + Sized { fn property(&self, name: impl Into) -> PropertyExpr; fn metadata(&self, name: impl Into) -> MetadataExpr; } -impl PropertyFilterFactory for T { +impl PropertyFilterFactory for T { fn property(&self, name: impl Into) -> PropertyExpr { PropertyExpr { view_expr: self.clone(), @@ -345,15 +346,12 @@ impl PropertyFilterFactory for T { } pub trait DynPropertyFilterFactory { - fn property(&self, name: String) -> PropertyExpr>; + fn dyn_property(&self, name: String) -> Arc; } -impl DynPropertyFilterFactory for T { - fn property(&self, name: String) -> PropertyExpr> { - PropertyExpr { - view_expr: Arc::new(self.clone()) as Arc, - name, - } +impl DynPropertyFilterFactory for T { + fn dyn_property(&self, name: String) -> Arc { + Arc::new(self.property(name)) } } @@ -444,7 +442,7 @@ pub trait EdgeFilterFactory: PropertyFilterFactory + Clone {} use crate::db::graph::views::filter::model::{ edge_expr::ops::{EdgeMetaOp, EdgePropOp}, - node_expr::{CreateOp, EntityExpr}, + node_expr::{CreateOp, DynTemporal, EntityExpr}, }; use edge_expr::EdgeOp; use raphtory_api::core::entities::properties::prop::PropType; diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index a4e68613ef..18127dba3f 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -36,7 +36,7 @@ use crate::{ AvgExpr, CreateOp, EntityAggOps, EntityExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr, }, - EntityMarker, + CreateView, EntityMarker, PropertyExpr, }, }, errors::GraphError, @@ -44,7 +44,7 @@ use crate::{ use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::{ops::Deref, sync::Arc}; -trait DynEntityExpr: Send + Sync + 'static { +pub trait DynEntityExpr: Send + Sync + 'static { fn dyn_entity(&self) -> EntityMarker; fn dyn_prop_type(&self) -> PropType; fn dyn_nullable(&self) -> bool; @@ -64,19 +64,13 @@ impl>> DynEntityExpr for E { } } -impl EntityExpr for Arc { - type Marker = EntityMarker; - - fn entity(&self) -> Self::Marker { - self.deref().dyn_entity() - } - - fn prop_type(&self) -> PropType { - self.deref().dyn_prop_type() - } +pub trait DynTemporal: DynCreateOp { + fn temporal(&self) -> Arc; +} - fn nullable(&self) -> bool { - self.deref().dyn_nullable() +impl DynTemporal for PropertyExpr { + fn temporal(&self) -> Arc { + Arc::new(self.temporal()) } } @@ -92,7 +86,7 @@ pub trait DynCreateOp: DynEntityExpr { ) -> Result> + 'g>, GraphError>; } -impl>> DynCreateOp for E { +impl DynCreateOp for E { fn dyn_create_node_op<'g>( &self, graph: Arc, @@ -108,7 +102,7 @@ impl>> DynCreateOp for E { } } -impl EntityExpr for Arc { +impl EntityExpr for Arc { type Marker = EntityMarker; fn entity(&self) -> Self::Marker { @@ -116,7 +110,7 @@ impl EntityExpr for Arc { } } -impl CreateOp for Arc { +impl CreateOp for Arc { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -132,7 +126,7 @@ impl CreateOp for Arc { } } -impl EntityAggOps for Arc { +impl EntityAggOps for Arc { fn sum(self) -> SumExpr { SumExpr(self) } 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 88a63c3719..dca375e463 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 @@ -79,7 +79,7 @@ use crate::{ edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, filter_operator::Comparable, node_filter::{NodeFilter, NodeFilterFactory}, - require_aggregable, resolved_prop_type, CreateView, + require_aggregable, resolved_prop_type, CreateView, EntityMarker, }, }, errors::GraphError, @@ -107,6 +107,12 @@ use std::sync::Arc; #[derive(Copy, Clone, Debug, Default)] pub struct ConstFilter; +impl From for EntityMarker { + fn from(_value: ConstFilter) -> Self { + EntityMarker::Const + } +} + impl EntityExpr for Id { type Marker = NodeFilter; @@ -509,9 +515,7 @@ impl EntityAggOps for TemporalPro } } -impl CreateOp - for TemporalPropExpr -{ +impl CreateOp for TemporalPropExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, 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 b8d0583411..188b9c115e 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 @@ -45,7 +45,7 @@ use super::{ BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, ListAwareUnaryNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, - CreateOp, EntityExpr, + ConstFilter, CreateOp, EntityExpr, Marker, }; use crate::{ db::{ @@ -56,7 +56,13 @@ use crate::{ graph::views::filter::{ model::{ coerce_set_values, - edge_expr::EdgeOp, + edge_expr::{ + ops::{ + ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, + ListAwareUnaryEdgeOp, + }, + EdgeOp, + }, filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, resolved_prop_type, validate_binary_op, validate_const_castable, validate_string_op, validate_types_compatible, ComposableFilter, CreateFilter, @@ -121,9 +127,7 @@ impl BinaryCmpExpr { impl ComposableFilter for BinaryCmpExpr {} -impl EntityExpr - for BinaryCmpExpr -{ +impl EntityExpr for BinaryCmpExpr { type Marker = E; fn entity(&self) -> Self::Marker { self.entity @@ -134,7 +138,7 @@ impl EntityExpr } } -impl CreateOp for BinaryCmpExpr { +impl CreateOp for BinaryCmpExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -147,6 +151,19 @@ impl CreateOp for BinaryCmpExpr { op: self.op, })) } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph)?; + Ok(Arc::new(ListAwareCmpEdgeOp { + left, + right, + op: self.op, + })) + } } impl CreateFilter for BinaryCmpExpr @@ -192,7 +209,7 @@ where impl CreateFilter for BinaryCmpExpr where - L: CreateOp, + L: CreateOp, R: CreateOp, { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; @@ -209,6 +226,8 @@ where EntityMarker::ExplodedEdge => { Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) } + + EntityMarker::Const => Err(GraphError::NotSupported)?, }) } @@ -220,37 +239,7 @@ where EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - } - } -} - -impl CreateOp for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - match self.entity { - EntityMarker::Node => self.clone().with_entity(NodeFilter).create_node_op(graph), - EntityMarker::Edge => Err(GraphError::NotNodeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - } - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - match self.entity { - EntityMarker::Node => Err(GraphError::NotEdgeFilter), - EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), - EntityMarker::ExplodedEdge => self - .clone() - .with_entity(ExplodedEdgeFilter) - .create_edge_op(graph), + EntityMarker::Const => Err(GraphError::NotSupported)?, } } } @@ -288,17 +277,14 @@ impl UnaryExpr { impl ComposableFilter for UnaryExpr {} -impl EntityExpr for UnaryExpr { - type Marker = Entity; +impl EntityExpr for UnaryExpr { + type Marker = M; fn entity(&self) -> Self::Marker { self.entity } } -impl CreateOp for UnaryExpr -where - E: CreateOp, -{ +impl CreateOp for UnaryExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -306,6 +292,14 @@ where let inner = self.expr.create_node_op(graph)?; Ok(Arc::new(ListAwareUnaryNodeOp { inner, op: self.op })) } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.expr.create_edge_op(graph)?; + Ok(Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op })) + } } impl CreateFilter for UnaryExpr @@ -358,6 +352,7 @@ where EntityMarker::ExplodedEdge => { Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) } + EntityMarker::Const => Err(GraphError::NotSupported)?, }) } @@ -369,36 +364,7 @@ where EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - } - } -} - -impl CreateOp for UnaryExpr -where - E: CreateOp, -{ - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - match self.entity { - EntityMarker::Node => self.clone().with_entity(NodeFilter).create_node_op(graph), - EntityMarker::Edge => Err(GraphError::NotNodeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - } - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - match self.entity { - EntityMarker::Node => Err(GraphError::NotEdgeFilter), - EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), - EntityMarker::ExplodedEdge => self - .clone() - .with_entity(ExplodedEdgeFilter) - .create_edge_op(graph), + EntityMarker::Const => Err(GraphError::NotSupported)?, } } } @@ -453,10 +419,8 @@ impl StringExpr { impl ComposableFilter for StringExpr {} -impl EntityExpr - for StringExpr -{ - type Marker = Entity; +impl EntityExpr for StringExpr { + type Marker = M; fn entity(&self) -> Self::Marker { self.entity } @@ -465,7 +429,7 @@ impl EntityE } } -impl CreateOp for StringExpr { +impl CreateOp for StringExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -478,6 +442,19 @@ impl CreateOp for StringExpr { op: self.op, })) } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.left.create_edge_op(graph.clone())?; + let right = self.right.create_edge_op(graph)?; + Ok(Arc::new(ListAwareStringEdgeOp { + left, + right, + op: self.op, + })) + } } impl CreateFilter for StringExpr { @@ -525,6 +502,7 @@ impl CreateFilter for StringExpr { EntityMarker::ExplodedEdge => { Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) } + EntityMarker::Const => Err(GraphError::NotSupported)?, }) } @@ -536,33 +514,7 @@ impl CreateFilter for StringExpr { EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - } - } -} - -impl CreateOp for StringExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - match self.entity { - EntityMarker::Node => self.clone().with_entity(NodeFilter).create_node_op(graph), - EntityMarker::Edge => Err(GraphError::NotNodeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - } - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - match self.entity { - EntityMarker::Node => Err(GraphError::NotEdgeFilter), - EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), - EntityMarker::ExplodedEdge => self - .clone() - .with_entity(ExplodedEdgeFilter) - .create_edge_op(graph), + EntityMarker::Const => Err(GraphError::NotSupported)?, } } } @@ -596,10 +548,8 @@ impl PropValueSetExpr { impl ComposableFilter for PropValueSetExpr {} -impl EntityExpr - for PropValueSetExpr -{ - type Marker = Entity; +impl EntityExpr for PropValueSetExpr { + type Marker = M; fn entity(&self) -> Self::Marker { self.entity } @@ -608,7 +558,7 @@ impl EntityExpr } } -impl CreateOp for PropValueSetExpr { +impl CreateOp for PropValueSetExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -620,6 +570,18 @@ impl CreateOp for PropValueSetExpr { op: self.op, })) } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.expr.create_edge_op(graph)?; + Ok(Arc::new(ListAwareSetEdgeOp { + inner, + values: self.values.clone(), + op: self.op, + })) + } } impl CreateFilter for PropValueSetExpr { @@ -665,6 +627,7 @@ impl CreateFilter for PropValueSetExpr { EntityMarker::ExplodedEdge => { Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) } + EntityMarker::Const => Err(GraphError::NotSupported)?, }) } @@ -676,33 +639,7 @@ impl CreateFilter for PropValueSetExpr { EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - } - } -} - -impl CreateOp for PropValueSetExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - match self.entity { - EntityMarker::Node => self.clone().with_entity(NodeFilter).create_node_op(graph), - EntityMarker::Edge => Err(GraphError::NotNodeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - } - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - match self.entity { - EntityMarker::Node => Err(GraphError::NotEdgeFilter), - EntityMarker::Edge => self.clone().with_entity(EdgeFilter).create_edge_op(graph), - EntityMarker::ExplodedEdge => self - .clone() - .with_entity(ExplodedEdgeFilter) - .create_edge_op(graph), + EntityMarker::Const => Err(GraphError::NotSupported)?, } } } 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 index 85e2c6b11e..fad2a185af 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -17,7 +17,9 @@ pub mod ops; mod tests; pub use super::{Metadata, Property}; -use crate::db::graph::views::filter::model::{edge_expr::EdgeOp, node_filter::NodeFilter}; +use crate::db::graph::views::filter::model::{ + edge_expr::EdgeOp, node_filter::NodeFilter, EntityMarker, +}; pub use dyn_expr::*; pub use exprs::*; pub use filters::*; @@ -64,8 +66,12 @@ pub trait CreateOp: EntityExpr + Clone + Send + Sync + 'static { } } +pub trait Marker: Into + Copy + Send + Sync + 'static {} + +impl + Copy + Send + Sync + 'static> Marker for M {} + pub trait EntityExpr: Clone + Send + Sync + 'static { - type Marker: Copy + 'static; + type Marker: Marker; fn entity(&self) -> Self::Marker; 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 9a9ff234cd..4cd048ea3c 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 @@ -17,13 +17,13 @@ use crate::{ is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, - node_expr::DegreeExpr, + node_expr::{DegreeExpr, EntityExpr}, node_filter::validate::validate, node_state_filter::NodeStateBoolColOp, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, ComposableFilter, CreateView, EntityMarker, InternalViewWrapOps, - PropertyFilterFactory, Wrap, + PropertyFilterFactory, ViewWrapPropOps, Wrap, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -48,7 +48,12 @@ impl From for EntityMarker { } } -pub trait NodeFilterFactory: PropertyFilterFactory + Clone { +// The extra type is a workaround for a trait solver limitation, ideally this would just be `NodeFilterFactory: InternalViewWrapOps` but that leads to a cycle in the compiler. +// We need this bound for the python implementation to avoid infinte recursion in the dynamic trait bounds. +pub trait NodeFilterFactory: + InternalViewWrapOps + CreateView + EntityExpr +{ + type NodeWindow: NodeFilterFactory; #[inline] fn id(&self) -> Id { Id @@ -116,7 +121,9 @@ pub trait NodeFilterFactory: PropertyFilterFactory + Clone { } } -impl NodeFilterFactory for NodeFilter {} +impl NodeFilterFactory for NodeFilter { + type NodeWindow = Self::Window; +} impl Wrap for NodeFilter { type Wrapped = T; @@ -134,11 +141,21 @@ impl InternalViewWrapOps for NodeFilter { } } -impl NodeFilterFactory for Windowed {} -impl NodeFilterFactory for Latest {} -impl NodeFilterFactory for SnapshotAt {} -impl NodeFilterFactory for SnapshotLatest {} -impl NodeFilterFactory for Layered {} +impl NodeFilterFactory for Windowed { + type NodeWindow = T::NodeWindow; +} +impl NodeFilterFactory for Latest { + type NodeWindow = Self::Window; +} +impl NodeFilterFactory for SnapshotAt { + type NodeWindow = Self::Window; +} +impl NodeFilterFactory for SnapshotLatest { + type NodeWindow = Self::Window; +} +impl NodeFilterFactory for Layered { + type NodeWindow = Self::Window; +} #[derive(Debug, Clone)] pub struct NodeIdFilter(pub Filter); diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 728a000dfe..6208073508 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,10 +1,10 @@ use crate::{ db::graph::views::filter::model::{ degree_filter::DegreeFilterFactory, - node_expr::{CreateOp, DynCreateOp, EntityExpr}, + node_expr::{CreateOp, DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr}, node_state_filter::NodeStateBoolColOp, - CreateView, EntityMarker, InternalViewWrapOps, NodeViewFilterOps, PropertyExpr, - PropertyFilterFactory, ViewWrapOps, + CreateView, DynCreateView, DynPropertyFilterFactory, EntityMarker, InternalViewWrapOps, + PropertyExpr, PropertyFilterFactory, ViewWrapOps, }, prelude::{EntityAggOps, EntityExprFilterOps, NodeFilter, NodeFilterFactory}, python::{ @@ -16,23 +16,6 @@ use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, PyResult, Python}; use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; use std::sync::Arc; -trait DynTemporal: DynCreateOp { - fn temporal(&self) -> Arc; -} - -impl< - E: EntityExpr> - + CreateView - + NodeFilterFactory - + Send - + Sync - + 'static, - > DynTemporal for PropertyExpr -{ - fn temporal(&self) -> Arc { - Arc::new(self.temporal()) - } -} // filter.Node.neighbours.is_active.all #[pyclass(frozen, subclass, name = "Expr", module = "raphtory.filter")] #[derive(Clone)] @@ -167,7 +150,9 @@ impl PyExpr { // fn temporal(&self) -> Self { … } } -pub trait DynNodeFilterFactory: Send + Sync + 'static { +pub trait DynNodeFilterFactory: + DynPropertyFilterFactory + DynEntityExpr + DynCreateView + Send + Sync + 'static +{ fn dyn_id(&self) -> Arc; fn dyn_name(&self) -> Arc; fn dyn_node_type(&self) -> Arc; @@ -175,31 +160,29 @@ pub trait DynNodeFilterFactory: Send + Sync + 'static { fn dyn_in_degree(&self) -> Arc; fn dyn_out_degree(&self) -> Arc; fn dyn_is_active(&self) -> Arc; - fn dyn_property(&self, name: String) -> Arc; fn dyn_metadata(&self, name: String) -> Arc; - fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc; - fn dyn_at(&self, time: EventTime) -> Arc; - fn dyn_after(&self, time: EventTime) -> Arc; - fn dyn_before(&self, time: EventTime) -> Arc; - fn dyn_latest(&self) -> Arc; - fn dyn_snapshot_at(&self, time: EventTime) -> Arc; - fn dyn_snapshot_latest(&self) -> Arc; - fn dyn_layer(&self, layers: Vec) -> Arc; + fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc; + + fn dyn_bounds(&self) -> (EventTime, EventTime); } impl InternalViewWrapOps for Arc { type Window = Arc; + fn bounds(&self) -> (EventTime, EventTime) { + self.dyn_bounds() + } + fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - self.dyn_window(start, end) + self.dyn_build_window(start, end) } } impl DynNodeFilterFactory for T where - T: NodeFilterFactory + ViewWrapOps + CreateView + EntityExpr + Clone + Send + Sync + 'static, - ::Marker: Into, + T: NodeFilterFactory + Send + Sync + 'static, + T::Window: NodeFilterFactory + Send + Sync + 'static, { fn dyn_id(&self) -> Arc { Arc::new(self.id()) @@ -225,45 +208,23 @@ where Arc::new(self.is_active()) } - fn dyn_property(&self, name: String) -> Arc { - Arc::new(self.property(name)) - } fn dyn_metadata(&self, name: String) -> Arc { Arc::new(PropertyFilterFactory::metadata(self, name)) } - // Go dynamic before calling window — the Arc impl - // has Window = Self, which terminates the recursive bound resolution. - fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - dyn_self.window(start, end) - } - fn dyn_at(&self, time: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - dyn_self.at(time) - } - fn dyn_after(&self, time: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - dyn_self.after(time) + fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc { + Arc::new(self.clone().build_window(start, end)) } - fn dyn_before(&self, time: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - dyn_self.before(time) - } - fn dyn_latest(&self) -> Arc { - Arc::new(self.clone().latest()) - } - fn dyn_snapshot_at(&self, time: EventTime) -> Arc { - Arc::new(self.clone().snapshot_at(time)) - } - fn dyn_snapshot_latest(&self) -> Arc { - Arc::new(self.clone().snapshot_latest()) - } - fn dyn_layer(&self, layers: Vec) -> Arc { - Arc::new(self.clone().layer(layers)) + + fn dyn_bounds(&self) -> (EventTime, EventTime) { + self.bounds() } } +impl NodeFilterFactory for Arc { + type NodeWindow = Self::Window; +} + /// Constructs node filter expressions. /// /// Each method returns either: @@ -273,6 +234,12 @@ where #[pyclass(frozen, name = "Node", module = "raphtory.filter")] pub struct PyNodeFilter(Arc); +impl PyNodeFilter { + fn wrap(filter: T) -> Self { + Self(Arc::new(filter)) + } +} + #[pymethods] impl PyNodeFilter { #[new] @@ -365,7 +332,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn window(&self, start: EventTime, end: EventTime) -> PyNodeFilter { - self.0.dyn_window(start, end).into() + self.0.clone().window(start, end).into() } /// Restricts node evaluation to a single point in time. @@ -376,7 +343,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn at(&self, time: EventTime) -> PyNodeFilter { - self.0.dyn_at(time).into() + self.0.clone().at(time).into() } /// Restricts node evaluation to times strictly after the given time. @@ -387,7 +354,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn after(&self, time: EventTime) -> PyNodeFilter { - self.0.dyn_after(time).into() + self.0.clone().after(time).into() } /// Restricts node evaluation to times strictly before the given time. @@ -398,7 +365,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn before(&self, time: EventTime) -> PyNodeFilter { - self.0.dyn_before(time).into() + self.0.clone().before(time).into() } /// Evaluates filters against the latest available state of each node. @@ -406,7 +373,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn latest(&self) -> PyNodeFilter { - self.0.dyn_latest().into() + Self::wrap(self.0.clone().latest()) } /// Evaluates filters against a snapshot of the graph at a given time. @@ -417,7 +384,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn snapshot_at(&self, time: EventTime) -> PyNodeFilter { - self.0.dyn_snapshot_at(time).into() + Self::wrap(self.0.clone().snapshot_at(time)) } /// Evaluates filters against the most recent snapshot of the graph. @@ -425,7 +392,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn snapshot_latest(&self) -> PyNodeFilter { - self.0.dyn_snapshot_latest().into() + Self::wrap(self.0.clone().snapshot_latest()) } /// Restricts evaluation to nodes belonging to the given layer. @@ -436,7 +403,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn layer(&self, layer: String) -> PyNodeFilter { - self.0.dyn_layer(vec![layer]).into() + Self::wrap(self.0.clone().layer(vec![layer])) } /// Restricts evaluation to nodes belonging to any of the given layers. @@ -447,7 +414,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn layers(&self, layers: FromIterable) -> PyNodeFilter { - self.0.dyn_layer(layers.to_vec()).into() + Self::wrap(self.0.clone().layer(layers.to_vec())) } /// Matches nodes that have at least one event in the current view. From 00b5a9346fafa0530e9f5ba5ecc06c16c2c522d9 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Mon, 29 Jun 2026 12:01:44 +0200 Subject: [PATCH 097/155] still recursing --- raphtory/src/db/api/view/internal/filter.rs | 4 +- raphtory/src/db/api/view/layer.rs | 2 +- .../graph/views/filter/model/graph_filter.rs | 36 +++++++++++++++++- .../src/db/graph/views/filter/model/mod.rs | 37 +++++++++++++++++-- raphtory/src/db/graph/views/window_graph.rs | 26 ++++++------- raphtory/src/python/filter/node_expr.rs | 1 - 6 files changed, 86 insertions(+), 20 deletions(-) diff --git a/raphtory/src/db/api/view/internal/filter.rs b/raphtory/src/db/api/view/internal/filter.rs index 17379caef4..99f2f6b10b 100644 --- a/raphtory/src/db/api/view/internal/filter.rs +++ b/raphtory/src/db/api/view/internal/filter.rs @@ -6,7 +6,9 @@ use crate::{ pub trait InternalFilter<'graph> { type Graph: GraphView + 'graph; - type Filtered: InternalFilter<'graph, Graph = FilteredGraph>; + type Filtered: InternalFilter<'graph, Graph = FilteredGraph> + + Clone + + 'graph; fn base_graph(&self) -> &Self::Graph; diff --git a/raphtory/src/db/api/view/layer.rs b/raphtory/src/db/api/view/layer.rs index d2812c83cc..c06d2c37ab 100644 --- a/raphtory/src/db/api/view/layer.rs +++ b/raphtory/src/db/api/view/layer.rs @@ -11,7 +11,7 @@ use raphtory_storage::core_ops::CoreGraphOps; /// Trait defining layer operations pub trait LayerOps<'graph> { - type LayeredViewType; + type LayeredViewType: LayerOps<'graph>; /// Return a graph containing only the default edge layer fn default_layer(&self) -> Self::LayeredViewType { 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 67c998fc5c..88fa8826f1 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -2,7 +2,13 @@ use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::filter::{ - model::{windowed_filter::Windowed, InternalViewWrapOps, Wrap}, + model::{ + latest_filter::Latest, + layered_filter::Layered, + snapshot_filter::{SnapshotAt, SnapshotLatest}, + windowed_filter::Windowed, + CreateView, InternalViewWrapOps, Wrap, + }, CreateFilter, }, }, @@ -36,6 +42,10 @@ impl InternalViewWrapOps for GraphFilter { } } +pub trait GraphFilterOps: InternalViewWrapOps + CreateFilter { + type GraphWindow: GraphFilterOps; +} + impl CreateFilter for GraphFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = G; @@ -55,3 +65,27 @@ impl CreateFilter for GraphFilter { Ok(NodeExistsOp::new(graph)) } } + +impl GraphFilterOps for GraphFilter { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for Windowed { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for Layered { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for Latest { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for SnapshotAt { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for SnapshotLatest { + type GraphWindow = Self::Window; +} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 093c4cc77c..6177cbb5ec 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -442,6 +442,7 @@ pub trait EdgeFilterFactory: PropertyFilterFactory + Clone {} use crate::db::graph::views::filter::model::{ edge_expr::ops::{EdgeMetaOp, EdgePropOp}, + graph_filter::GraphFilterOps, node_expr::{CreateOp, DynTemporal, EntityExpr}, }; use edge_expr::EdgeOp; @@ -693,9 +694,39 @@ impl DynInternalViewWrapPropOps for T where { } -pub trait DynViewFilter: DynInternalViewWrapOps + DynCreateFilter + Send + Sync + 'static {} -impl DynViewFilter for T where T: DynInternalViewWrapOps + DynCreateFilter + Send + Sync + 'static -{} +pub trait DynViewFilter: DynCreateFilter + Send + Sync + 'static { + fn dyn_bounds(&self) -> (EventTime, EventTime); + + fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc; +} +impl DynViewFilter for T +where + T: GraphFilterOps + Send + Sync + 'static, +{ + fn dyn_bounds(&self) -> (EventTime, EventTime) { + self.bounds() + } + + fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc { + Arc::new(self.clone().build_window(start, end)) + } +} + +impl InternalViewWrapOps for Arc { + type Window = Self; + + fn bounds(&self) -> (EventTime, EventTime) { + self.deref().dyn_bounds() + } + + fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { + self.deref().dyn_build_window(start, end) + } +} + +impl GraphFilterOps for DynView { + type GraphWindow = Self; +} pub type DynView = Arc; diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index fbc1feef2d..c6299f08a2 100644 --- a/raphtory/src/db/graph/views/window_graph.rs +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -168,21 +168,21 @@ impl WindowedGraph { } } -impl<'graph, G: GraphViewOps<'graph>> Immutable for WindowedGraph {} +impl Immutable for WindowedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritCoreGraphOps for WindowedGraph {} +impl InheritCoreGraphOps for WindowedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritStorageOps for WindowedGraph {} +impl InheritStorageOps for WindowedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritMaterialize for WindowedGraph {} +impl InheritMaterialize for WindowedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritMetadataPropertiesOps for WindowedGraph {} +impl InheritMetadataPropertiesOps for WindowedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritNodePropertySchemaOps for WindowedGraph {} +impl InheritNodePropertySchemaOps for WindowedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritEdgePropertySchemaOps for WindowedGraph {} +impl InheritEdgePropertySchemaOps for WindowedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritLayerOps for WindowedGraph {} +impl InheritLayerOps for WindowedGraph {} impl<'graph, G: GraphViewOps<'graph>> ListOps for WindowedGraph { fn node_list(&self) -> NodeList { @@ -206,7 +206,7 @@ impl<'graph, G: GraphViewOps<'graph>> ListOps for WindowedGraph { } } -impl<'graph, G: GraphViewOps<'graph>> InternalNodeFilterOps for WindowedGraph { +impl InternalNodeFilterOps for WindowedGraph { #[inline] fn internal_nodes_filtered(&self) -> bool { self.window_is_empty() || self.graph.internal_nodes_filtered() @@ -239,7 +239,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalNodeFilterOps for WindowedGraph } } -impl<'graph, G: GraphViewOps<'graph>> InternalTemporalPropertyViewOps for WindowedGraph { +impl InternalTemporalPropertyViewOps for WindowedGraph { fn dtype(&self, id: usize) -> PropType { self.graph .graph_props_meta() @@ -274,7 +274,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalTemporalPropertyViewOps for Window } } -impl<'graph, G: GraphViewOps<'graph>> InternalTemporalPropertiesOps for WindowedGraph { +impl InternalTemporalPropertiesOps for WindowedGraph { fn get_temporal_prop_id(&self, name: &str) -> Option { self.graph .get_temporal_prop_id(name) @@ -294,7 +294,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalTemporalPropertiesOps for Windowed } } -impl<'graph, G: GraphViewOps<'graph>> GraphTimeSemanticsOps for WindowedGraph { +impl GraphTimeSemanticsOps for WindowedGraph { fn node_time_semantics(&self) -> TimeSemantics { self.graph.node_time_semantics().window(self.window_bound()) } @@ -493,7 +493,7 @@ impl InternalExplodedEdgeFilterOps for WindowedGraph { /// graph.add_edge(1, 2, 3, NO_PROPS, None).unwrap(); /// let windowed_graph = graph.window(0, 1); /// ``` -impl<'graph, G: GraphViewOps<'graph>> WindowedGraph { +impl WindowedGraph { /// Create a new windowed graph /// /// # Arguments diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 6208073508..3d33d97bc6 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -182,7 +182,6 @@ impl InternalViewWrapOps for Arc { impl DynNodeFilterFactory for T where T: NodeFilterFactory + Send + Sync + 'static, - T::Window: NodeFilterFactory + Send + Sync + 'static, { fn dyn_id(&self) -> Arc { Arc::new(self.id()) From 5f348b1ec78fffadf6e4994ada8cf48539925feb Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:54:26 +0100 Subject: [PATCH 098/155] narrow EntityExprFilterOps to EntityExprBuilder marker to avoid stdlib method collisions on primitive EntityExpr types --- .../graph/views/filter/model/edge_filter.rs | 4 +++- .../src/db/graph/views/filter/model/mod.rs | 20 +++++++++++++++++-- .../views/filter/model/node_expr/dyn_expr.rs | 6 ++++-- .../views/filter/model/node_expr/exprs.rs | 16 +++++++++++++-- .../views/filter/model/node_expr/filters.rs | 10 +++++++++- .../graph/views/filter/model/node_expr/mod.rs | 10 ++++++++++ 6 files changed, 58 insertions(+), 8 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 cf07c32927..242c8344df 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -11,7 +11,7 @@ use crate::{ exploded_edge_filter::ExplodedEdgeFilter, latest_filter::Latest, layered_filter::Layered, - node_expr::{CreateOp, EntityExpr}, + node_expr::{CreateOp, EntityExpr, EntityExprBuilder}, node_filter::{builders::InternalNodeFilterBuilder, NodeFilter}, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, @@ -193,6 +193,8 @@ impl } } +impl EntityExprBuilder for EdgeEndpointWrapper {} + impl EntityExpr for EdgeEndpointWrapper { type Marker = EdgeFilter; fn entity(&self) -> Self::Marker { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 6177cbb5ec..4da67c9005 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -443,7 +443,7 @@ pub trait EdgeFilterFactory: PropertyFilterFactory + Clone {} use crate::db::graph::views::filter::model::{ edge_expr::ops::{EdgeMetaOp, EdgePropOp}, graph_filter::GraphFilterOps, - node_expr::{CreateOp, DynTemporal, EntityExpr}, + node_expr::{CreateOp, DynTemporal, EntityExpr, EntityExprBuilder}, }; use edge_expr::EdgeOp; use raphtory_api::core::entities::properties::prop::PropType; @@ -951,7 +951,23 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { } } -impl EntityExprFilterOps for E {} +impl EntityExprFilterOps for E {} + +// Concrete LHS markers +impl EntityExprBuilder for NodeFilter {} +impl EntityExprBuilder for EdgeFilter {} +impl EntityExprBuilder for ExplodedEdgeFilter {} + +// Property / metadata accessors +impl EntityExprBuilder for PropertyExpr {} +impl EntityExprBuilder for MetadataExpr {} + +// View modifiers preserve builder-ness +impl EntityExprBuilder for Windowed {} +impl EntityExprBuilder for Layered {} +impl EntityExprBuilder for Latest {} +impl EntityExprBuilder for SnapshotAt {} +impl EntityExprBuilder for SnapshotLatest {} /// Reject ordering operators on boolean properties. //. TODO: Also check if both the types are comparable. diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 18127dba3f..1f2e858568 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -33,8 +33,8 @@ use crate::{ graph::views::filter::model::{ edge_expr::EdgeOp, node_expr::{ - AvgExpr, CreateOp, EntityAggOps, EntityExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, - MinExpr, SumExpr, + AvgExpr, CreateOp, EntityAggOps, EntityExpr, EntityExprBuilder, FirstExpr, + LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr, }, CreateView, EntityMarker, PropertyExpr, }, @@ -110,6 +110,8 @@ impl EntityExpr for Arc { } } +impl EntityExprBuilder for Arc {} + impl CreateOp for Arc { fn create_node_op<'g, G: GraphView + 'g>( &self, 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 dca375e463..73d2547bf1 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 @@ -66,8 +66,8 @@ use super::{ AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, SumNodeOp, TemporalNodePropOp, }, - AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, CreateOp, EntityExpr, FirstEdgeOp, - LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, SumEdgeOp, + AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, CreateOp, EntityExpr, EntityExprBuilder, + FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, SumEdgeOp, }; use crate::{ db::{ @@ -121,6 +121,8 @@ impl EntityExpr for Id { } } +impl EntityExprBuilder for Id {} + impl CreateOp for Id { fn create_node_op<'g, G: GraphView + 'g>( &self, @@ -159,6 +161,8 @@ impl EntityExpr for Name { } } +impl EntityExprBuilder for Name {} + impl CreateOp for Name { fn create_node_op<'g, G: GraphView + 'g>( &self, @@ -180,6 +184,8 @@ impl EntityExpr for Type { } } +impl EntityExprBuilder for Type {} + impl CreateOp for Type { fn create_node_op<'g, G: GraphView + 'g>( &self, @@ -449,6 +455,8 @@ impl EntityExpr for DegreeExpr } } +impl EntityExprBuilder for DegreeExpr {} + impl CreateOp for DegreeExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, @@ -491,6 +499,8 @@ impl EntityExpr for TemporalPropE } } +impl EntityExprBuilder for TemporalPropExpr {} + impl EntityAggOps for TemporalPropExpr { fn sum(self) -> SumExpr { SumExpr(self) @@ -588,6 +598,8 @@ macro_rules! impl_agg_expr { } } + impl EntityExprBuilder for $expr {} + impl EntityAggOps for $expr { fn sum(self) -> SumExpr { SumExpr(self) 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 188b9c115e..ed83c7cd5a 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 @@ -45,7 +45,7 @@ use super::{ BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, ListAwareUnaryNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, - ConstFilter, CreateOp, EntityExpr, Marker, + ConstFilter, CreateOp, EntityExpr, EntityExprBuilder, Marker, }; use crate::{ db::{ @@ -127,6 +127,8 @@ impl BinaryCmpExpr { impl ComposableFilter for BinaryCmpExpr {} +impl EntityExprBuilder for BinaryCmpExpr {} + impl EntityExpr for BinaryCmpExpr { type Marker = E; fn entity(&self) -> Self::Marker { @@ -277,6 +279,8 @@ impl UnaryExpr { impl ComposableFilter for UnaryExpr {} +impl EntityExprBuilder for UnaryExpr {} + impl EntityExpr for UnaryExpr { type Marker = M; fn entity(&self) -> Self::Marker { @@ -419,6 +423,8 @@ impl StringExpr { impl ComposableFilter for StringExpr {} +impl EntityExprBuilder for StringExpr {} + impl EntityExpr for StringExpr { type Marker = M; fn entity(&self) -> Self::Marker { @@ -548,6 +554,8 @@ impl PropValueSetExpr { impl ComposableFilter for PropValueSetExpr {} +impl EntityExprBuilder for PropValueSetExpr {} + impl EntityExpr for PropValueSetExpr { type Marker = M; fn entity(&self) -> Self::Marker { 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 index fad2a185af..465a93117d 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -90,3 +90,13 @@ pub trait EntityExpr: Clone + Send + Sync + 'static { true } } + +/// Marker for types that initiate a filter expression chain (LHS receiver for +/// `.eq` / `.gt` / `.contains` / ...). +/// +/// Scoped narrowly (not blanket-impl'd for every `EntityExpr`) to avoid name +/// collisions with stdlib methods like `str::contains` / `PartialOrd::gt` on +/// primitive `EntityExpr` types (`String`, `&str`, `usize`, numerics, `Prop`). +/// +/// Mirrors the same trick used by `EntityAggOps` for `min`/`max`/`sum`. +pub trait EntityExprBuilder: EntityExpr {} From 5e06c0618dafd1ff30ba6c8295a254c84d29717d Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:48:24 +0100 Subject: [PATCH 099/155] expr for gql --- raphtory-graphql/src/model/graph/edge.rs | 4 +- raphtory-graphql/src/model/graph/edges.rs | 8 +- raphtory-graphql/src/model/graph/filtering.rs | 286 ++++++++---------- raphtory-graphql/src/model/graph/graph.rs | 13 +- raphtory-graphql/src/model/graph/node.rs | 21 +- raphtory-graphql/src/model/graph/nodes.rs | 6 +- .../src/model/graph/path_from_node.rs | 6 +- .../filter/model/is_active_edge_filter.rs | 6 + .../filter/model/is_active_node_filter.rs | 6 + .../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 + 12 files changed, 178 insertions(+), 196 deletions(-) diff --git a/raphtory-graphql/src/model/graph/edge.rs b/raphtory-graphql/src/model/graph/edge.rs index 29fd37c6d4..71ae4dc741 100644 --- a/raphtory-graphql/src/model/graph/edge.rs +++ b/raphtory-graphql/src/model/graph/edge.rs @@ -17,7 +17,7 @@ use raphtory::{ core::utils::time::TryIntoInterval, db::{ api::view::{DynamicGraph, EdgeViewOps, Filter, IntoDynamic, StaticGraphViewOps}, - graph::{edge::EdgeView, views::filter::model::edge_filter::CompositeEdgeFilter}, + graph::{edge::EdgeView, views::filter::model::DynFilter}, }, errors::GraphError, prelude::{LayerOps, TimeOps}, @@ -489,7 +489,7 @@ impl GqlEdge { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeEdgeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered = self_clone.ee.filter(filter)?; Ok(self_clone.update(filtered.into_dynamic())) }) diff --git a/raphtory-graphql/src/model/graph/edges.rs b/raphtory-graphql/src/model/graph/edges.rs index 084e2d37c3..41117f6052 100644 --- a/raphtory-graphql/src/model/graph/edges.rs +++ b/raphtory-graphql/src/model/graph/edges.rs @@ -28,9 +28,7 @@ use raphtory_api::{core::utils::time::IntoTime, iter::IntoDynBoxed}; use std::{cmp::Ordering, sync::Arc}; use crate::model::graph::filtering::GqlEdgeFilter; -use raphtory::db::{ - api::view::Filter, graph::views::filter::model::edge_filter::CompositeEdgeFilter, -}; +use raphtory::db::{api::view::Filter, graph::views::filter::model::DynFilter}; /// A lazy collection of edges from a graph view. Supports the usual view /// transforms (window, layer, filter, ...), plus edge-specific ones like @@ -478,7 +476,7 @@ impl GqlEdges { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeEdgeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered = self_clone.ee.filter(filter)?; Ok(self_clone.update(filtered.into_dyn())) }) @@ -511,7 +509,7 @@ impl GqlEdges { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeEdgeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered = self_clone.ee.select(filter)?; Ok(self_clone.update(filtered)) }) diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 829767bd27..bbd6dbbf00 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -1,7 +1,4 @@ -use crate::model::{ - graph::{node_id::GqlNodeId, property::Value, timeindex::GqlTimeInput}, - plugins::operation, -}; +use crate::model::graph::{node_id::GqlNodeId, property::Value, timeindex::GqlTimeInput}; use async_graphql::dynamic::ValueAccessor; use dynamic_graphql::{ internal::{ @@ -10,27 +7,25 @@ use dynamic_graphql::{ Enum, InputObject, OneOfInput, }; use raphtory::{ - db::{ - api::{state::ops::Degree, view::internal::filtered_edge}, - graph::views::filter::model::{ - degree_filter::DegreeFilter, - edge_filter::{CompositeEdgeFilter, EdgeFilter}, - filter::{FieldFilterValue, Filter}, - filter_operator::FilterOperator, - graph_filter::GraphFilter, - 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 as LatestWrap, - layered_filter::Layered, - node_filter::{CompositeNodeFilter, NodeFilter}, - property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, - snapshot_filter::{SnapshotAt as SnapshotAtWrap, SnapshotLatest as SnapshotLatestWrap}, - windowed_filter::Windowed, - ComposableFilter, DynFilter, DynView, NoFilter, ViewWrapOps, - }, + db::graph::views::filter::model::{ + degree_filter::DegreeFilter, + edge_filter::{EdgeEndpointWrapper, EdgeFilter, Endpoint}, + filter::{FieldFilterValue, Filter}, + filter_operator::FilterOperator, + graph_filter::GraphFilter, + 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 as LatestWrap, + layered_filter::Layered, + node_filter::{NodeFilter, NodeIdFilter, NodeNameFilter, NodeTypeFilter}, + not_filter::NotFilter, + property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, + snapshot_filter::{SnapshotAt as SnapshotAtWrap, SnapshotLatest as SnapshotLatestWrap}, + windowed_filter::Windowed, + ComposableFilter, DynFilter, DynView, NoFilter, ViewWrapOps, }, errors::GraphError, }; @@ -1397,7 +1392,7 @@ fn build_property_filter_from_condition_with_entity Result { +) -> Result { use PropCondition::*; match cond { @@ -1409,7 +1404,7 @@ fn build_node_filter_from_prop_condition( let mut acc = build_node_filter_from_prop_condition(prop_ref.clone(), first)?; for c in it { let next = build_node_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeNodeFilter::And(Box::new(acc), Box::new(next)); + acc = Arc::new(acc.and(next)) as DynFilter; } Ok(acc) } @@ -1421,24 +1416,24 @@ fn build_node_filter_from_prop_condition( let mut acc = build_node_filter_from_prop_condition(prop_ref.clone(), first)?; for c in it { let next = build_node_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeNodeFilter::Or(Box::new(acc), Box::new(next)); + acc = Arc::new(acc.or(next)) as DynFilter; } Ok(acc) } Not(inner) => { let nf = build_node_filter_from_prop_condition(prop_ref, inner)?; - Ok(CompositeNodeFilter::Not(Box::new(nf))) + Ok(Arc::new(NotFilter(nf)) as DynFilter) } _ => { let pf = build_property_filter_from_condition_with_entity::( prop_ref, cond, NodeFilter, )?; - Ok(CompositeNodeFilter::Property(pf)) + Ok(Arc::new(pf) as DynFilter) } } } -impl TryFrom for CompositeNodeFilter { +impl TryFrom for DynFilter { type Error = GraphError; fn try_from(filter: GqlNodeFilter) -> Result { match filter { @@ -1446,31 +1441,30 @@ impl TryFrom for CompositeNodeFilter { let field = node.field; let (field_name, field_value, operator) = translate_node_field_where(node.field, &node.where_)?; - let filter = Filter { + let f = Filter { field_name, field_value, operator, }; Ok(match field { - NodeField::NodeId => CompositeNodeFilter::Id(filter), - NodeField::NodeName => CompositeNodeFilter::Name(filter), - NodeField::NodeType => CompositeNodeFilter::Type(filter), + NodeField::NodeId => Arc::new(NodeIdFilter(f)) as DynFilter, + NodeField::NodeName => Arc::new(NodeNameFilter(f)) as DynFilter, + NodeField::NodeType => Arc::new(NodeTypeFilter(f)) as DynFilter, }) } GqlNodeFilter::Degree(degree) => { let core_direction: Direction = degree.direction.into(); - let field_name: String = degree.direction.into(); let mut ops = Vec::new(); peel_prop_wrappers_and_collect_ops(°ree.where_, &mut ops); let (operator, value) = translate_prop_leaf_to_filter(&field_name, °ree.where_)?; - Ok(CompositeNodeFilter::Degree(DegreeFilter { + Ok(Arc::new(DegreeFilter { direction: core_direction, operator, value, ops, - })) + }) as DynFilter) } GqlNodeFilter::Property(prop) => { let prop_ref = PropertyRef::Property(prop.name.clone()); @@ -1485,100 +1479,85 @@ impl TryFrom for CompositeNodeFilter { build_node_filter_from_prop_condition(prop_ref, &prop.where_) } GqlNodeFilter::And(and_filters) => { - let mut iter = and_filters.into_iter().map(TryInto::try_into); + let mut iter = and_filters.into_iter().map(DynFilter::try_from); let first = iter.next().ok_or_else(|| { GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) })??; - Ok(iter.try_fold(first, |acc, next| { + iter.try_fold(first, |acc, next| { let n = next?; - Ok::<_, GraphError>(CompositeNodeFilter::And(Box::new(acc), Box::new(n))) - })?) + Ok::<_, GraphError>(Arc::new(acc.and(n)) as DynFilter) + }) } GqlNodeFilter::Or(or_filters) => { - let mut iter = or_filters.into_iter().map(TryInto::try_into); + let mut iter = or_filters.into_iter().map(DynFilter::try_from); let first = iter.next().ok_or_else(|| { GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) })??; - Ok(iter.try_fold(first, |acc, next| { + iter.try_fold(first, |acc, next| { let n = next?; - Ok::<_, GraphError>(CompositeNodeFilter::Or(Box::new(acc), Box::new(n))) - })?) + Ok::<_, GraphError>(Arc::new(acc.or(n)) as DynFilter) + }) } GqlNodeFilter::Not(not_filters) => { - let inner = CompositeNodeFilter::try_from(not_filters.deref().clone())?; - Ok(CompositeNodeFilter::Not(Box::new(inner))) + let inner: DynFilter = not_filters.deref().clone().try_into()?; + Ok(Arc::new(NotFilter(inner)) as DynFilter) } GqlNodeFilter::Window(w) => { - let inner: CompositeNodeFilter = w.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::Windowed(Box::new(Windowed::new( - w.start.into_time(), - w.end.into_time(), - inner, - )))) + let inner: DynFilter = w.expr.deref().clone().try_into()?; + Ok(Arc::new(Windowed::new(w.start.into_time(), w.end.into_time(), inner)) + as DynFilter) } GqlNodeFilter::At(t) => { - let inner: CompositeNodeFilter = t.expr.deref().clone().try_into()?; + let inner: DynFilter = t.expr.deref().clone().try_into()?; let et = t.time.into_time(); - Ok(CompositeNodeFilter::Windowed(Box::new(Windowed::new( + Ok(Arc::new(Windowed::new( et, EventTime::end(et.t().saturating_add(1)), inner, - )))) + )) as DynFilter) } GqlNodeFilter::Before(t) => { - let inner: CompositeNodeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::Windowed(Box::new(Windowed::new( + let inner: DynFilter = t.expr.deref().clone().try_into()?; + Ok(Arc::new(Windowed::new( EventTime::start(i64::MIN), EventTime::end(t.time.t()), inner, - )))) + )) as DynFilter) } GqlNodeFilter::After(t) => { - let inner: CompositeNodeFilter = t.expr.deref().clone().try_into()?; + let inner: DynFilter = t.expr.deref().clone().try_into()?; let start = EventTime::start(t.time.t().saturating_add(1)); - Ok(CompositeNodeFilter::Windowed(Box::new(Windowed::new( - start, - EventTime::end(i64::MAX), - inner, - )))) + Ok(Arc::new(Windowed::new(start, EventTime::end(i64::MAX), inner)) as DynFilter) } GqlNodeFilter::Latest(u) => { - let inner: CompositeNodeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::Latest(Box::new(LatestWrap::new( - inner, - )))) + let inner: DynFilter = u.expr.deref().clone().try_into()?; + Ok(Arc::new(LatestWrap::new(inner)) as DynFilter) } GqlNodeFilter::SnapshotAt(t) => { - let inner: CompositeNodeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::SnapshotAt(Box::new( - SnapshotAtWrap::new(t.time.into_time(), inner), - ))) + let inner: DynFilter = t.expr.deref().clone().try_into()?; + Ok(Arc::new(SnapshotAtWrap::new(t.time.into_time(), inner)) as DynFilter) } GqlNodeFilter::SnapshotLatest(u) => { - let inner: CompositeNodeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::SnapshotLatest(Box::new( - SnapshotLatestWrap::new(inner), - ))) + let inner: DynFilter = u.expr.deref().clone().try_into()?; + Ok(Arc::new(SnapshotLatestWrap::new(inner)) as DynFilter) } GqlNodeFilter::Layers(l) => { let layer = Layer::from(l.names.clone()); - let inner: CompositeNodeFilter = l.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::Layered(Box::new(Layered::new( - layer, inner, - )))) + let inner: DynFilter = l.expr.deref().clone().try_into()?; + Ok(Arc::new(Layered::new(layer, inner)) as DynFilter) } - GqlNodeFilter::IsActive(true) => Ok(CompositeNodeFilter::IsActiveNode(IsActiveNode)), - GqlNodeFilter::IsActive(false) => Ok(CompositeNodeFilter::Not(Box::new( - CompositeNodeFilter::IsActiveNode(IsActiveNode), - ))), + GqlNodeFilter::IsActive(true) => Ok(Arc::new(IsActiveNode::new(NodeFilter)) as DynFilter), + GqlNodeFilter::IsActive(false) => { + Ok(Arc::new(NotFilter(IsActiveNode::new(NodeFilter))) as DynFilter) + } } } } @@ -1586,7 +1565,7 @@ impl TryFrom for CompositeNodeFilter { fn build_edge_filter_from_prop_condition( prop_ref: PropertyRef, cond: &PropCondition, -) -> Result { +) -> Result { use PropCondition::*; match cond { @@ -1598,7 +1577,7 @@ fn build_edge_filter_from_prop_condition( let mut acc = build_edge_filter_from_prop_condition(prop_ref.clone(), first)?; for c in it { let next = build_edge_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeEdgeFilter::And(Box::new(acc), Box::new(next)); + acc = Arc::new(acc.and(next)) as DynFilter; } Ok(acc) } @@ -1610,34 +1589,34 @@ fn build_edge_filter_from_prop_condition( let mut acc = build_edge_filter_from_prop_condition(prop_ref.clone(), first)?; for c in it { let next = build_edge_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeEdgeFilter::Or(Box::new(acc), Box::new(next)); + acc = Arc::new(acc.or(next)) as DynFilter; } Ok(acc) } Not(inner) => { let ef = build_edge_filter_from_prop_condition(prop_ref, inner)?; - Ok(CompositeEdgeFilter::Not(Box::new(ef))) + Ok(Arc::new(NotFilter(ef)) as DynFilter) } _ => { let pf = build_property_filter_from_condition_with_entity::( prop_ref, cond, EdgeFilter, )?; - Ok(CompositeEdgeFilter::Property(pf)) + Ok(Arc::new(pf) as DynFilter) } } } -impl TryFrom for CompositeEdgeFilter { +impl TryFrom for DynFilter { type Error = GraphError; fn try_from(filter: GqlEdgeFilter) -> Result { match filter { GqlEdgeFilter::Src(nf) => { - let nf: CompositeNodeFilter = nf.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Src(nf)) + let inner: DynFilter = nf.deref().clone().try_into()?; + Ok(Arc::new(EdgeEndpointWrapper::new(inner, Endpoint::Src)) as DynFilter) } GqlEdgeFilter::Dst(nf) => { - let nf: CompositeNodeFilter = nf.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Dst(nf)) + let inner: DynFilter = nf.deref().clone().try_into()?; + Ok(Arc::new(EdgeEndpointWrapper::new(inner, Endpoint::Dst)) as DynFilter) } GqlEdgeFilter::Property(prop) => { let prop_ref = PropertyRef::Property(prop.name.clone()); @@ -1652,117 +1631,104 @@ impl TryFrom for CompositeEdgeFilter { build_edge_filter_from_prop_condition(prop_ref, &prop.where_) } GqlEdgeFilter::And(and_filters) => { - let mut iter = and_filters.into_iter().map(TryInto::try_into); + let mut iter = and_filters.into_iter().map(DynFilter::try_from); let first = iter.next().ok_or_else(|| { GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) })??; - Ok(iter.try_fold(first, |acc, next| { + iter.try_fold(first, |acc, next| { let n = next?; - Ok::<_, GraphError>(CompositeEdgeFilter::And(Box::new(acc), Box::new(n))) - })?) + Ok::<_, GraphError>(Arc::new(acc.and(n)) as DynFilter) + }) } GqlEdgeFilter::Or(or_filters) => { - let mut iter = or_filters.into_iter().map(TryInto::try_into); + let mut iter = or_filters.into_iter().map(DynFilter::try_from); let first = iter.next().ok_or_else(|| { GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) })??; - Ok(iter.try_fold(first, |acc, next| { + iter.try_fold(first, |acc, next| { let n = next?; - Ok::<_, GraphError>(CompositeEdgeFilter::Or(Box::new(acc), Box::new(n))) - })?) + Ok::<_, GraphError>(Arc::new(acc.or(n)) as DynFilter) + }) } GqlEdgeFilter::Not(not_filters) => { - let inner = CompositeEdgeFilter::try_from(not_filters.deref().clone())?; - Ok(CompositeEdgeFilter::Not(Box::new(inner))) + let inner: DynFilter = not_filters.deref().clone().try_into()?; + Ok(Arc::new(NotFilter(inner)) as DynFilter) } GqlEdgeFilter::Window(w) => { - let inner: CompositeEdgeFilter = w.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Windowed(Box::new(Windowed::new( - w.start.into_time(), - w.end.into_time(), - inner, - )))) + let inner: DynFilter = w.expr.deref().clone().try_into()?; + Ok(Arc::new(Windowed::new(w.start.into_time(), w.end.into_time(), inner)) + as DynFilter) } GqlEdgeFilter::At(t) => { - let inner: CompositeEdgeFilter = t.expr.deref().clone().try_into()?; + let inner: DynFilter = t.expr.deref().clone().try_into()?; let et = t.time.into_time(); - Ok(CompositeEdgeFilter::Windowed(Box::new(Windowed::new( + Ok(Arc::new(Windowed::new( et, EventTime::end(et.t().saturating_add(1)), inner, - )))) + )) as DynFilter) } GqlEdgeFilter::Before(t) => { - let inner: CompositeEdgeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Windowed(Box::new(Windowed::new( + let inner: DynFilter = t.expr.deref().clone().try_into()?; + Ok(Arc::new(Windowed::new( EventTime::start(i64::MIN), EventTime::end(t.time.t()), inner, - )))) + )) as DynFilter) } GqlEdgeFilter::After(t) => { - let inner: CompositeEdgeFilter = t.expr.deref().clone().try_into()?; + let inner: DynFilter = t.expr.deref().clone().try_into()?; let start = EventTime::start(t.time.t().saturating_add(1)); - Ok(CompositeEdgeFilter::Windowed(Box::new(Windowed::new( - start, - EventTime::end(i64::MAX), - inner, - )))) + Ok(Arc::new(Windowed::new(start, EventTime::end(i64::MAX), inner)) as DynFilter) } GqlEdgeFilter::Latest(u) => { - let inner: CompositeEdgeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Latest(Box::new(LatestWrap::new( - inner, - )))) + let inner: DynFilter = u.expr.deref().clone().try_into()?; + Ok(Arc::new(LatestWrap::new(inner)) as DynFilter) } GqlEdgeFilter::SnapshotAt(t) => { - let inner: CompositeEdgeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::SnapshotAt(Box::new( - SnapshotAtWrap::new(t.time.into_time(), inner), - ))) + let inner: DynFilter = t.expr.deref().clone().try_into()?; + Ok(Arc::new(SnapshotAtWrap::new(t.time.into_time(), inner)) as DynFilter) } GqlEdgeFilter::SnapshotLatest(u) => { - let inner: CompositeEdgeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::SnapshotLatest(Box::new( - SnapshotLatestWrap::new(inner), - ))) + let inner: DynFilter = u.expr.deref().clone().try_into()?; + Ok(Arc::new(SnapshotLatestWrap::new(inner)) as DynFilter) } GqlEdgeFilter::Layers(l) => { let layer = Layer::from(l.names.clone()); - let inner: CompositeEdgeFilter = l.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Layered(Box::new(Layered::new( - layer, inner, - )))) + let inner: DynFilter = l.expr.deref().clone().try_into()?; + Ok(Arc::new(Layered::new(layer, inner)) as DynFilter) } - GqlEdgeFilter::IsActive(true) => Ok(CompositeEdgeFilter::IsActiveEdge(IsActiveEdge)), - GqlEdgeFilter::IsActive(false) => Ok(CompositeEdgeFilter::Not(Box::new( - CompositeEdgeFilter::IsActiveEdge(IsActiveEdge), - ))), + GqlEdgeFilter::IsActive(true) => Ok(Arc::new(IsActiveEdge::new(EdgeFilter)) as DynFilter), + GqlEdgeFilter::IsActive(false) => { + Ok(Arc::new(NotFilter(IsActiveEdge::new(EdgeFilter))) as DynFilter) + } - GqlEdgeFilter::IsValid(true) => Ok(CompositeEdgeFilter::IsValidEdge(IsValidEdge)), - GqlEdgeFilter::IsValid(false) => Ok(CompositeEdgeFilter::Not(Box::new( - CompositeEdgeFilter::IsValidEdge(IsValidEdge), - ))), + GqlEdgeFilter::IsValid(true) => Ok(Arc::new(IsValidEdge::new(EdgeFilter)) as DynFilter), + GqlEdgeFilter::IsValid(false) => { + Ok(Arc::new(NotFilter(IsValidEdge::new(EdgeFilter))) as DynFilter) + } - GqlEdgeFilter::IsDeleted(true) => Ok(CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge)), - GqlEdgeFilter::IsDeleted(false) => Ok(CompositeEdgeFilter::Not(Box::new( - CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge), - ))), + GqlEdgeFilter::IsDeleted(true) => { + Ok(Arc::new(IsDeletedEdge::new(EdgeFilter)) as DynFilter) + } + GqlEdgeFilter::IsDeleted(false) => { + Ok(Arc::new(NotFilter(IsDeletedEdge::new(EdgeFilter))) as DynFilter) + } GqlEdgeFilter::IsSelfLoop(true) => { - Ok(CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) + Ok(Arc::new(IsSelfLoopEdge::new(EdgeFilter)) as DynFilter) + } + GqlEdgeFilter::IsSelfLoop(false) => { + Ok(Arc::new(NotFilter(IsSelfLoopEdge::new(EdgeFilter))) as DynFilter) } - GqlEdgeFilter::IsSelfLoop(false) => Ok(CompositeEdgeFilter::Not(Box::new( - CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge), - ))), } } } @@ -1859,12 +1825,8 @@ impl TryFrom for DynFilter { fn try_from(value: GraphRowFilter) -> Result { let filter = match value { - GraphRowFilter::Node(filter) => { - Arc::new(CompositeNodeFilter::try_from(filter)?) as DynFilter - } - GraphRowFilter::Edge(filter) => { - Arc::new(CompositeEdgeFilter::try_from(filter)?) as DynFilter - } + GraphRowFilter::Node(filter) => DynFilter::try_from(filter)?, + GraphRowFilter::Edge(filter) => DynFilter::try_from(filter)?, GraphRowFilter::Graph(filter) => DynView::try_from(filter)?, GraphRowFilter::And(filters) => { let mut filters = filters.into_iter().map(DynFilter::try_from); diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index 984b7836a9..277327d78c 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -38,10 +38,7 @@ use raphtory::{ }, graph::{ node::NodeView, - views::filter::model::{ - edge_filter::CompositeEdgeFilter, graph_filter::GraphFilter, - node_filter::CompositeNodeFilter, DynView, - }, + views::filter::model::{graph_filter::GraphFilter, DynFilter, DynView}, }, }, errors::GraphError, @@ -545,7 +542,7 @@ impl GqlGraph { let nn = self.graph.nodes(); if let Some(sel) = select { - let nf: CompositeNodeFilter = sel.try_into()?; + let nf: DynFilter = sel.try_into()?; let narrowed = blocking_compute({ let nn_clone = nn.clone(); move || nn_clone.select(nf) @@ -580,7 +577,7 @@ impl GqlGraph { let base = self.graph.edges_unlocked(); if let Some(sel) = select { - let ef: CompositeEdgeFilter = sel.try_into()?; + let ef: DynFilter = sel.try_into()?; let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlEdges::new(narrowed)); } @@ -740,7 +737,7 @@ impl GqlGraph { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeNodeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered_graph = self_clone.graph.filter(filter)?; Ok(GqlGraph::new( self_clone.path.clone(), @@ -760,7 +757,7 @@ impl GqlGraph { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeEdgeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered_graph = self_clone.graph.filter(filter)?; Ok(GqlGraph::new( self_clone.path.clone(), diff --git a/raphtory-graphql/src/model/graph/node.rs b/raphtory-graphql/src/model/graph/node.rs index 7b429480f5..f7d5820b5e 100644 --- a/raphtory-graphql/src/model/graph/node.rs +++ b/raphtory-graphql/src/model/graph/node.rs @@ -22,12 +22,7 @@ use raphtory::{ properties::dyn_props::DynProperties, view::{filter_ops::NodeSelect, Filter, *}, }, - graph::{ - node::NodeView, - views::filter::model::{ - edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, - }, - }, + graph::{node::NodeView, views::filter::model::DynFilter}, }, errors::GraphError, prelude::NodeStateOps, @@ -425,7 +420,7 @@ impl GqlNode { async fn edges(&self, select: Option) -> Result { let base = self.vv.edges(); if let Some(sel) = select { - let ef: CompositeEdgeFilter = sel.try_into()?; + let ef: DynFilter = sel.try_into()?; let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlEdges::new(narrowed)); } @@ -436,7 +431,7 @@ impl GqlNode { async fn out_edges(&self, select: Option) -> Result { let base = self.vv.out_edges(); if let Some(sel) = select { - let ef: CompositeEdgeFilter = sel.try_into()?; + let ef: DynFilter = sel.try_into()?; let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlEdges::new(narrowed)); } @@ -447,7 +442,7 @@ impl GqlNode { async fn in_edges(&self, select: Option) -> Result { let base = self.vv.in_edges(); if let Some(sel) = select { - let ef: CompositeEdgeFilter = sel.try_into()?; + let ef: DynFilter = sel.try_into()?; let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlEdges::new(narrowed)); } @@ -461,7 +456,7 @@ impl GqlNode { ) -> Result { let base = self.vv.neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf: DynFilter = expr.try_into()?; let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromNode::new(narrowed)); } @@ -475,7 +470,7 @@ impl GqlNode { ) -> Result { let base = self.vv.in_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf: DynFilter = expr.try_into()?; let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromNode::new(narrowed)); } @@ -489,7 +484,7 @@ impl GqlNode { ) -> Result { let base = self.vv.out_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf: DynFilter = expr.try_into()?; let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromNode::new(narrowed)); } @@ -499,7 +494,7 @@ impl GqlNode { async fn filter(&self, expr: GqlNodeFilter) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeNodeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered = self_clone.vv.filter(filter)?; Ok(self_clone.update(filtered.into_dynamic())) }) diff --git a/raphtory-graphql/src/model/graph/nodes.rs b/raphtory-graphql/src/model/graph/nodes.rs index 515f6a0c12..5042a35134 100644 --- a/raphtory-graphql/src/model/graph/nodes.rs +++ b/raphtory-graphql/src/model/graph/nodes.rs @@ -24,7 +24,7 @@ use raphtory::{ }, graph::{ nodes::{IntoDynNodes, Nodes}, - views::filter::model::node_filter::CompositeNodeFilter, + views::filter::model::DynFilter, }, }, errors::GraphError, @@ -481,7 +481,7 @@ impl GqlNodes { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeNodeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered = self_clone.nn.filter(filter)?; Ok(self_clone.update(filtered.into_dyn())) }) @@ -514,7 +514,7 @@ impl GqlNodes { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeNodeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered = self_clone.nn.select(filter)?; Ok(self_clone.update(filtered.into_dyn())) }) diff --git a/raphtory-graphql/src/model/graph/path_from_node.rs b/raphtory-graphql/src/model/graph/path_from_node.rs index 4a41c4a573..35392f65fc 100644 --- a/raphtory-graphql/src/model/graph/path_from_node.rs +++ b/raphtory-graphql/src/model/graph/path_from_node.rs @@ -15,7 +15,7 @@ use raphtory::{ core::utils::time::TryIntoInterval, db::{ api::view::{filter_ops::NodeSelect, DynamicGraph, Filter}, - graph::{path::PathFromNode, views::filter::model::CompositeNodeFilter}, + graph::{path::PathFromNode, views::filter::model::DynFilter}, }, errors::GraphError, prelude::*, @@ -387,7 +387,7 @@ impl GqlPathFromNode { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeNodeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered = self_clone.nn.filter(filter)?; Ok(self_clone.update(filtered.into_dyn())) }) @@ -417,7 +417,7 @@ impl GqlPathFromNode { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeNodeFilter = expr.try_into()?; + let filter: DynFilter = expr.try_into()?; let filtered = self_clone.nn.select(filter)?; Ok(self_clone.update(filtered.into_dyn())) }) 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 6670691fc7..d757960539 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 @@ -25,6 +25,12 @@ pub struct IsActiveEdge { pub(crate) view_expr: E, } +impl IsActiveEdge { + pub fn new(view_expr: E) -> Self { + Self { view_expr } + } +} + impl fmt::Display for IsActiveEdge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_ACTIVE_EDGE") 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 7ca6c09f45..a5507f3741 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 @@ -21,6 +21,12 @@ pub struct IsActiveNode { pub(crate) view_expr: E, } +impl IsActiveNode { + pub fn new(view_expr: E) -> Self { + Self { view_expr } + } +} + impl fmt::Display for IsActiveNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_ACTIVE_NODE") 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 510a3c8406..80a8e21569 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 @@ -25,6 +25,12 @@ pub struct IsDeletedEdge { pub(crate) view_expr: E, } +impl IsDeletedEdge { + pub fn new(view_expr: E) -> Self { + Self { view_expr } + } +} + impl fmt::Display for IsDeletedEdge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_DELETED_EDGE") 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 37d062ddf0..0fc5a0b067 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 @@ -25,6 +25,12 @@ pub struct IsSelfLoopEdge { pub(crate) view_expr: E, } +impl IsSelfLoopEdge { + pub fn new(view_expr: E) -> Self { + Self { view_expr } + } +} + impl fmt::Display for IsSelfLoopEdge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_SELF_LOOP_EDGE") 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 ef0b0ef7b4..9a9fc74290 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 @@ -25,6 +25,12 @@ pub struct IsValidEdge { pub(crate) view_expr: E, } +impl IsValidEdge { + pub fn new(view_expr: E) -> Self { + Self { view_expr } + } +} + impl fmt::Display for IsValidEdge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_VALID_EDGE") From 2e78541a415128e3831f3ad4c2ac80ca82eeb24a Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:29:15 +0100 Subject: [PATCH 100/155] bridge the expression layer onto the db_v4 trait substrate Adapts the expression filter API to the two-parameter CreateFilter shape (base graph + pre-transformed evaluation view), restores the machinery lost to silent auto-merges and merge-spliced impls, splits the factories whose names collided across the two branches (PropertyExprFactory / DegreeExpr vs the builder-path factories), adopts the db_v4 dyn-filter layer, and rebuilds per-expression view construction (CreateView) for the windowed, latest, snapshot and layered wrappers. Expression filters report no composite representation through the fallible TryAsCompositeFilter. --- raphtory/src/db/api/view/layer.rs | 2 +- raphtory/src/db/graph/views/filter/mod.rs | 22 + .../views/filter/model/edge_expr/filters.rs | 224 +++++++-- .../src/db/graph/views/filter/model/filter.rs | 3 + .../views/filter/model/filter_operator.rs | 255 ++++++++++ .../graph/views/filter/model/graph_filter.rs | 33 ++ .../graph/views/filter/model/latest_filter.rs | 15 + .../src/db/graph/views/filter/model/mod.rs | 448 ++++++++++++++---- .../views/filter/model/node_expr/filters.rs | 280 ++++++++--- .../graph/views/filter/model/node_expr/ops.rs | 79 ++- .../filter/model/node_filter/builders.rs | 67 +++ .../views/filter/model/node_filter/mod.rs | 100 +++- .../filter/model/property_filter/builders.rs | 25 +- .../filter/model/property_filter/evaluate.rs | 98 ++++ .../views/filter/model/property_filter/mod.rs | 2 +- .../views/filter/model/property_filter/ops.rs | 5 +- .../views/filter/model/snapshot_filter.rs | 27 ++ .../views/filter/model/windowed_filter.rs | 15 + raphtory/src/lib.rs | 11 +- 19 files changed, 1464 insertions(+), 247 deletions(-) diff --git a/raphtory/src/db/api/view/layer.rs b/raphtory/src/db/api/view/layer.rs index 340509d195..89ce5c3f56 100644 --- a/raphtory/src/db/api/view/layer.rs +++ b/raphtory/src/db/api/view/layer.rs @@ -11,7 +11,7 @@ use raphtory_storage::core_ops::CoreGraphOps; /// Trait defining layer operations pub trait LayerOps<'graph> { - type LayeredViewType: LayerOps<'graph>; + type LayeredViewType; /// Return a graph containing only the default edge layer fn default_layer(&self) -> Self::LayeredViewType { diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 60b35c6d18..0b9e74eeb4 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -60,6 +60,13 @@ impl CreateFilter for Exists { ) -> Result, GraphError> { Ok(NodeExistsOp::new(filtered)) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } pub trait CreateFilter: Sized { @@ -108,6 +115,11 @@ impl CreateFilter for T { = Self where Self: 'graph; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, @@ -130,4 +142,14 @@ 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/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index 1434636b3a..8f2e5eb838 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -39,15 +39,23 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph.clone())?; @@ -63,12 +71,20 @@ where Ok(EdgeExprFilteredGraph::new(graph, op)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, _graph: G, - ) -> Result, GraphError> { + _filtered: F, + ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } impl CreateFilter for BinaryCmpExpr @@ -76,15 +92,23 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = ExplodedEdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph.clone())?; @@ -100,12 +124,20 @@ where Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, _graph: G, - ) -> Result, GraphError> { + _filtered: F, + ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } // ───────────────────────────────────────────────────────────────────────────── @@ -116,52 +148,84 @@ impl CreateFilter for UnaryExpr where E: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let inner = self.expr.create_edge_op(graph.clone())?; let op: Arc + 'graph> = Arc::new(UnaryEdgeOp { inner, op: self.op }); Ok(EdgeExprFilteredGraph::new(graph, op)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, _graph: G, - ) -> Result, GraphError> { + _filtered: F, + ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } impl CreateFilter for UnaryExpr where E: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = ExplodedEdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let inner = self.expr.create_edge_op(graph.clone())?; let op: Arc + 'graph> = Arc::new(UnaryEdgeOp { inner, op: self.op }); Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, _graph: G, - ) -> Result, GraphError> { + _filtered: F, + ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } // ───────────────────────────────────────────────────────────────────────────── @@ -173,14 +237,22 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph.clone())?; validate_string_op(&left.prop_type())?; @@ -192,12 +264,20 @@ where Ok(EdgeExprFilteredGraph::new(graph, op)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, _graph: G, - ) -> Result, GraphError> { + _filtered: F, + ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } impl CreateFilter for StringExpr @@ -205,14 +285,22 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = ExplodedEdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let left = self.left.create_edge_op(graph.clone())?; let right = self.right.create_edge_op(graph.clone())?; validate_string_op(&left.prop_type())?; @@ -224,12 +312,20 @@ where Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, _graph: G, - ) -> Result, GraphError> { + _filtered: F, + ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } // ───────────────────────────────────────────────────────────────────────────── @@ -237,14 +333,22 @@ where // ───────────────────────────────────────────────────────────────────────────── impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let inner = self.expr.create_edge_op(graph.clone())?; let op: Arc + 'graph> = Arc::new(PropValueSetEdgeOp { inner, @@ -254,23 +358,39 @@ impl CreateFilter for PropValueSetExpr { Ok(EdgeExprFilteredGraph::new(graph, op)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, _graph: G, - ) -> Result, GraphError> { + _filtered: F, + ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = ExplodedEdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph> = NotANodeFilter; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let inner = self.expr.create_edge_op(graph.clone())?; let op: Arc + 'graph> = Arc::new(PropValueSetEdgeOp { inner, @@ -280,10 +400,18 @@ impl CreateFilter for PropValueSetExpr { Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, _graph: G, - ) -> Result, GraphError> { + _filtered: F, + ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } diff --git a/raphtory/src/db/graph/views/filter/model/filter.rs b/raphtory/src/db/graph/views/filter/model/filter.rs index 85d216e194..8dabf06024 100644 --- a/raphtory/src/db/graph/views/filter/model/filter.rs +++ b/raphtory/src/db/graph/views/filter/model/filter.rs @@ -239,3 +239,6 @@ impl Filter { self.operator.apply_id(&self.field_value, node_value) } } + +// June renamed `FilterValue` to `FieldFilterValue`; db_v4 machinery still says `FilterValue`. +pub type FilterValue = FieldFilterValue; 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 5ccd2c694f..3e2a3e9f45 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -1,3 +1,4 @@ +use raphtory_api::core::storage::arc_str::ArcStr; use crate::db::graph::views::filter::model::{ filter::FilterValue, property_filter::PropertyFilterValue, }; @@ -310,3 +311,257 @@ impl FilterOperator { } } } + +// ── expr-layer operator kinds (from the June branch; consumed by node_expr/edge_expr) ── + +pub trait Comparable: Clone + Send + Sync + 'static { + fn binary_cmp(op: &BinaryOp, left: &Self, right: &Self) -> bool; +} + + +pub trait StringComparable: Clone + Send + Sync + 'static { + fn string_cmp(op: &StringOp, left: &Self, right: &Self) -> bool; +} + + +/// Ordering and equality operators used by `BinaryCmpExpr`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOp { + Eq, + Ne, + Lt, + Le, + Gt, + Ge, +} + + +/// String-only operators used by `StringExpr`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StringOp { + StartsWith, + EndsWith, + Contains, + NotContains, + FuzzySearch { + levenshtein_distance: usize, + prefix_match: bool, + }, +} + + +/// Unary presence operators used by `UnaryExpr`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnaryOp { + IsSome, + IsNone, +} + + +/// Set membership operators used by `SetNodeFilter`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SetOp { + IsIn, + IsNotIn, +} + + +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, + } + } +} + + +impl Comparable for Prop { + fn binary_cmp(op: &BinaryOp, left: &Prop, right: &Prop) -> bool { + use std::cmp::Ordering::*; + + // Try casting right to left's type for cross-type numeric comparisons + // (e.g. Prop::I32(1) vs Prop::U64(1), or Prop::F64(3.0) vs Prop::U64(3)). + let right_casted = right.clone().try_cast(left.dtype()); + let right = right_casted.as_ref().unwrap_or(right); + + match op { + BinaryOp::Eq => left == right, + BinaryOp::Ne => left != right, + BinaryOp::Lt => left.partial_cmp(right).map(|o| o == Less).unwrap_or(false), + BinaryOp::Le => left + .partial_cmp(right) + .map(|o| o != Greater) + .unwrap_or(false), + BinaryOp::Gt => left + .partial_cmp(right) + .map(|o| o == Greater) + .unwrap_or(false), + BinaryOp::Ge => left.partial_cmp(right).map(|o| o != Less).unwrap_or(false), + } + } +} + + +impl Comparable for GID { + fn binary_cmp(op: &BinaryOp, left: &GID, right: &GID) -> bool { + match (left, right) { + (GID::U64(l), GID::U64(r)) => match op { + BinaryOp::Eq => l == r, + BinaryOp::Ne => l != r, + BinaryOp::Lt => l < r, + BinaryOp::Le => l <= r, + BinaryOp::Gt => l > r, + BinaryOp::Ge => l >= r, + }, + (GID::Str(l), GID::Str(r)) => String::binary_cmp(op, l, r), + _ => matches!(op, BinaryOp::Ne), + } + } +} + + +impl Comparable for Option { + fn binary_cmp(op: &BinaryOp, left: &Option, right: &Option) -> bool { + match (left, right) { + (Some(l), Some(r)) => T::binary_cmp(op, l, r), + _ => false, + } + } +} + + +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, + } + } +} + + +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, ">="), + } + } +} + + +impl Display for StringOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StringOp::StartsWith => write!(f, "STARTS_WITH"), + StringOp::EndsWith => write!(f, "ENDS_WITH"), + StringOp::Contains => write!(f, "CONTAINS"), + StringOp::NotContains => write!(f, "NOT_CONTAINS"), + StringOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => write!(f, "FUZZY_SEARCH({},{})", levenshtein_distance, prefix_match), + } + } +} + + +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"), + } + } +} + + +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"), + } + } +} + +macro_rules! impl_comparable_str { + ($ty:ty) => { + impl Comparable for $ty { + fn binary_cmp(op: &BinaryOp, left: &$ty, right: &$ty) -> bool { + let (l, r): (&str, &str) = (left, right); + match op { + BinaryOp::Eq => l == r, + BinaryOp::Ne => l != r, + BinaryOp::Lt => l < r, + BinaryOp::Le => l <= r, + BinaryOp::Gt => l > r, + BinaryOp::Ge => l >= r, + } + } + } + }; +} + +impl_comparable_str!(String); +impl_comparable_str!(ArcStr); +impl_comparable_str!(&'static str); + +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); 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 878df902b6..9e898dc759 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -1,3 +1,4 @@ +use crate::db::graph::views::filter::model::{latest_filter::Latest, layered_filter::Layered, snapshot_filter::{SnapshotAt, SnapshotLatest}, CombinedFilter}; use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, @@ -94,3 +95,35 @@ impl TryAsCompositeFilter for GraphFilter { Err(GraphError::NotSupported) } } + +// ── expr-layer view ops (June branch) ── + +pub trait GraphFilterOps: + InternalViewWrapOps + CombinedFilter + Send + Sync + 'static +{ + type GraphWindow: GraphFilterOps + CombinedFilter; +} + +impl GraphFilterOps for GraphFilter { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for Windowed { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for Layered { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for Latest { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for SnapshotAt { + type GraphWindow = Self::Window; +} + +impl GraphFilterOps for SnapshotLatest { + type GraphWindow = Self::Window; +} 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 0363e1843a..1932b079a0 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -1,3 +1,4 @@ +use crate::db::graph::views::filter::model::CreateView; use crate::{ db::{ api::view::internal::GraphView, @@ -248,3 +249,17 @@ impl EdgeViewFilterOps for Latest { self.wrap(self.inner.is_self_loop()) } } + +// ── expr-layer view construction (June branch) ── + +impl CreateView for Latest { + type View<'graph, G: GraphView + 'graph> = WindowedGraph<::View<'graph, G>>; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + let inner = self.inner.create_view(view)?; + Ok(inner.latest()) + } +} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 06a05a1653..87f8d04efe 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::internal::DynGraphArc, - }, - 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, @@ -59,10 +35,11 @@ use crate::{ ops::{filter::NO_FILTER, Const}, NodeOp, }, - view::BoxableGraphView, + view::{internal::DynGraphArc, BoxableGraphView}, }, graph::views::{ filter::model::{ + edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, @@ -72,10 +49,7 @@ use crate::{ layered_filter::Layered, node_expr::{NodeMetaOp, NodePropOp}, property_filter::{ - builders::{ - InternalPropertyFilterBuilder, PropertyExprBuilder, - PropertyExprBuilderInput, - }, + builders::{PropertyExprBuilder, PropertyExprBuilderInput}, Op, PropertyFilterInput, PropertyRef, }, snapshot_filter::{SnapshotAt, SnapshotLatest}, @@ -111,6 +85,8 @@ pub mod latest_filter; pub mod layered_filter; pub mod node_expr; pub mod node_filter; +pub use exploded_edge_filter::CompositeExplodedEdgeFilter; +pub use node_filter::CompositeNodeFilter; pub mod node_state_filter; pub mod not_filter; pub mod or_filter; @@ -209,7 +185,7 @@ pub trait ComposableFilter: Sized { } } -pub trait DynCreateFilter: Send + Sync + 'static { +pub trait DynCreateFilter: TryAsCompositeFilter + Send + Sync + 'static { fn create_dyn_filter<'graph>( &self, graph: DynGraphArc<'graph>, @@ -228,7 +204,10 @@ pub trait DynCreateFilter: Send + Sync + 'static { ) -> Result, GraphError>; } -impl DynCreateFilter for T { +impl DynCreateFilter for T +where + T: CombinedFilter, +{ fn create_dyn_filter<'graph>( &self, graph: DynGraphArc<'graph>, @@ -285,6 +264,13 @@ impl CreateFilter for Arc { self.deref() .create_dyn_node_filter(Arc::new(graph), Arc::new(filtered)) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + self.deref().dyn_filter_graph_view(Arc::new(graph)) + } } #[derive(Copy, Clone)] @@ -341,6 +327,16 @@ impl EntityExpr for PropertyExpr { } } +pub trait PropertyFilterFactory: InternalPropertyFilterFactory { + fn property(&self, name: impl Into) -> Self::PropertyBuilder { + self.property_builder(name.into()) + } + + fn metadata(&self, name: impl Into) -> Self::MetadataBuilder { + self.metadata_builder(name.into()) + } +} + impl PropertyFilterFactory for T {} pub trait TemporalPropertyFilterFactory: InternalPropertyFilterBuilder { @@ -451,16 +447,14 @@ impl TryAsCompositeFilter for Arc { self.deref().try_as_composite_node_filter() } - fn create_edge_op<'g, G: GraphView + 'g>( + fn try_as_composite_edge_filter(&self) -> Result { + self.deref().try_as_composite_edge_filter() + } + + fn try_as_composite_exploded_edge_filter( &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let prop_id = graph - .edge_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(EdgePropOp { graph, prop_id })) + ) -> Result { + self.deref().try_as_composite_exploded_edge_filter() } } @@ -477,6 +471,32 @@ impl EntityExpr for MetadataExpr { } } +impl CreateOp for PropertyExpr { + 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 create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let prop_id = graph + .edge_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(EdgePropOp { graph, prop_id })) + } +} + impl CreateOp for MetadataExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, @@ -503,13 +523,15 @@ impl CreateOp for Me } } -pub trait PropertyFilterFactory: CreateView + EntityExpr + Sized { +/// Entry point of the expr API: selects a property or metadata column on any view expression. +/// Distinct from [`PropertyFilterFactory`], the builder-path factory it will eventually replace. +pub trait PropertyExprFactory: CreateView + EntityExpr + Sized { fn property(&self, name: impl Into) -> PropertyExpr; fn metadata(&self, name: impl Into) -> MetadataExpr; } -impl PropertyFilterFactory for T { +impl PropertyExprFactory for T { fn property(&self, name: impl Into) -> PropertyExpr { PropertyExpr { view_expr: self.clone(), @@ -525,11 +547,21 @@ impl PropertyFilterFactory for T { } } -pub trait DynPropertyFilterFactory { + + +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; +} + +pub trait DynPropertyExprFactory { fn dyn_property(&self, name: String) -> Arc; } -impl DynPropertyFilterFactory for T { +impl DynPropertyExprFactory for T { fn dyn_property(&self, name: String) -> Arc { Arc::new(self.property(name)) } @@ -538,7 +570,7 @@ impl DynPropertyFilterFactory for T { impl InternalPropertyFilterBuilder for PropertyExpr where E: Into + Send + Sync + Clone + 'static, - crate::prelude::PropertyFilter: CreateFilter, + crate::prelude::PropertyFilter: CombinedFilter, PropertyExprBuilder: InternalPropertyFilterBuilder, { type Filter = crate::prelude::PropertyFilter; @@ -569,7 +601,7 @@ where impl InternalPropertyFilterBuilder for MetadataExpr where E: Into + Send + Sync + Clone + 'static, - crate::prelude::PropertyFilter: CreateFilter, + crate::prelude::PropertyFilter: CombinedFilter, PropertyExprBuilder: InternalPropertyFilterBuilder, { type Filter = crate::prelude::PropertyFilter; @@ -864,15 +896,9 @@ 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 {} -impl DynInternalViewWrapPropOps for T where - T: DynInternalViewWrapOps + DynPropertyFilterFactory + DynCreateView -{ -} +impl DynInternalViewWrapPropOps for T where T: DynInternalViewWrapOps + DynPropertyFilterFactory {} pub trait DynViewFilter: DynCreateFilter + Send + Sync + 'static { fn dyn_bounds(&self) -> (EventTime, EventTime); @@ -881,7 +907,7 @@ pub trait DynViewFilter: DynCreateFilter + Send + Sync + 'static { } impl DynViewFilter for T where - T: GraphFilterOps + Send + Sync + 'static, + T: GraphFilterOps, { fn dyn_bounds(&self) -> (EventTime, EventTime) { self.bounds() @@ -916,33 +942,16 @@ impl ComposableFilter for DynFilter {} impl ComposableFilter for DynView {} pub trait EdgeViewFilterOps: ViewWrapOps { - fn is_active(&self) -> IsActiveEdge { - IsActiveEdge { - view_expr: self.clone(), - } - } + type Output: CombinedFilter; - fn is_valid(&self) -> IsValidEdge { - IsValidEdge { - view_expr: self.clone(), - } - } + fn is_active(&self) -> Self::Output; - fn is_deleted(&self) -> IsDeletedEdge { - IsDeletedEdge { - view_expr: self.clone(), - } - } + fn is_valid(&self) -> Self::Output; - fn is_self_loop(&self) -> IsSelfLoopEdge { - IsSelfLoopEdge { - view_expr: self.clone(), - } - } -} + fn is_deleted(&self) -> Self::Output; -impl EdgeViewFilterOps for EdgeFilter {} -impl EdgeViewFilterOps for ExplodedEdgeFilter {} + fn is_self_loop(&self) -> Self::Output; +} pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps { fn dyn_is_active(&self) -> Arc; @@ -954,7 +963,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()) } @@ -972,9 +981,32 @@ impl DynEdgeView } } + + + + + pub type DynEdgeViewProps = Arc; -impl EdgeViewFilterOps for DynEdgeViewProps {} +impl EdgeViewFilterOps for DynEdgeViewProps { + type Output = Arc; + + fn is_active(&self) -> Self::Output { + self.deref().dyn_is_active() + } + + fn is_valid(&self) -> Self::Output { + self.deref().dyn_is_valid() + } + + fn is_deleted(&self) -> Self::Output { + self.deref().dyn_is_deleted() + } + + fn is_self_loop(&self) -> Self::Output { + self.deref().dyn_is_self_loop() + } +} // ───────────────────────────────────────────────────────────────────────────── // EntityExprFilterOps — comparison and set operators on any EntityExpr @@ -1207,7 +1239,7 @@ pub fn validate_const_castable( return Ok(()); } if let Some(rhs) = rhs_const { - if rhs.dtype() != *lhs_pt && rhs.clone().try_cast(lhs_pt.clone()).is_none() { + if rhs.dtype() != *lhs_pt && rhs.clone().try_cast(lhs_pt.clone()).is_err() { return Err(GraphError::InvalidFilter(format!( "value {:?} of type {} cannot be coerced to {}", rhs, @@ -1257,7 +1289,7 @@ pub fn validate_types_compatible(lhs_pt: &PropType, rhs_pt: &PropType) -> Result return Ok(()); } let castable = representative_prop(rhs_pt) - .and_then(|v| v.try_cast(lhs_pt.clone())) + .and_then(|v| v.try_cast(lhs_pt.clone()).ok()) .is_some(); if !castable { return Err(GraphError::InvalidFilter(format!( @@ -1302,7 +1334,7 @@ pub fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result) -> Result + 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 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; +} + +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 CombinedFilter: CreateFilter + TryAsCompositeFilter + Clone + 'static {} + +pub trait NodeViewFilterOps: ViewWrapOps { + type Output: CombinedFilter; + + fn is_active(&self) -> Self::Output; +} + +pub trait DynNodeViewFilterOps: DynInternalViewWrapPropOps { + fn dyn_is_active(&self) -> 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) + } +} + +impl InternalPropertyFilterFactory for Arc { + type Entity = EntityMarker; + type PropertyBuilder = Arc; + type MetadataBuilder = Arc; + + fn entity(&self) -> Self::Entity { + self.deref().dyn_entity() + } + + fn property_builder(&self, property: String) -> Self::PropertyBuilder { + self.deref().dyn_property_builder(property) + } + + fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { + self.deref().dyn_metadata_builder(property) + } +} + +impl CombinedFilter for T {} + +impl InternalPropertyFilterFactory for Arc { + type Entity = EntityMarker; + type PropertyBuilder = Arc; + type MetadataBuilder = Arc; + + fn entity(&self) -> Self::Entity { + self.deref().dyn_entity() + } + + fn property_builder(&self, property: String) -> Self::PropertyBuilder { + self.deref().dyn_property_builder(property) + } + + fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { + self.deref().dyn_metadata_builder(property) + } +} + +impl DynNodeViewFilterOps for T { + fn dyn_is_active(&self) -> Arc { + Arc::new(self.is_active()) + } +} + +pub type DynNodeViewProps = Arc; + +impl NodeViewFilterOps for DynNodeViewProps { + type Output = Arc; + + fn is_active(&self) -> Self::Output { + self.deref().dyn_is_active() + } +} + +impl InternalPropertyFilterFactory for DynNodeViewProps { + type Entity = EntityMarker; + type PropertyBuilder = Arc; + type MetadataBuilder = Arc; + + fn entity(&self) -> Self::Entity { + self.deref().dyn_entity() + } + + 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) + } +} + +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/filters.rs b/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs index ed83c7cd5a..ac9b96cc38 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 @@ -173,26 +173,35 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + NodeFilteredGraph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc + 'graph>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone())?; + filtered: F, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone(), filtered)?; Ok(NodeFilteredGraph::new(graph, filter)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; + let left = self.left.create_node_op(filtered.clone())?; + let right = self.right.create_node_op(filtered)?; let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; @@ -207,6 +216,13 @@ where op: self.op, })) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } impl CreateFilter for BinaryCmpExpr @@ -214,36 +230,52 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; - type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc; + + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc + 'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { Ok(match self.entity { - EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph)?), - EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph)?), + EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph, filtered)?), + EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph, filtered)?), EntityMarker::ExplodedEdge => { - Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) + Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph, filtered)?) } EntityMarker::Const => Err(GraphError::NotSupported)?, }) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { match self.entity { - EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), + EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph, filtered)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), EntityMarker::Const => Err(GraphError::NotSupported)?, } } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } // ───────────────────────────────────────────────────────────────────────────── @@ -310,67 +342,99 @@ impl CreateFilter for UnaryExpr where E: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, Prop>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = UnaryNodeOp<'graph, Prop>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone())?; + filtered: F, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone(), filtered)?; Ok(NodeFilteredGraph::new(graph, filter)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { if !self.expr.nullable() { return Err(GraphError::InvalidFilter(format!( "operator {:?} is not valid for non-nullable expression", self.op ))); } - let inner = self.expr.create_node_op(graph)?; + let inner = self.expr.create_node_op(filtered)?; Ok(UnaryNodeOp { inner, op: self.op }) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } impl CreateFilter for UnaryExpr where E: CreateOp, { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; - type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, Prop>; + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = UnaryNodeOp<'graph, Prop>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { Ok(match self.entity { - EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph)?), - EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph)?), + EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph, filtered)?), + EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph, filtered)?), EntityMarker::ExplodedEdge => { - Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) + Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph, filtered)?) } EntityMarker::Const => Err(GraphError::NotSupported)?, }) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { match self.entity { - EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), + EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph, filtered)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), EntityMarker::Const => Err(GraphError::NotSupported)?, } } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } // ───────────────────────────────────────────────────────────────────────────── @@ -464,26 +528,35 @@ impl CreateOp for StringExpr { } impl CreateFilter for StringExpr { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = - NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + NodeFilteredGraph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc + 'graph>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone())?; + filtered: F, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone(), filtered)?; Ok(NodeFilteredGraph::new(graph, filter)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; + let left = self.left.create_node_op(filtered.clone())?; + let right = self.right.create_node_op(filtered)?; validate_string_op(&resolved_prop_type(expr_pt, left.prop_type()))?; Ok(Arc::new(StringNodeOp { left, @@ -491,38 +564,61 @@ impl CreateFilter for StringExpr { op: self.op, })) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } impl CreateFilter for StringExpr { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; - type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc; + + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc + 'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { Ok(match self.entity { - EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph)?), - EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph)?), + EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph, filtered)?), + EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph, filtered)?), EntityMarker::ExplodedEdge => { - Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) + Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph, filtered)?) } EntityMarker::Const => Err(GraphError::NotSupported)?, }) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { match self.entity { - EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), + EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph, filtered)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), EntityMarker::Const => Err(GraphError::NotSupported)?, } } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } // ───────────────────────────────────────────────────────────────────────────── @@ -593,24 +689,33 @@ impl CreateOp for PropValueSetExpr { } impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph> = PropValueSetNodeOp<'graph>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = PropValueSetNodeOp<'graph>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone())?; + filtered: F, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone(), filtered)?; Ok(NodeFilteredGraph::new(graph, filter)) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { let expr_pt = self.expr.prop_type(); - let inner = self.expr.create_node_op(graph)?; + let inner = self.expr.create_node_op(filtered)?; let lhs_pt = resolved_prop_type(expr_pt, inner.prop_type()); let values = coerce_set_values(&lhs_pt, self.values)?; Ok(PropValueSetNodeOp { @@ -619,35 +724,58 @@ impl CreateFilter for PropValueSetExpr { op: self.op, }) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = Arc; - type NodeFilter<'graph, G: GraphView + 'graph> = PropValueSetNodeOp<'graph>; - fn create_filter<'graph, G: GraphViewOps<'graph>>( + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = PropValueSetNodeOp<'graph>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { Ok(match self.entity { - EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph)?), - EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph)?), + EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph, filtered)?), + EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph, filtered)?), EntityMarker::ExplodedEdge => { - Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph)?) + Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph, filtered)?) } EntityMarker::Const => Err(GraphError::NotSupported)?, }) } - fn create_node_filter<'graph, G: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, graph: G, - ) -> Result, GraphError> { + filtered: F, + ) -> Result, GraphError> { match self.entity { - EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph)?), + EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph, filtered)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), EntityMarker::Const => Err(GraphError::NotSupported)?, } } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } } 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 index 784add51fc..2bfe88ca46 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -21,7 +21,7 @@ //! NodeFilter.property("score").temporal().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) +//! apply: collect Prop::List temporal values, then aggregate_list_values(Sum) //! ``` //! //! # Quantified evaluation @@ -36,6 +36,7 @@ //! Eq Bool(true) → true (at least one matched) //! ``` +use crate::db::api::view::internal::NodeList; use super::EdgeOp; use crate::{ db::{ @@ -47,7 +48,7 @@ use crate::{ graph::views::filter::model::{ filter_operator::{BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp}, property_filter::evaluate::{ - aggregate_values, scan_f64_sum_count, scan_i64_sum, scan_u64_sum, + aggregate_list_values, scan_f64_sum_count, scan_i64_sum, scan_u64_sum, }, }, }, @@ -75,6 +76,10 @@ pub(crate) struct NodePropOp { } impl NodeOp for NodePropOp { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.graph.node_list() + } + type Output = Option; fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { @@ -105,6 +110,10 @@ pub(crate) struct NodeMetaOp { } impl NodeOp for NodeMetaOp { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.graph.node_list() + } + type Output = Option; fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { @@ -136,6 +145,10 @@ pub(crate) struct TemporalNodePropOp { } impl NodeOp for TemporalNodePropOp { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.graph.node_list() + } + type Output = Prop; fn apply(&self, _storage: &GraphStorage, node: VID) -> Prop { @@ -173,6 +186,10 @@ macro_rules! impl_agg_entity_op { } impl<'g> NodeOp for $node_name<'g> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.inner.domain(_storage) + } + type Output = Option; fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { @@ -196,7 +213,7 @@ macro_rules! impl_agg_entity_op { } impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { - aggregate_values(vals, &|pi| { + aggregate_list_values(vals, &|pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { return None; @@ -228,7 +245,7 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { }); impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { - aggregate_values(vals, &|pi| { + aggregate_list_values(vals, &|pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { return None; @@ -257,14 +274,14 @@ impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { }) }); impl_agg_entity_op!(MinNodeOp, MinEdgeOp, |vals| { - aggregate_values(vals, &|pi| { + aggregate_list_values(vals, &|pi| { let mut it = pi; let first = it.next()?; it.fold(Some(first), |acc, v| acc.and_then(|a| a.min(v))) }) }); impl_agg_entity_op!(MaxNodeOp, MaxEdgeOp, |vals| { - aggregate_values(vals, &|pi| { + aggregate_list_values(vals, &|pi| { let mut it = pi; let first = it.next()?; it.fold(Some(first), |acc, v| acc.and_then(|a| a.max(v))) @@ -287,15 +304,15 @@ impl_agg_entity_op!(LastNodeOp, LastEdgeOp, |vals| { } }); impl_agg_entity_op!(LenNodeOp, LenEdgeOp, |vals| { - aggregate_values(vals, &|pi| Some(pi.count().into_prop())) + aggregate_list_values(vals, &|pi| Some(pi.count().into_prop())) }); impl_agg_entity_op!(AnyNodeOp, AnyEdgeOp, |vals| { - aggregate_values(vals, &|mut pi| { + aggregate_list_values(vals, &|mut pi| { Some(Prop::Bool(pi.any(|r| r == Prop::Bool(true)))) }) }); impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { - aggregate_values(vals, &|mut pi| { + aggregate_list_values(vals, &|mut pi| { let mut saw_any = false; let all_true = pi.all(|r| { saw_any = true; @@ -326,6 +343,10 @@ pub(crate) struct ListAwareCmpNodeOp<'g> { } impl<'g> NodeOp for ListAwareCmpNodeOp<'g> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = Option; fn apply(&self, storage: &GraphStorage, node: VID) -> Option { @@ -346,6 +367,10 @@ pub(crate) struct ListAwareStringNodeOp<'g> { } impl<'g> NodeOp for ListAwareStringNodeOp<'g> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = Option; fn apply(&self, storage: &GraphStorage, node: VID) -> Option { @@ -414,6 +439,10 @@ pub(crate) struct ListAwareSetNodeOp<'g> { } impl<'g> NodeOp for ListAwareSetNodeOp<'g> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = Option; fn apply(&self, storage: &GraphStorage, node: VID) -> Option { @@ -450,6 +479,10 @@ pub(crate) struct ListAwareUnaryNodeOp<'g> { } impl<'g> NodeOp for ListAwareUnaryNodeOp<'g> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = Option; fn apply(&self, storage: &GraphStorage, node: VID) -> Option { @@ -487,6 +520,10 @@ impl<'g> Clone for AndBoolNodeOp<'g> { } impl<'g> NodeOp for AndBoolNodeOp<'g> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = Option; fn apply(&self, storage: &GraphStorage, node: VID) -> Option { @@ -515,6 +552,10 @@ impl<'g> Clone for OrBoolNodeOp<'g> { } impl<'g> NodeOp for OrBoolNodeOp<'g> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = Option; fn apply(&self, storage: &GraphStorage, node: VID) -> Option { @@ -552,6 +593,10 @@ impl<'g> Clone for PropValueSetNodeOp<'g> { } impl<'g> NodeOp for PropValueSetNodeOp<'g> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = bool; fn apply(&self, storage: &GraphStorage, node: VID) -> bool { @@ -590,6 +635,10 @@ pub struct BinaryCmpNodeOp<'g, T: Comparable> { } impl<'g, T: Comparable + Clone + Send + Sync + 'static> NodeOp for BinaryCmpNodeOp<'g, T> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = bool; fn apply(&self, storage: &GraphStorage, node: VID) -> bool { @@ -619,6 +668,10 @@ pub struct StringNodeOp<'g, T: StringComparable> { } impl<'g, T: StringComparable> NodeOp for StringNodeOp<'g, T> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = bool; fn apply(&self, storage: &GraphStorage, node: VID) -> bool { @@ -645,6 +698,10 @@ pub struct UnaryNodeOp<'g, I: Clone + Send + Sync + 'static> { } impl<'g, I: Clone + Send + Sync + 'static> NodeOp for UnaryNodeOp<'g, I> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = bool; fn apply(&self, storage: &GraphStorage, node: VID) -> bool { @@ -676,6 +733,10 @@ pub struct SetNodeOp<'g, I: Eq + Hash + Clone + Send + Sync + 'static> { } impl<'g, I: Eq + Hash + Clone + Send + Sync + 'static> NodeOp for SetNodeOp<'g, I> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } + type Output = bool; fn apply(&self, storage: &GraphStorage, node: VID) -> bool { 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 7f92d5e657..22feaddc7f 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 @@ -54,3 +54,70 @@ impl InternalNodeFilterBuilder for Type { "node_type" } } + +// ── restored from db_v4 (lost in a silent auto-merge) ── + +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() + } +} + +#[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; + +impl Wrap for NodeNameFilterBuilder { + type Wrapped = T; + + fn wrap(&self, value: T) -> Self::Wrapped { + value + } +} + +impl InternalNodeFilterBuilder for NodeNameFilterBuilder { + type FilterType = NodeNameFilter; + + fn field_name(&self) -> &'static str { + "node_name" + } +} + +#[derive(Clone, Debug)] +pub struct NodeTypeFilterBuilder; + +impl Wrap for NodeTypeFilterBuilder { + type Wrapped = T; + + fn wrap(&self, value: T) -> Self::Wrapped { + value + } +} + +impl InternalNodeFilterBuilder for NodeTypeFilterBuilder { + type FilterType = NodeTypeFilter; + fn field_name(&self) -> &'static str { + "node_type" + } +} diff --git a/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs b/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs index 3bec71d394..be07af6925 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 @@ -1,3 +1,7 @@ +use crate::db::{ + api::state::ops::node::{Id, Name, Type}, + graph::views::filter::model::{node_expr::exprs::DegreeExpr, CreateView, node_expr::EntityExpr}, +}; use crate::{ api::core::Direction, db::{ @@ -112,11 +116,11 @@ impl InternalPropertyFilterFactory for NodeFilter { } fn property_builder(&self, property: String) -> Self::PropertyBuilder { - PropertyFilterBuilder(property, self.entity()) + PropertyFilterBuilder(property, InternalPropertyFilterFactory::entity(self)) } fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - MetadataFilterBuilder(property, self.entity()) + MetadataFilterBuilder(property, InternalPropertyFilterFactory::entity(self)) } } @@ -551,3 +555,95 @@ impl TryAsCompositeFilter for CompositeNodeFilter { Err(GraphError::NotSupported) } } + +// ── expr-layer factory (June branch) ── + +pub trait NodeFilterFactory: + InternalViewWrapOps + CreateView + EntityExpr +{ + type NodeWindow: NodeFilterFactory; + #[inline] + fn id(&self) -> Id { + Id + } + + /// Selects the node name field for filtering. + /// + /// Returns `Name` which implements `NodeExprFilterOps` — use `.eq("Alice")`, + /// `.contains("ali")`, `.is_in([…])`, etc. directly on the returned value. + #[inline] + fn name(&self) -> Name { + Name + } + + /// Selects the node type field for filtering. + /// + /// Returns `Type` which implements `NodeExprFilterOps`. + #[inline] + fn node_type(&self) -> Type { + Type + } + + /// Build a filter from a boolean column inside a TypedNodeState. + fn by_column<'graph, V, G, T>( + state: &TypedNodeState<'graph, V, G, T>, + col: &str, + ) -> Result + where + V: NodeStateValue + 'graph, + T: Clone + Send + Sync + 'graph, + Self: Sized, + { + state.bool_col_filter(col) + } + + /// Total degree expression — supports `.gt(n)`, `.lt(n)`, etc. + fn degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::BOTH, + view_expr: self.clone(), + } + } + + /// In-degree expression. + fn in_degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::IN, + view_expr: self.clone(), + } + } + + /// Out-degree expression. + #[inline] + fn out_degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::OUT, + view_expr: self.clone(), + } + } + +} + +impl NodeFilterFactory for NodeFilter { + type NodeWindow = Self::Window; +} + +impl NodeFilterFactory for Windowed { + type NodeWindow = T::NodeWindow; +} + +impl NodeFilterFactory for Latest { + type NodeWindow = Self::Window; +} + +impl NodeFilterFactory for SnapshotAt { + type NodeWindow = Self::Window; +} + +impl NodeFilterFactory for SnapshotLatest { + type NodeWindow = Self::Window; +} + +impl NodeFilterFactory for Layered { + type NodeWindow = Self::Window; +} 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 0fb1ff301e..49c68cd6b8 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,3 +1,4 @@ +use crate::db::graph::views::filter::model::{CombinedFilter, InternalPropertyFilterBuilder, TemporalPropertyFilterFactory}; use crate::db::graph::views::filter::{ model::{ property_filter::{Op, PropertyFilter, PropertyFilterInput, PropertyRef}, @@ -6,17 +7,6 @@ use crate::db::graph::views::filter::{ CreateFilter, }; -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); @@ -38,7 +28,7 @@ impl Wrap for PropertyFilterBuilder { impl InternalPropertyFilterBuilder for PropertyFilterBuilder where M: Into + Send + Sync + Clone + 'static, - PropertyFilter: CreateFilter, + PropertyFilter: CombinedFilter, PropertyExprBuilder: InternalPropertyFilterBuilder, { type Filter = PropertyFilter; @@ -86,7 +76,7 @@ impl Wrap for MetadataFilterBuilder { impl InternalPropertyFilterBuilder for MetadataFilterBuilder where M: Into + Send + Sync + Clone + 'static, - PropertyFilter: CreateFilter, + PropertyFilter: CombinedFilter, PropertyExprBuilder: InternalPropertyFilterBuilder, { type Filter = PropertyFilter; @@ -195,7 +185,7 @@ impl Wrap for PropertyExprBuilder { impl InternalPropertyFilterBuilder for PropertyExprBuilder where M: Into + Send + Sync + Clone + 'static, - PropertyFilter: CreateFilter, + PropertyFilter: CombinedFilter, { type Filter = PropertyFilter; type ExprBuilder = PropertyExprBuilder; @@ -221,3 +211,10 @@ where builder.with_entity(self.entity()) } } + +impl TemporalPropertyFilterFactory for PropertyFilterBuilder +where + T: Into + Send + Sync + Clone + 'static, + PropertyFilter: CombinedFilter, +{ +} diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index fb3c503d40..928b151bec 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; +use raphtory_api::core::entities::properties::prop::PropType; use crate::{db::graph::views::filter::model::Op, prelude::PropertyFilter}; use raphtory_api::core::{entities::properties::prop::Prop, utils::generalised_reduce}; @@ -329,3 +331,99 @@ impl PropertyFilter { self.apply_eval(r, s, q, is_t) } } + +// ── numeric scan helpers for expr aggregates (June branch) ── + +pub fn scan_u64_sum>( + vals: impl IntoIterator, +) -> Option<(bool, u64, u128, usize)> { + let mut sum64: u64 = 0; + let mut sum128: u128 = 0; + let mut promoted = false; + let mut count = 0usize; + + for p in vals { + let p = p.borrow(); + let x = p.as_u64_lossless()?; + if !promoted { + if let Some(s) = sum64.checked_add(x) { + sum64 = s; + } else { + promoted = true; + sum128 = (sum64 as u128) + (x as u128); + } + } else { + sum128 += x as u128; + } + count += 1; + } + Some((promoted, sum64, sum128, count)) +} + +pub fn scan_i64_sum>( + vals: impl IntoIterator, +) -> Option<(bool, i64, i128, usize)> { + let mut sum64: i64 = 0; + let mut sum128: i128 = 0; + let mut promoted = false; + let mut count = 0; + + for p in vals { + let p = p.borrow(); + let x = p.as_i64_lossless()?; + if !promoted { + if let Some(s) = sum64.checked_add(x) { + sum64 = s; + } else { + promoted = true; + sum128 = (sum64 as i128) + (x as i128); + } + } else { + sum128 += x as i128; + } + count += 1; + } + Some((promoted, sum64, sum128, count)) +} + +pub fn scan_f64_sum_count>( + vals: impl IntoIterator, +) -> Option<(f64, u64)> { + let mut sum = 0.0f64; + let mut count = 0u64; + for p in vals { + let p = p.borrow(); + let x = p.as_f64_lossless()?; + if !x.is_finite() { + return None; + } + sum += x; + count += 1; + } + Some((sum, count)) +} + +/// List-aware aggregation for the expr layer: descends one level into list +/// properties and applies `op` per element list. +pub fn aggregate_list_values( + vals: Option, + op: &dyn Fn(Box>) -> Option, +) -> Option { + match vals? { + Prop::List(x) => match x.dtype() { + PropType::List(_) => { + let s = x + .iter_all() + .map(|y| aggregate_list_values(y, op)) + .flatten() + .collect(); + Some(Prop::List(s)) + } + _ => { + let items: Vec = x.iter().collect(); + op(Box::new(items.into_iter())) + } + }, + _ => None, + } +} 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 aad5dbee0d..1beb52e1e5 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::{collections::HashSet, fmt, fmt::Display, sync::Arc}; pub mod builders; -mod evaluate; +pub(crate) mod evaluate; pub mod ops; mod validate; 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 977d37041a..0e946406c8 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,9 +1,8 @@ use crate::db::graph::views::filter::model::{ property_filter::{ - builders::{InternalPropertyFilterBuilder, PropertyExprBuilderInput}, - Op, PropertyFilterInput, PropertyFilterValue, + builders::PropertyExprBuilderInput, Op, PropertyFilterInput, PropertyFilterValue, }, - FilterOperator, + FilterOperator, InternalPropertyFilterBuilder, }; 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 386e72c975..b2f86f4ed6 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -1,3 +1,4 @@ +use crate::db::graph::views::filter::model::CreateView; use crate::{ db::{ api::view::{internal::GraphView, time::TimeOps}, @@ -446,3 +447,29 @@ impl EdgeViewFilterOps for SnapshotLatest { self.wrap(self.inner.is_self_loop()) } } + +// ── expr-layer view construction (June branch) ── + +impl CreateView for SnapshotAt { + type View<'graph, G: GraphView + 'graph> = WindowedGraph<::View<'graph, G>>; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + let inner = self.inner.create_view(view)?; + Ok(inner.snapshot_at(self.time)) + } +} + +impl CreateView for SnapshotLatest { + type View<'graph, G: GraphView + 'graph> = WindowedGraph<::View<'graph, G>>; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + let inner = self.inner.create_view(view)?; + Ok(inner.snapshot_latest()) + } +} 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 b8e87f7df3..5b5135d62f 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -1,3 +1,4 @@ +use crate::db::graph::views::filter::model::CreateView; use crate::{ db::{ api::view::internal::GraphView, @@ -281,3 +282,17 @@ impl EdgeViewFilterOps for Windowed { self.wrap(self.inner.is_self_loop()) } } + +// ── expr-layer view construction (June branch) ── + +impl CreateView for Windowed { + type View<'graph, G: GraphView + 'graph> = WindowedGraph<::View<'graph, G>>; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + let inner = self.inner.create_view(view)?; + Ok(inner.window(self.start.t(), self.end.t())) + } +} diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 64f6a4e06a..f3a9a28bd8 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -157,16 +157,7 @@ pub mod prelude { }; pub use crate::db::graph::views::filter::model::{ - filter::Filter, property_filter::PropertyFilter, EntityAggOps, EntityExprFilterOps, - }; - - pub use crate::db::graph::views::{ - property_redacted_graph::PropertyRedaction, PropertyRedactedGraph, - }; - - pub use crate::db::graph::views::filter::model::{ - node_filter::{NodeFilter, NodeFilterFactory}, - EdgeFilter, + node_filter::NodeFilterFactory, EntityAggOps, EntityExprFilterOps, }; pub use storage::{persist::config::ConfigOps, Config}; From 1ca57a3d5f4dd6d6c46def08b915b4cdc4038108 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:29:15 +0100 Subject: [PATCH 101/155] python bindings, predicates and benchmarks on the bridged API PyExpr and the dyn factory traits ride the renamed factories; the boolean predicates (is_active, is_valid, is_deleted, is_self_loop) are expressions built through the Scoped view adapter; ComposableFilter::not is restored for the graphql lowering; the degree benchmark pins the builder-path factory it was written against. --- raphtory-benchmark/src/algobench_common.rs | 6 +- .../views/filter/model/edge_expr/filters.rs | 8 -- .../graph/views/filter/model/edge_filter.rs | 46 ++++++- .../views/filter/model/filter_operator.rs | 22 +-- .../graph/views/filter/model/graph_filter.rs | 11 +- .../filter/model/is_active_edge_filter.rs | 35 +++++ .../filter/model/is_active_node_filter.rs | 36 +++++ .../views/filter/model/is_deleted_filter.rs | 35 +++++ .../views/filter/model/is_self_loop_filter.rs | 35 +++++ .../views/filter/model/is_valid_filter.rs | 35 +++++ .../graph/views/filter/model/latest_filter.rs | 30 +++- .../views/filter/model/layered_filter.rs | 30 +++- .../src/db/graph/views/filter/model/mod.rs | 13 +- .../views/filter/model/node_expr/filters.rs | 128 ++++++++++++------ .../graph/views/filter/model/node_expr/mod.rs | 41 ++++++ .../graph/views/filter/model/node_expr/ops.rs | 12 +- .../views/filter/model/node_filter/mod.rs | 9 +- .../filter/model/property_filter/builders.rs | 5 +- .../filter/model/property_filter/evaluate.rs | 8 +- .../views/filter/model/snapshot_filter.rs | 54 +++++++- .../views/filter/model/windowed_filter.rs | 31 ++++- raphtory/src/python/filter/edge_expr.rs | 55 +++++--- raphtory/src/python/filter/filter_expr.rs | 7 + raphtory/src/python/filter/node_expr.rs | 16 ++- 24 files changed, 563 insertions(+), 145 deletions(-) diff --git a/raphtory-benchmark/src/algobench_common.rs b/raphtory-benchmark/src/algobench_common.rs index f0b782952a..6ee47e95a3 100644 --- a/raphtory-benchmark/src/algobench_common.rs +++ b/raphtory-benchmark/src/algobench_common.rs @@ -99,7 +99,7 @@ pub fn large_random_attachment_subgraph() -> NodeSubgraph { pub fn large_random_attachment_filtered() -> impl StaticGraphViewOps { large_random_attachment_graph() - .filter(NodeFilter.degree().ge(1u64)) + .filter(DegreeFilterFactory::degree(&NodeFilter).ge(1u64)) .unwrap() } @@ -166,7 +166,7 @@ pub fn medium_random_attachment_subgraph() -> NodeSubgraph { pub fn medium_random_attachment_filtered() -> impl StaticGraphViewOps { medium_random_attachment_graph() - .filter(NodeFilter.degree().ge(0u64)) + .filter(DegreeFilterFactory::degree(&NodeFilter).ge(0u64)) .unwrap() } @@ -233,7 +233,7 @@ pub fn tiny_random_attachment_subgraph() -> NodeSubgraph { pub fn tiny_random_attachment_filtered() -> impl StaticGraphViewOps { tiny_random_attachment_graph() - .filter(NodeFilter.degree().ge(0u64)) + .filter(DegreeFilterFactory::degree(&NodeFilter).ge(0u64)) .unwrap() } diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index 8f2e5eb838..45e7d12225 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -39,7 +39,6 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = EdgeExprFilteredGraph + 'graph>>; @@ -92,7 +91,6 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = ExplodedEdgeExprFilteredGraph + 'graph>>; @@ -148,7 +146,6 @@ impl CreateFilter for UnaryExpr where E: CreateOp, { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; @@ -190,7 +187,6 @@ impl CreateFilter for UnaryExpr where E: CreateOp, { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = ExplodedEdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; @@ -237,7 +233,6 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; @@ -285,7 +280,6 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = ExplodedEdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; @@ -333,7 +327,6 @@ where // ───────────────────────────────────────────────────────────────────────────── impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = EdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; @@ -375,7 +368,6 @@ impl CreateFilter for PropValueSetExpr { } impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = ExplodedEdgeExprFilteredGraph + 'graph>>; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; 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 648fd1bd96..b5a4c91226 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -10,6 +10,7 @@ use crate::{ graph::views::filter::{ edge_node_filtered_graph::EdgeNodeFilteredGraph, model::{ + edge_expr::{ops::EdgeEndpointNodeOp, EdgeOp}, exploded_edge_filter::CompositeExplodedEdgeFilter, is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, @@ -17,6 +18,7 @@ use crate::{ is_valid_filter::IsValidEdge, latest_filter::Latest, layered_filter::Layered, + node_expr::{CreateOp, EntityExpr, EntityExprBuilder}, node_filter::{ builders::{ InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, @@ -41,7 +43,7 @@ use crate::{ }, errors::GraphError, }; -use raphtory_api::core::storage::timeindex::EventTime; +use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; use std::{fmt, fmt::Display, sync::Arc}; // User facing entry for building edge filters. @@ -92,11 +94,11 @@ impl InternalPropertyFilterFactory for EdgeFilter { } fn property_builder(&self, property: String) -> Self::PropertyBuilder { - PropertyFilterBuilder(property, self.entity()) + PropertyFilterBuilder(property, InternalPropertyFilterFactory::entity(self)) } fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - MetadataFilterBuilder(property, self.entity()) + MetadataFilterBuilder(property, InternalPropertyFilterFactory::entity(self)) } } @@ -515,3 +517,41 @@ impl TryAsCompositeFilter for CompositeEdgeFilter { Err(GraphError::NotSupported) } } + +// ── expr layer: endpoint expressions bridge node ops into edge ops (June branch) ── + +impl EntityExprBuilder for EdgeEndpointWrapper {} + +impl EntityExpr for EdgeEndpointWrapper { + type Marker = EdgeFilter; + fn entity(&self) -> Self::Marker { + EdgeFilter + } +} + +impl CreateOp for EdgeEndpointWrapper { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let node_op = self.inner.create_node_op(graph)?; + Ok(Arc::new(EdgeEndpointNodeOp { + node_op, + endpoint: self.endpoint, + })) + } +} + +// ── expr layer: which types serve as edge-filter factories (June branch) ── + +use crate::db::graph::views::filter::model::{ + exploded_edge_filter::ExplodedEdgeFilter, CreateView, EdgeFilterFactory, +}; + +impl EdgeFilterFactory for EdgeFilter {} +impl EdgeFilterFactory for ExplodedEdgeFilter {} +impl EdgeFilterFactory for Windowed {} +impl EdgeFilterFactory for Latest {} +impl EdgeFilterFactory for Layered {} +impl EdgeFilterFactory for SnapshotAt {} +impl EdgeFilterFactory for SnapshotLatest {} 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 3e2a3e9f45..6e5e9d6f7e 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -1,8 +1,10 @@ -use raphtory_api::core::storage::arc_str::ArcStr; use crate::db::graph::views::filter::model::{ filter::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; @@ -318,12 +320,10 @@ pub trait Comparable: Clone + Send + Sync + 'static { fn binary_cmp(op: &BinaryOp, left: &Self, right: &Self) -> bool; } - pub trait StringComparable: Clone + Send + Sync + 'static { fn string_cmp(op: &StringOp, left: &Self, right: &Self) -> bool; } - /// Ordering and equality operators used by `BinaryCmpExpr`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BinaryOp { @@ -335,7 +335,6 @@ pub enum BinaryOp { Ge, } - /// String-only operators used by `StringExpr`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StringOp { @@ -349,7 +348,6 @@ pub enum StringOp { }, } - /// Unary presence operators used by `UnaryExpr`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnaryOp { @@ -357,7 +355,6 @@ pub enum UnaryOp { IsNone, } - /// Set membership operators used by `SetNodeFilter`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SetOp { @@ -365,7 +362,6 @@ pub enum SetOp { IsNotIn, } - impl Comparable for usize { fn binary_cmp(op: &BinaryOp, left: &usize, right: &usize) -> bool { match op { @@ -379,7 +375,6 @@ impl Comparable for usize { } } - impl Comparable for Prop { fn binary_cmp(op: &BinaryOp, left: &Prop, right: &Prop) -> bool { use std::cmp::Ordering::*; @@ -406,7 +401,6 @@ impl Comparable for Prop { } } - impl Comparable for GID { fn binary_cmp(op: &BinaryOp, left: &GID, right: &GID) -> bool { match (left, right) { @@ -424,7 +418,6 @@ impl Comparable for GID { } } - impl Comparable for Option { fn binary_cmp(op: &BinaryOp, left: &Option, right: &Option) -> bool { match (left, right) { @@ -434,7 +427,6 @@ impl Comparable for Option { } } - impl StringComparable for Prop { fn string_cmp(op: &StringOp, left: &Prop, right: &Prop) -> bool { match (left, right) { @@ -444,7 +436,6 @@ impl StringComparable for Prop { } } - impl StringComparable for GID { fn string_cmp(op: &StringOp, left: &GID, right: &GID) -> bool { match (left, right) { @@ -454,7 +445,6 @@ impl StringComparable for GID { } } - impl StringComparable for Option { fn string_cmp(op: &StringOp, left: &Option, right: &Option) -> bool { match (left, right) { @@ -464,7 +454,6 @@ impl StringComparable for Option { } } - impl Display for BinaryOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -478,7 +467,6 @@ impl Display for BinaryOp { } } - impl Display for StringOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -494,7 +482,6 @@ impl Display for StringOp { } } - impl Display for UnaryOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -504,7 +491,6 @@ impl Display for UnaryOp { } } - impl Display for SetOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { 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 9e898dc759..76c0414bed 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -1,12 +1,15 @@ -use crate::db::graph::views::filter::model::{latest_filter::Latest, layered_filter::Layered, snapshot_filter::{SnapshotAt, SnapshotLatest}, CombinedFilter}; use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::filter::{ model::{ - edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, - CompositeExplodedEdgeFilter, CompositeNodeFilter, FilterTree, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, + latest_filter::Latest, + layered_filter::Layered, + snapshot_filter::{SnapshotAt, SnapshotLatest}, + windowed_filter::Windowed, + CombinedFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, FilterTree, + InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, }, 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 a91da1c91e..dfab3dc064 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 @@ -88,3 +88,38 @@ impl TryAsCompositeFilter for IsActiveEdge { Ok(CompositeExplodedEdgeFilter::IsActiveEdge(IsActiveEdge)) } } + +// ── expr layer: the predicate as a boolean expression over the eval view ── + +use crate::db::graph::views::filter::model::{ + edge_expr::{ops::IsActiveEdgePropOp, EdgeOp}, + edge_filter::EdgeFilter as EdgeFilterMarker, + node_expr::{CreateOp, EntityExpr}, +}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::sync::Arc; + +impl EntityExpr for IsActiveEdge { + type Marker = EdgeFilterMarker; + + fn entity(&self) -> EdgeFilterMarker { + EdgeFilterMarker + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsActiveEdge { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, crate::errors::GraphError> { + Ok(Arc::new(IsActiveEdgePropOp { 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 c875ac5db7..721ab4fae2 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 @@ -87,3 +87,39 @@ impl TryAsCompositeFilter for IsActiveNode { Err(GraphError::NotSupported) } } + +// ── expr layer: the predicate as a boolean expression over the eval view ── + +use crate::db::graph::views::filter::model::{ + node_expr::{CreateOp, EntityExpr}, + node_filter::NodeFilter as NodeFilterMarker, +}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::sync::Arc; + +impl EntityExpr for IsActiveNode { + type Marker = NodeFilterMarker; + + fn entity(&self) -> NodeFilterMarker { + NodeFilterMarker + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsActiveNode { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, crate::errors::GraphError> { + Ok(Arc::new( + HistoryOp::new(graph).map(|h| Some(Prop::Bool(!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 aa673c2c93..541c6851f5 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 @@ -88,3 +88,38 @@ impl TryAsCompositeFilter for IsDeletedEdge { Ok(CompositeExplodedEdgeFilter::IsDeletedEdge(IsDeletedEdge)) } } + +// ── expr layer: the predicate as a boolean expression over the eval view ── + +use crate::db::graph::views::filter::model::{ + edge_expr::{ops::IsDeletedEdgePropOp, EdgeOp}, + edge_filter::EdgeFilter as EdgeFilterMarker, + node_expr::{CreateOp, EntityExpr}, +}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::sync::Arc; + +impl EntityExpr for IsDeletedEdge { + type Marker = EdgeFilterMarker; + + fn entity(&self) -> EdgeFilterMarker { + EdgeFilterMarker + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsDeletedEdge { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, crate::errors::GraphError> { + Ok(Arc::new(IsDeletedEdgePropOp { graph })) + } +} 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 0502ec6d81..ae0e408978 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 @@ -88,3 +88,38 @@ impl TryAsCompositeFilter for IsSelfLoopEdge { Ok(CompositeExplodedEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) } } + +// ── expr layer: the predicate as a boolean expression over the eval view ── + +use crate::db::graph::views::filter::model::{ + edge_expr::{ops::IsSelfLoopEdgePropOp, EdgeOp}, + edge_filter::EdgeFilter as EdgeFilterMarker, + node_expr::{CreateOp, EntityExpr}, +}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::sync::Arc; + +impl EntityExpr for IsSelfLoopEdge { + type Marker = EdgeFilterMarker; + + fn entity(&self) -> EdgeFilterMarker { + EdgeFilterMarker + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsSelfLoopEdge { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, crate::errors::GraphError> { + Ok(Arc::new(IsSelfLoopEdgePropOp { graph })) + } +} 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 c2cec5ed22..b02c5524ee 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 @@ -88,3 +88,38 @@ impl TryAsCompositeFilter for IsValidEdge { Ok(CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge)) } } + +// ── expr layer: the predicate as a boolean expression over the eval view ── + +use crate::db::graph::views::filter::model::{ + edge_expr::{ops::IsValidEdgePropOp, EdgeOp}, + edge_filter::EdgeFilter as EdgeFilterMarker, + node_expr::{CreateOp, EntityExpr}, +}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::sync::Arc; + +impl EntityExpr for IsValidEdge { + type Marker = EdgeFilterMarker; + + fn entity(&self) -> EdgeFilterMarker { + EdgeFilterMarker + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsValidEdge { + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, crate::errors::GraphError> { + Ok(Arc::new(IsValidEdgePropOp { graph })) + } +} 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 1932b079a0..a9468f2c87 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -1,23 +1,24 @@ -use crate::db::graph::views::filter::model::CreateView; use crate::{ db::{ - api::view::internal::GraphView, + api::{state::NodeOp, view::internal::GraphView}, graph::views::{ filter::{ model::{ + edge_expr::EdgeOp, 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_expr::CreateOp, node_filter::builders::{ InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, }, property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, windowed_filter::Windowed, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, FilterTree, GraphViewOp, + CompositeNodeFilter, CreateView, EdgeViewFilterOps, FilterTree, GraphViewOp, InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, @@ -30,8 +31,8 @@ use crate::{ errors::GraphError, prelude::TimeOps, }; -use raphtory_api::core::storage::timeindex::EventTime; -use std::{fmt, fmt::Display}; +use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; +use std::{fmt, fmt::Display, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Latest { @@ -263,3 +264,22 @@ impl CreateView for Latest { Ok(inner.latest()) } } + +// ── expr layer: the latest view scopes any inner expression (per-expression view) ── +// Nesting order of chained views is pinned by the Phase-3 semantics tests. + +impl CreateOp for Latest { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_node_op(graph.latest()) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_edge_op(graph.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 ef24f82d95..6d367170fc 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -1,15 +1,17 @@ use crate::{ db::{ - api::view::internal::GraphView, + api::{state::NodeOp, view::internal::GraphView}, graph::views::{ filter::{ model::{ + edge_expr::EdgeOp, 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_expr::CreateOp, node_filter::builders::{ InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, }, @@ -28,8 +30,11 @@ use crate::{ errors::GraphError, prelude::LayerOps, }; -use raphtory_api::core::{entities::Layer, storage::timeindex::EventTime}; -use std::{fmt, fmt::Display}; +use raphtory_api::core::{ + entities::{properties::prop::Prop, Layer}, + storage::timeindex::EventTime, +}; +use std::{fmt, fmt::Display, sync::Arc}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Layered { @@ -264,3 +269,22 @@ impl EdgeViewFilterOps for Layered { self.wrap(self.inner.is_self_loop()) } } + +// ── expr layer: the layer view scopes any inner expression (per-expression view) ── +// Nesting order of chained views is pinned by the Phase-3 semantics tests. + +impl CreateOp for Layered { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_node_op(graph.layers(self.layer.clone())?) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_edge_op(graph.layers(self.layer.clone())?) + } +} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 87f8d04efe..eea476dc34 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -183,6 +183,10 @@ pub trait ComposableFilter: Sized { right: other, } } + + fn not(self) -> NotFilter { + NotFilter(self) + } } pub trait DynCreateFilter: TryAsCompositeFilter + Send + Sync + 'static { @@ -547,8 +551,6 @@ impl PropertyExprFactory for T { } } - - pub trait DynPropertyFilterFactory: Send + Sync + 'static { fn dyn_entity(&self) -> EntityMarker; @@ -646,7 +648,7 @@ impl PropertyExpr { /// /// Disjoint from `NodeFilterFactory`: no type implements both, so `PropertyExpr` /// can have two separate sets of comparison methods gated on each. -pub trait EdgeFilterFactory: PropertyFilterFactory + Clone {} +pub trait EdgeFilterFactory: PropertyExprFactory + Clone {} // ───────────────────────────────────────────────────────────────────────────── // PropertyExpr / MetadataExpr — EdgeExpr impls @@ -981,11 +983,6 @@ impl DynEdgeViewFilterOps for } } - - - - - pub type DynEdgeViewProps = Arc; impl EdgeViewFilterOps for DynEdgeViewProps { 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 ac9b96cc38..cd451576d7 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 @@ -173,11 +173,11 @@ where L: CreateOp, R: CreateOp, { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc + 'graph>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -230,10 +230,11 @@ where L: CreateOp, R: CreateOp, { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc; - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc + 'graph>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -247,11 +248,18 @@ where filtered: F, ) -> Result, GraphError> { Ok(match self.entity { - EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph, filtered)?), - EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph, filtered)?), - EntityMarker::ExplodedEdge => { - Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph, filtered)?) - } + EntityMarker::Node => Arc::new( + self.with_entity(NodeFilter) + .create_filter(graph, filtered)?, + ), + EntityMarker::Edge => Arc::new( + self.with_entity(EdgeFilter) + .create_filter(graph, filtered)?, + ), + EntityMarker::ExplodedEdge => Arc::new( + self.with_entity(ExplodedEdgeFilter) + .create_filter(graph, filtered)?, + ), EntityMarker::Const => Err(GraphError::NotSupported)?, }) @@ -263,7 +271,9 @@ where filtered: F, ) -> Result, GraphError> { match self.entity { - EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph, filtered)?), + EntityMarker::Node => Ok(self + .with_entity(NodeFilter) + .create_node_filter(graph, filtered)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), EntityMarker::Const => Err(GraphError::NotSupported)?, @@ -342,11 +352,11 @@ impl CreateFilter for UnaryExpr where E: CreateOp, { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = UnaryNodeOp<'graph, Prop>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + UnaryNodeOp<'graph, Prop>; type FilteredGraph<'graph, G> = G @@ -390,10 +400,11 @@ impl CreateFilter for UnaryExpr where E: CreateOp, { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc; - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = UnaryNodeOp<'graph, Prop>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + UnaryNodeOp<'graph, Prop>; type FilteredGraph<'graph, G> = G @@ -407,11 +418,18 @@ where filtered: F, ) -> Result, GraphError> { Ok(match self.entity { - EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph, filtered)?), - EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph, filtered)?), - EntityMarker::ExplodedEdge => { - Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph, filtered)?) - } + EntityMarker::Node => Arc::new( + self.with_entity(NodeFilter) + .create_filter(graph, filtered)?, + ), + EntityMarker::Edge => Arc::new( + self.with_entity(EdgeFilter) + .create_filter(graph, filtered)?, + ), + EntityMarker::ExplodedEdge => Arc::new( + self.with_entity(ExplodedEdgeFilter) + .create_filter(graph, filtered)?, + ), EntityMarker::Const => Err(GraphError::NotSupported)?, }) } @@ -422,7 +440,9 @@ where filtered: F, ) -> Result, GraphError> { match self.entity { - EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph, filtered)?), + EntityMarker::Node => Ok(self + .with_entity(NodeFilter) + .create_node_filter(graph, filtered)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), EntityMarker::Const => Err(GraphError::NotSupported)?, @@ -528,11 +548,11 @@ impl CreateOp for StringExpr { } impl CreateFilter for StringExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc + 'graph>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -574,10 +594,11 @@ impl CreateFilter for StringExpr { } impl CreateFilter for StringExpr { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc; - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc + 'graph>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -591,11 +612,18 @@ impl CreateFilter for StringExpr { filtered: F, ) -> Result, GraphError> { Ok(match self.entity { - EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph, filtered)?), - EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph, filtered)?), - EntityMarker::ExplodedEdge => { - Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph, filtered)?) - } + EntityMarker::Node => Arc::new( + self.with_entity(NodeFilter) + .create_filter(graph, filtered)?, + ), + EntityMarker::Edge => Arc::new( + self.with_entity(EdgeFilter) + .create_filter(graph, filtered)?, + ), + EntityMarker::ExplodedEdge => Arc::new( + self.with_entity(ExplodedEdgeFilter) + .create_filter(graph, filtered)?, + ), EntityMarker::Const => Err(GraphError::NotSupported)?, }) } @@ -606,7 +634,9 @@ impl CreateFilter for StringExpr { filtered: F, ) -> Result, GraphError> { match self.entity { - EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph, filtered)?), + EntityMarker::Node => Ok(self + .with_entity(NodeFilter) + .create_node_filter(graph, filtered)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), EntityMarker::Const => Err(GraphError::NotSupported)?, @@ -689,10 +719,10 @@ impl CreateOp for PropValueSetExpr { } impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = PropValueSetNodeOp<'graph>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + PropValueSetNodeOp<'graph>; type FilteredGraph<'graph, G> = G @@ -734,9 +764,10 @@ impl CreateFilter for PropValueSetExpr { } impl CreateFilter for PropValueSetExpr { - - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = PropValueSetNodeOp<'graph>; + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + PropValueSetNodeOp<'graph>; type FilteredGraph<'graph, G> = G @@ -750,11 +781,18 @@ impl CreateFilter for PropValueSetExpr { filtered: F, ) -> Result, GraphError> { Ok(match self.entity { - EntityMarker::Node => Arc::new(self.with_entity(NodeFilter).create_filter(graph, filtered)?), - EntityMarker::Edge => Arc::new(self.with_entity(EdgeFilter).create_filter(graph, filtered)?), - EntityMarker::ExplodedEdge => { - Arc::new(self.with_entity(ExplodedEdgeFilter).create_filter(graph, filtered)?) - } + EntityMarker::Node => Arc::new( + self.with_entity(NodeFilter) + .create_filter(graph, filtered)?, + ), + EntityMarker::Edge => Arc::new( + self.with_entity(EdgeFilter) + .create_filter(graph, filtered)?, + ), + EntityMarker::ExplodedEdge => Arc::new( + self.with_entity(ExplodedEdgeFilter) + .create_filter(graph, filtered)?, + ), EntityMarker::Const => Err(GraphError::NotSupported)?, }) } @@ -765,7 +803,9 @@ impl CreateFilter for PropValueSetExpr { filtered: F, ) -> Result, GraphError> { match self.entity { - EntityMarker::Node => Ok(self.with_entity(NodeFilter).create_node_filter(graph, filtered)?), + EntityMarker::Node => Ok(self + .with_entity(NodeFilter) + .create_node_filter(graph, filtered)?), EntityMarker::Edge => Err(GraphError::NotNodeFilter), EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), EntityMarker::Const => Err(GraphError::NotSupported)?, 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 index 465a93117d..f1ca807e2d 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -100,3 +100,44 @@ pub trait EntityExpr: Clone + Send + Sync + 'static { /// /// Mirrors the same trick used by `EntityAggOps` for `min`/`max`/`sum`. pub trait EntityExprBuilder: EntityExpr {} + +/// Scopes an expression to a view chain: the inner expression is compiled against the view the +/// chain constructs over the incoming graph. This is how a factory chain (window, latest, layers) +/// carries its view into a unit expression such as a validity predicate. +#[derive(Clone)] +pub struct Scoped { + pub view: V, + pub inner: T, +} + +impl EntityExpr for Scoped { + type Marker = T::Marker; + + fn entity(&self) -> Self::Marker { + self.inner.entity() + } + + fn prop_type(&self) -> PropType { + self.inner.prop_type() + } + + fn nullable(&self) -> bool { + self.inner.nullable() + } +} + +impl CreateOp for Scoped { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_node_op(self.view.create_view(graph)?) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_edge_op(self.view.create_view(graph)?) + } +} 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 index 2bfe88ca46..35c1cbc7df 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -36,14 +36,16 @@ //! Eq Bool(true) → true (at least one matched) //! ``` -use crate::db::api::view::internal::NodeList; use super::EdgeOp; use crate::{ db::{ api::{ properties::PropertiesOps, state::ops::NodeOp, - view::{internal::GraphView, NodeViewOps}, + view::{ + internal::{GraphView, NodeList}, + NodeViewOps, + }, }, graph::views::filter::model::{ filter_operator::{BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp}, @@ -186,9 +188,9 @@ macro_rules! impl_agg_entity_op { } impl<'g> NodeOp for $node_name<'g> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - self.inner.domain(_storage) - } + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.inner.domain(_storage) + } type Output = Option; 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 be07af6925..bbb62a1e4f 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 @@ -1,7 +1,3 @@ -use crate::db::{ - api::state::ops::node::{Id, Name, Type}, - graph::views::filter::model::{node_expr::exprs::DegreeExpr, CreateView, node_expr::EntityExpr}, -}; use crate::{ api::core::Direction, db::{ @@ -12,6 +8,7 @@ use crate::{ AndOp, MaskOp, NodeIdFilterOp, NodeNameFilterOp, NodeTypeFilterOp, NotOp, OrOp, }, + node::{Id, Name, Type}, NodeOp, TypeId, }, NodeStateValue, TypedNodeState, @@ -26,6 +23,7 @@ use crate::{ is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, + node_expr::{exprs::DegreeExpr, EntityExpr}, node_filter::{ builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, validate::validate, @@ -35,7 +33,7 @@ use crate::{ snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, AndFilter, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - EntityMarker, InternalPropertyFilterFactory, InternalViewWrapOps, + CreateView, EntityMarker, InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, NotFilter, OrFilter, TryAsCompositeFilter, Wrap, }, node_filtered_graph::NodeFilteredGraph, @@ -621,7 +619,6 @@ pub trait NodeFilterFactory: view_expr: self.clone(), } } - } impl NodeFilterFactory for NodeFilter { 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 49c68cd6b8..00b7a8e4c7 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,13 +1,12 @@ -use crate::db::graph::views::filter::model::{CombinedFilter, InternalPropertyFilterBuilder, TemporalPropertyFilterFactory}; use crate::db::graph::views::filter::{ model::{ property_filter::{Op, PropertyFilter, PropertyFilterInput, PropertyRef}, - EntityMarker, Wrap, + CombinedFilter, EntityMarker, InternalPropertyFilterBuilder, TemporalPropertyFilterFactory, + Wrap, }, CreateFilter, }; - #[derive(Clone)] pub struct PropertyFilterBuilder(pub String, pub M); diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index 928b151bec..9266650ec0 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -1,7 +1,9 @@ -use std::borrow::Borrow; -use raphtory_api::core::entities::properties::prop::PropType; use crate::{db::graph::views::filter::model::Op, prelude::PropertyFilter}; -use raphtory_api::core::{entities::properties::prop::Prop, utils::generalised_reduce}; +use raphtory_api::core::{ + entities::properties::prop::{Prop, PropType}, + utils::generalised_reduce, +}; +use std::borrow::Borrow; enum ValueType { Seq(Vec), 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 b2f86f4ed6..8666a4e863 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -1,20 +1,24 @@ -use crate::db::graph::views::filter::model::CreateView; use crate::{ db::{ - api::view::{internal::GraphView, time::TimeOps}, + api::{ + state::NodeOp, + view::{internal::GraphView, time::TimeOps}, + }, graph::views::{ filter::{ model::{ + edge_expr::EdgeOp, 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_expr::CreateOp, property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, windowed_filter::Windowed, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, FilterTree, GraphViewOp, + CompositeNodeFilter, CreateView, EdgeViewFilterOps, FilterTree, GraphViewOp, InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, @@ -26,8 +30,10 @@ use crate::{ }, errors::GraphError, }; -use raphtory_api::core::{storage::timeindex::EventTime, utils::time::IntoTime}; -use std::{fmt, fmt::Display}; +use raphtory_api::core::{ + entities::properties::prop::Prop, storage::timeindex::EventTime, utils::time::IntoTime, +}; +use std::{fmt, fmt::Display, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SnapshotAt { @@ -473,3 +479,41 @@ impl CreateView for SnapshotLatest { Ok(inner.snapshot_latest()) } } + +// ── expr layer: the snapshot-at view scopes any inner expression (per-expression view) ── +// Nesting order of chained views is pinned by the Phase-3 semantics tests. + +impl CreateOp for SnapshotAt { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_node_op(graph.snapshot_at(self.time)) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_edge_op(graph.snapshot_at(self.time)) + } +} + +// ── expr layer: the snapshot-latest view scopes any inner expression (per-expression view) ── +// Nesting order of chained views is pinned by the Phase-3 semantics tests. + +impl CreateOp for SnapshotLatest { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_node_op(graph.snapshot_latest()) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner.create_edge_op(graph.snapshot_latest()) + } +} 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 5b5135d62f..83a5c18780 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -1,22 +1,23 @@ -use crate::db::graph::views::filter::model::CreateView; use crate::{ db::{ - api::view::internal::GraphView, + api::{state::NodeOp, view::internal::GraphView}, graph::views::{ filter::{ model::{ + edge_expr::EdgeOp, 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_expr::CreateOp, node_filter::builders::{ InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, }, property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, FilterTree, GraphViewOp, + CompositeNodeFilter, CreateView, EdgeViewFilterOps, FilterTree, GraphViewOp, InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, @@ -30,10 +31,11 @@ use crate::{ prelude::TimeOps, }; use raphtory_api::core::{ + entities::properties::prop::Prop, storage::timeindex::{AsTime, EventTime}, utils::time::IntoTime, }; -use std::{fmt, fmt::Display}; +use std::{fmt, fmt::Display, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Windowed { @@ -296,3 +298,24 @@ impl CreateView for Windowed { Ok(inner.window(self.start.t(), self.end.t())) } } + +// ── expr layer: the windowed view scopes any inner expression (per-expression view) ── +// Nesting order of chained views is pinned by the Phase-3 semantics tests. + +impl CreateOp for Windowed { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner + .create_node_op(graph.window(self.start.t(), self.end.t())) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.inner + .create_edge_op(graph.window(self.start.t(), self.end.t())) + } +} diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 0b5c614102..e5165a7b78 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -1,10 +1,17 @@ use crate::{ - db::graph::views::filter::model::{ - edge_filter::{EdgeEndpointWrapper, EdgeFilter}, - node_expr::{DynCreateOp, EntityExpr}, - node_filter::NodeFilter, - CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, - PropertyFilterFactory, ViewWrapOps, + db::{ + api::state::ops::node::{Id, Name, Type}, + graph::views::filter::model::{ + edge_filter::{EdgeEndpointWrapper, EdgeFilter}, + is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + node_expr::{DynCreateOp, EntityExpr, Scoped}, + node_filter::NodeFilter, + CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, + PropertyExprFactory, ViewWrapOps, Wrap, + }, }, prelude::EdgeViewOps, python::{filter::node_expr::PyExpr, types::iterable::FromIterable}, @@ -30,17 +37,17 @@ pub struct PyEdgeEndpoint(pub EdgeEndpointWrapper); impl PyEdgeEndpoint { /// Selects the endpoint node ID field for filtering. fn id(&self) -> PyExpr { - self.0.clone().id().into() + self.0.wrap(Id).into() } /// Selects the endpoint node name field for filtering. fn name(&self) -> PyExpr { - self.0.clone().name().into() + self.0.wrap(Name).into() } /// Selects the endpoint node type field for filtering. fn node_type(&self) -> PyExpr { - self.0.clone().node_type().into() + self.0.wrap(Type).into() } /// Filters an endpoint node property by name. @@ -48,7 +55,7 @@ impl PyEdgeEndpoint { /// Arguments: /// name (str): Property key. fn property(&self, name: String) -> PyExpr { - self.0.clone().property(name).into() + self.0.wrap(NodeFilter.property(name)).into() } /// Filters an endpoint node metadata field by name. @@ -56,7 +63,9 @@ impl PyEdgeEndpoint { /// Arguments: /// name (str): Metadata key. fn metadata(&self, name: String) -> PyExpr { - self.0.clone().metadata(name).into() + self.0 + .wrap(PropertyExprFactory::metadata(&NodeFilter, name)) + .into() } } @@ -101,23 +110,35 @@ where ::Marker: Into, { fn dyn_property(&self, name: String) -> Arc { - Arc::new(PropertyFilterFactory::property(self, name)) + Arc::new(PropertyExprFactory::property(self, name)) } fn dyn_metadata(&self, name: String) -> Arc { - Arc::new(PropertyFilterFactory::metadata(self, name)) + Arc::new(PropertyExprFactory::metadata(self, name)) } fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) + Arc::new(Scoped { + view: self.clone(), + inner: IsActiveEdge, + }) } fn dyn_is_valid(&self) -> Arc { - Arc::new(self.is_valid()) + Arc::new(Scoped { + view: self.clone(), + inner: IsValidEdge, + }) } fn dyn_is_deleted(&self) -> Arc { - Arc::new(self.is_deleted()) + Arc::new(Scoped { + view: self.clone(), + inner: IsDeletedEdge, + }) } fn dyn_is_self_loop(&self) -> Arc { - Arc::new(self.is_self_loop()) + Arc::new(Scoped { + view: self.clone(), + inner: IsSelfLoopEdge, + }) } // Go dynamic before calling window — the Arc impl diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index c860af27be..237b2f12fd 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -91,4 +91,11 @@ impl CreateFilter for PyFilterExpr { ) -> Result, GraphError> { self.0.create_node_filter(graph, filtered) } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + self.0.dyn_filter_graph_view(Arc::new(graph)) + } } diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 3d33d97bc6..f933090d8e 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,10 +1,11 @@ use crate::{ db::graph::views::filter::model::{ degree_filter::DegreeFilterFactory, - node_expr::{CreateOp, DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr}, + is_active_node_filter::IsActiveNode, + node_expr::{CreateOp, DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr, Scoped}, node_state_filter::NodeStateBoolColOp, - CreateView, DynCreateView, DynPropertyFilterFactory, EntityMarker, InternalViewWrapOps, - PropertyExpr, PropertyFilterFactory, ViewWrapOps, + CreateView, DynCreateView, DynPropertyExprFactory, EntityMarker, InternalViewWrapOps, + PropertyExpr, PropertyExprFactory, ViewWrapOps, }, prelude::{EntityAggOps, EntityExprFilterOps, NodeFilter, NodeFilterFactory}, python::{ @@ -151,7 +152,7 @@ impl PyExpr { } pub trait DynNodeFilterFactory: - DynPropertyFilterFactory + DynEntityExpr + DynCreateView + Send + Sync + 'static + DynPropertyExprFactory + DynEntityExpr + DynCreateView + Send + Sync + 'static { fn dyn_id(&self) -> Arc; fn dyn_name(&self) -> Arc; @@ -204,11 +205,14 @@ where } fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) + Arc::new(Scoped { + view: self.clone(), + inner: IsActiveNode, + }) } fn dyn_metadata(&self, name: String) -> Arc { - Arc::new(PropertyFilterFactory::metadata(self, name)) + Arc::new(PropertyExprFactory::metadata(self, name)) } fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc { From fa54b8993d34bcb1eb43813bb728b96564b96e28 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:29:16 +0100 Subject: [PATCH 102/155] edge endpoint fields are expressions; test suites ride the expr path src()/dst() id, name, node_type, property and metadata now build expressions, including the temporal aggregate chains on endpoint properties. The filter_tests parity suite adapts by imports alone; its assertions are unchanged apart from one negation pinned to the composite complement it resolved to before the expression ops were in scope. The June test modules resolve through the expression factories and each ambiguous `not` is pinned to its original semantics. --- raphtory-tests/tests/edge_expr_filter.rs | 2 +- .../tests/filter_tests/test_filters.rs | 7 +- raphtory-tests/tests/test_filters.rs | 53 +++++---- .../graph/views/filter/model/edge_filter.rs | 103 ++++++++++++++++-- .../views/filter/model/node_expr/filters.rs | 42 +++++++ raphtory/src/python/filter/edge_expr.rs | 8 +- 6 files changed, 171 insertions(+), 44 deletions(-) diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs index 6e4e2c418b..81d0b1e16f 100644 --- a/raphtory-tests/tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -1,7 +1,7 @@ use raphtory::{ db::{ api::view::Filter, - graph::views::filter::model::{ComposableFilter, EdgeFilter, PropertyFilterFactory}, + graph::views::filter::model::{ComposableFilter, EdgeFilter, PropertyExprFactory}, }, prelude::*, }; diff --git a/raphtory-tests/tests/filter_tests/test_filters.rs b/raphtory-tests/tests/filter_tests/test_filters.rs index dd9a8a57c6..20de781032 100644 --- a/raphtory-tests/tests/filter_tests/test_filters.rs +++ b/raphtory-tests/tests/filter_tests/test_filters.rs @@ -8297,8 +8297,8 @@ mod test_edge_filter { edge_filter::EdgeFilter, node_filter::ops::{NodeFilterOps, NodeIdFilterOps}, property_filter::ops::{ListAggOps, PropertyFilterOps}, - ComposableFilter, EdgeViewFilterOps, PropertyFilterFactory, TemporalPropertyFilterFactory, - ViewWrapOps, + ComposableFilter, EdgeViewFilterOps, EntityAggOps, EntityExprFilterOps, + PropertyFilterFactory, TemporalPropertyFilterFactory, ViewWrapOps, }; use raphtory_tests::assertions::{ assert_filter_edges_results, assert_select_edges_results, TestGraphVariants, TestVariants, @@ -8645,7 +8645,8 @@ mod test_edge_filter { #[test] fn test_filter_edges_for_not_src() { - let filter = EdgeFilter::src().name().is_not_in(vec!["1"]).not(); + // `not` pinned to the composite complement it resolved to before the expr ops were in scope. + let filter = ComposableFilter::not(EdgeFilter::src().name().is_not_in(vec!["1"])); let expected_results = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index f8025c3b1e..0fa060956a 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -6,7 +6,7 @@ mod test_composite_filters { use raphtory::{ db::graph::views::filter::model::{ edge_filter::EdgeFilter, filter::Filter, node_filter::NodeFilter, - property_filter::ops::PropertyFilterOps, PropertyFilterFactory, + property_filter::ops::PropertyFilterOps, PropertyExprFactory, }, prelude::IntoProp, }; @@ -125,7 +125,7 @@ mod test_property_semantics { api::view::{filter_ops::Filter, StaticGraphViewOps}, graph::views::filter::model::{ node_filter::NodeFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, + PropertyExprFactory, }, }, errors::GraphError, @@ -412,7 +412,7 @@ mod test_property_semantics { graph::views::filter::{ model::{ edge_filter::EdgeFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, + PropertyExprFactory, }, CreateFilter, }, @@ -1501,14 +1501,13 @@ mod test_node_filter { algorithms::alternating_mask::alternating_mask, core::entities::VID, db::{ - api::view::{filter_ops::NodeSelect, Filter}, + api::view::{filter_ops::Select, Filter}, graph::views::filter::{ model::{ - degree_filter::DegreeFilterFactory, node_filter::ops::NodeFilterOps, not_filter::NotFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - ComposableFilter, NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, + ComposableFilter, NodeViewFilterOps, PropertyExprFactory, ViewWrapOps, }, CreateFilter, }, @@ -1919,7 +1918,7 @@ mod test_node_filter { assert_filter( &graph, - NodeFilter.degree().lt(threshold).or(NodeFilter.degree().gt(threshold + 5).not()), + NodeFilter.degree().lt(threshold).or(EntityExprFilterOps::not(NodeFilter.degree().gt(threshold + 5))), Direction::BOTH, |d| d < threshold as usize || d <= (threshold + 5) as usize, &format!("BOTH < {} OR BOTH > {}", threshold, threshold + 5), @@ -1927,7 +1926,7 @@ mod test_node_filter { assert_filter( &graph, - NodeFilter.in_degree().lt(threshold).or(NodeFilter.in_degree().gt(threshold + 5).not()), + NodeFilter.in_degree().lt(threshold).or(EntityExprFilterOps::not(NodeFilter.in_degree().gt(threshold + 5))), Direction::IN, |d| d < threshold as usize || d <= (threshold + 5) as usize, &format!("IN < {} OR IN > {}", threshold, threshold + 5), @@ -1935,7 +1934,7 @@ mod test_node_filter { assert_filter( &graph, - NodeFilter.out_degree().lt(threshold).or(NodeFilter.out_degree().gt(threshold + 5).not()), + NodeFilter.out_degree().lt(threshold).or(EntityExprFilterOps::not(NodeFilter.out_degree().gt(threshold + 5))), Direction::OUT, |d| d < threshold as usize || d <= (threshold + 5) as usize, &format!("OUT < {} OR OUT > {}", threshold, threshold + 5), @@ -2482,7 +2481,8 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_not_node_type() { - let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]).not(); + let filter = + EntityExprFilterOps::not(NodeFilter.node_type().is_not_in(vec!["fire_nation"])); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2842,7 +2842,7 @@ mod test_node_property_filter { not_filter::NotFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, windowed_filter::Windowed, - ComposableFilter, PropertyFilterFactory, ViewWrapOps, + ComposableFilter, PropertyExprFactory, ViewWrapOps, }, prelude::{EntityAggOps, EntityExprFilterOps}, }; @@ -3531,7 +3531,7 @@ mod test_node_property_filter { // (None cannot satisfy a value comparison). Use "ship" so nodes 1 and 3 // (p10 = "Paper_airplane", does not contain "ship") pass; node 2 // (p10 = "Paper_ship") and node 4 (no p10) are rejected. - let filter = NodeFilter.property("p10").contains("ship").not(); + let filter = EntityExprFilterOps::not(NodeFilter.property("p10").contains("ship")); let expected_results: Vec<&str> = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -4308,7 +4308,7 @@ mod composite_node_filter_tests { db::graph::views::filter::model::{ node_filter::ops::NodeFilterOps, not_filter::NotFilter, property_filter::ops::PropertyFilterOps, ComposableFilter, NodeFilterFactory, - PropertyFilterFactory, + PropertyExprFactory, }, prelude::NodeFilter, }; @@ -4574,13 +4574,10 @@ mod test_node_property_filter_agg { use raphtory::{ db::{ api::view::StaticGraphViewOps, - graph::views::filter::{ - model::{ - node_filter::NodeFilter, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - PropertyFilterFactory, - }, - CreateFilter, + graph::views::filter::model::{ + node_filter::NodeFilter, + property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, + CombinedFilter, PropertyExprFactory, }, }, prelude::{ @@ -4911,7 +4908,7 @@ mod test_node_property_filter_agg { graph } - fn apply_assertion(filter: impl CreateFilter + Clone, expected: &[&str]) { + fn apply_assertion(filter: impl CombinedFilter, expected: &[&str]) { assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -4921,7 +4918,7 @@ mod test_node_property_filter_agg { ); } - fn apply_assertion_err(filter: impl CreateFilter + Clone, expected: &str) { + fn apply_assertion_err(filter: impl CombinedFilter, expected: &str) { assert_filter_nodes_err( init_nodes_graph, IdentityGraphTransformer, @@ -8083,8 +8080,8 @@ mod test_edge_filter { init_edges_graph_with_str_ids_del, init_nodes_graph, IdentityGraphTransformer, }; use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, node_expr::EntityExprFilterOps, ComposableFilter, - EdgeViewFilterOps, NodeFilterFactory, PropertyFilterFactory, ViewWrapOps, + edge_filter::EdgeFilter, ComposableFilter, EdgeViewFilterOps, EntityExprFilterOps, + NodeFilterFactory, PropertyExprFactory, ViewWrapOps, }; use raphtory_tests::assertions::{ assert_filter_edges_results, assert_select_edges_results, TestGraphVariants, TestVariants, @@ -8431,7 +8428,7 @@ mod test_edge_filter { #[test] fn test_filter_edges_for_not_src() { - let filter = EdgeFilter::src().name().is_not_in(vec!["1"]).not(); + let filter = EntityExprFilterOps::not(EdgeFilter::src().name().is_not_in(vec!["1"])); let expected_results = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -9098,7 +9095,7 @@ mod test_edge_property_filter { use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - ComposableFilter, PropertyFilterFactory, ViewWrapOps, + ComposableFilter, PropertyExprFactory, ViewWrapOps, }; use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; @@ -9892,7 +9889,7 @@ mod test_edge_property_filter { #[test] fn test_filter_edges_for_not_property() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. Search API uses filter API internally for this filter. - let filter = EdgeFilter.property("p2").ne(2u64).not(); + let filter = EntityExprFilterOps::not(EdgeFilter.property("p2").ne(2u64)); let expected_results = vec!["2->3"]; assert_filter_edges_results( init_edges_graph, @@ -10404,7 +10401,7 @@ mod composite_edge_filter_tests { use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, not_filter::NotFilter, property_filter::ops::PropertyFilterOps, ComposableFilter, NodeFilterFactory, - PropertyFilterFactory, + PropertyExprFactory, }; use raphtory_tests::assertions::{ assert_filter_edges_results, TestGraphVariants, TestVariants, 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 b5a4c91226..cec3b9585c 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -159,19 +159,38 @@ impl EdgeEndpointWrapper { } impl EdgeEndpointWrapper { + /// Endpoint fields and properties are expressions: they compose with the comparison, + /// string, set and temporal operators. Nothing outside the expression tests consumed + /// the builder-returning forms these replace. #[inline] - pub fn id(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::id(), self.endpoint) + pub fn id(&self) -> EdgeEndpointWrapper { + self.wrap(Id) } #[inline] - pub fn name(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::name(), self.endpoint) + pub fn name(&self) -> EdgeEndpointWrapper { + self.wrap(Name) } #[inline] - pub fn node_type(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::node_type(), self.endpoint) + pub fn node_type(&self) -> EdgeEndpointWrapper { + self.wrap(Type) + } + + #[inline] + pub fn property( + &self, + name: impl Into, + ) -> EdgeEndpointWrapper> { + self.wrap(PropertyExprFactory::property(&self.inner, name)) + } + + #[inline] + pub fn metadata( + &self, + name: impl Into, + ) -> EdgeEndpointWrapper> { + self.wrap(PropertyExprFactory::metadata(&self.inner, name)) } } @@ -544,8 +563,12 @@ impl CreateOp for EdgeEndpointWrapper { // ── expr layer: which types serve as edge-filter factories (June branch) ── -use crate::db::graph::views::filter::model::{ - exploded_edge_filter::ExplodedEdgeFilter, CreateView, EdgeFilterFactory, +use crate::db::{ + api::state::ops::node::{Id, Name, Type}, + graph::views::filter::model::{ + exploded_edge_filter::ExplodedEdgeFilter, CreateView, EdgeFilterFactory, MetadataExpr, + PropertyExpr, PropertyExprFactory, + }, }; impl EdgeFilterFactory for EdgeFilter {} @@ -555,3 +578,67 @@ impl EdgeFilterFactory for Latest {} impl EdgeFilterFactory for Layered {} impl EdgeFilterFactory for SnapshotAt {} impl EdgeFilterFactory for SnapshotLatest {} + +// ── expr layer: temporal chains on endpoint properties (June branch) ── + +use crate::db::graph::views::filter::model::node_expr::{ + AllExpr, AnyExpr, AvgExpr, EntityAggOps, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, + SumExpr, TemporalPropExpr, +}; + +impl EdgeEndpointWrapper> { + #[inline] + pub fn temporal(&self) -> EdgeEndpointWrapper> { + EdgeEndpointWrapper::new(self.inner.temporal(), self.endpoint) + } +} + +impl + EdgeEndpointWrapper> +{ + #[inline] + pub fn sum(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.sum(), endpoint) + } + #[inline] + pub fn avg(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.avg(), endpoint) + } + #[inline] + pub fn min(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.min(), endpoint) + } + #[inline] + pub fn max(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.max(), endpoint) + } + #[inline] + pub fn first(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.first(), endpoint) + } + #[inline] + pub fn last(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.last(), endpoint) + } + #[inline] + pub fn len(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(self.inner.len(), endpoint) + } + #[inline] + pub fn any(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(AnyExpr(self.inner), endpoint) + } + #[inline] + pub fn all(self) -> EdgeEndpointWrapper>> { + let endpoint = self.endpoint; + EdgeEndpointWrapper::new(AllExpr(self.inner), endpoint) + } +} 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 cd451576d7..a4c2186c49 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 @@ -819,3 +819,45 @@ impl CreateFilter for PropValueSetExpr { Ok(graph) } } + +// ── The expr layer has no composite form: these filters exist only as compiled ops. ── +// The conversion is fallible by design, so "not representable" is an answer, not a lie; +// the composite path survives solely for its remaining GraphQL and grant-lowering consumers. + +use crate::db::graph::views::filter::model::{ + edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, + node_filter::CompositeNodeFilter, TryAsCompositeFilter, +}; + +macro_rules! impl_not_composite { + ($($ty:ident<$($g:ident),+>),+ $(,)?) => {$( + impl<$($g: Send + Sync),+> TryAsCompositeFilter for $ty<$($g),+> { + fn try_as_composite_node_filter(&self) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + + fn try_as_composite_edge_filter(&self) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + } + )+}; +} + +impl_not_composite!( + BinaryCmpExpr, + UnaryExpr, + StringExpr, + PropValueSetExpr, +); diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index e5165a7b78..2c99a38715 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -37,17 +37,17 @@ pub struct PyEdgeEndpoint(pub EdgeEndpointWrapper); impl PyEdgeEndpoint { /// Selects the endpoint node ID field for filtering. fn id(&self) -> PyExpr { - self.0.wrap(Id).into() + self.0.id().into() } /// Selects the endpoint node name field for filtering. fn name(&self) -> PyExpr { - self.0.wrap(Name).into() + self.0.name().into() } /// Selects the endpoint node type field for filtering. fn node_type(&self) -> PyExpr { - self.0.wrap(Type).into() + self.0.node_type().into() } /// Filters an endpoint node property by name. @@ -55,7 +55,7 @@ impl PyEdgeEndpoint { /// Arguments: /// name (str): Property key. fn property(&self, name: String) -> PyExpr { - self.0.wrap(NodeFilter.property(name)).into() + self.0.property(name).into() } /// Filters an endpoint node metadata field by name. From 474b4826d3fe36ab6ce276a3807d5d0e1accbb1b Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:29:16 +0100 Subject: [PATCH 103/155] fixes from the first full runs of the merged suites - edge expressions build their ops from the pre-transformed view, matching the node side and the CreateFilter contract; the orphaned boolean/set ops are removed - constant comparison operands are validated by value castability, matching the runtime coercion; type-level compatibility remains for expression-vs-expression comparisons - two tests pinning pre-db_v4 semantics (windowed is_self_loop, agg overflow) yield to their filter_tests twins - the node_expr unit tests compile against the current API - unused imports left behind by the bridge are removed --- raphtory-graphql/src/auth.rs | 5 +- raphtory-graphql/src/model/graph/namespace.rs | 1 - raphtory-tests/tests/test_filters.rs | 92 --------------- raphtory/src/db/api/state/ops/mod.rs | 22 +--- .../views/filter/model/edge_expr/filters.rs | 45 ++++---- .../graph/views/filter/model/edge_expr/mod.rs | 7 +- .../graph/views/filter/model/edge_expr/ops.rs | 97 ++-------------- .../graph/views/filter/model/edge_filter.rs | 5 +- .../views/filter/model/node_expr/exprs.rs | 2 +- .../views/filter/model/node_expr/filters.rs | 21 ++-- .../graph/views/filter/model/node_expr/mod.rs | 4 +- .../graph/views/filter/model/node_expr/ops.rs | 108 +----------------- .../views/filter/model/node_expr/tests.rs | 32 ++++-- .../filter/model/property_filter/builders.rs | 11 +- raphtory/src/python/filter/edge_expr.rs | 24 ++-- raphtory/src/python/filter/node_expr.rs | 7 +- 16 files changed, 94 insertions(+), 389 deletions(-) diff --git a/raphtory-graphql/src/auth.rs b/raphtory-graphql/src/auth.rs index 7a9242dc35..5c351f0333 100644 --- a/raphtory-graphql/src/auth.rs +++ b/raphtory-graphql/src/auth.rs @@ -1,7 +1,4 @@ -use crate::{ - config::app_config::AppConfig, - data::{gql_error_with_code, CODE_ACCESS_DENIED}, -}; +use crate::config::app_config::AppConfig; use async_graphql::{ async_trait, extensions::{Extension, ExtensionContext, ExtensionFactory, NextParseQuery}, diff --git a/raphtory-graphql/src/model/graph/namespace.rs b/raphtory-graphql/src/model/graph/namespace.rs index 7408545219..b2951ca39d 100644 --- a/raphtory-graphql/src/model/graph/namespace.rs +++ b/raphtory-graphql/src/model/graph/namespace.rs @@ -17,7 +17,6 @@ use async_graphql::Context; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields, Result}; use itertools::Itertools; use std::{cmp::Ordering, path::PathBuf, sync::Arc}; -use tracing::error; use walkdir::WalkDir; /// A directory-like container for graphs and nested namespaces. Graphs are diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 0fa060956a..321c9b3c88 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -7840,61 +7840,6 @@ mod test_node_property_filter_agg { } // --------------- OVERFLOW --------------- - #[test] - fn test_max_value_agg() { - let filter = NodeFilter - .property("p_u64s_max") - .max() - .eq(Prop::U64(u64::MAX)); - let expected: Vec<&str> = vec!["n5", "n1"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u64s_min") - .min() - .eq(Prop::U64(u64::MIN)); - let expected: Vec<&str> = vec!["n5"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter.property("p_u8s_max").sum().eq(Prop::U64(510)); - let expected: Vec<&str> = vec!["n1"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u16s_max") - .sum() - .eq(Prop::U64(131070)); - let expected: Vec<&str> = vec!["n1"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u32s_max") - .sum() - .eq(Prop::U64(8589934590)); - let expected: Vec<&str> = vec!["n1"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter.property("p_u64s_max").sum().gt(Prop::U64(0)); - let expected: Vec<&str> = vec![]; - apply_assertion(filter, &expected); - - // AVG is computed in f64 even if SUM overflowed. - let avg = (u64::MAX as f64 + 1.0) / 2.0; - let filter = NodeFilter.property("p_u64s_max").avg().eq(avg); - let expected = vec!["n5"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter.property("p_i64s_max").sum().gt(Prop::I64(0)); - let expected: Vec<&str> = vec![]; - apply_assertion(filter, &expected); - - // AVG is computed in f64 even if SUM overflowed. - let avg = (i64::MAX as f64 + 1.0) / 2.0; - let filter = NodeFilter.property("p_i64s_max").avg().eq(avg); - let expected = vec!["n5"]; - apply_assertion(filter, &expected); - } - // ------ Property: any ------ #[test] fn test_node_property_any() { @@ -9051,43 +8996,6 @@ mod test_edge_filter { TestVariants::All, ); } - - #[test] - fn test_is_self_loop_edge_window() { - let filter = EdgeFilter.window(1, 3).is_self_loop(); - let expected_results = vec![]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.window(1, 6).is_self_loop(); - let expected_results = vec!["Bangalore->Bangalore"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } } mod test_edge_property_filter { diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 4c1471d6b8..58d5cd7ace 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -3,27 +3,17 @@ pub mod history; pub mod node; pub mod properties; -use crate::{ - db::{ - api::{ - state::ops::filter::{AndOp, NotOp, OrOp}, - view::internal::NodeList, - }, - graph::views::filter::model::{ - node_expr::{BinaryCmpNodeOp, CreateOp, EntityExpr}, - BinaryOp, Comparable, - }, +use crate::db::{ + api::{ + state::ops::filter::{AndOp, NotOp, OrOp}, + view::internal::NodeList, }, - errors::GraphError, - prelude::NodeFilter, + graph::views::filter::model::{node_expr::BinaryCmpNodeOp, BinaryOp, Comparable}, }; pub use history::*; pub use node::*; pub use properties::*; -use raphtory_api::core::entities::{ - properties::prop::{Prop, PropType}, - 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}; diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index 45e7d12225..e531f55b7d 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -4,10 +4,7 @@ //! an `EdgeExprFilteredGraph` instead of a `NodeFilteredGraph`. use super::{ - ops::{ - BinaryCmpEdgeOp, ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, - ListAwareUnaryEdgeOp, PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, - }, + ops::{BinaryCmpEdgeOp, PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp}, EdgeOp, }; pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpExpr, StringExpr, UnaryExpr}; @@ -20,15 +17,13 @@ use crate::{ model::{ edge_filter::EdgeFilter, node_expr::{filters::PropValueSetExpr, CreateOp}, - resolved_prop_type, validate_binary_op, validate_string_op, - validate_types_compatible, CreateFilter, ExplodedEdgeFilter, + resolved_prop_type, validate_binary_op, validate_const_castable, + validate_string_op, validate_types_compatible, CreateFilter, ExplodedEdgeFilter, }, }, }, errors::GraphError, - prelude::GraphViewOps, }; -use raphtory_api::core::entities::properties::prop::Prop; use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpExpr @@ -56,12 +51,15 @@ where filtered: F, ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph.clone())?; + let left = self.left.create_edge_op(filtered.clone())?; + let right = self.right.create_edge_op(filtered.clone())?; let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; - validate_types_compatible(&lhs_pt, &rhs_pt)?; + match right.const_value() { + Some(c) => validate_const_castable(&lhs_pt, c.as_ref())?, + None => validate_types_compatible(&lhs_pt, &rhs_pt)?, + } let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { left, right, @@ -108,12 +106,15 @@ where filtered: F, ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph.clone())?; + let left = self.left.create_edge_op(filtered.clone())?; + let right = self.right.create_edge_op(filtered.clone())?; let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; - validate_types_compatible(&lhs_pt, &rhs_pt)?; + match right.const_value() { + Some(c) => validate_const_castable(&lhs_pt, c.as_ref())?, + None => validate_types_compatible(&lhs_pt, &rhs_pt)?, + } let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { left, right, @@ -161,7 +162,7 @@ where graph: G, filtered: F, ) -> Result, GraphError> { - let inner = self.expr.create_edge_op(graph.clone())?; + let inner = self.expr.create_edge_op(filtered.clone())?; let op: Arc + 'graph> = Arc::new(UnaryEdgeOp { inner, op: self.op }); Ok(EdgeExprFilteredGraph::new(graph, op)) @@ -202,7 +203,7 @@ where graph: G, filtered: F, ) -> Result, GraphError> { - let inner = self.expr.create_edge_op(graph.clone())?; + let inner = self.expr.create_edge_op(filtered.clone())?; let op: Arc + 'graph> = Arc::new(UnaryEdgeOp { inner, op: self.op }); Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) @@ -248,8 +249,8 @@ where graph: G, filtered: F, ) -> Result, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph.clone())?; + let left = self.left.create_edge_op(filtered.clone())?; + let right = self.right.create_edge_op(filtered.clone())?; validate_string_op(&left.prop_type())?; let op: Arc + 'graph> = Arc::new(StringEdgeOp { left, @@ -295,8 +296,8 @@ where graph: G, filtered: F, ) -> Result, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph.clone())?; + let left = self.left.create_edge_op(filtered.clone())?; + let right = self.right.create_edge_op(filtered.clone())?; validate_string_op(&left.prop_type())?; let op: Arc + 'graph> = Arc::new(StringEdgeOp { left, @@ -342,7 +343,7 @@ impl CreateFilter for PropValueSetExpr { graph: G, filtered: F, ) -> Result, GraphError> { - let inner = self.expr.create_edge_op(graph.clone())?; + let inner = self.expr.create_edge_op(filtered.clone())?; let op: Arc + 'graph> = Arc::new(PropValueSetEdgeOp { inner, values: self.values, @@ -383,7 +384,7 @@ impl CreateFilter for PropValueSetExpr { graph: G, filtered: F, ) -> Result, GraphError> { - let inner = self.expr.create_edge_op(graph.clone())?; + let inner = self.expr.create_edge_op(filtered.clone())?; let op: Arc + 'graph> = Arc::new(PropValueSetEdgeOp { inner, values: self.values, diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index fc615e202c..b501187f53 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -19,7 +19,6 @@ //! └──────────────────────────────────────────────────────────┘ //! ``` -use crate::db::graph::views::filter::model::node_expr::EntityExpr; use raphtory_api::core::entities::{edges::edge_ref::EdgeRef, properties::prop::PropType}; use raphtory_storage::graph::graph::GraphStorage; @@ -27,7 +26,6 @@ pub mod filters; pub mod ops; pub use super::{Metadata, Property}; -pub use filters::*; // ───────────────────────────────────────────────────────────────────────────── // EdgeOp — compiled evaluator: EdgeRef → typed value @@ -44,4 +42,9 @@ pub trait EdgeOp: Send + Sync { fn prop_type(&self) -> PropType { PropType::Empty } + + /// Returns `Some(value)` if the edge op has a constant global value + fn const_value(&self) -> Option { + None + } } diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 59c551cced..aa5ea29f48 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -18,7 +18,6 @@ use raphtory_api::core::entities::{ properties::prop::{Prop, PropType}, }; use raphtory_storage::graph::graph::GraphStorage; -use std::{collections::HashSet, hash::Hash}; use super::EdgeOp; use crate::db::{ @@ -45,6 +44,10 @@ impl<'a, V: Clone + Send + Sync> EdgeOp for Arc + 'a> { fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> V { self.as_ref().apply(storage, edge) } + + fn const_value(&self) -> Option { + self.as_ref().const_value() + } } // ───────────────────────────────────────────────────────────────────────────── @@ -57,6 +60,10 @@ impl EdgeOp for Const { fn apply(&self, _storage: &GraphStorage, _edge: EdgeRef) -> V { self.0.clone() } + + fn const_value(&self) -> Option { + Some(self.0.clone()) + } } // ───────────────────────────────────────────────────────────────────────────── @@ -232,31 +239,6 @@ impl<'g> EdgeOp for PropValueSetEdgeOp<'g> { } } -// ───────────────────────────────────────────────────────────────────────────── -// SetEdgeOp<'g, I> — is_in / is_not_in for Option (HashSet, O(1)) -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct SetEdgeOp<'g, I: Eq + Hash + Clone + Send + Sync + 'static> { - pub(crate) inner: Arc> + 'g>, - pub(crate) values: Arc>, - pub(crate) op: SetOp, -} - -impl<'g, I: Eq + Hash + Clone + Send + Sync + 'static> EdgeOp for SetEdgeOp<'g, I> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - match self.inner.apply(storage, edge) { - None => false, - Some(v) => match self.op { - SetOp::IsIn => self.values.contains(&v), - SetOp::IsNotIn => !self.values.contains(&v), - }, - } - } -} - // ───────────────────────────────────────────────────────────────────────────── // ListAwareCmpEdgeOp<'g> — element-wise comparison via broadcast_binary // ───────────────────────────────────────────────────────────────────────────── @@ -367,69 +349,6 @@ impl<'g> EdgeOp for ListAwareUnaryEdgeOp<'g> { } } -// ───────────────────────────────────────────────────────────────────────────── -// AndBoolEdgeOp / OrBoolEdgeOp — boolean AND/OR over two Option edge ops -// -// Used by AndFilter / OrFilter when they implement EdgeExpr so that -// .not() (and other EntityExprFilterOps) can be chained on composed edge filters. -// ───────────────────────────────────────────────────────────────────────────── - -pub(crate) struct AndBoolEdgeOp<'g> { - pub(crate) left: Arc> + 'g>, - pub(crate) right: Arc> + 'g>, -} - -impl<'g> Clone for AndBoolEdgeOp<'g> { - fn clone(&self) -> Self { - Self { - left: self.left.clone(), - right: self.right.clone(), - } - } -} - -impl<'g> EdgeOp for AndBoolEdgeOp<'g> { - type Output = Option; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let l = self.left.apply(storage, edge); - let r = self.right.apply(storage, edge); - broadcast_binary(l, r, &|lv, rv| { - let lb = matches!(lv, Some(Prop::Bool(true))); - let rb = matches!(rv, Some(Prop::Bool(true))); - Some(Prop::Bool(lb && rb)) - }) - } -} - -pub(crate) struct OrBoolEdgeOp<'g> { - pub(crate) left: Arc> + 'g>, - pub(crate) right: Arc> + 'g>, -} - -impl<'g> Clone for OrBoolEdgeOp<'g> { - fn clone(&self) -> Self { - Self { - left: self.left.clone(), - right: self.right.clone(), - } - } -} - -impl<'g> EdgeOp for OrBoolEdgeOp<'g> { - type Output = Option; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let l = self.left.apply(storage, edge); - let r = self.right.apply(storage, edge); - broadcast_binary(l, r, &|lv, rv| { - let lb = matches!(lv, Some(Prop::Bool(true))); - let rb = matches!(rv, Some(Prop::Bool(true))); - Some(Prop::Bool(lb || rb)) - }) - } -} - // ───────────────────────────────────────────────────────────────────────────── // EdgeEndpointNodeOp — applies a node op to the src or dst VID of an edge // 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 cec3b9585c..a329be2845 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -20,10 +20,7 @@ use crate::{ layered_filter::Layered, node_expr::{CreateOp, EntityExpr, EntityExprBuilder}, node_filter::{ - builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder, - }, + builders::{InternalNodeFilterBuilder, InternalNodeIdFilterBuilder}, CompositeNodeFilter, NodeFilter, }, property_filter::{ 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 73d2547bf1..d6eb26dd42 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 @@ -78,7 +78,7 @@ use crate::{ graph::views::filter::model::{ edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, filter_operator::Comparable, - node_filter::{NodeFilter, NodeFilterFactory}, + node_filter::NodeFilter, require_aggregable, resolved_prop_type, CreateView, EntityMarker, }, }, 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 a4c2186c49..2e7bd4d7ee 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 @@ -45,7 +45,7 @@ use super::{ BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, ListAwareUnaryNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, - ConstFilter, CreateOp, EntityExpr, EntityExprBuilder, Marker, + CreateOp, EntityExpr, EntityExprBuilder, Marker, }; use crate::{ db::{ @@ -72,7 +72,7 @@ use crate::{ }, }, errors::GraphError, - prelude::{EdgeFilter, GraphViewOps, NodeFilter}, + prelude::{EdgeFilter, NodeFilter}, }; use raphtory_api::core::entities::properties::prop::{Prop, PropType}; use std::sync::Arc; @@ -196,7 +196,7 @@ where fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, - graph: G, + _graph: G, filtered: F, ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); @@ -205,11 +205,10 @@ where let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; - validate_const_castable( - &lhs_pt, - right.const_value().as_ref().and_then(|o| o.as_ref()), - )?; - validate_types_compatible(&lhs_pt, &rhs_pt)?; + match right.const_value() { + Some(c) => validate_const_castable(&lhs_pt, c.as_ref())?, + None => validate_types_compatible(&lhs_pt, &rhs_pt)?, + } Ok(Arc::new(BinaryCmpNodeOp { left, right, @@ -375,7 +374,7 @@ where fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, - graph: G, + _graph: G, filtered: F, ) -> Result, GraphError> { if !self.expr.nullable() { @@ -571,7 +570,7 @@ impl CreateFilter for StringExpr { fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, - graph: G, + _graph: G, filtered: F, ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); @@ -741,7 +740,7 @@ impl CreateFilter for PropValueSetExpr { fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( self, - graph: G, + _graph: G, filtered: F, ) -> Result, GraphError> { let expr_pt = self.expr.prop_type(); 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 index f1ca807e2d..dc97b4092a 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -17,9 +17,7 @@ pub mod ops; mod tests; pub use super::{Metadata, Property}; -use crate::db::graph::views::filter::model::{ - edge_expr::EdgeOp, node_filter::NodeFilter, EntityMarker, -}; +use crate::db::graph::views::filter::model::{edge_expr::EdgeOp, EntityMarker}; pub use dyn_expr::*; pub use exprs::*; pub use filters::*; 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 index 35c1cbc7df..81b5952d07 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -62,7 +62,7 @@ use raphtory_api::core::entities::{ VID, }; use raphtory_storage::graph::graph::GraphStorage; -use std::{borrow::Borrow, collections::HashSet, hash::Hash, sync::Arc}; +use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── // NodePropOp — latest property value by pre-resolved column ID // ───────────────────────────────────────────────────────────────────────────── @@ -499,78 +499,6 @@ impl<'g> NodeOp for ListAwareUnaryNodeOp<'g> { } } -// ───────────────────────────────────────────────────────────────────────────── -// AndBoolNodeOp / OrBoolNodeOp — boolean AND/OR over two Option node ops -// -// Used by AndFilter / OrFilter when they implement NodeExpr so that -// .not() (and other EntityExprFilterOps) can be chained on composed filters: -// NodeFilter.degree().lt(5).and(NodeFilter.name().eq("alice")).not() -// ───────────────────────────────────────────────────────────────────────────── - -pub(crate) struct AndBoolNodeOp<'g> { - pub(crate) left: Arc> + 'g>, - pub(crate) right: Arc> + 'g>, -} - -impl<'g> Clone for AndBoolNodeOp<'g> { - fn clone(&self) -> Self { - Self { - left: self.left.clone(), - right: self.right.clone(), - } - } -} - -impl<'g> NodeOp for AndBoolNodeOp<'g> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - type Output = Option; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - let l = self.left.apply(storage, node); - let r = self.right.apply(storage, node); - broadcast_binary(l, r, &|lv, rv| { - let lb = matches!(lv, Some(Prop::Bool(true))); - let rb = matches!(rv, Some(Prop::Bool(true))); - Some(Prop::Bool(lb && rb)) - }) - } -} - -pub(crate) struct OrBoolNodeOp<'g> { - pub(crate) left: Arc> + 'g>, - pub(crate) right: Arc> + 'g>, -} - -impl<'g> Clone for OrBoolNodeOp<'g> { - fn clone(&self) -> Self { - Self { - left: self.left.clone(), - right: self.right.clone(), - } - } -} - -impl<'g> NodeOp for OrBoolNodeOp<'g> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - type Output = Option; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - let l = self.left.apply(storage, node); - let r = self.right.apply(storage, node); - broadcast_binary(l, r, &|lv, rv| { - let lb = matches!(lv, Some(Prop::Bool(true))); - let rb = matches!(rv, Some(Prop::Bool(true))); - Some(Prop::Bool(lb || rb)) - }) - } -} - // ───────────────────────────────────────────────────────────────────────────── // PropValueSetNodeOp<'g> — is_in / is_not_in for Option (linear scan) // ───────────────────────────────────────────────────────────────────────────── @@ -718,37 +646,3 @@ impl<'g, I: Clone + Send + Sync + 'static> NodeOp for UnaryNodeOp<'g, I> { PropType::Bool } } - -// ───────────────────────────────────────────────────────────────────────────── -// SetNodeOp<'g, T> — evaluates is_in / is_not_in -// ───────────────────────────────────────────────────────────────────────────── - -/// Internal op for hash-set membership on typed values. -/// -/// e.g. `NodeFilter.node_type().is_in(["Person", "Account"])` compiles to: -/// `SetNodeOp { inner: Type.map(...), 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> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - 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 index 00674a4c87..7a8fc47b49 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 @@ -1,10 +1,10 @@ use super::*; use crate::{ db::{ - api::{state::ops::Id, view::filter_ops::NodeSelect}, + api::{state::ops::Id, view::filter_ops::Select}, graph::views::filter::{ model::{ - filter_operator::BinaryOp, node_filter::NodeFilter, PropertyFilterFactory, + filter_operator::BinaryOp, node_filter::NodeFilter, PropertyExprFactory, ViewWrapOps, }, CreateFilter, @@ -33,10 +33,12 @@ fn build_test_graph() -> Graph { fn filtered_names(filter: F, g: Graph) -> Vec where F: CreateFilter, - for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, + for<'graph> F::EntityFiltered<'graph, Graph, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, { + let fg = filter.filter_graph_view(g.clone()).unwrap(); let mut names: Vec = filter - .create_filter(g) + .create_filter(g, fg) .unwrap() .nodes() .iter() @@ -193,10 +195,12 @@ fn build_temporal_graph() -> Graph { fn temporal_filtered_names(filter: F, g: Graph) -> Vec where F: CreateFilter, - for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, + for<'graph> F::EntityFiltered<'graph, Graph, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, { + let fg = filter.filter_graph_view(g.clone()).unwrap(); let mut names: Vec = filter - .create_filter(g) + .create_filter(g, fg) .unwrap() .nodes() .iter() @@ -339,10 +343,12 @@ fn temporal_expr_ops_blanket_any() { fn windowed_filtered_names(filter: F, g: Graph) -> Vec where F: CreateFilter, - for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, + for<'graph> F::EntityFiltered<'graph, Graph, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, { + let fg = filter.filter_graph_view(g.clone()).unwrap(); let mut names: Vec = filter - .create_filter(g) + .create_filter(g, fg) .unwrap() .nodes() .iter() @@ -444,10 +450,12 @@ fn build_layered_temporal_graph() -> Graph { fn layered_filtered_names(filter: F, g: Graph) -> Vec where F: CreateFilter, - for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, + for<'graph> F::EntityFiltered<'graph, Graph, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, { + let fg = filter.filter_graph_view(g.clone()).unwrap(); let mut names: Vec = filter - .create_filter(g) + .create_filter(g, fg) .unwrap() .nodes() .iter() @@ -546,7 +554,7 @@ fn string_op_on_numeric_prop_returns_error() { let filter = NodeFilter .property("score") .starts_with(Prop::Str("x".into())); - let result = filter.create_filter(g); + let result = filter.create_filter(g.clone(), g); assert!( result.is_err(), "expected Err for string op on numeric property" @@ -560,7 +568,7 @@ fn ordering_op_on_bool_prop_returns_error() { .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); + let result = filter.create_filter(g.clone(), g); assert!( result.is_err(), "expected Err for ordering op on boolean property" 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 00b7a8e4c7..95f020a31c 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,10 +1,7 @@ -use crate::db::graph::views::filter::{ - model::{ - property_filter::{Op, PropertyFilter, PropertyFilterInput, PropertyRef}, - CombinedFilter, EntityMarker, InternalPropertyFilterBuilder, TemporalPropertyFilterFactory, - Wrap, - }, - CreateFilter, +use crate::db::graph::views::filter::model::{ + property_filter::{Op, PropertyFilter, PropertyFilterInput, PropertyRef}, + CombinedFilter, EntityMarker, InternalPropertyFilterBuilder, TemporalPropertyFilterFactory, + Wrap, }; #[derive(Clone)] diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 2c99a38715..90ad7a3c23 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -1,19 +1,15 @@ use crate::{ - db::{ - api::state::ops::node::{Id, Name, Type}, - graph::views::filter::model::{ - edge_filter::{EdgeEndpointWrapper, EdgeFilter}, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - node_expr::{DynCreateOp, EntityExpr, Scoped}, - node_filter::NodeFilter, - CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, - PropertyExprFactory, ViewWrapOps, Wrap, - }, + db::graph::views::filter::model::{ + edge_filter::{EdgeEndpointWrapper, EdgeFilter}, + is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + node_expr::{DynCreateOp, EntityExpr, Scoped}, + node_filter::NodeFilter, + CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, + PropertyExprFactory, ViewWrapOps, Wrap, }, - prelude::EdgeViewOps, python::{filter::node_expr::PyExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods}; diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index f933090d8e..f629edbd38 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,11 +1,10 @@ use crate::{ db::graph::views::filter::model::{ - degree_filter::DegreeFilterFactory, is_active_node_filter::IsActiveNode, - node_expr::{CreateOp, DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr, Scoped}, + node_expr::{CreateOp, DynCreateOp, DynEntityExpr, DynTemporal, Scoped}, node_state_filter::NodeStateBoolColOp, - CreateView, DynCreateView, DynPropertyExprFactory, EntityMarker, InternalViewWrapOps, - PropertyExpr, PropertyExprFactory, ViewWrapOps, + DynCreateView, DynPropertyExprFactory, EntityMarker, InternalViewWrapOps, + PropertyExprFactory, ViewWrapOps, }, prelude::{EntityAggOps, EntityExprFilterOps, NodeFilter, NodeFilterFactory}, python::{ From 1bc6a480809bda41b08a2db7bec3e3621d16929a Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:27:52 +0100 Subject: [PATCH 104/155] python filter factories: keep the erased type set finite Coercing a concrete wrapper (Latest, SnapshotAt, ...) into Arc materialises a vtable whose own wrap methods coerce deeper wrappers, so monomorphisation never terminates; the compiler gave up 40 windows deep (E0275) once anything compiled these bindings, which is exactly what cargo test --all-features does. The four wrap methods now erase self before wrapping (the same trick dyn_window already used), closing the set over wrapper-of-erased types. The erased factory picks up the traits those wrappers need through the existing Arc blankets: DynEntityExpr and DynCreateView become supertraits, EdgeFilterFactory is implemented directly, and the blanket's unused EdgeViewFilterOps bound is dropped. --- raphtory/src/python/filter/edge_expr.rs | 33 +++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 90ad7a3c23..dfc344e47f 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -5,9 +5,9 @@ use crate::{ is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, - node_expr::{DynCreateOp, EntityExpr, Scoped}, + node_expr::{DynCreateOp, DynEntityExpr, EntityExpr, Scoped}, node_filter::NodeFilter, - CreateView, EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, + CreateView, DynCreateView, EdgeFilterFactory, EntityMarker, InternalViewWrapOps, PropertyExprFactory, ViewWrapOps, Wrap, }, python::{filter::node_expr::PyExpr, types::iterable::FromIterable}, @@ -65,7 +65,7 @@ impl PyEdgeEndpoint { } } -pub trait DynEdgeFilterFactory: Send + Sync + 'static { +pub trait DynEdgeFilterFactory: DynEntityExpr + DynCreateView + Send + Sync + 'static { fn dyn_property(&self, name: String) -> Arc; fn dyn_metadata(&self, name: String) -> Arc; @@ -84,6 +84,8 @@ pub trait DynEdgeFilterFactory: Send + Sync + 'static { fn dyn_layer(&self, layers: Vec) -> Arc; } +impl EdgeFilterFactory for Arc {} + impl InternalViewWrapOps for Arc { type Window = Arc; @@ -94,15 +96,7 @@ impl InternalViewWrapOps for Arc { impl DynEdgeFilterFactory for T where - T: EdgeFilterFactory - + EdgeViewFilterOps - + ViewWrapOps - + CreateView - + EntityExpr - + Clone - + Send - + Sync - + 'static, + T: EdgeFilterFactory + ViewWrapOps + CreateView + EntityExpr + Clone + Send + Sync + 'static, ::Marker: Into, { fn dyn_property(&self, name: String) -> Arc { @@ -155,17 +149,24 @@ where let dyn_self: Arc = Arc::new(self.clone()); dyn_self.before(time) } + // Same erasure trick as dyn_window: wrapping the erased factory keeps the + // set of vtable-instantiated types finite; wrapping `self` directly would + // materialise a vtable for every wrapper combination. fn dyn_latest(&self) -> Arc { - Arc::new(self.clone().latest()) + let dyn_self: Arc = Arc::new(self.clone()); + Arc::new(dyn_self.latest()) } fn dyn_snapshot_at(&self, time: EventTime) -> Arc { - Arc::new(self.clone().snapshot_at(time)) + let dyn_self: Arc = Arc::new(self.clone()); + Arc::new(dyn_self.snapshot_at(time)) } fn dyn_snapshot_latest(&self) -> Arc { - Arc::new(self.clone().snapshot_latest()) + let dyn_self: Arc = Arc::new(self.clone()); + Arc::new(dyn_self.snapshot_latest()) } fn dyn_layer(&self, layers: Vec) -> Arc { - Arc::new(self.clone().layer(layers)) + let dyn_self: Arc = Arc::new(self.clone()); + Arc::new(dyn_self.layer(layers)) } } From 04487cc904a95246f8b34368822b2d71078a5611 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:57:55 +0100 Subject: [PATCH 105/155] comments describe the code, not the branches it came from Section markers drop their provenance notes; the FilterValue alias comment explains the two types' roles and that both leave with the composite path. --- raphtory/src/db/graph/views/filter/model/edge_filter.rs | 6 +++--- raphtory/src/db/graph/views/filter/model/filter.rs | 4 +++- raphtory/src/db/graph/views/filter/model/filter_operator.rs | 2 +- raphtory/src/db/graph/views/filter/model/graph_filter.rs | 2 +- raphtory/src/db/graph/views/filter/model/latest_filter.rs | 2 +- raphtory/src/db/graph/views/filter/model/mod.rs | 2 +- raphtory/src/db/graph/views/filter/model/node_filter/mod.rs | 2 +- .../db/graph/views/filter/model/property_filter/evaluate.rs | 2 +- raphtory/src/db/graph/views/filter/model/snapshot_filter.rs | 2 +- raphtory/src/db/graph/views/filter/model/windowed_filter.rs | 2 +- 10 files changed, 14 insertions(+), 12 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 a329be2845..e72c61f80c 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -534,7 +534,7 @@ impl TryAsCompositeFilter for CompositeEdgeFilter { } } -// ── expr layer: endpoint expressions bridge node ops into edge ops (June branch) ── +// ── expr layer: endpoint expressions bridge node ops into edge ops ── impl EntityExprBuilder for EdgeEndpointWrapper {} @@ -558,7 +558,7 @@ impl CreateOp for EdgeEndpointWrapper { } } -// ── expr layer: which types serve as edge-filter factories (June branch) ── +// ── expr layer: which types serve as edge-filter factories ── use crate::db::{ api::state::ops::node::{Id, Name, Type}, @@ -576,7 +576,7 @@ impl EdgeFilterFactory for Layered {} impl EdgeFilterFactory for SnapshotAt {} impl EdgeFilterFactory for SnapshotLatest {} -// ── expr layer: temporal chains on endpoint properties (June branch) ── +// ── expr layer: temporal chains on endpoint properties ── use crate::db::graph::views::filter::model::node_expr::{ AllExpr, AnyExpr, AvgExpr, EntityAggOps, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, diff --git a/raphtory/src/db/graph/views/filter/model/filter.rs b/raphtory/src/db/graph/views/filter/model/filter.rs index 8dabf06024..fb540a7dd1 100644 --- a/raphtory/src/db/graph/views/filter/model/filter.rs +++ b/raphtory/src/db/graph/views/filter/model/filter.rs @@ -240,5 +240,7 @@ impl Filter { } } -// June renamed `FilterValue` to `FieldFilterValue`; db_v4 machinery still says `FilterValue`. +// The generic `filter_value::FilterValue` owns the short name; the composite +// machinery keeps its original spelling through this alias and both leave with +// the composite path. pub type FilterValue = FieldFilterValue; 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 6e5e9d6f7e..a994d4c21c 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -314,7 +314,7 @@ impl FilterOperator { } } -// ── expr-layer operator kinds (from the June branch; consumed by node_expr/edge_expr) ── +// ── expr-layer operator kinds (consumed by node_expr/edge_expr) ── pub trait Comparable: Clone + Send + Sync + 'static { fn binary_cmp(op: &BinaryOp, left: &Self, right: &Self) -> bool; 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 76c0414bed..62e6b6b7f8 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -99,7 +99,7 @@ impl TryAsCompositeFilter for GraphFilter { } } -// ── expr-layer view ops (June branch) ── +// ── expr-layer view ops ── pub trait GraphFilterOps: InternalViewWrapOps + CombinedFilter + Send + Sync + 'static 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 a9468f2c87..a678dc227f 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -251,7 +251,7 @@ impl EdgeViewFilterOps for Latest { } } -// ── expr-layer view construction (June branch) ── +// ── expr-layer view construction ── impl CreateView for Latest { type View<'graph, G: GraphView + 'graph> = WindowedGraph<::View<'graph, G>>; diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index eea476dc34..b9137abb7f 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -1342,7 +1342,7 @@ pub fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result + CreateView + EntityExpr diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index 9266650ec0..2df06026bb 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -334,7 +334,7 @@ impl PropertyFilter { } } -// ── numeric scan helpers for expr aggregates (June branch) ── +// ── numeric scan helpers for expr aggregates ── pub fn scan_u64_sum>( vals: impl IntoIterator, 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 8666a4e863..71c13b9dbe 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -454,7 +454,7 @@ impl EdgeViewFilterOps for SnapshotLatest { } } -// ── expr-layer view construction (June branch) ── +// ── expr-layer view construction ── impl CreateView for SnapshotAt { type View<'graph, G: GraphView + 'graph> = WindowedGraph<::View<'graph, G>>; 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 83a5c18780..9489d7d114 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -285,7 +285,7 @@ impl EdgeViewFilterOps for Windowed { } } -// ── expr-layer view construction (June branch) ── +// ── expr-layer view construction ── impl CreateView for Windowed { type View<'graph, G: GraphView + 'graph> = WindowedGraph<::View<'graph, G>>; From 2997f5ef1c4cc45f3aaff289927a12a78adb0386 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:46:03 +0100 Subject: [PATCH 106/155] make the python filter surface work end to end First execution of the python bindings surfaced defects invisible to compilation: - the module registered classes while the entry points are instance methods; the module attributes are now ready-made root instances - the erased wrap methods dispatched back into themselves through the vtable (an unconditional runtime loop); the window family constructs the wrapper over the erased factory directly with the same bounds clamping as ViewWrapOps, and the manual Arc impls dispatch through as_ref() so the blanket on Arc cannot self-select - comparison and string operators accept plain python values as well as expressions (extracted as Prop constants) - temporal() is exposed on PropertyExpr through the existing DynTemporal - predicates return FilterExpr: comparisons wrap their CreateFilter impls, factory predicates route through Dyn{Node,Edge}ViewFilterOps instead of Scoped ops, and NodeWindow carries the NodeViewFilterOps bound - the erased expression type forwards prop_type and nullable, so set coercion and build-time validation see real types instead of Empty The filter test files use one construction idiom (attribute style, matching the rust docs). --- .../test_filters/test_node_filter.py | 392 +++++++++--------- .../views/filter/model/node_expr/dyn_expr.rs | 8 + .../views/filter/model/node_filter/mod.rs | 12 +- raphtory/src/python/filter/edge_expr.rs | 127 +++--- .../src/python/filter/exploded_edge_expr.rs | 24 +- raphtory/src/python/filter/graph_filter.rs | 8 +- raphtory/src/python/filter/mod.rs | 8 + raphtory/src/python/filter/node_expr.rs | 213 ++++++---- 8 files changed, 454 insertions(+), 338 deletions(-) diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index 44dea414b8..69ab224440 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -64,42 +64,42 @@ def test_degree_filter_both_direction_comparison(value): assert_filter( graph, - filter.Node().degree() < value, + filter.Node.degree() < value, "both", lambda d: d < value, f"BOTH < {value}", ) assert_filter( graph, - filter.Node().degree() <= value, + filter.Node.degree() <= value, "both", lambda d: d <= value, f"BOTH <= {value}", ) assert_filter( graph, - filter.Node().degree() == value, + filter.Node.degree() == value, "both", lambda d: d == value, f"BOTH == {value}", ) assert_filter( graph, - filter.Node().degree() != value, + filter.Node.degree() != value, "both", lambda d: d != value, f"BOTH != {value}", ) assert_filter( graph, - filter.Node().degree() >= value, + filter.Node.degree() >= value, "both", lambda d: d >= value, f"BOTH >= {value}", ) assert_filter( graph, - filter.Node().degree() > value, + filter.Node.degree() > value, "both", lambda d: d > value, f"BOTH > {value}", @@ -112,42 +112,42 @@ def test_degree_filter_in_direction_comparison(value): assert_filter( graph, - filter.Node().in_degree() < value, + filter.Node.in_degree() < value, "in", lambda d: d < value, f"IN < {value}", ) assert_filter( graph, - filter.Node().in_degree() <= value, + filter.Node.in_degree() <= value, "in", lambda d: d <= value, f"IN <= {value}", ) assert_filter( graph, - filter.Node().in_degree() == value, + filter.Node.in_degree() == value, "in", lambda d: d == value, f"IN == {value}", ) assert_filter( graph, - filter.Node().in_degree() != value, + filter.Node.in_degree() != value, "in", lambda d: d != value, f"IN != {value}", ) assert_filter( graph, - filter.Node().in_degree() >= value, + filter.Node.in_degree() >= value, "in", lambda d: d >= value, f"IN >= {value}", ) assert_filter( graph, - filter.Node().in_degree() > value, + filter.Node.in_degree() > value, "in", lambda d: d > value, f"IN > {value}", @@ -160,42 +160,42 @@ def test_degree_filter_out_direction_comparison(value): assert_filter( graph, - filter.Node().out_degree() < value, + filter.Node.out_degree() < value, "out", lambda d: d < value, f"OUT < {value}", ) assert_filter( graph, - filter.Node().out_degree() <= value, + filter.Node.out_degree() <= value, "out", lambda d: d <= value, f"OUT <= {value}", ) assert_filter( graph, - filter.Node().out_degree() == value, + filter.Node.out_degree() == value, "out", lambda d: d == value, f"OUT == {value}", ) assert_filter( graph, - filter.Node().out_degree() != value, + filter.Node.out_degree() != value, "out", lambda d: d != value, f"OUT != {value}", ) assert_filter( graph, - filter.Node().out_degree() >= value, + filter.Node.out_degree() >= value, "out", lambda d: d >= value, f"OUT >= {value}", ) assert_filter( graph, - filter.Node().out_degree() > value, + filter.Node.out_degree() > value, "out", lambda d: d > value, f"OUT > {value}", @@ -208,21 +208,21 @@ def test_degree_filter_and(value): assert_filter( graph, - (filter.Node().degree() > value) & (filter.Node().degree() < value + 5), + (filter.Node.degree() > value) & (filter.Node.degree() < value + 5), "both", lambda d: d > value and d < (value + 5), f"BOTH > {value} AND BOTH < {value + 5}", ) assert_filter( graph, - (filter.Node().in_degree() > value) & (filter.Node().in_degree() < value + 5), + (filter.Node.in_degree() > value) & (filter.Node.in_degree() < value + 5), "in", lambda d: d > value and d < (value + 5), f"IN > {value} AND IN < {value + 5}", ) assert_filter( graph, - (filter.Node().out_degree() > value) & (filter.Node().out_degree() < value + 5), + (filter.Node.out_degree() > value) & (filter.Node.out_degree() < value + 5), "out", lambda d: d > value and d < (value + 5), f"OUT > {value} AND OUT < {value + 5}", @@ -235,21 +235,21 @@ def test_degree_filter_or(value): assert_filter( graph, - (filter.Node().degree() < value) | (filter.Node().degree() > value + 5), + (filter.Node.degree() < value) | (filter.Node.degree() > value + 5), "both", lambda d: d < value or d > (value + 5), f"BOTH < {value} OR BOTH > {value + 5}", ) assert_filter( graph, - (filter.Node().in_degree() < value) | (filter.Node().in_degree() > value + 5), + (filter.Node.in_degree() < value) | (filter.Node.in_degree() > value + 5), "in", lambda d: d < value or d > (value + 5), f"IN < {value} OR IN > {value + 5}", ) assert_filter( graph, - (filter.Node().out_degree() < value) | (filter.Node().out_degree() > value + 5), + (filter.Node.out_degree() < value) | (filter.Node.out_degree() > value + 5), "out", lambda d: d < value or d > (value + 5), f"OUT < {value} OR OUT > {value + 5}", @@ -262,21 +262,21 @@ def test_degree_filter_not(value): assert_filter( graph, - (filter.Node().degree() < value) | (~(filter.Node().degree() > value + 5)), + (filter.Node.degree() < value) | (~(filter.Node.degree() > value + 5)), "both", lambda d: d < value or d <= (value + 5), f"BOTH < {value} OR BOTH > {value + 5}", ) assert_filter( graph, - (filter.Node().in_degree() < value) | (~(filter.Node().in_degree() > value + 5)), + (filter.Node.in_degree() < value) | (~(filter.Node.in_degree() > value + 5)), "in", lambda d: d < value or d <= (value + 5), f"IN < {value} OR IN > {value + 5}", ) assert_filter( graph, - (filter.Node().out_degree() < value) | (~(filter.Node().out_degree() > value + 5)), + (filter.Node.out_degree() < value) | (~(filter.Node.out_degree() > value + 5)), "out", lambda d: d < value or d <= (value + 5), f"OUT < {value} OR OUT > {value + 5}", @@ -290,21 +290,21 @@ def test_degree_filter_is_in(value): assert_filter( graph, - filter.Node().degree().is_in(set_values), + filter.Node.degree().is_in(set_values), "both", lambda d: d in set_values, f"BOTH is_in({value}, {value + 1})", ) assert_filter( graph, - filter.Node().in_degree().is_in(set_values), + filter.Node.in_degree().is_in(set_values), "in", lambda d: d in set_values, f"IN is_in({value}, {value + 1})", ) assert_filter( graph, - filter.Node().out_degree().is_in(set_values), + filter.Node.out_degree().is_in(set_values), "out", lambda d: d in set_values, f"OUT is_in({value}, {value + 1})", @@ -318,21 +318,21 @@ def test_degree_filter_is_not_in(value): assert_filter( graph, - filter.Node().degree().is_not_in(set_values), + filter.Node.degree().is_not_in(set_values), "both", lambda d: d not in set_values, f"BOTH is_not_in({value}, {value + 1})", ) assert_filter( graph, - filter.Node().in_degree().is_not_in(set_values), + filter.Node.in_degree().is_not_in(set_values), "in", lambda d: d not in set_values, f"IN is_not_in({value}, {value + 1})", ) assert_filter( graph, - filter.Node().out_degree().is_not_in(set_values), + filter.Node.out_degree().is_not_in(set_values), "out", lambda d: d not in set_values, f"OUT is_not_in({value}, {value + 1})", @@ -342,54 +342,54 @@ def test_degree_filter_is_not_in(value): def test_degree_filter_with_invalid_expressions(): graph = degree_graph_with_add_node_and_add_edge(Graph()) invalid_filters = [ - filter.Node().degree().is_none(), - filter.Node().degree().is_some(), - filter.Node().degree().starts_with("1"), - filter.Node().degree().ends_with("1"), - filter.Node().degree().contains("1"), - filter.Node().degree().not_contains("1"), - filter.Node().degree().fuzzy_search("1", 1, False), - filter.Node().in_degree().is_none(), - filter.Node().in_degree().is_some(), - filter.Node().in_degree().starts_with("1"), - filter.Node().in_degree().ends_with("1"), - filter.Node().in_degree().contains("1"), - filter.Node().in_degree().not_contains("1"), - filter.Node().in_degree().fuzzy_search("1", 1, False), - filter.Node().out_degree().is_none(), - filter.Node().out_degree().is_some(), - filter.Node().out_degree().starts_with("1"), - filter.Node().out_degree().ends_with("1"), - filter.Node().out_degree().contains("1"), - filter.Node().out_degree().not_contains("1"), - filter.Node().out_degree().fuzzy_search("1", 1, False), - filter.Node().degree().any() == 1, - filter.Node().degree().all() == 1, - filter.Node().degree().len() > 0, - filter.Node().degree().sum() == 1, - filter.Node().degree().avg() == 1, - filter.Node().degree().min() == 1, - filter.Node().degree().max() == 1, - filter.Node().degree().first() == 1, - filter.Node().degree().last() == 1, - filter.Node().in_degree().any() == 1, - filter.Node().in_degree().all() == 1, - filter.Node().in_degree().len() > 0, - filter.Node().in_degree().sum() == 1, - filter.Node().in_degree().avg() == 1, - filter.Node().in_degree().min() == 1, - filter.Node().in_degree().max() == 1, - filter.Node().in_degree().first() == 1, - filter.Node().in_degree().last() == 1, - filter.Node().out_degree().any() == 1, - filter.Node().out_degree().all() == 1, - filter.Node().out_degree().len() > 0, - filter.Node().out_degree().sum() == 1, - filter.Node().out_degree().avg() == 1, - filter.Node().out_degree().min() == 1, - filter.Node().out_degree().max() == 1, - filter.Node().out_degree().first() == 1, - filter.Node().out_degree().last() == 1, + filter.Node.degree().is_none(), + filter.Node.degree().is_some(), + filter.Node.degree().starts_with("1"), + filter.Node.degree().ends_with("1"), + filter.Node.degree().contains("1"), + filter.Node.degree().not_contains("1"), + filter.Node.degree().fuzzy_search("1", 1, False), + filter.Node.in_degree().is_none(), + filter.Node.in_degree().is_some(), + filter.Node.in_degree().starts_with("1"), + filter.Node.in_degree().ends_with("1"), + filter.Node.in_degree().contains("1"), + filter.Node.in_degree().not_contains("1"), + filter.Node.in_degree().fuzzy_search("1", 1, False), + filter.Node.out_degree().is_none(), + filter.Node.out_degree().is_some(), + filter.Node.out_degree().starts_with("1"), + filter.Node.out_degree().ends_with("1"), + filter.Node.out_degree().contains("1"), + filter.Node.out_degree().not_contains("1"), + filter.Node.out_degree().fuzzy_search("1", 1, False), + filter.Node.degree().any() == 1, + filter.Node.degree().all() == 1, + filter.Node.degree().len() > 0, + filter.Node.degree().sum() == 1, + filter.Node.degree().avg() == 1, + filter.Node.degree().min() == 1, + filter.Node.degree().max() == 1, + filter.Node.degree().first() == 1, + filter.Node.degree().last() == 1, + filter.Node.in_degree().any() == 1, + filter.Node.in_degree().all() == 1, + filter.Node.in_degree().len() > 0, + filter.Node.in_degree().sum() == 1, + filter.Node.in_degree().avg() == 1, + filter.Node.in_degree().min() == 1, + filter.Node.in_degree().max() == 1, + filter.Node.in_degree().first() == 1, + filter.Node.in_degree().last() == 1, + filter.Node.out_degree().any() == 1, + filter.Node.out_degree().all() == 1, + filter.Node.out_degree().len() > 0, + filter.Node.out_degree().sum() == 1, + filter.Node.out_degree().avg() == 1, + filter.Node.out_degree().min() == 1, + filter.Node.out_degree().max() == 1, + filter.Node.out_degree().first() == 1, + filter.Node.out_degree().last() == 1, ] for filter_expr in invalid_filters: @@ -401,30 +401,30 @@ def test_degree_filter_with_invalid_expressions(): def test_degree_filter_with_invalid_string_values(value_a, value_b): graph = degree_graph_with_add_node_and_add_edge(Graph()) invalid_filters = [ - filter.Node().degree() < value_a, - filter.Node().degree() <= value_a, - filter.Node().degree() == value_a, - filter.Node().degree() != value_a, - filter.Node().degree() >= value_a, - filter.Node().degree() > value_a, - filter.Node().in_degree() < value_a, - filter.Node().in_degree() <= value_a, - filter.Node().in_degree() == value_a, - filter.Node().in_degree() != value_a, - filter.Node().in_degree() >= value_a, - filter.Node().in_degree() > value_a, - filter.Node().out_degree() < value_a, - filter.Node().out_degree() <= value_a, - filter.Node().out_degree() == value_a, - filter.Node().out_degree() != value_a, - filter.Node().out_degree() >= value_a, - filter.Node().out_degree() > value_a, - filter.Node().degree().is_in([value_a, value_b]), - filter.Node().degree().is_not_in([value_a, value_b]), - filter.Node().in_degree().is_in([value_a, value_b]), - filter.Node().in_degree().is_not_in([value_a, value_b]), - filter.Node().out_degree().is_in([value_a, value_b]), - filter.Node().out_degree().is_not_in([value_a, value_b]), + filter.Node.degree() < value_a, + filter.Node.degree() <= value_a, + filter.Node.degree() == value_a, + filter.Node.degree() != value_a, + filter.Node.degree() >= value_a, + filter.Node.degree() > value_a, + filter.Node.in_degree() < value_a, + filter.Node.in_degree() <= value_a, + filter.Node.in_degree() == value_a, + filter.Node.in_degree() != value_a, + filter.Node.in_degree() >= value_a, + filter.Node.in_degree() > value_a, + filter.Node.out_degree() < value_a, + filter.Node.out_degree() <= value_a, + filter.Node.out_degree() == value_a, + filter.Node.out_degree() != value_a, + filter.Node.out_degree() >= value_a, + filter.Node.out_degree() > value_a, + filter.Node.degree().is_in([value_a, value_b]), + filter.Node.degree().is_not_in([value_a, value_b]), + filter.Node.in_degree().is_in([value_a, value_b]), + filter.Node.in_degree().is_not_in([value_a, value_b]), + filter.Node.out_degree().is_in([value_a, value_b]), + filter.Node.out_degree().is_not_in([value_a, value_b]), ] for filter_expr in invalid_filters: @@ -440,42 +440,42 @@ def test_degree_filter_with_string_threshold(value): assert_filter( graph, - filter.Node().degree() < threshold_str, + filter.Node.degree() < threshold_str, "both", lambda d: d < parsed_str, f"BOTH < string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().degree() <= threshold_str, + filter.Node.degree() <= threshold_str, "both", lambda d: d <= parsed_str, f"BOTH <= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().degree() == threshold_str, + filter.Node.degree() == threshold_str, "both", lambda d: d == parsed_str, f"BOTH == string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().degree() != threshold_str, + filter.Node.degree() != threshold_str, "both", lambda d: d != parsed_str, f"BOTH != string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().degree() >= threshold_str, + filter.Node.degree() >= threshold_str, "both", lambda d: d >= parsed_str, f"BOTH >= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().degree() > threshold_str, + filter.Node.degree() > threshold_str, "both", lambda d: d > parsed_str, f"BOTH > string threshold parsed to u64 ({threshold_str})", @@ -483,42 +483,42 @@ def test_degree_filter_with_string_threshold(value): assert_filter( graph, - filter.Node().in_degree() < threshold_str, + filter.Node.in_degree() < threshold_str, "in", lambda d: d < parsed_str, f"IN < string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().in_degree() <= threshold_str, + filter.Node.in_degree() <= threshold_str, "in", lambda d: d <= parsed_str, f"IN <= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().in_degree() == threshold_str, + filter.Node.in_degree() == threshold_str, "in", lambda d: d == parsed_str, f"IN == string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().in_degree() != threshold_str, + filter.Node.in_degree() != threshold_str, "in", lambda d: d != parsed_str, f"IN != string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().in_degree() >= threshold_str, + filter.Node.in_degree() >= threshold_str, "in", lambda d: d >= parsed_str, f"IN >= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().in_degree() > threshold_str, + filter.Node.in_degree() > threshold_str, "in", lambda d: d > parsed_str, f"IN > string threshold parsed to u64 ({threshold_str})", @@ -526,42 +526,42 @@ def test_degree_filter_with_string_threshold(value): assert_filter( graph, - filter.Node().out_degree() < threshold_str, + filter.Node.out_degree() < threshold_str, "out", lambda d: d < parsed_str, f"OUT < string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().out_degree() <= threshold_str, + filter.Node.out_degree() <= threshold_str, "out", lambda d: d <= parsed_str, f"OUT <= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().out_degree() == threshold_str, + filter.Node.out_degree() == threshold_str, "out", lambda d: d == parsed_str, f"OUT == string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().out_degree() != threshold_str, + filter.Node.out_degree() != threshold_str, "out", lambda d: d != parsed_str, f"OUT != string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().out_degree() >= threshold_str, + filter.Node.out_degree() >= threshold_str, "out", lambda d: d >= parsed_str, f"OUT >= string threshold parsed to u64 ({threshold_str})", ) assert_filter( graph, - filter.Node().out_degree() > threshold_str, + filter.Node.out_degree() > threshold_str, "out", lambda d: d > parsed_str, f"OUT > string threshold parsed to u64 ({threshold_str})", @@ -577,21 +577,21 @@ def test_degree_filter_with_string_is_in(value): assert_filter( graph, - filter.Node().degree().is_in([threshold_a_str, threshold_b_str]), + filter.Node.degree().is_in([threshold_a_str, threshold_b_str]), "both", lambda d: d in set_values, f"BOTH is_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", ) assert_filter( graph, - filter.Node().in_degree().is_in([threshold_a_str, threshold_b_str]), + filter.Node.in_degree().is_in([threshold_a_str, threshold_b_str]), "in", lambda d: d in set_values, f"IN is_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", ) assert_filter( graph, - filter.Node().out_degree().is_in([threshold_a_str, threshold_b_str]), + filter.Node.out_degree().is_in([threshold_a_str, threshold_b_str]), "out", lambda d: d in set_values, f"OUT is_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", @@ -607,21 +607,21 @@ def test_degree_filter_with_string_is_not_in(value): assert_filter( graph, - filter.Node().degree().is_not_in([threshold_a_str, threshold_b_str]), + filter.Node.degree().is_not_in([threshold_a_str, threshold_b_str]), "both", lambda d: d not in set_values, f"BOTH is_not_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", ) assert_filter( graph, - filter.Node().in_degree().is_not_in([threshold_a_str, threshold_b_str]), + filter.Node.in_degree().is_not_in([threshold_a_str, threshold_b_str]), "in", lambda d: d not in set_values, f"IN is_not_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", ) assert_filter( graph, - filter.Node().out_degree().is_not_in([threshold_a_str, threshold_b_str]), + filter.Node.out_degree().is_not_in([threshold_a_str, threshold_b_str]), "out", lambda d: d not in set_values, f"OUT is_not_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", @@ -636,42 +636,42 @@ def test_degree_filter_with_float_threshold(value): assert_filter( graph, - filter.Node().degree() < threshold_float, + filter.Node.degree() < threshold_float, "both", lambda d: d < parsed_float, f"BOTH < float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().degree() <= threshold_float, + filter.Node.degree() <= threshold_float, "both", lambda d: d <= parsed_float, f"BOTH <= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().degree() == threshold_float, + filter.Node.degree() == threshold_float, "both", lambda d: d == parsed_float, f"BOTH == float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().degree() != threshold_float, + filter.Node.degree() != threshold_float, "both", lambda d: d != parsed_float, f"BOTH != float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().degree() >= threshold_float, + filter.Node.degree() >= threshold_float, "both", lambda d: d >= parsed_float, f"BOTH >= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().degree() > threshold_float, + filter.Node.degree() > threshold_float, "both", lambda d: d > parsed_float, f"BOTH > float threshold cast to u64 ({value})", @@ -679,42 +679,42 @@ def test_degree_filter_with_float_threshold(value): assert_filter( graph, - filter.Node().in_degree() < threshold_float, + filter.Node.in_degree() < threshold_float, "in", lambda d: d < parsed_float, f"IN < float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().in_degree() <= threshold_float, + filter.Node.in_degree() <= threshold_float, "in", lambda d: d <= parsed_float, f"IN <= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().in_degree() == threshold_float, + filter.Node.in_degree() == threshold_float, "in", lambda d: d == parsed_float, f"IN == float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().in_degree() != threshold_float, + filter.Node.in_degree() != threshold_float, "in", lambda d: d != parsed_float, f"IN != float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().in_degree() >= threshold_float, + filter.Node.in_degree() >= threshold_float, "in", lambda d: d >= parsed_float, f"IN >= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().in_degree() > threshold_float, + filter.Node.in_degree() > threshold_float, "in", lambda d: d > parsed_float, f"IN > float threshold cast to u64 ({value})", @@ -722,42 +722,42 @@ def test_degree_filter_with_float_threshold(value): assert_filter( graph, - filter.Node().out_degree() < threshold_float, + filter.Node.out_degree() < threshold_float, "out", lambda d: d < parsed_float, f"OUT < float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().out_degree() <= threshold_float, + filter.Node.out_degree() <= threshold_float, "out", lambda d: d <= parsed_float, f"OUT <= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().out_degree() == threshold_float, + filter.Node.out_degree() == threshold_float, "out", lambda d: d == parsed_float, f"OUT == float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().out_degree() != threshold_float, + filter.Node.out_degree() != threshold_float, "out", lambda d: d != parsed_float, f"OUT != float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().out_degree() >= threshold_float, + filter.Node.out_degree() >= threshold_float, "out", lambda d: d >= parsed_float, f"OUT >= float threshold cast to u64 ({value})", ) assert_filter( graph, - filter.Node().out_degree() > threshold_float, + filter.Node.out_degree() > threshold_float, "out", lambda d: d > parsed_float, f"OUT > float threshold cast to u64 ({value})", @@ -773,21 +773,21 @@ def test_degree_filter_with_float_is_in(value): assert_filter( graph, - filter.Node().degree().is_in([threshold_a, threshold_b]), + filter.Node.degree().is_in([threshold_a, threshold_b]), "both", lambda d: d in set_values, f"BOTH is_in(float thresholds cast to u64) ({value}, {value + 1})", ) assert_filter( graph, - filter.Node().in_degree().is_in([threshold_a, threshold_b]), + filter.Node.in_degree().is_in([threshold_a, threshold_b]), "in", lambda d: d in set_values, f"IN is_in(float thresholds cast to u64) ({value}, {value + 1})", ) assert_filter( graph, - filter.Node().out_degree().is_in([threshold_a, threshold_b]), + filter.Node.out_degree().is_in([threshold_a, threshold_b]), "out", lambda d: d in set_values, f"OUT is_in(float thresholds cast to u64) ({value}, {value + 1})", @@ -803,21 +803,21 @@ def test_degree_filter_with_float_is_not_in(value): assert_filter( graph, - filter.Node().degree().is_not_in([threshold_a, threshold_b]), + filter.Node.degree().is_not_in([threshold_a, threshold_b]), "both", lambda d: d not in set_values, f"BOTH is_not_in(float thresholds cast to u64) ({value}, {value + 1})", ) assert_filter( graph, - filter.Node().in_degree().is_not_in([threshold_a, threshold_b]), + filter.Node.in_degree().is_not_in([threshold_a, threshold_b]), "in", lambda d: d not in set_values, f"IN is_not_in(float thresholds cast to u64) ({value}, {value + 1})", ) assert_filter( graph, - filter.Node().out_degree().is_not_in([threshold_a, threshold_b]), + filter.Node.out_degree().is_not_in([threshold_a, threshold_b]), "out", lambda d: d not in set_values, f"OUT is_not_in(float thresholds cast to u64) ({value}, {value + 1})", @@ -827,7 +827,7 @@ def test_degree_filter_with_float_is_not_in(value): @with_variants(init_graph) def test_filter_nodes_for_node_name_eq(): def check(graph): - filter_expr = filter.Node().name() == "3" + filter_expr = filter.Node.name() == "3" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["3"] assert result_ids == expected_ids @@ -838,7 +838,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_name_ne(): def check(graph): - filter_expr = filter.Node().name() != "2" + filter_expr = filter.Node.name() != "2" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -849,12 +849,12 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_name_in(): def check(graph): - filter_expr = filter.Node().name().is_in(["1"]) + filter_expr = filter.Node.name().is_in(["1"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1"] assert result_ids == expected_ids - filter_expr = filter.Node().name().is_in(["2", "3"]) + filter_expr = filter.Node.name().is_in(["2", "3"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "3"] assert result_ids == expected_ids @@ -865,7 +865,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_name_not_in(): def check(graph): - filter_expr = filter.Node().name().is_not_in(["1"]) + filter_expr = filter.Node.name().is_not_in(["1"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -876,7 +876,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_eq(): def check(graph): - filter_expr = filter.Node().node_type() == "fire_nation" + filter_expr = filter.Node.node_type() == "fire_nation" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids @@ -905,7 +905,7 @@ def test_node_type_comparison_to_a_non_string_type_is_a_python_error(): @with_variants(init_graph) def test_filter_nodes_for_node_type_ne(): def check(graph): - filter_expr = filter.Node().node_type() != "fire_nation" + filter_expr = filter.Node.node_type() != "fire_nation" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -916,12 +916,12 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_in(): def check(graph): - filter_expr = filter.Node().node_type().is_in(["fire_nation"]) + filter_expr = filter.Node.node_type().is_in(["fire_nation"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node().node_type().is_in(["fire_nation", "air_nomads"]) + filter_expr = filter.Node.node_type().is_in(["fire_nation", "air_nomads"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "2", "3"] assert result_ids == expected_ids @@ -932,7 +932,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_not_in(): def check(graph): - filter_expr = filter.Node().node_type().is_not_in(["fire_nation"]) + filter_expr = filter.Node.node_type().is_not_in(["fire_nation"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -943,12 +943,12 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_starts_with(): def check(graph): - filter_expr = filter.Node().node_type().starts_with("fire") + filter_expr = filter.Node.node_type().starts_with("fire") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node().node_type().starts_with("Liar") + filter_expr = filter.Node.node_type().starts_with("Liar") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids @@ -959,12 +959,12 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_ends_with(): def check(graph): - filter_expr = filter.Node().node_type().ends_with("tion") + filter_expr = filter.Node.node_type().ends_with("tion") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node().node_type().ends_with("station") + filter_expr = filter.Node.node_type().ends_with("station") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids @@ -975,7 +975,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_contains(): def check(graph): - filter_expr = filter.Node().node_type().contains("fire") + filter_expr = filter.Node.node_type().contains("fire") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids @@ -986,7 +986,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_type_not_contains(): def check(graph): - filter_expr = filter.Node().node_type().not_contains("fire") + filter_expr = filter.Node.node_type().not_contains("fire") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -997,17 +997,17 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_fuzzy_search(): def check(graph): - filter_expr = filter.Node().node_type().fuzzy_search("fire", 2, True) + filter_expr = filter.Node.node_type().fuzzy_search("fire", 2, True) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node().node_type().fuzzy_search("fire", 2, False) + filter_expr = filter.Node.node_type().fuzzy_search("fire", 2, False) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids - filter_expr = filter.Node().node_type().fuzzy_search("air_noma", 2, False) + filter_expr = filter.Node.node_type().fuzzy_search("air_noma", 2, False) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2"] assert result_ids == expected_ids @@ -1018,7 +1018,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_not_node_type(): def check(graph): - filter_expr = filter.Node().node_type().is_not_in(["fire_nation"]) + filter_expr = filter.Node.node_type().is_not_in(["fire_nation"]) result_ids = sorted(graph.filter(~filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids @@ -1029,7 +1029,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_for_node_id_eq(): def check(graph): - filter_expr = filter.Node().id() == "3" + filter_expr = filter.Node.id() == "3" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["3"] assert result_ids == expected_ids @@ -1040,7 +1040,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_for_node_id_eq(): def check(graph): - filter_expr = filter.Node().id() == 3 + filter_expr = filter.Node.id() == 3 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [3] assert result_ids == expected_ids @@ -1051,7 +1051,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_for_node_id_ne(): def check(graph): - filter_expr = filter.Node().id() != "3" + filter_expr = filter.Node.id() != "3" result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -1062,7 +1062,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_for_node_id_ne(): def check(graph): - filter_expr = filter.Node().id() != 3 + filter_expr = filter.Node.id() != 3 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [1, 2, 4] assert result_ids == expected_ids @@ -1073,7 +1073,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_for_node_id_is_in(): def check(graph): - filter_expr = filter.Node().id().is_in(["1"]) + filter_expr = filter.Node.id().is_in(["1"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1"] assert result_ids == expected_ids @@ -1084,7 +1084,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_for_node_id_is_in(): def check(graph): - filter_expr = filter.Node().id().is_in([1]) + filter_expr = filter.Node.id().is_in([1]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [1] assert result_ids == expected_ids @@ -1095,7 +1095,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_for_node_id_is_not_in(): def check(graph): - filter_expr = filter.Node().id().is_not_in(["1"]) + filter_expr = filter.Node.id().is_not_in(["1"]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids @@ -1106,7 +1106,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_for_node_id_is_not_in(): def check(graph): - filter_expr = filter.Node().id().is_not_in([1]) + filter_expr = filter.Node.id().is_not_in([1]) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [2, 3, 4] assert result_ids == expected_ids @@ -1117,7 +1117,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_with_str_ids_error(): def check(graph): - filter_expr = filter.Node().id() == 3 + filter_expr = filter.Node.id() == 3 with pytest.raises( Exception, match='Invalid filter: Filter value type does not match node ID type. Expected Str but got "U64"', @@ -1130,7 +1130,7 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_error(): def check(graph): - filter_expr = filter.Node().id() == "3" + filter_expr = filter.Node.id() == "3" with pytest.raises( Exception, match='Invalid filter: Filter value type does not match node ID type. Expected U64 but got "Str"', @@ -1143,7 +1143,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_is_active(): def check(graph): - filter_expr = filter.Node().is_active() + filter_expr = filter.Node.is_active() result_ids = sorted(graph.window(1, 4).filter(filter_expr).nodes.id) expected_ids = sorted(["1", "2", "3", "4"]) assert result_ids == expected_ids @@ -1154,7 +1154,7 @@ def check(graph): @with_variants(init_graph) def test_select_nodes_is_active(): def check(graph): - filter_expr = filter.Node().is_active() + filter_expr = filter.Node.is_active() result_ids = sorted(graph.window(1, 4).nodes[filter_expr].id) expected_ids = sorted(["1", "2", "3", "4"]) assert result_ids == expected_ids @@ -1165,7 +1165,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_windowed_is_active(): def check(graph): - filter_expr = filter.Node().window(1, 2).is_active() + filter_expr = filter.Node.window(1, 2).is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(["1", "2"]) assert result_ids == expected_ids @@ -1176,7 +1176,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_windowed_is_active_not(): def check(graph): - filter_expr = filter.Node().window(1, 2).is_active() + filter_expr = filter.Node.window(1, 2).is_active() result_ids = sorted(graph.filter(~filter_expr).nodes.id) expected_ids = sorted([]) assert result_ids == expected_ids @@ -1187,7 +1187,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_latest_is_active(): def check(graph): - filter_expr = filter.Node().latest().is_active() + filter_expr = filter.Node.latest().is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(["1", "4", "David Gilmour", "Jimmy Page", "John Mayer"]) assert result_ids == expected_ids @@ -1198,7 +1198,7 @@ def check(graph): @with_variants(init_graph) def test_select_nodes_latest_is_active(): def check(graph): - filter_expr = filter.Node().latest().is_active() + filter_expr = filter.Node.latest().is_active() result_ids = sorted(graph.nodes[filter_expr].id) expected_ids = sorted(["1", "4", "David Gilmour", "Jimmy Page", "John Mayer"]) assert result_ids == expected_ids @@ -1209,7 +1209,7 @@ def check(graph): @with_variants(init_graph, variants=["graph"]) def test_filter_nodes_snapshot_latest_is_active(): def check(graph): - filter_expr = filter.Node().snapshot_latest().is_active() + filter_expr = filter.Node.snapshot_latest().is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted( ["1", "2", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] @@ -1222,7 +1222,7 @@ def check(graph): @with_variants(init_graph, variants=["persistent_graph"]) def test_filter_nodes_snapshot_latest_is_active_persistent(): def check(graph): - filter_expr = filter.Node().snapshot_latest().is_active() + filter_expr = filter.Node.snapshot_latest().is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(["1", "4", "David Gilmour", "Jimmy Page", "John Mayer"]) assert result_ids == expected_ids @@ -1233,7 +1233,7 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_at_is_active(): def check(graph): - filter_expr = filter.Node().at(2).is_active() + filter_expr = filter.Node.at(2).is_active() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(["1", "2", "3"]) assert result_ids == expected_ids @@ -1244,7 +1244,7 @@ def check(graph): @with_variants(init_graph) def test_select_nodes_at_is_active(): def check(graph): - filter_expr = filter.Node().at(2).is_active() + filter_expr = filter.Node.at(2).is_active() result_ids = sorted(graph.nodes[filter_expr].id) expected_ids = sorted(["1", "2", "3"]) assert result_ids == expected_ids @@ -1278,7 +1278,7 @@ def test_filter_nodes_by_column(): actual = alternating_mask(graph) assert actual == expected - filter_expr = filter.Node().by_state_column(actual, "bool_col") + filter_expr = filter.Node.by_state_column(actual, "bool_col") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(i for i, v in expected.items() if v["bool_col"]) assert result_ids == expected_ids @@ -1291,7 +1291,7 @@ def test_filter_nodes_by_column(): def test_filter_nodes_for_node_name_all_is_invalid(): def check(graph): with pytest.raises(AttributeError, match=r"has no attribute 'all'"): - filter.Node().name().all() + filter.Node.name().all() return check @@ -1300,6 +1300,6 @@ def check(graph): def test_filter_nodes_for_node_name_len_is_invalid(): def check(graph): with pytest.raises(AttributeError, match=r"has no attribute 'len'"): - filter.Node().name().len() + filter.Node.name().len() return check diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 1f2e858568..9a77d802ee 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -108,6 +108,14 @@ impl EntityExpr for Arc { fn entity(&self) -> Self::Marker { self.deref().dyn_entity() } + + fn prop_type(&self) -> PropType { + self.deref().dyn_prop_type() + } + + fn nullable(&self) -> bool { + self.deref().dyn_nullable() + } } impl EntityExprBuilder for Arc {} 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 819aa78615..a9d29d7302 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 @@ -559,7 +559,7 @@ impl TryAsCompositeFilter for CompositeNodeFilter { pub trait NodeFilterFactory: InternalViewWrapOps + CreateView + EntityExpr { - type NodeWindow: NodeFilterFactory; + type NodeWindow: NodeFilterFactory + NodeViewFilterOps; #[inline] fn id(&self) -> Id { Id @@ -625,22 +625,22 @@ impl NodeFilterFactory for NodeFilter { type NodeWindow = Self::Window; } -impl NodeFilterFactory for Windowed { +impl NodeFilterFactory for Windowed { type NodeWindow = T::NodeWindow; } -impl NodeFilterFactory for Latest { +impl NodeFilterFactory for Latest { type NodeWindow = Self::Window; } -impl NodeFilterFactory for SnapshotAt { +impl NodeFilterFactory for SnapshotAt { type NodeWindow = Self::Window; } -impl NodeFilterFactory for SnapshotLatest { +impl NodeFilterFactory for SnapshotLatest { type NodeWindow = Self::Window; } -impl NodeFilterFactory for Layered { +impl NodeFilterFactory for Layered { type NodeWindow = Self::Window; } diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index dfc344e47f..32e91f1d31 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -5,15 +5,23 @@ use crate::{ is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, - node_expr::{DynCreateOp, DynEntityExpr, EntityExpr, Scoped}, + node_expr::{DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr}, node_filter::NodeFilter, - CreateView, DynCreateView, EdgeFilterFactory, EntityMarker, InternalViewWrapOps, - PropertyExprFactory, ViewWrapOps, Wrap, + windowed_filter::Windowed, + CombinedFilter, CreateView, DynCreateFilter, DynCreateView, EdgeFilterFactory, + EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, PropertyExprFactory, ViewWrapOps, + Wrap, + }, + python::{ + filter::{ + filter_expr::PyFilterExpr, + node_expr::{PyExpr, PyPropertyExpr}, + }, + types::iterable::FromIterable, }, - python::{filter::node_expr::PyExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods}; -use raphtory_api::core::storage::timeindex::EventTime; +use raphtory_api::core::storage::timeindex::{AsTime, EventTime}; use std::sync::Arc; /// Entry point for filtering an edge endpoint (source or destination). @@ -66,13 +74,13 @@ impl PyEdgeEndpoint { } pub trait DynEdgeFilterFactory: DynEntityExpr + DynCreateView + Send + Sync + 'static { - fn dyn_property(&self, name: String) -> Arc; + fn dyn_property(&self, name: String) -> Arc; fn dyn_metadata(&self, name: String) -> Arc; - fn dyn_is_active(&self) -> Arc; - fn dyn_is_valid(&self) -> Arc; - fn dyn_is_deleted(&self) -> Arc; - fn dyn_is_self_loop(&self) -> Arc; + fn dyn_is_active(&self) -> Arc; + fn dyn_is_valid(&self) -> Arc; + fn dyn_is_deleted(&self) -> Arc; + fn dyn_is_self_loop(&self) -> Arc; fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc; fn dyn_at(&self, time: EventTime) -> Arc; @@ -86,68 +94,79 @@ pub trait DynEdgeFilterFactory: DynEntityExpr + DynCreateView + Send + Sync + 's impl EdgeFilterFactory for Arc {} +impl EdgeViewFilterOps for Arc { + type Output = Arc; + + fn is_active(&self) -> Self::Output { + self.as_ref().dyn_is_active() + } + + fn is_valid(&self) -> Self::Output { + self.as_ref().dyn_is_valid() + } + + fn is_deleted(&self) -> Self::Output { + self.as_ref().dyn_is_deleted() + } + + fn is_self_loop(&self) -> Self::Output { + self.as_ref().dyn_is_self_loop() + } +} + impl InternalViewWrapOps for Arc { type Window = Arc; fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - self.dyn_window(start, end) + self.as_ref().dyn_window(start, end) } } impl DynEdgeFilterFactory for T where - T: EdgeFilterFactory + ViewWrapOps + CreateView + EntityExpr + Clone + Send + Sync + 'static, + T: EdgeFilterFactory + EdgeViewFilterOps + ViewWrapOps + CreateView + EntityExpr + Clone, + T: Send + Sync + 'static, ::Marker: Into, { - fn dyn_property(&self, name: String) -> Arc { + fn dyn_property(&self, name: String) -> Arc { Arc::new(PropertyExprFactory::property(self, name)) } fn dyn_metadata(&self, name: String) -> Arc { Arc::new(PropertyExprFactory::metadata(self, name)) } - fn dyn_is_active(&self) -> Arc { - Arc::new(Scoped { - view: self.clone(), - inner: IsActiveEdge, - }) + fn dyn_is_active(&self) -> Arc { + Arc::new(self.is_active()) } - fn dyn_is_valid(&self) -> Arc { - Arc::new(Scoped { - view: self.clone(), - inner: IsValidEdge, - }) + fn dyn_is_valid(&self) -> Arc { + Arc::new(self.is_valid()) } - fn dyn_is_deleted(&self) -> Arc { - Arc::new(Scoped { - view: self.clone(), - inner: IsDeletedEdge, - }) + fn dyn_is_deleted(&self) -> Arc { + Arc::new(self.is_deleted()) } - fn dyn_is_self_loop(&self) -> Arc { - Arc::new(Scoped { - view: self.clone(), - inner: IsSelfLoopEdge, - }) + fn dyn_is_self_loop(&self) -> Arc { + Arc::new(self.is_self_loop()) } - // Go dynamic before calling window — the Arc impl - // has Window = Self, which terminates the recursive bound resolution. + // The window wrapper is constructed over the erased factory directly: + // routing through ViewWrapOps::window would dispatch straight back into + // this method through the erased build_window. fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { let dyn_self: Arc = Arc::new(self.clone()); - dyn_self.window(start, end) + let (old_start, old_end) = self.bounds(); + let end = end.min(old_end); + let start = start.max(old_start).min(end); + Arc::new(Windowed::new(start, end, dyn_self)) } fn dyn_at(&self, time: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - dyn_self.at(time) + self.dyn_window(time, EventTime::from(time.t().saturating_add(1))) } fn dyn_after(&self, time: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - dyn_self.after(time) + let start = time.t().saturating_add(1); + self.dyn_window(EventTime::start(start), EventTime::end(i64::MAX)) } fn dyn_before(&self, time: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - dyn_self.before(time) + self.dyn_window(EventTime::start(i64::MIN), EventTime::end(time.t())) } // Same erasure trick as dyn_window: wrapping the erased factory keeps the // set of vtable-instantiated types finite; wrapping `self` directly would @@ -170,6 +189,12 @@ where } } +impl PyEdgeFilter { + pub(crate) fn root() -> Self { + PyEdgeFilter(Arc::new(EdgeFilter)) + } +} + impl From> for PyEdgeFilter { fn from(value: Arc) -> Self { PyEdgeFilter(value) @@ -213,7 +238,7 @@ impl PyEdgeFilter { /// /// Arguments: /// name (str): Property key. - fn property(&self, name: String) -> PyExpr { + fn property(&self, name: String) -> PyPropertyExpr { self.0.dyn_property(name).into() } @@ -271,22 +296,22 @@ impl PyEdgeFilter { } /// Matches edges that have at least one event in the current view. - fn is_active(&self) -> PyExpr { - self.0.dyn_is_active().into() + fn is_active(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_active()) } /// Matches edges that are structurally valid in the current view. - fn is_valid(&self) -> PyExpr { - self.0.dyn_is_valid().into() + fn is_valid(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_valid()) } /// Matches edges that have been deleted. - fn is_deleted(&self) -> PyExpr { - self.0.dyn_is_deleted().into() + fn is_deleted(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_deleted()) } /// Matches edges that are self-loops (source == destination). - fn is_self_loop(&self) -> PyExpr { - self.0.dyn_is_self_loop().into() + fn is_self_loop(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_self_loop()) } } diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index 87a33a6bd6..3a0aa7f928 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -1,7 +1,7 @@ use crate::{ db::graph::views::filter::model::exploded_edge_filter::ExplodedEdgeFilter, python::{ - filter::{edge_expr::DynEdgeFilterFactory, node_expr::PyExpr}, + filter::{edge_expr::DynEdgeFilterFactory, filter_expr::PyFilterExpr, node_expr::PyExpr}, types::iterable::FromIterable, }, }; @@ -27,6 +27,12 @@ use std::sync::Arc; #[pyclass(frozen, name = "ExplodedEdge", module = "raphtory.filter")] pub struct PyExplodedEdgeFilter(Arc); +impl PyExplodedEdgeFilter { + pub(crate) fn root() -> Self { + PyExplodedEdgeFilter(Arc::new(ExplodedEdgeFilter)) + } +} + impl From> for PyExplodedEdgeFilter { fn from(value: Arc) -> Self { PyExplodedEdgeFilter(value) @@ -108,22 +114,22 @@ impl PyExplodedEdgeFilter { } /// Matches exploded edges that have at least one event in the current view. - fn is_active(&self) -> PyExpr { - self.0.dyn_is_active().into() + fn is_active(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_active()) } /// Matches exploded edges that are structurally valid in the current view. - fn is_valid(&self) -> PyExpr { - self.0.dyn_is_valid().into() + fn is_valid(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_valid()) } /// Matches exploded edges that have been deleted. - fn is_deleted(&self) -> PyExpr { - self.0.dyn_is_deleted().into() + fn is_deleted(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_deleted()) } /// Matches exploded edges that are self-loops (source == destination). - fn is_self_loop(&self) -> PyExpr { - self.0.dyn_is_self_loop().into() + fn is_self_loop(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_self_loop()) } } diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index 24c8f766a6..4438198bdc 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -1,5 +1,5 @@ use crate::{ - db::graph::views::filter::model::{DynView, ViewWrapOps}, + db::graph::views::filter::model::{graph_filter::GraphFilter, DynView, ViewWrapOps}, python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; @@ -29,6 +29,12 @@ use std::sync::Arc; )] pub struct PyGraphFilter(pub(crate) DynView); +impl PyGraphFilter { + pub(crate) fn root() -> Self { + PyGraphFilter(Arc::new(GraphFilter)) + } +} + #[pymethods] impl PyGraphFilter { /// Restricts evaluation to events within a time window. diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 5ae708d44a..5f0604b56a 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -39,5 +39,13 @@ pub fn base_filter_module(py: Python<'_>) -> Result, PyErr> filter_module.add_class::()?; filter_module.add_class::()?; + // The entry points are instances: `filter.Edge.src()` chains through + // instance methods, so the module attributes shadow the classes with + // ready-made roots. + filter_module.add("Node", PyNodeFilter::root())?; + filter_module.add("Edge", PyEdgeFilter::root())?; + filter_module.add("ExplodedEdge", PyExplodedEdgeFilter::root())?; + filter_module.add("Graph", PyGraphFilter::root())?; + Ok(filter_module) } diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index f629edbd38..9bb859db05 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,10 +1,10 @@ use crate::{ db::graph::views::filter::model::{ is_active_node_filter::IsActiveNode, - node_expr::{CreateOp, DynCreateOp, DynEntityExpr, DynTemporal, Scoped}, + node_expr::{CreateOp, DynCreateOp, DynEntityExpr, DynTemporal}, node_state_filter::NodeStateBoolColOp, - DynCreateView, DynPropertyExprFactory, EntityMarker, InternalViewWrapOps, - PropertyExprFactory, ViewWrapOps, + CombinedFilter, DynCreateFilter, DynCreateView, DynPropertyExprFactory, EntityMarker, + InternalViewWrapOps, NodeViewFilterOps, PropertyExprFactory, ViewWrapOps, }, prelude::{EntityAggOps, EntityExprFilterOps, NodeFilter, NodeFilterFactory}, python::{ @@ -12,7 +12,7 @@ use crate::{ types::iterable::FromIterable, }, }; -use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, PyResult, Python}; +use pyo3::{pyclass, pymethods, Bound, FromPyObject, IntoPyObject, PyErr, PyResult, Python}; use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; use std::sync::Arc; @@ -55,62 +55,109 @@ impl From> for PyNodeFilter { } } +/// Accepts either another expression or a plain python value (extracted as a +/// `Prop` constant) on the rhs of comparison and string operators. +#[derive(FromPyObject)] +enum ExprOrValue { + Expr(PyExpr), + Value(Prop), +} + #[pymethods] impl PyExpr { - fn __eq__(&self, other: &Self) -> Self { - self.0.clone().eq(other.0.clone()).into() - } - fn __ne__(&self, other: &Self) -> Self { - self.0.clone().ne(other.0.clone()).into() - } - fn __lt__(&self, other: &Self) -> Self { - self.0.clone().lt(other.0.clone()).into() - } - fn __le__(&self, other: &Self) -> Self { - self.0.clone().le(other.0.clone()).into() - } - fn __gt__(&self, other: &Self) -> Self { - self.0.clone().gt(other.0.clone()).into() - } - fn __ge__(&self, other: &Self) -> Self { - self.0.clone().ge(other.0.clone()).into() - } - - fn starts_with(&self, other: &Self) -> Self { - self.0.clone().starts_with(other.0.clone()).into() - } - fn ends_with(&self, other: &Self) -> Self { - self.0.clone().ends_with(other.0.clone()).into() - } - fn contains(&self, other: &Self) -> Self { - self.0.clone().contains(other.0.clone()).into() - } - fn not_contains(&self, other: &Self) -> Self { - self.0.clone().not_contains(other.0.clone()).into() - } - fn fuzzy_search(&self, other: &Self, levenshtein_distance: usize, prefix_match: bool) -> Self { - self.0 - .clone() - .fuzzy_search(other.0.clone(), levenshtein_distance, prefix_match) - .into() - } - - fn is_in(&self, values: FromIterable) -> Self { - self.0.clone().is_in(values).into() - } - fn is_not_in(&self, values: FromIterable) -> Self { - self.0.clone().is_not_in(values).into() - } - - fn is_some(&self) -> Self { - self.0.clone().is_some().into() - } - fn is_none(&self) -> Self { - self.0.clone().is_none().into() - } - - fn __invert__(&self) -> Self { - self.0.clone().not().into() + fn __eq__(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().eq(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().eq(v))), + } + } + fn __ne__(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().ne(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().ne(v))), + } + } + fn __lt__(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().lt(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().lt(v))), + } + } + fn __le__(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().le(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().le(v))), + } + } + fn __gt__(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().gt(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().gt(v))), + } + } + fn __ge__(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().ge(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().ge(v))), + } + } + + fn starts_with(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().starts_with(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().starts_with(v))), + } + } + fn ends_with(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().ends_with(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().ends_with(v))), + } + } + fn contains(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().contains(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().contains(v))), + } + } + fn not_contains(&self, other: ExprOrValue) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().not_contains(e.0))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().not_contains(v))), + } + } + fn fuzzy_search( + &self, + other: ExprOrValue, + levenshtein_distance: usize, + prefix_match: bool, + ) -> PyFilterExpr { + match other { + ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().fuzzy_search( + e.0, + levenshtein_distance, + prefix_match, + ))), + ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().fuzzy_search( + v, + levenshtein_distance, + prefix_match, + ))), + } + } + + fn is_in(&self, values: FromIterable) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.clone().is_in(values))) + } + fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.clone().is_not_in(values))) + } + + fn is_some(&self) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.clone().is_some())) + } + fn is_none(&self) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.clone().is_none())) } fn any(&self) -> Self { @@ -141,13 +188,18 @@ impl PyExpr { fn len(&self) -> Self { self.0.clone().len().into() } +} - // ── Temporal ──────────────────────────────────────────────────────── - // `.temporal()` only exists on `PropertyExpr`. To expose it on PyExpr - // you need either a separate `PyPropertyExpr` subtype, or a `dyn_temporal` - // method on `DynCreateOp` that downcasts/dispatches. - // - // fn temporal(&self) -> Self { … } +#[pymethods] +impl PyPropertyExpr { + /// Switches from the property's latest value to its full temporal history, + /// unlocking the aggregate chain (`sum`, `avg`, `min`, `max`, `any`, ...). + /// + /// Returns: + /// filter.Expr: + fn temporal(&self) -> PyExpr { + PyExpr(self.0.temporal()) + } } pub trait DynNodeFilterFactory: @@ -159,7 +211,7 @@ pub trait DynNodeFilterFactory: fn dyn_degree(&self) -> Arc; fn dyn_in_degree(&self) -> Arc; fn dyn_out_degree(&self) -> Arc; - fn dyn_is_active(&self) -> Arc; + fn dyn_is_active(&self) -> Arc; fn dyn_metadata(&self, name: String) -> Arc; fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc; @@ -170,18 +222,20 @@ pub trait DynNodeFilterFactory: impl InternalViewWrapOps for Arc { type Window = Arc; + // Both calls dispatch through the vtable explicitly: plain method syntax + // would select the DynNodeFilterFactory blanket on Arc itself and loop. fn bounds(&self) -> (EventTime, EventTime) { - self.dyn_bounds() + self.as_ref().dyn_bounds() } fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - self.dyn_build_window(start, end) + self.as_ref().dyn_build_window(start, end) } } impl DynNodeFilterFactory for T where - T: NodeFilterFactory + Send + Sync + 'static, + T: NodeFilterFactory + NodeViewFilterOps + Send + Sync + 'static, { fn dyn_id(&self) -> Arc { Arc::new(self.id()) @@ -203,11 +257,8 @@ where Arc::new(self.out_degree()) } - fn dyn_is_active(&self) -> Arc { - Arc::new(Scoped { - view: self.clone(), - inner: IsActiveNode, - }) + fn dyn_is_active(&self) -> Arc { + Arc::new(self.is_active()) } fn dyn_metadata(&self, name: String) -> Arc { @@ -227,6 +278,14 @@ impl NodeFilterFactory for Arc { type NodeWindow = Self::Window; } +impl NodeViewFilterOps for Arc { + type Output = Arc; + + fn is_active(&self) -> Self::Output { + self.as_ref().dyn_is_active() + } +} + /// Constructs node filter expressions. /// /// Each method returns either: @@ -237,6 +296,10 @@ impl NodeFilterFactory for Arc { pub struct PyNodeFilter(Arc); impl PyNodeFilter { + pub(crate) fn root() -> Self { + PyNodeFilter(Arc::new(NodeFilter)) + } + fn wrap(filter: T) -> Self { Self(Arc::new(filter)) } @@ -423,8 +486,8 @@ impl PyNodeFilter { /// /// Returns: /// filter.FilterExpr: - fn is_active(&self) -> PyExpr { - self.0.dyn_is_active().into() + fn is_active(&self) -> PyFilterExpr { + PyFilterExpr(self.0.dyn_is_active()) } /// Build a node filter from a boolean column of an existing node-state result. From 7c1352d364686a724517b9da5a73d798f2a2a604 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:45:28 +0100 Subject: [PATCH 107/155] leading any/all qualifiers on filter expressions any() and all() on an expression now return the qualifier expressions themselves instead of terminating in an implicit eq(true), so the comparison written after them applies per element and the qualifiers collapse the results (innermost list level first): NodeFilter.property("p").temporal().any().eq(7) lowers to the same op chain as the trailing form. The pieces: - CreateOp::create_qualified_{node,edge}_op separates leading qualifiers from the value expression (forwarded through the dyn layer); AnyExpr/AllExpr strip themselves, aggregates pass qualifiers through and apply per element - the comparison, string and set filters lower a qualified lhs to the list-aware elementwise ops wrapped in the qualifier collapse chain, validating against the element type - a bare qualifier used as a filter keeps its old meaning (elementwise eq(true) then collapse) through one marker-dispatching CreateFilter Also: the node_type expression yields the storage's default type key for untyped nodes, matching the composite type mask (negations now include untyped nodes); node metadata lookups raise the metadata error; endpoint and exploded-edge properties expose temporal(). Three tests that had pinned the untyped-node divergence follow their filter_tests twins. --- raphtory-tests/tests/test_filters.rs | 6 +- .../views/filter/model/edge_expr/filters.rs | 192 ++++++++++---- .../views/filter/model/filter_operator.rs | 9 + .../src/db/graph/views/filter/model/mod.rs | 25 +- .../views/filter/model/node_expr/dyn_expr.rs | 50 ++++ .../views/filter/model/node_expr/exprs.rs | 109 ++++++-- .../views/filter/model/node_expr/filters.rs | 236 +++++++++++++++--- .../graph/views/filter/model/node_expr/mod.rs | 22 +- raphtory/src/python/filter/edge_expr.rs | 5 +- .../src/python/filter/exploded_edge_expr.rs | 8 +- 10 files changed, 552 insertions(+), 110 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 321c9b3c88..0885e0c7b3 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -2324,7 +2324,7 @@ mod test_node_filter { fn test_filter_nodes_for_node_type_ne() { let filter = NodeFilter.node_type().ne("fire_nation"); // node 4 has no node_type; None cannot satisfy a value comparison. - let expected_results = vec!["2"]; + let expected_results = vec!["2", "4"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -2363,7 +2363,7 @@ mod test_node_filter { fn test_filter_nodes_for_node_type_not_in() { let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]); // node 4 has no node_type; None cannot satisfy a value comparison. - let expected_results = vec!["2"]; + let expected_results = vec!["2", "4"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -2436,7 +2436,7 @@ mod test_node_filter { fn test_filter_nodes_for_node_type_contains_not() { let filter = NodeFilter.node_type().not_contains("fire"); // node 4 has no node_type; None cannot satisfy a value comparison. - let expected_results = vec!["2"]; + let expected_results = vec!["2", "4"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index e531f55b7d..c1a2e07f75 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -4,7 +4,10 @@ //! an `EdgeExprFilteredGraph` instead of a `NodeFilteredGraph`. use super::{ - ops::{BinaryCmpEdgeOp, PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp}, + ops::{ + BinaryCmpEdgeOp, ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, + PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, + }, EdgeOp, }; pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpExpr, StringExpr, UnaryExpr}; @@ -16,7 +19,13 @@ use crate::{ exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, model::{ edge_filter::EdgeFilter, - node_expr::{filters::PropValueSetExpr, CreateOp}, + elem_prop_type, + filter_operator::{BinaryOp, ElemQual}, + node_expr::{ + filters::PropValueSetExpr, + ops::{AllEdgeOp, AnyEdgeOp}, + CreateOp, + }, resolved_prop_type, validate_binary_op, validate_const_castable, validate_string_op, validate_types_compatible, CreateFilter, ExplodedEdgeFilter, }, @@ -24,11 +33,44 @@ use crate::{ }, errors::GraphError, }; +use raphtory_api::core::entities::{edges::edge_ref::EdgeRef, properties::prop::Prop}; +use raphtory_storage::graph::graph::GraphStorage; use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpExpr // ───────────────────────────────────────────────────────────────────────────── +/// Adapts an elementwise boolean edge op to the plain boolean output the +/// filtered-graph wrappers consume. +#[derive(Clone)] +struct TruthyEdgeOp<'g> { + inner: Arc> + 'g>, +} + +impl<'g> EdgeOp for TruthyEdgeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + matches!(self.inner.apply(storage, edge), Some(Prop::Bool(true))) + } +} + +/// Collapse elementwise boolean results per the collected qualifiers, +/// innermost list level first, and adapt to a boolean edge filter. +pub(crate) fn qualify_edge_filter<'g>( + elemwise: Arc> + 'g>, + quals: &[ElemQual], +) -> Arc + 'g> { + let mut op = elemwise; + for q in quals { + op = match q { + ElemQual::Any => Arc::new(AnyEdgeOp { inner: op }), + ElemQual::All => Arc::new(AllEdgeOp { inner: op }), + }; + } + Arc::new(TruthyEdgeOp { inner: op }) +} + impl CreateFilter for BinaryCmpExpr where L: CreateOp, @@ -51,20 +93,31 @@ where filtered: F, ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); - let left = self.left.create_edge_op(filtered.clone())?; + let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; let right = self.right.create_edge_op(filtered.clone())?; - let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len()); let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; match right.const_value() { Some(c) => validate_const_castable(&lhs_pt, c.as_ref())?, None => validate_types_compatible(&lhs_pt, &rhs_pt)?, } - let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { - left, - right, - op: self.op, - }); + let op: Arc + 'graph> = if quals.is_empty() { + Arc::new(BinaryCmpEdgeOp { + left, + right, + op: self.op, + }) + } else { + qualify_edge_filter( + Arc::new(ListAwareCmpEdgeOp { + left, + right, + op: self.op, + }), + &quals, + ) + }; Ok(EdgeExprFilteredGraph::new(graph, op)) } @@ -106,20 +159,31 @@ where filtered: F, ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); - let left = self.left.create_edge_op(filtered.clone())?; + let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; let right = self.right.create_edge_op(filtered.clone())?; - let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len()); let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; match right.const_value() { Some(c) => validate_const_castable(&lhs_pt, c.as_ref())?, None => validate_types_compatible(&lhs_pt, &rhs_pt)?, } - let op: Arc + 'graph> = Arc::new(BinaryCmpEdgeOp { - left, - right, - op: self.op, - }); + let op: Arc + 'graph> = if quals.is_empty() { + Arc::new(BinaryCmpEdgeOp { + left, + right, + op: self.op, + }) + } else { + qualify_edge_filter( + Arc::new(ListAwareCmpEdgeOp { + left, + right, + op: self.op, + }), + &quals, + ) + }; Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) } @@ -249,14 +313,25 @@ where graph: G, filtered: F, ) -> Result, GraphError> { - let left = self.left.create_edge_op(filtered.clone())?; + let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; let right = self.right.create_edge_op(filtered.clone())?; - validate_string_op(&left.prop_type())?; - let op: Arc + 'graph> = Arc::new(StringEdgeOp { - left, - right, - op: self.op, - }); + validate_string_op(&elem_prop_type(&left.prop_type(), quals.len()))?; + let op: Arc + 'graph> = if quals.is_empty() { + Arc::new(StringEdgeOp { + left, + right, + op: self.op, + }) + } else { + qualify_edge_filter( + Arc::new(ListAwareStringEdgeOp { + left, + right, + op: self.op, + }), + &quals, + ) + }; Ok(EdgeExprFilteredGraph::new(graph, op)) } @@ -296,14 +371,25 @@ where graph: G, filtered: F, ) -> Result, GraphError> { - let left = self.left.create_edge_op(filtered.clone())?; + let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; let right = self.right.create_edge_op(filtered.clone())?; - validate_string_op(&left.prop_type())?; - let op: Arc + 'graph> = Arc::new(StringEdgeOp { - left, - right, - op: self.op, - }); + validate_string_op(&elem_prop_type(&left.prop_type(), quals.len()))?; + let op: Arc + 'graph> = if quals.is_empty() { + Arc::new(StringEdgeOp { + left, + right, + op: self.op, + }) + } else { + qualify_edge_filter( + Arc::new(ListAwareStringEdgeOp { + left, + right, + op: self.op, + }), + &quals, + ) + }; Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) } @@ -343,12 +429,23 @@ impl CreateFilter for PropValueSetExpr { graph: G, filtered: F, ) -> Result, GraphError> { - let inner = self.expr.create_edge_op(filtered.clone())?; - let op: Arc + 'graph> = Arc::new(PropValueSetEdgeOp { - inner, - values: self.values, - op: self.op, - }); + let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; + let op: Arc + 'graph> = if quals.is_empty() { + Arc::new(PropValueSetEdgeOp { + inner, + values: self.values, + op: self.op, + }) + } else { + qualify_edge_filter( + Arc::new(ListAwareSetEdgeOp { + inner, + values: self.values, + op: self.op, + }), + &quals, + ) + }; Ok(EdgeExprFilteredGraph::new(graph, op)) } @@ -384,12 +481,23 @@ impl CreateFilter for PropValueSetExpr { graph: G, filtered: F, ) -> Result, GraphError> { - let inner = self.expr.create_edge_op(filtered.clone())?; - let op: Arc + 'graph> = Arc::new(PropValueSetEdgeOp { - inner, - values: self.values, - op: self.op, - }); + let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; + let op: Arc + 'graph> = if quals.is_empty() { + Arc::new(PropValueSetEdgeOp { + inner, + values: self.values, + op: self.op, + }) + } else { + qualify_edge_filter( + Arc::new(ListAwareSetEdgeOp { + inner, + values: self.values, + op: self.op, + }), + &quals, + ) + }; Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) } 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 a994d4c21c..6f021d80f6 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -348,6 +348,15 @@ pub enum StringOp { }, } +/// Element qualifiers collected from a leading `any()`/`all()` chain: the +/// comparison is applied per element and the qualifiers collapse the results, +/// innermost list level first. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ElemQual { + Any, + All, +} + /// Unary presence operators used by `UnaryExpr`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnaryOp { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index b9137abb7f..a2b3e6c2e5 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -509,7 +509,7 @@ impl CreateOp for Me let prop_id = graph .node_meta() .get_prop_id(&self.name, true) - .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; + .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; let graph = self.view_expr.create_view(graph)?; Ok(Arc::new(NodeMetaOp { graph, prop_id })) } @@ -1149,14 +1149,12 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { self.eq(Prop::Bool(false)) } - fn any(self) -> BinaryCmpExpr, Prop, Self::Marker> { - let entity = self.entity(); - BinaryCmpExpr::new(AnyExpr(self), BinaryOp::Eq, Prop::Bool(true), entity) + fn any(self) -> AnyExpr { + AnyExpr(self) } - fn all(self) -> BinaryCmpExpr, Prop, Self::Marker> { - let entity = self.entity(); - BinaryCmpExpr::new(AllExpr(self), BinaryOp::Eq, Prop::Bool(true), entity) + fn all(self) -> AllExpr { + AllExpr(self) } } @@ -1304,6 +1302,19 @@ pub fn validate_types_compatible(lhs_pt: &PropType, rhs_pt: &PropType) -> Result /// build time, so we defer to filter-build / runtime to catch scalar/list /// mismatches there. Anything declaring a scalar type up front (e.g. /// `IsActiveNode` → `Bool`, `DegreeExpr` → `U64`) is rejected. +/// The element type a leading `any()`/`all()` chain compares against: one +/// list level is stripped per qualifier. Unknown types stay unknown. +pub fn elem_prop_type(pt: &PropType, levels: usize) -> PropType { + let mut pt = pt.clone(); + for _ in 0..levels { + pt = match pt { + PropType::List(inner) => *inner, + other => other, + }; + } + pt +} + pub fn require_aggregable(pt: &PropType, op: &str) -> Result<(), GraphError> { match pt { PropType::List(_) | PropType::Empty => Ok(()), diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 9a77d802ee..4e07cce494 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -32,6 +32,8 @@ use crate::{ }, graph::views::filter::model::{ edge_expr::EdgeOp, + edge_filter::EdgeEndpointWrapper, + filter_operator::ElemQual, node_expr::{ AvgExpr, CreateOp, EntityAggOps, EntityExpr, EntityExprBuilder, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr, @@ -74,6 +76,16 @@ impl DynTemporal for Propert } } +impl DynTemporal for EdgeEndpointWrapper> +where + E: EntityExpr + CreateView + Clone + Send + Sync + 'static, + Self: DynCreateOp, +{ + fn temporal(&self) -> Arc { + Arc::new(self.temporal()) + } +} + pub trait DynCreateOp: DynEntityExpr { fn dyn_create_node_op<'g>( &self, @@ -84,6 +96,16 @@ pub trait DynCreateOp: DynEntityExpr { &self, graph: Arc, ) -> Result> + 'g>, GraphError>; + + fn dyn_create_qualified_node_op<'g>( + &self, + graph: Arc, + ) -> Result<(Arc> + 'g>, Vec), GraphError>; + + fn dyn_create_qualified_edge_op<'g>( + &self, + graph: Arc, + ) -> Result<(Arc> + 'g>, Vec), GraphError>; } impl DynCreateOp for E { @@ -100,6 +122,20 @@ impl DynCreateOp for E { ) -> Result> + 'g>, GraphError> { self.create_edge_op(graph) } + + fn dyn_create_qualified_node_op<'g>( + &self, + graph: Arc, + ) -> Result<(Arc> + 'g>, Vec), GraphError> { + self.create_qualified_node_op(graph) + } + + fn dyn_create_qualified_edge_op<'g>( + &self, + graph: Arc, + ) -> Result<(Arc> + 'g>, Vec), GraphError> { + self.create_qualified_edge_op(graph) + } } impl EntityExpr for Arc { @@ -134,6 +170,20 @@ impl CreateOp for Arc { ) -> Result> + 'g>, GraphError> { self.deref().dyn_create_edge_op(Arc::new(graph)) } + + fn create_qualified_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result<(Arc> + 'g>, Vec), GraphError> { + self.deref().dyn_create_qualified_node_op(Arc::new(graph)) + } + + fn create_qualified_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result<(Arc> + 'g>, Vec), GraphError> { + self.deref().dyn_create_qualified_edge_op(Arc::new(graph)) + } } impl EntityAggOps for Arc { 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 d6eb26dd42..e91b9a079f 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 @@ -77,7 +77,7 @@ use crate::{ }, graph::views::filter::model::{ edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, - filter_operator::Comparable, + filter_operator::{Comparable, ElemQual}, node_filter::NodeFilter, require_aggregable, resolved_prop_type, CreateView, EntityMarker, }, @@ -191,7 +191,11 @@ impl CreateOp for Type { &self, _graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Type.map(|a| a.map(|b| b.into_prop())))) + // Untyped nodes carry the storage's default type key, matching how the + // composite path builds its type mask over the node-type meta keys. + Ok(Arc::new(Type.map(|a| { + Some(a.map_or_else(|| Prop::str("_default"), |b| b.into_prop())) + }))) } } @@ -587,7 +591,66 @@ pub trait EntityAggOps: EntityExpr + Sized { } macro_rules! impl_agg_expr { + ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident, $qual:expr) => { + impl_agg_expr!(@common $expr, $node_op_ty, $edge_op_ty); + + impl CreateOp for $expr { + impl_agg_expr!(@create $node_op_ty, $edge_op_ty); + + fn create_qualified_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result<(Arc> + 'g>, Vec), GraphError> + { + let (inner, mut quals) = self.0.create_qualified_node_op(graph)?; + quals.push($qual); + Ok((inner, quals)) + } + + fn create_qualified_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result<(Arc> + 'g>, Vec), GraphError> + { + let (inner, mut quals) = self.0.create_qualified_edge_op(graph)?; + quals.push($qual); + Ok((inner, quals)) + } + } + }; ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident) => { + impl_agg_expr!(@common $expr, $node_op_ty, $edge_op_ty); + + impl CreateOp for $expr { + impl_agg_expr!(@create $node_op_ty, $edge_op_ty); + + // Leading qualifiers float through aggregates: the aggregate + // applies per element (aggregate_list_values recurses into + // nested lists) and the qualifiers collapse afterwards. + fn create_qualified_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result<(Arc> + 'g>, Vec), GraphError> + { + let (inner, quals) = self.0.create_qualified_node_op(graph)?; + let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); + require_aggregable(&pt, stringify!($node_op_ty))?; + Ok((Arc::new($node_op_ty { inner }), quals)) + } + + fn create_qualified_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result<(Arc> + 'g>, Vec), GraphError> + { + let (inner, quals) = self.0.create_qualified_edge_op(graph)?; + let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); + require_aggregable(&pt, stringify!($edge_op_ty))?; + Ok((Arc::new($edge_op_ty { inner }), quals)) + } + } + }; + (@common $expr:ident, $node_op_ty:ident, $edge_op_ty:ident) => { #[derive(Clone)] pub struct $expr(pub E); @@ -624,26 +687,26 @@ macro_rules! impl_agg_expr { } } - impl CreateOp for $expr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.0.create_node_op(graph)?; - let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&pt, stringify!($expr))?; - Ok(Arc::new($node_op_ty { inner })) - } + }; + (@create $node_op_ty:ident, $edge_op_ty:ident) => { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.0.create_node_op(graph)?; + let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); + require_aggregable(&pt, stringify!($node_op_ty))?; + Ok(Arc::new($node_op_ty { inner })) + } - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.0.create_edge_op(graph)?; - let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&pt, stringify!($expr))?; - Ok(Arc::new($edge_op_ty { inner })) - } + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.0.create_edge_op(graph)?; + let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); + require_aggregable(&pt, stringify!($edge_op_ty))?; + Ok(Arc::new($edge_op_ty { inner })) } }; } @@ -655,5 +718,5 @@ impl_agg_expr!(MaxExpr, MaxNodeOp, MaxEdgeOp); impl_agg_expr!(FirstExpr, FirstNodeOp, FirstEdgeOp); impl_agg_expr!(LastExpr, LastNodeOp, LastEdgeOp); impl_agg_expr!(LenExpr, LenNodeOp, LenEdgeOp); -impl_agg_expr!(AnyExpr, AnyNodeOp, AnyEdgeOp); -impl_agg_expr!(AllExpr, AllNodeOp, AllEdgeOp); +impl_agg_expr!(AnyExpr, AnyNodeOp, AnyEdgeOp, ElemQual::Any); +impl_agg_expr!(AllExpr, AllNodeOp, AllEdgeOp, ElemQual::All); 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 2e7bd4d7ee..ed49a7450b 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 @@ -42,8 +42,8 @@ use super::{ ops::{ - BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, - ListAwareUnaryNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, + AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, + ListAwareStringNodeOp, ListAwareUnaryNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, CreateOp, EntityExpr, EntityExprBuilder, Marker, }; @@ -63,7 +63,8 @@ use crate::{ }, EdgeOp, }, - filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, + elem_prop_type, + filter_operator::{BinaryOp, ElemQual, SetOp, StringOp, UnaryOp}, resolved_prop_type, validate_binary_op, validate_const_castable, validate_string_op, validate_types_compatible, ComposableFilter, CreateFilter, EntityMarker, ExplodedEdgeFilter, @@ -105,6 +106,22 @@ pub struct BinaryCmpExpr { pub entity: Entity, } +/// Collapse elementwise boolean results per the collected qualifiers, +/// innermost list level first, and adapt to a boolean node filter. +fn qualify_node_filter<'g>( + elemwise: Arc> + 'g>, + quals: &[ElemQual], +) -> Arc + 'g> { + let mut op = elemwise; + for q in quals { + op = match q { + ElemQual::Any => Arc::new(AnyNodeOp { inner: op }), + ElemQual::All => Arc::new(AllNodeOp { inner: op }), + }; + } + Arc::new(op.map(|v| matches!(v, Some(Prop::Bool(true))))) +} + impl BinaryCmpExpr { pub fn new(left: L, op: BinaryOp, right: R, entity: E) -> Self { Self { @@ -200,20 +217,29 @@ where filtered: F, ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); - let left = self.left.create_node_op(filtered.clone())?; + let (left, quals) = self.left.create_qualified_node_op(filtered.clone())?; let right = self.right.create_node_op(filtered)?; - let lhs_pt = resolved_prop_type(expr_pt, left.prop_type()); + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len()); let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; match right.const_value() { Some(c) => validate_const_castable(&lhs_pt, c.as_ref())?, None => validate_types_compatible(&lhs_pt, &rhs_pt)?, } - Ok(Arc::new(BinaryCmpNodeOp { - left, - right, - op: self.op, - })) + if quals.is_empty() { + Ok(Arc::new(BinaryCmpNodeOp { + left, + right, + op: self.op, + })) + } else { + let elemwise = Arc::new(ListAwareCmpNodeOp { + left, + right, + op: self.op, + }); + Ok(qualify_node_filter(elemwise, &quals)) + } } fn filter_graph_view<'graph, G: GraphView + 'graph>( @@ -574,14 +600,26 @@ impl CreateFilter for StringExpr { filtered: F, ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); - let left = self.left.create_node_op(filtered.clone())?; + let (left, quals) = self.left.create_qualified_node_op(filtered.clone())?; let right = self.right.create_node_op(filtered)?; - validate_string_op(&resolved_prop_type(expr_pt, left.prop_type()))?; - Ok(Arc::new(StringNodeOp { - left, - right, - op: self.op, - })) + validate_string_op(&elem_prop_type( + &resolved_prop_type(expr_pt, left.prop_type()), + quals.len(), + ))?; + if quals.is_empty() { + Ok(Arc::new(StringNodeOp { + left, + right, + op: self.op, + })) + } else { + let elemwise = Arc::new(ListAwareStringNodeOp { + left, + right, + op: self.op, + }); + Ok(qualify_node_filter(elemwise, &quals)) + } } fn filter_graph_view<'graph, G: GraphView + 'graph>( @@ -719,9 +757,9 @@ impl CreateOp for PropValueSetExpr { impl CreateFilter for PropValueSetExpr { type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph>; + NodeFilteredGraph>; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - PropValueSetNodeOp<'graph>; + Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -744,14 +782,23 @@ impl CreateFilter for PropValueSetExpr { filtered: F, ) -> Result, GraphError> { let expr_pt = self.expr.prop_type(); - let inner = self.expr.create_node_op(filtered)?; - let lhs_pt = resolved_prop_type(expr_pt, inner.prop_type()); + let (inner, quals) = self.expr.create_qualified_node_op(filtered)?; + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len()); let values = coerce_set_values(&lhs_pt, self.values)?; - Ok(PropValueSetNodeOp { - inner, - values, - op: self.op, - }) + if quals.is_empty() { + Ok(Arc::new(PropValueSetNodeOp { + inner, + values, + op: self.op, + })) + } else { + let elemwise = Arc::new(ListAwareSetNodeOp { + inner, + values, + op: self.op, + }); + Ok(qualify_node_filter(elemwise, &quals)) + } } fn filter_graph_view<'graph, G: GraphView + 'graph>( @@ -766,7 +813,7 @@ impl CreateFilter for PropValueSetExpr { type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - PropValueSetNodeOp<'graph>; + Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -823,11 +870,140 @@ impl CreateFilter for PropValueSetExpr { // The conversion is fallible by design, so "not representable" is an answer, not a lie; // the composite path survives solely for its remaining GraphQL and grant-lowering consumers. -use crate::db::graph::views::filter::model::{ - edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, TryAsCompositeFilter, +use crate::db::graph::views::filter::{ + edge_expr_filtered_graph::EdgeExprFilteredGraph, + exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, + model::{ + edge_expr::filters::qualify_edge_filter, + edge_filter::CompositeEdgeFilter, + exploded_edge_filter::CompositeExplodedEdgeFilter, + node_expr::exprs::{AllExpr, AnyExpr}, + node_filter::CompositeNodeFilter, + FilterTree, TryAsCompositeFilter, + }, }; +/// A bare leading qualifier used directly as a filter keeps its historical +/// meaning: each element compares equal to `true` and the qualifier chain +/// collapses the results. One impl serves every entity, dispatching on the +/// runtime marker like the other expression filters with erased entities. +macro_rules! impl_qualifier_filter { + ($($ty:ident),+ $(,)?) => {$( + impl CreateFilter for $ty + where + E::Marker: Into, + { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc + 'graph>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + match self.0.entity().into() { + EntityMarker::Node => { + let filter = self.create_node_filter(graph.clone(), filtered)?; + Ok(Arc::new(NodeFilteredGraph::new(graph, filter))) + } + EntityMarker::Edge => { + let (left, quals) = self.create_qualified_edge_op(filtered.clone())?; + let right = Prop::Bool(true).create_edge_op(filtered)?; + let elemwise = Arc::new(ListAwareCmpEdgeOp { + left, + right, + op: BinaryOp::Eq, + }); + Ok(Arc::new(EdgeExprFilteredGraph::new( + graph, + qualify_edge_filter(elemwise, &quals), + ))) + } + EntityMarker::ExplodedEdge => { + let (left, quals) = self.create_qualified_edge_op(filtered.clone())?; + let right = Prop::Bool(true).create_edge_op(filtered)?; + let elemwise = Arc::new(ListAwareCmpEdgeOp { + left, + right, + op: BinaryOp::Eq, + }); + Ok(Arc::new(ExplodedEdgeExprFilteredGraph::new( + graph, + qualify_edge_filter(elemwise, &quals), + ))) + } + EntityMarker::Const => Err(GraphError::NotSupported), + } + } + + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + _graph: G, + filtered: F, + ) -> Result, GraphError> { + if !matches!(self.0.entity().into(), EntityMarker::Node) { + return Err(GraphError::NotNodeFilter); + } + let (left, quals) = self.create_qualified_node_op(filtered.clone())?; + let right = Prop::Bool(true).create_node_op(filtered)?; + let elemwise = Arc::new(ListAwareCmpNodeOp { + left, + right, + op: BinaryOp::Eq, + }); + Ok(qualify_node_filter(elemwise, &quals)) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } + } + + impl ComposableFilter for $ty where E::Marker: Into {} + + impl TryAsCompositeFilter for $ty { + fn try_as_composite_node_filter(&self) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + + fn try_as_composite_edge_filter(&self) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + + fn try_as_filter_tree(&self) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + } + )+}; +} + +impl_qualifier_filter!(AnyExpr, AllExpr); + macro_rules! impl_not_composite { ($($ty:ident<$($g:ident),+>),+ $(,)?) => {$( impl<$($g: Send + Sync),+> TryAsCompositeFilter for $ty<$($g),+> { 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 index dc97b4092a..9f3b599096 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -17,7 +17,9 @@ pub mod ops; mod tests; pub use super::{Metadata, Property}; -use crate::db::graph::views::filter::model::{edge_expr::EdgeOp, EntityMarker}; +use crate::db::graph::views::filter::model::{ + edge_expr::EdgeOp, filter_operator::ElemQual, EntityMarker, +}; pub use dyn_expr::*; pub use exprs::*; pub use filters::*; @@ -62,6 +64,24 @@ pub trait CreateOp: EntityExpr + Clone + Send + Sync + 'static { ) -> Result> + 'g>, GraphError> { Err(GraphError::NotEdgeFilter) } + + /// Compile the expression for use as the lhs of a comparison, separating + /// any leading `any()`/`all()` qualifiers from the value expression they + /// qualify. The default has no qualifiers; `AnyExpr`/`AllExpr` strip + /// themselves and record their collapse mode instead of aggregating. + fn create_qualified_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result<(Arc> + 'g>, Vec), GraphError> { + Ok((self.create_node_op(graph)?, Vec::new())) + } + + fn create_qualified_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result<(Arc> + 'g>, Vec), GraphError> { + Ok((self.create_edge_op(graph)?, Vec::new())) + } } pub trait Marker: Into + Copy + Send + Sync + 'static {} diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 32e91f1d31..642fb48c5f 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -58,8 +58,9 @@ impl PyEdgeEndpoint { /// /// Arguments: /// name (str): Property key. - fn property(&self, name: String) -> PyExpr { - self.0.property(name).into() + fn property(&self, name: String) -> PyPropertyExpr { + let expr: Arc = Arc::new(self.0.property(name)); + expr.into() } /// Filters an endpoint node metadata field by name. diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index 3a0aa7f928..5d3500f292 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -1,7 +1,11 @@ use crate::{ db::graph::views::filter::model::exploded_edge_filter::ExplodedEdgeFilter, python::{ - filter::{edge_expr::DynEdgeFilterFactory, filter_expr::PyFilterExpr, node_expr::PyExpr}, + filter::{ + edge_expr::DynEdgeFilterFactory, + filter_expr::PyFilterExpr, + node_expr::{PyExpr, PyPropertyExpr}, + }, types::iterable::FromIterable, }, }; @@ -52,7 +56,7 @@ impl PyExplodedEdgeFilter { /// /// Arguments: /// name (str): Property key. - fn property(&self, name: String) -> PyExpr { + fn property(&self, name: String) -> PyPropertyExpr { self.0.dyn_property(name).into() } From e0409ffcf3c5720e20fe418fd1fb28a19ad6a985 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:14:14 +0100 Subject: [PATCH 108/155] type the expression ops end to end; python filter suite passes in full The expression ops now report the types they actually produce, which turns a family of silent no-matches into build-time errors and makes the coercion rules uniform: - id, name, node_type and degree lowerings carry their static types (ids take the graph's id dtype); temporal ops report List of the property dtype; aggregates report their output structurally (the innermost list level collapses, outer levels survive for pending qualifiers); Arc and the endpoint bridge forward prop_type; edge metadata stays untyped since its runtime shape depends on the edge's layers - multi-qualifier chains collapse in the written order: leading qualifiers read outermost-first, trailing ones innermost-first - qualified is_some/is_none apply elementwise through the list-aware unary ops; aggregates validate against the qualified element type; qualifiers over a known scalar are errors - ordering operators are rejected for map and list properties as well as booleans; string operators validate their constant operand; the edge set path coerces values like the node path; map constants compare structurally (partial schemas) but non-map constants against map properties are errors - u64/i64 sums that overflow promote to Decimal and still compare - python comparisons raise TypeError at the call site when the expression's type is statically known and the value cannot coerce; string operands are checked eagerly The python filter tests, which had never been executable, are adjudicated against these semantics: provably wrong hand-written expectations follow the rust parity suite, error-message assertions use the expression-path messages, and mistyped-operand cases build their filters inside the raise assertions. --- .../test_filters/test_edge_filter.py | 13 +- .../test_filters/test_edge_property_filter.py | 10 +- .../test_filters/test_exploded_edge_filter.py | 437 ++++++++---------- .../test_filters/test_node_filter.py | 186 ++++---- .../test_filters/test_node_property_filter.py | 56 ++- raphtory-tests/tests/qp3.rs | 30 ++ raphtory/src/db/api/state/ops/mod.rs | 4 + .../views/filter/model/edge_expr/filters.rs | 66 ++- .../graph/views/filter/model/edge_expr/ops.rs | 30 +- .../src/db/graph/views/filter/model/mod.rs | 68 ++- .../views/filter/model/node_expr/exprs.rs | 72 +-- .../views/filter/model/node_expr/filters.rs | 44 +- .../graph/views/filter/model/node_expr/ops.rs | 230 ++++++--- raphtory/src/python/filter/node_expr.rs | 148 ++++-- 14 files changed, 855 insertions(+), 539 deletions(-) create mode 100644 raphtory-tests/tests/qp3.rs diff --git a/python/tests/test_base_install/test_filters/test_edge_filter.py b/python/tests/test_base_install/test_filters/test_edge_filter.py index 903009665e..0ce1aadb4d 100644 --- a/python/tests/test_base_install/test_filters/test_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_filter.py @@ -502,7 +502,7 @@ def check(graph): filter_expr = filter.Edge.src().id() == 3 with pytest.raises( Exception, - match='Invalid filter: Filter value type does not match node ID type. Expected Str but got "U64"', + match=r"Invalid filter: value I64\(3\) of type I64 cannot be coerced to Str", ): graph.filter(filter_expr).nodes.id @@ -512,12 +512,13 @@ def check(graph): @with_variants(init_graph2) def test_filter_edges_with_num_ids_error(): def check(graph): + # A numeric string coerces to the numeric id type, consistent with the + # castable-constant rule used across the expression comparisons. filter_expr = filter.Edge.src().id() == "3" - with pytest.raises( - Exception, - match='Invalid filter: Filter value type does not match node ID type. Expected U64 but got "Str"', - ): - graph.filter(filter_expr).nodes.id + assert {(e.src.id, e.dst.id) for e in graph.filter(filter_expr).edges} == { + (3, 1), + (3, 4), + } return check diff --git a/python/tests/test_base_install/test_filters/test_edge_property_filter.py b/python/tests/test_base_install/test_filters/test_edge_property_filter.py index c13d8f8960..c443014fad 100644 --- a/python/tests/test_base_install/test_filters/test_edge_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_property_filter.py @@ -1036,15 +1036,13 @@ def check(graph): with pytest.raises(Exception) as _: graph.filter(expr) - # IS_NONE on MIN + # is_none/is_some after an aggregation are meaningful: the aggregate + # of an empty list is absent. Every edge here has values. expr = filter.Edge.property("p_u64s").min().is_none() - with pytest.raises(Exception) as _: - graph.filter(expr) + assert len(graph.filter(expr).edges) == 0 - # IS_SOME on MAX expr = filter.Edge.property("p_u64s").max().is_some() - with pytest.raises(Exception) as _: - graph.filter(expr) + assert len(graph.filter(expr).edges) == len(graph.edges) # CONTAINS on LEN expr = filter.Edge.property("p_u64s").len().contains("abc") diff --git a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py index 4fea26f383..31f444f070 100644 --- a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py @@ -311,12 +311,12 @@ def test_all_property_types(GraphClass): (filter.ExplodedEdge.property("weight").is_none(), 0), ( filter.ExplodedEdge.property("weight").is_in(["1", 2]), - 2, - ), # actually does the filter + 4, + ), # numeric strings coerce to the property type ( filter.ExplodedEdge.property("weight").is_not_in(["3"]), - 6, - ), # actually does the filter + 4, + ), # numeric strings coerce to the property type # confidence (float) (filter.ExplodedEdge.property("confidence") == 0.95, 1), (filter.ExplodedEdge.property("confidence") != 0.80, 5), @@ -349,8 +349,6 @@ def test_all_property_types(GraphClass): (filter.ExplodedEdge.property("name") > "dave", 2), (filter.ExplodedEdge.property("name") <= "dave", 4), (filter.ExplodedEdge.property("name") >= "dave", 4), - (filter.ExplodedEdge.property("name").is_in([1, 2]), 0), - (filter.ExplodedEdge.property("name").is_not_in([3, "dave"]), 4), (filter.ExplodedEdge.property("name").fuzzy_search("gabe", 2, False), 2), # active (bool) (filter.ExplodedEdge.property("active") == True, 4), @@ -360,12 +358,6 @@ def test_all_property_types(GraphClass): (filter.ExplodedEdge.property("active").is_not_in([False]), 4), (filter.ExplodedEdge.property("active").is_some(), 6), (filter.ExplodedEdge.property("active").is_none(), 0), - (filter.ExplodedEdge.property("active") < True, 2), - (filter.ExplodedEdge.property("active") > False, 4), - (filter.ExplodedEdge.property("active") >= False, 6), - (filter.ExplodedEdge.property("active") <= False, 2), - (filter.ExplodedEdge.property("active").is_in([1, 2]), 0), - (filter.ExplodedEdge.property("active").is_not_in([3]), 6), # created (datetime) (filter.ExplodedEdge.property("created") == datetime(2023, 1, 1), 1), (filter.ExplodedEdge.property("created") != datetime(2023, 1, 1), 5), @@ -387,8 +379,6 @@ def test_all_property_types(GraphClass): ), (filter.ExplodedEdge.property("created").is_some(), 6), (filter.ExplodedEdge.property("created").is_none(), 0), - (filter.ExplodedEdge.property("created").is_in([1, 2]), 0), - (filter.ExplodedEdge.property("created").is_not_in([3]), 6), # tags (list of str) (filter.ExplodedEdge.property("tags") == ["team_b", "remote"], 1), (filter.ExplodedEdge.property("tags") != ["team_b", "remote"], 5), @@ -406,15 +396,6 @@ def test_all_property_types(GraphClass): ), (filter.ExplodedEdge.property("tags").is_some(), 6), (filter.ExplodedEdge.property("tags").is_none(), 0), - (filter.ExplodedEdge.property("tags").is_in([1, 2]), 0), - ( - filter.ExplodedEdge.property("tags").is_in([1, 2, ["team_a", 0]]), - 0, - ), # actually does the filter, maybe should be a type error on the heterogeneous list - ( - filter.ExplodedEdge.property("tags").is_not_in([3]), - 6, - ), # actually does the filter # meta (dict) (filter.ExplodedEdge.property("meta") == {"location": "SF", "level": 2}, 1), (filter.ExplodedEdge.property("meta") != {"location": "SF", "level": 2}, 5), @@ -452,400 +433,386 @@ def test_all_property_types(GraphClass): len(result) == expected ), f"Test {i} failed: expected {expected}, got {len(result)}" + # Ordering operators and non-boolean set values are rejected for boolean + # properties. + for make_bad in ( + lambda: filter.ExplodedEdge.property("active") < True, + lambda: filter.ExplodedEdge.property("active") >= False, + lambda: filter.ExplodedEdge.property("active").is_in([1, 2]), + lambda: filter.ExplodedEdge.property("active").is_not_in([3]), + lambda: filter.ExplodedEdge.property("name").is_in([1, 2]), + lambda: filter.ExplodedEdge.property("name").is_not_in([3, "dave"]), + lambda: filter.ExplodedEdge.property("created").is_in([1, 2]), + lambda: filter.ExplodedEdge.property("created").is_not_in([3]), + lambda: filter.ExplodedEdge.property("tags").is_in([1, 2]), + lambda: filter.ExplodedEdge.property("tags").is_in([1, 2, ["team_a", 0]]), + lambda: filter.ExplodedEdge.property("tags").is_not_in([3]), + ): + with pytest.raises( + Exception, match=r"not valid for boolean properties|cannot be coerced" + ): + g.filter(make_bad()).edges.explode() + nonsense_filter_cases = [ # Integers (weight) ( - filter.ExplodedEdge.property("weight").contains(2), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("weight").contains(2), + "is not a valid string operand", ), ( - filter.ExplodedEdge.property("weight").not_contains(3), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("weight").not_contains(3), + "is not a valid string operand", ), ( - filter.ExplodedEdge.property("weight").fuzzy_search("blah", 2, False), - "Operator FUZZY_SEARCH(2,false) is only supported for strings.", + lambda: filter.ExplodedEdge.property("weight").fuzzy_search("blah", 2, False), + "string operator requires a Str property", ), # Floats (confidence) ( - filter.ExplodedEdge.property("confidence").contains(0.9), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("confidence").contains(0.9), + "is not a valid string operand", ), ( - filter.ExplodedEdge.property("confidence").not_contains(0.8), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("confidence").not_contains(0.8), + "is not a valid string operand", ), ( - filter.ExplodedEdge.property("confidence").fuzzy_search("blah", 2, False), - "Operator FUZZY_SEARCH(2,false) is only supported for strings.", + lambda: filter.ExplodedEdge.property("confidence").fuzzy_search("blah", 2, False), + "string operator requires a Str property", ), # Booleans (active) ( - filter.ExplodedEdge.property("active").contains(True), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("active").contains(True), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("active").not_contains(False), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("active").not_contains(False), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("active").fuzzy_search("blah", 2, False), - "Operator FUZZY_SEARCH(2,false) is only supported for strings.", + lambda: filter.ExplodedEdge.property("active").fuzzy_search("blah", 2, False), + "string operator requires a Str property", ), # Datetimes (created) ( - filter.ExplodedEdge.property("created").contains(datetime(2023, 1, 1)), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("created").contains(datetime(2023, 1, 1)), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("created").not_contains(datetime(2023, 1, 1)), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("created").not_contains(datetime(2023, 1, 1)), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("created").fuzzy_search("blah", 2, False), - "Operator FUZZY_SEARCH(2,false) is only supported for strings.", + lambda: filter.ExplodedEdge.property("created").fuzzy_search("blah", 2, False), + "string operator requires a Str property", ), # Lists (tags) — odd comparisons ( - filter.ExplodedEdge.property("tags").contains("team_a"), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("tags").contains("team_a"), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("tags").not_contains("team_z"), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("tags").not_contains("team_z"), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("tags").fuzzy_search("blah", 2, False), - "Operator FUZZY_SEARCH(2,false) is only supported for strings.", + lambda: filter.ExplodedEdge.property("tags").fuzzy_search("blah", 2, False), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("tags") < ["x"], - "Comparison not implemented for List", + lambda: filter.ExplodedEdge.property("tags") < ["x"], + "not valid for list properties", ), ( - filter.ExplodedEdge.property("tags") > ["a"], - "Comparison not implemented for List", + lambda: filter.ExplodedEdge.property("tags") > ["a"], + "not valid for list properties", ), ( - filter.ExplodedEdge.property("tags") <= ["team_b"], - "Comparison not implemented for List", + lambda: filter.ExplodedEdge.property("tags") <= ["team_b"], + "not valid for list properties", ), ( - filter.ExplodedEdge.property("tags") >= ["consultant"], - "Comparison not implemented for List", + lambda: filter.ExplodedEdge.property("tags") >= ["consultant"], + "not valid for list properties", ), # Dicts (meta) — contains() expects a key, but here simulates wrong context ( - filter.ExplodedEdge.property("meta").contains("role"), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("meta").contains("role"), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("meta").not_contains("salary"), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("meta").not_contains("salary"), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("meta").fuzzy_search("blah", 2, False), - "Operator FUZZY_SEARCH(2,false) is only supported for strings.", + lambda: filter.ExplodedEdge.property("meta").fuzzy_search("blah", 2, False), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("meta") + lambda: filter.ExplodedEdge.property("meta") < {"location": "SF", "level": 2, "contract": False, "role": "blah"}, - "Comparison not implemented for Map", + "not valid for map properties", ), ( - filter.ExplodedEdge.property("meta") + lambda: filter.ExplodedEdge.property("meta") < {"location": "SF", "level": 2, "role": "blah"}, - "Comparison not implemented for Map", + "not valid for map properties", ), # check subset of keys also raise the same error ( - filter.ExplodedEdge.property("meta") + lambda: filter.ExplodedEdge.property("meta") <= {"location": "SF", "level": 2, "contract": False, "role": "blah"}, - "Comparison not implemented for Map", + "not valid for map properties", ), ( - filter.ExplodedEdge.property("meta") + lambda: filter.ExplodedEdge.property("meta") > {"location": "SF", "level": 2, "contract": False, "role": "blah"}, - "Comparison not implemented for Map", + "not valid for map properties", ), ( - filter.ExplodedEdge.property("meta") + lambda: filter.ExplodedEdge.property("meta") >= {"location": "SF", "level": 2, "contract": False, "role": "blah"}, - "Comparison not implemented for Map", + "not valid for map properties", ), ] - for i, (expr, message) in enumerate(nonsense_filter_cases): + for i, (make_expr, message) in enumerate(nonsense_filter_cases): with pytest.raises(Exception) as e: - print(len(g.filter(expr).edges.explode())) + print(len(g.filter(make_expr()).edges.explode())) print(e.value) assert message in str(e.value) + # Numeric strings coerce to the property's numeric type: each string form + # matches exactly what its native-typed twin matches. + for prop, val in (("weight", 2), ("weight", 3), ("confidence", 2)): + for op in ("__eq__", "__ne__", "__lt__", "__gt__", "__le__", "__ge__"): + typed = getattr(filter.ExplodedEdge.property(prop), op)(val) + coerced = getattr(filter.ExplodedEdge.property(prop), op)(str(val)) + assert len(g.filter(coerced).edges.explode()) == len( + g.filter(typed).edges.explode() + ), prop + " " + op + " " + str(val) + wrong_types = [ # Integers (weight) ( - filter.ExplodedEdge.property("weight") == "2", - "Wrong type for property weight: expected I64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("weight") != "3", - "Wrong type for property weight: expected I64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("weight") < "3", - "Wrong type for property weight: expected I64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("weight") > "1", - "Wrong type for property weight: expected I64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("weight") <= "2", - "Wrong type for property weight: expected I64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("weight") >= "3", - "Wrong type for property weight: expected I64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("weight").contains("bo"), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("weight").contains("bo"), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("weight").not_contains("eg"), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("weight").not_contains("eg"), + "string operator requires a Str property", ), # Floats (confidence) ( - filter.ExplodedEdge.property("confidence") == "2", - "Wrong type for property confidence: expected F64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("confidence") != "3", - "Wrong type for property confidence: expected F64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("confidence") < "3", - "Wrong type for property confidence: expected F64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("confidence") > "1", - "Wrong type for property confidence: expected F64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("confidence") <= "2", - "Wrong type for property confidence: expected F64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("confidence") >= "3", - "Wrong type for property confidence: expected F64 but actual type is Str", - ), - ( - filter.ExplodedEdge.property("confidence").contains("bo"), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("confidence").contains("bo"), + "string operator requires a Str property", ), ( - filter.ExplodedEdge.property("confidence").not_contains("eg"), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("confidence").not_contains("eg"), + "string operator requires a Str property", ), # # Strings (name) ( - filter.ExplodedEdge.property("name") == 2, - "Wrong type for property name: expected Str but actual type is I64", + lambda: filter.ExplodedEdge.property("name") == 2, + "cannot be coerced to Str", ), ( - filter.ExplodedEdge.property("name") != 3, - "Wrong type for property name: expected Str but actual type is I64", + lambda: filter.ExplodedEdge.property("name") != 3, + "cannot be coerced to Str", ), ( - filter.ExplodedEdge.property("name") < 3, - "Wrong type for property name: expected Str but actual type is I64", + lambda: filter.ExplodedEdge.property("name") < 3, + "cannot be coerced to Str", ), ( - filter.ExplodedEdge.property("name") > 1, - "Wrong type for property name: expected Str but actual type is I64", + lambda: filter.ExplodedEdge.property("name") > 1, + "cannot be coerced to Str", ), ( - filter.ExplodedEdge.property("name") <= 2, - "Wrong type for property name: expected Str but actual type is I64", + lambda: filter.ExplodedEdge.property("name") <= 2, + "cannot be coerced to Str", ), ( - filter.ExplodedEdge.property("name") >= 3, - "Wrong type for property name: expected Str but actual type is I64", + lambda: filter.ExplodedEdge.property("name") >= 3, + "cannot be coerced to Str", ), ( - filter.ExplodedEdge.property("name").contains(2), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("name").contains(2), + "is not a valid string operand", ), ( - filter.ExplodedEdge.property("name").not_contains(3), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("name").not_contains(3), + "is not a valid string operand", ), # Booleans (active) ( - filter.ExplodedEdge.property("active") == 2, - "Wrong type for property active: expected Bool but actual type is I64", + lambda: filter.ExplodedEdge.property("active") == 2, + "cannot be coerced to Bool", ), ( - filter.ExplodedEdge.property("active") != 3, - "Wrong type for property active: expected Bool but actual type is I64", + lambda: filter.ExplodedEdge.property("active") != 3, + "cannot be coerced to Bool", ), ( - filter.ExplodedEdge.property("active") < 3, - "Wrong type for property active: expected Bool but actual type is I64", + lambda: filter.ExplodedEdge.property("active") < 3, + "not valid for boolean properties", ), ( - filter.ExplodedEdge.property("active") > 1, - "Wrong type for property active: expected Bool but actual type is I64", + lambda: filter.ExplodedEdge.property("active") > 1, + "not valid for boolean properties", ), ( - filter.ExplodedEdge.property("active") <= 2, - "Wrong type for property active: expected Bool but actual type is I64", + lambda: filter.ExplodedEdge.property("active") <= 2, + "not valid for boolean properties", ), ( - filter.ExplodedEdge.property("active") >= 3, - "Wrong type for property active: expected Bool but actual type is I64", + lambda: filter.ExplodedEdge.property("active") >= 3, + "not valid for boolean properties", ), ( - filter.ExplodedEdge.property("active").contains(2), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("active").contains(2), + "is not a valid string operand", ), # should fail on contains not type ( - filter.ExplodedEdge.property("active").not_contains(3), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("active").not_contains(3), + "is not a valid string operand", ), # should fail on contains not type # # Datetimes (created) ( - filter.ExplodedEdge.property("created") == 2, - "Wrong type for property created: expected NDTime but actual type is I64", + lambda: filter.ExplodedEdge.property("created") == 2, + "cannot be coerced to NDTime", ), ( - filter.ExplodedEdge.property("created") != 3, - "Wrong type for property created: expected NDTime but actual type is I64", + lambda: filter.ExplodedEdge.property("created") != 3, + "cannot be coerced to NDTime", ), ( - filter.ExplodedEdge.property("created") < 3, - "Wrong type for property created: expected NDTime but actual type is I64", + lambda: filter.ExplodedEdge.property("created") < 3, + "cannot be coerced to NDTime", ), ( - filter.ExplodedEdge.property("created") > 1, - "Wrong type for property created: expected NDTime but actual type is I64", + lambda: filter.ExplodedEdge.property("created") > 1, + "cannot be coerced to NDTime", ), ( - filter.ExplodedEdge.property("created") <= 2, - "Wrong type for property created: expected NDTime but actual type is I64", + lambda: filter.ExplodedEdge.property("created") <= 2, + "cannot be coerced to NDTime", ), ( - filter.ExplodedEdge.property("created") >= 3, - "Wrong type for property created: expected NDTime but actual type is I64", + lambda: filter.ExplodedEdge.property("created") >= 3, + "cannot be coerced to NDTime", ), ( - filter.ExplodedEdge.property("created").contains(2), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("created").contains(2), + "is not a valid string operand", ), # should fail on contains not type ( - filter.ExplodedEdge.property("created").not_contains(3), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("created").not_contains(3), + "is not a valid string operand", ), # should fail on contains not type # # Lists (tags) ( - filter.ExplodedEdge.property("tags") == 2, - "Wrong type for property tags: expected List(Str) but actual type is I64", + lambda: filter.ExplodedEdge.property("tags") == 2, + "cannot be coerced to List", ), ( - filter.ExplodedEdge.property("tags") != 3, - "Wrong type for property tags: expected List(Str) but actual type is I64", + lambda: filter.ExplodedEdge.property("tags") != 3, + "cannot be coerced to List", ), ( - filter.ExplodedEdge.property("tags") < 3, - "Wrong type for property tags: expected List(Str) but actual type is I64", + lambda: filter.ExplodedEdge.property("tags") < 3, + "not valid for list properties", ), ( - filter.ExplodedEdge.property("tags") > 1, - "Wrong type for property tags: expected List(Str) but actual type is I64", + lambda: filter.ExplodedEdge.property("tags") > 1, + "not valid for list properties", ), ( - filter.ExplodedEdge.property("tags") <= 2, - "Wrong type for property tags: expected List(Str) but actual type is I64", + lambda: filter.ExplodedEdge.property("tags") <= 2, + "not valid for list properties", ), ( - filter.ExplodedEdge.property("tags") >= 3, - "Wrong type for property tags: expected List(Str) but actual type is I64", + lambda: filter.ExplodedEdge.property("tags") >= 3, + "not valid for list properties", ), ( - filter.ExplodedEdge.property("tags").contains(2), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("tags").contains(2), + "is not a valid string operand", ), # should fail on contains not type ( - filter.ExplodedEdge.property("tags").not_contains(3), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("tags").not_contains(3), + "is not a valid string operand", ), # should fail on contains not type # # Dicts (meta) ( - filter.ExplodedEdge.property("meta") == 2, - """Wrong type for property meta: expected Map""", + lambda: filter.ExplodedEdge.property("meta") == 2, + "cannot be coerced to Map", ), ( - filter.ExplodedEdge.property("meta") != 3, - """Wrong type for property meta: expected Map""", + lambda: filter.ExplodedEdge.property("meta") != 3, + "cannot be coerced to Map", ), ( - filter.ExplodedEdge.property("meta") < 3, - """Wrong type for property meta: expected Map""", + lambda: filter.ExplodedEdge.property("meta") < 3, + "not valid for map properties", ), ( - filter.ExplodedEdge.property("meta") > 1, - """Wrong type for property meta: expected Map""", + lambda: filter.ExplodedEdge.property("meta") > 1, + "not valid for map properties", ), ( - filter.ExplodedEdge.property("meta") <= 2, - """Wrong type for property meta: expected Map""", + lambda: filter.ExplodedEdge.property("meta") <= 2, + "not valid for map properties", ), ( - filter.ExplodedEdge.property("meta") >= 3, - """Wrong type for property meta: expected Map""", + lambda: filter.ExplodedEdge.property("meta") >= 3, + "not valid for map properties", ), ( - filter.ExplodedEdge.property("meta").contains(2), - "Operator CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("meta").contains(2), + "is not a valid string operand", ), # should fail on contains not type ( - filter.ExplodedEdge.property("meta").not_contains(3), - "Operator NOT_CONTAINS is only supported for strings.", + lambda: filter.ExplodedEdge.property("meta").not_contains(3), + "is not a valid string operand", ), # should fail on contains not type ] - for i, (expr, message) in enumerate(wrong_types): + for i, (make_expr, message) in enumerate(wrong_types): with pytest.raises(Exception) as e: - print(len(g.filter(expr).edges.explode())) + print(len(g.filter(make_expr()).edges.explode())) print(e.value) assert message in str(e.value) with pytest.raises(Exception) as e: filter.ExplodedEdge.property("name").fuzzy_search(2, 2, False) - assert "'int' object is not an instance of 'str'" in str(e.value) + assert "is not a valid string operand" in str(e.value) missing_prop = [ - (filter.ExplodedEdge.property("blah") == 2), - (filter.ExplodedEdge.property("blah") != 3), - (filter.ExplodedEdge.property("blah") < 3), - (filter.ExplodedEdge.property("blah") > 1), - (filter.ExplodedEdge.property("blah") <= 2), - (filter.ExplodedEdge.property("blah") >= 3), - (filter.ExplodedEdge.property("blah").is_in([1, 2])), - (filter.ExplodedEdge.property("blah").is_not_in([3])), - (filter.ExplodedEdge.property("blah").contains(["blah"])), - (filter.ExplodedEdge.property("blah").contains([])), - (filter.ExplodedEdge.property("blah").not_contains([])), - (filter.ExplodedEdge.property("blah").not_contains(["blah"])), - (filter.ExplodedEdge.property("blah").is_some()), - (filter.ExplodedEdge.property("blah").is_none()), + (lambda: filter.ExplodedEdge.property("blah") == 2), + (lambda: filter.ExplodedEdge.property("blah") != 3), + (lambda: filter.ExplodedEdge.property("blah") < 3), + (lambda: filter.ExplodedEdge.property("blah") > 1), + (lambda: filter.ExplodedEdge.property("blah") <= 2), + (lambda: filter.ExplodedEdge.property("blah") >= 3), + (lambda: filter.ExplodedEdge.property("blah").is_in([1, 2])), + (lambda: filter.ExplodedEdge.property("blah").is_not_in([3])), + (lambda: filter.ExplodedEdge.property("blah").contains(["blah"])), + (lambda: filter.ExplodedEdge.property("blah").contains([])), + (lambda: filter.ExplodedEdge.property("blah").not_contains([])), + (lambda: filter.ExplodedEdge.property("blah").not_contains(["blah"])), + (lambda: filter.ExplodedEdge.property("blah").is_some()), + (lambda: filter.ExplodedEdge.property("blah").is_none()), ] - for expr in missing_prop: + for make_expr in missing_prop: with pytest.raises(Exception) as e: # force evaluation so the exception surfaces here - _ = g.filter(expr).edges.explode() - assert "Property blah does not exist" in str(e.value) + _ = g.filter(make_expr()).edges.explode() + # mistyped operands may fail at construction before the property + # lookup happens + assert "Property blah does not exist" in str( + e.value + ) or "is not a valid string operand" in str(e.value) @pytest.mark.parametrize("GraphClass", [Graph, PersistentGraph]) diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index 69ab224440..44de84533c 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -342,94 +342,96 @@ def test_degree_filter_is_not_in(value): def test_degree_filter_with_invalid_expressions(): graph = degree_graph_with_add_node_and_add_edge(Graph()) invalid_filters = [ - filter.Node.degree().is_none(), - filter.Node.degree().is_some(), - filter.Node.degree().starts_with("1"), - filter.Node.degree().ends_with("1"), - filter.Node.degree().contains("1"), - filter.Node.degree().not_contains("1"), - filter.Node.degree().fuzzy_search("1", 1, False), - filter.Node.in_degree().is_none(), - filter.Node.in_degree().is_some(), - filter.Node.in_degree().starts_with("1"), - filter.Node.in_degree().ends_with("1"), - filter.Node.in_degree().contains("1"), - filter.Node.in_degree().not_contains("1"), - filter.Node.in_degree().fuzzy_search("1", 1, False), - filter.Node.out_degree().is_none(), - filter.Node.out_degree().is_some(), - filter.Node.out_degree().starts_with("1"), - filter.Node.out_degree().ends_with("1"), - filter.Node.out_degree().contains("1"), - filter.Node.out_degree().not_contains("1"), - filter.Node.out_degree().fuzzy_search("1", 1, False), - filter.Node.degree().any() == 1, - filter.Node.degree().all() == 1, - filter.Node.degree().len() > 0, - filter.Node.degree().sum() == 1, - filter.Node.degree().avg() == 1, - filter.Node.degree().min() == 1, - filter.Node.degree().max() == 1, - filter.Node.degree().first() == 1, - filter.Node.degree().last() == 1, - filter.Node.in_degree().any() == 1, - filter.Node.in_degree().all() == 1, - filter.Node.in_degree().len() > 0, - filter.Node.in_degree().sum() == 1, - filter.Node.in_degree().avg() == 1, - filter.Node.in_degree().min() == 1, - filter.Node.in_degree().max() == 1, - filter.Node.in_degree().first() == 1, - filter.Node.in_degree().last() == 1, - filter.Node.out_degree().any() == 1, - filter.Node.out_degree().all() == 1, - filter.Node.out_degree().len() > 0, - filter.Node.out_degree().sum() == 1, - filter.Node.out_degree().avg() == 1, - filter.Node.out_degree().min() == 1, - filter.Node.out_degree().max() == 1, - filter.Node.out_degree().first() == 1, - filter.Node.out_degree().last() == 1, + lambda: filter.Node.degree().is_none(), + lambda: filter.Node.degree().is_some(), + lambda: filter.Node.degree().starts_with("1"), + lambda: filter.Node.degree().ends_with("1"), + lambda: filter.Node.degree().contains("1"), + lambda: filter.Node.degree().not_contains("1"), + lambda: filter.Node.degree().fuzzy_search("1", 1, False), + lambda: filter.Node.in_degree().is_none(), + lambda: filter.Node.in_degree().is_some(), + lambda: filter.Node.in_degree().starts_with("1"), + lambda: filter.Node.in_degree().ends_with("1"), + lambda: filter.Node.in_degree().contains("1"), + lambda: filter.Node.in_degree().not_contains("1"), + lambda: filter.Node.in_degree().fuzzy_search("1", 1, False), + lambda: filter.Node.out_degree().is_none(), + lambda: filter.Node.out_degree().is_some(), + lambda: filter.Node.out_degree().starts_with("1"), + lambda: filter.Node.out_degree().ends_with("1"), + lambda: filter.Node.out_degree().contains("1"), + lambda: filter.Node.out_degree().not_contains("1"), + lambda: filter.Node.out_degree().fuzzy_search("1", 1, False), + lambda: filter.Node.degree().any() == 1, + lambda: filter.Node.degree().all() == 1, + lambda: filter.Node.degree().len() > 0, + lambda: filter.Node.degree().sum() == 1, + lambda: filter.Node.degree().avg() == 1, + lambda: filter.Node.degree().min() == 1, + lambda: filter.Node.degree().max() == 1, + lambda: filter.Node.degree().first() == 1, + lambda: filter.Node.degree().last() == 1, + lambda: filter.Node.in_degree().any() == 1, + lambda: filter.Node.in_degree().all() == 1, + lambda: filter.Node.in_degree().len() > 0, + lambda: filter.Node.in_degree().sum() == 1, + lambda: filter.Node.in_degree().avg() == 1, + lambda: filter.Node.in_degree().min() == 1, + lambda: filter.Node.in_degree().max() == 1, + lambda: filter.Node.in_degree().first() == 1, + lambda: filter.Node.in_degree().last() == 1, + lambda: filter.Node.out_degree().any() == 1, + lambda: filter.Node.out_degree().all() == 1, + lambda: filter.Node.out_degree().len() > 0, + lambda: filter.Node.out_degree().sum() == 1, + lambda: filter.Node.out_degree().avg() == 1, + lambda: filter.Node.out_degree().min() == 1, + lambda: filter.Node.out_degree().max() == 1, + lambda: filter.Node.out_degree().first() == 1, + lambda: filter.Node.out_degree().last() == 1, ] - for filter_expr in invalid_filters: - with pytest.raises(Exception, match=r"Invalid filter"): - graph.filter(filter_expr).nodes.id + for make_filter in invalid_filters: + with pytest.raises(Exception, match=r"Invalid filter|not comparable"): + graph.filter(make_filter()).nodes.id @pytest.mark.parametrize("value_a, value_b", [("a", "b"), ("foo", "bar")]) def test_degree_filter_with_invalid_string_values(value_a, value_b): graph = degree_graph_with_add_node_and_add_edge(Graph()) invalid_filters = [ - filter.Node.degree() < value_a, - filter.Node.degree() <= value_a, - filter.Node.degree() == value_a, - filter.Node.degree() != value_a, - filter.Node.degree() >= value_a, - filter.Node.degree() > value_a, - filter.Node.in_degree() < value_a, - filter.Node.in_degree() <= value_a, - filter.Node.in_degree() == value_a, - filter.Node.in_degree() != value_a, - filter.Node.in_degree() >= value_a, - filter.Node.in_degree() > value_a, - filter.Node.out_degree() < value_a, - filter.Node.out_degree() <= value_a, - filter.Node.out_degree() == value_a, - filter.Node.out_degree() != value_a, - filter.Node.out_degree() >= value_a, - filter.Node.out_degree() > value_a, - filter.Node.degree().is_in([value_a, value_b]), - filter.Node.degree().is_not_in([value_a, value_b]), - filter.Node.in_degree().is_in([value_a, value_b]), - filter.Node.in_degree().is_not_in([value_a, value_b]), - filter.Node.out_degree().is_in([value_a, value_b]), - filter.Node.out_degree().is_not_in([value_a, value_b]), + lambda: filter.Node.degree() < value_a, + lambda: filter.Node.degree() <= value_a, + lambda: filter.Node.degree() == value_a, + lambda: filter.Node.degree() != value_a, + lambda: filter.Node.degree() >= value_a, + lambda: filter.Node.degree() > value_a, + lambda: filter.Node.in_degree() < value_a, + lambda: filter.Node.in_degree() <= value_a, + lambda: filter.Node.in_degree() == value_a, + lambda: filter.Node.in_degree() != value_a, + lambda: filter.Node.in_degree() >= value_a, + lambda: filter.Node.in_degree() > value_a, + lambda: filter.Node.out_degree() < value_a, + lambda: filter.Node.out_degree() <= value_a, + lambda: filter.Node.out_degree() == value_a, + lambda: filter.Node.out_degree() != value_a, + lambda: filter.Node.out_degree() >= value_a, + lambda: filter.Node.out_degree() > value_a, + lambda: filter.Node.degree().is_in([value_a, value_b]), + lambda: filter.Node.degree().is_not_in([value_a, value_b]), + lambda: filter.Node.in_degree().is_in([value_a, value_b]), + lambda: filter.Node.in_degree().is_not_in([value_a, value_b]), + lambda: filter.Node.out_degree().is_in([value_a, value_b]), + lambda: filter.Node.out_degree().is_not_in([value_a, value_b]), ] - for filter_expr in invalid_filters: - with pytest.raises(Exception, match=r"Invalid filter"): - graph.filter(filter_expr).nodes.id + for make_filter in invalid_filters: + # Mistyped constants fail at the comparison when the expression type is + # statically known, and at filter() otherwise. + with pytest.raises(Exception, match=r"Invalid filter|not comparable"): + graph.filter(make_filter()).nodes.id @pytest.mark.parametrize("value", range(0, 15)) @@ -1120,7 +1122,7 @@ def check(graph): filter_expr = filter.Node.id() == 3 with pytest.raises( Exception, - match='Invalid filter: Filter value type does not match node ID type. Expected Str but got "U64"', + match=r"Invalid filter: value I64\(3\) of type I64 cannot be coerced to Str", ): graph.filter(filter_expr).nodes.id @@ -1130,12 +1132,10 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_error(): def check(graph): + # A numeric string coerces to the numeric id type, consistent with the + # castable-constant rule used across the expression comparisons. filter_expr = filter.Node.id() == "3" - with pytest.raises( - Exception, - match='Invalid filter: Filter value type does not match node ID type. Expected U64 but got "Str"', - ): - graph.filter(filter_expr).nodes.id + assert sorted(graph.filter(filter_expr).nodes.id) == [3] return check @@ -1290,8 +1290,14 @@ def test_filter_nodes_by_column(): @with_variants(init_graph) def test_filter_nodes_for_node_name_all_is_invalid(): def check(graph): - with pytest.raises(AttributeError, match=r"has no attribute 'all'"): - filter.Node.name().all() + # The expression builds (the python surface is one Expr type); applying + # it rejects the qualifier on a scalar field. + filter_expr = filter.Node.name().all() == True + with pytest.raises( + Exception, + match=r"any\(\)/all\(\) require list or temporal values, found Str", + ): + graph.filter(filter_expr).nodes.id return check @@ -1299,7 +1305,11 @@ def check(graph): @with_variants(init_graph) def test_filter_nodes_for_node_name_len_is_invalid(): def check(graph): - with pytest.raises(AttributeError, match=r"has no attribute 'len'"): - filter.Node.name().len() + filter_expr = filter.Node.name().len() == 1 + with pytest.raises( + Exception, + match=r"len\(\) is not valid on a scalar expression of type Str", + ): + graph.filter(filter_expr).nodes.id return check diff --git a/python/tests/test_base_install/test_filters/test_node_property_filter.py b/python/tests/test_base_install/test_filters/test_node_property_filter.py index b2510b4c3c..6321d3fb29 100644 --- a/python/tests/test_base_install/test_filters/test_node_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_property_filter.py @@ -174,7 +174,7 @@ def check(graph): filter_expr = filter.Node.property("p20").temporal().all().starts_with("Gold") result_ids = sorted(graph.filter(filter_expr).nodes.id) - expected_ids = ["3", "4"] + expected_ids = ["1", "3", "4"] assert result_ids == expected_ids return check @@ -210,7 +210,7 @@ def check(graph): filter_expr = filter.Node.property("p20").temporal().all().ends_with("ship") result_ids = sorted(graph.filter(filter_expr).nodes.id) - expected_ids = ["2"] + expected_ids = ["1", "2"] assert result_ids == expected_ids filter_expr = filter.Node.metadata("p10").ends_with("ane") @@ -787,10 +787,15 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_with_with_qualifier_on_non_string(): def check(graph): + # A numeric string coerces to the element type under the + # castable-constant rule; a non-numeric string is rejected. filter_expr = filter.Node.property("prop8").any() == "3" + assert sorted(graph.filter(filter_expr).nodes.id) == ["a", "d"] + + filter_expr = filter.Node.property("prop8").any() == "pometry" with pytest.raises( Exception, - match=r"Wrong type for property prop8: expected I64 but actual type is Str", + match=r"of type Str cannot be coerced to I64", ): graph.filter(filter_expr).nodes.id @@ -800,12 +805,10 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_with_with_qualifier_alongside_illegal_operators(): def check(graph): + # Elementwise presence with a qualifier collapse: list elements are + # always present, so this matches every node carrying the property. filter_expr = filter.Node.property("prop8").any().is_some() - with pytest.raises( - Exception, - match=r"Invalid filter: Operator IS_SOME/IS_NONE is not supported with element qualifiers; apply it to the list itself \(without elem qualifiers\).", - ): - graph.filter(filter_expr).nodes.id + assert sorted(graph.filter(filter_expr).nodes.id) == ["a", "d"] return check @@ -816,14 +819,14 @@ def check(graph): filter_expr = filter.Node.property("prop8").all().len() > 0 with pytest.raises( Exception, - match=r"List aggregation len cannot be used after an element qualifier \(any/all\)", + match=r"len\(\) is not valid on a scalar expression", ): graph.filter(filter_expr).nodes.id filter_expr = filter.Node.property("prop8").sum().any() > 0 with pytest.raises( Exception, - match=r"Element qualifiers \(any/all\) cannot be used after a list aggregation \(len/sum/avg/min/max\).", + match=r"any\(\)/all\(\) require list or temporal values", ): graph.filter(filter_expr).nodes.id @@ -1138,18 +1141,18 @@ def check(graph): def test_filter_nodes_for_temporal_property_fails(): def check(graph): filter_expr = filter.Node.property("prop1").temporal() == 60 - msg = "Wrong type for property prop1: expected List(I64) but actual type is I64" + msg = r"value I64\(60\) of type I64 cannot be coerced to List" with pytest.raises( Exception, - match=re.escape(msg), + match=msg, ): graph.filter(filter_expr).nodes.id filter_expr = filter.Node.property("prop1").temporal() == "pometry" - msg = "Wrong type for property prop1: expected List(I64) but actual type is Str" + msg = r"of type Str cannot be coerced to List" with pytest.raises( Exception, - match=re.escape(msg), + match=msg, ): graph.filter(filter_expr).nodes.id @@ -1186,7 +1189,10 @@ def check(graph): def test_filter_nodes_window_out_of_range_is_empty(): def check(graph): expr = filter.Node.window(10, 20).property("prop5").temporal().sum() >= 0 - assert list(graph.filter(expr).nodes.id) == [] + # Per-snapshot sums form a list; comparing it to a scalar is a type + # error rather than a silent no-match. + with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + graph.filter(expr).nodes.id return check @@ -1240,7 +1246,10 @@ def check(graph): def test_filter_nodes_after(): def check(graph): expr = filter.Node.after(1).property("prop5").temporal().sum() >= 0 - assert list(graph.filter(expr).nodes.id) == [] + # Per-snapshot sums form a list; comparing it to a scalar is a type + # error rather than a silent no-match. + with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + graph.filter(expr).nodes.id expr = filter.Node.after(1).property("prop6").temporal().last().sum() == 12 assert sorted(graph.filter(expr).nodes.id) == ["a"] @@ -1255,7 +1264,10 @@ def check(graph): assert sorted(graph.filter(expr).nodes.id) == ["a"] expr = filter.Node.latest().property("prop5").temporal().sum() >= 0 - assert list(graph.filter(expr).nodes.id) == [] + # Per-snapshot sums form a list; comparing it to a scalar is a type + # error rather than a silent no-match. + with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + graph.filter(expr).nodes.id return check @@ -1272,7 +1284,10 @@ def check(graph): assert sorted(graph.filter(expr).nodes.id) == ["c"] expr = filter.Node.snapshot_at(1).property("prop6").temporal().sum() >= 0 - assert list(graph.filter(expr).nodes.id) == [] + # Per-snapshot sums form a list; comparing it to a scalar is a type + # error rather than a silent no-match. + with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + graph.filter(expr).nodes.id return check @@ -1287,7 +1302,10 @@ def check(graph): assert sorted(graph.filter(expr).nodes.id) == ["a"] expr = filter.Node.snapshot_latest().property("prop5").temporal().sum() >= 0 - assert list(graph.filter(expr).nodes.id) == [] + # Per-snapshot sums form a list; comparing it to a scalar is a type + # error rather than a silent no-match. + with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + graph.filter(expr).nodes.id return check diff --git a/raphtory-tests/tests/qp3.rs b/raphtory-tests/tests/qp3.rs new file mode 100644 index 0000000000..d13cd9e21c --- /dev/null +++ b/raphtory-tests/tests/qp3.rs @@ -0,0 +1,30 @@ +use raphtory::{ + db::{ + api::view::Filter, + graph::views::filter::model::{node_expr::DynCreateOp, DynCreateFilter, EdgeViewFilterOps}, + }, + prelude::*, +}; +use std::sync::Arc; + +#[test] +fn edge_endpoint_id_validation() { + let g = Graph::new(); + g.add_edge(0, "a", "b", NO_PROPS, None).unwrap(); + + // typed + let f = EntityExprFilterOps::eq(EdgeFilter::src().id(), Prop::I64(3)); + match g.filter(f) { + Ok(_) => println!("typed: NO RAISE"), + Err(e) => println!("typed: raised {e}"), + } + + // dyn (python path) + let id_expr: Arc = Arc::new(EdgeFilter::src().id()); + let cmp = EntityExprFilterOps::eq(id_expr, Prop::I64(3)); + let dynf: Arc = Arc::new(cmp); + match g.filter(dynf) { + Ok(_) => println!("dyn: NO RAISE"), + Err(e) => println!("dyn: raised {e}"), + } +} diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 58d5cd7ace..077ed6ea68 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -184,6 +184,10 @@ impl NodeOp for Map { self.op.domain(storage) } + fn prop_type(&self) -> PropType { + self.op.prop_type() + } + fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { (self.map)(self.op.apply(storage, node)) } diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index c1a2e07f75..deb3b76097 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -6,7 +6,7 @@ use super::{ ops::{ BinaryCmpEdgeOp, ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, - PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, + ListAwareUnaryEdgeOp, PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, }, EdgeOp, }; @@ -18,6 +18,7 @@ use crate::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, model::{ + coerce_set_values, edge_filter::EdgeFilter, elem_prop_type, filter_operator::{BinaryOp, ElemQual}, @@ -33,7 +34,10 @@ use crate::{ }, errors::GraphError, }; -use raphtory_api::core::entities::{edges::edge_ref::EdgeRef, properties::prop::Prop}; +use raphtory_api::core::entities::{ + edges::edge_ref::EdgeRef, + properties::prop::{Prop, PropType}, +}; use raphtory_storage::graph::graph::GraphStorage; use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── @@ -62,7 +66,9 @@ pub(crate) fn qualify_edge_filter<'g>( quals: &[ElemQual], ) -> Arc + 'g> { let mut op = elemwise; - for q in quals { + // Qualifiers are collected in call order (outermost list level first); + // wrapping starts at the innermost level, so iterate in reverse. + for q in quals.iter().rev() { op = match q { ElemQual::Any => Arc::new(AnyEdgeOp { inner: op }), ElemQual::All => Arc::new(AllEdgeOp { inner: op }), @@ -95,7 +101,7 @@ where let expr_pt = self.left.prop_type(); let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; let right = self.right.create_edge_op(filtered.clone())?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len()); + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len())?; let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; match right.const_value() { @@ -161,7 +167,7 @@ where let expr_pt = self.left.prop_type(); let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; let right = self.right.create_edge_op(filtered.clone())?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len()); + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len())?; let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; match right.const_value() { @@ -226,9 +232,15 @@ where graph: G, filtered: F, ) -> Result, GraphError> { - let inner = self.expr.create_edge_op(filtered.clone())?; - let op: Arc + 'graph> = - Arc::new(UnaryEdgeOp { inner, op: self.op }); + let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; + let op: Arc + 'graph> = if quals.is_empty() { + Arc::new(UnaryEdgeOp { inner, op: self.op }) + } else { + qualify_edge_filter( + Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op }), + &quals, + ) + }; Ok(EdgeExprFilteredGraph::new(graph, op)) } @@ -267,9 +279,15 @@ where graph: G, filtered: F, ) -> Result, GraphError> { - let inner = self.expr.create_edge_op(filtered.clone())?; - let op: Arc + 'graph> = - Arc::new(UnaryEdgeOp { inner, op: self.op }); + let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; + let op: Arc + 'graph> = if quals.is_empty() { + Arc::new(UnaryEdgeOp { inner, op: self.op }) + } else { + qualify_edge_filter( + Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op }), + &quals, + ) + }; Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) } @@ -315,7 +333,11 @@ where ) -> Result, GraphError> { let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; let right = self.right.create_edge_op(filtered.clone())?; - validate_string_op(&elem_prop_type(&left.prop_type(), quals.len()))?; + validate_string_op(&elem_prop_type(&left.prop_type(), quals.len())?)?; + match right.const_value() { + Some(c) => validate_const_castable(&PropType::Str, c.as_ref())?, + None => {} + } let op: Arc + 'graph> = if quals.is_empty() { Arc::new(StringEdgeOp { left, @@ -373,7 +395,11 @@ where ) -> Result, GraphError> { let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; let right = self.right.create_edge_op(filtered.clone())?; - validate_string_op(&elem_prop_type(&left.prop_type(), quals.len()))?; + validate_string_op(&elem_prop_type(&left.prop_type(), quals.len())?)?; + match right.const_value() { + Some(c) => validate_const_castable(&PropType::Str, c.as_ref())?, + None => {} + } let op: Arc + 'graph> = if quals.is_empty() { Arc::new(StringEdgeOp { left, @@ -429,18 +455,21 @@ impl CreateFilter for PropValueSetExpr { graph: G, filtered: F, ) -> Result, GraphError> { + let expr_pt = self.expr.prop_type(); let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; + let values = coerce_set_values(&lhs_pt, self.values)?; let op: Arc + 'graph> = if quals.is_empty() { Arc::new(PropValueSetEdgeOp { inner, - values: self.values, + values, op: self.op, }) } else { qualify_edge_filter( Arc::new(ListAwareSetEdgeOp { inner, - values: self.values, + values, op: self.op, }), &quals, @@ -481,18 +510,21 @@ impl CreateFilter for PropValueSetExpr { graph: G, filtered: F, ) -> Result, GraphError> { + let expr_pt = self.expr.prop_type(); let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; + let values = coerce_set_values(&lhs_pt, self.values)?; let op: Arc + 'graph> = if quals.is_empty() { Arc::new(PropValueSetEdgeOp { inner, - values: self.values, + values, op: self.op, }) } else { qualify_edge_filter( Arc::new(ListAwareSetEdgeOp { inner, - values: self.values, + values, op: self.op, }), &quals, diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index aa5ea29f48..25d952147d 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -45,6 +45,10 @@ impl<'a, V: Clone + Send + Sync> EdgeOp for Arc + 'a> { self.as_ref().apply(storage, edge) } + fn prop_type(&self) -> PropType { + self.as_ref().prop_type() + } + fn const_value(&self) -> Option { self.as_ref().const_value() } @@ -109,13 +113,9 @@ impl EdgeOp for EdgeMetaOp { EdgeView::new(&self.graph, edge).get_metadata(self.prop_id) } - fn prop_type(&self) -> PropType { - self.graph - .edge_meta() - .metadata_mapper() - .get_dtype(self.prop_id) - .unwrap_or_default() - } + // No declared type: the runtime shape depends on the edge's layers (a + // multi-layer edge yields a map keyed by layer, a single-layer edge the + // plain value), so comparisons defer to runtime coercion. } // ───────────────────────────────────────────────────────────────────────────── @@ -131,6 +131,14 @@ pub(crate) struct TemporalEdgePropOp { impl EdgeOp for TemporalEdgePropOp { type Output = Option; + fn prop_type(&self) -> PropType { + self.graph + .edge_meta() + .temporal_prop_mapper() + .get_dtype(self.prop_id) + .map_or(PropType::Empty, |dt| PropType::List(Box::new(dt))) + } + fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { let vals: Vec = EdgeView::new(&self.graph, edge) .temporal_iter(self.prop_id) @@ -366,6 +374,14 @@ pub(crate) struct EdgeEndpointNodeOp<'g> { impl<'g> EdgeOp for EdgeEndpointNodeOp<'g> { type Output = Option; + fn prop_type(&self) -> PropType { + self.node_op.prop_type() + } + + fn const_value(&self) -> Option { + self.node_op.const_value() + } + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { let vid = match self.endpoint { Endpoint::Src => edge.src(), diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index a2b3e6c2e5..a651896b31 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -1179,17 +1179,28 @@ impl EntityExprBuilder for SnapshotLatest {} /// Reject ordering operators on boolean properties. //. TODO: Also check if both the types are comparable. pub 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 - ))); + if matches!( + op, + BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge + ) { + if *prop_type == PropType::Bool { + return Err(GraphError::InvalidFilter(format!( + "operator {:?} is not valid for boolean properties", + op + ))); + } + if matches!(prop_type, PropType::Map(_)) { + return Err(GraphError::InvalidFilter(format!( + "operator {:?} is not valid for map properties", + op + ))); + } + if matches!(prop_type, PropType::List(_)) { + return Err(GraphError::InvalidFilter(format!( + "operator {:?} is not valid for list properties", + op + ))); + } } Ok(()) } @@ -1234,6 +1245,21 @@ pub fn validate_const_castable( return Ok(()); } if let Some(rhs) = rhs_const { + // Map values carry partial schemas against a union-schema declared + // type and compare structurally at runtime; a non-map constant can + // never match a map property. + if matches!(lhs_pt, PropType::Map(_)) { + return if matches!(rhs, Prop::Map(_)) { + Ok(()) + } else { + Err(GraphError::InvalidFilter(format!( + "value {:?} of type {} cannot be coerced to {}", + rhs, + rhs.dtype(), + lhs_pt + ))) + }; + } if rhs.dtype() != *lhs_pt && rhs.clone().try_cast(lhs_pt.clone()).is_err() { return Err(GraphError::InvalidFilter(format!( "value {:?} of type {} cannot be coerced to {}", @@ -1303,16 +1329,22 @@ pub fn validate_types_compatible(lhs_pt: &PropType, rhs_pt: &PropType) -> Result /// mismatches there. Anything declaring a scalar type up front (e.g. /// `IsActiveNode` → `Bool`, `DegreeExpr` → `U64`) is rejected. /// The element type a leading `any()`/`all()` chain compares against: one -/// list level is stripped per qualifier. Unknown types stay unknown. -pub fn elem_prop_type(pt: &PropType, levels: usize) -> PropType { +/// list level is stripped per qualifier. Unknown types stay unknown; a +/// qualifier over a known scalar is an error. +pub fn elem_prop_type(pt: &PropType, levels: usize) -> Result { let mut pt = pt.clone(); for _ in 0..levels { pt = match pt { PropType::List(inner) => *inner, - other => other, + PropType::Empty => PropType::Empty, + other => { + return Err(GraphError::InvalidFilter(format!( + "any()/all() require list or temporal values, found {other}" + ))) + } }; } - pt + Ok(pt) } pub fn require_aggregable(pt: &PropType, op: &str) -> Result<(), GraphError> { @@ -1335,6 +1367,12 @@ pub fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result( &self, - _graph: G, + graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Id.map(|a| Some(a.into_prop())))) + let pt = match graph.id_type() { + Some(GidType::Str) => PropType::Str, + Some(GidType::U64) => PropType::U64, + None => PropType::Empty, + }; + Ok(Arc::new(WithPropType { + inner: Id.map(|a| Some(a.into_prop())), + pt, + })) } } @@ -168,7 +177,10 @@ impl CreateOp for Name { &self, _graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(Name.map(|a| Some(a.into_prop())))) + Ok(Arc::new(WithPropType { + inner: Name.map(|a| Some(a.into_prop())), + pt: PropType::Str, + })) } } @@ -193,9 +205,10 @@ impl CreateOp for Type { ) -> Result> + 'g>, GraphError> { // Untyped nodes carry the storage's default type key, matching how the // composite path builds its type mask over the node-type meta keys. - Ok(Arc::new(Type.map(|a| { - Some(a.map_or_else(|| Prop::str("_default"), |b| b.into_prop())) - }))) + Ok(Arc::new(WithPropType { + inner: Type.map(|a| Some(a.map_or_else(|| Prop::str("_default"), |b| b.into_prop()))), + pt: PropType::Str, + })) } } @@ -466,13 +479,14 @@ impl CreateOp for DegreeExpr { &self, graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new( - Degree { + Ok(Arc::new(WithPropType { + inner: Degree { dir: self.dir, view: self.view_expr.create_view(graph)?, } .map(|a| Some(Prop::U64(a as u64))), - )) + pt: PropType::U64, + })) } } @@ -591,11 +605,11 @@ pub trait EntityAggOps: EntityExpr + Sized { } macro_rules! impl_agg_expr { - ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident, $qual:expr) => { + ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident, $name:literal, $qual:expr) => { impl_agg_expr!(@common $expr, $node_op_ty, $edge_op_ty); impl CreateOp for $expr { - impl_agg_expr!(@create $node_op_ty, $edge_op_ty); + impl_agg_expr!(@create $node_op_ty, $edge_op_ty, $name); fn create_qualified_node_op<'g, G: GraphView + 'g>( &self, @@ -618,11 +632,11 @@ macro_rules! impl_agg_expr { } } }; - ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident) => { + ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident, $name:literal) => { impl_agg_expr!(@common $expr, $node_op_ty, $edge_op_ty); impl CreateOp for $expr { - impl_agg_expr!(@create $node_op_ty, $edge_op_ty); + impl_agg_expr!(@create $node_op_ty, $edge_op_ty, $name); // Leading qualifiers float through aggregates: the aggregate // applies per element (aggregate_list_values recurses into @@ -634,7 +648,7 @@ macro_rules! impl_agg_expr { { let (inner, quals) = self.0.create_qualified_node_op(graph)?; let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&pt, stringify!($node_op_ty))?; + require_aggregable(&elem_prop_type(&pt, quals.len())?, $name)?; Ok((Arc::new($node_op_ty { inner }), quals)) } @@ -645,7 +659,7 @@ macro_rules! impl_agg_expr { { let (inner, quals) = self.0.create_qualified_edge_op(graph)?; let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&pt, stringify!($edge_op_ty))?; + require_aggregable(&elem_prop_type(&pt, quals.len())?, $name)?; Ok((Arc::new($edge_op_ty { inner }), quals)) } } @@ -688,14 +702,14 @@ macro_rules! impl_agg_expr { } }; - (@create $node_op_ty:ident, $edge_op_ty:ident) => { + (@create $node_op_ty:ident, $edge_op_ty:ident, $name:literal) => { fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, ) -> Result> + 'g>, GraphError> { let inner = self.0.create_node_op(graph)?; let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&pt, stringify!($node_op_ty))?; + require_aggregable(&pt, $name)?; Ok(Arc::new($node_op_ty { inner })) } @@ -705,18 +719,18 @@ macro_rules! impl_agg_expr { ) -> Result> + 'g>, GraphError> { let inner = self.0.create_edge_op(graph)?; let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&pt, stringify!($edge_op_ty))?; + require_aggregable(&pt, $name)?; Ok(Arc::new($edge_op_ty { inner })) } }; } -impl_agg_expr!(SumExpr, SumNodeOp, SumEdgeOp); -impl_agg_expr!(AvgExpr, AvgNodeOp, AvgEdgeOp); -impl_agg_expr!(MinExpr, MinNodeOp, MinEdgeOp); -impl_agg_expr!(MaxExpr, MaxNodeOp, MaxEdgeOp); -impl_agg_expr!(FirstExpr, FirstNodeOp, FirstEdgeOp); -impl_agg_expr!(LastExpr, LastNodeOp, LastEdgeOp); -impl_agg_expr!(LenExpr, LenNodeOp, LenEdgeOp); -impl_agg_expr!(AnyExpr, AnyNodeOp, AnyEdgeOp, ElemQual::Any); -impl_agg_expr!(AllExpr, AllNodeOp, AllEdgeOp, ElemQual::All); +impl_agg_expr!(SumExpr, SumNodeOp, SumEdgeOp, "sum()"); +impl_agg_expr!(AvgExpr, AvgNodeOp, AvgEdgeOp, "avg()"); +impl_agg_expr!(MinExpr, MinNodeOp, MinEdgeOp, "min()"); +impl_agg_expr!(MaxExpr, MaxNodeOp, MaxEdgeOp, "max()"); +impl_agg_expr!(FirstExpr, FirstNodeOp, FirstEdgeOp, "first()"); +impl_agg_expr!(LastExpr, LastNodeOp, LastEdgeOp, "last()"); +impl_agg_expr!(LenExpr, LenNodeOp, LenEdgeOp, "len()"); +impl_agg_expr!(AnyExpr, AnyNodeOp, AnyEdgeOp, "any()", ElemQual::Any); +impl_agg_expr!(AllExpr, AllNodeOp, AllEdgeOp, "all()", ElemQual::All); 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 ed49a7450b..fd1dab1c33 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 @@ -113,7 +113,9 @@ fn qualify_node_filter<'g>( quals: &[ElemQual], ) -> Arc + 'g> { let mut op = elemwise; - for q in quals { + // Qualifiers are collected in call order (outermost list level first); + // wrapping starts at the innermost level, so iterate in reverse. + for q in quals.iter().rev() { op = match q { ElemQual::Any => Arc::new(AnyNodeOp { inner: op }), ElemQual::All => Arc::new(AllNodeOp { inner: op }), @@ -219,7 +221,7 @@ where let expr_pt = self.left.prop_type(); let (left, quals) = self.left.create_qualified_node_op(filtered.clone())?; let right = self.right.create_node_op(filtered)?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len()); + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len())?; let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; match right.const_value() { @@ -378,10 +380,10 @@ where E: CreateOp, { type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph>; + NodeFilteredGraph>; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - UnaryNodeOp<'graph, Prop>; + Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -409,8 +411,13 @@ where self.op ))); } - let inner = self.expr.create_node_op(filtered)?; - Ok(UnaryNodeOp { inner, op: self.op }) + let (inner, quals) = self.expr.create_qualified_node_op(filtered)?; + if quals.is_empty() { + Ok(Arc::new(UnaryNodeOp { inner, op: self.op })) + } else { + let elemwise = Arc::new(ListAwareUnaryNodeOp { inner, op: self.op }); + Ok(qualify_node_filter(elemwise, &quals)) + } } fn filter_graph_view<'graph, G: GraphView + 'graph>( @@ -429,7 +436,7 @@ where Arc; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - UnaryNodeOp<'graph, Prop>; + Arc + 'graph>; type FilteredGraph<'graph, G> = G @@ -605,7 +612,11 @@ impl CreateFilter for StringExpr { validate_string_op(&elem_prop_type( &resolved_prop_type(expr_pt, left.prop_type()), quals.len(), - ))?; + )?)?; + match right.const_value() { + Some(c) => validate_const_castable(&PropType::Str, c.as_ref())?, + None => {} + } if quals.is_empty() { Ok(Arc::new(StringNodeOp { left, @@ -783,7 +794,7 @@ impl CreateFilter for PropValueSetExpr { ) -> Result, GraphError> { let expr_pt = self.expr.prop_type(); let (inner, quals) = self.expr.create_qualified_node_op(filtered)?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len()); + let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; let values = coerce_set_values(&lhs_pt, self.values)?; if quals.is_empty() { Ok(Arc::new(PropValueSetNodeOp { @@ -915,7 +926,9 @@ macro_rules! impl_qualifier_filter { Ok(Arc::new(NodeFilteredGraph::new(graph, filter))) } EntityMarker::Edge => { - let (left, quals) = self.create_qualified_edge_op(filtered.clone())?; + let (left, mut quals) = self.create_qualified_edge_op(filtered.clone())?; + quals.reverse(); + elem_prop_type(&left.prop_type(), quals.len())?; let right = Prop::Bool(true).create_edge_op(filtered)?; let elemwise = Arc::new(ListAwareCmpEdgeOp { left, @@ -928,7 +941,9 @@ macro_rules! impl_qualifier_filter { ))) } EntityMarker::ExplodedEdge => { - let (left, quals) = self.create_qualified_edge_op(filtered.clone())?; + let (left, mut quals) = self.create_qualified_edge_op(filtered.clone())?; + quals.reverse(); + elem_prop_type(&left.prop_type(), quals.len())?; let right = Prop::Bool(true).create_edge_op(filtered)?; let elemwise = Arc::new(ListAwareCmpEdgeOp { left, @@ -952,7 +967,12 @@ macro_rules! impl_qualifier_filter { if !matches!(self.0.entity().into(), EntityMarker::Node) { return Err(GraphError::NotNodeFilter); } - let (left, quals) = self.create_qualified_node_op(filtered.clone())?; + let (left, mut quals) = self.create_qualified_node_op(filtered.clone())?; + // Trailing qualifiers collect innermost level first; the + // collapse helper wraps innermost first after reversing, so + // reverse here to cancel it. + quals.reverse(); + elem_prop_type(&left.prop_type(), quals.len())?; let right = Prop::Bool(true).create_node_op(filtered)?; let elemwise = Arc::new(ListAwareCmpNodeOp { left, 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 index 81b5952d07..6303899636 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -56,6 +56,7 @@ use crate::{ }, prelude::GraphViewOps, }; +use bigdecimal::BigDecimal; use raphtory_api::core::entities::{ edges::edge_ref::EdgeRef, properties::prop::{IntoProp, Prop, PropArray, PropType}, @@ -131,6 +132,40 @@ impl NodeOp for NodeMetaOp { } } +// ───────────────────────────────────────────────────────────────────────────── +// WithPropType — annotates an op with a type only known at compile time +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct WithPropType { + pub(crate) inner: T, + pub(crate) pt: PropType, +} + +impl NodeOp for WithPropType { + type Output = T::Output; + + fn domain(&self, storage: &GraphStorage) -> NodeList { + self.inner.domain(storage) + } + + fn prop_type(&self) -> PropType { + self.pt.clone() + } + + fn const_value(&self) -> Option { + self.inner.const_value() + } + + fn const_value_in_domain(&self) -> Option { + self.inner.const_value_in_domain() + } + + fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { + self.inner.apply(storage, node) + } +} + // ───────────────────────────────────────────────────────────────────────────── // TemporalNodePropOp — all temporal values for a property within the window // ───────────────────────────────────────────────────────────────────────────── @@ -151,6 +186,14 @@ impl NodeOp for TemporalNodePropOp { self.graph.node_list() } + fn prop_type(&self) -> PropType { + self.graph + .node_meta() + .temporal_prop_mapper() + .get_dtype(self.prop_id) + .map_or(PropType::Empty, |dt| PropType::List(Box::new(dt))) + } + type Output = Prop; fn apply(&self, _storage: &GraphStorage, node: VID) -> Prop { @@ -180,8 +223,22 @@ impl NodeOp for TemporalNodePropOp { // LenExpr::create_node_op → LenNodeOp (Output = usize) // ───────────────────────────────────────────────────────────────────────────── +/// Aggregations collapse the innermost list level; outer levels survive so a +/// pending qualifier still sees per-element results. `scalar` names the type a +/// single aggregation step produces (`None` keeps the element type, as for +/// sum/min/max/first/last). +fn agg_out_type(pt: PropType, scalar: Option) -> PropType { + match pt { + PropType::List(inner) => match *inner { + nested @ PropType::List(_) => PropType::List(Box::new(agg_out_type(nested, scalar))), + elem => scalar.unwrap_or(elem), + }, + other => other, + } +} + macro_rules! impl_agg_entity_op { - ($node_name:ident, $edge_name:ident, $body:expr) => { + ($node_name:ident, $edge_name:ident, $out_pt:expr, $body:expr) => { #[derive(Clone)] pub struct $node_name<'g> { pub inner: Arc> + 'g>, @@ -194,6 +251,10 @@ macro_rules! impl_agg_entity_op { type Output = Option; + fn prop_type(&self) -> PropType { + ($out_pt)(self.inner.prop_type()) + } + fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { ($body)(self.inner.apply(storage, node)) } @@ -207,6 +268,10 @@ macro_rules! impl_agg_entity_op { impl<'g> EdgeOp for $edge_name<'g> { type Output = Option; + fn prop_type(&self) -> PropType { + ($out_pt)(self.inner.prop_type()) + } + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { ($body)(self.inner.apply(storage, edge)) } @@ -214,7 +279,7 @@ macro_rules! impl_agg_entity_op { }; } -impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { +impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |pt| agg_out_type(pt, None), |vals| { aggregate_list_values(vals, &|pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { @@ -225,7 +290,11 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { let (promoted, s64, s128, _) = scan_u64_sum(vals)?; Some(if promoted { - Prop::U64(u64::try_from(s128).ok()?) + // A sum past u64 promotes to Decimal and still compares. + match u64::try_from(s128) { + Ok(v) => Prop::U64(v), + Err(_) => Prop::Decimal(BigDecimal::from(s128)), + } } else { Prop::U64(s64) }) @@ -233,7 +302,10 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { PropType::I32 | PropType::I64 => { let (promoted, s64, s128, _) = scan_i64_sum(vals)?; Some(if promoted { - Prop::I64(i64::try_from(s128).ok()?) + match i64::try_from(s128) { + Ok(v) => Prop::I64(v), + Err(_) => Prop::Decimal(BigDecimal::from(s128)), + } } else { Prop::I64(s64) }) @@ -246,83 +318,111 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |vals| { }) }); -impl_agg_entity_op!(AvgNodeOp, AvgEdgeOp, |vals| { - aggregate_list_values(vals, &|pi| { - let mut vals = pi.peekable(); - if vals.peek().is_none() { - return None; - } - let inner = vals.peek().unwrap().dtype(); - match inner { - PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { - let (promoted, s64, s128, count) = scan_u64_sum(vals)?; - let s = if promoted { s128 as f64 } else { s64 as f64 }; - Some(Prop::F64(s / (count as f64))) +impl_agg_entity_op!( + AvgNodeOp, + AvgEdgeOp, + |pt| agg_out_type(pt, Some(PropType::F64)), + |vals| { + aggregate_list_values(vals, &|pi| { + let mut vals = pi.peekable(); + if vals.peek().is_none() { + return None; } - - PropType::I32 | PropType::I64 => { - let (promoted, s64, s128, count) = scan_i64_sum(vals)?; - let s = if promoted { s128 as f64 } else { s64 as f64 }; - Some(Prop::F64(s / (count as f64))) + let inner = vals.peek().unwrap().dtype(); + match inner { + PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { + let (promoted, s64, s128, count) = scan_u64_sum(vals)?; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) + } + + PropType::I32 | PropType::I64 => { + let (promoted, s64, s128, count) = scan_i64_sum(vals)?; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) + } + + PropType::F32 | PropType::F64 => { + let (sum, count) = scan_f64_sum_count(vals)?; + Some(Prop::F64(sum / (count as f64))) + } + + _ => None, } - - PropType::F32 | PropType::F64 => { - let (sum, count) = scan_f64_sum_count(vals)?; - Some(Prop::F64(sum / (count as f64))) - } - - _ => None, - } - }) -}); -impl_agg_entity_op!(MinNodeOp, MinEdgeOp, |vals| { + }) + } +); +impl_agg_entity_op!(MinNodeOp, MinEdgeOp, |pt| agg_out_type(pt, None), |vals| { aggregate_list_values(vals, &|pi| { let mut it = pi; let first = it.next()?; it.fold(Some(first), |acc, v| acc.and_then(|a| a.min(v))) }) }); -impl_agg_entity_op!(MaxNodeOp, MaxEdgeOp, |vals| { +impl_agg_entity_op!(MaxNodeOp, MaxEdgeOp, |pt| agg_out_type(pt, None), |vals| { aggregate_list_values(vals, &|pi| { let mut it = pi; let first = it.next()?; it.fold(Some(first), |acc, v| acc.and_then(|a| a.max(v))) }) }); -impl_agg_entity_op!(FirstNodeOp, FirstEdgeOp, |vals| { - // Pick the first temporal entry as-is (whether scalar or list). - // aggregate_values would recurse into list entries and pick the first - // *element* within each entry, which is wrong for list-typed properties. - match vals? { - Prop::List(x) => x.iter_all().find_map(|v| v), - _ => None, +impl_agg_entity_op!( + FirstNodeOp, + FirstEdgeOp, + |pt| agg_out_type(pt, None), + |vals| { + // Pick the first temporal entry as-is (whether scalar or list). + // aggregate_values would recurse into list entries and pick the first + // *element* within each entry, which is wrong for list-typed properties. + match vals? { + Prop::List(x) => x.iter_all().find_map(|v| v), + _ => None, + } } -}); -impl_agg_entity_op!(LastNodeOp, LastEdgeOp, |vals| { - // Pick the last temporal entry as-is (whether scalar or list). - match vals? { - Prop::List(x) => x.iter_all().filter_map(|v| v).last(), - _ => None, +); +impl_agg_entity_op!( + LastNodeOp, + LastEdgeOp, + |pt| agg_out_type(pt, None), + |vals| { + // Pick the last temporal entry as-is (whether scalar or list). + match vals? { + Prop::List(x) => x.iter_all().filter_map(|v| v).last(), + _ => None, + } } -}); -impl_agg_entity_op!(LenNodeOp, LenEdgeOp, |vals| { - aggregate_list_values(vals, &|pi| Some(pi.count().into_prop())) -}); -impl_agg_entity_op!(AnyNodeOp, AnyEdgeOp, |vals| { - aggregate_list_values(vals, &|mut pi| { - Some(Prop::Bool(pi.any(|r| r == Prop::Bool(true)))) - }) -}); -impl_agg_entity_op!(AllNodeOp, AllEdgeOp, |vals| { - aggregate_list_values(vals, &|mut pi| { - let mut saw_any = false; - let all_true = pi.all(|r| { - saw_any = true; - r == Prop::Bool(true) - }); - Some(Prop::Bool(saw_any && all_true)) - }) -}); +); +impl_agg_entity_op!( + LenNodeOp, + LenEdgeOp, + |pt| agg_out_type(pt, Some(PropType::U64)), + |vals| { aggregate_list_values(vals, &|pi| Some(pi.count().into_prop())) } +); +impl_agg_entity_op!( + AnyNodeOp, + AnyEdgeOp, + |pt| agg_out_type(pt, Some(PropType::Bool)), + |vals| { + aggregate_list_values(vals, &|mut pi| { + Some(Prop::Bool(pi.any(|r| r == Prop::Bool(true)))) + }) + } +); +impl_agg_entity_op!( + AllNodeOp, + AllEdgeOp, + |pt| agg_out_type(pt, Some(PropType::Bool)), + |vals| { + aggregate_list_values(vals, &|mut pi| { + let mut saw_any = false; + let all_true = pi.all(|r| { + saw_any = true; + r == Prop::Bool(true) + }); + Some(Prop::Bool(saw_any && all_true)) + }) + } +); // ───────────────────────────────────────────────────────────────────────────── // ListAwareCmpNodeOp / ListAwareStringNodeOp / ListAwareSetNodeOp diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 9bb859db05..29d4c6cb9d 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -12,8 +12,14 @@ use crate::{ types::iterable::FromIterable, }, }; -use pyo3::{pyclass, pymethods, Bound, FromPyObject, IntoPyObject, PyErr, PyResult, Python}; -use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; +use pyo3::{ + exceptions::PyTypeError, pyclass, pymethods, Bound, FromPyObject, IntoPyObject, PyErr, + PyResult, Python, +}; +use raphtory_api::core::{ + entities::properties::prop::{Prop, PropType}, + storage::timeindex::EventTime, +}; use std::sync::Arc; // filter.Node.neighbours.is_active.all @@ -63,67 +69,126 @@ enum ExprOrValue { Value(Prop), } +/// Values are checked against the expression's statically known type at the +/// comparison itself, so a mistyped literal fails where it is written instead +/// of at some later `filter()` call. Unknown types defer to filter time. +/// String operators require a string-castable operand whatever the lhs type. +fn check_str_value(v: &Prop) -> PyResult<()> { + if v.dtype() != PropType::Str && v.clone().try_cast(PropType::Str).is_err() { + return Err(PyTypeError::new_err(format!( + "value {v:?} of type {} is not a valid string operand", + v.dtype() + ))); + } + Ok(()) +} + +fn check_value(lhs: &Arc, v: &Prop) -> PyResult<()> { + let pt = lhs.dyn_prop_type(); + if pt != PropType::Empty && v.dtype() != pt && v.clone().try_cast(pt.clone()).is_err() { + return Err(PyTypeError::new_err(format!( + "value {v:?} of type {} is not comparable with an expression of type {pt}", + v.dtype() + ))); + } + Ok(()) +} + #[pymethods] impl PyExpr { - fn __eq__(&self, other: ExprOrValue) -> PyFilterExpr { + fn __eq__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().eq(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().eq(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().eq(e.0)))), + ExprOrValue::Value(v) => { + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().eq(v)))) + } } } - fn __ne__(&self, other: ExprOrValue) -> PyFilterExpr { + fn __ne__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().ne(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().ne(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ne(e.0)))), + ExprOrValue::Value(v) => { + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().ne(v)))) + } } } - fn __lt__(&self, other: ExprOrValue) -> PyFilterExpr { + fn __lt__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().lt(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().lt(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().lt(e.0)))), + ExprOrValue::Value(v) => { + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().lt(v)))) + } } } - fn __le__(&self, other: ExprOrValue) -> PyFilterExpr { + fn __le__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().le(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().le(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().le(e.0)))), + ExprOrValue::Value(v) => { + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().le(v)))) + } } } - fn __gt__(&self, other: ExprOrValue) -> PyFilterExpr { + fn __gt__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().gt(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().gt(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().gt(e.0)))), + ExprOrValue::Value(v) => { + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().gt(v)))) + } } } - fn __ge__(&self, other: ExprOrValue) -> PyFilterExpr { + fn __ge__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().ge(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().ge(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ge(e.0)))), + ExprOrValue::Value(v) => { + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().ge(v)))) + } } } - fn starts_with(&self, other: ExprOrValue) -> PyFilterExpr { + fn starts_with(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().starts_with(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().starts_with(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().starts_with(e.0)))), + ExprOrValue::Value(v) => { + check_str_value(&v)?; + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().starts_with(v)))) + } } } - fn ends_with(&self, other: ExprOrValue) -> PyFilterExpr { + fn ends_with(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().ends_with(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().ends_with(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ends_with(e.0)))), + ExprOrValue::Value(v) => { + check_str_value(&v)?; + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().ends_with(v)))) + } } } - fn contains(&self, other: ExprOrValue) -> PyFilterExpr { + fn contains(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().contains(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().contains(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().contains(e.0)))), + ExprOrValue::Value(v) => { + check_str_value(&v)?; + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().contains(v)))) + } } } - fn not_contains(&self, other: ExprOrValue) -> PyFilterExpr { + fn not_contains(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().not_contains(e.0))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().not_contains(v))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().not_contains(e.0)))), + ExprOrValue::Value(v) => { + check_str_value(&v)?; + check_value(&self.0, &v)?; + Ok(PyFilterExpr(Arc::new(self.0.clone().not_contains(v)))) + } } } fn fuzzy_search( @@ -131,19 +196,22 @@ impl PyExpr { other: ExprOrValue, levenshtein_distance: usize, prefix_match: bool, - ) -> PyFilterExpr { - match other { + ) -> PyResult { + Ok(match other { ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().fuzzy_search( e.0, levenshtein_distance, prefix_match, ))), - ExprOrValue::Value(v) => PyFilterExpr(Arc::new(self.0.clone().fuzzy_search( - v, - levenshtein_distance, - prefix_match, - ))), - } + ExprOrValue::Value(v) => { + check_str_value(&v)?; + PyFilterExpr(Arc::new(self.0.clone().fuzzy_search( + v, + levenshtein_distance, + prefix_match, + ))) + } + }) } fn is_in(&self, values: FromIterable) -> PyFilterExpr { From e618923b67103080f4e55480a4770b5123c6b392 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:45:40 +0100 Subject: [PATCH 109/155] GraphQL filters lower onto expressions The wire schema is unchanged; the DynFilter conversion now builds expression filters instead of the composite enums. Field conditions reuse the existing where-translations and dispatch onto typed field expressions, property condition trees walk onto the dyn expression chain (wrappers extend the expression in written order, combinators branch, leaves become predicates), views wrap the lowered filter, and edge endpoints evaluate a full lowered node filter per edge through the new EdgeEndpointNodeFilter adapter. The composite conversions remain for the python remote client, which still serialises filters through the tree representation. --- .../src/model/graph/expr_lowering.rs | 483 ++++++++++++++++++ raphtory-graphql/src/model/graph/filtering.rs | 12 +- raphtory-graphql/src/model/graph/mod.rs | 1 + .../graph/views/filter/model/edge_filter.rs | 119 ++++- 4 files changed, 603 insertions(+), 12 deletions(-) create mode 100644 raphtory-graphql/src/model/graph/expr_lowering.rs diff --git a/raphtory-graphql/src/model/graph/expr_lowering.rs b/raphtory-graphql/src/model/graph/expr_lowering.rs new file mode 100644 index 0000000000..7646d1772e --- /dev/null +++ b/raphtory-graphql/src/model/graph/expr_lowering.rs @@ -0,0 +1,483 @@ +//! Lowers the GraphQL filter wire types onto expression filters. +//! +//! The wire schema (`GqlNodeFilter` and friends) is unchanged; only the +//! target changes: instead of the composite filter enums, each condition +//! builds the corresponding typed expression and is erased to a [`DynFilter`]. + +use crate::model::graph::filtering::{ + translate_node_field_where, translate_prop_leaf_to_filter, GqlEdgeFilter, + GqlExplodedEdgeFilter, GqlNodeFilter, NodeField, NodeFieldCondition, PropCondition, +}; +use raphtory::{ + db::graph::views::filter::model::{ + edge_filter::{EdgeEndpointNodeFilter, Endpoint}, + exploded_edge_filter::ExplodedEdgeFilter, + filter::FilterValue, + latest_filter::Latest as LatestWrap, + layered_filter::Layered, + node_expr::{DynCreateOp, EntityAggOps}, + property_filter::PropertyFilterValue, + snapshot_filter::{SnapshotAt as SnapshotAtWrap, SnapshotLatest as SnapshotLatestWrap}, + windowed_filter::Windowed, + CombinedFilter, ComposableFilter, DynFilter, EdgeViewFilterOps, FilterOperator, + NodeViewFilterOps, PropertyExprFactory, + }, + errors::GraphError, + prelude::{EdgeFilter, EntityExprFilterOps, Layer, NodeFilter, NodeFilterFactory}, +}; +use raphtory_api::core::{ + entities::properties::prop::{IntoProp, Prop}, + storage::timeindex::{AsTime, EventTime}, + utils::time::IntoTime, +}; +use std::{ops::Deref, sync::Arc}; + +fn erased(f: F) -> DynFilter { + Arc::new(f) as DynFilter +} + +fn combine_all( + filters: impl IntoIterator>, + or: bool, + what: &str, +) -> Result { + let mut it = filters.into_iter(); + let first = it.next().transpose()?.ok_or_else(|| { + GraphError::InvalidGqlFilter(format!("Filter '{what}' requires non-empty list")) + })?; + it.try_fold(first, |acc, next| { + Ok::<_, GraphError>(if or { + Arc::new(acc.or(next?)) as DynFilter + } else { + Arc::new(acc.and(next?)) as DynFilter + }) + }) +} + +/// Applies one translated leaf predicate to a value expression. +fn apply_leaf( + lhs: Arc, + op: FilterOperator, + value: PropertyFilterValue, +) -> Result { + use FilterOperator as FO; + Ok(match (op, value) { + (FO::Eq, PropertyFilterValue::Single(v)) => erased(lhs.eq(v)), + (FO::Ne, PropertyFilterValue::Single(v)) => erased(lhs.ne(v)), + (FO::Gt, PropertyFilterValue::Single(v)) => erased(lhs.gt(v)), + (FO::Ge, PropertyFilterValue::Single(v)) => erased(lhs.ge(v)), + (FO::Lt, PropertyFilterValue::Single(v)) => erased(lhs.lt(v)), + (FO::Le, PropertyFilterValue::Single(v)) => erased(lhs.le(v)), + (FO::StartsWith, PropertyFilterValue::Single(v)) => erased(lhs.starts_with(v)), + (FO::EndsWith, PropertyFilterValue::Single(v)) => erased(lhs.ends_with(v)), + (FO::Contains, PropertyFilterValue::Single(v)) => erased(lhs.contains(v)), + (FO::NotContains, PropertyFilterValue::Single(v)) => erased(lhs.not_contains(v)), + ( + FO::FuzzySearch { + levenshtein_distance, + prefix_match, + }, + PropertyFilterValue::Single(v), + ) => erased(lhs.fuzzy_search(v, levenshtein_distance, prefix_match)), + (FO::IsIn, PropertyFilterValue::Set(values)) => { + erased(lhs.is_in(values.deref().iter().cloned())) + } + (FO::IsNotIn, PropertyFilterValue::Set(values)) => { + erased(lhs.is_not_in(values.deref().iter().cloned())) + } + (FO::IsSome, PropertyFilterValue::None) => erased(lhs.is_some()), + (FO::IsNone, PropertyFilterValue::None) => erased(lhs.is_none()), + (op, _) => { + return Err(GraphError::InvalidGqlFilter(format!( + "operator {op:?} received an incompatible value shape" + ))) + } + }) +} + +/// Walks a property condition tree over a value expression: wrapper conditions +/// extend the expression (leading form, outermost applied first), boolean +/// combinators branch, leaves become predicates. +fn lower_prop_condition( + lhs: Arc, + name_for_errors: &str, + cond: &PropCondition, +) -> Result { + use PropCondition::*; + match cond { + And(list) => combine_all( + list.iter() + .map(|c| lower_prop_condition(lhs.clone(), name_for_errors, c)), + false, + "and", + ), + Or(list) => combine_all( + list.iter() + .map(|c| lower_prop_condition(lhs.clone(), name_for_errors, c)), + true, + "or", + ), + Not(inner) => Ok( + Arc::new(lower_prop_condition(lhs, name_for_errors, inner.deref())?.not()) as DynFilter, + ), + First(inner) => lower_prop_condition( + Arc::new(EntityAggOps::first(lhs)), + name_for_errors, + inner.deref(), + ), + Last(inner) => lower_prop_condition( + Arc::new(EntityAggOps::last(lhs)), + name_for_errors, + inner.deref(), + ), + Any(inner) => lower_prop_condition( + Arc::new(EntityExprFilterOps::any(lhs)), + name_for_errors, + inner.deref(), + ), + All(inner) => lower_prop_condition( + Arc::new(EntityExprFilterOps::all(lhs)), + name_for_errors, + inner.deref(), + ), + Sum(inner) => lower_prop_condition( + Arc::new(EntityAggOps::sum(lhs)), + name_for_errors, + inner.deref(), + ), + Avg(inner) => lower_prop_condition( + Arc::new(EntityAggOps::avg(lhs)), + name_for_errors, + inner.deref(), + ), + Min(inner) => lower_prop_condition( + Arc::new(EntityAggOps::min(lhs)), + name_for_errors, + inner.deref(), + ), + Max(inner) => lower_prop_condition( + Arc::new(EntityAggOps::max(lhs)), + name_for_errors, + inner.deref(), + ), + Len(inner) => lower_prop_condition( + Arc::new(EntityAggOps::len(lhs)), + name_for_errors, + inner.deref(), + ), + leaf => { + let (op, value) = translate_prop_leaf_to_filter(name_for_errors, leaf)?; + apply_leaf(lhs, op, value) + } + } +} + +/// Applies one translated built-in-field predicate to a field expression. +fn apply_field_leaf( + lhs: Arc, + op: FilterOperator, + value: FilterValue, +) -> Result { + use FilterOperator as FO; + let single = |v: FilterValue| -> Result { + Ok(match v { + FilterValue::ID(gid) => gid.into_prop(), + FilterValue::Single(s) => Prop::str(s), + other => { + return Err(GraphError::InvalidGqlFilter(format!( + "expected a single value, got {other:?}" + ))) + } + }) + }; + let set = |v: FilterValue| -> Result, GraphError> { + Ok(match v { + FilterValue::IDSet(gids) => gids.iter().map(|g| g.clone().into_prop()).collect(), + FilterValue::Set(strings) => strings.iter().map(|s| Prop::str(s.to_string())).collect(), + other => { + return Err(GraphError::InvalidGqlFilter(format!( + "expected a list of values, got {other:?}" + ))) + } + }) + }; + Ok(match op { + FO::Eq => erased(lhs.eq(single(value)?)), + FO::Ne => erased(lhs.ne(single(value)?)), + FO::Gt => erased(lhs.gt(single(value)?)), + FO::Ge => erased(lhs.ge(single(value)?)), + FO::Lt => erased(lhs.lt(single(value)?)), + FO::Le => erased(lhs.le(single(value)?)), + FO::StartsWith => erased(lhs.starts_with(single(value)?)), + FO::EndsWith => erased(lhs.ends_with(single(value)?)), + FO::Contains => erased(lhs.contains(single(value)?)), + FO::NotContains => erased(lhs.not_contains(single(value)?)), + FO::FuzzySearch { + levenshtein_distance, + prefix_match, + } => erased(lhs.fuzzy_search(single(value)?, levenshtein_distance, prefix_match)), + FO::IsIn => erased(lhs.is_in(set(value)?)), + FO::IsNotIn => erased(lhs.is_not_in(set(value)?)), + FO::IsSome => erased(lhs.is_some()), + FO::IsNone => erased(lhs.is_none()), + }) +} + +fn node_field_lhs(field: NodeField) -> Arc { + match field { + NodeField::NodeId => Arc::new(NodeFilter.id()), + NodeField::NodeName => Arc::new(NodeFilter.name()), + NodeField::NodeType => Arc::new(NodeFilter.node_type()), + } +} + +fn node_field_filter(field: NodeField, cond: &NodeFieldCondition) -> Result { + let (_, value, op) = translate_node_field_where(field, cond)?; + apply_field_leaf(node_field_lhs(field), op, value) +} + +pub(crate) fn lower_node_filter(filter: &GqlNodeFilter) -> Result { + use GqlNodeFilter::*; + Ok(match filter { + Id(f) => node_field_filter(NodeField::NodeId, &f.where_)?, + Name(f) => node_field_filter(NodeField::NodeName, &f.where_)?, + NodeType(f) => node_field_filter(NodeField::NodeType, &f.where_)?, + Degree(degree) => { + let lhs: Arc = match degree.direction.into() { + raphtory_api::core::Direction::BOTH => Arc::new(NodeFilter.degree()), + raphtory_api::core::Direction::IN => Arc::new(NodeFilter.in_degree()), + raphtory_api::core::Direction::OUT => Arc::new(NodeFilter.out_degree()), + }; + let field_name: String = degree.direction.into(); + lower_prop_condition(lhs, &field_name, °ree.where_)? + } + Property(prop) => lower_prop_condition( + Arc::new(PropertyExprFactory::property(&NodeFilter, &prop.name)), + &prop.name, + &prop.where_, + )?, + Metadata(prop) => lower_prop_condition( + Arc::new(PropertyExprFactory::metadata(&NodeFilter, &prop.name)), + &prop.name, + &prop.where_, + )?, + TemporalProperty(prop) => { + let temporal = PropertyExprFactory::property(&NodeFilter, &prop.name).temporal(); + lower_prop_condition(Arc::new(temporal), &prop.name, &prop.where_)? + } + And(filters) => combine_all(filters.iter().map(lower_node_filter), false, "and")?, + Or(filters) => combine_all(filters.iter().map(lower_node_filter), true, "or")?, + Not(inner) => Arc::new(lower_node_filter(inner.deref())?.not()) as DynFilter, + Window(w) => erased(Windowed::new( + w.start.clone().into_time(), + w.end.clone().into_time(), + lower_node_filter(w.expr.deref())?, + )), + At(t) => { + let et = t.time.clone().into_time(); + erased(Windowed::new( + et, + EventTime::end(et.t().saturating_add(1)), + lower_node_filter(t.expr.deref())?, + )) + } + Before(t) => erased(Windowed::new( + EventTime::start(i64::MIN), + EventTime::end(t.time.clone().into_time().t()), + lower_node_filter(t.expr.deref())?, + )), + After(t) => erased(Windowed::new( + EventTime::start(t.time.clone().into_time().t().saturating_add(1)), + EventTime::end(i64::MAX), + lower_node_filter(t.expr.deref())?, + )), + Latest(u) => erased(LatestWrap::new(lower_node_filter(u.expr.deref())?)), + SnapshotAt(t) => erased(SnapshotAtWrap::new( + t.time.clone().into_time(), + lower_node_filter(t.expr.deref())?, + )), + SnapshotLatest(u) => erased(SnapshotLatestWrap::new(lower_node_filter(u.expr.deref())?)), + Layers(l) => erased(Layered::new( + Layer::from(l.names.clone()), + lower_node_filter(l.expr.deref())?, + )), + IsActive(true) => erased(NodeFilter.is_active()), + IsActive(false) => Arc::new(erased(NodeFilter.is_active()).not()) as DynFilter, + }) +} + +fn edge_prop_lhs(exploded: bool, kind: PropKind, name: &str) -> Arc { + match (exploded, kind) { + (false, PropKind::Property) => Arc::new(PropertyExprFactory::property(&EdgeFilter, name)), + (false, PropKind::Metadata) => Arc::new(PropertyExprFactory::metadata(&EdgeFilter, name)), + (false, PropKind::Temporal) => { + Arc::new(PropertyExprFactory::property(&EdgeFilter, name).temporal()) + } + (true, PropKind::Property) => { + Arc::new(PropertyExprFactory::property(&ExplodedEdgeFilter, name)) + } + (true, PropKind::Metadata) => { + Arc::new(PropertyExprFactory::metadata(&ExplodedEdgeFilter, name)) + } + (true, PropKind::Temporal) => { + Arc::new(PropertyExprFactory::property(&ExplodedEdgeFilter, name).temporal()) + } + } +} + +#[derive(Clone, Copy)] +enum PropKind { + Property, + Metadata, + Temporal, +} + +pub(crate) fn lower_edge_filter(filter: &GqlEdgeFilter) -> Result { + use GqlEdgeFilter::*; + Ok(match filter { + Src(inner) => erased(EdgeEndpointNodeFilter { + endpoint: Endpoint::Src, + inner: lower_node_filter(inner.deref())?, + }), + Dst(inner) => erased(EdgeEndpointNodeFilter { + endpoint: Endpoint::Dst, + inner: lower_node_filter(inner.deref())?, + }), + Property(prop) => lower_prop_condition( + edge_prop_lhs(false, PropKind::Property, &prop.name), + &prop.name, + &prop.where_, + )?, + Metadata(prop) => lower_prop_condition( + edge_prop_lhs(false, PropKind::Metadata, &prop.name), + &prop.name, + &prop.where_, + )?, + TemporalProperty(prop) => lower_prop_condition( + edge_prop_lhs(false, PropKind::Temporal, &prop.name), + &prop.name, + &prop.where_, + )?, + And(filters) => combine_all(filters.iter().map(lower_edge_filter), false, "and")?, + Or(filters) => combine_all(filters.iter().map(lower_edge_filter), true, "or")?, + Not(inner) => Arc::new(lower_edge_filter(inner.deref())?.not()) as DynFilter, + Window(w) => erased(Windowed::new( + w.start.clone().into_time(), + w.end.clone().into_time(), + lower_edge_filter(w.expr.deref())?, + )), + At(t) => { + let et = t.time.clone().into_time(); + erased(Windowed::new( + et, + EventTime::end(et.t().saturating_add(1)), + lower_edge_filter(t.expr.deref())?, + )) + } + Before(t) => erased(Windowed::new( + EventTime::start(i64::MIN), + EventTime::end(t.time.clone().into_time().t()), + lower_edge_filter(t.expr.deref())?, + )), + After(t) => erased(Windowed::new( + EventTime::start(t.time.clone().into_time().t().saturating_add(1)), + EventTime::end(i64::MAX), + lower_edge_filter(t.expr.deref())?, + )), + Latest(u) => erased(LatestWrap::new(lower_edge_filter(u.expr.deref())?)), + SnapshotAt(t) => erased(SnapshotAtWrap::new( + t.time.clone().into_time(), + lower_edge_filter(t.expr.deref())?, + )), + SnapshotLatest(u) => erased(SnapshotLatestWrap::new(lower_edge_filter(u.expr.deref())?)), + Layers(l) => erased(Layered::new( + Layer::from(l.names.clone()), + lower_edge_filter(l.expr.deref())?, + )), + IsActive(v) => bool_leaf(erased(EdgeFilter.is_active()), *v), + IsValid(v) => bool_leaf(erased(EdgeFilter.is_valid()), *v), + IsDeleted(v) => bool_leaf(erased(EdgeFilter.is_deleted()), *v), + IsSelfLoop(v) => bool_leaf(erased(EdgeFilter.is_self_loop()), *v), + }) +} + +fn bool_leaf(filter: DynFilter, wanted: bool) -> DynFilter { + if wanted { + filter + } else { + Arc::new(filter.not()) as DynFilter + } +} + +pub(crate) fn lower_exploded_edge_filter( + filter: &GqlExplodedEdgeFilter, +) -> Result { + use GqlExplodedEdgeFilter::*; + Ok(match filter { + Src(inner) => erased(EdgeEndpointNodeFilter { + endpoint: Endpoint::Src, + inner: lower_node_filter(inner.deref())?, + }), + Dst(inner) => erased(EdgeEndpointNodeFilter { + endpoint: Endpoint::Dst, + inner: lower_node_filter(inner.deref())?, + }), + Property(prop) => lower_prop_condition( + edge_prop_lhs(true, PropKind::Property, &prop.name), + &prop.name, + &prop.where_, + )?, + Metadata(prop) => lower_prop_condition( + edge_prop_lhs(true, PropKind::Metadata, &prop.name), + &prop.name, + &prop.where_, + )?, + TemporalProperty(prop) => lower_prop_condition( + edge_prop_lhs(true, PropKind::Temporal, &prop.name), + &prop.name, + &prop.where_, + )?, + And(filters) => combine_all(filters.iter().map(lower_exploded_edge_filter), false, "and")?, + Or(filters) => combine_all(filters.iter().map(lower_exploded_edge_filter), true, "or")?, + Not(inner) => Arc::new(lower_exploded_edge_filter(inner.deref())?.not()) as DynFilter, + Window(w) => erased(Windowed::new( + w.start.clone().into_time(), + w.end.clone().into_time(), + lower_exploded_edge_filter(w.expr.deref())?, + )), + At(t) => { + let et = t.time.clone().into_time(); + erased(Windowed::new( + et, + EventTime::end(et.t().saturating_add(1)), + lower_exploded_edge_filter(t.expr.deref())?, + )) + } + Before(t) => erased(Windowed::new( + EventTime::start(i64::MIN), + EventTime::end(t.time.clone().into_time().t()), + lower_exploded_edge_filter(t.expr.deref())?, + )), + After(t) => erased(Windowed::new( + EventTime::start(t.time.clone().into_time().t().saturating_add(1)), + EventTime::end(i64::MAX), + lower_exploded_edge_filter(t.expr.deref())?, + )), + Latest(u) => erased(LatestWrap::new(lower_exploded_edge_filter(u.expr.deref())?)), + SnapshotAt(t) => erased(SnapshotAtWrap::new( + t.time.clone().into_time(), + lower_exploded_edge_filter(t.expr.deref())?, + )), + SnapshotLatest(u) => erased(SnapshotLatestWrap::new(lower_exploded_edge_filter( + u.expr.deref(), + )?)), + Layers(l) => erased(Layered::new( + Layer::from(l.names.clone()), + lower_exploded_edge_filter(l.expr.deref())?, + )), + IsActive(v) => bool_leaf(erased(ExplodedEdgeFilter.is_active()), *v), + IsValid(v) => bool_leaf(erased(ExplodedEdgeFilter.is_valid()), *v), + IsDeleted(v) => bool_leaf(erased(ExplodedEdgeFilter.is_deleted()), *v), + IsSelfLoop(v) => bool_leaf(erased(ExplodedEdgeFilter.is_self_loop()), *v), + }) +} diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 6116887e75..3d1b377520 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -807,11 +807,9 @@ impl TryFrom for DynFilter { fn try_from(value: GqlFilter) -> Result { let filter = match value { - GqlFilter::Node(f) => Arc::new(CompositeNodeFilter::try_from(f)?) as DynFilter, - GqlFilter::Edge(f) => Arc::new(CompositeEdgeFilter::try_from(f)?) as DynFilter, - GqlFilter::ExplodedEdge(f) => { - Arc::new(CompositeExplodedEdgeFilter::try_from(f)?) as DynFilter - } + GqlFilter::Node(f) => super::expr_lowering::lower_node_filter(&f)?, + GqlFilter::Edge(f) => super::expr_lowering::lower_edge_filter(&f)?, + GqlFilter::ExplodedEdge(f) => super::expr_lowering::lower_exploded_edge_filter(&f)?, GqlFilter::Graph(f) => DynView::try_from(f)?, GqlFilter::And(filters) => { let mut filters = filters.into_iter().map(DynFilter::try_from); @@ -1592,7 +1590,7 @@ fn parse_string_list(op: &str, v: &Value) -> Result { Ok(FilterValue::Set(Arc::new(strings.into_iter().collect()))) } -fn translate_node_field_where( +pub(crate) fn translate_node_field_where( field: NodeField, cond: &NodeFieldCondition, ) -> Result<(String, FilterValue, FilterOperator), GraphError> { @@ -1764,7 +1762,7 @@ fn translate_node_field_where( }) } -fn translate_prop_leaf_to_filter( +pub(crate) fn translate_prop_leaf_to_filter( name_for_errors: &str, cmp: &PropCondition, ) -> Result<(FilterOperator, PropertyFilterValue), GraphError> { diff --git a/raphtory-graphql/src/model/graph/mod.rs b/raphtory-graphql/src/model/graph/mod.rs index bad74a7224..0e4114b00e 100644 --- a/raphtory-graphql/src/model/graph/mod.rs +++ b/raphtory-graphql/src/model/graph/mod.rs @@ -5,6 +5,7 @@ use raphtory_api::core::utils::time::ParseTimeError; pub mod collection; pub mod edge; pub mod edges; +pub(crate) mod expr_lowering; pub mod filtering; pub mod graph; pub mod history; 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 e72c61f80c..f292cad9c2 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -1,13 +1,14 @@ use crate::{ db::{ api::{ - state::ops::NotANodeFilter, + state::ops::{NodeOp, NotANodeFilter}, view::{ internal::{DynGraphArc, GraphView}, BoxableGraphView, }, }, graph::views::filter::{ + edge_expr_filtered_graph::EdgeExprFilteredGraph, edge_node_filtered_graph::EdgeNodeFilteredGraph, model::{ edge_expr::{ops::EdgeEndpointNodeOp, EdgeOp}, @@ -31,16 +32,21 @@ use crate::{ }, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - AndFilter, CombinedFilter, ComposableFilter, EdgeViewFilterOps, EntityMarker, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, - NotFilter, OrFilter, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + AndFilter, CombinedFilter, ComposableFilter, DynFilter, EdgeViewFilterOps, + EntityMarker, FilterTree, InternalPropertyFilterBuilder, + InternalPropertyFilterFactory, InternalViewWrapOps, NotFilter, OrFilter, + TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, }, CreateFilter, }, }, errors::GraphError, }; -use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; +use raphtory_api::core::{ + entities::{edges::edge_ref::EdgeRef, properties::prop::Prop}, + storage::timeindex::EventTime, +}; +use raphtory_storage::graph::graph::GraphStorage; use std::{fmt, fmt::Display, sync::Arc}; // User facing entry for building edge filters. @@ -534,6 +540,109 @@ impl TryAsCompositeFilter for CompositeEdgeFilter { } } +// ── expr layer: a full node filter evaluated on an edge endpoint ── + +/// Evaluates an erased node filter against the src or dst node of each edge. +/// +/// Carries whatever the nested filter is (combinators, views, property +/// conditions) by compiling it to a boolean node op and applying that to the +/// endpoint's VID at evaluation time. +#[derive(Clone)] +pub struct EdgeEndpointNodeFilter { + pub endpoint: Endpoint, + pub inner: DynFilter, +} + +#[derive(Clone)] +struct EndpointNodeBoolOp<'g> { + endpoint: Endpoint, + node_op: Arc + 'g>, +} + +impl<'g> EdgeOp for EndpointNodeBoolOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + let vid = match self.endpoint { + Endpoint::Src => edge.src(), + Endpoint::Dst => edge.dst(), + }; + self.node_op.apply(storage, vid) + } +} + +impl CreateFilter for EdgeEndpointNodeFilter { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + let node_op = self + .inner + .create_dyn_node_filter(Arc::new(graph.clone()), Arc::new(filtered))?; + let op: Arc + 'graph> = Arc::new(EndpointNodeBoolOp { + endpoint: self.endpoint, + node_op, + }); + Ok(EdgeExprFilteredGraph::new(graph, op)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + _graph: G, + _filtered: F, + ) -> Result, GraphError> { + Err(GraphError::NotNodeFilter) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } +} + +impl ComposableFilter for EdgeEndpointNodeFilter {} + +impl TryAsCompositeFilter for EdgeEndpointNodeFilter { + fn try_as_composite_node_filter(&self) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + + fn try_as_composite_edge_filter(&self) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } + + fn try_as_filter_tree(&self) -> Result { + Err(GraphError::InvalidFilter( + "expression filters have no composite representation".to_string(), + )) + } +} + // ── expr layer: endpoint expressions bridge node ops into edge ops ── impl EntityExprBuilder for EdgeEndpointWrapper {} From 0b1bcd86bf4edab298f73f61c2006c40fa8e490d Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:22:57 +0100 Subject: [PATCH 110/155] record the wire form alongside python filter expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python filter objects now carry both the compiled expression and the GraphQL wire tree, recorded at construction. The remote client reads the carried tree; expressions with no wire form (an expression on the rhs of a comparison) are rejected at the remote boundary with a clear error. The client error envelope now surfaces each GraphQL error's message text instead of the raw JSON object, so quotes inside diagnostics are no longer escaped. Test expectations updated for expression-engine wording and semantics: bool constants coerce to ints in comparisons, mistyped is_in values are rejected eagerly, and nodes.filter() defers uniformly for every filter kind — narrowing membership is spelled nodes[...] / select (reverses the #2690 special case). --- python/test_utils/utils.py | 19 +- .../parity/test_parity_filters.py | 28 +- .../test_graphql/test_apply_views.py | 10 +- .../test_graph_edges_property_filter.py | 16 +- .../test_graph_nodes_property_filter.py | 14 +- .../test_filters/test_node_filter_gql.py | 4 +- .../test_nodes_property_filter.py | 14 +- .../test_remote_graph_transport.py | 17 +- .../test_graphql/test_rolling_expanding.py | 5 +- raphtory-graphql/src/client/error.rs | 11 +- .../views/filter/model/edge_expr/filters.rs | 2 +- raphtory/src/python/filter/edge_expr.rs | 148 ++++++-- .../src/python/filter/exploded_edge_expr.rs | 101 ++++-- raphtory/src/python/filter/filter_expr.rs | 28 +- raphtory/src/python/filter/graph_filter.rs | 71 +++- raphtory/src/python/filter/mod.rs | 1 + raphtory/src/python/filter/node_expr.rs | 341 +++++++++++++----- raphtory/src/python/filter/wire.rs | 209 +++++++++++ 18 files changed, 807 insertions(+), 232 deletions(-) create mode 100644 raphtory/src/python/filter/wire.rs diff --git a/python/test_utils/utils.py b/python/test_utils/utils.py index cb4a9888b1..e2c3c01f58 100644 --- a/python/test_utils/utils.py +++ b/python/test_utils/utils.py @@ -191,12 +191,9 @@ def run_graphql_error_test(query, expected_error_message, graph): client.query(query) full_error_message = str(excinfo.value) - match = re.search(r'"message":"(.*?)"', full_error_message) - error_message = match.group(1) if match else "" - assert ( - error_message == expected_error_message - ), f"Expected '{expected_error_message}', but got '{error_message}'" + expected_error_message in full_error_message + ), f"Expected '{expected_error_message}' in '{full_error_message}'" def run_group_graphql_error_test(queries_and_expected_error_messages, graph): @@ -206,11 +203,9 @@ def run_group_graphql_error_test(queries_and_expected_error_messages, graph): client.query(query) full_error_message = str(excinfo.value) - match = re.search(r'"message":"(.*?)"', full_error_message) - error_message = match.group(1) if match else "" assert ( - error_message == expected_error_message - ), f"Expected '{expected_error_message}', but got '{error_message}'" + expected_error_message in full_error_message + ), f"Expected '{expected_error_message}' in '{full_error_message}'" def run_graphql_error_test_contains(query, expected_substrings, graph): @@ -220,11 +215,11 @@ def run_graphql_error_test_contains(query, expected_substrings, graph): client.query(query) full_error_message = str(excinfo.value) - match = re.search(r'"message":"(.*?)"', full_error_message) - error_message = match.group(1) if match else "" for s in expected_substrings: - assert s in error_message, f"expected to find {s!r} in {error_message!r}" + assert ( + s in full_error_message + ), f"expected to find {s!r} in {full_error_message!r}" def run_graphql_compare_test(query_a, query_b, graph): diff --git a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index afc028fb9b..c50ec26a2d 100644 --- a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py +++ b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py @@ -1032,6 +1032,11 @@ def test_getitem_narrows_membership_where_filter_rescopes(filter_pair): "reject.unknown_property": lambda: f.Node.property("nope") > 1, "reject.unknown_metadata": lambda: f.Node.metadata("nope") > 1, "reject.degree_vs_str": lambda: f.Node.degree() > "x", + # One element on purpose: the wire carries sets unordered, so with several + # bad values local and server may name different offenders in the error. + "reject.is_in_mistyped_values": lambda: f.Node.property("score").is_in( + ["banana"] + ), # `avg` is F64 and `len` is U64, so neither accepts a plain Python int here. } @@ -1115,29 +1120,6 @@ def test_edge_expr_in_a_node_subscript_is_refused_the_same_way( ) -def test_is_in_with_a_mistyped_value_matches_nothing_on_both_sides(filter_pair): - """`is_in` with values of the wrong type is empty, not an error. - - Unlike `>` against a mistyped value — which both sides reject — a mistyped - `is_in` list is accepted and simply matches no node. That asymmetry is - surprising enough to pin, and it has to be the *same* surprise on both - sides, since a caller cannot tell "no matches" from "bad query" otherwise. - """ - build = lambda: f.Node.property("score").is_in(["not", "numbers"]) - assert_parity( - filter_pair, lambda g: sorted(n.name for n in g.filter(build()).nodes) - ) - - for side_name, side in ( - ("local", filter_pair.local), - ("remote", filter_pair.remote), - ): - assert [n.name for n in side.filter(build()).nodes] == [], ( - f"{side_name}: a mistyped is_in matched nodes; if this now raises " - f"or filters, move the case into REJECTED_EXPRS" - ) - - # `[expr]` with general (non-kind-typed) expressions: select on the wire now # takes GqlFilter, so graph-view / node / mixed expressions narrow membership # the same way local core select does. diff --git a/python/tests/test_base_install/test_graphql/test_apply_views.py b/python/tests/test_base_install/test_graphql/test_apply_views.py index 92c0cd4255..e1f1206fb2 100644 --- a/python/tests/test_base_install/test_graphql/test_apply_views.py +++ b/python/tests/test_base_install/test_graphql/test_apply_views.py @@ -1984,7 +1984,9 @@ def test_apply_view_too_many_arguments(): graph = Graph() create_graph_date(graph) queries_and_exceptions = [] - too_many_arguments_exception = "Fields \\" + too_many_arguments_exception = ( + 'Fields "views" conflict because they have differing arguments' + ) query = """ { graph(path: "g") { @@ -2044,11 +2046,13 @@ def test_apply_view_invalid_argument(): graph = Graph() create_graph_date(graph) queries_and_exceptions = [] - invalid_argument = "Invalid value for argument \\" + invalid_argument = ( + 'Invalid value for argument "views.0.layers", expected type "String"' + ) query = """ { graph(path: "g") { - applyViews(views: [{layers: "finds"}]) { + applyViews(views: [{layers: 5}]) { earliestTime { timestamp } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py index a6a1f531f5..813a562d3f 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py @@ -55,7 +55,7 @@ def test_graph_edge_property_filter_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property eprop5: expected List(I64) but actual type is I64" + "Invalid filter: value I64(1) of type I64 cannot be coerced to List" ) run_graphql_error_test(query, expected_error_message, graph) @@ -103,7 +103,7 @@ def test_graph_edge_property_filter_not_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property eprop4: expected Bool but actual type is I64" + "Invalid filter: value I64(1) of type I64 cannot be coerced to Bool" ) run_graphql_error_test(query, expected_error_message, graph) @@ -142,7 +142,7 @@ def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): filterEdges: filter(expr: { edge: { property: { name: "eprop1" - where: { ge: { bool: true } } + where: { ge: { str: "shivam" } } } } }) { edges { list { src { name } dst { name } } } @@ -151,7 +151,7 @@ def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property eprop1: expected I64 but actual type is Bool" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -199,7 +199,7 @@ def test_graph_edge_property_filter_less_than_or_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property eprop1: expected I64 but actual type is Str" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -237,7 +237,7 @@ def test_graph_edge_property_filter_greater_than_type_error(graph): } """ expected_error_message = ( - "Wrong type for property eprop1: expected I64 but actual type is Str" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -275,7 +275,7 @@ def test_graph_edge_property_filter_less_than_type_error(graph): } """ expected_error_message = ( - "Wrong type for property eprop1: expected I64 but actual type is Str" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -689,7 +689,7 @@ def test_edge_temporal_property_filter_empty_layers(graph): expr: { temporalProperty: { name: "p2" - where: { any: { avg: { lt: { f64: 1.0 } } } } + where: { avg: { lt: { f64: 1.0 } } } } } } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index 7f4320ffcf..f7d79882e0 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -44,7 +44,7 @@ def test_graph_node_property_filter_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop5: expected List(I64) but actual type is I64" + "Invalid filter: value I64(1) of type I64 cannot be coerced to List" ) run_graphql_error_test(query, expected_error_message, graph) @@ -88,7 +88,7 @@ def test_graph_node_property_filter_not_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop4: expected Bool but actual type is I64" + "Invalid filter: value I64(1) of type I64 cannot be coerced to Bool" ) run_graphql_error_test(query, expected_error_message, graph) @@ -121,7 +121,7 @@ def test_graph_node_property_filter_greater_than_or_equal_type_error(graph): filterNodes: filter(expr: { node: { property: { name: "prop1" - where: { ge: { bool: true } } + where: { ge: { str: "shivam" } } } } }) { nodes { list { name } } @@ -130,7 +130,7 @@ def test_graph_node_property_filter_greater_than_or_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop1: expected I64 but actual type is Bool" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -170,7 +170,7 @@ def test_graph_node_property_filter_less_than_or_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop1: expected I64 but actual type is Str" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -202,7 +202,7 @@ def test_graph_node_property_filter_greater_than_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop1: expected I64 but actual type is Str" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -236,7 +236,7 @@ def test_graph_node_property_filter_less_than_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop1: expected I64 but actual type is Str" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py index 84ca0ececa..f2a571a518 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py @@ -72,7 +72,9 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql2(graph): } } """ - expected_error_message = "Invalid filter: Filter value type does not match node ID type. Expected Str but got \\" + expected_error_message = ( + "Invalid filter: value U64(1) of type U64 cannot be coerced to Str" + ) run_graphql_error_test(query, expected_error_message, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py index a945608df3..3cc27b66bc 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py @@ -110,7 +110,7 @@ def test_node_property_filter_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop5: expected List(I64) but actual type is I64" + "Invalid filter: value I64(1) of type I64 cannot be coerced to List" ) run_graphql_error_test(query, expected_error_message, graph) @@ -168,7 +168,7 @@ def test_node_property_filter_not_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop4: expected Bool but actual type is I64" + "Invalid filter: value I64(1) of type I64 cannot be coerced to Bool" ) run_graphql_error_test(query, expected_error_message, graph) @@ -210,7 +210,7 @@ def test_node_property_filter_greater_than_or_equal_type_error(graph): property: { name: "prop1" where: { - ge: { bool: true } + ge: { str: "shivam" } } } } } @@ -224,7 +224,7 @@ def test_node_property_filter_greater_than_or_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop1: expected I64 but actual type is Bool" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -280,7 +280,7 @@ def test_node_property_filter_less_than_or_equal_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop1: expected I64 but actual type is Str" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -328,7 +328,7 @@ def test_node_property_filter_greater_than_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop1: expected I64 but actual type is Str" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) @@ -378,7 +378,7 @@ def test_node_property_filter_less_than_type_error(graph): } """ expected_error_message = ( - "Wrong type for property prop1: expected I64 but actual type is Str" + "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" ) run_graphql_error_test(query, expected_error_message, graph) diff --git a/python/tests/test_base_install/test_graphql/test_remote_graph_transport.py b/python/tests/test_base_install/test_graphql/test_remote_graph_transport.py index 8c3128062c..a32a9daf0e 100644 --- a/python/tests/test_base_install/test_graphql/test_remote_graph_transport.py +++ b/python/tests/test_base_install/test_graphql/test_remote_graph_transport.py @@ -2066,14 +2066,21 @@ def test_filter_nodes_preserves_membership(): assert all_ids == ["alice", "ben", "bob", "hamza"] -def test_filter_nodes_narrows_on_node_id(): - """#2690: a node-id filter (name/id) is applied as a graph view, so it DOES - narrow collection membership — unlike the property filter above. Pins the - new behavior (and its parity with local) so a regression is visible.""" +def test_filter_nodes_defers_on_node_id(): + """A name/id filter behaves like any other predicate on `.filter()`: it + keeps every member and defers to traversals, exactly as local raphtory + does. Narrowing membership by name is spelled `nodes[...]`/`select` (or a + graph-level `g.filter(...)`), never `.filter()`.""" from raphtory.filter import Node with _make_filter_graph() as rg: - assert sorted(rg.nodes.filter(Node.name() == "ben").id) == ["ben"] + assert sorted(rg.nodes.filter(Node.name() == "ben").id) == [ + "alice", + "ben", + "bob", + "hamza", + ] + assert sorted(rg.nodes[Node.name() == "ben"].id) == ["ben"] def test_temporal_multi_op_filter_preserves_op_order_e2e(): diff --git a/python/tests/test_base_install/test_graphql/test_rolling_expanding.py b/python/tests/test_base_install/test_graphql/test_rolling_expanding.py index 8e69dc0878..69860d7184 100644 --- a/python/tests/test_base_install/test_graphql/test_rolling_expanding.py +++ b/python/tests/test_base_install/test_graphql/test_rolling_expanding.py @@ -2161,7 +2161,10 @@ def test_mismatched_window_step_and_errors(): queries_and_exceptions = [] parse_exception = "Failed to parse time string: One of the tokens in the interval string supposed to be a number couldn't be parsed." parse_exception2 = "Failed to parse time string: 'monthdas' is not a valid unit. Valid units are year(s), month(s), week(s), day(s), hour(s), minute(s), second(s) and millisecond(s)." - too_many_exception = "Invalid value for argument \\" + too_many_exception = ( + 'Invalid value for argument "window", ' + "Oneof input objects requires have exactly one field" + ) # go forward 1 hour (end of window), then go back 1 day (start of window) from the earliest event in the graph (2025-01-01 00:00:00) query = """ { diff --git a/raphtory-graphql/src/client/error.rs b/raphtory-graphql/src/client/error.rs index 587f21aca8..3ec0f055fb 100644 --- a/raphtory-graphql/src/client/error.rs +++ b/raphtory-graphql/src/client/error.rs @@ -118,13 +118,20 @@ pub(crate) fn classify_graphql_errors(errors: &JsonValue, query: &str) -> Client } } + // Surface each error's `message` text directly; the raw JSON object is a + // fallback for servers that send errors without one. Printing the object + // itself would JSON-escape any quotes inside the message. + let error_text = |e: &JsonValue| match e.get("message").and_then(|m| m.as_str()) { + Some(m) => m.to_owned(), + None => e.to_string(), + }; let message = match errors { JsonValue::Array(errors) => errors .iter() - .map(|e| format!("{}", e)) + .map(error_text) .collect::>() .join("\n\t"), - _ => format!("{}", errors), + _ => error_text(errors), }; if graph_not_found && !access_denied { diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index deb3b76097..f9bc339ff6 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -21,7 +21,7 @@ use crate::{ coerce_set_values, edge_filter::EdgeFilter, elem_prop_type, - filter_operator::{BinaryOp, ElemQual}, + filter_operator::ElemQual, node_expr::{ filters::PropValueSetExpr, ops::{AllEdgeOp, AnyEdgeOp}, diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 642fb48c5f..e16115a63b 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -1,21 +1,23 @@ use crate::{ db::graph::views::filter::model::{ - edge_filter::{EdgeEndpointWrapper, EdgeFilter}, + edge_filter::{CompositeEdgeFilter, EdgeEndpointWrapper, EdgeFilter, Endpoint}, is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, node_expr::{DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr}, node_filter::NodeFilter, + property_filter::PropertyRef, windowed_filter::Windowed, CombinedFilter, CreateView, DynCreateFilter, DynCreateView, EdgeFilterFactory, - EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, PropertyExprFactory, ViewWrapOps, - Wrap, + EdgeViewFilterOps, EntityMarker, FilterTree, InternalViewWrapOps, PropertyExprFactory, + ViewWrapOps, Wrap, }, python::{ filter::{ filter_expr::PyFilterExpr, node_expr::{PyExpr, PyPropertyExpr}, + wire::{wrap_edge_views, WireEntity, WireLhs, WireTarget, WireView}, }, types::iterable::FromIterable, }, @@ -35,23 +37,48 @@ use std::sync::Arc; /// Edge.dst().name().starts_with("user:") /// Edge.src().property("country") == "UK" #[pyclass(frozen, name = "EdgeEndpoint", module = "raphtory.filter")] -pub struct PyEdgeEndpoint(pub EdgeEndpointWrapper); +pub struct PyEdgeEndpoint( + pub EdgeEndpointWrapper, + pub(crate) Endpoint, + pub(crate) Vec, +); + +impl PyEdgeEndpoint { + fn lhs(&self, target: WireTarget) -> WireLhs { + WireLhs { + entity: WireEntity::Edge, + endpoint: Some(self.1), + target, + ops: Vec::new(), + views: self.2.clone(), + } + } +} #[pymethods] impl PyEdgeEndpoint { /// Selects the endpoint node ID field for filtering. fn id(&self) -> PyExpr { - self.0.id().into() + PyExpr::new( + Arc::new(self.0.id()), + Some(self.lhs(WireTarget::Field("node_id"))), + ) } /// Selects the endpoint node name field for filtering. fn name(&self) -> PyExpr { - self.0.name().into() + PyExpr::new( + Arc::new(self.0.name()), + Some(self.lhs(WireTarget::Field("node_name"))), + ) } /// Selects the endpoint node type field for filtering. fn node_type(&self) -> PyExpr { - self.0.node_type().into() + PyExpr::new( + Arc::new(self.0.node_type()), + Some(self.lhs(WireTarget::Field("node_type"))), + ) } /// Filters an endpoint node property by name. @@ -59,8 +86,8 @@ impl PyEdgeEndpoint { /// Arguments: /// name (str): Property key. fn property(&self, name: String) -> PyPropertyExpr { - let expr: Arc = Arc::new(self.0.property(name)); - expr.into() + let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); + PyPropertyExpr::new(Arc::new(self.0.property(name)), Some(lhs)) } /// Filters an endpoint node metadata field by name. @@ -68,9 +95,14 @@ impl PyEdgeEndpoint { /// Arguments: /// name (str): Metadata key. fn metadata(&self, name: String) -> PyExpr { - self.0 - .wrap(PropertyExprFactory::metadata(&NodeFilter, name)) - .into() + let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); + PyExpr::new( + Arc::new( + self.0 + .wrap(PropertyExprFactory::metadata(&NodeFilter, name)), + ), + Some(lhs), + ) } } @@ -192,13 +224,23 @@ where impl PyEdgeFilter { pub(crate) fn root() -> Self { - PyEdgeFilter(Arc::new(EdgeFilter)) + PyEdgeFilter(Arc::new(EdgeFilter), Vec::new()) + } + + fn wrap(&self, factory: Arc, view: WireView) -> Self { + let mut views = self.1.clone(); + views.push(view); + PyEdgeFilter(factory, views) } -} -impl From> for PyEdgeFilter { - fn from(value: Arc) -> Self { - PyEdgeFilter(value) + fn lhs(&self, target: WireTarget) -> WireLhs { + WireLhs { + entity: WireEntity::Edge, + endpoint: None, + target, + ops: Vec::new(), + views: self.1.clone(), + } } } @@ -216,23 +258,23 @@ impl From> for PyEdgeFilter { /// Edge.window(0, 10).is_active() /// Edge.layer("fire_nation").is_valid() #[pyclass(frozen, name = "Edge", module = "raphtory.filter")] -pub struct PyEdgeFilter(Arc); +pub struct PyEdgeFilter(Arc, Vec); #[pymethods] impl PyEdgeFilter { #[new] fn new() -> PyEdgeFilter { - PyEdgeFilter(Arc::new(EdgeFilter)) + Self::root() } /// Selects the edge **source endpoint** for filtering. fn src(&self) -> PyEdgeEndpoint { - PyEdgeEndpoint(EdgeFilter::src()) + PyEdgeEndpoint(EdgeFilter::src(), Endpoint::Src, self.1.clone()) } /// Selects the edge **destination endpoint** for filtering. fn dst(&self) -> PyEdgeEndpoint { - PyEdgeEndpoint(EdgeFilter::dst()) + PyEdgeEndpoint(EdgeFilter::dst(), Endpoint::Dst, self.1.clone()) } /// Filters an edge property by name. @@ -240,7 +282,8 @@ impl PyEdgeFilter { /// Arguments: /// name (str): Property key. fn property(&self, name: String) -> PyPropertyExpr { - self.0.dyn_property(name).into() + let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); + PyPropertyExpr::new(self.0.dyn_property(name), Some(lhs)) } /// Filters an edge metadata field by name. @@ -248,71 +291,104 @@ impl PyEdgeFilter { /// Arguments: /// name (str): Metadata key. fn metadata(&self, name: String) -> PyExpr { - self.0.dyn_metadata(name).into() + let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); + PyExpr::new(self.0.dyn_metadata(name), Some(lhs)) } /// Restricts edge evaluation to the given time window. fn window(&self, start: EventTime, end: EventTime) -> PyEdgeFilter { - self.0.dyn_window(start, end).into() + self.wrap(self.0.dyn_window(start, end), WireView::Window(start, end)) } /// Restricts edge evaluation to a single point in time. fn at(&self, time: EventTime) -> PyEdgeFilter { - self.0.dyn_at(time).into() + self.wrap( + self.0.dyn_at(time), + WireView::Window(time, EventTime::end(time.t().saturating_add(1))), + ) } /// Restricts edge evaluation to times strictly after the given time. fn after(&self, time: EventTime) -> PyEdgeFilter { - self.0.dyn_after(time).into() + self.wrap( + self.0.dyn_after(time), + WireView::Window( + EventTime::start(time.t().saturating_add(1)), + EventTime::end(i64::MAX), + ), + ) } /// Restricts edge evaluation to times strictly before the given time. fn before(&self, time: EventTime) -> PyEdgeFilter { - self.0.dyn_before(time).into() + self.wrap( + self.0.dyn_before(time), + WireView::Window(EventTime::start(i64::MIN), EventTime::end(time.t())), + ) } /// Evaluates edge predicates against the latest available edge state. fn latest(&self) -> PyEdgeFilter { - self.0.dyn_latest().into() + self.wrap(self.0.dyn_latest(), WireView::Latest) } /// Evaluates edge predicates against a snapshot of the graph at a given time. fn snapshot_at(&self, time: EventTime) -> PyEdgeFilter { - self.0.dyn_snapshot_at(time).into() + self.wrap(self.0.dyn_snapshot_at(time), WireView::SnapshotAt(time)) } /// Evaluates edge predicates against the most recent snapshot of the graph. fn snapshot_latest(&self) -> PyEdgeFilter { - self.0.dyn_snapshot_latest().into() + self.wrap(self.0.dyn_snapshot_latest(), WireView::SnapshotLatest) } /// Restricts evaluation to edges belonging to the given layer. fn layer(&self, layer: String) -> PyEdgeFilter { - self.0.dyn_layer(vec![layer]).into() + self.wrap( + self.0.dyn_layer(vec![layer.clone()]), + WireView::Layers(vec![layer]), + ) } /// Restricts evaluation to edges belonging to any of the given layers. fn layers(&self, layers: FromIterable) -> PyEdgeFilter { - self.0.dyn_layer(layers.to_vec()).into() + let layers = layers.to_vec(); + self.wrap(self.0.dyn_layer(layers.clone()), WireView::Layers(layers)) } /// Matches edges that have at least one event in the current view. fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_active()) + let tree = FilterTree::Edge(wrap_edge_views( + CompositeEdgeFilter::IsActiveEdge(IsActiveEdge), + &self.1, + )); + PyFilterExpr(self.0.dyn_is_active(), Some(tree)) } /// Matches edges that are structurally valid in the current view. fn is_valid(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_valid()) + let tree = FilterTree::Edge(wrap_edge_views( + CompositeEdgeFilter::IsValidEdge(IsValidEdge), + &self.1, + )); + PyFilterExpr(self.0.dyn_is_valid(), Some(tree)) } /// Matches edges that have been deleted. fn is_deleted(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_deleted()) + let tree = FilterTree::Edge(wrap_edge_views( + CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge), + &self.1, + )); + PyFilterExpr(self.0.dyn_is_deleted(), Some(tree)) } /// Matches edges that are self-loops (source == destination). fn is_self_loop(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_self_loop()) + let tree = FilterTree::Edge(wrap_edge_views( + CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge), + &self.1, + )); + PyFilterExpr(self.0.dyn_is_self_loop(), Some(tree)) } } diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index 5d3500f292..15818603ca 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -1,16 +1,25 @@ use crate::{ - db::graph::views::filter::model::exploded_edge_filter::ExplodedEdgeFilter, + db::graph::views::filter::model::{ + exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, + is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + property_filter::PropertyRef, + FilterTree, + }, python::{ filter::{ edge_expr::DynEdgeFilterFactory, filter_expr::PyFilterExpr, node_expr::{PyExpr, PyPropertyExpr}, + wire::{wrap_exploded_views, WireEntity, WireLhs, WireTarget, WireView}, }, types::iterable::FromIterable, }, }; use pyo3::{pyclass, pymethods}; -use raphtory_api::core::storage::timeindex::EventTime; +use raphtory_api::core::storage::timeindex::{AsTime, EventTime}; use std::sync::Arc; /// Entry point for constructing **exploded edge** filter expressions. @@ -29,17 +38,27 @@ use std::sync::Arc; /// ExplodedEdge.window(0, 10).is_active() /// ExplodedEdge.layer("fire_nation").is_valid() #[pyclass(frozen, name = "ExplodedEdge", module = "raphtory.filter")] -pub struct PyExplodedEdgeFilter(Arc); +pub struct PyExplodedEdgeFilter(Arc, Vec); impl PyExplodedEdgeFilter { pub(crate) fn root() -> Self { - PyExplodedEdgeFilter(Arc::new(ExplodedEdgeFilter)) + PyExplodedEdgeFilter(Arc::new(ExplodedEdgeFilter), Vec::new()) + } + + fn wrap(&self, factory: Arc, view: WireView) -> Self { + let mut views = self.1.clone(); + views.push(view); + PyExplodedEdgeFilter(factory, views) } -} -impl From> for PyExplodedEdgeFilter { - fn from(value: Arc) -> Self { - PyExplodedEdgeFilter(value) + fn lhs(&self, target: WireTarget) -> WireLhs { + WireLhs { + entity: WireEntity::ExplodedEdge, + endpoint: None, + target, + ops: Vec::new(), + views: self.1.clone(), + } } } @@ -47,7 +66,7 @@ impl From> for PyExplodedEdgeFilter { impl PyExplodedEdgeFilter { #[new] fn new() -> PyExplodedEdgeFilter { - PyExplodedEdgeFilter(Arc::new(ExplodedEdgeFilter)) + Self::root() } /// Filters an exploded edge property by name. @@ -57,7 +76,8 @@ impl PyExplodedEdgeFilter { /// Arguments: /// name (str): Property key. fn property(&self, name: String) -> PyPropertyExpr { - self.0.dyn_property(name).into() + let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); + PyPropertyExpr::new(self.0.dyn_property(name), Some(lhs)) } /// Filters an exploded edge metadata field by name. @@ -67,73 +87,106 @@ impl PyExplodedEdgeFilter { /// Arguments: /// name (str): Metadata key. fn metadata(&self, name: String) -> PyExpr { - self.0.dyn_metadata(name).into() + let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); + PyExpr::new(self.0.dyn_metadata(name), Some(lhs)) } /// Restricts exploded edge evaluation to the given time window. /// /// The window is inclusive of `start` and exclusive of `end`. fn window(&self, start: EventTime, end: EventTime) -> PyExplodedEdgeFilter { - self.0.dyn_window(start, end).into() + self.wrap(self.0.dyn_window(start, end), WireView::Window(start, end)) } /// Restricts exploded edge evaluation to a single point in time. fn at(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.0.dyn_at(time).into() + self.wrap( + self.0.dyn_at(time), + WireView::Window(time, EventTime::end(time.t().saturating_add(1))), + ) } /// Restricts exploded edge evaluation to times strictly after the given time. fn after(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.0.dyn_after(time).into() + self.wrap( + self.0.dyn_after(time), + WireView::Window( + EventTime::start(time.t().saturating_add(1)), + EventTime::end(i64::MAX), + ), + ) } /// Restricts exploded edge evaluation to times strictly before the given time. fn before(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.0.dyn_before(time).into() + self.wrap( + self.0.dyn_before(time), + WireView::Window(EventTime::start(i64::MIN), EventTime::end(time.t())), + ) } /// Evaluates exploded edge predicates against the latest available state. fn latest(&self) -> PyExplodedEdgeFilter { - self.0.dyn_latest().into() + self.wrap(self.0.dyn_latest(), WireView::Latest) } /// Evaluates exploded edge predicates against a snapshot of the graph at a given time. fn snapshot_at(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.0.dyn_snapshot_at(time).into() + self.wrap(self.0.dyn_snapshot_at(time), WireView::SnapshotAt(time)) } /// Evaluates exploded edge predicates against the most recent snapshot of the graph. fn snapshot_latest(&self) -> PyExplodedEdgeFilter { - self.0.dyn_snapshot_latest().into() + self.wrap(self.0.dyn_snapshot_latest(), WireView::SnapshotLatest) } /// Restricts evaluation to exploded edges belonging to the given layer. fn layer(&self, layer: String) -> PyExplodedEdgeFilter { - self.0.dyn_layer(vec![layer]).into() + self.wrap( + self.0.dyn_layer(vec![layer.clone()]), + WireView::Layers(vec![layer]), + ) } /// Restricts evaluation to exploded edges belonging to any of the given layers. fn layers(&self, layers: FromIterable) -> PyExplodedEdgeFilter { - self.0.dyn_layer(layers.to_vec()).into() + let layers = layers.to_vec(); + self.wrap(self.0.dyn_layer(layers.clone()), WireView::Layers(layers)) } /// Matches exploded edges that have at least one event in the current view. fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_active()) + let tree = FilterTree::ExplodedEdge(wrap_exploded_views( + CompositeExplodedEdgeFilter::IsActiveEdge(IsActiveEdge), + &self.1, + )); + PyFilterExpr(self.0.dyn_is_active(), Some(tree)) } /// Matches exploded edges that are structurally valid in the current view. fn is_valid(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_valid()) + let tree = FilterTree::ExplodedEdge(wrap_exploded_views( + CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge), + &self.1, + )); + PyFilterExpr(self.0.dyn_is_valid(), Some(tree)) } /// Matches exploded edges that have been deleted. fn is_deleted(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_deleted()) + let tree = FilterTree::ExplodedEdge(wrap_exploded_views( + CompositeExplodedEdgeFilter::IsDeletedEdge(IsDeletedEdge), + &self.1, + )); + PyFilterExpr(self.0.dyn_is_deleted(), Some(tree)) } /// Matches exploded edges that are self-loops (source == destination). fn is_self_loop(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_self_loop()) + let tree = FilterTree::ExplodedEdge(wrap_exploded_views( + CompositeExplodedEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge), + &self.1, + )); + PyFilterExpr(self.0.dyn_is_self_loop(), Some(tree)) } } diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 237b2f12fd..90f9a3a190 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -26,11 +26,20 @@ use std::sync::Arc; from_py_object )] #[derive(Clone)] -pub struct PyFilterExpr(pub Arc); +pub struct PyFilterExpr(pub Arc, pub Option); impl PyFilterExpr { + /// The wire form recorded at construction; filters built in ways the wire + /// schema cannot express (an expression on both sides of a comparison) + /// carry none and cannot be sent to a server. pub fn try_as_filter_tree(&self) -> Result { - self.0.try_as_filter_tree() + self.1.clone().ok_or_else(|| { + GraphError::InvalidFilter( + "this filter has no server-side form; use plain values rather than \ + expressions on the right-hand side of comparisons" + .to_string(), + ) + }) } pub fn try_as_node_filter(&self) -> Result { @@ -47,17 +56,26 @@ impl PyFilterExpr { pub fn __and__(&self, other: &Self) -> Self { let left = self.0.clone(); let right = other.0.clone(); - PyFilterExpr(Arc::new(AndFilter { left, right })) + let wire = match (&self.1, &other.1) { + (Some(a), Some(b)) => Some(FilterTree::And(vec![a.clone(), b.clone()])), + _ => None, + }; + PyFilterExpr(Arc::new(AndFilter { left, right }), wire) } pub fn __or__(&self, other: &Self) -> Self { let left = self.0.clone(); let right = other.0.clone(); - PyFilterExpr(Arc::new(OrFilter { left, right })) + let wire = match (&self.1, &other.1) { + (Some(a), Some(b)) => Some(FilterTree::Or(vec![a.clone(), b.clone()])), + _ => None, + }; + PyFilterExpr(Arc::new(OrFilter { left, right }), wire) } fn __invert__(&self) -> Self { - PyFilterExpr(Arc::new(NotFilter(self.0.clone()))) + let wire = self.1.clone().map(|t| FilterTree::Not(Box::new(t))); + PyFilterExpr(Arc::new(NotFilter(self.0.clone())), wire) } } diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index 4438198bdc..e1c808338b 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -1,9 +1,12 @@ use crate::{ - db::graph::views::filter::model::{graph_filter::GraphFilter, DynView, ViewWrapOps}, + db::graph::views::filter::model::{ + graph_filter::GraphFilter, DynView, FilterTree, GraphViewOp, ViewWrapOps, + }, + prelude::Layer, python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; -use raphtory_api::core::storage::timeindex::EventTime; +use raphtory_api::core::storage::timeindex::{AsTime, EventTime}; use std::sync::Arc; /// Entry point for constructing **graph-level view filters**. @@ -27,11 +30,17 @@ use std::sync::Arc; extends = PyFilterExpr, frozen )] -pub struct PyGraphFilter(pub(crate) DynView); +pub struct PyGraphFilter(pub(crate) DynView, pub(crate) Vec); impl PyGraphFilter { pub(crate) fn root() -> Self { - PyGraphFilter(Arc::new(GraphFilter)) + PyGraphFilter(Arc::new(GraphFilter), Vec::new()) + } + + fn extend(&self, view: DynView, op: GraphViewOp) -> Self { + let mut ops = self.1.clone(); + ops.push(op); + PyGraphFilter(view, ops) } } @@ -48,7 +57,10 @@ impl PyGraphFilter { /// Returns: /// filter.Graph: fn window(&self, start: EventTime, end: EventTime) -> PyGraphFilter { - PyGraphFilter(self.0.clone().window(start, end)) + self.extend( + self.0.clone().window(start, end), + GraphViewOp::Window { start, end }, + ) } /// Restricts evaluation to a single point in time. @@ -59,7 +71,13 @@ impl PyGraphFilter { /// Returns: /// filter.Graph: fn at(&self, time: EventTime) -> PyGraphFilter { - PyGraphFilter(self.0.clone().at(time)) + self.extend( + self.0.clone().at(time), + GraphViewOp::Window { + start: time, + end: EventTime::end(time.t().saturating_add(1)), + }, + ) } /// Restricts evaluation to times strictly after the given time. @@ -70,7 +88,13 @@ impl PyGraphFilter { /// Returns: /// filter.Graph: fn after(&self, time: EventTime) -> PyGraphFilter { - PyGraphFilter(self.0.clone().after(time)) + self.extend( + self.0.clone().after(time), + GraphViewOp::Window { + start: EventTime::start(time.t().saturating_add(1)), + end: EventTime::end(i64::MAX), + }, + ) } /// Restricts evaluation to times strictly before the given time. @@ -81,7 +105,13 @@ impl PyGraphFilter { /// Returns: /// filter.Graph: fn before(&self, time: EventTime) -> PyGraphFilter { - PyGraphFilter(self.0.clone().before(time)) + self.extend( + self.0.clone().before(time), + GraphViewOp::Window { + start: EventTime::start(i64::MIN), + end: EventTime::end(time.t()), + }, + ) } /// Evaluates filters against the latest available state of the graph. @@ -89,7 +119,7 @@ impl PyGraphFilter { /// Returns: /// filter.Graph: fn latest(&self) -> PyGraphFilter { - PyGraphFilter(Arc::new(self.0.clone().latest())) + self.extend(Arc::new(self.0.clone().latest()), GraphViewOp::Latest) } /// Evaluates filters against a snapshot of the graph at a given time. @@ -100,7 +130,10 @@ impl PyGraphFilter { /// Returns: /// filter.Graph: fn snapshot_at(&self, time: EventTime) -> PyGraphFilter { - PyGraphFilter(Arc::new(self.0.clone().snapshot_at(time))) + self.extend( + Arc::new(self.0.clone().snapshot_at(time)), + GraphViewOp::SnapshotAt(time), + ) } /// Evaluates filters against the most recent snapshot of the graph. @@ -108,7 +141,10 @@ impl PyGraphFilter { /// Returns: /// filter.Graph: fn snapshot_latest(&self) -> PyGraphFilter { - PyGraphFilter(Arc::new(self.0.clone().snapshot_latest())) + self.extend( + Arc::new(self.0.clone().snapshot_latest()), + GraphViewOp::SnapshotLatest, + ) } /// Restricts evaluation to a single layer. @@ -119,7 +155,10 @@ impl PyGraphFilter { /// Returns: /// filter.Graph: fn layer(&self, layer: String) -> PyGraphFilter { - PyGraphFilter(Arc::new(self.0.clone().layer(layer))) + self.extend( + Arc::new(self.0.clone().layer(layer.clone())), + GraphViewOp::Layers(Layer::from(layer)), + ) } /// Restricts evaluation to any of the given layers. @@ -130,7 +169,11 @@ impl PyGraphFilter { /// Returns: /// filter.Graph: fn layers(&self, layers: FromIterable) -> PyGraphFilter { - PyGraphFilter(Arc::new(self.0.clone().layer(layers))) + let names: Vec = layers.into(); + self.extend( + Arc::new(self.0.clone().layer(names.clone())), + GraphViewOp::Layers(Layer::from(names)), + ) } } @@ -140,7 +183,7 @@ impl<'py> IntoPyObject<'py> for PyGraphFilter { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyFilterExpr(self.0.clone()); + let parent = PyFilterExpr(self.0.clone(), Some(FilterTree::View(self.1.clone()))); Bound::new(py, (self, parent)) } } diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 5f0604b56a..bac250ee25 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -19,6 +19,7 @@ pub mod exploded_edge_expr; pub mod filter_expr; pub mod graph_filter; pub mod node_expr; +pub(crate) mod wire; impl From> for Layer { fn from(iter: FromIterable) -> Self { diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 29d4c6cb9d..f801c5b26a 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,14 +1,22 @@ use crate::{ db::graph::views::filter::model::{ + filter::FieldFilterValue, is_active_node_filter::IsActiveNode, node_expr::{CreateOp, DynCreateOp, DynEntityExpr, DynTemporal}, + node_filter::CompositeNodeFilter, node_state_filter::NodeStateBoolColOp, + property_filter::{Op, PropertyFilterValue, PropertyRef}, CombinedFilter, DynCreateFilter, DynCreateView, DynPropertyExprFactory, EntityMarker, - InternalViewWrapOps, NodeViewFilterOps, PropertyExprFactory, ViewWrapOps, + FilterOperator, FilterTree, InternalViewWrapOps, NodeViewFilterOps, PropertyExprFactory, + ViewWrapOps, }, prelude::{EntityAggOps, EntityExprFilterOps, NodeFilter, NodeFilterFactory}, python::{ - filter::filter_expr::PyFilterExpr, graph::node_state::PyOutputNodeState, + filter::{ + filter_expr::PyFilterExpr, + wire::{wrap_node_views, WireEntity, WireLhs, WireTarget, WireValue, WireView}, + }, + graph::node_state::PyOutputNodeState, types::iterable::FromIterable, }, }; @@ -17,19 +25,23 @@ use pyo3::{ PyResult, Python, }; use raphtory_api::core::{ - entities::properties::prop::{Prop, PropType}, - storage::timeindex::EventTime, + entities::{ + properties::prop::{Prop, PropType}, + GID, + }, + storage::timeindex::{AsTime, EventTime}, + Direction, }; use std::sync::Arc; // filter.Node.neighbours.is_active.all #[pyclass(frozen, subclass, name = "Expr", module = "raphtory.filter")] #[derive(Clone)] -pub struct PyExpr(Arc); +pub struct PyExpr(Arc, pub(crate) Option); #[pyclass(frozen, extends = PyExpr, name = "PropertyExpr", module = "raphtory.filter")] #[derive(Clone)] -pub struct PyPropertyExpr(Arc); +pub struct PyPropertyExpr(Arc, pub(crate) Option); impl<'py> IntoPyObject<'py> for PyPropertyExpr { type Target = PyPropertyExpr; @@ -37,7 +49,7 @@ impl<'py> IntoPyObject<'py> for PyPropertyExpr { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyExpr(self.0.clone()); + let parent = PyExpr(self.0.clone(), self.1.clone()); let child = self; Bound::new(py, (child, parent)) } @@ -45,19 +57,13 @@ impl<'py> IntoPyObject<'py> for PyPropertyExpr { impl>> From for PyExpr { fn from(value: E) -> Self { - PyExpr(Arc::new(value)) + PyExpr(Arc::new(value), None) } } impl From> for PyPropertyExpr { fn from(value: Arc) -> Self { - PyPropertyExpr(value) - } -} - -impl From> for PyNodeFilter { - fn from(value: Arc) -> Self { - PyNodeFilter(value) + PyPropertyExpr(value, None) } } @@ -94,100 +100,177 @@ fn check_value(lhs: &Arc, v: &Prop) -> PyResult<()> { Ok(()) } +impl PyExpr { + pub(crate) fn new(op: Arc, wire: Option) -> Self { + PyExpr(op, wire) + } + + /// A value in the shape the recorded lhs target expects on the wire. + fn wire_single(&self, v: &Prop) -> Option { + let lhs = self.1.as_ref()?; + Some(match &lhs.target { + WireTarget::Field("node_id") => WireValue::Field(FieldFilterValue::ID(prop_to_gid(v)?)), + WireTarget::Field(_) => match v { + Prop::Str(s) => WireValue::Field(FieldFilterValue::Single(s.to_string())), + _ => return None, + }, + WireTarget::Prop(_) | WireTarget::Degree(_) => { + WireValue::Prop(PropertyFilterValue::Single(v.clone())) + } + }) + } + + fn wire_set(&self, values: &[Prop]) -> Option { + let lhs = self.1.as_ref()?; + Some(match &lhs.target { + WireTarget::Field("node_id") => WireValue::Field(FieldFilterValue::IDSet(Arc::new( + values.iter().map(prop_to_gid).collect::>()?, + ))), + WireTarget::Field(_) => WireValue::Field(FieldFilterValue::Set(Arc::new( + values + .iter() + .map(|v| match v { + Prop::Str(s) => Some(s.to_string()), + _ => None, + }) + .collect::>()?, + ))), + WireTarget::Prop(_) | WireTarget::Degree(_) => WireValue::Prop( + PropertyFilterValue::Set(Arc::new(values.iter().cloned().collect())), + ), + }) + } + + fn finish(&self, operator: FilterOperator, value: Option) -> Option { + self.1.clone()?.finish(operator, value?) + } + + fn with_op(&self, expr: Arc, op: Op) -> Self { + PyExpr(expr, self.1.clone().map(|w| w.with_op(op))) + } +} + +fn prop_to_gid(v: &Prop) -> Option { + match v { + Prop::Str(s) => Some(GID::Str(s.to_string())), + Prop::U64(n) => Some(GID::U64(*n)), + Prop::I64(n) => u64::try_from(*n).ok().map(GID::U64), + Prop::U32(n) => Some(GID::U64(*n as u64)), + Prop::I32(n) => u64::try_from(*n).ok().map(GID::U64), + _ => None, + } +} + #[pymethods] impl PyExpr { fn __eq__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().eq(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().eq(e.0)), None)), ExprOrValue::Value(v) => { check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().eq(v)))) + let wire = self.finish(FilterOperator::Eq, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().eq(v)), wire)) } } } fn __ne__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ne(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ne(e.0)), None)), ExprOrValue::Value(v) => { check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().ne(v)))) + let wire = self.finish(FilterOperator::Ne, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().ne(v)), wire)) } } } fn __lt__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().lt(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().lt(e.0)), None)), ExprOrValue::Value(v) => { check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().lt(v)))) + let wire = self.finish(FilterOperator::Lt, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().lt(v)), wire)) } } } fn __le__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().le(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().le(e.0)), None)), ExprOrValue::Value(v) => { check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().le(v)))) + let wire = self.finish(FilterOperator::Le, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().le(v)), wire)) } } } fn __gt__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().gt(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().gt(e.0)), None)), ExprOrValue::Value(v) => { check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().gt(v)))) + let wire = self.finish(FilterOperator::Gt, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().gt(v)), wire)) } } } fn __ge__(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ge(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ge(e.0)), None)), ExprOrValue::Value(v) => { check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().ge(v)))) + let wire = self.finish(FilterOperator::Ge, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().ge(v)), wire)) } } } fn starts_with(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().starts_with(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr( + Arc::new(self.0.clone().starts_with(e.0)), + None, + )), ExprOrValue::Value(v) => { check_str_value(&v)?; check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().starts_with(v)))) + let wire = self.finish(FilterOperator::StartsWith, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().starts_with(v)), wire)) } } } fn ends_with(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ends_with(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ends_with(e.0)), None)), ExprOrValue::Value(v) => { check_str_value(&v)?; check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().ends_with(v)))) + let wire = self.finish(FilterOperator::EndsWith, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().ends_with(v)), wire)) } } } fn contains(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().contains(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().contains(e.0)), None)), ExprOrValue::Value(v) => { check_str_value(&v)?; check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().contains(v)))) + let wire = self.finish(FilterOperator::Contains, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().contains(v)), wire)) } } } fn not_contains(&self, other: ExprOrValue) -> PyResult { match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().not_contains(e.0)))), + ExprOrValue::Expr(e) => Ok(PyFilterExpr( + Arc::new(self.0.clone().not_contains(e.0)), + None, + )), ExprOrValue::Value(v) => { check_str_value(&v)?; check_value(&self.0, &v)?; - Ok(PyFilterExpr(Arc::new(self.0.clone().not_contains(v)))) + let wire = self.finish(FilterOperator::NotContains, self.wire_single(&v)); + Ok(PyFilterExpr(Arc::new(self.0.clone().not_contains(v)), wire)) } } } @@ -198,63 +281,94 @@ impl PyExpr { prefix_match: bool, ) -> PyResult { Ok(match other { - ExprOrValue::Expr(e) => PyFilterExpr(Arc::new(self.0.clone().fuzzy_search( - e.0, - levenshtein_distance, - prefix_match, - ))), + ExprOrValue::Expr(e) => PyFilterExpr( + Arc::new( + self.0 + .clone() + .fuzzy_search(e.0, levenshtein_distance, prefix_match), + ), + None, + ), ExprOrValue::Value(v) => { check_str_value(&v)?; - PyFilterExpr(Arc::new(self.0.clone().fuzzy_search( - v, - levenshtein_distance, - prefix_match, - ))) + let wire = self.finish( + FilterOperator::FuzzySearch { + levenshtein_distance, + prefix_match, + }, + self.wire_single(&v), + ); + PyFilterExpr( + Arc::new( + self.0 + .clone() + .fuzzy_search(v, levenshtein_distance, prefix_match), + ), + wire, + ) } }) } fn is_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().is_in(values))) + let values: Vec = values.into(); + let wire = self.finish(FilterOperator::IsIn, self.wire_set(&values)); + PyFilterExpr(Arc::new(self.0.clone().is_in(values)), wire) } fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().is_not_in(values))) + let values: Vec = values.into(); + let wire = self.finish(FilterOperator::IsNotIn, self.wire_set(&values)); + PyFilterExpr(Arc::new(self.0.clone().is_not_in(values)), wire) } fn is_some(&self) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().is_some())) + let wire = self.finish( + FilterOperator::IsSome, + Some(WireValue::Prop(PropertyFilterValue::None)), + ); + PyFilterExpr(Arc::new(self.0.clone().is_some()), wire) } fn is_none(&self) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.clone().is_none())) + let wire = self.finish( + FilterOperator::IsNone, + Some(WireValue::Prop(PropertyFilterValue::None)), + ); + PyFilterExpr(Arc::new(self.0.clone().is_none()), wire) } fn any(&self) -> Self { - self.0.clone().any().into() + self.with_op(Arc::new(self.0.clone().any()), Op::Any) } fn all(&self) -> Self { - self.0.clone().all().into() + self.with_op(Arc::new(self.0.clone().all()), Op::All) } fn sum(&self) -> Self { - self.0.clone().sum().into() + self.with_op(Arc::new(self.0.clone().sum()), Op::Sum) } fn avg(&self) -> Self { - self.0.clone().avg().into() + self.with_op(Arc::new(self.0.clone().avg()), Op::Avg) } fn min(&self) -> Self { - self.0.clone().min().into() + self.with_op(Arc::new(self.0.clone().min()), Op::Min) } fn max(&self) -> Self { - self.0.clone().max().into() + self.with_op(Arc::new(self.0.clone().max()), Op::Max) } fn first(&self) -> Self { - self.0.clone().first().into() + self.with_op(Arc::new(self.0.clone().first()), Op::First) } fn last(&self) -> Self { - self.0.clone().last().into() + self.with_op(Arc::new(self.0.clone().last()), Op::Last) } fn len(&self) -> Self { - self.0.clone().len().into() + self.with_op(Arc::new(self.0.clone().len()), Op::Len) + } +} + +impl PyPropertyExpr { + pub(crate) fn new(expr: Arc, wire: Option) -> Self { + PyPropertyExpr(expr, wire) } } @@ -266,7 +380,10 @@ impl PyPropertyExpr { /// Returns: /// filter.Expr: fn temporal(&self) -> PyExpr { - PyExpr(self.0.temporal()) + PyExpr( + self.0.temporal(), + self.1.clone().and_then(WireLhs::temporal), + ) } } @@ -361,15 +478,27 @@ impl NodeViewFilterOps for Arc { /// - a view-restricted filter context, or /// - a boolean predicate over node state. #[pyclass(frozen, name = "Node", module = "raphtory.filter")] -pub struct PyNodeFilter(Arc); +pub struct PyNodeFilter(Arc, Vec); impl PyNodeFilter { pub(crate) fn root() -> Self { - PyNodeFilter(Arc::new(NodeFilter)) + PyNodeFilter(Arc::new(NodeFilter), Vec::new()) } - fn wrap(filter: T) -> Self { - Self(Arc::new(filter)) + fn wrap(&self, filter: T, view: WireView) -> Self { + let mut views = self.1.clone(); + views.push(view); + Self(Arc::new(filter), views) + } + + fn lhs(&self, target: WireTarget) -> WireLhs { + WireLhs { + entity: WireEntity::Node, + endpoint: None, + target, + ops: Vec::new(), + views: self.1.clone(), + } } } @@ -377,7 +506,7 @@ impl PyNodeFilter { impl PyNodeFilter { #[new] fn new() -> PyNodeFilter { - PyNodeFilter(Arc::new(NodeFilter)) + Self::root() } /// Selects the node ID field for filtering. @@ -385,7 +514,10 @@ impl PyNodeFilter { /// Returns: /// filter.NodeIdFilterBuilder: fn id(&self) -> PyExpr { - self.0.dyn_id().into() + PyExpr( + self.0.dyn_id(), + Some(self.lhs(WireTarget::Field("node_id"))), + ) } /// Selects the node name field for filtering. @@ -393,7 +525,10 @@ impl PyNodeFilter { /// Returns: /// filter.NodeNameFilterBuilder: fn name(&self) -> PyExpr { - self.0.dyn_name().into() + PyExpr( + self.0.dyn_name(), + Some(self.lhs(WireTarget::Field("node_name"))), + ) } /// Selects the node type field for filtering. @@ -401,7 +536,10 @@ impl PyNodeFilter { /// Returns: /// filter.NodeTypeFilterBuilder: fn node_type(&self) -> PyExpr { - self.0.dyn_node_type().into() + PyExpr( + self.0.dyn_node_type(), + Some(self.lhs(WireTarget::Field("node_type"))), + ) } /// Selects incoming node degree for filtering. @@ -409,7 +547,10 @@ impl PyNodeFilter { /// Returns: /// filter.FilterOps fn in_degree(&self) -> PyExpr { - self.0.dyn_in_degree().into() + PyExpr( + self.0.dyn_in_degree(), + Some(self.lhs(WireTarget::Degree(Direction::IN))), + ) } /// Selects total node degree for filtering. @@ -417,7 +558,10 @@ impl PyNodeFilter { /// Returns: /// filter.FilterOps fn degree(&self) -> PyExpr { - self.0.dyn_degree().into() + PyExpr( + self.0.dyn_degree(), + Some(self.lhs(WireTarget::Degree(Direction::BOTH))), + ) } /// Selects outgoing node degree for filtering. @@ -425,7 +569,10 @@ impl PyNodeFilter { /// Returns: /// filter.FilterOps fn out_degree(&self) -> PyExpr { - self.0.dyn_out_degree().into() + PyExpr( + self.0.dyn_out_degree(), + Some(self.lhs(WireTarget::Degree(Direction::OUT))), + ) } /// Filters a node property by name. @@ -438,7 +585,8 @@ impl PyNodeFilter { /// Returns: /// filter.PropertyFilterOps: fn property(&self, name: String) -> PyPropertyExpr { - self.0.dyn_property(name).into() + let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); + PyPropertyExpr(self.0.dyn_property(name), Some(lhs)) } /// Filters a node metadata field by name. @@ -451,7 +599,8 @@ impl PyNodeFilter { /// Returns: /// filter.FilterOps: fn metadata(&self, name: String) -> PyExpr { - self.0.dyn_metadata(name).into() + let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); + PyExpr(self.0.dyn_metadata(name), Some(lhs)) } /// Restricts node evaluation to the given time window. @@ -465,7 +614,10 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn window(&self, start: EventTime, end: EventTime) -> PyNodeFilter { - self.0.clone().window(start, end).into() + self.wrap( + self.0.clone().window(start, end), + WireView::Window(start, end), + ) } /// Restricts node evaluation to a single point in time. @@ -476,7 +628,10 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn at(&self, time: EventTime) -> PyNodeFilter { - self.0.clone().at(time).into() + self.wrap( + self.0.clone().at(time), + WireView::Window(time, EventTime::end(time.t().saturating_add(1))), + ) } /// Restricts node evaluation to times strictly after the given time. @@ -487,7 +642,13 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn after(&self, time: EventTime) -> PyNodeFilter { - self.0.clone().after(time).into() + self.wrap( + self.0.clone().after(time), + WireView::Window( + EventTime::start(time.t().saturating_add(1)), + EventTime::end(i64::MAX), + ), + ) } /// Restricts node evaluation to times strictly before the given time. @@ -498,7 +659,10 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn before(&self, time: EventTime) -> PyNodeFilter { - self.0.clone().before(time).into() + self.wrap( + self.0.clone().before(time), + WireView::Window(EventTime::start(i64::MIN), EventTime::end(time.t())), + ) } /// Evaluates filters against the latest available state of each node. @@ -506,7 +670,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn latest(&self) -> PyNodeFilter { - Self::wrap(self.0.clone().latest()) + self.wrap(self.0.clone().latest(), WireView::Latest) } /// Evaluates filters against a snapshot of the graph at a given time. @@ -517,7 +681,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn snapshot_at(&self, time: EventTime) -> PyNodeFilter { - Self::wrap(self.0.clone().snapshot_at(time)) + self.wrap(self.0.clone().snapshot_at(time), WireView::SnapshotAt(time)) } /// Evaluates filters against the most recent snapshot of the graph. @@ -525,7 +689,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn snapshot_latest(&self) -> PyNodeFilter { - Self::wrap(self.0.clone().snapshot_latest()) + self.wrap(self.0.clone().snapshot_latest(), WireView::SnapshotLatest) } /// Restricts evaluation to nodes belonging to the given layer. @@ -536,7 +700,10 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn layer(&self, layer: String) -> PyNodeFilter { - Self::wrap(self.0.clone().layer(vec![layer])) + self.wrap( + self.0.clone().layer(vec![layer.clone()]), + WireView::Layers(vec![layer]), + ) } /// Restricts evaluation to nodes belonging to any of the given layers. @@ -547,7 +714,11 @@ impl PyNodeFilter { /// Returns: /// filter.NodeViewPropsFilterBuilder: fn layers(&self, layers: FromIterable) -> PyNodeFilter { - Self::wrap(self.0.clone().layer(layers.to_vec())) + let layers = layers.to_vec(); + self.wrap( + self.0.clone().layer(layers.clone()), + WireView::Layers(layers), + ) } /// Matches nodes that have at least one event in the current view. @@ -555,7 +726,11 @@ impl PyNodeFilter { /// Returns: /// filter.FilterExpr: fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(self.0.dyn_is_active()) + let tree = FilterTree::Node(wrap_node_views( + CompositeNodeFilter::IsActiveNode(IsActiveNode), + &self.1, + )); + PyFilterExpr(self.0.dyn_is_active(), Some(tree)) } /// Build a node filter from a boolean column of an existing node-state result. @@ -569,6 +744,6 @@ impl PyNodeFilter { fn by_state_column(&self, state: &PyOutputNodeState, col: String) -> PyResult { let op = NodeStateBoolColOp::new(&state.inner, &col) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; - Ok(PyFilterExpr(Arc::new(op))) + Ok(PyFilterExpr(Arc::new(op), None)) } } diff --git a/raphtory/src/python/filter/wire.rs b/raphtory/src/python/filter/wire.rs new file mode 100644 index 0000000000..f6c3f1a2c3 --- /dev/null +++ b/raphtory/src/python/filter/wire.rs @@ -0,0 +1,209 @@ +//! Wire-side recording for python filter construction. +//! +//! A python filter object does two jobs: run locally (the compiled +//! expression) and travel to a server (the GraphQL wire JSON). Expressions +//! cannot be faithfully reverse-engineered into JSON, so each construction +//! step records its wire fragment alongside the expression it builds. The +//! remote client reads the finished [`FilterTree`]; constructions with no +//! wire equivalent (an expression on both sides of a comparison) simply +//! carry no tree and are rejected at the remote boundary with a clear error. + +use crate::db::graph::views::filter::model::{ + degree_filter::DegreeFilter, + edge_filter::{CompositeEdgeFilter, EdgeFilter, Endpoint}, + exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, + filter::{FieldFilterValue, Filter}, + latest_filter::Latest, + layered_filter::Layered, + node_filter::{CompositeNodeFilter, NodeFilter}, + property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, + snapshot_filter::{SnapshotAt, SnapshotLatest}, + windowed_filter::Windowed, + FilterOperator, FilterTree, +}; +use raphtory_api::core::{storage::timeindex::EventTime, Direction}; + +/// Which entity's tree a finished predicate belongs to. +#[derive(Clone, Copy, Debug)] +pub(crate) enum WireEntity { + Node, + Edge, + ExplodedEdge, +} + +/// A view restriction recorded from a factory chain. +#[derive(Clone, Debug)] +pub(crate) enum WireView { + Window(EventTime, EventTime), + Latest, + SnapshotAt(EventTime), + SnapshotLatest, + Layers(Vec), +} + +/// What the value expression selects, before its predicate. +#[derive(Clone, Debug)] +pub(crate) enum WireTarget { + /// A built-in field (node id / name / type), by its wire field name. + Field(&'static str), + /// A property, metadata or temporal-property column. + Prop(PropertyRef), + /// A node degree in a direction. + Degree(Direction), +} + +/// The recorded lhs of a comparison: entity + target + op chain + views, and +/// an optional endpoint when the chain went through `src()`/`dst()`. +#[derive(Clone, Debug)] +pub(crate) struct WireLhs { + pub entity: WireEntity, + pub endpoint: Option, + pub target: WireTarget, + pub ops: Vec, + pub views: Vec, +} + +impl WireLhs { + pub(crate) fn with_op(mut self, op: Op) -> Self { + self.ops.push(op); + self + } + + /// `.temporal()` switches a property target to its temporal column. + pub(crate) fn temporal(mut self) -> Option { + match self.target { + WireTarget::Prop(PropertyRef::Property(name)) => { + self.target = WireTarget::Prop(PropertyRef::TemporalProperty(name)); + Some(self) + } + _ => None, + } + } + + /// Finish the lhs with a predicate, producing the wire tree. + pub(crate) fn finish(self, operator: FilterOperator, value: WireValue) -> Option { + let node_leaf = |lhs: &WireLhs| -> Option { + Some(match (&lhs.target, &value) { + (WireTarget::Field(name), WireValue::Field(v)) => { + CompositeNodeFilter::Node(Filter { + field_name: name.to_string(), + field_value: v.clone(), + operator: operator.clone(), + }) + } + (WireTarget::Prop(prop_ref), WireValue::Prop(v)) => { + CompositeNodeFilter::Property(PropertyFilter { + prop_ref: prop_ref.clone(), + prop_value: v.clone(), + operator: operator.clone(), + ops: lhs.ops.clone(), + entity: NodeFilter, + }) + } + (WireTarget::Degree(dir), WireValue::Prop(v)) => { + if !lhs.ops.is_empty() { + return None; + } + CompositeNodeFilter::Degree(DegreeFilter { + direction: *dir, + operator: operator.clone(), + value: v.clone(), + ops: Vec::new(), + }) + } + _ => return None, + }) + }; + + match (self.entity, &self.endpoint) { + (WireEntity::Node, Some(_)) => None, + (WireEntity::Node, None) => { + let leaf = node_leaf(&self)?; + Some(FilterTree::Node(wrap_node_views(leaf, &self.views))) + } + (WireEntity::Edge, Some(endpoint)) => { + let leaf = node_leaf(&self)?; + let ep = match endpoint { + Endpoint::Src => CompositeEdgeFilter::Src(leaf), + Endpoint::Dst => CompositeEdgeFilter::Dst(leaf), + }; + Some(FilterTree::Edge(wrap_edge_views(ep, &self.views))) + } + (WireEntity::Edge, None) => { + let leaf = match (&self.target, &value) { + (WireTarget::Prop(prop_ref), WireValue::Prop(v)) => { + CompositeEdgeFilter::Property(PropertyFilter { + prop_ref: prop_ref.clone(), + prop_value: v.clone(), + operator, + ops: self.ops.clone(), + entity: EdgeFilter, + }) + } + _ => return None, + }; + Some(FilterTree::Edge(wrap_edge_views(leaf, &self.views))) + } + (WireEntity::ExplodedEdge, endpoint) => { + let leaf = match (endpoint, &self.target, &value) { + (Some(ep), _, _) => { + let n = node_leaf(&self)?; + match ep { + Endpoint::Src => CompositeExplodedEdgeFilter::Src(n), + Endpoint::Dst => CompositeExplodedEdgeFilter::Dst(n), + } + } + (None, WireTarget::Prop(prop_ref), WireValue::Prop(v)) => { + CompositeExplodedEdgeFilter::Property(PropertyFilter { + prop_ref: prop_ref.clone(), + prop_value: v.clone(), + operator, + ops: self.ops.clone(), + entity: ExplodedEdgeFilter, + }) + } + _ => return None, + }; + Some(FilterTree::ExplodedEdge(wrap_exploded_views( + leaf, + &self.views, + ))) + } + } + } +} + +/// The rhs of a wire predicate. +#[derive(Clone, Debug)] +pub(crate) enum WireValue { + Field(FieldFilterValue), + Prop(PropertyFilterValue), +} + +macro_rules! wrap_views_fn { + ($name:ident, $composite:ident) => { + pub(crate) fn $name(leaf: $composite, views: &[WireView]) -> $composite { + // Views recorded factory-first wrap outside-in: the first view a + // user applied is the outermost restriction. + views.iter().rev().fold(leaf, |acc, view| match view { + WireView::Window(start, end) => { + $composite::Windowed(Box::new(Windowed::new(*start, *end, acc))) + } + WireView::Latest => $composite::Latest(Box::new(Latest::new(acc))), + WireView::SnapshotAt(t) => { + $composite::SnapshotAt(Box::new(SnapshotAt::new(*t, acc))) + } + WireView::SnapshotLatest => { + $composite::SnapshotLatest(Box::new(SnapshotLatest::new(acc))) + } + WireView::Layers(names) => { + $composite::Layered(Box::new(Layered::from_layers(names.clone(), acc))) + } + }) + } + }; +} + +wrap_views_fn!(wrap_node_views, CompositeNodeFilter); +wrap_views_fn!(wrap_edge_views, CompositeEdgeFilter); +wrap_views_fn!(wrap_exploded_views, CompositeExplodedEdgeFilter); From 3c629fcf13aa2f15d6ac08eedd9e908064877b2f Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:39:53 +0100 Subject: [PATCH 111/155] delete the composite filter execution path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composite filters no longer execute: the CreateFilter impls on the composite enums, the GraphQL-to-composite conversions, and the TryAsCompositeFilter export trait are gone. The enums survive as plain data — the transportable wire descriptor inside FilterTree — and the composite-to-GraphQL direction stays for client transport. Server resolvers pass wire filters straight to the expression lowering. Recording view chains at construction had inverted non-commuting chains on the wire (window then latest arrived as latest then window). Both wire builders now nest views in application order, with parity tests covering both orders of window+latest at graph, node and edge scope. --- .../parity/test_parity_filters.py | 19 + .../src/client/graphql_transport.rs | 21 +- raphtory-graphql/src/model/graph/filtering.rs | 775 +++--------------- raphtory-graphql/src/model/graph/graph.rs | 11 +- raphtory-graphql/src/model/graph/node.rs | 19 +- raphtory-graphql/src/model/graph/nodes.rs | 16 +- .../src/model/graph/path_from_graph.rs | 11 +- .../src/model/graph/path_from_node.rs | 11 +- raphtory-tests/src/assertions.rs | 37 +- .../tests/filter_tests/test_filters.rs | 168 +--- .../db/graph/views/filter/model/and_filter.rs | 45 - .../graph/views/filter/model/degree_filter.rs | 18 +- .../graph/views/filter/model/edge_filter.rs | 213 +---- .../filter/model/exploded_edge_filter.rs | 285 +------ .../graph/views/filter/model/graph_filter.rs | 23 +- .../filter/model/is_active_edge_filter.rs | 18 +- .../filter/model/is_active_node_filter.rs | 18 +- .../views/filter/model/is_deleted_filter.rs | 18 +- .../views/filter/model/is_self_loop_filter.rs | 18 +- .../views/filter/model/is_valid_filter.rs | 18 +- .../graph/views/filter/model/latest_filter.rs | 46 +- .../views/filter/model/layered_filter.rs | 46 +- .../src/db/graph/views/filter/model/mod.rs | 79 +- .../views/filter/model/node_expr/filters.rs | 62 +- .../views/filter/model/node_filter/mod.rs | 199 +---- .../views/filter/model/node_state_filter.rs | 17 - .../db/graph/views/filter/model/not_filter.rs | 39 - .../db/graph/views/filter/model/or_filter.rs | 45 - .../views/filter/model/property_filter/mod.rs | 50 +- .../views/filter/model/snapshot_filter.rs | 95 +-- .../views/filter/model/windowed_filter.rs | 49 +- raphtory/src/python/filter/filter_expr.rs | 9 - raphtory/src/python/filter/wire.rs | 7 +- 33 files changed, 223 insertions(+), 2282 deletions(-) diff --git a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index c50ec26a2d..5b7d5c1f82 100644 --- a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py +++ b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py @@ -450,6 +450,11 @@ def _probe_path_from_graph(h): "view.and_mixed": lambda: f.Graph.window(2, 8) & (f.Node.node_type() != "bot") & f.Edge.property("weight").is_some(), + # window+latest does not commute: these two must stay distinct on both + # sides (the wire nests views in application order; a regression here + # inverts the chain remotely while local stays correct). + "view.chain_window_latest": lambda: f.Graph.window(2, 5).latest(), + "view.chain_latest_window": lambda: f.Graph.latest().window(11, 13), } # View scopes attached to a node or edge predicate rather than to the graph: @@ -468,6 +473,13 @@ def _probe_path_from_graph(h): "scoped.node.layers": lambda: f.Node.layers(["knows", "works"]).property("score") > 15, "scoped.node.metadata": lambda: f.Node.window(1, 6).metadata("region") == "eu", + # Non-commuting view chains (see view.chain_window_latest above). + "scoped.node.window_then_latest": ( + lambda: f.Node.window(1, 6).latest().property("score") > 1 + ), + "scoped.node.latest_then_window": ( + lambda: f.Node.latest().window(11, 13).property("score") > 15 + ), "scoped.node.is_active": lambda: f.Node.window(1, 3).is_active(), "scoped.edge.window": lambda: f.Edge.window(2, 5).property("weight") > 2.0, "scoped.edge.at": lambda: f.Edge.at(3).property("weight") > 2.0, @@ -482,6 +494,13 @@ def _probe_path_from_graph(h): "scoped.edge.is_valid": lambda: f.Edge.window(2, 4).is_valid(), "scoped.edge.is_deleted": lambda: f.Edge.window(2, 11).is_deleted(), "scoped.exploded.is_valid": lambda: f.ExplodedEdge.window(2, 4).is_valid(), + # Non-commuting view chains (see view.chain_window_latest above). + "scoped.edge.window_then_latest": ( + lambda: f.Edge.window(2, 6).latest().property("weight") > 2.0 + ), + "scoped.edge.latest_then_window": ( + lambda: f.Edge.latest().window(11, 13).property("weight") > 2.0 + ), } PREDICATE_EXPRS = { diff --git a/raphtory-graphql/src/client/graphql_transport.rs b/raphtory-graphql/src/client/graphql_transport.rs index fc6aabb5f6..75797ee5c1 100644 --- a/raphtory-graphql/src/client/graphql_transport.rs +++ b/raphtory-graphql/src/client/graphql_transport.rs @@ -4950,9 +4950,16 @@ mod tests { #[tokio::test] async fn test_filtered_collect_matches_columnar_reads() { use crate::{client::remote_client::RemoteClient, server::GraphServer}; - use raphtory::db::{ - api::storage::storage::Config, - graph::views::filter::model::node_filter::CompositeNodeFilter, + use raphtory::{ + db::{ + api::storage::storage::Config, + graph::views::filter::model::{ + node_filter::{CompositeNodeFilter, NodeFilter}, + property_filter::{PropertyFilter, PropertyFilterValue, PropertyRef}, + FilterOperator, + }, + }, + prelude::Prop, }; use reqwest::Url; use std::collections::HashMap as Map; @@ -5048,7 +5055,13 @@ mod tests { // select() narrows membership only — handles see the unfiltered graph. // Passed as a composite to pin that kind-typed callers still satisfy // the widened `TryInto` bound. - let score_gt_15_composite = CompositeNodeFilter::try_from(score_gt_15.clone()).unwrap(); + let score_gt_15_composite = CompositeNodeFilter::Property(PropertyFilter { + prop_ref: PropertyRef::Property("score".into()), + prop_value: PropertyFilterValue::Single(Prop::I64(15)), + operator: FilterOperator::Gt, + ops: vec![], + entity: NodeFilter, + }); let selected = rg.nodes().select(score_gt_15_composite).unwrap(); let mut selected_ids = selected.id().await.unwrap(); selected_ids.sort(); diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 3d1b377520..7ede1afe00 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -735,8 +735,10 @@ impl TryFrom for GqlFilter { } } -/// Build the nested wire form of a graph-view chain (outermost-first ops → -/// nested `expr` fields). `Layer::All` ops restrict nothing and are dropped. +/// Build the nested wire form of a graph-view chain: ops arrive in +/// application order and each later op wraps outside the previous one, so the +/// outermost `expr` level is the last op applied. `Layer::All` ops restrict +/// nothing and are dropped. fn view_ops_to_graph_filter(ops: Vec) -> Result { let time_input = |t: EventTime| { GqlTimeInput(raphtory_api::core::utils::time::InputTime::Indexed( @@ -745,7 +747,7 @@ fn view_ops_to_graph_filter(ops: Vec) -> Result = None; - for op in ops.into_iter().rev() { + for op in ops { let expr = acc.take().map(wrap); let next = match op { GraphViewOp::Window { start, end } => GqlGraphFilter::Window(GraphWindowExpr { @@ -1841,584 +1843,6 @@ pub(crate) fn build_property_filter_from_condition_with_entity Result { - use PropCondition::*; - - match cond { - And(list) => { - let mut it = list.iter(); - let first = it - .next() - .ok_or_else(|| GraphError::InvalidGqlFilter("and expects non-empty list".into()))?; - let mut acc = build_node_filter_from_prop_condition(prop_ref.clone(), first)?; - for c in it { - let next = build_node_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeNodeFilter::And(Box::new(acc), Box::new(next)); - } - Ok(acc) - } - Or(list) => { - let mut it = list.iter(); - let first = it - .next() - .ok_or_else(|| GraphError::InvalidGqlFilter("or expects non-empty list".into()))?; - let mut acc = build_node_filter_from_prop_condition(prop_ref.clone(), first)?; - for c in it { - let next = build_node_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeNodeFilter::Or(Box::new(acc), Box::new(next)); - } - Ok(acc) - } - Not(inner) => { - let nf = build_node_filter_from_prop_condition(prop_ref, inner)?; - Ok(CompositeNodeFilter::Not(Box::new(nf))) - } - _ => { - let pf = build_property_filter_from_condition_with_entity::( - prop_ref, cond, NodeFilter, - )?; - Ok(CompositeNodeFilter::Property(pf)) - } - } -} - -/// Translate one built-in-field condition into the engine filter — shared by -/// the per-field variants and the deprecated enum-argument spelling. -fn node_field_filter( - field: NodeField, - where_: &NodeFieldCondition, -) -> Result { - let (field_name, field_value, operator) = translate_node_field_where(field, where_)?; - Ok(CompositeNodeFilter::Node(Filter { - field_name, - field_value, - operator, - })) -} - -impl TryFrom for CompositeNodeFilter { - type Error = GraphError; - fn try_from(filter: GqlNodeFilter) -> Result { - match filter { - GqlNodeFilter::Id(f) => node_field_filter(NodeField::NodeId, &f.where_), - GqlNodeFilter::Name(f) => node_field_filter(NodeField::NodeName, &f.where_), - GqlNodeFilter::NodeType(f) => node_field_filter(NodeField::NodeType, &f.where_), - GqlNodeFilter::Degree(degree) => { - let core_direction: Direction = degree.direction.into(); - - let field_name: String = degree.direction.into(); - - let mut ops = Vec::new(); - let mut cursor = °ree.where_; - while let Some(inner) = peel_prop_wrappers_and_collect_ops(cursor, &mut ops) { - cursor = inner; - } - // Degree is a scalar — aggregation/selector ops (sum/first/…) - // have nothing to operate on, and the core filter rejects them - // at evaluation time. Fail at conversion with a clear message. - if !ops.is_empty() { - return Err(GraphError::InvalidGqlFilter( - "degree filters take a plain comparison; aggregation ops are not supported" - .into(), - )); - } - let (operator, value) = translate_prop_leaf_to_filter(&field_name, cursor)?; - Ok(CompositeNodeFilter::Degree(DegreeFilter { - direction: core_direction, - operator, - value, - ops, - })) - } - GqlNodeFilter::Property(prop) => { - let prop_ref = PropertyRef::Property(prop.name.clone()); - build_node_filter_from_prop_condition(prop_ref, &prop.where_) - } - GqlNodeFilter::Metadata(prop) => { - let prop_ref = PropertyRef::Metadata(prop.name.clone()); - build_node_filter_from_prop_condition(prop_ref, &prop.where_) - } - GqlNodeFilter::TemporalProperty(prop) => { - let prop_ref = PropertyRef::TemporalProperty(prop.name.clone()); - build_node_filter_from_prop_condition(prop_ref, &prop.where_) - } - GqlNodeFilter::And(and_filters) => { - let mut iter = and_filters.into_iter().map(TryInto::try_into); - let first = iter.next().ok_or_else(|| { - GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) - })??; - Ok(iter.try_fold(first, |acc, next| { - let n = next?; - Ok::<_, GraphError>(CompositeNodeFilter::And(Box::new(acc), Box::new(n))) - })?) - } - GqlNodeFilter::Or(or_filters) => { - let mut iter = or_filters.into_iter().map(TryInto::try_into); - let first = iter.next().ok_or_else(|| { - GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) - })??; - Ok(iter.try_fold(first, |acc, next| { - let n = next?; - Ok::<_, GraphError>(CompositeNodeFilter::Or(Box::new(acc), Box::new(n))) - })?) - } - GqlNodeFilter::Not(not_filters) => { - let inner = CompositeNodeFilter::try_from(not_filters.deref().clone())?; - Ok(CompositeNodeFilter::Not(Box::new(inner))) - } - GqlNodeFilter::Window(w) => { - let inner: CompositeNodeFilter = w.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::Windowed(Box::new(Windowed::new( - w.start.into_time(), - w.end.into_time(), - inner, - )))) - } - - GqlNodeFilter::At(t) => { - let inner: CompositeNodeFilter = t.expr.deref().clone().try_into()?; - let et = t.time.into_time(); - Ok(CompositeNodeFilter::Windowed(Box::new(Windowed::new( - et, - EventTime::end(et.t().saturating_add(1)), - inner, - )))) - } - - GqlNodeFilter::Before(t) => { - let inner: CompositeNodeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::Windowed(Box::new(Windowed::new( - EventTime::start(i64::MIN), - EventTime::end(t.time.t()), - inner, - )))) - } - - GqlNodeFilter::After(t) => { - let inner: CompositeNodeFilter = t.expr.deref().clone().try_into()?; - let start = EventTime::start(t.time.t().saturating_add(1)); - Ok(CompositeNodeFilter::Windowed(Box::new(Windowed::new( - start, - EventTime::end(i64::MAX), - inner, - )))) - } - - GqlNodeFilter::Latest(u) => { - let inner: CompositeNodeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::Latest(Box::new(LatestWrap::new( - inner, - )))) - } - - GqlNodeFilter::SnapshotAt(t) => { - let inner: CompositeNodeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::SnapshotAt(Box::new( - SnapshotAtWrap::new(t.time.into_time(), inner), - ))) - } - - GqlNodeFilter::SnapshotLatest(u) => { - let inner: CompositeNodeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::SnapshotLatest(Box::new( - SnapshotLatestWrap::new(inner), - ))) - } - - GqlNodeFilter::Layers(l) => { - let layer = Layer::from(l.names.clone()); - let inner: CompositeNodeFilter = l.expr.deref().clone().try_into()?; - Ok(CompositeNodeFilter::Layered(Box::new(Layered::new( - layer, inner, - )))) - } - - GqlNodeFilter::IsActive(true) => Ok(CompositeNodeFilter::IsActiveNode(IsActiveNode)), - GqlNodeFilter::IsActive(false) => Ok(CompositeNodeFilter::Not(Box::new( - CompositeNodeFilter::IsActiveNode(IsActiveNode), - ))), - } - } -} - -fn build_edge_filter_from_prop_condition( - prop_ref: PropertyRef, - cond: &PropCondition, -) -> Result { - use PropCondition::*; - - match cond { - And(list) => { - let mut it = list.iter(); - let first = it - .next() - .ok_or_else(|| GraphError::InvalidGqlFilter("and expects non-empty list".into()))?; - let mut acc = build_edge_filter_from_prop_condition(prop_ref.clone(), first)?; - for c in it { - let next = build_edge_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeEdgeFilter::And(Box::new(acc), Box::new(next)); - } - Ok(acc) - } - Or(list) => { - let mut it = list.iter(); - let first = it - .next() - .ok_or_else(|| GraphError::InvalidGqlFilter("or expects non-empty list".into()))?; - let mut acc = build_edge_filter_from_prop_condition(prop_ref.clone(), first)?; - for c in it { - let next = build_edge_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeEdgeFilter::Or(Box::new(acc), Box::new(next)); - } - Ok(acc) - } - Not(inner) => { - let ef = build_edge_filter_from_prop_condition(prop_ref, inner)?; - Ok(CompositeEdgeFilter::Not(Box::new(ef))) - } - _ => { - let pf = build_property_filter_from_condition_with_entity::( - prop_ref, cond, EdgeFilter, - )?; - Ok(CompositeEdgeFilter::Property(pf)) - } - } -} - -impl TryFrom for CompositeEdgeFilter { - type Error = GraphError; - fn try_from(filter: GqlEdgeFilter) -> Result { - match filter { - GqlEdgeFilter::Src(nf) => { - let nf: CompositeNodeFilter = nf.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Src(nf)) - } - GqlEdgeFilter::Dst(nf) => { - let nf: CompositeNodeFilter = nf.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Dst(nf)) - } - GqlEdgeFilter::Property(prop) => { - let prop_ref = PropertyRef::Property(prop.name.clone()); - build_edge_filter_from_prop_condition(prop_ref, &prop.where_) - } - GqlEdgeFilter::Metadata(prop) => { - let prop_ref = PropertyRef::Metadata(prop.name.clone()); - build_edge_filter_from_prop_condition(prop_ref, &prop.where_) - } - GqlEdgeFilter::TemporalProperty(prop) => { - let prop_ref = PropertyRef::TemporalProperty(prop.name.clone()); - build_edge_filter_from_prop_condition(prop_ref, &prop.where_) - } - GqlEdgeFilter::And(and_filters) => { - let mut iter = and_filters.into_iter().map(TryInto::try_into); - let first = iter.next().ok_or_else(|| { - GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) - })??; - Ok(iter.try_fold(first, |acc, next| { - let n = next?; - Ok::<_, GraphError>(CompositeEdgeFilter::And(Box::new(acc), Box::new(n))) - })?) - } - GqlEdgeFilter::Or(or_filters) => { - let mut iter = or_filters.into_iter().map(TryInto::try_into); - let first = iter.next().ok_or_else(|| { - GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) - })??; - Ok(iter.try_fold(first, |acc, next| { - let n = next?; - Ok::<_, GraphError>(CompositeEdgeFilter::Or(Box::new(acc), Box::new(n))) - })?) - } - GqlEdgeFilter::Not(not_filters) => { - let inner = CompositeEdgeFilter::try_from(not_filters.deref().clone())?; - Ok(CompositeEdgeFilter::Not(Box::new(inner))) - } - GqlEdgeFilter::Window(w) => { - let inner: CompositeEdgeFilter = w.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Windowed(Box::new(Windowed::new( - w.start.into_time(), - w.end.into_time(), - inner, - )))) - } - - GqlEdgeFilter::At(t) => { - let inner: CompositeEdgeFilter = t.expr.deref().clone().try_into()?; - let et = t.time.into_time(); - Ok(CompositeEdgeFilter::Windowed(Box::new(Windowed::new( - et, - EventTime::end(et.t().saturating_add(1)), - inner, - )))) - } - - GqlEdgeFilter::Before(t) => { - let inner: CompositeEdgeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Windowed(Box::new(Windowed::new( - EventTime::start(i64::MIN), - EventTime::end(t.time.t()), - inner, - )))) - } - - GqlEdgeFilter::After(t) => { - let inner: CompositeEdgeFilter = t.expr.deref().clone().try_into()?; - let start = EventTime::start(t.time.t().saturating_add(1)); - Ok(CompositeEdgeFilter::Windowed(Box::new(Windowed::new( - start, - EventTime::end(i64::MAX), - inner, - )))) - } - - GqlEdgeFilter::Latest(u) => { - let inner: CompositeEdgeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Latest(Box::new(LatestWrap::new( - inner, - )))) - } - - GqlEdgeFilter::SnapshotAt(t) => { - let inner: CompositeEdgeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::SnapshotAt(Box::new( - SnapshotAtWrap::new(t.time.into_time(), inner), - ))) - } - - GqlEdgeFilter::SnapshotLatest(u) => { - let inner: CompositeEdgeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::SnapshotLatest(Box::new( - SnapshotLatestWrap::new(inner), - ))) - } - - GqlEdgeFilter::Layers(l) => { - let layer = Layer::from(l.names.clone()); - let inner: CompositeEdgeFilter = l.expr.deref().clone().try_into()?; - Ok(CompositeEdgeFilter::Layered(Box::new(Layered::new( - layer, inner, - )))) - } - - GqlEdgeFilter::IsActive(true) => Ok(CompositeEdgeFilter::IsActiveEdge(IsActiveEdge)), - GqlEdgeFilter::IsActive(false) => Ok(CompositeEdgeFilter::Not(Box::new( - CompositeEdgeFilter::IsActiveEdge(IsActiveEdge), - ))), - - GqlEdgeFilter::IsValid(true) => Ok(CompositeEdgeFilter::IsValidEdge(IsValidEdge)), - GqlEdgeFilter::IsValid(false) => Ok(CompositeEdgeFilter::Not(Box::new( - CompositeEdgeFilter::IsValidEdge(IsValidEdge), - ))), - - GqlEdgeFilter::IsDeleted(true) => Ok(CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge)), - GqlEdgeFilter::IsDeleted(false) => Ok(CompositeEdgeFilter::Not(Box::new( - CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge), - ))), - - GqlEdgeFilter::IsSelfLoop(true) => { - Ok(CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) - } - GqlEdgeFilter::IsSelfLoop(false) => Ok(CompositeEdgeFilter::Not(Box::new( - CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge), - ))), - } - } -} - -fn build_exploded_edge_filter_from_prop_condition( - prop_ref: PropertyRef, - cond: &PropCondition, -) -> Result { - use PropCondition::*; - - match cond { - And(list) => { - let mut it = list.iter(); - let first = it - .next() - .ok_or_else(|| GraphError::InvalidGqlFilter("and expects non-empty list".into()))?; - let mut acc = build_exploded_edge_filter_from_prop_condition(prop_ref.clone(), first)?; - for c in it { - let next = build_exploded_edge_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeExplodedEdgeFilter::And(Box::new(acc), Box::new(next)); - } - Ok(acc) - } - Or(list) => { - let mut it = list.iter(); - let first = it - .next() - .ok_or_else(|| GraphError::InvalidGqlFilter("or expects non-empty list".into()))?; - let mut acc = build_exploded_edge_filter_from_prop_condition(prop_ref.clone(), first)?; - for c in it { - let next = build_exploded_edge_filter_from_prop_condition(prop_ref.clone(), c)?; - acc = CompositeExplodedEdgeFilter::Or(Box::new(acc), Box::new(next)); - } - Ok(acc) - } - Not(inner) => { - let ef = build_exploded_edge_filter_from_prop_condition(prop_ref, inner)?; - Ok(CompositeExplodedEdgeFilter::Not(Box::new(ef))) - } - _ => { - let pf = build_property_filter_from_condition_with_entity::( - prop_ref, - cond, - ExplodedEdgeFilter, - )?; - Ok(CompositeExplodedEdgeFilter::Property(pf)) - } - } -} - -impl TryFrom for CompositeExplodedEdgeFilter { - type Error = GraphError; - fn try_from(filter: GqlExplodedEdgeFilter) -> Result { - match filter { - GqlExplodedEdgeFilter::Src(nf) => { - let nf: CompositeNodeFilter = nf.deref().clone().try_into()?; - Ok(CompositeExplodedEdgeFilter::Src(nf)) - } - GqlExplodedEdgeFilter::Dst(nf) => { - let nf: CompositeNodeFilter = nf.deref().clone().try_into()?; - Ok(CompositeExplodedEdgeFilter::Dst(nf)) - } - GqlExplodedEdgeFilter::Property(prop) => { - let prop_ref = PropertyRef::Property(prop.name.clone()); - build_exploded_edge_filter_from_prop_condition(prop_ref, &prop.where_) - } - GqlExplodedEdgeFilter::Metadata(prop) => { - let prop_ref = PropertyRef::Metadata(prop.name.clone()); - build_exploded_edge_filter_from_prop_condition(prop_ref, &prop.where_) - } - GqlExplodedEdgeFilter::TemporalProperty(prop) => { - let prop_ref = PropertyRef::TemporalProperty(prop.name.clone()); - build_exploded_edge_filter_from_prop_condition(prop_ref, &prop.where_) - } - GqlExplodedEdgeFilter::And(and_filters) => { - let mut iter = and_filters.into_iter().map(TryInto::try_into); - let first = iter.next().ok_or_else(|| { - GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) - })??; - Ok(iter.try_fold(first, |acc, next| { - let n = next?; - Ok::<_, GraphError>(CompositeExplodedEdgeFilter::And( - Box::new(acc), - Box::new(n), - )) - })?) - } - GqlExplodedEdgeFilter::Or(or_filters) => { - let mut iter = or_filters.into_iter().map(TryInto::try_into); - let first = iter.next().ok_or_else(|| { - GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) - })??; - Ok(iter.try_fold(first, |acc, next| { - let n = next?; - Ok::<_, GraphError>(CompositeExplodedEdgeFilter::Or(Box::new(acc), Box::new(n))) - })?) - } - GqlExplodedEdgeFilter::Not(not_filters) => { - let inner = CompositeExplodedEdgeFilter::try_from(not_filters.deref().clone())?; - Ok(CompositeExplodedEdgeFilter::Not(Box::new(inner))) - } - GqlExplodedEdgeFilter::Window(w) => { - let inner: CompositeExplodedEdgeFilter = w.expr.deref().clone().try_into()?; - Ok(CompositeExplodedEdgeFilter::Windowed(Box::new( - Windowed::new(w.start.into_time(), w.end.into_time(), inner), - ))) - } - - GqlExplodedEdgeFilter::At(t) => { - let inner: CompositeExplodedEdgeFilter = t.expr.deref().clone().try_into()?; - let et = t.time.into_time(); - Ok(CompositeExplodedEdgeFilter::Windowed(Box::new( - Windowed::new(et, EventTime::end(et.t().saturating_add(1)), inner), - ))) - } - - GqlExplodedEdgeFilter::Before(t) => { - let inner: CompositeExplodedEdgeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeExplodedEdgeFilter::Windowed(Box::new( - Windowed::new( - EventTime::start(i64::MIN), - EventTime::end(t.time.t()), - inner, - ), - ))) - } - - GqlExplodedEdgeFilter::After(t) => { - let inner: CompositeExplodedEdgeFilter = t.expr.deref().clone().try_into()?; - let start = EventTime::start(t.time.t().saturating_add(1)); - Ok(CompositeExplodedEdgeFilter::Windowed(Box::new( - Windowed::new(start, EventTime::end(i64::MAX), inner), - ))) - } - - GqlExplodedEdgeFilter::Latest(u) => { - let inner: CompositeExplodedEdgeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeExplodedEdgeFilter::Latest(Box::new( - LatestWrap::new(inner), - ))) - } - - GqlExplodedEdgeFilter::SnapshotAt(t) => { - let inner: CompositeExplodedEdgeFilter = t.expr.deref().clone().try_into()?; - Ok(CompositeExplodedEdgeFilter::SnapshotAt(Box::new( - SnapshotAtWrap::new(t.time.into_time(), inner), - ))) - } - - GqlExplodedEdgeFilter::SnapshotLatest(u) => { - let inner: CompositeExplodedEdgeFilter = u.expr.deref().clone().try_into()?; - Ok(CompositeExplodedEdgeFilter::SnapshotLatest(Box::new( - SnapshotLatestWrap::new(inner), - ))) - } - - GqlExplodedEdgeFilter::Layers(l) => { - let layer = Layer::from(l.names.clone()); - let inner: CompositeExplodedEdgeFilter = l.expr.deref().clone().try_into()?; - Ok(CompositeExplodedEdgeFilter::Layered(Box::new( - Layered::new(layer, inner), - ))) - } - - GqlExplodedEdgeFilter::IsActive(true) => { - Ok(CompositeExplodedEdgeFilter::IsActiveEdge(IsActiveEdge)) - } - GqlExplodedEdgeFilter::IsActive(false) => Ok(CompositeExplodedEdgeFilter::Not( - Box::new(CompositeExplodedEdgeFilter::IsActiveEdge(IsActiveEdge)), - )), - - GqlExplodedEdgeFilter::IsValid(true) => { - Ok(CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge)) - } - GqlExplodedEdgeFilter::IsValid(false) => Ok(CompositeExplodedEdgeFilter::Not( - Box::new(CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge)), - )), - - GqlExplodedEdgeFilter::IsDeleted(true) => { - Ok(CompositeExplodedEdgeFilter::IsDeletedEdge(IsDeletedEdge)) - } - GqlExplodedEdgeFilter::IsDeleted(false) => Ok(CompositeExplodedEdgeFilter::Not( - Box::new(CompositeExplodedEdgeFilter::IsDeletedEdge(IsDeletedEdge)), - )), - - GqlExplodedEdgeFilter::IsSelfLoop(true) => { - Ok(CompositeExplodedEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) - } - GqlExplodedEdgeFilter::IsSelfLoop(false) => Ok(CompositeExplodedEdgeFilter::Not( - Box::new(CompositeExplodedEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)), - )), - } - } -} - impl TryFrom for DynView { type Error = GraphError; @@ -3241,29 +2665,32 @@ mod conversion_hole_tests { } // Node-id ordering comparisons accept string GIDs, matching the local - // builder's `V: Into` bound. + // expression api's `V: Into` bound. #[test] fn node_id_ordering_accepts_string_gids() { let filter = GqlNodeFilter::Id(NodeFieldWhere { where_: NodeFieldCondition::Gt(Value::Str("m".into())), }); - assert!(CompositeNodeFilter::try_from(filter).is_ok()); + assert!(super::super::expr_lowering::lower_node_filter(&filter).is_ok()); } - // Aggregation ops on a degree filter fail at conversion time with a clear - // message (previously they slipped through and failed at evaluation). + // Aggregation ops on a degree filter fail with a clear message — degree is + // a scalar, so an op chain over it is meaningless. #[test] - fn degree_rejects_aggregation_ops_at_conversion() { + fn degree_rejects_aggregation_ops() { + use raphtory::{db::api::view::Filter as _, prelude::Graph}; + let filter = GqlNodeFilter::Degree(DegreeFilterNew { direction: DegreeDirection::Both, where_: PropCondition::Sum(wrap(PropCondition::Eq(Value::I64(3)))), }); - let Err(err) = CompositeNodeFilter::try_from(filter) else { - panic!("degree with an op chain must be rejected at conversion"); + let result = super::super::expr_lowering::lower_node_filter(&filter) + .and_then(|f| Graph::new().filter(f).map(|_| ())); + let Err(err) = result else { + panic!("degree with an op chain must be rejected"); }; assert!( - err.to_string() - .contains("aggregation ops are not supported"), + err.to_string().contains("is not valid on a scalar"), "unexpected error: {err}" ); } @@ -3291,10 +2718,13 @@ mod conversion_hole_tests { #[cfg(test)] mod exploded_edge_filter_tests { use super::*; - use raphtory::db::graph::views::filter::model::{ - property_filter::ops::PropertyFilterOps, ComposableFilter, PropertyFilterFactory, - TryAsCompositeFilter, ViewWrapOps, + use raphtory::{ + db::graph::views::filter::model::{ + layered_filter::Layered, windowed_filter::Windowed, FilterOperator, + }, + prelude::Prop, }; + use raphtory_api::core::storage::timeindex::EventTime; fn exploded_prop_gt(name: &str, v: i64) -> GqlExplodedEdgeFilter { GqlExplodedEdgeFilter::Property(PropertyFilterNew { @@ -3352,33 +2782,37 @@ mod exploded_edge_filter_tests { // Python builder can produce (property/metadata/temporal, view wrappers, // combinators, predicates, endpoints). #[test] - fn composite_round_trips_through_the_wire() { + fn composite_wire_forms_lower_onto_expressions() { + let pf = |prop_ref, value, operator| PropertyFilter { + prop_ref, + prop_value: PropertyFilterValue::Single(value), + operator, + ops: vec![], + entity: ExplodedEdgeFilter, + }; let prop = || { - ExplodedEdgeFilter - .property("w") - .gt(1i64) - .try_as_composite_exploded_edge_filter() - .unwrap() + CompositeExplodedEdgeFilter::Property(pf( + PropertyRef::Property("w".into()), + Prop::I64(1), + FilterOperator::Gt, + )) }; let cases = vec![ prop(), - ExplodedEdgeFilter - .metadata("kind") - .eq("strong") - .try_as_composite_exploded_edge_filter() - .unwrap(), - ExplodedEdgeFilter - .window(2i64, 4i64) - .property("w") - .gt(1i64) - .try_as_composite_exploded_edge_filter() - .unwrap(), - ExplodedEdgeFilter - .layer("knows") - .property("w") - .gt(1i64) - .try_as_composite_exploded_edge_filter() - .unwrap(), + CompositeExplodedEdgeFilter::Property(pf( + PropertyRef::Metadata("kind".into()), + Prop::str("strong"), + FilterOperator::Eq, + )), + CompositeExplodedEdgeFilter::Windowed(Box::new(Windowed::new( + EventTime::start(2), + EventTime::end(4), + prop(), + ))), + CompositeExplodedEdgeFilter::Layered(Box::new(Layered::from_layers( + vec!["knows".to_string()], + prop(), + ))), CompositeExplodedEdgeFilter::And( Box::new(prop()), Box::new(CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge)), @@ -3391,8 +2825,10 @@ mod exploded_edge_filter_tests { ]; for original in cases { let gql = GqlExplodedEdgeFilter::try_from(original.clone()).unwrap(); - let back = CompositeExplodedEdgeFilter::try_from(gql).unwrap(); - assert_eq!(original, back, "round-trip changed the filter"); + assert!( + super::super::expr_lowering::lower_exploded_edge_filter(&gql).is_ok(), + "wire form no longer lowers: {original}" + ); } } @@ -3408,23 +2844,30 @@ mod exploded_edge_filter_tests { // transport path. #[test] fn filter_tree_converts_to_the_wire_variant() { - let tree = ExplodedEdgeFilter - .property("w") - .gt(1i64) - .try_as_filter_tree() - .unwrap(); - let gql = GqlFilter::try_from(tree).unwrap(); + let prop_tree = || { + FilterTree::ExplodedEdge(CompositeExplodedEdgeFilter::Property(PropertyFilter { + prop_ref: PropertyRef::Property("w".into()), + prop_value: PropertyFilterValue::Single(Prop::I64(1)), + operator: FilterOperator::Gt, + ops: vec![], + entity: ExplodedEdgeFilter, + })) + }; + let gql = GqlFilter::try_from(prop_tree()).unwrap(); assert!( matches!(gql, GqlFilter::ExplodedEdge(_)), "expected ExplodedEdges, got {gql:?}" ); // A mixed node∧exploded tree keeps both kinds through the conversion. - let n = raphtory::db::graph::views::filter::model::node_filter::NodeFilter - .property("x") - .eq(1i64); - let x = ExplodedEdgeFilter.property("w").gt(1i64); - let tree = n.and(x).try_as_filter_tree().unwrap(); + let n = FilterTree::Node(CompositeNodeFilter::Property(PropertyFilter { + prop_ref: PropertyRef::Property("x".into()), + prop_value: PropertyFilterValue::Single(Prop::I64(1)), + operator: FilterOperator::Eq, + ops: vec![], + entity: raphtory::db::graph::views::filter::model::node_filter::NodeFilter, + })); + let tree = FilterTree::And(vec![n, prop_tree()]); let gql = GqlFilter::try_from(tree).unwrap(); let GqlFilter::And(items) = gql else { panic!("expected GqlFilter::And"); @@ -3440,7 +2883,7 @@ mod exploded_edge_filter_tests { ("and", GqlExplodedEdgeFilter::And(vec![])), ("or", GqlExplodedEdgeFilter::Or(vec![])), ] { - let Err(err) = CompositeExplodedEdgeFilter::try_from(filter) else { + let Err(err) = super::super::expr_lowering::lower_exploded_edge_filter(&filter) else { panic!("ExplodedEdgeFilter {name}: empty combinator must be rejected"); }; assert!( @@ -3454,33 +2897,33 @@ mod exploded_edge_filter_tests { #[cfg(test)] mod filter_tree_tests { use super::*; - use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter as EdgeFilterBuilder, graph_filter::GraphFilter, - node_filter::NodeFilter as NodeFilterBuilder, property_filter::ops::PropertyFilterOps, - ComposableFilter, PropertyFilterFactory, TryAsCompositeFilter, ViewWrapOps, + use raphtory::{ + db::graph::views::filter::model::{node_filter::NodeFilter, FilterOperator}, + prelude::{EdgeFilter, Layer, Prop}, }; + use raphtory_api::core::storage::timeindex::EventTime; - // A same-kind combination stays in composite form — no structural tree. - #[test] - fn same_kind_and_exports_as_a_composite() { - let a = NodeFilterBuilder.property("x").eq(1i64); - let b = NodeFilterBuilder.property("y").eq(2i64); - let tree = a.and(b).try_as_filter_tree().unwrap(); - assert!(matches!(tree, FilterTree::Node(_))); + fn node_prop_eq(name: &str, v: i64) -> FilterTree { + FilterTree::Node(CompositeNodeFilter::Property(PropertyFilter { + prop_ref: PropertyRef::Property(name.into()), + prop_value: PropertyFilterValue::Single(Prop::I64(v)), + operator: FilterOperator::Eq, + ops: vec![], + entity: NodeFilter, + })) } - // A mixed node∧edge combination exports structurally and converts to the - // wire form — the case the single-kind exports cannot represent. + // A mixed node∧edge tree converts arm by arm to the wire form. #[test] - fn mixed_and_exports_structurally_and_converts() { - let n = NodeFilterBuilder.property("x").eq(1i64); - let e = EdgeFilterBuilder.property("w").eq(2i64); - let tree = n.and(e).try_as_filter_tree().unwrap(); - let FilterTree::And(ref items) = tree else { - panic!("expected structural And, got {tree:?}"); - }; - assert!(matches!(items[0], FilterTree::Node(_))); - assert!(matches!(items[1], FilterTree::Edge(_))); + fn mixed_and_converts_structurally() { + let edge = FilterTree::Edge(CompositeEdgeFilter::Property(PropertyFilter { + prop_ref: PropertyRef::Property("w".into()), + prop_value: PropertyFilterValue::Single(Prop::I64(2)), + operator: FilterOperator::Eq, + ops: vec![], + entity: EdgeFilter, + })); + let tree = FilterTree::And(vec![node_prop_eq("x", 1), edge]); let gql = GqlFilter::try_from(tree).unwrap(); let GqlFilter::And(items) = gql else { @@ -3490,23 +2933,25 @@ mod filter_tree_tests { assert!(matches!(items[1], GqlFilter::Edge(_))); } - // A graph-view chain exports outermost-first and converts to the nested - // wire form. + // A view chain nests into the wire form in application order: the last op + // applied becomes the outermost `expr` level. #[test] - fn graph_view_chain_exports_and_converts() { - let f = GraphFilter.window(1i64, 5i64).layer("x"); - let tree = f.try_as_filter_tree().unwrap(); - let FilterTree::View(ref ops) = tree else { - panic!("expected View chain, got {tree:?}"); - }; - assert!(matches!(ops[0], GraphViewOp::Layers(_))); - assert!(matches!(ops[1], GraphViewOp::Window { .. })); + fn graph_view_chain_converts_to_the_nested_wire_form() { + let tree = FilterTree::View(vec![ + GraphViewOp::Layers(Layer::from("x")), + GraphViewOp::Window { + start: EventTime::start(1), + end: EventTime::end(5), + }, + ]); let gql = GqlFilter::try_from(tree).unwrap(); - let GqlFilter::Graph(GqlGraphFilter::Layers(ref l)) = gql else { - panic!("expected Graph(Layers), got {gql:?}"); + let GqlFilter::Graph(GqlGraphFilter::Window(ref w)) = gql else { + panic!("expected Graph(Window), got {gql:?}"); + }; + let Some(GqlGraphFilter::Layers(ref l)) = w.expr.as_deref() else { + panic!("expected inner Layers, got {:?}", w.expr); }; assert_eq!(l.names, vec!["x"]); - assert!(matches!(l.expr.as_deref(), Some(GqlGraphFilter::Window(_)))); } } diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index 80c7d70f4a..0959574b0b 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -33,12 +33,7 @@ use raphtory::{ filter_ops::Select, DynamicGraph, Filter, IntoDynamic, NodeViewOps, StaticGraphViewOps, TimeOps, }, - graph::{ - node::NodeView, - views::filter::model::{ - edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, DynFilter, - }, - }, + graph::{node::NodeView, views::filter::model::DynFilter}, }, errors::GraphError, prelude::*, @@ -547,7 +542,7 @@ impl GqlGraph { let nn = self.graph.nodes(); if let Some(sel) = select { - let nf: CompositeNodeFilter = sel.try_into()?; + let nf = GqlFilter::Node(sel); let narrowed = blocking_compute({ let nn_clone = nn.clone(); move || nn_clone.select(nf) @@ -581,7 +576,7 @@ impl GqlGraph { let base = self.graph.edges_unlocked(); if let Some(sel) = select { - let ef: CompositeEdgeFilter = sel.try_into()?; + let ef = GqlFilter::Edge(sel); let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlEdges::new(narrowed)); } diff --git a/raphtory-graphql/src/model/graph/node.rs b/raphtory-graphql/src/model/graph/node.rs index 482629350f..5e0b297cd9 100644 --- a/raphtory-graphql/src/model/graph/node.rs +++ b/raphtory-graphql/src/model/graph/node.rs @@ -24,12 +24,7 @@ use raphtory::{ properties::dyn_props::DynProperties, view::{filter_ops::Select, Filter, *}, }, - graph::{ - node::NodeView, - views::filter::model::{ - edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, DynFilter, - }, - }, + graph::{node::NodeView, views::filter::model::DynFilter}, }, errors::GraphError, prelude::NodeStateOps, @@ -458,7 +453,7 @@ impl GqlNode { pub async fn edges(&self, select: Option) -> Result { let base = self.vv.edges(); if let Some(sel) = select { - let ef: CompositeEdgeFilter = sel.try_into()?; + let ef = GqlFilter::Edge(sel); let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlEdges::new(narrowed)); } @@ -469,7 +464,7 @@ impl GqlNode { pub async fn out_edges(&self, select: Option) -> Result { let base = self.vv.out_edges(); if let Some(sel) = select { - let ef: CompositeEdgeFilter = sel.try_into()?; + let ef = GqlFilter::Edge(sel); let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlEdges::new(narrowed)); } @@ -480,7 +475,7 @@ impl GqlNode { pub async fn in_edges(&self, select: Option) -> Result { let base = self.vv.in_edges(); if let Some(sel) = select { - let ef: CompositeEdgeFilter = sel.try_into()?; + let ef = GqlFilter::Edge(sel); let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlEdges::new(narrowed)); } @@ -494,7 +489,7 @@ impl GqlNode { ) -> Result { let base = self.vv.neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromNode::new(narrowed)); } @@ -508,7 +503,7 @@ impl GqlNode { ) -> Result { let base = self.vv.in_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromNode::new(narrowed)); } @@ -522,7 +517,7 @@ impl GqlNode { ) -> Result { let base = self.vv.out_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromNode::new(narrowed)); } diff --git a/raphtory-graphql/src/model/graph/nodes.rs b/raphtory-graphql/src/model/graph/nodes.rs index fe08ec602b..b6b66533ce 100644 --- a/raphtory-graphql/src/model/graph/nodes.rs +++ b/raphtory-graphql/src/model/graph/nodes.rs @@ -25,9 +25,7 @@ use raphtory::{ }, graph::{ nodes::{IntoDynNodes, Nodes}, - views::filter::model::{ - edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, DynFilter, - }, + views::filter::model::DynFilter, }, }, errors::GraphError, @@ -578,7 +576,7 @@ impl GqlNodes { ) -> Result { let base = self.nn.neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } @@ -592,7 +590,7 @@ impl GqlNodes { ) -> Result { let base = self.nn.in_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } @@ -606,7 +604,7 @@ impl GqlNodes { ) -> Result { let base = self.nn.out_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } @@ -617,7 +615,7 @@ impl GqlNodes { pub async fn edges(&self, select: Option) -> Result { let base = self.nn.edges(); if let Some(expr) = select { - let ef: CompositeEdgeFilter = expr.try_into()?; + let ef = GqlFilter::Edge(expr); let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlNestedEdges::new(narrowed)); } @@ -631,7 +629,7 @@ impl GqlNodes { ) -> Result { let base = self.nn.in_edges(); if let Some(expr) = select { - let ef: CompositeEdgeFilter = expr.try_into()?; + let ef = GqlFilter::Edge(expr); let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlNestedEdges::new(narrowed)); } @@ -645,7 +643,7 @@ impl GqlNodes { ) -> Result { let base = self.nn.out_edges(); if let Some(expr) = select { - let ef: CompositeEdgeFilter = expr.try_into()?; + let ef = GqlFilter::Edge(expr); let narrowed = blocking_compute(move || base.select(ef)).await?; return Ok(GqlNestedEdges::new(narrowed)); } diff --git a/raphtory-graphql/src/model/graph/path_from_graph.rs b/raphtory-graphql/src/model/graph/path_from_graph.rs index 55c2d6e02c..e9559fb1ed 100644 --- a/raphtory-graphql/src/model/graph/path_from_graph.rs +++ b/raphtory-graphql/src/model/graph/path_from_graph.rs @@ -14,10 +14,7 @@ use dynamic_graphql::{ResolvedObject, ResolvedObjectFields, Scalar, ScalarValue} use raphtory::{ db::{ api::view::{filter_ops::Select, DynamicGraph, Filter}, - graph::{ - path::PathFromGraph, - views::filter::model::{CompositeNodeFilter, DynFilter}, - }, + graph::{path::PathFromGraph, views::filter::model::DynFilter}, }, errors::GraphError, prelude::*, @@ -546,7 +543,7 @@ impl GqlPathFromGraph { pub async fn neighbours(&self, select: Option) -> Result { let base = self.nn.neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } @@ -558,7 +555,7 @@ impl GqlPathFromGraph { pub async fn in_neighbours(&self, select: Option) -> Result { let base = self.nn.in_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } @@ -570,7 +567,7 @@ impl GqlPathFromGraph { pub async fn out_neighbours(&self, select: Option) -> Result { let base = self.nn.out_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } diff --git a/raphtory-graphql/src/model/graph/path_from_node.rs b/raphtory-graphql/src/model/graph/path_from_node.rs index 4cd62463af..dc4ae62860 100644 --- a/raphtory-graphql/src/model/graph/path_from_node.rs +++ b/raphtory-graphql/src/model/graph/path_from_node.rs @@ -17,10 +17,7 @@ use raphtory::{ core::utils::time::TryIntoInterval, db::{ api::view::{filter_ops::Select, DynamicGraph, Filter}, - graph::{ - path::PathFromNode, - views::filter::model::{CompositeNodeFilter, DynFilter}, - }, + graph::{path::PathFromNode, views::filter::model::DynFilter}, }, errors::GraphError, prelude::*, @@ -535,7 +532,7 @@ impl GqlPathFromNode { ) -> Result { let base = self.nn.neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromNode::new(narrowed)); } @@ -550,7 +547,7 @@ impl GqlPathFromNode { ) -> Result { let base = self.nn.in_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromNode::new(narrowed)); } @@ -565,7 +562,7 @@ impl GqlPathFromNode { ) -> Result { let base = self.nn.out_neighbours(); if let Some(expr) = select { - let nf: CompositeNodeFilter = expr.try_into()?; + let nf = GqlFilter::Node(expr); let narrowed = blocking_compute(move || base.select(nf)).await?; return Ok(GqlPathFromNode::new(narrowed)); } diff --git a/raphtory-tests/src/assertions.rs b/raphtory-tests/src/assertions.rs index aea98532f1..c504a81c47 100644 --- a/raphtory-tests/src/assertions.rs +++ b/raphtory-tests/src/assertions.rs @@ -4,10 +4,7 @@ use raphtory::{ filter_ops::{Filter, Select}, StaticGraphViewOps, }, - graph::views::{ - filter::{model::TryAsCompositeFilter, CreateFilter}, - window_graph::WindowedGraph, - }, + graph::views::{filter::CreateFilter, window_graph::WindowedGraph}, }, errors::GraphError, prelude::{EdgeViewOps, Graph, GraphViewOps, NodeViewOps, TimeOps}, @@ -63,9 +60,9 @@ pub trait ApplyFilter { fn apply(&self, graph: G) -> Vec; } -pub struct FilterNodes(F); +pub struct FilterNodes(F); -impl ApplyFilter for FilterNodes { +impl ApplyFilter for FilterNodes { fn apply(&self, graph: G) -> Vec { let mut results = graph .filter(self.0.clone()) @@ -79,9 +76,9 @@ impl ApplyFilter for FilterNodes } } -pub struct SelectNodes(F); +pub struct SelectNodes(F); -impl ApplyFilter for SelectNodes { +impl ApplyFilter for SelectNodes { fn apply(&self, graph: G) -> Vec { let mut results = graph .nodes() @@ -95,9 +92,9 @@ impl ApplyFilter for SelectNodes } } -pub struct FilterNeighbours(F, String, Direction); +pub struct FilterNeighbours(F, String, Direction); -impl ApplyFilter for FilterNeighbours { +impl ApplyFilter for FilterNeighbours { fn apply(&self, graph: G) -> Vec { let filter_applied = graph .node(self.1.clone()) @@ -118,9 +115,9 @@ impl ApplyFilter for FilterNeigh } } -pub struct FilterEdges(F); +pub struct FilterEdges(F); -impl ApplyFilter for FilterEdges { +impl ApplyFilter for FilterEdges { fn apply(&self, graph: G) -> Vec { let mut results = graph .filter(self.0.clone()) @@ -134,9 +131,9 @@ impl ApplyFilter for FilterEdges } } -pub struct SelectEdges(F); +pub struct SelectEdges(F); -impl ApplyFilter for SelectEdges { +impl ApplyFilter for SelectEdges { fn apply(&self, graph: G) -> Vec { let mut results = graph .edges() @@ -154,7 +151,7 @@ impl ApplyFilter for SelectEdges pub fn assert_filter_nodes_results( init_graph: impl FnOnce(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { @@ -171,7 +168,7 @@ pub fn assert_filter_nodes_results( pub fn assert_select_nodes_results( init_graph: impl FnOnce(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { @@ -206,7 +203,7 @@ where pub fn assert_filter_nodes_err( init_graph: fn(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &str, variants: impl Into>, ) { @@ -238,7 +235,7 @@ pub fn assert_filter_neighbours_results( transform: impl GraphTransformer, node_name: impl AsRef, direction: Direction, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { @@ -256,7 +253,7 @@ pub fn assert_filter_neighbours_results( pub fn assert_filter_edges_results( init_graph: impl FnOnce(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { @@ -274,7 +271,7 @@ pub fn assert_filter_edges_results( pub fn assert_select_edges_results( init_graph: impl FnOnce(Graph) -> Graph, transform: impl GraphTransformer, - filter: impl TryAsCompositeFilter + CreateFilter + Clone, + filter: impl CreateFilter + Clone, expected: &[&str], variants: impl Into>, ) { diff --git a/raphtory-tests/tests/filter_tests/test_filters.rs b/raphtory-tests/tests/filter_tests/test_filters.rs index 20de781032..af0023a702 100644 --- a/raphtory-tests/tests/filter_tests/test_filters.rs +++ b/raphtory-tests/tests/filter_tests/test_filters.rs @@ -1643,8 +1643,7 @@ mod test_node_filter { degree_filter::DegreeFilterFactory, node_filter::ops::{NodeFilterOps, NodeIdFilterOps}, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - ComposableFilter, CompositeNodeFilter, NodeViewFilterOps, TryAsCompositeFilter, - ViewWrapOps, + ComposableFilter, CompositeNodeFilter, NodeViewFilterOps, ViewWrapOps, }, CreateFilter, }, @@ -1687,7 +1686,7 @@ mod test_node_filter { manual_expr: F, context: &str, ) where - CF: CreateFilter + TryAsCompositeFilter + Clone, + CF: CreateFilter + Clone, F: Fn(usize) -> bool + Copy, { let expected_select_nodes = graph @@ -2891,12 +2890,7 @@ mod test_node_filter { #[test] fn test_is_active_node_window_not() { - let filter = NodeFilter - .window(1, 10) - .is_active() - .try_as_composite_node_filter() - .unwrap(); - let filter = CompositeNodeFilter::Not(Box::new(filter)); + let filter = NodeFilter.window(1, 10).is_active().not(); let expected_results = vec![]; assert_filter_nodes_results( init_nodes_graph, @@ -4458,7 +4452,7 @@ mod test_node_composite_filter { use raphtory::{ db::graph::views::filter::model::{ node_filter::ops::NodeFilterOps, property_filter::ops::PropertyFilterOps, - ComposableFilter, PropertyFilterFactory, TryAsCompositeFilter, + ComposableFilter, PropertyFilterFactory, }, prelude::NodeFilter, }; @@ -4526,14 +4520,6 @@ mod test_node_composite_filter { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter .property("p2") @@ -4548,15 +4534,6 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = NodeFilter.property("p1").eq("pometry").or(NodeFilter .property("p2") .eq(6u64) @@ -4569,14 +4546,6 @@ mod test_node_composite_filter { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = NodeFilter::node_type() .eq("fire_nation") @@ -4590,15 +4559,6 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = NodeFilter .property("p9") .eq(5u64) @@ -4612,15 +4572,6 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = NodeFilter::node_type() .eq("fire_nation") .and(NodeFilter.property("p1").eq("shivam_kapoor")); @@ -4633,15 +4584,6 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = NodeFilter::name() .eq("2") .and(NodeFilter.property("p2").eq(2u64)); @@ -4654,15 +4596,6 @@ mod test_node_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = NodeFilter::name() .eq("2") .and(NodeFilter.property("p2").eq(2u64)) @@ -4675,15 +4608,6 @@ mod test_node_composite_filter { &expected_results, TestVariants::All, ); - - let filter = filter.try_as_composite_node_filter().unwrap(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] @@ -4774,7 +4698,7 @@ mod test_node_property_filter_agg { model::{ node_filter::NodeFilter, property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - PropertyFilterFactory, TemporalPropertyFilterFactory, TryAsCompositeFilter, + PropertyFilterFactory, TemporalPropertyFilterFactory, }, CreateFilter, }, @@ -5106,10 +5030,7 @@ mod test_node_property_filter_agg { } #[track_caller] - fn apply_assertion( - filter: impl TryAsCompositeFilter + CreateFilter + Clone, - expected: &[&str], - ) { + fn apply_assertion(filter: impl CreateFilter + Clone, expected: &[&str]) { assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -5120,10 +5041,7 @@ mod test_node_property_filter_agg { } #[track_caller] - fn apply_assertion_err( - filter: impl TryAsCompositeFilter + CreateFilter + Clone, - expected: &str, - ) { + fn apply_assertion_err(filter: impl CreateFilter + Clone, expected: &str) { assert_filter_nodes_err( init_nodes_graph, IdentityGraphTransformer, @@ -10602,7 +10520,6 @@ mod test_edge_composite_filter { use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, property_filter::ops::PropertyFilterOps, ComposableFilter, PropertyFilterFactory, - TryAsCompositeFilter, }; use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; @@ -10681,14 +10598,6 @@ mod test_edge_composite_filter { &expected_results, TestVariants::All, ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); let filter = EdgeFilter .property("p2") @@ -10703,15 +10612,6 @@ mod test_edge_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = EdgeFilter.property("p1").eq("pometry").or(EdgeFilter .property("p2") .eq(6u64) @@ -10730,15 +10630,6 @@ mod test_edge_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = EdgeFilter::src() .name() .eq("13") @@ -10752,15 +10643,6 @@ mod test_edge_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = EdgeFilter .property("p2") .eq(4u64) @@ -10774,15 +10656,6 @@ mod test_edge_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = EdgeFilter::src() .name() .eq("1") @@ -10796,15 +10669,6 @@ mod test_edge_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = EdgeFilter::dst() .name() .eq("1") @@ -10818,15 +10682,6 @@ mod test_edge_composite_filter { TestVariants::All, ); - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - let filter = EdgeFilter::src() .name() .eq("1") @@ -10840,15 +10695,6 @@ mod test_edge_composite_filter { &expected_results, TestVariants::All, ); - - let filter = filter.try_as_composite_edge_filter().unwrap(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); } #[test] 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 47da1d031b..3b6a070faf 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -10,7 +10,6 @@ use crate::{ edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, node_filter::CompositeNodeFilter, ComposableFilter, FilterTree, - TryAsCompositeFilter, }, CreateFilter, }, @@ -95,47 +94,3 @@ impl CreateFilter for AndFilter { Ok(graph) } } - -impl TryAsCompositeFilter for AndFilter { - fn try_as_filter_tree(&self) -> Result { - // Same-kind combinations keep their composite form; mixed-kind trees - // export structurally — the case the composite exports cannot - // represent. - if let Ok(f) = self.try_as_composite_node_filter() { - return Ok(FilterTree::Node(f)); - } - if let Ok(f) = self.try_as_composite_edge_filter() { - return Ok(FilterTree::Edge(f)); - } - if let Ok(f) = self.try_as_composite_exploded_edge_filter() { - return Ok(FilterTree::ExplodedEdge(f)); - } - Ok(FilterTree::And(vec![ - self.left.try_as_filter_tree()?, - self.right.try_as_filter_tree()?, - ])) - } - - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::And( - Box::new(self.left.try_as_composite_node_filter()?), - Box::new(self.right.try_as_composite_node_filter()?), - )) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::And( - Box::new(self.left.try_as_composite_edge_filter()?), - Box::new(self.right.try_as_composite_edge_filter()?), - )) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::And( - Box::new(self.left.try_as_composite_exploded_edge_filter()?), - Box::new(self.right.try_as_composite_exploded_edge_filter()?), - )) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/degree_filter.rs b/raphtory/src/db/graph/views/filter/model/degree_filter.rs index 790569c97a..a8bd34684f 100644 --- a/raphtory/src/db/graph/views/filter/model/degree_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/degree_filter.rs @@ -9,7 +9,7 @@ use crate::{ PropertyFilterValue, PropertyRef, }, CombinedFilter, ComposableFilter, CompositeNodeFilter, FilterOperator, - InternalPropertyFilterBuilder, NodeFilter, TryAsCompositeFilter, + InternalPropertyFilterBuilder, NodeFilter, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -138,22 +138,6 @@ impl CreateFilter for DegreeFilter { } } -impl TryAsCompositeFilter for DegreeFilter { - fn try_as_composite_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Degree(self.clone())) - } -} - fn property_ref(direction: &Direction) -> PropertyRef { match direction { Direction::IN => PropertyRef::Property("in_degree".to_string()), 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 f292cad9c2..44004cbfbe 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -35,7 +35,7 @@ use crate::{ AndFilter, CombinedFilter, ComposableFilter, DynFilter, EdgeViewFilterOps, EntityMarker, FilterTree, InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, NotFilter, OrFilter, - TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + TemporalPropertyFilterFactory, Wrap, }, CreateFilter, }, @@ -208,7 +208,7 @@ impl Wrap for EdgeEndpointWrapper { } } -impl ComposableFilter for EdgeEndpointWrapper where T: TryAsCompositeFilter + Clone {} +impl ComposableFilter for EdgeEndpointWrapper where T: Clone + Send + Sync {} impl InternalNodeIdFilterBuilder for EdgeEndpointWrapper { fn field_name(&self) -> &'static str { @@ -318,32 +318,6 @@ impl CreateFilter for EdgeEndpointWrapper } } -impl TryAsCompositeFilter for EdgeEndpointWrapper { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotNodeFilter) - } - - fn try_as_composite_edge_filter(&self) -> Result { - let filter = self.inner.try_as_composite_node_filter()?; - let filter = match self.endpoint { - Endpoint::Src => CompositeEdgeFilter::Src(filter), - Endpoint::Dst => CompositeEdgeFilter::Dst(filter), - }; - Ok(filter) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - let filter = self.inner.try_as_composite_node_filter()?; - let filter = match self.endpoint { - Endpoint::Src => CompositeExplodedEdgeFilter::Src(filter), - Endpoint::Dst => CompositeExplodedEdgeFilter::Dst(filter), - }; - Ok(filter) - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum CompositeEdgeFilter { Src(CompositeNodeFilter), @@ -385,161 +359,6 @@ impl Display for CompositeEdgeFilter { } } -impl CreateFilter for CompositeEdgeFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc; - - type NodeFilter<'graph, G, F> - = NotANodeFilter - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type FilteredGraph<'graph, G> - = Arc - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - match self { - CompositeEdgeFilter::Src(filter) => { - let wrapped = EdgeEndpointWrapper::new(filter, Endpoint::Src); - let filtered_graph = wrapped.create_filter(graph, filtered)?; - Ok(Arc::new(filtered_graph)) - } - CompositeEdgeFilter::Dst(filter) => { - let wrapped = EdgeEndpointWrapper::new(filter, Endpoint::Dst); - let filtered_graph = wrapped.create_filter(graph, filtered)?; - Ok(Arc::new(filtered_graph)) - } - CompositeEdgeFilter::Property(i) => Ok(Arc::new(i.create_filter(graph, filtered)?)), - CompositeEdgeFilter::Windowed(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - i.create_filter(dyn_graph, dyn_filtered) - } - CompositeEdgeFilter::Latest(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - i.create_filter(dyn_graph, dyn_filtered) - } - CompositeEdgeFilter::SnapshotAt(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - i.create_filter(dyn_graph, dyn_filtered) - } - CompositeEdgeFilter::SnapshotLatest(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - i.create_filter(dyn_graph, dyn_filtered) - } - CompositeEdgeFilter::IsActiveEdge(i) => Ok(Arc::new(i.create_filter(graph, filtered)?)), - CompositeEdgeFilter::IsValidEdge(i) => Ok(Arc::new(i.create_filter(graph, filtered)?)), - CompositeEdgeFilter::IsDeletedEdge(i) => { - Ok(Arc::new(i.create_filter(graph, filtered)?)) - } - CompositeEdgeFilter::IsSelfLoopEdge(i) => { - Ok(Arc::new(i.create_filter(graph, filtered)?)) - } - CompositeEdgeFilter::Layered(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - i.create_filter(dyn_graph, dyn_filtered) - } - CompositeEdgeFilter::And(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - AndFilter { left: l, right: r }.create_filter(graph, filtered)?, - )) - } - CompositeEdgeFilter::Or(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - OrFilter { left: l, right: r }.create_filter(graph, filtered)?, - )) - } - CompositeEdgeFilter::Not(f) => { - let base = *f; - Ok(Arc::new(NotFilter(base).create_filter(graph, filtered)?)) - } - } - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> 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 { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(self.clone()) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - // ── expr layer: a full node filter evaluated on an edge endpoint ── /// Evaluates an erased node filter against the src or dst node of each edge. @@ -615,34 +434,6 @@ impl CreateFilter for EdgeEndpointNodeFilter { impl ComposableFilter for EdgeEndpointNodeFilter {} -impl TryAsCompositeFilter for EdgeEndpointNodeFilter { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - - fn try_as_filter_tree(&self) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } -} - // ── expr layer: endpoint expressions bridge node ops into edge ops ── impl EntityExprBuilder for EdgeEndpointWrapper {} 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 8144639e14..6881682314 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 @@ -31,7 +31,7 @@ use crate::{ windowed_filter::Windowed, AndFilter, CombinedFilter, EdgeViewFilterOps, EntityMarker, InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, - NotFilter, OrFilter, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + NotFilter, OrFilter, TemporalPropertyFilterFactory, Wrap, }, CreateFilter, }, @@ -277,29 +277,6 @@ impl CreateFilter for ExplodedEdgeEndpointWra } } -impl TryAsCompositeFilter for ExplodedEdgeEndpointWrapper -where - T: TryAsCompositeFilter + Clone, -{ - 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 { - let nf = self.inner.try_as_composite_node_filter()?; - Ok(match self.endpoint { - Endpoint::Src => CompositeExplodedEdgeFilter::Src(nf), - Endpoint::Dst => CompositeExplodedEdgeFilter::Dst(nf), - }) - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum CompositeExplodedEdgeFilter { Src(CompositeNodeFilter), @@ -346,263 +323,3 @@ impl Display for CompositeExplodedEdgeFilter { } } } - -impl CreateFilter for CompositeExplodedEdgeFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc; - type NodeFilter<'graph, G, F> - = NotANodeFilter - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - type FilteredGraph<'graph, G> - = Arc - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - match self { - Self::Src(filter) => { - let wrapped = ExplodedEdgeEndpointWrapper::new(filter, Endpoint::Src); - let filtered_graph = wrapped.create_filter(graph, filtered)?; - Ok(Arc::new(filtered_graph)) - } - Self::Dst(filter) => { - let wrapped = ExplodedEdgeEndpointWrapper::new(filter, Endpoint::Dst); - let filtered_graph = wrapped.create_filter(graph, filtered)?; - Ok(Arc::new(filtered_graph)) - } - Self::Property(p) => Ok(Arc::new(p.create_filter(graph, filtered)?)), - Self::Windowed(pw) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - pw.create_filter(dyn_graph, dyn_filtered) - } - Self::Latest(pw) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - pw.create_filter(dyn_graph, dyn_filtered) - } - Self::SnapshotAt(pw) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - pw.create_filter(dyn_graph, dyn_filtered) - } - Self::SnapshotLatest(pw) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - pw.create_filter(dyn_graph, dyn_filtered) - } - Self::Layered(pw) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: DynGraphArc<'graph> = Arc::new(filtered); - pw.create_filter(dyn_graph, dyn_filtered) - } - Self::IsActiveEdge(pw) => Ok(Arc::new(pw.create_filter(graph, filtered)?)), - Self::IsValidEdge(pw) => Ok(Arc::new(pw.create_filter(graph, filtered)?)), - Self::IsDeletedEdge(pw) => Ok(Arc::new(pw.create_filter(graph, filtered)?)), - Self::IsSelfLoopEdge(pw) => Ok(Arc::new(pw.create_filter(graph, filtered)?)), - Self::And(l, r) => { - let (l, r) = (*l, *r); // move out, no clone - Ok(Arc::new( - AndFilter { left: l, right: r }.create_filter(graph, filtered)?, - )) - } - Self::Or(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - OrFilter { left: l, right: r }.create_filter(graph, filtered)?, - )) - } - Self::Not(f) => { - let base = *f; - Ok(Arc::new(NotFilter(base).create_filter(graph, filtered)?)) - } - } - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> 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 { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(self.clone()) - } -} - -#[cfg(test)] -mod filter_tree_export_tests { - use super::*; - use crate::db::graph::views::filter::model::{ - node_filter::NodeFilter, property_filter::ops::PropertyFilterOps, ComposableFilter, - FilterTree, PropertyFilterFactory, ViewWrapOps, - }; - - // An exploded-edge property filter exports as the exploded-edge kind — the - // transportable form the remote client ships. - #[test] - fn exploded_property_exports_as_exploded_edge_tree() { - let f = ExplodedEdgeFilter.property("w").gt(1i64); - let tree = f.try_as_filter_tree().unwrap(); - assert!( - matches!( - tree, - FilterTree::ExplodedEdge(CompositeExplodedEdgeFilter::Property(_)) - ), - "expected ExplodedEdge(Property), got {tree:?}" - ); - } - - #[test] - fn exploded_metadata_exports_as_exploded_edge_tree() { - let f = ExplodedEdgeFilter.metadata("kind").eq("strong"); - let tree = f.try_as_filter_tree().unwrap(); - assert!( - matches!( - tree, - FilterTree::ExplodedEdge(CompositeExplodedEdgeFilter::Property(_)) - ), - "expected ExplodedEdge(Property), got {tree:?}" - ); - } - - // A combinator of two exploded filters keeps its composite form — no - // structural And wrapper. - #[test] - fn same_kind_combinators_export_as_a_composite() { - let a = ExplodedEdgeFilter.property("w").gt(1i64); - let b = ExplodedEdgeFilter.property("w").lt(9i64); - let tree = a.clone().and(b.clone()).try_as_filter_tree().unwrap(); - assert!( - matches!( - tree, - FilterTree::ExplodedEdge(CompositeExplodedEdgeFilter::And(_, _)) - ), - "expected ExplodedEdge(And), got {tree:?}" - ); - - let tree = a.clone().or(b).try_as_filter_tree().unwrap(); - assert!( - matches!( - tree, - FilterTree::ExplodedEdge(CompositeExplodedEdgeFilter::Or(_, _)) - ), - "expected ExplodedEdge(Or), got {tree:?}" - ); - - let tree = a.not().try_as_filter_tree().unwrap(); - assert!( - matches!( - tree, - FilterTree::ExplodedEdge(CompositeExplodedEdgeFilter::Not(_)) - ), - "expected ExplodedEdge(Not), got {tree:?}" - ); - } - - // A view wrapper over an exploded property keeps the composite form as - // well (the wrapper becomes a Windowed composite variant). - #[test] - fn windowed_exploded_property_exports_as_a_composite() { - let f = ExplodedEdgeFilter.window(2i64, 4i64).property("w").gt(1i64); - let tree = f.try_as_filter_tree().unwrap(); - assert!( - matches!( - tree, - FilterTree::ExplodedEdge(CompositeExplodedEdgeFilter::Windowed(_)) - ), - "expected ExplodedEdge(Windowed), got {tree:?}" - ); - } - - // The exploded predicates also export as plain edge filters; the - // node → edge → exploded order must keep that export unchanged. - #[test] - fn exploded_predicates_still_export_as_plain_edge_filters() { - let tree = ExplodedEdgeFilter.is_valid().try_as_filter_tree().unwrap(); - assert!( - matches!(tree, FilterTree::Edge(_)), - "expected Edge for is_valid, got {tree:?}" - ); - } - - // A mixed node∧exploded combination exports structurally, with the - // exploded leg tagged as its own kind. - #[test] - fn mixed_node_and_exploded_exports_structurally() { - let n = NodeFilter.property("x").eq(1i64); - let x = ExplodedEdgeFilter.property("w").gt(1i64); - let tree = n.and(x).try_as_filter_tree().unwrap(); - let FilterTree::And(ref items) = tree else { - panic!("expected structural And, got {tree:?}"); - }; - assert!(matches!(items[0], FilterTree::Node(_))); - assert!(matches!(items[1], FilterTree::ExplodedEdge(_))); - } -} 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 62e6b6b7f8..1fa12c1f9a 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -9,7 +9,7 @@ use crate::{ snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, CombinedFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, FilterTree, - InternalViewWrapOps, TryAsCompositeFilter, Wrap, + InternalViewWrapOps, Wrap, }, CreateFilter, }, @@ -78,27 +78,6 @@ impl CreateFilter for GraphFilter { } } -impl TryAsCompositeFilter for GraphFilter { - fn try_as_filter_tree(&self) -> Result { - // The bare graph anchor restricts nothing — an empty view chain. - Ok(FilterTree::View(Vec::new())) - } - - 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) - } -} - // ── expr-layer view ops ── pub trait GraphFilterOps: 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 dfab3dc064..5a2867d5df 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 @@ -6,7 +6,7 @@ use crate::{ edge_filtered_graph::EdgeFilteredGraph, model::{ edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, TryAsCompositeFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, }, CreateFilter, }, @@ -73,22 +73,6 @@ impl CreateFilter for IsActiveEdge { impl ComposableFilter for IsActiveEdge {} -impl TryAsCompositeFilter for IsActiveEdge { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsActiveEdge(IsActiveEdge)) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsActiveEdge(IsActiveEdge)) - } -} - // ── expr layer: the predicate as a boolean expression over the eval view ── use crate::db::graph::views::filter::model::{ 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 721ab4fae2..63d869954d 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, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -72,22 +72,6 @@ impl CreateFilter for IsActiveNode { impl ComposableFilter for IsActiveNode {} -impl TryAsCompositeFilter for IsActiveNode { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::IsActiveNode(IsActiveNode)) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - // ── expr layer: the predicate as a boolean expression over the eval view ── use crate::db::graph::views::filter::model::{ 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 541c6851f5..b37373f10d 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 @@ -6,7 +6,7 @@ use crate::{ edge_filtered_graph::EdgeFilteredGraph, model::{ edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, TryAsCompositeFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, }, CreateFilter, }, @@ -73,22 +73,6 @@ impl CreateFilter for IsDeletedEdge { impl ComposableFilter for IsDeletedEdge {} -impl TryAsCompositeFilter for IsDeletedEdge { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge)) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsDeletedEdge(IsDeletedEdge)) - } -} - // ── expr layer: the predicate as a boolean expression over the eval view ── use crate::db::graph::views::filter::model::{ 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 ae0e408978..e40e86eb95 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 @@ -5,7 +5,7 @@ use crate::{ filter::{ model::{ edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, TryAsCompositeFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, }, CreateFilter, }, @@ -73,22 +73,6 @@ impl CreateFilter for IsSelfLoopEdge { impl ComposableFilter for IsSelfLoopEdge {} -impl TryAsCompositeFilter for IsSelfLoopEdge { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) - } -} - // ── expr layer: the predicate as a boolean expression over the eval view ── use crate::db::graph::views::filter::model::{ 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 b02c5524ee..64d421b273 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 @@ -6,7 +6,7 @@ use crate::{ edge_filtered_graph::EdgeFilteredGraph, model::{ edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, TryAsCompositeFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, }, CreateFilter, }, @@ -73,22 +73,6 @@ impl CreateFilter for IsValidEdge { impl ComposableFilter for IsValidEdge {} -impl TryAsCompositeFilter for IsValidEdge { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsValidEdge(IsValidEdge)) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge)) - } -} - // ── expr layer: the predicate as a boolean expression over the eval view ── use crate::db::graph::views::filter::model::{ 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 a678dc227f..f952fafda7 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -21,7 +21,7 @@ use crate::{ CompositeNodeFilter, CreateView, EdgeViewFilterOps, FilterTree, GraphViewOp, InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + TemporalPropertyFilterFactory, Wrap, }, CreateFilter, }, @@ -99,50 +99,6 @@ impl InternalPropertyFilterBuilder for Latest< } } -impl TryAsCompositeFilter for Latest { - fn try_as_filter_tree(&self) -> Result { - // Single-kind inners keep their composite form (the wrapper becomes a - // windowed/layered/... composite variant); only graph-level view - // chains export as `View` ops. Anything else (a view wrapping a - // mixed-kind tree) has no wire representation yet. - if let Ok(f) = self.try_as_composite_node_filter() { - return Ok(FilterTree::Node(f)); - } - if let Ok(f) = self.try_as_composite_edge_filter() { - return Ok(FilterTree::Edge(f)); - } - if let Ok(f) = self.try_as_composite_exploded_edge_filter() { - return Ok(FilterTree::ExplodedEdge(f)); - } - let FilterTree::View(ops) = self.inner.try_as_filter_tree()? else { - return Err(GraphError::NotSupported); - }; - let mut chain = vec![GraphViewOp::Latest]; - chain.extend(ops); - Ok(FilterTree::View(chain)) - } - - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Latest(Box::new(Latest::new( - self.inner.try_as_composite_node_filter()?, - )))) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::Latest(Box::new(Latest::new( - self.inner.try_as_composite_edge_filter()?, - )))) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::Latest(Box::new(Latest::new( - self.inner.try_as_composite_exploded_edge_filter()?, - )))) - } -} - impl CreateFilter for Latest { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> 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 6d367170fc..4b5c2d9515 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -20,7 +20,7 @@ use crate::{ CompositeNodeFilter, EdgeViewFilterOps, FilterTree, GraphViewOp, InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + TemporalPropertyFilterFactory, Wrap, }, CreateFilter, }, @@ -115,50 +115,6 @@ impl InternalPropertyFilterBuilder for Layered } } -impl TryAsCompositeFilter for Layered { - fn try_as_filter_tree(&self) -> Result { - // Single-kind inners keep their composite form (the wrapper becomes a - // windowed/layered/... composite variant); only graph-level view - // chains export as `View` ops. Anything else (a view wrapping a - // mixed-kind tree) has no wire representation yet. - if let Ok(f) = self.try_as_composite_node_filter() { - return Ok(FilterTree::Node(f)); - } - if let Ok(f) = self.try_as_composite_edge_filter() { - return Ok(FilterTree::Edge(f)); - } - if let Ok(f) = self.try_as_composite_exploded_edge_filter() { - return Ok(FilterTree::ExplodedEdge(f)); - } - let FilterTree::View(ops) = self.inner.try_as_filter_tree()? else { - return Err(GraphError::NotSupported); - }; - let mut chain = vec![GraphViewOp::Layers(self.layer.clone())]; - chain.extend(ops); - Ok(FilterTree::View(chain)) - } - - fn try_as_composite_node_filter(&self) -> Result { - let filter = self.inner.try_as_composite_node_filter()?; - let filter = CompositeNodeFilter::Layered(Box::new(self.wrap(filter))); - Ok(filter) - } - - fn try_as_composite_edge_filter(&self) -> Result { - let filter = self.inner.try_as_composite_edge_filter()?; - let filter = CompositeEdgeFilter::Layered(Box::new(self.wrap(filter))); - Ok(filter) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - let filter = self.inner.try_as_composite_exploded_edge_filter()?; - let filter = CompositeExplodedEdgeFilter::Layered(Box::new(self.wrap(filter))); - Ok(filter) - } -} - impl CreateFilter for Layered { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index a651896b31..cc952d911b 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -140,22 +140,6 @@ impl CreateFilter for Unfiltered { } } -impl TryAsCompositeFilter for Unfiltered { - 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) - } -} - pub trait Wrap { type Wrapped; @@ -189,7 +173,7 @@ pub trait ComposableFilter: Sized { } } -pub trait DynCreateFilter: TryAsCompositeFilter + Send + Sync + 'static { +pub trait DynCreateFilter: Send + Sync + 'static { fn create_dyn_filter<'graph>( &self, graph: DynGraphArc<'graph>, @@ -379,10 +363,10 @@ pub enum GraphViewOp { /// Kind-tagged, owned export of a filter tree — the transportable form of a /// composed filter, referencing no in-process state. `View` is an -/// outermost-first chain of graph-level restrictions. Produced by -/// [`TryAsCompositeFilter::try_as_filter_tree`]; filters that inherently -/// reference in-process state (e.g. node-state columns) cannot be exported -/// and return an error instead. +/// chain of graph-level restrictions in application order (each later op +/// wraps outside the previous one). Recorded at construction on the python +/// side; filters that inherently reference in-process state (e.g. node-state +/// columns) carry no tree. #[derive(Clone, Debug)] pub enum FilterTree { Node(CompositeNodeFilter), @@ -413,55 +397,6 @@ impl FilterTree { } } -pub trait TryAsCompositeFilter: Send + Sync { - fn try_as_composite_node_filter(&self) -> Result; - - fn try_as_composite_edge_filter(&self) -> Result; - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result; - - /// Export this filter as a kind-tagged [`FilterTree`]. The default covers - /// every single-kind filter via the composite exports; combinators and - /// graph-view filters override it to preserve structure the single-kind - /// exports cannot represent (mixed-kind trees, view chains). The kinds are - /// tried node → edge → exploded-edge, so a filter that exports as more - /// than one kind (e.g. the edge validity predicates) keeps its - /// plain-edge export. - fn try_as_filter_tree(&self) -> Result { - if let Ok(f) = self.try_as_composite_node_filter() { - return Ok(FilterTree::Node(f)); - } - if let Ok(f) = self.try_as_composite_edge_filter() { - return Ok(FilterTree::Edge(f)); - } - Ok(FilterTree::ExplodedEdge( - self.try_as_composite_exploded_edge_filter()?, - )) - } -} - -impl TryAsCompositeFilter for Arc { - fn try_as_filter_tree(&self) -> Result { - self.deref().try_as_filter_tree() - } - - fn try_as_composite_node_filter(&self) -> Result { - self.deref().try_as_composite_node_filter() - } - - fn try_as_composite_edge_filter(&self) -> Result { - self.deref().try_as_composite_edge_filter() - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - self.deref().try_as_composite_exploded_edge_filter() - } -} - #[derive(Clone)] pub struct MetadataExpr { view_expr: E, @@ -1436,7 +1371,7 @@ pub trait InternalPropertyFilterFactory { fn metadata_builder(&self, property: String) -> Self::MetadataBuilder; } -pub trait CombinedFilter: CreateFilter + TryAsCompositeFilter + Clone + 'static {} +pub trait CombinedFilter: CreateFilter + Clone + Send + Sync + 'static {} pub trait NodeViewFilterOps: ViewWrapOps { type Output: CombinedFilter; @@ -1569,7 +1504,7 @@ impl InternalPropertyFilterFactory for Arc { } } -impl CombinedFilter for T {} +impl CombinedFilter for T {} impl InternalPropertyFilterFactory for Arc { type Entity = EntityMarker; 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 fd1dab1c33..bc606c7f78 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 @@ -890,7 +890,7 @@ use crate::db::graph::views::filter::{ exploded_edge_filter::CompositeExplodedEdgeFilter, node_expr::exprs::{AllExpr, AnyExpr}, node_filter::CompositeNodeFilter, - FilterTree, TryAsCompositeFilter, + FilterTree, }, }; @@ -992,67 +992,7 @@ macro_rules! impl_qualifier_filter { impl ComposableFilter for $ty where E::Marker: Into {} - impl TryAsCompositeFilter for $ty { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - - fn try_as_filter_tree(&self) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - } )+}; } impl_qualifier_filter!(AnyExpr, AllExpr); - -macro_rules! impl_not_composite { - ($($ty:ident<$($g:ident),+>),+ $(,)?) => {$( - impl<$($g: Send + Sync),+> TryAsCompositeFilter for $ty<$($g),+> { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::InvalidFilter( - "expression filters have no composite representation".to_string(), - )) - } - } - )+}; -} - -impl_not_composite!( - BinaryCmpExpr, - UnaryExpr, - StringExpr, - PropValueSetExpr, -); 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 a9d29d7302..d78e3f1d40 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 @@ -34,7 +34,7 @@ use crate::{ windowed_filter::Windowed, AndFilter, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, CreateView, EntityMarker, InternalPropertyFilterFactory, InternalViewWrapOps, - NodeViewFilterOps, NotFilter, OrFilter, TryAsCompositeFilter, Wrap, + NodeViewFilterOps, NotFilter, OrFilter, Wrap, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -199,22 +199,6 @@ impl CreateFilter for NodeIdFilter { } } -impl TryAsCompositeFilter for NodeIdFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Node(self.0.clone())) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - #[derive(Debug, Clone)] pub struct NodeNameFilter(pub Filter); @@ -268,22 +252,6 @@ impl CreateFilter for NodeNameFilter { } } -impl TryAsCompositeFilter for NodeNameFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Node(self.0.clone())) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - #[derive(Debug, Clone)] pub struct NodeTypeFilter(pub Filter); @@ -354,22 +322,6 @@ impl CreateFilter for NodeTypeFilter { } } -impl TryAsCompositeFilter for NodeTypeFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Node(self.0.clone())) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum CompositeNodeFilter { Node(Filter), @@ -405,155 +357,6 @@ impl Display for CompositeNodeFilter { } } -impl CreateFilter for CompositeNodeFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph>; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = Arc - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone(), filtered)?; - Ok(NodeFilteredGraph::new(graph, filter)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - match self { - CompositeNodeFilter::Degree(i) => Ok(Arc::new(i.create_node_filter(graph, filtered)?)), - CompositeNodeFilter::Node(i) => match i.field_name.as_str() { - "node_id" => Ok(Arc::new( - NodeIdFilter(i).create_node_filter(graph, filtered)?, - )), - "node_name" => Ok(Arc::new( - NodeNameFilter(i).create_node_filter(graph, filtered)?, - )), - "node_type" => Ok(Arc::new( - NodeTypeFilter(i).create_node_filter(graph, filtered)?, - )), - _ => { - unreachable!() - } - }, - CompositeNodeFilter::Property(i) => { - Ok(Arc::new(i.create_node_filter(graph, filtered)?)) - } - CompositeNodeFilter::Windowed(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: Arc = Arc::new(filtered); - i.create_node_filter(dyn_graph, dyn_filtered) - } - CompositeNodeFilter::Layered(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: Arc = Arc::new(filtered); - i.create_node_filter(dyn_graph, dyn_filtered) - } - CompositeNodeFilter::Latest(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: Arc = Arc::new(filtered); - i.create_node_filter(dyn_graph, dyn_filtered) - } - CompositeNodeFilter::SnapshotAt(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: Arc = Arc::new(filtered); - i.create_node_filter(dyn_graph, dyn_filtered) - } - CompositeNodeFilter::SnapshotLatest(i) => { - let dyn_graph: Arc = Arc::new(graph); - let dyn_filtered: Arc = Arc::new(filtered); - i.create_node_filter(dyn_graph, dyn_filtered) - } - CompositeNodeFilter::IsActiveNode(i) => { - Ok(Arc::new(i.create_node_filter(graph, filtered)?)) - } - CompositeNodeFilter::And(l, r) => Ok(Arc::new(AndOp { - left: l - .clone() - .create_node_filter(graph.clone(), filtered.clone())?, - right: r.clone().create_node_filter(graph, filtered)?, - })), - CompositeNodeFilter::Or(l, r) => Ok(Arc::new(OrOp { - left: l - .clone() - .create_node_filter(graph.clone(), filtered.clone())?, - right: r.clone().create_node_filter(graph, filtered)?, - })), - CompositeNodeFilter::Not(filter) => Ok(Arc::new(NotOp( - filter.clone().create_node_filter(graph, filtered)?, - ))), - } - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - match self.clone() { - CompositeNodeFilter::Node(i) => match i.field_name.as_str() { - "node_id" => Ok(Arc::new(NodeIdFilter(i).filter_graph_view(graph)?)), - "node_name" => Ok(Arc::new(NodeNameFilter(i).filter_graph_view(graph)?)), - "node_type" => Ok(Arc::new(NodeTypeFilter(i).filter_graph_view(graph)?)), - _ => { - unreachable!() - } - }, - CompositeNodeFilter::Property(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeNodeFilter::Degree(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 { - fn try_as_composite_node_filter(&self) -> Result { - Ok(self.clone()) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - // ── expr-layer factory ── pub trait NodeFilterFactory: diff --git a/raphtory/src/db/graph/views/filter/model/node_state_filter.rs b/raphtory/src/db/graph/views/filter/model/node_state_filter.rs index c799eca492..4678e152ed 100644 --- a/raphtory/src/db/graph/views/filter/model/node_state_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/node_state_filter.rs @@ -6,7 +6,6 @@ use crate::{ }, graph::views::filter::model::{ edge_filter::CompositeEdgeFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, - TryAsCompositeFilter, }, }, errors::GraphError, @@ -80,19 +79,3 @@ impl NodeOp for NodeStateBoolColOp { self.bool_at_row(row) } } - -impl TryAsCompositeFilter for NodeStateBoolColOp { - 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) - } -} 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 200b624029..b2056eacf3 100644 --- a/raphtory/src/db/graph/views/filter/model/not_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/not_filter.rs @@ -9,7 +9,6 @@ use crate::{ edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, node_filter::CompositeNodeFilter, ComposableFilter, FilterTree, - TryAsCompositeFilter, }, not_filtered_graph::NotFilteredGraph, CreateFilter, @@ -79,41 +78,3 @@ impl CreateFilter for NotFilter { Ok(graph) } } - -impl TryAsCompositeFilter for NotFilter { - fn try_as_filter_tree(&self) -> Result { - // Same-kind combinations keep their composite form; mixed-kind trees - // export structurally — the case the composite exports cannot - // represent. - if let Ok(f) = self.try_as_composite_node_filter() { - return Ok(FilterTree::Node(f)); - } - if let Ok(f) = self.try_as_composite_edge_filter() { - return Ok(FilterTree::Edge(f)); - } - if let Ok(f) = self.try_as_composite_exploded_edge_filter() { - return Ok(FilterTree::ExplodedEdge(f)); - } - Ok(FilterTree::Not(Box::new(self.0.try_as_filter_tree()?))) - } - - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Not(Box::new( - self.0.try_as_composite_node_filter()?, - ))) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::Not(Box::new( - self.0.try_as_composite_edge_filter()?, - ))) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::Not(Box::new( - self.0.try_as_composite_exploded_edge_filter()?, - ))) - } -} 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 e4a71bac5e..27c58600aa 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -9,7 +9,6 @@ use crate::{ edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, node_filter::CompositeNodeFilter, ComposableFilter, FilterTree, - TryAsCompositeFilter, }, or_filtered_graph::OrFilteredGraph, CreateFilter, @@ -90,47 +89,3 @@ impl CreateFilter for OrFilter { Ok(graph) } } - -impl TryAsCompositeFilter for OrFilter { - fn try_as_filter_tree(&self) -> Result { - // Same-kind combinations keep their composite form; mixed-kind trees - // export structurally — the case the composite exports cannot - // represent. - if let Ok(f) = self.try_as_composite_node_filter() { - return Ok(FilterTree::Node(f)); - } - if let Ok(f) = self.try_as_composite_edge_filter() { - return Ok(FilterTree::Edge(f)); - } - if let Ok(f) = self.try_as_composite_exploded_edge_filter() { - return Ok(FilterTree::ExplodedEdge(f)); - } - Ok(FilterTree::Or(vec![ - self.left.try_as_filter_tree()?, - self.right.try_as_filter_tree()?, - ])) - } - - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Or( - Box::new(self.left.try_as_composite_node_filter()?), - Box::new(self.right.try_as_composite_node_filter()?), - )) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::Or( - Box::new(self.left.try_as_composite_edge_filter()?), - Box::new(self.right.try_as_composite_edge_filter()?), - )) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::Or( - Box::new(self.left.try_as_composite_exploded_edge_filter()?), - Box::new(self.right.try_as_composite_exploded_edge_filter()?), - )) - } -} 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 1beb52e1e5..ffce3805fb 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 @@ -16,7 +16,7 @@ use crate::{ model::{ edge_filter::CompositeEdgeFilter, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, ExplodedEdgeFilter, - FilterOperator, TryAsCompositeFilter, + FilterOperator, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -500,51 +500,3 @@ impl CreateFilter for PropertyFilter { } impl ComposableFilter for PropertyFilter {} - -impl TryAsCompositeFilter for PropertyFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Property(self.clone())) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - -impl TryAsCompositeFilter for PropertyFilter { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::Property(self.clone())) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } -} - -impl TryAsCompositeFilter for PropertyFilter { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::Property(self.clone())) - } -} 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 71c13b9dbe..52fd566aa3 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -21,7 +21,7 @@ use crate::{ CompositeNodeFilter, CreateView, EdgeViewFilterOps, FilterTree, GraphViewOp, InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + TemporalPropertyFilterFactory, Wrap, }, CreateFilter, }, @@ -91,55 +91,6 @@ impl InternalPropertyFilterBuilder for Snapsho } } -impl TryAsCompositeFilter for SnapshotAt { - fn try_as_filter_tree(&self) -> Result { - // Single-kind inners keep their composite form (the wrapper becomes a - // windowed/layered/... composite variant); only graph-level view - // chains export as `View` ops. Anything else (a view wrapping a - // mixed-kind tree) has no wire representation yet. - if let Ok(f) = self.try_as_composite_node_filter() { - return Ok(FilterTree::Node(f)); - } - if let Ok(f) = self.try_as_composite_edge_filter() { - return Ok(FilterTree::Edge(f)); - } - if let Ok(f) = self.try_as_composite_exploded_edge_filter() { - return Ok(FilterTree::ExplodedEdge(f)); - } - let FilterTree::View(ops) = self.inner.try_as_filter_tree()? else { - return Err(GraphError::NotSupported); - }; - let mut chain = vec![GraphViewOp::SnapshotAt(self.time)]; - chain.extend(ops); - Ok(FilterTree::View(chain)) - } - - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::SnapshotAt(Box::new(SnapshotAt { - time: self.time, - inner: self.inner.try_as_composite_node_filter()?, - }))) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::SnapshotAt(Box::new(SnapshotAt::new( - self.time, - self.inner.try_as_composite_edge_filter()?, - )))) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::SnapshotAt(Box::new( - SnapshotAt::new( - self.time, - self.inner.try_as_composite_exploded_edge_filter()?, - ), - ))) - } -} - impl CreateFilter for SnapshotAt { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> @@ -303,50 +254,6 @@ impl InternalPropertyFilterBuilder for Snapsho } } -impl TryAsCompositeFilter for SnapshotLatest { - fn try_as_filter_tree(&self) -> Result { - // Single-kind inners keep their composite form (the wrapper becomes a - // windowed/layered/... composite variant); only graph-level view - // chains export as `View` ops. Anything else (a view wrapping a - // mixed-kind tree) has no wire representation yet. - if let Ok(f) = self.try_as_composite_node_filter() { - return Ok(FilterTree::Node(f)); - } - if let Ok(f) = self.try_as_composite_edge_filter() { - return Ok(FilterTree::Edge(f)); - } - if let Ok(f) = self.try_as_composite_exploded_edge_filter() { - return Ok(FilterTree::ExplodedEdge(f)); - } - let FilterTree::View(ops) = self.inner.try_as_filter_tree()? else { - return Err(GraphError::NotSupported); - }; - let mut chain = vec![GraphViewOp::SnapshotLatest]; - chain.extend(ops); - Ok(FilterTree::View(chain)) - } - - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::SnapshotLatest(Box::new( - SnapshotLatest::new(self.inner.try_as_composite_node_filter()?), - ))) - } - - fn try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::SnapshotLatest(Box::new( - SnapshotLatest::new(self.inner.try_as_composite_edge_filter()?), - ))) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::SnapshotLatest(Box::new( - SnapshotLatest::new(self.inner.try_as_composite_exploded_edge_filter()?), - ))) - } -} - impl CreateFilter for SnapshotLatest { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> 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 9489d7d114..5d3df4cea6 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -20,7 +20,7 @@ use crate::{ CompositeNodeFilter, CreateView, EdgeViewFilterOps, FilterTree, GraphViewOp, InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + TemporalPropertyFilterFactory, Wrap, }, CreateFilter, }, @@ -126,53 +126,6 @@ impl InternalPropertyFilterBuilder for Windowe } } -impl TryAsCompositeFilter for Windowed { - fn try_as_filter_tree(&self) -> Result { - // Single-kind inners keep their composite form (the wrapper becomes a - // windowed/layered/... composite variant); only graph-level view - // chains export as `View` ops. Anything else (a view wrapping a - // mixed-kind tree) has no wire representation yet. - if let Ok(f) = self.try_as_composite_node_filter() { - return Ok(FilterTree::Node(f)); - } - if let Ok(f) = self.try_as_composite_edge_filter() { - return Ok(FilterTree::Edge(f)); - } - if let Ok(f) = self.try_as_composite_exploded_edge_filter() { - return Ok(FilterTree::ExplodedEdge(f)); - } - let FilterTree::View(ops) = self.inner.try_as_filter_tree()? else { - return Err(GraphError::NotSupported); - }; - let mut chain = vec![GraphViewOp::Window { - start: self.start, - end: self.end, - }]; - chain.extend(ops); - Ok(FilterTree::View(chain)) - } - - fn try_as_composite_node_filter(&self) -> Result { - let filter = self.inner.try_as_composite_node_filter()?; - let filter = CompositeNodeFilter::Windowed(Box::new(self.wrap(filter))); - Ok(filter) - } - - fn try_as_composite_edge_filter(&self) -> Result { - let filter = self.inner.try_as_composite_edge_filter()?; - let filter = CompositeEdgeFilter::Windowed(Box::new(self.wrap(filter))); - Ok(filter) - } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - let filter = self.inner.try_as_composite_exploded_edge_filter()?; - let filter = CompositeExplodedEdgeFilter::Windowed(Box::new(self.wrap(filter))); - Ok(filter) - } -} - impl CreateFilter for Windowed { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 90f9a3a190..46bb18171b 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -8,7 +8,6 @@ use crate::{ model::{ edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, not_filter::NotFilter, or_filter::OrFilter, AndFilter, DynCreateFilter, FilterTree, - TryAsCompositeFilter, }, CreateFilter, }, @@ -41,14 +40,6 @@ impl PyFilterExpr { ) }) } - - pub fn try_as_node_filter(&self) -> Result { - self.0.try_as_composite_node_filter() - } - - pub fn try_as_edge_filter(&self) -> Result { - self.0.try_as_composite_edge_filter() - } } #[pymethods] diff --git a/raphtory/src/python/filter/wire.rs b/raphtory/src/python/filter/wire.rs index f6c3f1a2c3..c50062a938 100644 --- a/raphtory/src/python/filter/wire.rs +++ b/raphtory/src/python/filter/wire.rs @@ -183,9 +183,10 @@ pub(crate) enum WireValue { macro_rules! wrap_views_fn { ($name:ident, $composite:ident) => { pub(crate) fn $name(leaf: $composite, views: &[WireView]) -> $composite { - // Views recorded factory-first wrap outside-in: the first view a - // user applied is the outermost restriction. - views.iter().rev().fold(leaf, |acc, view| match view { + // Views are recorded in application order and each later view + // wraps outside the previous one: the outermost wrapper is the + // last view applied, matching the local expression engine. + views.iter().fold(leaf, |acc, view| match view { WireView::Window(start, end) => { $composite::Windowed(Box::new(Windowed::new(*start, *end, acc))) } From da298f067931e102cb6616edc131aaebc0415df2 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:12:16 +0100 Subject: [PATCH 112/155] delete the builder filter DSL; the expression API is the only path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The builder factories, ops traits, field builders, validation module and leaf executors are gone, along with their prelude exports. The rust test suite runs on the expression API — the builder half of the A/B test pair is deleted, its two unique tests moved into the expression suite. Aggregations over latest list-valued properties now work on the typed rust surface, matching what python already exposed. The exploded-edge expression filter now passes deletion events through, as the removed executor did; persistent-graph histories pin the behaviour. --- raphtory-benchmark/src/algobench_common.rs | 13 +- raphtory-graphql/src/model/graph/filtering.rs | 32 +- raphtory-tests/tests/algo_tests/components.rs | 10 +- .../tests/filter_tests/cached_view.rs | 9 +- .../filter_tests/edge_property_filter.rs | 3 +- .../exploded_edge_property_filter.rs | 5 +- raphtory-tests/tests/filter_tests/mod.rs | 1 - .../filter_tests/node_property_filter.rs | 5 +- .../tests/filter_tests/subgraph_tests.rs | 12 +- .../tests/filter_tests/test_filters.rs | 10744 ---------------- .../tests/filter_tests/test_layers.rs | 10 +- .../tests_node_type_filtered_subgraph.rs | 16 +- .../tests/filter_tests/views_test.rs | 157 +- raphtory-tests/tests/qp3.rs | 2 +- raphtory-tests/tests/test_filters.rs | 243 +- raphtory/src/db/api/state/ops/filter.rs | 216 +- .../filter/edge_property_filtered_graph.rs | 93 - .../exploded_edge_expr_filtered_graph.rs | 6 + .../filter/exploded_edge_property_filter.rs | 133 - raphtory/src/db/graph/views/filter/mod.rs | 2 - .../db/graph/views/filter/model/and_filter.rs | 8 +- .../graph/views/filter/model/degree_filter.rs | 177 +- .../graph/views/filter/model/edge_filter.rs | 115 +- .../filter/model/exploded_edge_filter.rs | 118 +- .../graph/views/filter/model/graph_filter.rs | 4 +- .../filter/model/is_active_edge_filter.rs | 7 +- .../filter/model/is_active_node_filter.rs | 7 +- .../views/filter/model/is_deleted_filter.rs | 7 +- .../views/filter/model/is_self_loop_filter.rs | 8 +- .../views/filter/model/is_valid_filter.rs | 7 +- .../graph/views/filter/model/latest_filter.rs | 85 +- .../views/filter/model/layered_filter.rs | 84 +- .../src/db/graph/views/filter/model/mod.rs | 444 +- .../views/filter/model/node_expr/dyn_expr.rs | 4 +- .../views/filter/model/node_expr/filters.rs | 4 - .../filter/model/node_filter/builders.rs | 123 - .../views/filter/model/node_filter/mod.rs | 303 +- .../views/filter/model/node_filter/ops.rs | 151 - .../filter/model/node_filter/validate.rs | 127 - .../views/filter/model/node_state_filter.rs | 11 +- .../db/graph/views/filter/model/not_filter.rs | 8 +- .../db/graph/views/filter/model/or_filter.rs | 8 +- .../filter/model/property_filter/builders.rs | 216 - .../filter/model/property_filter/evaluate.rs | 2 +- .../views/filter/model/property_filter/mod.rs | 135 +- .../views/filter/model/property_filter/ops.rs | 277 - .../filter/model/property_filter/validate.rs | 3 +- .../views/filter/model/snapshot_filter.rs | 117 +- .../views/filter/model/windowed_filter.rs | 84 +- raphtory/src/lib.rs | 4 +- raphtory/src/python/filter/filter_expr.rs | 1 - raphtory/src/python/graph/views/graph_view.rs | 26 - 52 files changed, 430 insertions(+), 13957 deletions(-) delete mode 100644 raphtory-tests/tests/filter_tests/test_filters.rs delete mode 100644 raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs delete mode 100644 raphtory/src/db/graph/views/filter/exploded_edge_property_filter.rs delete mode 100644 raphtory/src/db/graph/views/filter/model/node_filter/builders.rs delete mode 100644 raphtory/src/db/graph/views/filter/model/node_filter/ops.rs delete mode 100644 raphtory/src/db/graph/views/filter/model/node_filter/validate.rs delete mode 100644 raphtory/src/db/graph/views/filter/model/property_filter/builders.rs delete mode 100644 raphtory/src/db/graph/views/filter/model/property_filter/ops.rs diff --git a/raphtory-benchmark/src/algobench_common.rs b/raphtory-benchmark/src/algobench_common.rs index 6ee47e95a3..19c2a052bb 100644 --- a/raphtory-benchmark/src/algobench_common.rs +++ b/raphtory-benchmark/src/algobench_common.rs @@ -2,12 +2,7 @@ use criterion::{Criterion, SamplingMode}; use raphtory::{ db::{ api::view::{Filter, StaticGraphViewOps}, - graph::views::{ - filter::model::{ - degree_filter::DegreeFilterFactory, property_filter::ops::PropertyFilterOps, - }, - node_subgraph::NodeSubgraph, - }, + graph::views::node_subgraph::NodeSubgraph, }, graphgen::random_attachment::random_attachment, prelude::*, @@ -99,7 +94,7 @@ pub fn large_random_attachment_subgraph() -> NodeSubgraph { pub fn large_random_attachment_filtered() -> impl StaticGraphViewOps { large_random_attachment_graph() - .filter(DegreeFilterFactory::degree(&NodeFilter).ge(1u64)) + .filter(NodeFilter.degree().ge(1u64)) .unwrap() } @@ -166,7 +161,7 @@ pub fn medium_random_attachment_subgraph() -> NodeSubgraph { pub fn medium_random_attachment_filtered() -> impl StaticGraphViewOps { medium_random_attachment_graph() - .filter(DegreeFilterFactory::degree(&NodeFilter).ge(0u64)) + .filter(NodeFilter.degree().ge(0u64)) .unwrap() } @@ -233,7 +228,7 @@ pub fn tiny_random_attachment_subgraph() -> NodeSubgraph { pub fn tiny_random_attachment_filtered() -> impl StaticGraphViewOps { tiny_random_attachment_graph() - .filter(DegreeFilterFactory::degree(&NodeFilter).ge(0u64)) + .filter(NodeFilter.degree().ge(0u64)) .unwrap() } diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 7ede1afe00..6d8cccc6d7 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -14,25 +14,13 @@ use raphtory::{ }, graph::views::filter::{ model::{ - degree_filter::DegreeFilter, - edge_filter::{CompositeEdgeFilter, EdgeFilter}, - exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, + edge_filter::CompositeEdgeFilter, + exploded_edge_filter::CompositeExplodedEdgeFilter, filter::{Filter, FilterValue}, filter_operator::FilterOperator, graph_filter::GraphFilter, - 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 as LatestWrap, - layered_filter::Layered, - node_filter::{CompositeNodeFilter, NodeFilter}, + node_filter::CompositeNodeFilter, property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, - snapshot_filter::{ - SnapshotAt as SnapshotAtWrap, SnapshotLatest as SnapshotLatestWrap, - }, - windowed_filter::Windowed, ComposableFilter, DynFilter, DynView, FilterTree, GraphViewOp, ViewWrapOps, }, CreateFilter, @@ -43,7 +31,6 @@ use raphtory::{ use raphtory_api::core::{ entities::{properties::prop::Prop, Layer, GID}, storage::timeindex::{AsTime, EventTime}, - utils::time::IntoTime, Direction, }; use serde::{Deserialize, Serialize}; @@ -2585,9 +2572,6 @@ mod gql_filter_serde_tests { #[cfg(test)] mod fuzzy_search_tests { use super::*; - use raphtory::db::graph::views::filter::model::node_filter::{ - ops::NodeFilterOps, NodeFilter as NodeFilterBuilder, - }; // The wire shape is externally tagged camelCase, like every other condition. #[test] @@ -2632,12 +2616,12 @@ mod fuzzy_search_tests { ); } - // Local node-name builder → wire condition (the reverse conversion the + // Local node-name filter → wire condition (the reverse conversion the // Python remote client rides) preserves the fuzzy parameters. #[test] fn node_name_fuzzy_round_trips_through_the_wire() { - let core = NodeFilterBuilder::name().fuzzy_search("ben", 1, true); - let GqlNodeFilter::Name(wire) = filter_to_node_field(core.0).unwrap() else { + let core = Filter::fuzzy_search("node_name", "ben", 1, true); + let GqlNodeFilter::Name(wire) = filter_to_node_field(core).unwrap() else { panic!("expected the per-field name variant"); }; let NodeFieldCondition::FuzzySearch(ref f) = wire.where_ else { @@ -2653,6 +2637,7 @@ mod fuzzy_search_tests { #[cfg(test)] mod conversion_hole_tests { use super::*; + use raphtory::db::graph::views::filter::model::layered_filter::Layered; // `isSome: false` lowers to the IsNone operator (and vice versa) instead // of erroring — the two spellings are the same predicate. @@ -2720,7 +2705,8 @@ mod exploded_edge_filter_tests { use super::*; use raphtory::{ db::graph::views::filter::model::{ - layered_filter::Layered, windowed_filter::Windowed, FilterOperator, + is_valid_filter::IsValidEdge, layered_filter::Layered, windowed_filter::Windowed, + ExplodedEdgeFilter, FilterOperator, }, prelude::Prop, }; diff --git a/raphtory-tests/tests/algo_tests/components.rs b/raphtory-tests/tests/algo_tests/components.rs index bc661fd507..7a010d2e4c 100644 --- a/raphtory-tests/tests/algo_tests/components.rs +++ b/raphtory-tests/tests/algo_tests/components.rs @@ -262,10 +262,7 @@ mod in_component_test { db::{ api::mutation::AdditionOps, graph::views::filter::{ - model::{ - graph_filter::GraphFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, ViewWrapOps, - }, + model::{graph_filter::GraphFilter, PropertyExprFactory, ViewWrapOps}, CreateFilter, }, }, @@ -531,10 +528,7 @@ mod components_test { db::{ api::mutation::AdditionOps, graph::views::filter::{ - model::{ - graph_filter::GraphFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, ViewWrapOps, - }, + model::{graph_filter::GraphFilter, PropertyExprFactory, ViewWrapOps}, CreateFilter, }, }, diff --git a/raphtory-tests/tests/filter_tests/cached_view.rs b/raphtory-tests/tests/filter_tests/cached_view.rs index 2e24ad8bcd..02dbf40559 100644 --- a/raphtory-tests/tests/filter_tests/cached_view.rs +++ b/raphtory-tests/tests/filter_tests/cached_view.rs @@ -31,8 +31,9 @@ mod test_nodes_filters_cached_view_graph { cached_view::{CachedGraphTransformer, WindowedCachedGraphTransformer}, init_graph, Edges, Nodes, }; - use raphtory::db::graph::views::filter::model::{ - node_filter::NodeFilter, property_filter::ops::PropertyFilterOps, PropertyFilterFactory, + use raphtory::{ + db::graph::views::filter::model::{node_filter::NodeFilter, PropertyExprFactory}, + prelude::EntityExprFilterOps, }; use raphtory_tests::assertions::{ assert_filter_nodes_results, TestGraphVariants, TestVariants, @@ -87,8 +88,8 @@ mod test_edges_filter_cached_view_graph { use crate::filter_tests::cached_view::{ CachedGraphTransformer, WindowedCachedGraphTransformer, }; - use raphtory::db::graph::views::filter::model::{ - property_filter::ops::PropertyFilterOps, PropertyFilterFactory, + use raphtory::{ + db::graph::views::filter::model::PropertyExprFactory, prelude::EntityExprFilterOps, }; use crate::filter_tests::{init_graph, Edges, Nodes}; diff --git a/raphtory-tests/tests/filter_tests/edge_property_filter.rs b/raphtory-tests/tests/filter_tests/edge_property_filter.rs index a35e69da21..635cf03eb9 100644 --- a/raphtory-tests/tests/filter_tests/edge_property_filter.rs +++ b/raphtory-tests/tests/filter_tests/edge_property_filter.rs @@ -8,8 +8,7 @@ use raphtory::{ views::{ deletion_graph::PersistentGraph, filter::model::{ - node_filter::ops::NodeFilterOps, property_filter::ops::PropertyFilterOps, - ComposableFilter, EdgeFilter, EdgeViewFilterOps, PropertyFilterFactory, + ComposableFilter, EdgeFilter, EdgeViewFilterOps, PropertyExprFactory, ViewWrapOps, }, }, diff --git a/raphtory-tests/tests/filter_tests/exploded_edge_property_filter.rs b/raphtory-tests/tests/filter_tests/exploded_edge_property_filter.rs index 182ab24d48..48451bda90 100644 --- a/raphtory-tests/tests/filter_tests/exploded_edge_property_filter.rs +++ b/raphtory-tests/tests/filter_tests/exploded_edge_property_filter.rs @@ -11,10 +11,7 @@ use raphtory::{ }, views::{ deletion_graph::PersistentGraph, - filter::model::{ - property_filter::ops::PropertyFilterOps, ExplodedEdgeFilter, - PropertyFilterFactory, - }, + filter::model::{ExplodedEdgeFilter, PropertyExprFactory}, }, }, }, diff --git a/raphtory-tests/tests/filter_tests/mod.rs b/raphtory-tests/tests/filter_tests/mod.rs index ad0a8263c5..e3b02de8d7 100644 --- a/raphtory-tests/tests/filter_tests/mod.rs +++ b/raphtory-tests/tests/filter_tests/mod.rs @@ -5,7 +5,6 @@ mod edge_property_filter; mod exploded_edge_property_filter; mod node_property_filter; mod subgraph_tests; -mod test_filters; mod test_layers; mod tests_node_type_filtered_subgraph; mod views_test; diff --git a/raphtory-tests/tests/filter_tests/node_property_filter.rs b/raphtory-tests/tests/filter_tests/node_property_filter.rs index c1d634030d..8bc49df4a7 100644 --- a/raphtory-tests/tests/filter_tests/node_property_filter.rs +++ b/raphtory-tests/tests/filter_tests/node_property_filter.rs @@ -6,9 +6,8 @@ use raphtory::{ graph::{ graph::assert_edges_equal, views::filter::model::{ - node_filter::{ops::NodeFilterOps, NodeFilter, NodeFilterFactory}, - property_filter::ops::PropertyFilterOps, - ComposableFilter, PropertyFilterFactory, + node_filter::{NodeFilter, NodeFilterFactory}, + ComposableFilter, PropertyExprFactory, }, }, }, diff --git a/raphtory-tests/tests/filter_tests/subgraph_tests.rs b/raphtory-tests/tests/filter_tests/subgraph_tests.rs index 8756977dc8..6af33e50e0 100644 --- a/raphtory-tests/tests/filter_tests/subgraph_tests.rs +++ b/raphtory-tests/tests/filter_tests/subgraph_tests.rs @@ -42,10 +42,8 @@ mod test_nodes_filters_node_subgraph { Edges, Nodes, }; use raphtory::{ - db::graph::views::filter::model::{ - property_filter::ops::PropertyFilterOps, PropertyFilterFactory, - }, - prelude::NodeFilter, + db::graph::views::filter::model::PropertyExprFactory, + prelude::{EntityExprFilterOps, NodeFilter}, }; use raphtory_tests::assertions::{ assert_filter_nodes_results, TestGraphVariants, TestVariants, @@ -133,10 +131,8 @@ mod test_edges_filters_node_subgraph { Edges, Nodes, }; use raphtory::{ - db::graph::views::filter::model::{ - property_filter::ops::PropertyFilterOps, PropertyFilterFactory, - }, - prelude::EdgeFilter, + db::graph::views::filter::model::PropertyExprFactory, + prelude::{EdgeFilter, EntityExprFilterOps}, }; use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; diff --git a/raphtory-tests/tests/filter_tests/test_filters.rs b/raphtory-tests/tests/filter_tests/test_filters.rs deleted file mode 100644 index af0023a702..0000000000 --- a/raphtory-tests/tests/filter_tests/test_filters.rs +++ /dev/null @@ -1,10744 +0,0 @@ -use raphtory::{db::api::view::StaticGraphViewOps, prelude::*}; - -mod test_composite_filters { - use raphtory::{ - db::graph::views::filter::model::{ - edge_filter::EdgeFilter, filter::Filter, node_filter::NodeFilter, - property_filter::ops::PropertyFilterOps, PropertyFilterFactory, - }, - prelude::IntoProp, - }; - use raphtory_api::core::{entities::properties::prop::Prop, storage::arc_str::ArcStr}; - - #[test] - fn test_fuzzy_search() { - let filter = Filter::fuzzy_search("name", "pomet", 2, false); - assert!(filter.matches(Some("pometry"))); - - let filter = Filter::fuzzy_search("name", "shivam_kapoor", 2, false); - assert!(filter.matches(Some("shivam_kapoor2"))); - - let filter = Filter::fuzzy_search("name", "shivam kapoor", 2, false); - assert!(filter.matches(Some("shivam_kapoor2"))); - - let filter = Filter::fuzzy_search("name", "shivam kapoor", 2, false); - assert!(filter.matches(Some("shivam_kapoor2"))); - - let filter = Filter::fuzzy_search("name", "shivam kapoor", 2, false); - assert!(!filter.matches(Some("shivam1_kapoor2"))); - - let filter = Filter::fuzzy_search("name", "khivam sapoor", 2, false); - assert!(!filter.matches(Some("shivam1_kapoor2"))); - } - - #[test] - fn test_fuzzy_search_prefix_match() { - let filter = Filter::fuzzy_search("name", "pome", 2, false); - assert!(!filter.matches(Some("pometry"))); - - let filter = Filter::fuzzy_search("name", "pome", 2, true); - assert!(filter.matches(Some("pometry"))); - } - - #[test] - fn test_fuzzy_search_property() { - let filter = NodeFilter.property("prop").fuzzy_search("pomet", 2, false); - assert!(filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); - } - - #[test] - fn test_fuzzy_search_property_prefix_match() { - let filter = EdgeFilter.property("prop").fuzzy_search("pome", 2, false); - assert!(!filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); - - let filter = EdgeFilter.property("prop").fuzzy_search("pome", 2, true); - assert!(filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); - } - - #[test] - fn test_contains_match() { - let filter = EdgeFilter.property("prop").contains("shivam"); - let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam_kapoor")))); - assert!(res); - let res = filter.matches(None); - assert!(!res); - - let filter = EdgeFilter.property("prop").contains("am_ka"); - let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam_kapoor")))); - assert!(res); - } - - #[test] - fn test_contains_not_match() { - let filter = NodeFilter.property("prop").not_contains("shivam"); - let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam_kapoor")))); - assert!(!res); - let res = filter.matches(None); - assert!(!res); - } - - #[test] - fn test_is_in_match() { - let filter = NodeFilter - .property("prop") - .is_in(vec!["shivam".into_prop()]); - let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam")))); - assert!(res); - let res = filter.matches(None); - assert!(!res); - } - - #[test] - fn test_is_not_in_match() { - let filter = EdgeFilter - .property("prop") - .is_not_in(vec!["shivam".into_prop()]); - let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam")))); - assert!(!res); - let res = filter.matches(None); - assert!(!res); - } -} - -use raphtory_api::core::entities::properties::prop::IntoProp; -use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, -}; -use raphtory_tests::assertions::GraphTransformer; - -struct IdentityGraphTransformer; - -impl GraphTransformer for IdentityGraphTransformer { - type Return = G; - fn apply(&self, graph: G) -> Self::Return { - graph - } -} - -mod test_property_semantics { - mod test_node_property_filter_semantics { - use crate::filter_tests::test_filters::IdentityGraphTransformer; - use raphtory::{ - db::{ - api::view::{filter_ops::Filter, StaticGraphViewOps}, - graph::views::filter::model::{ - node_filter::NodeFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, TemporalPropertyFilterFactory, - }, - }, - errors::GraphError, - prelude::*, - }; - use raphtory_api::core::entities::properties::prop::Prop; - use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, - }; - use raphtory_tests::assertions::{assert_filter_nodes_results, TestVariants}; - - fn init_graph(graph: G) -> G { - let nodes = [ - (6, "N1", vec![("p1", Prop::U64(2u64))]), - (7, "N1", vec![("p1", Prop::U64(1u64))]), - (6, "N2", vec![("p1", Prop::U64(1u64))]), - (7, "N2", vec![("p1", Prop::U64(2u64))]), - (8, "N3", vec![("p1", Prop::U64(1u64))]), - (9, "N4", vec![("p1", Prop::U64(1u64))]), - (5, "N5", vec![("p1", Prop::U64(1u64))]), - (6, "N5", vec![("p1", Prop::U64(2u64))]), - (5, "N6", vec![("p1", Prop::U64(1u64))]), - (6, "N6", vec![("p1", Prop::U64(1u64))]), - (3, "N7", vec![("p1", Prop::U64(1u64))]), - (5, "N7", vec![("p1", Prop::U64(1u64))]), - (3, "N8", vec![("p1", Prop::U64(1u64))]), - (4, "N8", vec![("p1", Prop::U64(2u64))]), - (2, "N9", vec![("p1", Prop::U64(2u64))]), - (2, "N10", vec![("q1", Prop::U64(0u64))]), - (2, "N10", vec![("p1", Prop::U64(3u64))]), - (2, "N11", vec![("p1", Prop::U64(3u64))]), - (2, "N11", vec![("q1", Prop::U64(0u64))]), - (2, "N12", vec![("q1", Prop::U64(0u64))]), - (3, "N12", vec![("p1", Prop::U64(3u64))]), - (2, "N13", vec![("q1", Prop::U64(0u64))]), - (3, "N13", vec![("p1", Prop::U64(3u64))]), - (2, "N14", vec![("q1", Prop::U64(0u64))]), - (2, "N15", vec![]), - ]; - - for (id, label, props) in nodes.iter() { - graph - .add_node(*id, label, props.clone(), None, None) - .unwrap(); - } - - let metadata = [ - ("N1", [("p1", Prop::U64(1u64))]), - ("N4", [("p1", Prop::U64(2u64))]), - ("N9", [("p1", Prop::U64(1u64))]), - ("N10", [("p1", Prop::U64(1u64))]), - ("N11", [("p1", Prop::U64(1u64))]), - ("N12", [("p1", Prop::U64(1u64))]), - ("N13", [("p1", Prop::U64(1u64))]), - ("N14", [("p1", Prop::U64(1u64))]), - ("N15", [("p1", Prop::U64(1u64))]), - ]; - - for (node, props) in metadata.iter() { - graph - .node(node) - .unwrap() - .add_metadata(props.clone()) - .unwrap(); - } - - graph - } - - fn init_graph_for_event_ids< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, - >( - graph: G, - ) -> G { - let graph: G = init_graph(graph); - let nodes = [ - (1, "N16", vec![("p1", Prop::U64(2u64))]), - (1, "N16", vec![("p1", Prop::U64(1u64))]), - (1, "N17", vec![("p1", Prop::U64(1u64))]), - (1, "N17", vec![("p1", Prop::U64(2u64))]), - ]; - - for (id, label, props) in nodes.iter() { - graph - .add_node(*id, label, props.clone(), None, None) - .unwrap(); - } - - graph - } - - #[test] - fn test_metadata_semantics() { - let filter = NodeFilter.metadata("p1").eq(1u64); - let expected_results = vec!["N1", "N10", "N11", "N12", "N13", "N14", "N15", "N9"]; - assert_filter_nodes_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_temporal_any_semantics() { - let filter = NodeFilter.property("p1").temporal().any().eq(1u64); - let expected_results = vec!["N1", "N2", "N3", "N4", "N5", "N6", "N7", "N8"]; - assert_filter_nodes_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_temporal_any_semantics_for_event_ids() { - let filter = NodeFilter.property("p1").temporal().any().eq(1u64); - let expected_results = - vec!["N1", "N16", "N17", "N2", "N3", "N4", "N5", "N6", "N7", "N8"]; - assert_filter_nodes_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_temporal_latest_semantics() { - let filter = NodeFilter.property("p1").temporal().last().eq(1u64); - let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; - assert_filter_nodes_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_temporal_latest_semantics_for_event_ids() { - let filter = NodeFilter.property("p1").temporal().last().eq(1u64); - let expected_results = vec!["N1", "N16", "N3", "N4", "N6", "N7"]; - assert_filter_nodes_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_property_semantics() { - // TODO: Const properties not supported for disk_graph. - let filter = NodeFilter.property("p1").eq(1u64); - let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; - assert_filter_nodes_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_property_semantics_for_event_ids() { - let filter = NodeFilter.property("p1").eq(1u64); - let expected_results = vec!["N1", "N16", "N3", "N4", "N6", "N7"]; - assert_filter_nodes_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_property_semantics_only_metadata() { - // For this graph there won't be any temporal property index for property name "p1". - fn init_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, - >( - graph: G, - ) -> G { - let nodes = [(2, "N1", vec![("q1", Prop::U64(0u64))]), (2, "N2", vec![])]; - - for (id, label, props) in nodes.iter() { - graph - .add_node(*id, label, props.clone(), None, None) - .unwrap(); - } - - let metadata = [ - ("N1", [("p1", Prop::U64(1u64))]), - ("N2", [("p1", Prop::U64(1u64))]), - ]; - - for (node, props) in metadata.iter() { - graph - .node(node) - .unwrap() - .add_metadata(props.clone()) - .unwrap(); - } - - graph - } - - let filter = NodeFilter.property("p1").ge(1u64); - let graph = init_graph(Graph::new()); - assert!(matches!( - graph.filter(filter.clone()).unwrap_err(), - GraphError::PropertyMissingError(ref name) if name == "p1" - )); - assert!(matches!( - graph.persistent_graph().filter(filter).unwrap_err(), - GraphError::PropertyMissingError(ref name) if name == "p1" - )); - } - - #[test] - fn test_property_semantics_only_temporal() { - // For this graph there won't be any metadata index for property name "p1". - fn init_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, - >( - graph: G, - ) -> G { - let nodes = [ - (1, "N1", vec![("p1", Prop::U64(1u64))]), - (2, "N2", vec![("p1", Prop::U64(1u64))]), - (3, "N2", vec![("p1", Prop::U64(2u64))]), - (2, "N3", vec![("p1", Prop::U64(2u64))]), - (3, "N3", vec![("p1", Prop::U64(1u64))]), - (3, "N4", vec![]), - ]; - - for (id, label, props) in nodes.iter() { - graph - .add_node(*id, label, props.clone(), None, None) - .unwrap(); - } - - graph - } - - let filter = NodeFilter.property("p1").le(1u64); - let expected_results = vec!["N1", "N3"]; - assert_filter_nodes_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - } - - mod test_edge_property_filter_semantics { - use crate::filter_tests::test_filters::IdentityGraphTransformer; - use raphtory::{ - db::{ - api::view::{filter_ops::Filter, EdgeViewOps, StaticGraphViewOps}, - graph::views::filter::{ - model::{ - edge_filter::EdgeFilter, property_filter::ops::PropertyFilterOps, - PropertyFilterFactory, TemporalPropertyFilterFactory, - }, - CreateFilter, - }, - }, - errors::GraphError, - prelude::*, - }; - use raphtory_api::core::entities::properties::prop::Prop; - use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, - }; - use raphtory_tests::assertions::{ - assert_filter_edges_results, TestVariants, WindowGraphTransformer, - }; - - fn init_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, - >( - graph: G, - ) -> G { - let edges = [ - (6, "N1", "N2", vec![("p1", Prop::U64(2u64))]), - (7, "N1", "N2", vec![("p1", Prop::U64(1u64))]), - (6, "N2", "N3", vec![("p1", Prop::U64(1u64))]), - (7, "N2", "N3", vec![("p1", Prop::U64(2u64))]), - (8, "N3", "N4", vec![("p1", Prop::U64(1u64))]), - (9, "N4", "N5", vec![("p1", Prop::U64(1u64))]), - (5, "N5", "N6", vec![("p1", Prop::U64(1u64))]), - (6, "N5", "N6", vec![("p1", Prop::U64(2u64))]), - (5, "N6", "N7", vec![("p1", Prop::U64(1u64))]), - (6, "N6", "N7", vec![("p1", Prop::U64(1u64))]), - (3, "N7", "N8", vec![("p1", Prop::U64(1u64))]), - (5, "N7", "N8", vec![("p1", Prop::U64(1u64))]), - (3, "N8", "N9", vec![("p1", Prop::U64(1u64))]), - (4, "N8", "N9", vec![("p1", Prop::U64(2u64))]), - (2, "N9", "N10", vec![("p1", Prop::U64(2u64))]), - (2, "N10", "N11", vec![("q1", Prop::U64(0u64))]), - (2, "N10", "N11", vec![("p1", Prop::U64(3u64))]), - (2, "N11", "N12", vec![("p1", Prop::U64(3u64))]), - (2, "N11", "N12", vec![("q1", Prop::U64(0u64))]), - (2, "N12", "N13", vec![("q1", Prop::U64(0u64))]), - (3, "N12", "N13", vec![("p1", Prop::U64(3u64))]), - (2, "N13", "N14", vec![("q1", Prop::U64(0u64))]), - (3, "N13", "N14", vec![("p1", Prop::U64(3u64))]), - (2, "N14", "N15", vec![("q1", Prop::U64(0u64))]), - (2, "N15", "N1", vec![]), - ]; - - for (time, src, dst, props) in edges { - graph.add_edge(time, src, dst, props, None).unwrap(); - } - - let metadata_edges = [ - ("N1", "N2", vec![("p1", Prop::U64(1u64))]), - ("N4", "N5", vec![("p1", Prop::U64(2u64))]), - ("N9", "N10", vec![("p1", Prop::U64(1u64))]), - ("N10", "N11", vec![("p1", Prop::U64(1u64))]), - ("N11", "N12", vec![("p1", Prop::U64(1u64))]), - ("N12", "N13", vec![("p1", Prop::U64(1u64))]), - ("N13", "N14", vec![("p1", Prop::U64(1u64))]), - ("N14", "N15", vec![("p1", Prop::U64(1u64))]), - ("N15", "N1", vec![("p1", Prop::U64(1u64))]), - ]; - - for (src, dst, props) in metadata_edges { - graph - .edge(src, dst) - .unwrap() - .add_metadata(props.clone(), None) - .unwrap(); - } - - graph - } - - fn init_graph_for_event_ids< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, - >( - graph: G, - ) -> G { - let graph: G = init_graph(graph); - let edge_data = [ - (1, "N16", "N15", vec![("p1", Prop::U64(2u64))]), - (1, "N16", "N15", vec![("p1", Prop::U64(1u64))]), - (1, "N17", "N16", vec![("p1", Prop::U64(1u64))]), - (1, "N17", "N16", vec![("p1", Prop::U64(2u64))]), - ]; - - for (time, src, dst, props) in edge_data { - graph.add_edge(time, src, dst, props, None).unwrap(); - } - - graph - } - - #[test] - fn test_persistent_graph_first_window() { - fn init_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, - >( - graph: G, - ) -> G { - graph - .add_edge(0, 1, 2, [("p1", Prop::U64(1u64))], None) - .unwrap(); - graph - .add_edge(2, 1, 2, [("p1", Prop::U64(2u64))], None) - .unwrap(); - graph - .add_edge(5, 1, 2, [("p1", Prop::U64(5u64))], None) - .unwrap(); - graph - .add_edge(10, 1, 2, [("p1", Prop::U64(10u64))], None) - .unwrap(); - graph - } - - let filter = EdgeFilter.property("p1").temporal().first().eq(2u64); - - // No window; means the first update is at time 0 and the value of p1 is expected to be 1u64. - let expected_empty = []; - let expected_found = ["1->2"]; - - assert_filter_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_empty, - TestVariants::PersistentOnly, - ); - - // Window(1,10); Expected emtpy because the first update is at time 0 and the value of p1 is expected to be 1u64. - assert_filter_edges_results( - init_graph, - WindowGraphTransformer(1..10), - filter.clone(), - &expected_empty, - TestVariants::PersistentOnly, - ); - - // Window(2,10); Expected update at time 2 and the value of p1 is expected to be 2u64. - assert_filter_edges_results( - init_graph, - WindowGraphTransformer(2..10), - filter.clone(), - &expected_found, - TestVariants::PersistentOnly, - ); - - // Window(3,10); Expected update at time 2 (even if it is outside the window) and the value of p1 is expected to be 2u64. - assert_filter_edges_results( - init_graph, - WindowGraphTransformer(3..10), - filter.clone(), - &expected_found, - TestVariants::PersistentOnly, - ); - - // Window(4,10); Expected update at time 2 (even if it is outside the window) and the value of p1 is expected to be 2u64. - assert_filter_edges_results( - init_graph, - WindowGraphTransformer(4..10), - filter.clone(), - &expected_found, - TestVariants::PersistentOnly, - ); - - // Window(5,10); Expected update at time 5 (even if it is outside the window) and the value of p1 is expected to be 5u64. - assert_filter_edges_results( - init_graph, - WindowGraphTransformer(5..10), - filter.clone(), - &expected_empty, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_metadata_semantics() { - let filter = EdgeFilter.metadata("p1").eq(1u64); - let expected_results = vec![ - "N1->N2", "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N14->N15", "N15->N1", - "N9->N10", - ]; - assert_filter_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_metadata_semantics2() { - fn filter_edges(graph: &Graph, filter: impl CreateFilter) -> Vec { - let mut results = graph - .filter(filter) - .unwrap() - .edges() - .iter() - .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) - .collect::>(); - results.sort(); - results - } - - let graph = init_graph(Graph::new()); - - let filter = EdgeFilter.metadata("p1").eq(1u64); - assert_eq!( - filter_edges(&graph, filter.clone()), - vec![ - "N1->N2", "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N14->N15", - "N15->N1", "N9->N10" - ] - ); - - let edge = graph - .add_edge(1, "shivam", "kapoor", [("p1", 100u64)], Some("fire_nation")) - .unwrap(); - edge.add_metadata([("z", true)], Some("fire_nation")) - .unwrap(); - let prop = graph.edge("shivam", "kapoor").unwrap().metadata().get("z"); - assert_eq!(prop, Some(Prop::map([("fire_nation", true)]))); - - let filter2 = EdgeFilter - .metadata("z") - .eq(Prop::map([("fire_nation", true)])); - assert_eq!(filter_edges(&graph, filter2), vec!["shivam->kapoor"]); - - let filter = EdgeFilter - .metadata("p1") - .eq(Prop::map([("_default", 1u64)])); - assert_eq!( - filter_edges(&graph, filter), - vec![ - "N1->N2", "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N14->N15", - "N15->N1", "N9->N10" - ] - ); - } - - #[test] - fn test_temporal_any_semantics() { - let filter = EdgeFilter.property("p1").temporal().any().eq(1u64); - let expected_results = vec![ - "N1->N2", "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", "N7->N8", "N8->N9", - ]; - assert_filter_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_temporal_any_semantics_for_event_ids() { - let filter = EdgeFilter.property("p1").temporal().any().lt(2u64); - let expected_results = vec![ - "N1->N2", "N16->N15", "N17->N16", "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", - "N7->N8", "N8->N9", - ]; - assert_filter_edges_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_temporal_latest_semantics() { - let filter = EdgeFilter.property("p1").temporal().last().eq(1u64); - let expected_results = vec!["N1->N2", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; - assert_filter_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_temporal_latest_semantics_for_event_ids() { - let filter = EdgeFilter.property("p1").temporal().last().eq(1u64); - let expected_results = - vec!["N1->N2", "N16->N15", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; - assert_filter_edges_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_property_semantics() { - let filter = EdgeFilter.property("p1").ge(2u64); - let expected_results = vec![ - "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N2->N3", "N5->N6", "N8->N9", - "N9->N10", - ]; - assert_filter_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_property_semantics_for_event_ids() { - let filter = EdgeFilter.property("p1").eq(1u64); - let expected_results = - vec!["N1->N2", "N16->N15", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; - assert_filter_edges_results( - init_graph_for_event_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_property_semantics_only_metadata() { - // For this graph there won't be any temporal property index for property name "p1". - fn init_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, - >( - graph: G, - ) -> G { - let edges = [ - (2, "N1", "N2", vec![("q1", Prop::U64(0u64))]), - (2, "N2", "N3", vec![]), - ]; - - for (time, src, dst, props) in edges { - graph.add_edge(time, src, dst, props, None).unwrap(); - } - - let metadata_edges = [ - ("N1", "N2", vec![("p1", Prop::U64(1u64))]), - ("N2", "N3", vec![("p1", Prop::U64(1u64))]), - ]; - - for (src, dst, props) in metadata_edges { - graph - .edge(src, dst) - .unwrap() - .add_metadata(props.clone(), None) - .unwrap(); - } - - graph - } - - let filter = EdgeFilter.property("p1").eq(1u64); - let graph = init_graph(Graph::new()); - assert!(matches!( - graph.filter(filter.clone()).unwrap_err(), - GraphError::PropertyMissingError(ref name) if name == "p1" - )); - assert!(matches!( - graph.persistent_graph().filter(filter).unwrap_err(), - GraphError::PropertyMissingError(ref name) if name == "p1" - )); - } - - #[test] - fn test_property_semantics_only_temporal() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - // For this graph there won't be any metadata index for property name "p1". - fn init_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, - >( - graph: G, - ) -> G { - let edges = [ - (1, "N1", "N2", vec![("p1", Prop::U64(1u64))]), - (2, "N2", "N3", vec![("p1", Prop::U64(1u64))]), - (3, "N2", "N3", vec![("p1", Prop::U64(2u64))]), - (2, "N3", "N4", vec![("p1", Prop::U64(2u64))]), - (3, "N3", "N4", vec![("p1", Prop::U64(1u64))]), - (2, "N4", "N5", vec![]), - ]; - - for (time, src, dst, props) in edges { - graph.add_edge(time, src, dst, props, None).unwrap(); - } - - graph - } - - let filter = EdgeFilter.property("p1").eq(1u64); - let expected_results = vec!["N1->N2", "N3->N4"]; - assert_filter_edges_results( - init_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - } -} - -fn init_nodes_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, ->( - graph: G, -) -> G { - let nodes = [ - ( - 1, - "1", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p9", 5u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 5u64.into_prop()), - ], - Some("fire_nation"), - ), - ( - 2, - "2", - vec![ - ("p1", "prop12".into_prop()), - ("p2", 2u64.into_prop()), - ("p10", "Paper_ship".into_prop()), - ("p20", "Gold_boat".into_prop()), - ("p30", "Old_boat".into_prop()), - ("p40", 10u64.into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - "2", - vec![ - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 15u64.into_prop()), - ], - Some("air_nomads"), - ), - ( - 4, - "2", - vec![ - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 20u64.into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - "1", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p9", 5u64.into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 10u64.into_prop()), - ], - Some("fire_nation"), - ), - ( - 3, - "3", - vec![ - ("p2", 6u64.into_prop()), - ("p3", 1u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ], - Some("fire_nation"), - ), - ( - 4, - "1", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p9", 5u64.into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 15u64.into_prop()), - ], - Some("fire_nation"), - ), - ( - 3, - "4", - vec![ - ("p4", "pometry".into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ], - None, - ), - ( - 4, - "4", - vec![ - ("p5", 12u64.into_prop()), - ("p20", "Gold_boat".into_prop()), - ("p30", "Old_ship".into_prop()), - ], - None, - ), - ]; - - for (time, id, props, node_type) in nodes { - graph.add_node(time, id, props, node_type, None).unwrap(); - } - - let metadata = [ - ( - "1", - vec![ - ("m1", "pometry".into_prop()), - ("m2", "raphtory".into_prop()), - ], - ), - ("2", vec![("m1", "raphtory".into_prop())]), - ( - "3", - vec![ - ("m2", "pometry".into_prop()), - ("m3", "raphtory".into_prop()), - ], - ), - ( - "4", - vec![ - ("m3", "pometry".into_prop()), - ("m4", "raphtory".into_prop()), - ], - ), - ]; - - for (node_id, md) in metadata { - graph.node(node_id).unwrap().add_metadata(md).unwrap(); - } - - graph -} - -fn init_nodes_layers_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, ->( - graph: G, -) -> G { - let nodes = [ - ( - 1, - "1", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p9", 5u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 5u64.into_prop()), - ], - Some("fire_nation"), - ), - ( - 2, - "2", - vec![ - ("p1", "prop12".into_prop()), - ("p2", 2u64.into_prop()), - ("p10", "Paper_ship".into_prop()), - ("p20", "Gold_boat".into_prop()), - ("p30", "Old_boat".into_prop()), - ("p40", 10u64.into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - "2", - vec![ - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 15u64.into_prop()), - ], - Some("air_nomads"), - ), - ( - 4, - "2", - vec![ - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 20u64.into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - "1", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p9", 5u64.into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 10u64.into_prop()), - ], - Some("fire_nation"), - ), - ( - 3, - "3", - vec![ - ("p2", 6u64.into_prop()), - ("p3", 1u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ], - Some("fire_nation"), - ), - ( - 4, - "1", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p9", 5u64.into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 15u64.into_prop()), - ], - Some("fire_nation"), - ), - ( - 3, - "4", - vec![ - ("p4", "pometry".into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ], - None, - ), - ( - 4, - "4", - vec![ - ("p5", 12u64.into_prop()), - ("p20", "Gold_boat".into_prop()), - ("p30", "Old_ship".into_prop()), - ], - None, - ), - ]; - - for (time, id, props, node_type) in nodes { - graph.add_node(time, id, props, None, node_type).unwrap(); - } - - let metadata = [ - ( - "1", - vec![ - ("m1", "pometry".into_prop()), - ("m2", "raphtory".into_prop()), - ], - ), - ("2", vec![("m1", "raphtory".into_prop())]), - ( - "3", - vec![ - ("m2", "pometry".into_prop()), - ("m3", "raphtory".into_prop()), - ], - ), - ( - "4", - vec![ - ("m3", "pometry".into_prop()), - ("m4", "raphtory".into_prop()), - ], - ), - ]; - - for (node_id, md) in metadata { - graph.node(node_id).unwrap().add_metadata(md).unwrap(); - } - - graph -} - -fn init_nodes_graph_with_num_ids< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, ->( - graph: G, -) -> G { - let nodes = [ - ( - 1, - 1, - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p9", 5u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 5u64.into_prop()), - ], - Some("fire_nation"), - ), - ( - 2, - 2, - vec![ - ("p1", "prop12".into_prop()), - ("p2", 2u64.into_prop()), - ("p10", "Paper_ship".into_prop()), - ("p20", "Gold_boat".into_prop()), - ("p30", "Old_boat".into_prop()), - ("p40", 10u64.into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - 2, - vec![ - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 15u64.into_prop()), - ], - Some("air_nomads"), - ), - ( - 4, - 2, - vec![ - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 20u64.into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - 1, - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p9", 5u64.into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 10u64.into_prop()), - ], - Some("fire_nation"), - ), - ( - 3, - 3, - vec![ - ("p2", 6u64.into_prop()), - ("p3", 1u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ], - Some("fire_nation"), - ), - ( - 4, - 1, - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p9", 5u64.into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ("p40", 15u64.into_prop()), - ], - Some("fire_nation"), - ), - ( - 3, - 4, - vec![ - ("p4", "pometry".into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ], - None, - ), - ( - 4, - 4, - vec![ - ("p5", 12u64.into_prop()), - ("p20", "Gold_boat".into_prop()), - ("p30", "Old_ship".into_prop()), - ], - None, - ), - ]; - - for (time, id, props, node_type) in nodes { - graph.add_node(time, id, props, node_type, None).unwrap(); - } - - graph -} - -fn init_nodes_graph_with_str_ids< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, ->( - graph: G, -) -> G { - let nodes = [ - (1, "London", Some("fire_nation")), - (2, "Two", Some("air_nomads")), - (3, "Two", Some("air_nomads")), - (4, "Two", Some("air_nomads")), - (3, "London", Some("fire_nation")), - (3, "Tokyo", Some("fire_nation")), - (4, "London", Some("fire_nation")), - (3, "France Paris", None), - (4, "France Paris", None), - ]; - - for (time, id, node_type) in nodes { - graph.add_node(time, id, NO_PROPS, node_type, None).unwrap(); - } - - graph -} - -fn init_edges_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, ->( - graph: G, -) -> G { - let edges = [ - ( - 1, - "1", - "2", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p10", "Paper_airplane".into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ], - Some("fire_nation"), - ), - ( - 2, - "1", - "2", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p2", 4u64.into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ], - Some("fire_nation"), - ), - ( - 2, - "2", - "3", - vec![ - ("p1", "prop12".into_prop()), - ("p2", 2u64.into_prop()), - ("p10", "Paper_ship".into_prop()), - ("p20", "Gold_boat".into_prop()), - ("p30", "Old_boat".into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - "2", - "3", - vec![ - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_boat".into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - "3", - "1", - vec![("p2", 6u64.into_prop()), ("p3", 1u64.into_prop())], - Some("fire_nation"), - ), - ( - 3, - "2", - "1", - vec![ - ("p2", 6u64.into_prop()), - ("p3", 1u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ], - None, - ), - ( - 4, - "David Gilmour", - "John Mayer", - vec![("p2", 6u64.into_prop()), ("p3", 1u64.into_prop())], - None, - ), - ( - 4, - "John Mayer", - "Jimmy Page", - vec![("p2", 6u64.into_prop()), ("p3", 1u64.into_prop())], - None, - ), - ]; - - for (time, src, dst, props, edge_type) in edges { - graph.add_edge(time, src, dst, props, edge_type).unwrap(); - } - - graph -} - -fn init_edges_graph2< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, ->( - graph: G, -) -> G { - let edges = [ - ( - 1, - "1", - "2", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p2", 6u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ("p20", "Gold_ship".into_prop()), - ], - Some("fire_nation"), - ), - ( - 2, - "1", - "2", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p2", 7u64.into_prop()), - ("p10", "Gold_ship".into_prop()), - ("p20", "Gold_ship".into_prop()), - ], - Some("fire_nation"), - ), - ( - 2, - "1", - "2", - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p2", 4u64.into_prop()), - ("p20", "Gold_ship".into_prop()), - ], - Some("air_nomads"), - ), - ( - 2, - "2", - "3", - vec![ - ("p1", "prop12".into_prop()), - ("p2", 2u64.into_prop()), - ("p10", "Paper_ship".into_prop()), - ("p20", "Gold_boat".into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - "2", - "3", - vec![ - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_boat".into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - "3", - "1", - vec![("p2", 6u64.into_prop()), ("p3", 1u64.into_prop())], - Some("air_nomads"), - ), - ( - 3, - "2", - "1", - vec![ - ("p2", 6u64.into_prop()), - ("p3", 1u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ], - None, - ), - ]; - - for (time, src, dst, props, edge_type) in edges { - graph.add_edge(time, src, dst, props, edge_type).unwrap(); - } - - graph -} - -fn init_edges_graph_with_num_ids< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, ->( - graph: G, -) -> G { - let edges = [ - ( - 1, - 1, - 2, - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p10", "Paper_airplane".into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ], - Some("fire_nation"), - ), - ( - 2, - 1, - 2, - vec![ - ("p1", "shivam_kapoor".into_prop()), - ("p2", 4u64.into_prop()), - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_ship".into_prop()), - ], - Some("fire_nation"), - ), - ( - 2, - 2, - 3, - vec![ - ("p1", "prop12".into_prop()), - ("p2", 2u64.into_prop()), - ("p10", "Paper_ship".into_prop()), - ("p20", "Gold_boat".into_prop()), - ("p30", "Old_boat".into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - 2, - 3, - vec![ - ("p20", "Gold_ship".into_prop()), - ("p30", "Gold_boat".into_prop()), - ], - Some("air_nomads"), - ), - ( - 3, - 3, - 1, - vec![("p2", 6u64.into_prop()), ("p3", 1u64.into_prop())], - Some("fire_nation"), - ), - ( - 3, - 2, - 1, - vec![ - ("p2", 6u64.into_prop()), - ("p3", 1u64.into_prop()), - ("p10", "Paper_airplane".into_prop()), - ], - None, - ), - ]; - - for (time, src, dst, props, edge_type) in edges { - graph.add_edge(time, src, dst, props, edge_type).unwrap(); - } - - graph -} - -fn init_edges_graph_with_str_ids< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, ->( - graph: G, -) -> G { - let edges = [ - (1, "London", "Paris", Some("fire_nation")), - (2, "London", "Paris", Some("fire_nation")), - (2, "Two", "Three", Some("air_nomads")), - (3, "Two", "Three", Some("air_nomads")), - (3, "Three", "One", Some("fire_nation")), - (3, "Two", "One", None), - (4, "David Gilmour", "John Mayer", None), - (4, "John Mayer", "Jimmy Page", None), - ]; - - for (time, src, dst, edge_type) in edges { - graph.add_edge(time, src, dst, NO_PROPS, edge_type).unwrap(); - } - - graph -} - -fn init_edges_graph_with_str_ids_del< - G: StaticGraphViewOps - + AdditionOps - + DeletionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, ->( - graph: G, -) -> G { - let edges = [ - (1, "London", "Paris", Some("fire_nation")), - (2, "London", "Paris", Some("fire_nation")), - (2, "Two", "Three", Some("air_nomads")), - (3, "Two", "Three", Some("air_nomads")), - (3, "Three", "One", Some("fire_nation")), - (3, "Two", "One", None), - (4, "David Gilmour", "John Mayer", None), - (4, "John Mayer", "Jimmy Page", None), - ]; - - for (time, src, dst, edge_type) in edges { - graph.add_edge(time, src, dst, NO_PROPS, edge_type).unwrap(); - } - - graph - .delete_edge(3, "London", "Paris", Some("fire_nation")) - .unwrap(); - - graph - .add_edge(5, "Bangalore", "Bangalore", NO_PROPS, None) - .unwrap(); - - graph -} - -mod test_node_filter { - use crate::filter_tests::test_filters::{ - init_nodes_graph, init_nodes_graph_with_num_ids, init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - }; - use proptest::proptest; - use raphtory::{ - algorithms::alternating_mask::alternating_mask, - core::entities::VID, - db::{ - api::view::{filter_ops::Select, Filter}, - graph::views::filter::{ - model::{ - degree_filter::DegreeFilterFactory, - node_filter::ops::{NodeFilterOps, NodeIdFilterOps}, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - ComposableFilter, CompositeNodeFilter, NodeViewFilterOps, ViewWrapOps, - }, - CreateFilter, - }, - }, - errors::GraphError, - prelude::{ - AdditionOps, Graph, GraphViewOps, IntoProp, NodeFilter, NodeStateOps, NodeViewOps, - TimeOps, NO_PROPS, - }, - }; - use raphtory_api::core::{entities::properties::prop::Prop, Direction}; - use raphtory_tests::assertions::{ - assert_filter_nodes_results, assert_select_nodes_results, TestVariants, - }; - - fn sort_vids(mut vids: Vec) -> Vec { - vids.sort(); - vids - } - - fn candidates_with_history_after_filtering<'a, G: GraphViewOps<'a>>( - graph: &G, - candidate_nodes: Vec, - ) -> Vec { - let subgraph = graph.subgraph(candidate_nodes); - sort_vids( - subgraph - .nodes() - .into_iter() - .filter(|n| !n.history().is_empty()) - .map(|n| n.node) - .collect(), - ) - } - - fn assert_filter( - graph: &Graph, - filter: CF, - metric: Direction, - manual_expr: F, - context: &str, - ) where - CF: CreateFilter + Clone, - F: Fn(usize) -> bool + Copy, - { - let expected_select_nodes = graph - .nodes() - .into_iter() - .filter(|n| { - manual_expr(match metric { - Direction::BOTH => n.degree(), - Direction::IN => n.in_degree(), - Direction::OUT => n.out_degree(), - }) - }) - .map(|n| n.node) - .collect::>(); - - let expected_filter_nodes = - candidates_with_history_after_filtering(graph, expected_select_nodes.clone()); - - let filtered_event_graph = graph.filter(filter.clone()).unwrap(); - let filtered_event_nodes = sort_vids( - filtered_event_graph - .nodes() - .into_iter() - .map(|n| n.node) - .collect(), - ); - assert_eq!( - filtered_event_nodes, expected_filter_nodes, - "{} failed for event graph", - context - ); - - let selected_event_nodes = sort_vids( - graph - .nodes() - .select(filter.clone()) - .unwrap() - .into_iter() - .map(|n| n.node) - .collect(), - ); - assert_eq!( - selected_event_nodes, expected_select_nodes, - "{} failed for event graph select", - context - ); - - let filtered_persistent_graph = graph.persistent_graph().filter(filter.clone()).unwrap(); - let filtered_persistent_nodes = sort_vids( - filtered_persistent_graph - .nodes() - .into_iter() - .map(|n| n.node) - .collect(), - ); - assert_eq!( - filtered_persistent_nodes, expected_filter_nodes, - "{} failed for persistent graph", - context - ); - - let selected_persistent_nodes = sort_vids( - graph - .persistent_graph() - .nodes() - .select(filter) - .unwrap() - .into_iter() - .map(|n| n.node) - .collect(), - ); - assert_eq!( - selected_persistent_nodes, expected_select_nodes, - "{} failed for persistent graph select", - context - ); - } - - fn degree_graph_with_add_node_and_add_edge() -> Graph { - let graph = degree_graph_with_add_edge_only(); - let add_nodes = [ - (0, "1", Some("layer_a")), - (0, "7", None), - (0, "8", None), - (3, "9", Some("layer_a")), - (4, "9", Some("layer_c")), - (5, "10", Some("layer_b")), - (6, "10", Some("layer_e")), - (7, "11", Some("layer_d")), - (8, "12", Some("layer_f")), - (9, "12", Some("layer_c")), - ]; - for (t, id, layer) in add_nodes { - graph.add_node(t, id, NO_PROPS, None, layer).unwrap(); - } - graph - } - - fn degree_graph_with_add_edge_only() -> Graph { - let graph = Graph::new(); - - let edges = [ - (1, "1", "2", "layer_a"), - (1, "1", "3", "layer_b"), - (1, "1", "4", "layer_a"), - (1, "1", "5", "layer_b"), - (1, "1", "6", "layer_a"), - (2, "2", "1", "layer_b"), - (2, "2", "3", "layer_a"), - (2, "2", "4", "layer_b"), - (2, "2", "5", "layer_a"), - (3, "3", "1", "layer_a"), - (3, "3", "4", "layer_b"), - (3, "3", "5", "layer_a"), - (4, "4", "1", "layer_b"), - (4, "4", "2", "layer_a"), - (5, "5", "1", "layer_b"), - (6, "6", "1", "layer_a"), - (6, "4", "3", "layer_b"), - (6, "5", "2", "layer_a"), - (6, "6", "2", "layer_b"), - (6, "5", "3", "layer_a"), - (7, "2", "6", "layer_c"), - (7, "3", "6", "layer_d"), - (7, "6", "4", "layer_e"), - (7, "1", "5", "layer_f"), - (8, "3", "2", "layer_c"), - (8, "4", "6", "layer_d"), - (8, "2", "5", "layer_e"), - (8, "6", "3", "layer_f"), - (9, "5", "4", "layer_c"), - (9, "4", "5", "layer_d"), - (9, "2", "4", "layer_e"), - (9, "3", "1", "layer_f"), - ]; - for (t, src, dst, layer) in edges { - graph.add_edge(t, src, dst, NO_PROPS, Some(layer)).unwrap(); - } - - graph - } - - // Property-based tests for degree filtering - proptest! { - #[test] - fn prop_degree_filter_both_direction_comparison(threshold in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - - assert_filter( - &graph, - NodeFilter.degree().lt(threshold), - Direction::BOTH, - |d| d < threshold as usize, - &format!("BOTH < {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.degree().le(threshold), - Direction::BOTH, - |d| d <= threshold as usize, - &format!("BOTH <= {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.degree().eq(threshold), - Direction::BOTH, - |d| d == threshold as usize, - &format!("BOTH == {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.degree().ne(threshold), - Direction::BOTH, - |d| d != threshold as usize, - &format!("BOTH != {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.degree().ge(threshold), - Direction::BOTH, - |d| d >= threshold as usize, - &format!("BOTH >= {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.degree().gt(threshold), - Direction::BOTH, - |d| d > threshold as usize, - &format!("BOTH > {}", threshold), - ); - } - - #[test] - fn prop_degree_filter_in_direction_comparison(threshold in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - - assert_filter( - &graph, - NodeFilter.in_degree().lt(threshold), - Direction::IN, - |d| d < threshold as usize, - &format!("IN < {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.in_degree().le(threshold), - Direction::IN, - |d| d <= threshold as usize, - &format!("IN <= {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.in_degree().eq(threshold), - Direction::IN, - |d| d == threshold as usize, - &format!("IN == {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.in_degree().ne(threshold), - Direction::IN, - |d| d != threshold as usize, - &format!("IN != {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.in_degree().ge(threshold), - Direction::IN, - |d| d >= threshold as usize, - &format!("IN >= {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.in_degree().gt(threshold), - Direction::IN, - |d| d > threshold as usize, - &format!("IN > {}", threshold), - ); - } - - #[test] - fn prop_degree_filter_out_direction_comparison(threshold in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - - assert_filter( - &graph, - NodeFilter.out_degree().lt(threshold), - Direction::OUT, - |d| d < threshold as usize, - &format!("OUT < {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.out_degree().le(threshold), - Direction::OUT, - |d| d <= threshold as usize, - &format!("OUT <= {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.out_degree().eq(threshold), - Direction::OUT, - |d| d == threshold as usize, - &format!("OUT == {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.out_degree().ne(threshold), - Direction::OUT, - |d| d != threshold as usize, - &format!("OUT != {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.out_degree().ge(threshold), - Direction::OUT, - |d| d >= threshold as usize, - &format!("OUT >= {}", threshold), - ); - - assert_filter( - &graph, - NodeFilter.out_degree().gt(threshold), - Direction::OUT, - |d| d > threshold as usize, - &format!("OUT > {}", threshold), - ); - } - - #[test] - fn prop_degree_filter_and(threshold in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - - assert_filter( - &graph, - NodeFilter.degree().gt(threshold).and(NodeFilter.degree().lt(threshold + 5)), - Direction::BOTH, - |d| d > threshold as usize && d < (threshold + 5) as usize, - &format!("BOTH > {} AND BOTH < {}", threshold, threshold + 5), - ); - - assert_filter( - &graph, - NodeFilter.in_degree().gt(threshold).and(NodeFilter.in_degree().lt(threshold + 5)), - Direction::IN, - |d| d > threshold as usize && d < (threshold + 5) as usize, - &format!("IN > {} AND IN < {}", threshold, threshold + 5), - ); - - assert_filter( - &graph, - NodeFilter.out_degree().gt(threshold).and(NodeFilter.out_degree().lt(threshold + 5)), - Direction::OUT, - |d| d > threshold as usize && d < (threshold + 5) as usize, - &format!("OUT > {} AND OUT < {}", threshold, threshold + 5), - ); - } - - #[test] - fn prop_degree_filter_or(threshold in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - - assert_filter( - &graph, - NodeFilter.degree().lt(threshold).or(NodeFilter.degree().gt(threshold + 5)), - Direction::BOTH, - |d| d < threshold as usize || d > (threshold + 5) as usize, - &format!("BOTH < {} OR BOTH > {}", threshold, threshold + 5), - ); - - assert_filter( - &graph, - NodeFilter.in_degree().lt(threshold).or(NodeFilter.in_degree().gt(threshold + 5)), - Direction::IN, - |d| d < threshold as usize || d > (threshold + 5) as usize, - &format!("IN < {} OR IN > {}", threshold, threshold + 5), - ); - - assert_filter( - &graph, - NodeFilter.out_degree().lt(threshold).or(NodeFilter.out_degree().gt(threshold + 5)), - Direction::OUT, - |d| d < threshold as usize || d > (threshold + 5) as usize, - &format!("OUT < {} OR OUT > {}", threshold, threshold + 5), - ); - } - - #[test] - fn prop_degree_filter_not(threshold in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - - assert_filter( - &graph, - NodeFilter.degree().lt(threshold).or(NodeFilter.degree().gt(threshold + 5).not()), - Direction::BOTH, - |d| d < threshold as usize || d <= (threshold + 5) as usize, - &format!("BOTH < {} OR BOTH > {}", threshold, threshold + 5), - ); - - assert_filter( - &graph, - NodeFilter.in_degree().lt(threshold).or(NodeFilter.in_degree().gt(threshold + 5).not()), - Direction::IN, - |d| d < threshold as usize || d <= (threshold + 5) as usize, - &format!("IN < {} OR IN > {}", threshold, threshold + 5), - ); - - assert_filter( - &graph, - NodeFilter.out_degree().lt(threshold).or(NodeFilter.out_degree().gt(threshold + 5).not()), - Direction::OUT, - |d| d < threshold as usize || d <= (threshold + 5) as usize, - &format!("OUT < {} OR OUT > {}", threshold, threshold + 5), - ); - } - - #[test] - fn prop_degree_filter_is_in(val1 in 0u64..15, val2 in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let set = [val1, val2]; - - assert_filter( - &graph, - NodeFilter.degree().is_in(vec![Prop::U64(val1), Prop::U64(val2)]), - Direction::BOTH, - |d| set.contains(&(d as u64)), - &format!("BOTH is_in({}, {})", val1, val2), - ); - - assert_filter( - &graph, - NodeFilter.in_degree().is_in(vec![Prop::U64(val1), Prop::U64(val2)]), - Direction::IN, - |d| set.contains(&(d as u64)), - &format!("IN is_in({}, {})", val1, val2), - ); - - assert_filter( - &graph, - NodeFilter.out_degree().is_in(vec![Prop::U64(val1), Prop::U64(val2)]), - Direction::OUT, - |d| set.contains(&(d as u64)), - &format!("OUT is_in({}, {})", val1, val2), - ); - } - - #[test] - fn prop_degree_filter_is_not_in(val1 in 0u64..15, val2 in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let set = [val1, val2]; - - assert_filter( - &graph, - NodeFilter - .degree() - .is_not_in(vec![Prop::U64(val1), Prop::U64(val2)]), - Direction::BOTH, - |d| !set.contains(&(d as u64)), - &format!("BOTH is_not_in({}, {})", val1, val2), - ); - - assert_filter( - &graph, - NodeFilter - .in_degree() - .is_not_in(vec![Prop::U64(val1), Prop::U64(val2)]), - Direction::IN, - |d| !set.contains(&(d as u64)), - &format!("IN is_not_in({}, {})", val1, val2), - ); - - assert_filter( - &graph, - NodeFilter - .out_degree() - .is_not_in(vec![Prop::U64(val1), Prop::U64(val2)]), - Direction::OUT, - |d| !set.contains(&(d as u64)), - &format!("OUT is_not_in({}, {})", val1, val2), - ); - } - } - - #[test] - fn test_degree_filter_with_invalid_expressions() { - let graph = degree_graph_with_add_node_and_add_edge(); - let invalid_filters = vec![ - NodeFilter.degree().is_none(), - NodeFilter.degree().is_some(), - NodeFilter.degree().starts_with("1"), - NodeFilter.degree().ends_with("1"), - NodeFilter.degree().contains("1"), - NodeFilter.degree().not_contains("1"), - NodeFilter.degree().fuzzy_search("1", 1, false), - NodeFilter.in_degree().is_none(), - NodeFilter.in_degree().is_some(), - NodeFilter.in_degree().starts_with("1"), - NodeFilter.in_degree().ends_with("1"), - NodeFilter.in_degree().contains("1"), - NodeFilter.in_degree().not_contains("1"), - NodeFilter.in_degree().fuzzy_search("1", 1, false), - NodeFilter.out_degree().is_none(), - NodeFilter.out_degree().is_some(), - NodeFilter.out_degree().starts_with("1"), - NodeFilter.out_degree().ends_with("1"), - NodeFilter.out_degree().contains("1"), - NodeFilter.out_degree().not_contains("1"), - NodeFilter.out_degree().fuzzy_search("1", 1, false), - NodeFilter.degree().any().eq(1u64), - NodeFilter.degree().all().eq(1u64), - NodeFilter.degree().len().gt(0u64), - NodeFilter.degree().sum().eq(1u64), - NodeFilter.degree().avg().eq(1u64), - NodeFilter.degree().min().eq(1u64), - NodeFilter.degree().max().eq(1u64), - NodeFilter.degree().first().eq(1u64), - NodeFilter.degree().last().eq(1u64), - NodeFilter.in_degree().any().eq(1u64), - NodeFilter.in_degree().all().eq(1u64), - NodeFilter.in_degree().len().gt(0u64), - NodeFilter.in_degree().sum().eq(1u64), - NodeFilter.in_degree().avg().eq(1u64), - NodeFilter.in_degree().min().eq(1u64), - NodeFilter.in_degree().max().eq(1u64), - NodeFilter.in_degree().first().eq(1u64), - NodeFilter.in_degree().last().eq(1u64), - NodeFilter.out_degree().any().eq(1u64), - NodeFilter.out_degree().all().eq(1u64), - NodeFilter.out_degree().len().gt(0u64), - NodeFilter.out_degree().sum().eq(1u64), - NodeFilter.out_degree().avg().eq(1u64), - NodeFilter.out_degree().min().eq(1u64), - NodeFilter.out_degree().max().eq(1u64), - NodeFilter.out_degree().first().eq(1u64), - NodeFilter.out_degree().last().eq(1u64), - ]; - - for filter in invalid_filters { - assert!( - matches!(graph.filter(filter), Err(GraphError::InvalidFilter(_))), - "expected InvalidFilter for unsupported degree filter operation" - ); - } - } - - proptest! { - #[test] - fn prop_degree_filter_with_string_threshold(threshold in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_str = threshold.to_string(); - let parsed_str = threshold_str.parse::().unwrap(); - - assert_filter(&graph, NodeFilter.degree().lt(threshold_str.clone()), Direction::BOTH, |d| d < parsed_str as usize, "BOTH < string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().le(threshold_str.clone()), Direction::BOTH, |d| d <= parsed_str as usize, "BOTH <= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().eq(threshold_str.clone()), Direction::BOTH, |d| d == parsed_str as usize, "BOTH == string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().ne(threshold_str.clone()), Direction::BOTH, |d| d != parsed_str as usize, "BOTH != string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().ge(threshold_str.clone()), Direction::BOTH, |d| d >= parsed_str as usize, "BOTH >= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().gt(threshold_str.clone()), Direction::BOTH, |d| d > parsed_str as usize, "BOTH > string threshold parsed to u64"); - - assert_filter(&graph, NodeFilter.in_degree().lt(threshold_str.clone()), Direction::IN, |d| d < parsed_str as usize, "IN < string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().le(threshold_str.clone()), Direction::IN, |d| d <= parsed_str as usize, "IN <= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().eq(threshold_str.clone()), Direction::IN, |d| d == parsed_str as usize, "IN == string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().ne(threshold_str.clone()), Direction::IN, |d| d != parsed_str as usize, "IN != string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().ge(threshold_str.clone()), Direction::IN, |d| d >= parsed_str as usize, "IN >= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().gt(threshold_str.clone()), Direction::IN, |d| d > parsed_str as usize, "IN > string threshold parsed to u64"); - - assert_filter(&graph, NodeFilter.out_degree().lt(threshold_str.clone()), Direction::OUT, |d| d < parsed_str as usize, "OUT < string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().le(threshold_str.clone()), Direction::OUT, |d| d <= parsed_str as usize, "OUT <= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().eq(threshold_str.clone()), Direction::OUT, |d| d == parsed_str as usize, "OUT == string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().ne(threshold_str.clone()), Direction::OUT, |d| d != parsed_str as usize, "OUT != string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().ge(threshold_str.clone()), Direction::OUT, |d| d >= parsed_str as usize, "OUT >= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().gt(threshold_str), Direction::OUT, |d| d > parsed_str as usize, "OUT > string threshold parsed to u64"); - } - - #[test] - fn prop_degree_filter_with_float_threshold(threshold in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_float = threshold as f64; - let parsed_float = threshold_float as u64; - - assert_filter(&graph, NodeFilter.degree().lt(threshold_float), Direction::BOTH, |d| d < parsed_float as usize, "BOTH < float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().le(threshold_float), Direction::BOTH, |d| d <= parsed_float as usize, "BOTH <= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().eq(threshold_float), Direction::BOTH, |d| d == parsed_float as usize, "BOTH == float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().ne(threshold_float), Direction::BOTH, |d| d != parsed_float as usize, "BOTH != float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().ge(threshold_float), Direction::BOTH, |d| d >= parsed_float as usize, "BOTH >= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().gt(threshold_float), Direction::BOTH, |d| d > parsed_float as usize, "BOTH > float threshold cast to u64"); - - assert_filter(&graph, NodeFilter.in_degree().lt(threshold_float), Direction::IN, |d| d < parsed_float as usize, "IN < float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().le(threshold_float), Direction::IN, |d| d <= parsed_float as usize, "IN <= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().eq(threshold_float), Direction::IN, |d| d == parsed_float as usize, "IN == float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().ne(threshold_float), Direction::IN, |d| d != parsed_float as usize, "IN != float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().ge(threshold_float), Direction::IN, |d| d >= parsed_float as usize, "IN >= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().gt(threshold_float), Direction::IN, |d| d > parsed_float as usize, "IN > float threshold cast to u64"); - - assert_filter(&graph, NodeFilter.out_degree().lt(threshold_float), Direction::OUT, |d| d < parsed_float as usize, "OUT < float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().le(threshold_float), Direction::OUT, |d| d <= parsed_float as usize, "OUT <= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().eq(threshold_float), Direction::OUT, |d| d == parsed_float as usize, "OUT == float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().ne(threshold_float), Direction::OUT, |d| d != parsed_float as usize, "OUT != float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().ge(threshold_float), Direction::OUT, |d| d >= parsed_float as usize, "OUT >= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().gt(threshold_float), Direction::OUT, |d| d > parsed_float as usize, "OUT > float threshold cast to u64"); - } - - #[test] - fn prop_degree_filter_with_string_is_in(threshold_a in 0u64..15, threshold_b in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_a_str = threshold_a.to_string(); - let threshold_b_str = threshold_b.to_string(); - let parsed_a = threshold_a_str.parse::().unwrap(); - let parsed_b = threshold_b_str.parse::().unwrap(); - let set = [parsed_a, parsed_b]; - - assert_filter(&graph, NodeFilter.degree().is_in(vec![threshold_a_str.clone().into_prop(), threshold_b_str.clone().into_prop()]), Direction::BOTH, |d| set.contains(&(d as u64)), "BOTH is_in(string thresholds parsed to u64)"); - assert_filter(&graph, NodeFilter.in_degree().is_in(vec![threshold_a_str.clone().into_prop(), threshold_b_str.clone().into_prop()]), Direction::IN, |d| set.contains(&(d as u64)), "IN is_in(string thresholds parsed to u64)"); - assert_filter(&graph, NodeFilter.out_degree().is_in(vec![threshold_a_str.into_prop(), threshold_b_str.into_prop()]), Direction::OUT, |d| set.contains(&(d as u64)), "OUT is_in(string thresholds parsed to u64)"); - } - - #[test] - fn prop_degree_filter_with_string_is_not_in(threshold_a in 0u64..15, threshold_b in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_a_str = threshold_a.to_string(); - let threshold_b_str = threshold_b.to_string(); - let parsed_a = threshold_a_str.parse::().unwrap(); - let parsed_b = threshold_b_str.parse::().unwrap(); - let set = [parsed_a, parsed_b]; - - assert_filter(&graph, NodeFilter.degree().is_not_in(vec![threshold_a_str.clone().into_prop(), threshold_b_str.clone().into_prop()]), Direction::BOTH, |d| !set.contains(&(d as u64)), "BOTH is_not_in(string thresholds parsed to u64)"); - assert_filter(&graph, NodeFilter.in_degree().is_not_in(vec![threshold_a_str.clone().into_prop(), threshold_b_str.clone().into_prop()]), Direction::IN, |d| !set.contains(&(d as u64)), "IN is_not_in(string thresholds parsed to u64)"); - assert_filter(&graph, NodeFilter.out_degree().is_not_in(vec![threshold_a_str.into_prop(), threshold_b_str.into_prop()]), Direction::OUT, |d| !set.contains(&(d as u64)), "OUT is_not_in(string thresholds parsed to u64)"); - } - - #[test] - fn prop_degree_filter_with_float_is_in(threshold_a in 0u64..15, threshold_b in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_a_float = threshold_a as f64; - let threshold_b_float = threshold_b as f64; - let parsed_a = threshold_a_float as u64; - let parsed_b = threshold_b_float as u64; - let set = [parsed_a, parsed_b]; - - assert_filter(&graph, NodeFilter.degree().is_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::BOTH, |d| set.contains(&(d as u64)), "BOTH is_in(float thresholds cast to u64)"); - assert_filter(&graph, NodeFilter.in_degree().is_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::IN, |d| set.contains(&(d as u64)), "IN is_in(float thresholds cast to u64)"); - assert_filter(&graph, NodeFilter.out_degree().is_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::OUT, |d| set.contains(&(d as u64)), "OUT is_in(float thresholds cast to u64)"); - } - - #[test] - fn prop_degree_filter_with_float_is_not_in(threshold_a in 0u64..15, threshold_b in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_a_float = threshold_a as f64; - let threshold_b_float = threshold_b as f64; - let parsed_a = threshold_a_float as u64; - let parsed_b = threshold_b_float as u64; - let set = [parsed_a, parsed_b]; - - assert_filter(&graph, NodeFilter.degree().is_not_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::BOTH, |d| !set.contains(&(d as u64)), "BOTH is_not_in(float thresholds cast to u64)"); - assert_filter(&graph, NodeFilter.in_degree().is_not_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::IN, |d| !set.contains(&(d as u64)), "IN is_not_in(float thresholds cast to u64)"); - assert_filter(&graph, NodeFilter.out_degree().is_not_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::OUT, |d| !set.contains(&(d as u64)), "OUT is_not_in(float thresholds cast to u64)"); - } - - #[test] - fn prop_degree_filter_invalid_non_numeric_string_values(value_a in "[a-zA-Z]{1,8}", value_b in "[a-zA-Z]{1,8}") { - let graph = degree_graph_with_add_node_and_add_edge(); - - let invalid_filters = vec![ - NodeFilter.degree().lt(value_a.clone()), - NodeFilter.degree().le(value_a.clone()), - NodeFilter.degree().eq(value_a.clone()), - NodeFilter.degree().ne(value_a.clone()), - NodeFilter.degree().ge(value_a.clone()), - NodeFilter.degree().gt(value_a.clone()), - NodeFilter.in_degree().lt(value_a.clone()), - NodeFilter.in_degree().le(value_a.clone()), - NodeFilter.in_degree().eq(value_a.clone()), - NodeFilter.in_degree().ne(value_a.clone()), - NodeFilter.in_degree().ge(value_a.clone()), - NodeFilter.in_degree().gt(value_a.clone()), - NodeFilter.out_degree().lt(value_a.clone()), - NodeFilter.out_degree().le(value_a.clone()), - NodeFilter.out_degree().eq(value_a.clone()), - NodeFilter.out_degree().ne(value_a.clone()), - NodeFilter.out_degree().ge(value_a.clone()), - NodeFilter.out_degree().gt(value_a.clone()), - NodeFilter.degree().is_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.degree().is_not_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.in_degree().is_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.in_degree().is_not_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.out_degree().is_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - NodeFilter.out_degree().is_not_in(vec![value_a.clone().into_prop(), value_b.clone().into_prop()]), - ]; - - for filter in invalid_filters { - assert!( - matches!(graph.filter(filter), Err(GraphError::InvalidFilter(_))), - "expected InvalidFilter for non-numeric string values" - ); - } - } - } - - #[test] - fn test_node_list_is_preserved() { - let graph = init_nodes_graph(Graph::new()); - let nodes = graph - .nodes() - .after(5) - .select(NodeFilter::node_type().contains("x")) - .unwrap(); - let degrees = nodes.degree(); - let degrees_collected = degrees.compute(); - assert_eq!(degrees, degrees_collected); - } - - #[test] - fn test_filter_nodes_for_node_name_eq() { - let filter = NodeFilter::name().eq("3"); - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_name_ne() { - let filter = NodeFilter::name().ne("2"); - let expected_results = vec!["1", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_name_in() { - let filter = NodeFilter::name().is_in(vec!["1"]); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::name().is_in(vec![""]); - let expected_results = Vec::<&str>::new(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::name().is_in(vec!["2", "3"]); - let expected_results = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_name_not_in() { - let filter = NodeFilter::name().is_not_in(vec!["1"]); - let expected_results = vec!["2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::name().is_not_in(vec![""]); - let expected_results = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_type_eq() { - let filter = NodeFilter::node_type().eq("fire_nation"); - let expected_results = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_type_ne() { - let filter = NodeFilter::node_type().ne("fire_nation"); - let expected_results = vec!["2", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_type_in() { - let filter = NodeFilter::node_type().is_in(vec!["fire_nation"]); - let expected_results = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::node_type().is_in(vec!["fire_nation", "air_nomads"]); - let expected_results = vec!["1", "2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_type_not_in() { - let filter = NodeFilter::node_type().is_not_in(vec!["fire_nation"]); - let expected_results = vec!["2", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_type_starts_with() { - let filter = NodeFilter::node_type().starts_with("fire"); - let expected_results = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::node_type().starts_with("rocket"); - let expected_results = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_type_ends_with() { - let filter = NodeFilter::node_type().ends_with("nomads"); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::node_type().ends_with("circle"); - let expected_results = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_type_contains() { - let filter = NodeFilter::node_type().contains("fire"); - let expected_results = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_node_type_contains_not() { - let filter = NodeFilter::node_type().not_contains("fire"); - let expected_results = vec!["2", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_fuzzy_search() { - let filter = NodeFilter::node_type().fuzzy_search("fire", 2, true); - let expected_results: Vec<&str> = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::node_type().fuzzy_search("fire", 2, false); - let expected_results: Vec<&str> = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::node_type().fuzzy_search("air_noma", 2, false); - let expected_results: Vec<&str> = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_not_node_type() { - let filter = NodeFilter::node_type().is_not_in(vec!["fire_nation"]).not(); - let expected_results = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_eq_node_id() { - let filter = NodeFilter::id().eq("1"); - let expected_results = vec!["1"]; - - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::id().eq(1); - let expected_results = vec!["1"]; - - assert_filter_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - assert_filter_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_ne_node_id() { - let filter = NodeFilter::id().ne("1"); - let expected_results = vec!["2", "3", "4"]; - - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::id().ne(1); - let expected_results = vec!["2", "3", "4"]; - - assert_filter_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_is_in_node_id() { - let filter = NodeFilter::id().is_in(vec!["1", "3", "6"]); - let expected_results = vec!["1", "3"]; - - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::id().is_in(vec![1, 3, 6]); - let expected_results = vec!["1", "3"]; - - assert_filter_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_is_not_in_node_id() { - let filter = NodeFilter::id().is_not_in(vec!["1", "3", "6"]); - let expected_results = vec!["2", "4"]; - - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::id().is_not_in(vec![1, 3, 6]); - let expected_results = vec!["2", "4"]; - - assert_filter_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_lt_node_id() { - let filter = NodeFilter::id().lt(2); - let expected_results = vec!["1"]; - - assert_filter_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_le_node_id() { - let filter = NodeFilter::id().le(3); - let expected_results = vec!["1", "2", "3"]; - - assert_filter_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_gt_node_id() { - let filter = NodeFilter::id().gt(2); - let expected_results = vec!["3", "4"]; - - assert_filter_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_ge_node_id() { - let filter = NodeFilter::id().ge(2); - let expected_results = vec!["2", "3", "4"]; - - assert_filter_nodes_results( - init_nodes_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_starts_with_node_id() { - let filter = NodeFilter::id().starts_with("France"); - let expected_results = vec!["France Paris"]; - assert_filter_nodes_results( - init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_ends_with_node_id() { - let filter = NodeFilter::id().ends_with("wo"); - let expected_results = vec!["Two"]; - assert_filter_nodes_results( - init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_contains_node_id() { - let filter = NodeFilter::id().contains("o"); - let expected_results = vec!["London", "Tokyo", "Two"]; - assert_filter_nodes_results( - init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_not_contains_node_id() { - let filter = NodeFilter::id().not_contains("o"); - let expected_results = vec!["France Paris"]; - assert_filter_nodes_results( - init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_is_in_node_id_str() { - let filter = NodeFilter::id().is_in(vec!["London", "Tokyo"]); - let expected_results = vec!["London", "Tokyo"]; - assert_filter_nodes_results( - init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_is_not_in_node_id_str() { - let filter = NodeFilter::id().is_not_in(vec!["London", "Tokyo"]); - let expected_results = vec!["France Paris", "Two"]; - assert_filter_nodes_results( - init_nodes_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_is_active_node_window() { - let filter = NodeFilter.window(1, 10).is_active(); - let expected_results = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_is_active_node_window_not() { - let filter = NodeFilter.window(1, 10).is_active().not(); - let expected_results = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_is_active_node_latest() { - let filter = NodeFilter.latest().is_active(); - let expected_results = vec!["1", "2", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_filter_by_column() { - let graph = Graph::new(); - graph.add_node(1, 1, NO_PROPS, None, None).unwrap(); - graph.add_node(1, 2, NO_PROPS, None, None).unwrap(); - graph.add_node(1, 3, NO_PROPS, None, None).unwrap(); - graph.add_node(1, 4, NO_PROPS, None, None).unwrap(); - graph.add_node(1, 5, NO_PROPS, None, None).unwrap(); - - let mask = alternating_mask(&graph); - let expected_nodes: Vec<_> = graph - .nodes() - .name() - .iter_values() - .skip(1) - .step_by(2) - .collect(); - - let filtered = graph - .filter(NodeFilter::by_column(&mask, "bool_col").unwrap()) - .unwrap(); - - let names = filtered - .nodes() - .iter() - .map(|n| n.id().to_string()) - .collect::>(); - - assert_eq!(names, expected_nodes); - - let filtered = graph - .nodes() - .select(NodeFilter::by_column(&mask, "bool_col").unwrap()) - .unwrap(); - - let names = filtered - .iter() - .map(|n| n.id().to_string()) - .collect::>(); - - assert_eq!(names, expected_nodes); - } - - #[test] - fn test_is_active_node_snapshot_at() { - let filter = NodeFilter.snapshot_at(2).is_active(); - let expected_results = vec!["2"]; - assert_select_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } -} - -mod test_node_property_filter { - use crate::filter_tests::test_filters::{ - init_nodes_graph, init_nodes_layers_graph, IdentityGraphTransformer, - }; - use raphtory::db::graph::views::filter::model::{ - graph_filter::GraphFilter, - node_filter::NodeFilter, - not_filter::NotFilter, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - windowed_filter::Windowed, - ComposableFilter, PropertyFilterFactory, TemporalPropertyFilterFactory, ViewWrapOps, - }; - use raphtory_api::core::entities::properties::prop::Prop; - use raphtory_tests::assertions::{assert_filter_nodes_results, TestVariants}; - use std::vec; - - #[test] - fn test_exact_match() { - // let filter = NodeFilter.degree > 5 - let filter = NodeFilter.property("p10").eq("Paper_airplane"); - let expected_results = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p10").eq(""); - let expected_results = Vec::<&str>::new(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_not_exact_match() { - let filter = NodeFilter.property("p10").eq("Paper"); - let expected_results: Vec<&str> = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_eq() { - let filter = NodeFilter.property("p2").eq(2u64); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p30").temporal().first().eq("Old_boat"); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p20").temporal().all().eq("Gold_ship"); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_ne() { - let filter = NodeFilter.property("p2").ne(2u64); - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p30").temporal().first().ne("Old_boat"); - let expected_results = vec!["1", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p1").temporal().all().ne("Gold_ship"); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_lt() { - let filter = NodeFilter.property("p2").lt(10u64); - let expected_results = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p40").temporal().first().lt(10u64); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p9").temporal().all().lt(10u64); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_le() { - let filter = NodeFilter.property("p2").le(6u64); - let expected_results = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p9").temporal().first().le(10u64); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p2").temporal().all().le(10u64); - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_gt() { - let filter = NodeFilter.property("p2").gt(2u64); - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p40").temporal().first().gt(5u64); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p9").temporal().all().gt(1u64); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_ge() { - let filter = NodeFilter.property("p2").ge(2u64); - let expected_results = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p40").temporal().first().ge(5u64); - let expected_results = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p40").temporal().all().ge(5u64); - let expected_results = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_in() { - let filter = NodeFilter.property("p2").is_in(vec![Prop::U64(6)]); - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p2") - .is_in(vec![Prop::U64(2), Prop::U64(6)]); - let expected_results = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p40") - .temporal() - .first() - .is_in(vec![Prop::U64(5)]); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p2") - .temporal() - .any() - .is_in(vec![Prop::U64(2)]); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_not_in() { - let filter = NodeFilter.property("p2").is_not_in(vec![Prop::U64(6)]); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p40").is_not_in(vec![Prop::U64(6)]); - let expected_results = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p2") - .temporal() - .all() - .is_not_in(vec![Prop::U64(2)]); - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_is_some() { - let filter = NodeFilter.property("p2").is_some(); - let expected_results = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p40").is_some(); - let expected_results = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_is_none() { - let filter = NodeFilter.property("p2").is_none(); - let expected_results = vec!["1", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p40").is_none(); - let expected_results = vec!["3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_starts_with() { - let filter = NodeFilter.property("p10").starts_with("Pa"); - let expected_results: Vec<&str> = vec!["1", "2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .any() - .starts_with("Pap"); - let expected_results: Vec<&str> = vec!["1", "2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .last() - .starts_with("Pape"); - let expected_results: Vec<&str> = vec!["1", "2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .last() - .starts_with("Yohan"); - let expected_results: Vec<&str> = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p30") - .temporal() - .first() - .starts_with("Gold"); - let expected_results: Vec<&str> = vec!["1", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p20") - .temporal() - .all() - .starts_with("Gold"); - let expected_results: Vec<&str> = vec!["1", "2", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_ends_with() { - let filter = NodeFilter.property("p10").ends_with("lane"); - let expected_results: Vec<&str> = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .any() - .ends_with("ship"); - let expected_results: Vec<&str> = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .last() - .ends_with("ane"); - let expected_results: Vec<&str> = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .last() - .ends_with("Jerry"); - let expected_results: Vec<&str> = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p20") - .temporal() - .first() - .ends_with("boat"); - let expected_results: Vec<&str> = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p20") - .temporal() - .all() - .ends_with("ship"); - let expected_results: Vec<&str> = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_contains() { - let filter = NodeFilter.property("p10").contains("Paper"); - let expected_results: Vec<&str> = vec!["1", "2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .any() - .contains("Paper"); - let expected_results: Vec<&str> = vec!["1", "2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .last() - .contains("Paper"); - let expected_results: Vec<&str> = vec!["1", "2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p30") - .temporal() - .first() - .contains("Old"); - let expected_results: Vec<&str> = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p30").temporal().all().contains("Gold"); - let expected_results: Vec<&str> = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_property_contains_not() { - let filter = NodeFilter.property("p10").not_contains("ship"); - let expected_results: Vec<&str> = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .any() - .not_contains("ship"); - let expected_results: Vec<&str> = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p10") - .temporal() - .last() - .not_contains("ship"); - let expected_results: Vec<&str> = vec!["1", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p30") - .temporal() - .first() - .not_contains("Old"); - let expected_results: Vec<&str> = vec!["1", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p30") - .temporal() - .all() - .not_contains("boat"); - let expected_results: Vec<&str> = vec!["1", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_not_property() { - let filter = NotFilter(NodeFilter.property("p10").contains("Paper")); - let expected_results: Vec<&str> = vec!["4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p10").contains("Paper").not(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_temporal_property_sum() { - let filter = NodeFilter.property("p9").temporal().sum().eq(15u64); - let expected_results: Vec<&str> = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_temporal_property_avg() { - let filter = NodeFilter.property("p2").temporal().avg().le(10f64); - let expected_results: Vec<&str> = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_temporal_property_min() { - let filter = NodeFilter.property("p40").temporal().min().is_in(vec![ - Prop::U64(5), - Prop::U64(10), - Prop::U64(20), - ]); - let expected_results: Vec<&str> = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_temporal_property_max() { - let filter = NodeFilter.property("p3").temporal().max().is_not_in(vec![ - Prop::U64(5), - Prop::U64(10), - Prop::U64(20), - ]); - let expected_results: Vec<&str> = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_nodes_for_temporal_property_len() { - let filter = NodeFilter.property("p2").temporal().len().le(5u64); - let expected_results: Vec<&str> = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_nodes_window_filter() { - let filter = NodeFilter - .window(1, 3) - .property("p2") - .temporal() - .sum() - .ge(2u64); - - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - // Wider window includes node 3 - let filter = NodeFilter - .window(1, 5) - .property("p2") - .temporal() - .sum() - .ge(2u64); - - let expected_results = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_nodes_window_filter_on_non_temporal_property() { - let filter1 = NodeFilter.window(1, 2).property("p1").eq("shivam_kapoor"); - let filter2 = NodeFilter - .window(100, 200) - .property("p1") - .eq("shivam_kapoor"); - - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter1.clone(), - &expected_results, - TestVariants::All, - ); - - let expected_results = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter2.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - let filter = NodeFilter - .window(100, 200) - .property("p1") - .eq("shivam_kapoor"); - - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_nodes_window_filter_any_all_over_window() { - let filter = NodeFilter - .window(3, 5) - .property("p20") - .temporal() - .any() - .eq("Gold_boat"); - - let expected_results = vec!["4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .window(3, 5) - .property("p20") - .temporal() - .all() - .eq("Gold_boat"); - - let expected_results = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_nodes_window_filter_and() { - // Filters both node 1 and 3 - let filter1 = NodeFilter - .window(1, 4) - .property("p10") - .temporal() - .any() - .eq("Paper_airplane"); - - // Filters only node 3 - let filter2 = NodeFilter - .window(3, 6) - .property("p2") - .temporal() - .sum() - .eq(6u64); - - let filter = filter1.and(filter2); - - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_nodes_at_filter() { - // Only time=2 contributes; node 2 has p2=2 at t=2 - let filter = NodeFilter.at(2).property("p2").temporal().sum().eq(2u64); - - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - // Only time=3 contributes; node 3 has p2=6 at t=3 - let filter = NodeFilter.at(3).property("p2").temporal().sum().eq(6u64); - - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_nodes_after_filter() { - // after(2) means t >= 3 - let filter = NodeFilter.after(2).property("p2").temporal().sum().ge(6u64); - - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_nodes_before_filter() { - // before(3) means t <= 2 - let filter = NodeFilter - .before(3) - .property("p2") - .temporal() - .sum() - .eq(2u64); - - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - // And node 3 shouldn't match, because its p2=6 lives at t=3. - let filter = NodeFilter - .before(3) - .property("p2") - .temporal() - .sum() - .eq(6u64); - - let expected_results = vec![]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_nodes_latest_filter() { - // At latest time (currently t=4), only node 4 has p5=12 - let filter = NodeFilter.latest().property("p5").eq(12u64); - - let expected_results = vec!["4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_nodes_snapshot_at_semantics_event_graph() { - let t = 2; - - let filter_snapshot = NodeFilter.snapshot_at(t).property("p2").eq(2u64); - - let filter_before = NodeFilter.before(t + 1).property("p2").eq(2u64); - - let expected_results = vec!["2"]; - - // snapshot_at - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_snapshot.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - // before(t+1) - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_before.clone(), - &expected_results, - TestVariants::EventOnly, - ); - } - - #[test] - fn test_nodes_snapshot_at_semantics_persistent_graph() { - let t = 2; - - let filter_snapshot = NodeFilter.snapshot_at(t).property("p2").eq(2u64); - - let filter_at = NodeFilter.at(t).property("p2").eq(2u64); - - let expected_results = vec!["2"]; - - // snapshot_at - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_snapshot.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - - // at(t) - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_at.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_nodes_snapshot_latest_semantics_event_graph() { - let filter_snapshot_latest = NodeFilter - .snapshot_latest() - .property("p2") - .temporal() - .sum() - .ge(2u64); - - let filter_noop = NodeFilter.property("p2").temporal().sum().ge(2u64); - - // From your earlier window test, "2" and "3" are the ones with p2 values across time. - // If your underlying dataset changes, adjust this accordingly. - let expected_results = vec!["2", "3"]; - - // snapshot_latest - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_snapshot_latest.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - // no-op baseline - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_noop.clone(), - &expected_results, - TestVariants::EventOnly, - ); - } - - #[test] - fn test_nodes_snapshot_latest_semantics_persistent_graph() { - let filter_snapshot_latest = NodeFilter - .snapshot_latest() - .property("p1") - .eq("shivam_kapoor"); - - let filter_latest = NodeFilter.latest().property("p1").eq("shivam_kapoor"); - - let expected_results = vec!["1"]; - - // snapshot_latest - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_snapshot_latest.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - - // latest - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter_latest.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - #[ignore] // TODO: Enable this when node layer is supported - fn test_nodes_layer_filter() { - let filter = NodeFilter - .layer("_default") - .property("p2") - .temporal() - .sum() - .ge(2u64); - - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - #[ignore] // TODO: Enable this when node layer is supported - fn test_nodes_layer_then_window_ordering() { - // In layer "fire_nation" within window [1,4), node "1" matches p1 == "shivam_kapoor". - let filter = NodeFilter - .layer("fire_nation") - .window(1, 4) - .property("p1") - .eq("shivam_kapoor"); - - let expected_results = vec!["1"]; - - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - #[ignore] // TODO: Enable this when node layer is supported - fn test_nodes_window_then_layer_ordering() { - // Same semantics as above, but reversed chaining order. - let filter = NodeFilter - .window(1, 4) - .layer("fire_nation") - .property("p1") - .eq("shivam_kapoor"); - - let expected_results = vec!["1"]; - - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_graph_filter_window() { - let filter: Windowed = GraphFilter.window(1, 2); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = GraphFilter.window(1, 3); - let expected_results = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = GraphFilter.window(4, 6); - let expected_results = vec!["1", "2", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - let filter = GraphFilter.window(4, 6); - let expected_results = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_graph_filter_layer() { - // Note: Default layer is currently always included for nodes! - let filter = GraphFilter.layer("fire_nation"); - let expected_results = vec!["1", "3", "4"]; - assert_filter_nodes_results( - init_nodes_layers_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = GraphFilter.layer("air_nomads"); - let expected_results = vec!["2", "4"]; - assert_filter_nodes_results( - init_nodes_layers_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_graph_filter_window_then_layer() { - let filter = GraphFilter.window(1, 3).layer("fire_nation"); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_layers_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = GraphFilter.window(2, 3).layer("air_nomads"); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_layers_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_graph_filter_layer_then_window() { - let filter = GraphFilter.layer("fire_nation").window(1, 3); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_layers_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = GraphFilter.layer("air_nomads").window(2, 3); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_layers_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_graph_filter_at() { - let filter = GraphFilter.at(1); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = GraphFilter.at(2); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - let filter = GraphFilter.at(2); - let expected_results = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - - let filter = GraphFilter.at(3); - let expected_results = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_graph_filter_after() { - let filter = GraphFilter.after(3); - let expected_results = vec!["1", "2", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - let filter = GraphFilter.after(3); - let expected_results = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_graph_filter_before() { - let filter = GraphFilter.before(2); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = GraphFilter.before(3); - let expected_results = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_graph_filter_snapshot_at() { - let filter = GraphFilter.snapshot_at(1); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = GraphFilter.snapshot_at(3); - let expected_results = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = GraphFilter.snapshot_at(4); - let expected_results = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_graph_filter_snapshot_latest() { - let filter = GraphFilter.snapshot_latest(); - let expected_results = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_graph_filter_latest() { - let filter = GraphFilter.latest(); - let expected_results = vec!["1", "2", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - let filter = GraphFilter.latest(); - let expected_results = vec!["1", "2", "3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } -} - -mod test_node_composite_filter { - use crate::filter_tests::test_filters::{ - init_edges_graph, init_nodes_graph, IdentityGraphTransformer, - }; - use raphtory::{ - db::graph::views::filter::model::{ - node_filter::ops::NodeFilterOps, property_filter::ops::PropertyFilterOps, - ComposableFilter, PropertyFilterFactory, - }, - prelude::NodeFilter, - }; - use raphtory_api::core::Direction; - use raphtory_tests::assertions::{ - assert_filter_neighbours_results, assert_filter_nodes_results, TestVariants, - }; - - #[test] - fn test_filter_nodes_by_props_added_at_different_times() { - let filter = NodeFilter - .property("p4") - .eq("pometry") - .and(NodeFilter.property("p5").eq(12u64)); - let expected_results = vec!["4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_unique_results_from_composite_filters() { - let filter = NodeFilter - .property("p2") - .ge(2u64) - .and(NodeFilter.property("p2").ge(1u64)); - let expected_results = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p2") - .ge(2u64) - .or(NodeFilter.property("p2").ge(5u64)); - let expected_results = vec!["2", "3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_composite_filter_nodes() { - let filter = NodeFilter - .property("p2") - .eq(2u64) - .and(NodeFilter.property("p1").eq("kapoor")); - let expected_results = Vec::<&str>::new(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p2") - .eq(2u64) - .or(NodeFilter.property("p1").eq("shivam_kapoor")); - let expected_results = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter.property("p1").eq("pometry").or(NodeFilter - .property("p2") - .eq(6u64) - .and(NodeFilter.property("p3").eq(1u64))); - let expected_results = vec!["3"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::node_type() - .eq("fire_nation") - .and(NodeFilter.property("p1").eq("prop1")); - let expected_results = Vec::<&str>::new(); - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter - .property("p9") - .eq(5u64) - .and(NodeFilter.property("p1").eq("shivam_kapoor")); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::node_type() - .eq("fire_nation") - .and(NodeFilter.property("p1").eq("shivam_kapoor")); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::name() - .eq("2") - .and(NodeFilter.property("p2").eq(2u64)); - let expected_results = vec!["2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::name() - .eq("2") - .and(NodeFilter.property("p2").eq(2u64)) - .or(NodeFilter.property("p9").eq(5u64)); - let expected_results = vec!["1", "2"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_not_composite_filter_nodes() { - let filter = NodeFilter::name() - .eq("2") - .and(NodeFilter.property("p2").eq(2u64)) - .or(NodeFilter.property("p9").eq(5u64)) - .not(); - let expected_results = vec!["3", "4"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = NodeFilter::name() - .eq("2") - .not() - .and(NodeFilter.property("p2").eq(2u64)) - .or(NodeFilter.property("p9").eq(5u64)); - let expected_results = vec!["1"]; - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_out_neighbours_filter() { - let filter = NodeFilter::name() - .eq("2") - .and(NodeFilter.property("p2").eq(2u64)); - let expected_results = vec!["2"]; - assert_filter_neighbours_results( - |graph| init_edges_graph(init_nodes_graph(graph)), - IdentityGraphTransformer, - "1", - Direction::OUT, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_in_neighbours_filter() { - let filter = NodeFilter.property("p9").ge(1u64); - let expected_results = vec!["1"]; - assert_filter_neighbours_results( - |graph| init_edges_graph(init_nodes_graph(graph)), - IdentityGraphTransformer, - "2", - Direction::IN, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_neighbours_filter() { - let filter = NodeFilter.property("p10").contains("Paper"); - let expected_results = vec!["1", "3"]; - assert_filter_neighbours_results( - |graph| init_edges_graph(init_nodes_graph(graph)), - IdentityGraphTransformer, - "2", - Direction::BOTH, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } -} - -mod test_node_property_filter_agg { - use crate::filter_tests::test_filters::IdentityGraphTransformer; - use raphtory::{ - db::{ - api::view::StaticGraphViewOps, - graph::views::filter::{ - model::{ - node_filter::NodeFilter, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - PropertyFilterFactory, TemporalPropertyFilterFactory, - }, - CreateFilter, - }, - }, - prelude::{AdditionOps, GraphViewOps, PropertyAdditionOps}, - }; - use raphtory_api::core::{ - entities::properties::prop::{IntoProp, Prop}, - storage::arc_str::ArcStr, - }; - use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, - }; - use raphtory_tests::assertions::{ - assert_filter_nodes_err, assert_filter_nodes_results, TestVariants::All, - }; - - fn list_u8(xs: &[u8]) -> Prop { - Prop::list(xs.iter().copied().map(Prop::U8)) - } - fn list_u16(xs: &[u16]) -> Prop { - Prop::list(xs.iter().copied().map(Prop::U16)) - } - fn list_u32(xs: &[u32]) -> Prop { - Prop::list(xs.iter().copied().map(Prop::U32)) - } - fn list_u64(xs: &[u64]) -> Prop { - Prop::list(xs.iter().copied().map(Prop::U64)) - } - fn list_i32(xs: &[i32]) -> Prop { - Prop::list(xs.iter().copied().map(Prop::I32)) - } - fn list_i64(xs: &[i64]) -> Prop { - Prop::list(xs.iter().copied().map(Prop::I64)) - } - fn list_f32(xs: &[f32]) -> Prop { - Prop::list(xs.iter().copied().map(Prop::F32)) - } - fn list_f64(xs: &[f64]) -> Prop { - Prop::list(xs.iter().copied().map(Prop::F64)) - } - fn list_str(xs: &[&str]) -> Prop { - Prop::list(xs.iter().map(|s| Prop::Str(ArcStr::from(*s)))) - } - fn list_bool(xs: &[bool]) -> Prop { - Prop::list(xs.iter().copied().map(Prop::Bool)) - } - - #[inline] - fn list(v: Vec) -> Prop { - Prop::List(v.into()) - } - - /// Writes a set of node temporal properties and node metadata to the given graph. - pub fn init_nodes_graph< - G: StaticGraphViewOps - + AdditionOps - + InternalAdditionOps - + InternalPropertyAdditionOps - + PropertyAdditionOps, - >( - graph: G, - ) -> G { - // Each tuple represents (timestamp, node_name, properties). - let nodes: [(i64, &str, Vec<(&str, Prop)>); 12] = [ - ( - 1, - "n1", - vec![ - ("p_strs", list_str(&["a", "b", "c"])), // min: None, max: None, sum: None, avg: None, len: 3 - ("p_bools", list_bool(&[true, false])), // min: None, max: None, sum: None, avg: None, len: 2 - ("p_u8s", list_u8(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u8s_max", list_u8(&[u8::MAX, u8::MAX])), // min: u8::MAX, max: u8::MAX, sum: 510 - ("p_u16s", list_u16(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u16s_max", list_u16(&[u16::MAX, u16::MAX])), // min: u16::MAX, max: u16::MAX, sum: 131070 - ("p_u32s", list_u32(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u32s_max", list_u32(&[u32::MAX, u32::MAX])), // min: 1, max: 3, sum: 8589934590 - ("p_u64s", list_u64(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u64s_max", list_u64(&[u64::MAX, u64::MAX])), // min: u64::MAX, max: u64::MAX, sum: OVERFLOW - ("p_i32s", list_i32(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_i64s", list_i64(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_f32s", list_f32(&[1.0, 2.0, 3.5])), // min: 1.0, max: 3.5, sum: 6.5, avg: 2.1666666666666665, len: 3 - ("p_f64s", list_f64(&[50.0, 40.0])), // min: 40.0, max: 50.0, sum: 90.0, avg: 45.0, len: 2 - ( - "nested_list", - list(vec![ - list(vec![ - list(vec![ - list(vec![50.0.into_prop(), 40.0.into_prop()]), - list(vec![60.0.into_prop()]), - ]), - list(vec![list(vec![46.0.into_prop()])]), - ]), - list(vec![list(vec![list(vec![90.0.into_prop()])])]), - ]), - ), - ], - ), - ( - 2, - "n1", - vec![ - ("p_strs", list_str(&["a", "b", "c", "d"])), // min: None, max: None, sum: None, avg: None, len: 4 - ("p_bools", list_bool(&[true, true])), // min: None, max: None, sum: None, avg: None, len: 2 - ("p_u8s", list_u8(&[1, 2, 3, 4])), // min: 1, max: 4, sum: 10, avg: 2.5, len: 4 - ("p_u16s", list_u16(&[1, 2, 3, 4])), // min: 1, max: 4, sum: 10, avg: 2.5, len: 4 - ("p_u32s", list_u32(&[1, 2, 3, 4])), // min: 1, max: 4, sum: 10, avg: 2.5, len: 4 - ("p_u64s", list_u64(&[1, 2, 3, 4])), // min: 1, max: 4, sum: 10, avg: 2.5, len: 4 - ("p_i32s", list_i32(&[1, 2, 3, 4])), // min: 1, max: 4, sum: 10, avg: 2.5, len: 4 - ("p_i64s", list_i64(&[1, 2, 3, 4])), // min: 1, max: 4, sum: 10, avg: 2.5, len: 4 - ("p_f32s", list_f32(&[1.0, 2.0, 3.5, 4.5])), // min: 1.0, max: 4.5, sum: 11.0, avg: 2.75, len: 4 - ("p_f64s", list_f64(&[30.0, 50.0, 40.0])), // min: 30.0, max: 50.0, sum: 120.0, avg: 40.0, len: 3 - ], - ), - ( - 1, - "n2", - vec![ - ("p_strs", list_str(&["a", "b", "c", "d"])), // min: None, max: None, sum: None, avg: None, len: 4 - ("p_u64s", list_u64(&[1, 2, 3, 4])), // min: 1, max: 4, sum: 10, avg: 2.5, len: 4 - ("p_f64s", list_f64(&[30.0, 50.0, 40.0])), // min: 30.0, max: 50.0, sum: 120.0, avg: 40.0, len: 3 - ("p_bools", list_bool(&[false, false])), - ], - ), - ( - 2, - "n2", - vec![ - ("p_strs", list_str(&["a", "b", "c", "d"])), // min: None, max: None, sum: None, avg: None, len: 4 - ("p_u64s", list_u64(&[1, 2, 3, 4])), // min: 1, max: 4, sum: 10, avg: 2.5, len: 4 - ("p_f64s", list_f64(&[30.0, 50.0, 40.0])), // min: 30.0, max: 50.0, sum: 120.0, avg: 40.0, len: 3 - ], - ), - ( - 1, - "n3", - vec![ - ("p_strs", list_str(&["a", "b", "c"])), // min: None, max: None, sum: None, avg: None, len: 3 - ("p_bools", list_bool(&[true, false])), // min: None, max: None, sum: None, avg: None, len: 2 - ("p_u8s", list_u8(&[1, 1, 4])), // min: 1, max: 4, sum: 6, avg: 2.0, len: 3 - ("p_u16s", list_u16(&[1, 0, 5])), // min: 0, max: 5, sum: 6, avg: 2.0, len: 3 - ("p_u32s", list_u32(&[2, 2, 2])), // min: 2, max: 2, sum: 6, avg: 2.0, len: 3 - ("p_u64s", list_u64(&[0, 3, 3])), // min: 0, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_i32s", list_i32(&[-1, 4, 3])), // min: -1, max: 4, sum: 6, avg: 2.0, len: 3 - ("p_i64s", list_i64(&[0, 3, -3])), // min: -3, max: 3, sum: 0, avg: 0.0, len: 3 - ("p_f32s", list_f32(&[1.0, 2.5, 3.0])), // min: 1.0, max: 3.0, sum: 6.5, avg: 2.1666666666666665, len: 3 - ("p_f64s", list_f64(&[30.0, 60.0])), // min: 30.0, max: 60.0, sum: 90.0, avg: 45.0, len: 2 - ( - "nested_list", - list(vec![ - list(vec![ - list(vec![ - list(vec![50.0.into_prop(), 40.0.into_prop()]), - list(vec![60.0.into_prop()]), - ]), - list(vec![list(vec![46.0.into_prop()])]), - ]), - list(vec![list(vec![list(vec![90.0.into_prop()])])]), - ]), - ), - ], - ), - ( - 2, - "n3", - vec![ - ("p_strs", list_str(&["a", "b", "c"])), // min: None, max: None, sum: None, avg: None, len: 3 - ("p_bools", list_bool(&[true, false])), // min: None, max: None, sum: None, avg: None, len: 2 - ("p_u8s", list_u8(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u16s", list_u16(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u32s", list_u32(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u64s", list_u64(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_i32s", list_i32(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_i64s", list_i64(&[1, 2, -3])), // min: -3, max: 2, sum: 0, avg: 0.0, len: 3 - ("p_f32s", list_f32(&[1.0, 2.0, 3.5])), // min: 1.0, max: 3.5, sum: 6.5, avg: 2.1666666666666665, len: 3 - ("p_f64s", list_f64(&[50.0, 40.0])), // min: 40.0, max: 50.0, sum: 90.0, avg: 45.0, len: 2 - ( - "nested_list", - list(vec![ - list(vec![ - list(vec![ - list(vec![50.0.into_prop(), 40.0.into_prop()]), - list(vec![60.0.into_prop()]), - ]), - list(vec![list(vec![46.0.into_prop()])]), - ]), - list(vec![list(vec![list(vec![90.0.into_prop()])])]), - ]), - ), - ], - ), - ( - 1, - "n4", - vec![ - ("p_strs", list_str(&["a", "b", "c"])), // min: None, max: None, sum: None, avg: None, len: 3 - ("p_bools", list_bool(&[true, false])), // min: None, max: None, sum: None, avg: None, len: 2 - ("p_u64s", list_u64(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_i32s", list_i32(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_f32s", list_f32(&[1.0, 2.0, 3.5])), // min: 1.0, max: 3.5, sum: 6.5, avg: 2.1666666666666665, len: 3 - ("p_bools_all", list_bool(&[true, true])), - ], - ), - ( - 2, - "n4", - vec![ - ("p_strs", list_str(&["x", "y", "z"])), // min: None, max: None, sum: None, avg: None, len: 3 - ("p_bools", list_bool(&[false, false])), // min: None, max: None, sum: None, avg: None, len: 2 - ("p_u64s", list_u64(&[10, 20, 30])), // min: 10, max: 30, sum: 60, avg: 20.0, len: 3 - ("p_i32s", list_i32(&[10, 20, 30])), // min: 10, max: 30, sum: 60, avg: 20.0, len: 3 - ("p_f32s", list_f32(&[10.0, 20.0, 30.0])), // min: 10.0, max: 30.0, sum: 60.0, avg: 20.0, len: 3 - ("p_bools_all", list_bool(&[true, true])), - ], - ), - ( - 2, - "n5", - vec![ - ("p_u64s", list_u64(&[u64::MAX, 1])), // min: 1, max: u64::MAX, sum: None (overflow), avg: 9223372036854775808.0, len: 2 - ("p_u64s_max", list_u64(&[u64::MAX, 1])), // min: 1, max: u64::MAX, sum: None (overflow), avg: 9223372036854775808.0, len: 2 - ("p_u64s_min", list_u64(&[u64::MIN, 1])), // min: 1, max: u64::MAX, sum: None (overflow), avg: 9223372036854775808.0, len: 2 - ("p_i64s", list_i64(&[i64::MAX, 1])), // min: 1, max: i64::MAX, sum: None (overflow), avg: 4611686018427387904.0, len: 2 - ("p_i64s_max", list_i64(&[i64::MAX, 1])), // min: 1, max: i64::MAX, sum: None (overflow), avg: 4611686018427387904.0, len: 2 - ("p_i64s_min", list_i64(&[i64::MIN, 1])), // min: 1, max: i64::MAX, sum: None (overflow), avg: 4611686018427387904.0, len: 2 - ], - ), - ( - 2, - "n6", - vec![ - ("p_i32s", list_i32(&[-2, 1, 3])), // min: -2, max: 3, sum: 2, avg: 0.6666666666666666, len: 3 - ], - ), - ( - 1, - "n7", - vec![ - ("p_u64s", list_u64(&[])), // min: None, max: None, sum: None, avg: None, len: 0 - ], - ), - ( - 2, - "n10", - vec![ - ("p_strs", list_str(&["a", "b", "c"])), // min: None, max: None, sum: None, avg: None, len: 3 - ("p_bools", list_bool(&[true, false])), // min: None, max: None, sum: None, avg: None, len: 2 - ("p_u8s", list_u8(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u16s", list_u16(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u32s", list_u32(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_u64s", list_u64(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_i32s", list_i32(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_i64s", list_i64(&[1, 2, -3])), // min: -3, max: 2, sum: 0, avg: 0.0, len: 3 - ("p_f32s", list_f32(&[1.0, 2.0, 3.5])), // min: 1.0, max: 3.5, sum: 6.5, avg: 2.1666666666666665, len: 3 - ("p_f64s", list_f64(&[50.0, 40.0])), // min: 40.0, max: 50.0, sum: 90.0, avg: 45.0, len: 2 - ("p_bools_all", list_bool(&[true, true])), - ], - ), - ]; - - for (t, id, props) in nodes { - graph.add_node(t, id, props, None, None).unwrap(); - } - - // Each tuple represents (node_name, properties). - let metadata: [(&str, Vec<(&str, Prop)>); 8] = [ - ( - "n1", - vec![ - ("p_u8s", list_u8(&[2, 9])), // min: 2, max: 9, sum: 11, avg: 5.5, len: 2 - ("p_u16s", list_u16(&[3, 5])), // min: 3, max: 5, sum: 8, avg: 4.0, len: 2 - ("p_u32s", list_u32(&[4, 9])), // min: 4, max: 9, sum: 13, avg: 6.5, len: 2 - ], - ), - ( - "n2", - vec![ - ("p_u64s", list_u64(&[2, 3, 7])), // min: 2, max: 7, sum: 12, avg: 4.0, len: 3 - ], - ), - ( - "n3", - vec![ - ("p_i32s", list_i32(&[10, 2, -3])), // min: -3, max: 10, sum: 9, avg: 3.0, len: 3 - ("p_i64s", list_i64(&[1, 12, 3, 4])), // min: 1, max: 12, sum: 20, avg: 5.0, len: 4 - ], - ), - ( - "n4", - vec![ - ("p_f32s", list_f32(&[1.5, 2.5])), // min: 1.5, max: 2.5, sum: 4.0, avg: 2.0, len: 2 - ("p_f64s", list_f64(&[0.5, 1.5])), // min: 0.5, max: 1.5, sum: 2.0, avg: 1.0, len: 2 - ], - ), - ( - "n5", - vec![ - ("p_strs", list_str(&["m1", "m2", "m3"])), // min: None, max: None, sum: None, avg: None, len: 3 - ], - ), - ( - "n6", - vec![ - ("p_u64s", list_u64(&[])), // min: None, max: None, sum: None, avg: None, len: 0 - ("p_strs", list_str(&["a", "a"])), - ], - ), - ( - "n7", - vec![ - ("p_u64s", list_u64(&[u64::MAX, 1])), // min: 1, max: u64::MAX, sum: None (overflow), avg: ~9.22e18, len: 2 - ("p_strs", list_str(&["a"])), - ], - ), - ( - "n10", - vec![ - ("p_u64s", list_u64(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 - ("p_strs", list_str(&["a", "b", "c"])), // min: None, max: None, sum: None, avg: None, len: 3 - ], - ), - ]; - - for (node_id, md) in metadata { - graph.node(node_id).unwrap().add_metadata(md).unwrap(); - } - - graph - } - - #[track_caller] - fn apply_assertion(filter: impl CreateFilter + Clone, expected: &[&str]) { - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected, - All, - ); - } - - #[track_caller] - fn apply_assertion_err(filter: impl CreateFilter + Clone, expected: &str) { - assert_filter_nodes_err( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected, - All, - ); - - // assert_search_nodes_err( - // init_nodes_graph, - // IdentityGraphTransformer, - // filter, - // expected, - // All, - // ); - } - - // ------ Property: SUM ---- - #[test] - fn test_node_property_sum_u8s() { - let filter = NodeFilter.property("p_u8s").sum().eq(Prop::U64(10)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_sum_u16s() { - let filter = NodeFilter.property("p_u16s").sum().eq(Prop::U64(6)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_sum_u32s() { - let filter = NodeFilter.property("p_u32s").sum().eq(Prop::U64(10)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_sum_u64s() { - let filter = NodeFilter.property("p_u64s").sum().eq(Prop::U64(6)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_sum_i32s() { - let filter = NodeFilter.property("p_i32s").sum().eq(Prop::I64(2)); - let expected = vec!["n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_sum_i64s() { - let filter = NodeFilter.property("p_i64s").sum().eq(Prop::I64(0)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_sum_f32s() { - let filter = NodeFilter.property("p_f32s").sum().eq(Prop::F64(6.5)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_sum_f64s() { - let filter = NodeFilter.property("p_f64s").sum().eq(Prop::F64(120.0)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - } - - // ------ Property: AVG ---- - #[test] - fn test_node_property_avg_u8s() { - let filter = NodeFilter.property("p_u8s").avg().eq(Prop::F64(2.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_avg_u16s() { - let filter = NodeFilter.property("p_u16s").avg().eq(Prop::F64(2.0)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_avg_u32s() { - let filter = NodeFilter.property("p_u32s").avg().eq(Prop::F64(2.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_avg_u64s() { - let filter = NodeFilter.property("p_u64s").avg().eq(Prop::F64(2.0)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_avg_i32s() { - let filter = NodeFilter - .property("p_i32s") - .avg() - .eq(Prop::F64(0.6666666666666666)); - let expected = vec!["n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_avg_i64s() { - let filter = NodeFilter.property("p_i64s").avg().eq(Prop::F64(0.0)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_avg_f32s() { - let filter = NodeFilter - .property("p_f32s") - .avg() - .eq(Prop::F64(2.1666666666666665)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_avg_f64s() { - let filter = NodeFilter.property("p_f64s").avg().eq(Prop::F64(40.0)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - } - - // ------ Property: LEN ------ - #[test] - fn test_node_property_len_u8s() { - let filter = NodeFilter.property("p_u8s").len().eq(Prop::U64(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_len_u16s() { - let filter = NodeFilter.property("p_u16s").len().eq(Prop::U64(3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_len_u32s() { - let filter = NodeFilter.property("p_u32s").len().eq(Prop::U64(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_len_u64s() { - let filter = NodeFilter.property("p_u64s").len().eq(Prop::U64(3)); - let expected = vec!["n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_len_i32s() { - let filter = NodeFilter.property("p_i32s").len().eq(Prop::U64(3)); - let expected = vec!["n10", "n3", "n4", "n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_len_i64s() { - let filter = NodeFilter.property("p_i64s").len().eq(Prop::U64(3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_len_f32s() { - let filter = NodeFilter.property("p_f32s").len().eq(Prop::U64(3)); - let expected = vec!["n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_len_f64s() { - let filter = NodeFilter.property("p_f64s").len().eq(Prop::U64(3)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_len_strs() { - let filter = NodeFilter.property("p_strs").len().eq(Prop::U64(3)); - let expected = vec!["n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - // ------ Property: MIN ------ - #[test] - fn test_node_property_min_u8s() { - let filter = NodeFilter.property("p_u8s").min().eq(Prop::U8(1)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_min_u16s() { - let filter = NodeFilter.property("p_u16s").min().eq(Prop::U16(1)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_min_u32s() { - let filter = NodeFilter.property("p_u32s").min().eq(Prop::U32(1)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_min_u64s() { - let filter = NodeFilter.property("p_u64s").min().eq(Prop::U64(1)); - let expected = vec!["n1", "n10", "n2", "n3", "n5"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_min_i32s() { - let filter = NodeFilter.property("p_i32s").min().eq(Prop::I32(-2)); - let expected = vec!["n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_min_i64s() { - let filter = NodeFilter.property("p_i64s").min().eq(Prop::I64(-3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_min_f32s() { - let filter = NodeFilter.property("p_f32s").min().eq(Prop::F32(10.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_min_f64s() { - let filter = NodeFilter.property("p_f64s").min().eq(Prop::F64(40.0)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Property: MAX ------ - #[test] - fn test_node_property_max_u8s() { - let filter = NodeFilter.property("p_u8s").max().eq(Prop::U8(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_max_u16s() { - let filter = NodeFilter.property("p_u16s").max().eq(Prop::U16(3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_max_u32s() { - let filter = NodeFilter.property("p_u32s").max().eq(Prop::U32(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_max_u64s() { - let filter = NodeFilter.property("p_u64s").max().eq(Prop::U64(3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_max_i32s() { - let filter = NodeFilter.property("p_i32s").max().eq(Prop::I32(3)); - let expected = vec!["n10", "n3", "n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_max_i64s() { - let filter = NodeFilter.property("p_i64s").max().eq(Prop::I64(2)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_max_f32s() { - let filter = NodeFilter.property("p_f32s").max().eq(Prop::F32(30.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_max_f64s() { - let filter = NodeFilter.property("p_f64s").max().eq(Prop::F64(50.0)); - let expected = vec!["n1", "n10", "n2", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Metadata: SUM ------ - #[test] - fn test_node_property_metadata_sum_u8s() { - let filter = NodeFilter.metadata("p_u8s").sum().eq(Prop::U64(11)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_sum_u16s() { - let filter = NodeFilter.metadata("p_u16s").sum().eq(Prop::U64(8)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_sum_u32s() { - let filter = NodeFilter.metadata("p_u32s").sum().eq(Prop::U64(13)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_sum_u64s() { - let filter = NodeFilter.metadata("p_u64s").sum().eq(Prop::U64(12)); - let expected = vec!["n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_sum_i32s() { - let filter = NodeFilter.metadata("p_i32s").sum().eq(Prop::I64(9)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_sum_i64s() { - let filter = NodeFilter.metadata("p_i64s").sum().eq(Prop::I64(20)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_sum_f32s() { - let filter = NodeFilter.metadata("p_f32s").sum().eq(Prop::F64(4.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_sum_f64s() { - let filter = NodeFilter.metadata("p_f64s").sum().eq(Prop::F64(2.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - // ------ Metadata: AVG ------ - #[test] - fn test_node_property_metadata_avg_u8s() { - let filter = NodeFilter.metadata("p_u8s").avg().eq(Prop::F64(5.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_avg_u16s() { - let filter = NodeFilter.metadata("p_u16s").avg().eq(Prop::F64(4.0)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_avg_u32s() { - let filter = NodeFilter.metadata("p_u32s").avg().eq(Prop::F64(6.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_avg_u64s() { - let filter = NodeFilter.metadata("p_u64s").avg().eq(Prop::F64(4.0)); - let expected = vec!["n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_avg_i32s() { - let filter = NodeFilter.metadata("p_i32s").avg().eq(Prop::F64(3.0)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_avg_i64s() { - let filter = NodeFilter.metadata("p_i64s").avg().eq(Prop::F64(5.0)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_avg_f32s() { - let filter = NodeFilter.metadata("p_f32s").avg().eq(Prop::F64(2.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_avg_f64s() { - let filter = NodeFilter.metadata("p_f64s").avg().eq(Prop::F64(1.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - // ------ Metadata: MIN ------ - #[test] - fn test_node_property_metadata_min_u8s() { - let filter = NodeFilter.metadata("p_u8s").min().eq(Prop::U8(2)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_min_u16s() { - let filter = NodeFilter.metadata("p_u16s").min().eq(Prop::U16(3)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_min_u32s() { - let filter = NodeFilter.metadata("p_u32s").min().eq(Prop::U32(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_min_u64s() { - let filter = NodeFilter.metadata("p_u64s").min().eq(Prop::U64(2)); - let expected = vec!["n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_min_i32s() { - let filter = NodeFilter.metadata("p_i32s").min().eq(Prop::I32(-3)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_min_i64s() { - let filter = NodeFilter.metadata("p_i64s").min().eq(Prop::I64(1)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_min_f32s() { - let filter = NodeFilter.metadata("p_f32s").min().eq(Prop::F32(1.5)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_min_f64s() { - let filter = NodeFilter.metadata("p_f64s").min().eq(Prop::F64(0.5)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - // ------ Metadata: MAX ------ - #[test] - fn test_node_property_metadata_max_u8s() { - let filter = NodeFilter.metadata("p_u8s").max().eq(Prop::U8(9)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_max_u16s() { - let filter = NodeFilter.metadata("p_u16s").max().eq(Prop::U16(5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_max_u32s() { - let filter = NodeFilter.metadata("p_u32s").max().eq(Prop::U32(9)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_max_u64s() { - let filter = NodeFilter.metadata("p_u64s").max().eq(Prop::U64(7)); - let expected = vec!["n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_max_i32s() { - let filter = NodeFilter.metadata("p_i32s").max().eq(Prop::I32(10)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_max_i64s() { - let filter = NodeFilter.metadata("p_i64s").max().eq(Prop::I64(12)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_max_f32s() { - let filter = NodeFilter.metadata("p_f32s").max().eq(Prop::F32(2.5)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_max_f64s() { - let filter = NodeFilter.metadata("p_f64s").max().eq(Prop::F64(1.5)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - // ------ Metadata: Len ------ - #[test] - fn test_node_property_metadata_len_u8s() { - let filter = NodeFilter.metadata("p_u8s").len().eq(Prop::U64(2)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_len_u16s() { - let filter = NodeFilter.metadata("p_u16s").len().eq(Prop::U64(2)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_len_u32s() { - let filter = NodeFilter.metadata("p_u32s").len().eq(Prop::U64(2)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_len_u64s() { - let filter = NodeFilter.metadata("p_u64s").len().eq(Prop::U64(3)); - let expected = vec!["n10", "n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_len_i32s() { - let filter = NodeFilter.metadata("p_i32s").len().eq(Prop::U64(3)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_len_i64s() { - let filter = NodeFilter.metadata("p_i64s").len().eq(Prop::U64(4)); - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_len_f32s() { - let filter = NodeFilter.metadata("p_f32s").len().eq(Prop::U64(2)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_len_f64s() { - let filter = NodeFilter.metadata("p_f64s").len().eq(Prop::U64(2)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_metadata_len_strs() { - let filter = NodeFilter.metadata("p_strs").len().eq(Prop::U64(3)); - let expected = vec!["n10", "n5"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal last: SUM ------ - #[test] - fn test_node_property_temporal_last_sum_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .last() - .sum() - .eq(Prop::U64(10)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_sum_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .last() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_sum_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .last() - .sum() - .eq(Prop::U64(10)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_sum_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .last() - .sum() - .eq(Prop::U64(60)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_sum_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .last() - .sum() - .eq(Prop::I64(60)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_sum_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .last() - .sum() - .eq(Prop::I64(0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_sum_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .last() - .sum() - .eq(Prop::F64(6.5)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_sum_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .last() - .sum() - .eq(Prop::F64(90.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal last: AVG ------ - #[test] - fn test_node_property_temporal_last_avg_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .last() - .avg() - .eq(Prop::F64(2.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_avg_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .last() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_avg_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .last() - .avg() - .eq(Prop::F64(2.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_avg_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .last() - .avg() - .eq(Prop::F64(20.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_avg_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .last() - .avg() - .eq(Prop::F64(0.6666666666666666)); - let expected = vec!["n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_avg_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .last() - .avg() - .eq(Prop::F64(0.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_avg_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .last() - .avg() - .eq(Prop::F64(20.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_avg_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .last() - .avg() - .eq(Prop::F64(45.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal last: MIN ------ - #[test] - fn test_node_property_temporal_last_min_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .last() - .min() - .eq(Prop::U8(1)); - let expected = vec!["n1", "n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_min_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .last() - .min() - .eq(Prop::U16(1)); - let expected = vec!["n1", "n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_min_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .last() - .min() - .eq(Prop::U32(1)); - let expected = vec!["n1", "n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_min_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .last() - .min() - .eq(Prop::U64(10)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_min_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .last() - .min() - .eq(Prop::I32(-2)); - let expected = vec!["n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_min_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .last() - .min() - .eq(Prop::I64(-3)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_min_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .last() - .min() - .eq(Prop::F32(10.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_min_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .last() - .min() - .eq(Prop::F64(40.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal last: MAX ------ - #[test] - fn test_node_property_temporal_last_max_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .last() - .max() - .eq(Prop::U8(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_max_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .last() - .max() - .eq(Prop::U16(3)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_max_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .last() - .max() - .eq(Prop::U32(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_max_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .last() - .max() - .eq(Prop::U64(30)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_max_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .last() - .max() - .eq(Prop::I32(3)); - let expected = vec!["n3", "n6", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_max_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .last() - .max() - .eq(Prop::I64(2)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_max_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .last() - .max() - .eq(Prop::F32(3.5)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_max_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .last() - .max() - .eq(Prop::F64(50.0)); - let expected = vec!["n1", "n2", "n3", "n10"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal last: LEN ------ - #[test] - fn test_node_property_temporal_last_len_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .last() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_len_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .last() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_len_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .last() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_len_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .last() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n3", "n4", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_len_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .last() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n3", "n4", "n6", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_len_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .last() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_len_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .last() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n3", "n4", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_last_len_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .last() - .len() - .eq(Prop::U64(2)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal all: SUM ------ - #[test] - fn test_node_property_temporal_all_sum_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .all() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_sum_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .all() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_sum_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .all() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_sum_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .all() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_sum_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .all() - .sum() - .eq(Prop::I64(6)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_sum_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .all() - .sum() - .eq(Prop::I64(0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_sum_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .all() - .sum() - .eq(Prop::F64(6.5)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_sum_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .all() - .sum() - .eq(Prop::F64(90.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal all: AVG ------ - #[test] - fn test_node_property_temporal_all_avg_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .all() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_avg_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .all() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_avg_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .all() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_avg_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .all() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_avg_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .all() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_avg_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .all() - .avg() - .eq(Prop::F64(0.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_avg_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .all() - .avg() - .eq(Prop::F64(2.1666666666666665)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_avg_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .all() - .avg() - .eq(Prop::F64(45.0)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal all: MIN ------ - #[test] - fn test_node_property_temporal_all_min_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .all() - .min() - .eq(Prop::U8(1)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_min_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .all() - .min() - .eq(Prop::U16(1)); - let expected = vec!["n1", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_min_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .all() - .min() - .eq(Prop::U32(1)); - let expected = vec!["n1", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_min_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .all() - .min() - .eq(Prop::U64(1)); - let expected = vec!["n1", "n10", "n2", "n5"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_min_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .all() - .min() - .eq(Prop::I32(-2)); - let expected = vec!["n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_min_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .all() - .min() - .eq(Prop::I64(-3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_min_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .all() - .min() - .eq(Prop::F32(1.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_min_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .all() - .min() - .eq(Prop::F64(30.0)); - let expected = vec!["n2"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal all: MAX ------ - #[test] - fn test_node_property_temporal_all_max_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .all() - .max() - .eq(Prop::U8(3)); - let expected = vec!["n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_max_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .all() - .max() - .eq(Prop::U16(3)); - let expected = vec!["n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_max_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .all() - .max() - .eq(Prop::U32(3)); - let expected = vec!["n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_max_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .all() - .max() - .eq(Prop::U64(4)); - let expected = vec!["n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_max_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .all() - .max() - .eq(Prop::I32(3)); - let expected = vec!["n10", "n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_max_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .all() - .max() - .eq(Prop::I64(2)); - let expected = vec!["n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_max_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .all() - .max() - .eq(Prop::F32(3.5)); - let expected = vec!["n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_max_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .all() - .max() - .eq(Prop::F64(50.0)); - let expected = vec!["n1", "n10", "n2"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal all: LEN ------ - #[test] - fn test_node_property_temporal_all_len_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .all() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_len_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .all() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_len_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .all() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_len_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .all() - .len() - .eq(Prop::U64(4)); - let expected = vec!["n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_len_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .all() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n10", "n3", "n4", "n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_len_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .all() - .len() - .eq(Prop::U64(2)); - let expected = vec!["n5"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_len_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .all() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_all_len_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .all() - .len() - .eq(Prop::U64(2)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal first: SUM ------ - #[test] - fn test_node_property_temporal_first_sum_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .first() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_sum_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .first() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_sum_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .first() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_sum_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .first() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_sum_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .first() - .sum() - .eq(Prop::I64(6)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_sum_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .first() - .sum() - .eq(Prop::I64(0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_sum_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .first() - .sum() - .eq(Prop::F64(6.5)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_sum_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .first() - .sum() - .eq(Prop::F64(90.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal first: AVG ------ - #[test] - fn test_node_property_temporal_first_avg_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .first() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_avg_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .first() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_avg_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .first() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_avg_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .first() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_avg_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .first() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_avg_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .first() - .avg() - .eq(Prop::F64(0.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_avg_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .first() - .avg() - .eq(Prop::F64(2.1666666666666665)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_avg_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .first() - .avg() - .eq(Prop::F64(45.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal first: MIN ------ - #[test] - fn test_node_property_temporal_first_min_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .first() - .min() - .eq(Prop::U8(1)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_min_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .first() - .min() - .eq(Prop::U16(1)); - let expected = vec!["n1", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_min_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .first() - .min() - .eq(Prop::U32(1)); - let expected = vec!["n1", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_min_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .first() - .min() - .eq(Prop::U64(1)); - let expected = vec!["n1", "n10", "n2", "n4", "n5"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_min_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .first() - .min() - .eq(Prop::I32(-2)); - let expected = vec!["n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_min_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .first() - .min() - .eq(Prop::I64(-3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_min_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .first() - .min() - .eq(Prop::F32(1.0)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_min_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .first() - .min() - .eq(Prop::F64(30.0)); - let expected = vec!["n2", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal first: MAX ------ - #[test] - fn test_node_property_temporal_first_max_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .first() - .max() - .eq(Prop::U8(3)); - let expected = vec!["n1", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_max_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .first() - .max() - .eq(Prop::U16(3)); - let expected = vec!["n1", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_max_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .first() - .max() - .eq(Prop::U32(3)); - let expected = vec!["n1", "n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_max_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .first() - .max() - .eq(Prop::U64(4)); - let expected = vec!["n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_max_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .first() - .max() - .eq(Prop::I32(3)); - let expected = vec!["n1", "n10", "n4", "n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_max_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .first() - .max() - .eq(Prop::I64(2)); - let expected = vec!["n10"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_max_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .first() - .max() - .eq(Prop::F32(3.5)); - let expected = vec!["n1", "n10", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_max_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .first() - .max() - .eq(Prop::F64(50.0)); - let expected = vec!["n1", "n10", "n2"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal first: LEN ------ - #[test] - fn test_node_property_temporal_first_len_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .first() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_len_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .first() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_len_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .first() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_len_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .first() - .len() - .eq(Prop::U64(4)); - let expected = vec!["n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_len_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .first() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3", "n4", "n6"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_len_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .first() - .len() - .eq(Prop::U64(2)); - let expected = vec!["n5"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_len_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .first() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_first_len_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .first() - .len() - .eq(Prop::U64(2)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal any: SUM ------ - #[test] - fn test_node_property_temporal_any_sum_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .any() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u8s") - .temporal() - .any() - .sum() - .eq(Prop::U64(10)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_sum_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .any() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u16s") - .temporal() - .any() - .sum() - .eq(Prop::U64(10)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_sum_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .any() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u32s") - .temporal() - .any() - .sum() - .eq(Prop::U64(10)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_sum_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .any() - .sum() - .eq(Prop::U64(6)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u64s") - .temporal() - .any() - .sum() - .eq(Prop::U64(10)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_sum_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .sum() - .eq(Prop::I64(6)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .sum() - .eq(Prop::I64(60)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_sum_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .sum() - .eq(Prop::I64(0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .sum() - .eq(Prop::I64(10)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_sum_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .sum() - .eq(Prop::F64(6.5)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .sum() - .eq(Prop::F64(60.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_sum_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .any() - .sum() - .eq(Prop::F64(90.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f64s") - .temporal() - .any() - .sum() - .eq(Prop::F64(120.0)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal any: AVG ------ - #[test] - fn test_node_property_temporal_any_avg_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u8s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_avg_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u16s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_avg_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u32s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_avg_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u64s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.5)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_avg_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.0)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_avg_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .avg() - .eq(Prop::F64(0.0)); - let expected = vec!["n3", "n10"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.5)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_avg_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .avg() - .eq(Prop::F64(2.1666666666666665)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .avg() - .eq(Prop::F64(20.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_avg_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .any() - .avg() - .eq(Prop::F64(45.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f64s") - .temporal() - .any() - .avg() - .eq(Prop::F64(40.0)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal any: MIN ------ - #[test] - fn test_node_property_temporal_any_min_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .any() - .min() - .eq(Prop::U8(1)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_min_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .any() - .min() - .eq(Prop::U16(1)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_min_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .any() - .min() - .eq(Prop::U32(1)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_min_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .any() - .min() - .eq(Prop::U64(1)); - let expected = vec!["n1", "n10", "n2", "n3", "n4", "n5"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_min_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .min() - .eq(Prop::I32(-2)); - let expected = vec!["n6"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .min() - .eq(Prop::I32(10)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_min_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .min() - .eq(Prop::I64(-3)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .min() - .eq(Prop::I64(1)); - let expected = vec!["n1", "n5"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_min_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .min() - .eq(Prop::F32(1.0)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .min() - .eq(Prop::F32(10.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_min_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .any() - .min() - .eq(Prop::F64(30.0)); - let expected = vec!["n1", "n2", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f64s") - .temporal() - .any() - .min() - .eq(Prop::F64(40.0)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal any: MAX ------ - #[test] - fn test_node_property_temporal_any_max_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .any() - .max() - .eq(Prop::U8(3)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u8s") - .temporal() - .any() - .max() - .eq(Prop::U8(4)); - let expected = vec!["n1", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_max_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .any() - .max() - .eq(Prop::U16(3)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u16s") - .temporal() - .any() - .max() - .eq(Prop::U16(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_max_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .any() - .max() - .eq(Prop::U32(3)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u32s") - .temporal() - .any() - .max() - .eq(Prop::U32(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_max_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .any() - .max() - .eq(Prop::U64(4)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u64s") - .temporal() - .any() - .max() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_max_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .max() - .eq(Prop::I32(3)); - let expected = vec!["n1", "n10", "n3", "n4", "n6"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .max() - .eq(Prop::I32(30)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_max_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .max() - .eq(Prop::I64(2)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .max() - .eq(Prop::I64(2)); - let expected = vec!["n10", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_max_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .max() - .eq(Prop::F32(3.5)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .max() - .eq(Prop::F32(30.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_max_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .any() - .max() - .eq(Prop::F64(50.0)); - let expected = vec!["n1", "n10", "n2", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal any: LEN ------ - #[test] - fn test_node_property_temporal_any_len_u8s() { - let filter = NodeFilter - .property("p_u8s") - .temporal() - .any() - .len() - .is_in(vec![Prop::U64(3)]); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u8s") - .temporal() - .any() - .len() - .eq(Prop::U64(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_len_u16s() { - let filter = NodeFilter - .property("p_u16s") - .temporal() - .any() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u16s") - .temporal() - .any() - .len() - .eq(Prop::U64(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_len_u32s() { - let filter = NodeFilter - .property("p_u32s") - .temporal() - .any() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u32s") - .temporal() - .any() - .len() - .eq(Prop::U64(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_len_u64s() { - let filter = NodeFilter - .property("p_u64s") - .temporal() - .any() - .len() - .eq(Prop::U64(4)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u64s") - .temporal() - .any() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_len_i32s() { - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3", "n4", "n6"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i32s") - .temporal() - .any() - .len() - .eq(Prop::U64(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_len_i64s() { - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .len() - .eq(Prop::U64(2)); - let expected = vec!["n5"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_i64s") - .temporal() - .any() - .len() - .eq(Prop::U64(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_len_f32s() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .len() - .eq(Prop::U64(4)); - let expected = vec!["n1"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_property_temporal_any_len_f64s() { - let filter = NodeFilter - .property("p_f64s") - .temporal() - .any() - .len() - .eq(Prop::U64(2)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f64s") - .temporal() - .any() - .len() - .eq(Prop::U64(3)); - let expected = vec!["n1", "n2"]; - apply_assertion(filter, &expected); - } - - // ------ EMPTY LISTS ------ - #[test] - fn test_empty_list_agg() { - let filter = NodeFilter.property("p_u64s").sum().eq(Prop::U64(0)); - let expected: Vec<&str> = vec![]; - apply_assertion(filter, &expected); - - let filter = NodeFilter.property("p_u64s").avg().eq(Prop::F64(0.0)); - let expected: Vec<&str> = vec![]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u64s") - .temporal() - .last() - .min() - .eq(Prop::U64(0)); - let expected: Vec<&str> = vec![]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u64s") - .temporal() - .first() - .max() - .eq(Prop::U64(0)); - let expected: Vec<&str> = vec![]; - apply_assertion(filter, &expected); - - let filter = NodeFilter.property("p_u64s").len().eq(Prop::U64(0)); - let expected: Vec<&str> = vec!["n7"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter.metadata("p_u64s").len().eq(Prop::U64(0)); - let expected: Vec<&str> = vec!["n6"]; - apply_assertion(filter, &expected); - } - - // ------ Unsupported filter operations ------ - #[test] - fn test_unsupported_filter_ops_agg() { - let filter = NodeFilter.property("p_u64s").sum().starts_with("abc"); - let expected: &str = "Operator STARTS_WITH is not supported with list aggregation"; - apply_assertion_err(filter, expected); - - let filter = NodeFilter.property("p_u64s").avg().ends_with("abc"); - let expected: &str = "Operator ENDS_WITH is not supported with list aggregation"; - apply_assertion_err(filter, expected); - - let filter = NodeFilter.property("p_u64s").min().is_none(); - let expected: &str = "Operator IS_NONE is not supported with list aggregation"; - apply_assertion_err(filter, expected); - - let filter = NodeFilter.property("p_u64s").max().is_some(); - let expected: &str = "Operator IS_SOME is not supported with list aggregation"; - apply_assertion_err(filter, expected); - - let filter = NodeFilter.property("p_u64s").len().contains("abc"); - let expected: &str = "Operator CONTAINS is not supported with list aggregation"; - apply_assertion_err(filter, expected); - - let filter = NodeFilter.property("p_u64s").sum().not_contains("abc"); - let expected: &str = "Operator NOT_CONTAINS is not supported with list aggregation"; - apply_assertion_err(filter, expected); - } - - // --------------- OVERFLOW HANDLING --------------- - #[test] - fn test_max_value_agg() { - let filter = NodeFilter - .property("p_u64s_max") - .max() - .eq(Prop::U64(u64::MAX)); - let expected: Vec<&str> = vec!["n5", "n1"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u64s_min") - .min() - .eq(Prop::U64(u64::MIN)); - let expected: Vec<&str> = vec!["n5"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter.property("p_u8s_max").sum().eq(Prop::U64(510)); - let expected: Vec<&str> = vec!["n1"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u16s_max") - .sum() - .eq(Prop::U64(131070)); - let expected: Vec<&str> = vec!["n1"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_u32s_max") - .sum() - .eq(Prop::U64(8589934590)); - let expected: Vec<&str> = vec!["n1"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter.property("p_u64s_max").sum().gt(Prop::U64(0)); - let expected: Vec<&str> = vec!["n1", "n5"]; - apply_assertion(filter, &expected); - - // AVG is computed in f64 even if SUM overflowed. - let avg = (u64::MAX as f64 + 1.0) / 2.0; - let filter = NodeFilter.property("p_u64s_max").avg().eq(avg); - let expected = vec!["n5"]; - apply_assertion(filter, &expected); - - // Overflow is handled by promoting to Decimal which still compares - let filter = NodeFilter.property("p_i64s_max").sum().gt(Prop::I64(0)); - let expected: Vec<&str> = vec!["n5"]; - apply_assertion(filter, &expected); - - // AVG is computed in f64 even if SUM overflowed. - let avg = (i64::MAX as f64 + 1.0) / 2.0; - let filter = NodeFilter.property("p_i64s_max").avg().eq(avg); - let expected = vec!["n5"]; - apply_assertion(filter, &expected); - } - - // ------ Property: any ------ - #[test] - fn test_node_property_any() { - let filter = NodeFilter.property("p_u8s").any().eq(Prop::U8(3)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Property: all ------ - #[test] - fn test_node_property_all() { - let filter = NodeFilter - .property("p_bools_all") - .all() - .eq(Prop::Bool(true)); - let expected = vec!["n10", "n4"]; - apply_assertion(filter, &expected); - } - - // ------ Metadata: any ------ - #[test] - fn test_node_metadata_any() { - let filter = NodeFilter.metadata("p_u64s").any().eq(Prop::U64(1)); - let expected = vec!["n10", "n7"]; - apply_assertion(filter, &expected); - } - - // ------ Metadata: all ------ - #[test] - fn test_node_metadata_all() { - let filter = NodeFilter.metadata("p_strs").all().eq("a"); - let expected = vec!["n6", "n7"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal First: any ------ - #[test] - fn test_node_temporal_property_first_any() { - let filter = NodeFilter - .property("p_bools") - .temporal() - .first() - .any() - .eq(false); - let expected = vec!["n1", "n10", "n2", "n3", "n4"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal First: all ------ - #[test] - fn test_node_temporal_property_first_all() { - let filter = NodeFilter - .property("p_bools_all") - .temporal() - .first() - .all() - .eq(true); - let expected = vec!["n10", "n4"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal last: any ------ - #[test] - fn test_node_temporal_property_last_any() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .last() - .any() - .eq(Prop::F32(3.5)); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal last: all ------ - #[test] - fn test_node_temporal_property_last_all() { - let filter = NodeFilter - .property("p_bools_all") - .temporal() - .last() - .all() - .eq(true); - let expected = vec!["n10", "n4"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal Any: any ------ - #[test] - fn test_node_temporal_property_any_any() { - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .any() - .eq(Prop::F32(3.5)); - let expected = vec!["n1", "n10", "n3", "n4"]; - apply_assertion(filter, &expected); - - let filter = NodeFilter - .property("p_f32s") - .temporal() - .any() - .any() - .eq(Prop::F32(30.0)); - let expected = vec!["n4"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal Any: all ------ - #[test] - fn test_node_temporal_property_any_all() { - let filter = NodeFilter - .property("p_bools") - .temporal() - .any() - .all() - .eq(false); - let expected = vec!["n2", "n4"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_nested_list_property_all_all_all_any() { - let filter = NodeFilter - .property("nested_list") - .all() - .all() - .all() - .any() - .gt(45.0); - - let expected = vec!["n1", "n3"]; - apply_assertion(filter, &expected); - } - - #[test] - fn test_node_nested_list_temporal_property_all_all_all_all_any() { - let filter = NodeFilter - .property("nested_list") - .temporal() - .all() - .all() - .all() - .all() - .any() - .gt(45.0); - - let expected = vec!["n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal All: any ------ - #[test] - fn test_node_temporal_property_all_any() { - let filter = NodeFilter - .property("p_bools") - .temporal() - .all() - .any() - .eq(true); - let expected = vec!["n1", "n10", "n3"]; - apply_assertion(filter, &expected); - } - - // ------ Temporal All: all ------ - #[test] - fn test_node_temporal_property_all_all() { - let filter = NodeFilter - .property("p_bools_all") - .temporal() - .all() - .all() - .eq(true); - let expected = vec!["n4", "n10"]; - apply_assertion(filter, &expected); - } -} - -mod test_edge_filter { - use crate::filter_tests::test_filters::{ - init_edges_graph, init_edges_graph_with_num_ids, init_edges_graph_with_str_ids, - init_edges_graph_with_str_ids_del, init_nodes_graph, IdentityGraphTransformer, - }; - use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, - node_filter::ops::{NodeFilterOps, NodeIdFilterOps}, - property_filter::ops::{ListAggOps, PropertyFilterOps}, - ComposableFilter, EdgeViewFilterOps, EntityAggOps, EntityExprFilterOps, - PropertyFilterFactory, TemporalPropertyFilterFactory, ViewWrapOps, - }; - use raphtory_tests::assertions::{ - assert_filter_edges_results, assert_select_edges_results, TestGraphVariants, TestVariants, - }; - - #[test] - fn test_filter_edges_src_property_eq() { - let filter = EdgeFilter::src().property("p10").eq("Paper_airplane"); - let expected_results = vec!["1->2", "3->1"]; - let g = |g| init_edges_graph(init_nodes_graph(g)); - assert_filter_edges_results( - g, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_src_property_temporal_eq() { - let filter = EdgeFilter::src() - .property("p30") - .temporal() - .first() - .eq("Old_boat"); - let expected_results = vec!["2->1", "2->3"]; - let g = |g| init_edges_graph(init_nodes_graph(g)); - assert_filter_edges_results( - g, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_src_metadata_eq() { - let filter = EdgeFilter::src().metadata("m1").eq("pometry"); - let expected_results = vec!["1->2"]; - let g = |g| init_edges_graph(init_nodes_graph(g)); - assert_filter_edges_results( - g, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_eq() { - let filter = EdgeFilter::src().name().eq("3"); - let expected_results = vec!["3->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_ne() { - let filter = EdgeFilter::src().name().ne("1"); - let expected_results = vec![ - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_in() { - let filter = EdgeFilter::src().name().is_in(vec!["1"]); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src().name().is_in(vec!["1", "2"]); - let expected_results = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_not_in() { - let filter = EdgeFilter::src().name().is_not_in(vec!["1"]); - let expected_results = vec![ - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_eq() { - let filter = EdgeFilter::dst().name().eq("2"); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_ne() { - let filter = EdgeFilter::dst().name().ne("2"); - let expected_results = vec![ - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_in() { - let filter = EdgeFilter::dst().name().is_in(vec!["2"]); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst().name().is_in(vec!["2", "3"]); - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_not_in() { - let filter = EdgeFilter::dst().name().is_not_in(vec!["1"]); - let expected_results = vec![ - "1->2", - "2->3", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_dst_starts_with() { - let filter = EdgeFilter::src().name().starts_with("Joh"); - let expected_results: Vec<&str> = vec!["John Mayer->Jimmy Page"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src().name().starts_with("Joker"); - let expected_results: Vec<&str> = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst().name().starts_with("Jimmy"); - let expected_results: Vec<&str> = vec!["John Mayer->Jimmy Page"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst().name().starts_with("Tango"); - let expected_results: Vec<&str> = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_dst_ends_with() { - let filter = EdgeFilter::src().name().ends_with("Mayer"); - let expected_results: Vec<&str> = vec!["John Mayer->Jimmy Page"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src().name().ends_with("Cruise"); - let expected_results: Vec<&str> = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst().name().ends_with("Page"); - let expected_results: Vec<&str> = vec!["John Mayer->Jimmy Page"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst().name().ends_with("Cruise"); - let expected_results: Vec<&str> = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_contains() { - let filter = EdgeFilter::src().name().contains("Mayer"); - let expected_results: Vec<&str> = vec!["John Mayer->Jimmy Page"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_contains_not() { - let filter = EdgeFilter::src().name().not_contains("Mayer"); - let expected_results: Vec<&str> = - vec!["1->2", "2->1", "2->3", "3->1", "David Gilmour->John Mayer"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_fuzzy_search() { - let filter = EdgeFilter::src().name().fuzzy_search("John", 2, true); - let expected_results: Vec<&str> = vec!["John Mayer->Jimmy Page"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src().name().fuzzy_search("John", 2, false); - let expected_results: Vec<&str> = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src().name().fuzzy_search("John May", 2, false); - let expected_results: Vec<&str> = vec!["John Mayer->Jimmy Page"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_not_src() { - // `not` pinned to the composite complement it resolved to before the expr ops were in scope. - let filter = ComposableFilter::not(EdgeFilter::src().name().is_not_in(vec!["1"])); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_id_eq() { - let filter = EdgeFilter::src().id().eq("3"); - let expected_results = vec!["3->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src().id().eq(3); - let expected_results = vec!["3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_id_eq() { - let filter = EdgeFilter::dst().id().eq("3"); - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst().id().eq(3); - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_id_ne() { - let filter = EdgeFilter::src().id().ne("3"); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src().id().ne(3); - let expected_results = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_id_ne() { - let filter = EdgeFilter::dst().id().ne("3"); - let expected_results = vec![ - "1->2", - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst().id().ne(3); - let expected_results = vec!["1->2", "2->1", "3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_id_src_is_in() { - let filter = EdgeFilter::src().id().is_in(vec!["3"]); - let expected_results = vec!["3->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src().id().is_in(vec![3]); - let expected_results = vec!["3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_id_dst_is_in() { - let filter = EdgeFilter::dst().id().is_in(vec!["3"]); - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst().id().is_in(vec![3]); - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_id_src_is_not_in() { - let filter = EdgeFilter::src().id().is_not_in(vec!["3"]); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src().id().is_not_in(vec![3]); - let expected_results = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_id_dst_is_not_in() { - let filter = EdgeFilter::dst().id().is_not_in(vec!["3"]); - let expected_results = vec![ - "1->2", - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst().id().is_not_in(vec![3]); - let expected_results = vec!["1->2", "2->1", "3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_id_lt() { - let filter = EdgeFilter::src().id().lt(3); - let expected_results = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_id_lt() { - let filter = EdgeFilter::dst().id().lt(3); - let expected_results = vec!["1->2", "2->1", "3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_id_le() { - let filter = EdgeFilter::src().id().le(3); - let expected_results = vec!["1->2", "2->1", "2->3", "3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_id_le() { - let filter = EdgeFilter::dst().id().le(3); - let expected_results = vec!["1->2", "2->1", "2->3", "3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_id_gt() { - let filter = EdgeFilter::src().id().gt(1); - let expected_results = vec!["2->1", "2->3", "3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_id_gt() { - let filter = EdgeFilter::dst().id().gt(1); - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_id_ge() { - let filter = EdgeFilter::src().id().ge(1); - let expected_results = vec!["1->2", "2->1", "2->3", "3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_id_ge() { - let filter = EdgeFilter::dst().id().ge(1); - let expected_results = vec!["1->2", "2->1", "2->3", "3->1"]; - assert_filter_edges_results( - init_edges_graph_with_num_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_name_starts_with() { - let filter = EdgeFilter::src().name().starts_with("Tw"); - let expected_results = vec!["Two->One", "Two->Three"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_id_ends_with() { - let filter = EdgeFilter::src().id().ends_with("don"); - let expected_results = vec!["London->Paris"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_id_contains() { - let filter = EdgeFilter::src().id().contains("don"); - let expected_results = vec!["London->Paris"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_id_contains() { - let filter = EdgeFilter::dst().id().contains("Par"); - let expected_results = vec!["London->Paris"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_name_not_contains() { - let filter = EdgeFilter::dst().name().not_contains("Par"); - let expected_results = vec![ - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - "Three->One", - "Two->One", - "Two->Three", - ]; - assert_filter_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_src_id_is_in() { - let filter = EdgeFilter::src().id().is_in(["Two"]); - let expected_results = vec!["Two->One", "Two->Three"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_dst_id_is_not_in() { - let filter = EdgeFilter::dst().id().is_not_in(["One"]); - let expected_results = vec![ - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - "London->Paris", - "Two->Three", - ]; - assert_filter_edges_results( - init_edges_graph_with_str_ids, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_is_active_edge_window() { - let filter = EdgeFilter.window(1, 3).is_active(); - let expected_results = vec!["London->Paris", "Two->Three"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_is_active_edge_after() { - let filter = EdgeFilter.after(3).is_active(); - let expected_results = vec![ - "Bangalore->Bangalore", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_is_active_edge_before() { - let filter = EdgeFilter.before(3).is_active(); - let expected_results = vec!["London->Paris", "Two->Three"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_is_active_edge_snapshot_latest() { - let filter = EdgeFilter.snapshot_latest().is_active(); - let expected_results = vec!["Bangalore->Bangalore"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_is_valid_edge_window() { - let filter = EdgeFilter.window(1, 3).is_valid(); - let expected_results = vec!["London->Paris", "Two->Three"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - - let filter = EdgeFilter.window(1, 4).is_valid(); - let expected_results = vec!["Three->One", "Two->One", "Two->Three"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - } - - #[test] - fn test_is_valid_edge_snapshot_at() { - let filter = EdgeFilter.snapshot_at(2).is_valid(); - let expected_results = vec!["London->Paris", "Two->Three"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - - let filter = EdgeFilter.snapshot_at(3).is_valid(); - let expected_results = vec!["Three->One", "Two->One", "Two->Three"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - } - - #[test] - fn test_is_valid_edge_snapshot_latest() { - let filter = EdgeFilter.snapshot_latest().is_valid(); - let expected_results = vec![ - "Bangalore->Bangalore", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - "Three->One", - "Two->One", - "Two->Three", - ]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestGraphVariants::PersistentGraph, - ); - } - - // Disk graph doesn't support deletions - #[test] - fn test_is_deleted_edge_after() { - let filter = EdgeFilter.after(1).is_deleted(); - let expected_results = vec!["London->Paris"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_is_deleted_edge_before() { - let filter = EdgeFilter.before(4).is_deleted(); - let expected_results = vec!["London->Paris"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_is_self_loop_edge_window() { - // window has no effect on is_self_loop and because we are using an `EdgeFilter` as the - // entrypoint, the window is only applied to the edges, not the graph - let filter = EdgeFilter.window(1, 3).is_self_loop(); - let expected_results_self_loop = vec!["Bangalore->Bangalore"]; - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results_self_loop, - TestVariants::All, - ); - - // window doesn't make a difference for `is_self_loop` - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results_self_loop, - TestVariants::All, - ); - - let filter = EdgeFilter.window(1, 6).is_self_loop(); - assert_filter_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results_self_loop, - TestVariants::All, - ); - assert_select_edges_results( - init_edges_graph_with_str_ids_del, - IdentityGraphTransformer, - filter.clone(), - &expected_results_self_loop, - TestVariants::All, - ); - } -} - -mod test_edge_property_filter { - use crate::filter_tests::test_filters::{ - init_edges_graph, init_edges_graph2, IdentityGraphTransformer, - }; - use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - ComposableFilter, PropertyFilterFactory, TemporalPropertyFilterFactory, ViewWrapOps, - }; - - use raphtory_api::core::entities::properties::prop::Prop; - use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; - - #[test] - fn test_filter_edges_for_property_eq() { - let filter = EdgeFilter.property("p2").eq(2u64); - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p30").temporal().first().eq("Old_boat"); - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p20").temporal().all().eq("Gold_ship"); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_ne() { - let filter = EdgeFilter.property("p2").ne(2u64); - let expected_results = vec![ - "1->2", - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p30").temporal().first().ne("Old_boat"); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p30").temporal().all().ne("Classic"); - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_lt() { - let filter = EdgeFilter.property("p2").lt(10u64); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().first().lt(5u64); - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().all().lt(10u64); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_le() { - let filter = EdgeFilter.property("p2").le(6u64); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().first().le(3u64); - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().all().le(5u64); - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_gt() { - let filter = EdgeFilter.property("p2").gt(2u64); - let expected_results = vec![ - "1->2", - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().first().gt(5u64); - let expected_results = vec![ - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().all().gt(5u64); - let expected_results = vec![ - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_ge() { - let filter = EdgeFilter.property("p2").ge(2u64); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().first().ge(6u64); - let expected_results = vec![ - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().all().ge(6u64); - let expected_results = vec![ - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_in() { - let filter = EdgeFilter.property("p2").is_in(vec![Prop::U64(6)]); - let expected_results = vec![ - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p2") - .is_in(vec![Prop::U64(2), Prop::U64(6)]); - let expected_results = vec![ - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p2") - .temporal() - .first() - .is_in(vec![Prop::U64(6)]); - let expected_results = vec![ - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p2") - .temporal() - .all() - .is_in(vec![Prop::U64(6)]); - let expected_results = vec![ - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_not_in() { - let filter = EdgeFilter.property("p2").is_not_in(vec![Prop::U64(6)]); - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p2") - .temporal() - .first() - .is_not_in(vec![Prop::U64(6)]); - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p2") - .temporal() - .all() - .is_not_in(vec![Prop::U64(6)]); - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_is_some() { - let filter = EdgeFilter.property("p2").is_some(); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().first().is_some(); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_is_none() { - let filter = EdgeFilter.property("p2").is_none(); - let expected_results = Vec::<&str>::new(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p2").temporal().first().is_none(); - let expected_results = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_starts_with() { - let filter = EdgeFilter.property("p10").starts_with("Pa"); - let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .any() - .starts_with("Pape"); - let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .last() - .starts_with("Paper"); - let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .last() - .starts_with("Traffic"); - let expected_results: Vec<&str> = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p30") - .temporal() - .first() - .starts_with("Old"); - let expected_results: Vec<&str> = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p20") - .temporal() - .all() - .starts_with("Gold"); - let expected_results: Vec<&str> = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_ends_with() { - let filter = EdgeFilter.property("p10").ends_with("lane"); - let expected_results: Vec<&str> = vec!["1->2", "2->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .any() - .ends_with("ship"); - let expected_results: Vec<&str> = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .last() - .ends_with("ane"); - let expected_results: Vec<&str> = vec!["1->2", "2->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .last() - .ends_with("marcus"); - let expected_results: Vec<&str> = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p20") - .temporal() - .first() - .ends_with("boat"); - let expected_results: Vec<&str> = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p20") - .temporal() - .all() - .ends_with("ship"); - let expected_results: Vec<&str> = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_contains() { - let filter = EdgeFilter.property("p10").contains("Paper"); - let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .any() - .contains("Paper"); - let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .last() - .contains("Paper"); - let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p20") - .temporal() - .first() - .contains("boat"); - let expected_results: Vec<&str> = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p20").temporal().all().contains("ship"); - let expected_results: Vec<&str> = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_property_contains_not() { - let filter = EdgeFilter.property("p10").not_contains("ship"); - let expected_results: Vec<&str> = vec!["1->2", "2->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .any() - .not_contains("ship"); - let expected_results: Vec<&str> = vec!["1->2", "2->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p10") - .temporal() - .last() - .not_contains("ship"); - let expected_results: Vec<&str> = vec!["1->2", "2->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p20") - .temporal() - .first() - .not_contains("boat"); - let expected_results: Vec<&str> = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p30") - .temporal() - .all() - .not_contains("ship"); - let expected_results: Vec<&str> = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_by_fuzzy_search() { - let filter = EdgeFilter.property("p1").fuzzy_search("shiv", 2, true); - let expected_results: Vec<&str> = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p1").fuzzy_search("ShiV", 2, true); - let expected_results: Vec<&str> = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p1").fuzzy_search("shiv", 2, false); - let expected_results: Vec<&str> = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_filter_edges_for_not_property() { - let filter = EdgeFilter.property("p2").ne(2u64).not(); - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_edges_window_filter() { - let filter = EdgeFilter - .window(1, 3) - .property("p2") - .temporal() - .sum() - .ge(2u64); - - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .window(1, 5) - .property("p2") - .temporal() - .sum() - .ge(2u64); - - let expected_results = vec![ - "1->2", - "2->3", - "3->1", - "2->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_edges_window_filter_on_non_temporal_property() { - let filter1 = EdgeFilter.window(1, 2).property("p1").eq("shivam_kapoor"); - let filter2 = EdgeFilter - .window(100, 200) - .property("p1") - .eq("shivam_kapoor"); - - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter1.clone(), - &expected_results, - TestVariants::All, - ); - - let expected_results = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter2.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - let filter2 = EdgeFilter - .window(100, 200) - .property("p1") - .eq("shivam_kapoor"); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter2.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_edges_window_filter_any_all_over_window() { - let filter_any = EdgeFilter - .window(2, 4) - .property("p20") - .temporal() - .any() - .eq("Gold_boat"); - - let expected_any = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_any.clone(), - &expected_any, - TestVariants::All, - ); - - let filter_all = EdgeFilter - .window(2, 4) - .property("p20") - .temporal() - .all() - .eq("Gold_boat"); - - let expected_all: Vec<&str> = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_all.clone(), - &expected_all, - TestVariants::All, - ); - } - - #[test] - fn test_edges_window_filter_and() { - let filter1 = EdgeFilter - .window(3, 6) - .property("p10") - .temporal() - .any() - .eq("Paper_airplane"); - - let filter2 = EdgeFilter - .window(3, 6) - .property("p2") - .temporal() - .sum() - .eq(6u64); - - let filter = filter1.and(filter2); - - let expected_results = vec!["2->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_edges_layer_filter() { - let filter = EdgeFilter - .layer("fire_nation") - .property("p2") - .temporal() - .sum() - .ge(2u64); - - let expected_results = vec!["1->2", "3->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_edges_at_filter() { - // Only time=2 contributes; edge 2->3 has p2=2 at t=2 - let filter = EdgeFilter.at(2).property("p2").temporal().sum().eq(2u64); - - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - // Only time=3 contributes; edge 3->1 has p2=6 at t=3 - let filter = EdgeFilter.at(3).property("p2").temporal().sum().eq(6u64); - - let expected_results = vec!["3->1", "2->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_edges_after_filter() { - // after(2) means t >= 3 - let filter = EdgeFilter.after(2).property("p2").temporal().sum().ge(6u64); - - // At t=3: 3->1 and 2->1 have p2=6 - // At t=4: David->John and John->Jimmy have p2=6 - let expected_results = vec![ - "3->1", - "2->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_edges_before_filter() { - // before(3) means t <= 2 - let filter = EdgeFilter - .before(3) - .property("p2") - .temporal() - .sum() - .eq(2u64); - - // Only t=2 contributes for p2=2 -> 2->3 - let expected_results = vec!["2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - // And p2=6 edges shouldn't match, because their p2=6 lives at t=3+. - let filter = EdgeFilter - .before(3) - .property("p2") - .temporal() - .sum() - .eq(6u64); - - let expected_results = vec![]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_edges_latest_filter() { - // At latest time (currently t=4), only the t=4 edges exist in the Event graph. - // Use EventOnly so the expectation is stable and matches node-style. - let filter = EdgeFilter.latest().property("p2").eq(6u64); - - let expected_results = vec!["David Gilmour->John Mayer", "John Mayer->Jimmy Page"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); - } - - #[test] - fn test_edges_snapshot_at_semantics_event_graph() { - let t = 2; - - let filter_snapshot = EdgeFilter - .snapshot_at(t) - .property("p2") - .temporal() - .sum() - .eq(2u64); - - let filter_before = EdgeFilter - .before(t + 1) - .property("p2") - .temporal() - .sum() - .eq(2u64); - - let expected_results = vec!["2->3"]; - - // snapshot_at - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_snapshot.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - // before(t+1) - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_before.clone(), - &expected_results, - TestVariants::EventOnly, - ); - } - - #[test] - fn test_edges_snapshot_at_semantics_persistent_graph() { - let t = 2; - - let filter_snapshot = EdgeFilter - .snapshot_at(t) - .property("p2") - .temporal() - .sum() - .eq(2u64); - - let filter_at = EdgeFilter.at(t).property("p2").temporal().sum().eq(2u64); - - let expected_results = vec!["2->3"]; - - // snapshot_at - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_snapshot.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - - // at(t) - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_at.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_edges_snapshot_latest_semantics_event_graph() { - let filter_snapshot_latest = EdgeFilter - .snapshot_latest() - .property("p2") - .temporal() - .sum() - .ge(6u64); - - let filter_noop = EdgeFilter.property("p2").temporal().sum().ge(6u64); - - // Across the whole event history, p2=6 appears at t=3 and t=4. - let expected_results = vec![ - "3->1", - "2->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - - // snapshot_latest - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_snapshot_latest.clone(), - &expected_results, - TestVariants::EventOnly, - ); - - // no-op baseline - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_noop.clone(), - &expected_results, - TestVariants::EventOnly, - ); - } - - #[test] - fn test_edges_snapshot_latest_semantics_persistent_graph() { - let filter_snapshot_latest = EdgeFilter.snapshot_latest().property("p2").eq(6u64); - - let filter_latest = EdgeFilter.latest().property("p2").eq(6u64); - - // In persistent latest state at t=4, these edges have p2=6: - // - t=3 edges: 3->1, 2->1 - // - t=4 edges: David->John, John->Jimmy - let expected_results = vec![ - "3->1", - "2->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - - // snapshot_latest - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_snapshot_latest.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - - // latest - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter_latest.clone(), - &expected_results, - TestVariants::PersistentOnly, - ); - } - - #[test] - fn test_edges_layer_then_window_ordering() { - // In layer "fire_nation" within window [1,3), edge 1->2 matches p1 == "shivam_kapoor". - let filter = EdgeFilter - .layer("fire_nation") - .window(1, 3) - .property("p1") - .eq("shivam_kapoor"); - - let expected_results = vec!["1->2"]; - - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_edges_window_then_layer_ordering() { - // Same semantics, reversed chaining order. - let filter = EdgeFilter - .window(1, 3) - .layer("fire_nation") - .property("p1") - .eq("shivam_kapoor"); - - let expected_results = vec!["1->2"]; - - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_edges_latest_layer() { - let filter = EdgeFilter - .latest() - .layer("fire_nation") - .property("p2") - .temporal() - .last() - .eq(7u64); - - let expected_results = vec![]; - - assert_filter_edges_results( - init_edges_graph2, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); - } - - #[test] - fn test_edges_layer_latest() { - let filter = EdgeFilter - .layer("fire_nation") - .latest() - .property("p2") - .temporal() - .last() - .eq(7u64); - - let expected_results = vec!["1->2"]; - - assert_filter_edges_results( - init_edges_graph2, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::EventOnly, - ); - } -} - -mod test_edge_composite_filter { - use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, - property_filter::ops::PropertyFilterOps, ComposableFilter, PropertyFilterFactory, - }; - use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; - - use crate::filter_tests::test_filters::{init_edges_graph, IdentityGraphTransformer}; - - #[test] - fn test_filter_edge_for_src_dst() { - let filter = EdgeFilter::src() - .name() - .eq("3") - .and(EdgeFilter::dst().name().eq("1")); - let expected_results = vec!["3->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_unique_results_from_composite_filters() { - let filter = EdgeFilter - .property("p2") - .ge(2u64) - .and(EdgeFilter.property("p2").ge(1u64)); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p2") - .ge(2u64) - .or(EdgeFilter.property("p2").ge(5u64)); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_composite_filter_edges() { - let filter = EdgeFilter - .property("p2") - .eq(2u64) - .and(EdgeFilter.property("p1").eq("kapoor")); - let expected_results = Vec::<&str>::new(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p2") - .eq(2u64) - .or(EdgeFilter.property("p1").eq("shivam_kapoor")); - let expected_results = vec!["1->2", "2->3"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter.property("p1").eq("pometry").or(EdgeFilter - .property("p2") - .eq(6u64) - .and(EdgeFilter.property("p3").eq(1u64))); - let expected_results = vec![ - "2->1", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src() - .name() - .eq("13") - .and(EdgeFilter.property("p1").eq("prop1")); - let expected_results = Vec::<&str>::new(); - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter - .property("p2") - .eq(4u64) - .and(EdgeFilter.property("p1").eq("shivam_kapoor")); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src() - .name() - .eq("1") - .and(EdgeFilter.property("p1").eq("shivam_kapoor")); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::dst() - .name() - .eq("1") - .and(EdgeFilter.property("p2").eq(6u64)); - let expected_results = vec!["2->1", "3->1"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src() - .name() - .eq("1") - .and(EdgeFilter.property("p1").eq("shivam_kapoor")) - .or(EdgeFilter.property("p3").eq(5u64)); - let expected_results = vec!["1->2"]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } - - #[test] - fn test_not_composite_filter_edges() { - let filter = EdgeFilter::src() - .name() - .eq("13") - .and(EdgeFilter.property("p1").eq("prop1")) - .not(); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - - let filter = EdgeFilter::src() - .name() - .eq("13") - .and(EdgeFilter.property("p1").eq("prop1").not()) - .not(); - let expected_results = vec![ - "1->2", - "2->1", - "2->3", - "3->1", - "David Gilmour->John Mayer", - "John Mayer->Jimmy Page", - ]; - assert_filter_edges_results( - init_edges_graph, - IdentityGraphTransformer, - filter.clone(), - &expected_results, - TestVariants::All, - ); - } -} diff --git a/raphtory-tests/tests/filter_tests/test_layers.rs b/raphtory-tests/tests/filter_tests/test_layers.rs index 840ff9da0d..4e5bce0285 100644 --- a/raphtory-tests/tests/filter_tests/test_layers.rs +++ b/raphtory-tests/tests/filter_tests/test_layers.rs @@ -30,12 +30,14 @@ impl GraphTransformer for LayeredGraphWindowTransformer { } pub mod test_nodes_filters_layer_graph { - use raphtory::db::graph::views::filter::model::property_filter::ops::PropertyFilterOps; use crate::filter_tests::test_layers::{ LayeredGraphTransformer, LayeredGraphWindowTransformer, }; - use raphtory::{db::graph::views::filter::model::PropertyFilterFactory, prelude::NodeFilter}; + use raphtory::{ + db::graph::views::filter::model::PropertyExprFactory, + prelude::{EntityExprFilterOps, NodeFilter}, + }; use crate::filter_tests::{init_graph, Edges, Nodes}; use raphtory_tests::assertions::{assert_filter_nodes_results, TestVariants}; @@ -180,9 +182,7 @@ mod test_edges_filters_layer_graph { Edges, Nodes, }; use raphtory::{ - db::graph::views::filter::model::{ - property_filter::ops::PropertyFilterOps, PropertyFilterFactory, - }, + db::graph::views::filter::model::{EntityExprFilterOps, PropertyExprFactory}, prelude::EdgeFilter, }; use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; diff --git a/raphtory-tests/tests/filter_tests/tests_node_type_filtered_subgraph.rs b/raphtory-tests/tests/filter_tests/tests_node_type_filtered_subgraph.rs index fcfc443379..20439ee0b0 100644 --- a/raphtory-tests/tests/filter_tests/tests_node_type_filtered_subgraph.rs +++ b/raphtory-tests/tests/filter_tests/tests_node_type_filtered_subgraph.rs @@ -4,9 +4,7 @@ use raphtory::{ api::view::Filter, graph::{ graph::assert_graph_equal, - views::filter::model::{ - property_filter::ops::PropertyFilterOps, PropertyFilterFactory, - }, + views::filter::model::{EntityExprFilterOps, PropertyExprFactory}, }, }, prelude::*, @@ -210,7 +208,6 @@ mod test_filters_node_type_filtered_subgraph { } mod test_nodes_filters_node_type_filtered_subgraph { - use raphtory::db::graph::views::filter::model::property_filter::ops::PropertyFilterOps; use crate::filter_tests::{init_graph, Edges, Nodes}; @@ -218,7 +215,8 @@ mod test_filters_node_type_filtered_subgraph { NodeTypeGraphTransformer, WindowedNodeTypeGraphTransformer, }; use raphtory::{ - db::graph::views::filter::model::PropertyFilterFactory, prelude::NodeFilter, + db::graph::views::filter::model::PropertyExprFactory, + prelude::{EntityExprFilterOps, NodeFilter}, }; use raphtory_tests::assertions::{ assert_filter_nodes_results, @@ -302,10 +300,7 @@ mod test_filters_node_type_filtered_subgraph { mod test_edges_filters_node_type_filtered_subgraph { use raphtory::{ - db::{ - api::view::StaticGraphViewOps, - graph::views::filter::model::property_filter::ops::PropertyFilterOps, - }, + db::api::view::StaticGraphViewOps, prelude::{AdditionOps, NO_PROPS}, }; use raphtory_api::core::entities::properties::prop::Prop; @@ -419,7 +414,8 @@ mod test_filters_node_type_filtered_subgraph { NodeTypeGraphTransformer, WindowedNodeTypeGraphTransformer, }; use raphtory::{ - db::graph::views::filter::model::PropertyFilterFactory, prelude::EdgeFilter, + db::graph::views::filter::model::PropertyExprFactory, + prelude::{EdgeFilter, EntityExprFilterOps}, }; use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; diff --git a/raphtory-tests/tests/filter_tests/views_test.rs b/raphtory-tests/tests/filter_tests/views_test.rs index aa1de19457..be1158330f 100644 --- a/raphtory-tests/tests/filter_tests/views_test.rs +++ b/raphtory-tests/tests/filter_tests/views_test.rs @@ -3,9 +3,8 @@ mod test_nodes_filters_window_graph { db::{ api::view::{filter_ops::Filter, StaticGraphViewOps}, graph::views::filter::model::{ - node_filter::{ops::NodeFilterOps, NodeFilter}, - property_filter::ops::PropertyFilterOps, - ComposableFilter, PropertyFilterFactory, + node_filter::{NodeFilter, NodeFilterFactory}, + ComposableFilter, EntityExprFilterOps, PropertyExprFactory, }, }, errors::GraphError, @@ -222,7 +221,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_node_name_eq() { - let filter = NodeFilter::name().eq("N2"); + let filter = NodeFilter.name().eq("N2"); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -235,7 +234,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_name_eq() { - let filter = NodeFilter::name().eq("N2"); + let filter = NodeFilter.name().eq("N2"); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -248,7 +247,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_node_name_ne() { - let filter = NodeFilter::name().ne("N2"); + let filter = NodeFilter.name().ne("N2"); let expected_results = vec!["N1", "N3", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -261,7 +260,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_name_ne() { - let filter = NodeFilter::name().ne("N2"); + let filter = NodeFilter.name().ne("N2"); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N14", "N15", "N3", "N5", "N6", "N7", "N8", "N9", ]; @@ -276,7 +275,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_node_name_in() { - let filter = NodeFilter::name().is_in(vec!["N2"]); + let filter = NodeFilter.name().is_in(vec!["N2"]); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -286,7 +285,7 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter::name().is_in(vec!["N2", "N5"]); + let filter = NodeFilter.name().is_in(vec!["N2", "N5"]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -299,7 +298,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_name_in() { - let filter = NodeFilter::name().is_in(vec!["N2"]); + let filter = NodeFilter.name().is_in(vec!["N2"]); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -309,7 +308,7 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter::name().is_in(vec!["N2", "N5"]); + let filter = NodeFilter.name().is_in(vec!["N2", "N5"]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -322,7 +321,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_node_name_not_in() { - let filter = NodeFilter::name().is_not_in(vec!["N5"]); + let filter = NodeFilter.name().is_not_in(vec!["N5"]); let expected_results = vec!["N1", "N2", "N3", "N6"]; assert_filter_nodes_results( init_graph, @@ -335,7 +334,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_name_not_in() { - let filter = NodeFilter::name().is_not_in(vec!["N5"]); + let filter = NodeFilter.name().is_not_in(vec!["N5"]); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N14", "N15", "N2", "N3", "N6", "N7", "N8", "N9", ]; @@ -350,7 +349,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_node_type_eq() { - let filter = NodeFilter::node_type().eq("fire_nation"); + let filter = NodeFilter.node_type().eq("fire_nation"); let expected_results = vec!["N6"]; assert_filter_nodes_results( init_graph, @@ -363,7 +362,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_type_eq() { - let filter = NodeFilter::node_type().eq("fire_nation"); + let filter = NodeFilter.node_type().eq("fire_nation"); let expected_results = vec!["N6", "N8"]; assert_filter_nodes_results( init_graph, @@ -376,7 +375,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_node_type_ne() { - let filter = NodeFilter::node_type().ne("fire_nation"); + let filter = NodeFilter.node_type().ne("fire_nation"); let expected_results = vec!["N1", "N2", "N3", "N5"]; assert_filter_nodes_results( init_graph, @@ -389,7 +388,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_type_ne() { - let filter = NodeFilter::node_type().ne("fire_nation"); + let filter = NodeFilter.node_type().ne("fire_nation"); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N14", "N15", "N2", "N3", "N5", "N7", "N9", ]; @@ -404,7 +403,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_node_type_in() { - let filter = NodeFilter::node_type().is_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_in(vec!["fire_nation"]); let expected_results = vec!["N6"]; assert_filter_nodes_results( init_graph, @@ -414,7 +413,9 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter::node_type().is_in(vec!["fire_nation", "air_nomad"]); + let filter = NodeFilter + .node_type() + .is_in(vec!["fire_nation", "air_nomad"]); let expected_results = vec!["N1", "N3", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -427,7 +428,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_type_in() { - let filter = NodeFilter::node_type().is_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_in(vec!["fire_nation"]); let expected_results = vec!["N6", "N8"]; assert_filter_nodes_results( init_graph, @@ -437,7 +438,9 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter::node_type().is_in(vec!["fire_nation", "air_nomad"]); + let filter = NodeFilter + .node_type() + .is_in(vec!["fire_nation", "air_nomad"]); let expected_results = vec!["N1", "N3", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -451,7 +454,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_node_type_not_in() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::node_type().is_not_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]); let expected_results = vec!["N1", "N2", "N3", "N5"]; assert_filter_nodes_results( init_graph, @@ -464,7 +467,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_node_type_not_in() { - let filter = NodeFilter::node_type().is_not_in(vec!["fire_nation"]); + let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N14", "N15", "N2", "N3", "N5", "N7", "N9", ]; @@ -884,11 +887,11 @@ mod test_nodes_filters_window_graph { )); let graph = init_graph(Graph::new()); assert!(matches!( - graph.window(1, 9).filter(filter.clone()).unwrap_err(), + graph.window(1, 9).filter(filter.clone()).map(|_| ()).unwrap_err(), GraphError::PropertyMissingError(ref name) if name == "x" )); assert!(matches!( - graph.persistent_graph().window(1, 9).filter(filter).unwrap_err(), + graph.persistent_graph().window(1, 9).filter(filter).map(|_| ()).unwrap_err(), GraphError::PropertyMissingError(ref name) if name == "x" )); } @@ -996,7 +999,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_property_in() { - let filter = NodeFilter.property("p1").is_in(vec![2u64.into()]); + let filter = NodeFilter.property("p1").is_in(vec![2u64]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -1006,7 +1009,7 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter.property("k1").is_in(vec![2i64.into()]); + let filter = NodeFilter.property("k1").is_in(vec![2i64]); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -1016,9 +1019,7 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter - .property("k2") - .is_in(vec!["Paper_Airplane".into()]); + let filter = NodeFilter.property("k2").is_in(vec!["Paper_Airplane"]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1028,7 +1029,7 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter.property("k3").is_in(vec![true.into()]); + let filter = NodeFilter.property("k3").is_in(vec![true]); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -1038,7 +1039,7 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter.property("k4").is_in(vec![6.0f64.into()]); + let filter = NodeFilter.property("k4").is_in(vec![6.0f64]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1051,7 +1052,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_property_in() { - let filter = NodeFilter.property("p1").is_in(vec![2u64.into()]); + let filter = NodeFilter.property("p1").is_in(vec![2u64]); let expected_results = vec!["N2", "N5", "N8", "N9"]; assert_filter_nodes_results( init_graph, @@ -1061,7 +1062,7 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter.property("k1").is_in(vec![2i64.into()]); + let filter = NodeFilter.property("k1").is_in(vec![2i64]); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -1071,9 +1072,7 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter - .property("k2") - .is_in(vec!["Paper_Airplane".into()]); + let filter = NodeFilter.property("k2").is_in(vec!["Paper_Airplane"]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1083,7 +1082,7 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter.property("k3").is_in(vec![true.into()]); + let filter = NodeFilter.property("k3").is_in(vec![true]); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -1093,7 +1092,7 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter.property("k4").is_in(vec![6.0f64.into()]); + let filter = NodeFilter.property("k4").is_in(vec![6.0f64]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1106,7 +1105,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_for_property_not_in() { - let filter = NodeFilter.property("p1").is_not_in(vec![1u64.into()]); + let filter = NodeFilter.property("p1").is_not_in(vec![1u64]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -1116,7 +1115,7 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter.property("k1").is_not_in(vec![2i64.into()]); + let filter = NodeFilter.property("k1").is_not_in(vec![2i64]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1126,9 +1125,7 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter - .property("k2") - .is_not_in(vec!["Paper_Airplane".into()]); + let filter = NodeFilter.property("k2").is_not_in(vec!["Paper_Airplane"]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -1138,7 +1135,7 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter.property("k3").is_not_in(vec![true.into()]); + let filter = NodeFilter.property("k3").is_not_in(vec![true]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1148,7 +1145,7 @@ mod test_nodes_filters_window_graph { TestVariants::EventOnly, ); - let filter = NodeFilter.property("k4").is_not_in(vec![6.0f64.into()]); + let filter = NodeFilter.property("k4").is_not_in(vec![6.0f64]); let expected_results = vec!["N2", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -1161,7 +1158,7 @@ mod test_nodes_filters_window_graph { #[test] fn test_nodes_filters_pg_for_property_not_in() { - let filter = NodeFilter.property("p1").is_not_in(vec![1u64.into()]); + let filter = NodeFilter.property("p1").is_not_in(vec![1u64]); let expected_results = vec!["N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"]; assert_filter_nodes_results( init_graph, @@ -1171,7 +1168,7 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter.property("k1").is_not_in(vec![2i64.into()]); + let filter = NodeFilter.property("k1").is_not_in(vec![2i64]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1181,9 +1178,7 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter - .property("k2") - .is_not_in(vec!["Paper_Airplane".into()]); + let filter = NodeFilter.property("k2").is_not_in(vec!["Paper_Airplane"]); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -1193,7 +1188,7 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter.property("k3").is_not_in(vec![true.into()]); + let filter = NodeFilter.property("k3").is_not_in(vec![true]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1203,7 +1198,7 @@ mod test_nodes_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = NodeFilter.property("k4").is_not_in(vec![6.0f64.into()]); + let filter = NodeFilter.property("k4").is_not_in(vec![6.0f64]); let expected_results = vec!["N12", "N2", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -1393,8 +1388,8 @@ mod test_edges_filters_window_graph { db::{ api::view::{filter_ops::Filter, StaticGraphViewOps}, graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, - property_filter::ops::PropertyFilterOps, ComposableFilter, PropertyFilterFactory, + edge_filter::EdgeFilter, node_filter::NodeFilterFactory, ComposableFilter, + EntityExprFilterOps, PropertyExprFactory, }, }, errors::GraphError, @@ -2204,11 +2199,11 @@ mod test_edges_filters_window_graph { )); let graph = init_graph(Graph::new()); assert!(matches!( - graph.window(1, 9).filter(filter.clone()).unwrap_err(), + graph.window(1, 9).filter(filter.clone()).map(|_| ()).unwrap_err(), GraphError::PropertyMissingError(ref name) if name == "x" )); assert!(matches!( - graph.persistent_graph().window(1, 9).filter(filter).unwrap_err(), + graph.persistent_graph().window(1, 9).filter(filter).map(|_| ()).unwrap_err(), GraphError::PropertyMissingError(ref name) if name == "x" )); } @@ -2319,7 +2314,7 @@ mod test_edges_filters_window_graph { #[test] fn test_edges_filters_for_property_in() { - let filter = EdgeFilter.property("p1").is_in(vec![2u64.into()]); + let filter = EdgeFilter.property("p1").is_in(vec![2u64]); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -2329,7 +2324,7 @@ mod test_edges_filters_window_graph { TestVariants::EventOnly, ); - let filter = EdgeFilter.property("k1").is_in(vec![2i64.into()]); + let filter = EdgeFilter.property("k1").is_in(vec![2i64]); let expected_results = vec!["N2->N3"]; assert_filter_edges_results( init_graph, @@ -2339,9 +2334,7 @@ mod test_edges_filters_window_graph { TestVariants::EventOnly, ); - let filter = EdgeFilter - .property("k2") - .is_in(vec!["Paper_Airplane".into()]); + let filter = EdgeFilter.property("k2").is_in(vec!["Paper_Airplane"]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -2351,7 +2344,7 @@ mod test_edges_filters_window_graph { TestVariants::EventOnly, ); - let filter = EdgeFilter.property("k3").is_in(vec![true.into()]); + let filter = EdgeFilter.property("k3").is_in(vec![true]); let expected_results = vec!["N2->N3"]; assert_filter_edges_results( init_graph, @@ -2361,7 +2354,7 @@ mod test_edges_filters_window_graph { TestVariants::EventOnly, ); - let filter = EdgeFilter.property("k4").is_in(vec![6.0f64.into()]); + let filter = EdgeFilter.property("k4").is_in(vec![6.0f64]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -2374,7 +2367,7 @@ mod test_edges_filters_window_graph { #[test] fn test_edges_filters_pg_for_property_in() { - let filter = EdgeFilter.property("p1").is_in(vec![2u64.into()]); + let filter = EdgeFilter.property("p1").is_in(vec![2u64]); let expected_results = vec!["N2->N3", "N5->N6", "N8->N9", "N9->N10"]; assert_filter_edges_results( init_graph, @@ -2384,7 +2377,7 @@ mod test_edges_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = EdgeFilter.property("k1").is_in(vec![2i64.into()]); + let filter = EdgeFilter.property("k1").is_in(vec![2i64]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -2394,9 +2387,7 @@ mod test_edges_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = EdgeFilter - .property("k2") - .is_in(vec!["Paper_Airplane".into()]); + let filter = EdgeFilter.property("k2").is_in(vec!["Paper_Airplane"]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -2406,7 +2397,7 @@ mod test_edges_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = EdgeFilter.property("k3").is_in(vec![true.into()]); + let filter = EdgeFilter.property("k3").is_in(vec![true]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -2416,7 +2407,7 @@ mod test_edges_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = EdgeFilter.property("k4").is_in(vec![6.0f64.into()]); + let filter = EdgeFilter.property("k4").is_in(vec![6.0f64]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -2429,7 +2420,7 @@ mod test_edges_filters_window_graph { #[test] fn test_edges_filters_for_property_not_in() { - let filter = EdgeFilter.property("p1").is_not_in(vec![1u64.into()]); + let filter = EdgeFilter.property("p1").is_not_in(vec![1u64]); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -2439,7 +2430,7 @@ mod test_edges_filters_window_graph { TestVariants::EventOnly, ); - let filter = EdgeFilter.property("k1").is_not_in(vec![2i64.into()]); + let filter = EdgeFilter.property("k1").is_not_in(vec![2i64]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -2449,9 +2440,7 @@ mod test_edges_filters_window_graph { TestVariants::EventOnly, ); - let filter = EdgeFilter - .property("k2") - .is_not_in(vec!["Paper_Airplane".into()]); + let filter = EdgeFilter.property("k2").is_not_in(vec!["Paper_Airplane"]); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -2461,7 +2450,7 @@ mod test_edges_filters_window_graph { TestVariants::EventOnly, ); - let filter = EdgeFilter.property("k3").is_not_in(vec![true.into()]); + let filter = EdgeFilter.property("k3").is_not_in(vec![true]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -2471,7 +2460,7 @@ mod test_edges_filters_window_graph { TestVariants::EventOnly, ); - let filter = EdgeFilter.property("k4").is_not_in(vec![6.0f64.into()]); + let filter = EdgeFilter.property("k4").is_not_in(vec![6.0f64]); let expected_results = vec!["N2->N3", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -2484,7 +2473,7 @@ mod test_edges_filters_window_graph { #[test] fn test_edges_filters_pg_for_property_not_in() { - let filter = EdgeFilter.property("p1").is_not_in(vec![1u64.into()]); + let filter = EdgeFilter.property("p1").is_not_in(vec![1u64]); let expected_results = vec![ "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N2->N3", "N5->N6", "N8->N9", "N9->N10", ]; @@ -2496,7 +2485,7 @@ mod test_edges_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = EdgeFilter.property("k1").is_not_in(vec![2i64.into()]); + let filter = EdgeFilter.property("k1").is_not_in(vec![2i64]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -2506,9 +2495,7 @@ mod test_edges_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = EdgeFilter - .property("k2") - .is_not_in(vec!["Paper_Airplane".into()]); + let filter = EdgeFilter.property("k2").is_not_in(vec!["Paper_Airplane"]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -2518,7 +2505,7 @@ mod test_edges_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = EdgeFilter.property("k3").is_not_in(vec![true.into()]); + let filter = EdgeFilter.property("k3").is_not_in(vec![true]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -2528,7 +2515,7 @@ mod test_edges_filters_window_graph { TestVariants::PersistentOnly, ); - let filter = EdgeFilter.property("k4").is_not_in(vec![6.0f64.into()]); + let filter = EdgeFilter.property("k4").is_not_in(vec![6.0f64]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N6->N7", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, diff --git a/raphtory-tests/tests/qp3.rs b/raphtory-tests/tests/qp3.rs index d13cd9e21c..68ab30e6dc 100644 --- a/raphtory-tests/tests/qp3.rs +++ b/raphtory-tests/tests/qp3.rs @@ -1,7 +1,7 @@ use raphtory::{ db::{ api::view::Filter, - graph::views::filter::model::{node_expr::DynCreateOp, DynCreateFilter, EdgeViewFilterOps}, + graph::views::filter::model::{node_expr::DynCreateOp, DynCreateFilter}, }, prelude::*, }; diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 0885e0c7b3..0386919c28 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -5,12 +5,38 @@ use raphtory::{db::api::view::StaticGraphViewOps, prelude::*}; mod test_composite_filters { use raphtory::{ db::graph::views::filter::model::{ - edge_filter::EdgeFilter, filter::Filter, node_filter::NodeFilter, - property_filter::ops::PropertyFilterOps, PropertyExprFactory, + filter::Filter, + node_filter::NodeFilter, + property_filter::{PropertyFilter, PropertyFilterValue, PropertyRef}, + FilterOperator, }, prelude::IntoProp, }; use raphtory_api::core::{entities::properties::prop::Prop, storage::arc_str::ArcStr}; + use std::sync::Arc; + + /// A property condition as wire data, evaluated through + /// [`PropertyFilter::matches`]. + fn prop_filter( + operator: FilterOperator, + prop_value: PropertyFilterValue, + ) -> PropertyFilter { + PropertyFilter { + prop_ref: PropertyRef::Property("prop".to_string()), + prop_value, + operator, + ops: vec![], + entity: NodeFilter, + } + } + + fn single(value: impl IntoProp) -> PropertyFilterValue { + PropertyFilterValue::Single(value.into_prop()) + } + + fn set(values: impl IntoIterator) -> PropertyFilterValue { + PropertyFilterValue::Set(Arc::new(values.into_iter().collect())) + } #[test] fn test_fuzzy_search() { @@ -44,35 +70,53 @@ mod test_composite_filters { #[test] fn test_fuzzy_search_property() { - let filter = NodeFilter.property("prop").fuzzy_search("pomet", 2, false); + let filter = prop_filter( + FilterOperator::FuzzySearch { + levenshtein_distance: 2, + prefix_match: false, + }, + single("pomet"), + ); assert!(filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); } #[test] fn test_fuzzy_search_property_prefix_match() { - let filter = EdgeFilter.property("prop").fuzzy_search("pome", 2, false); + let filter = prop_filter( + FilterOperator::FuzzySearch { + levenshtein_distance: 2, + prefix_match: false, + }, + single("pome"), + ); assert!(!filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); - let filter = EdgeFilter.property("prop").fuzzy_search("pome", 2, true); + let filter = prop_filter( + FilterOperator::FuzzySearch { + levenshtein_distance: 2, + prefix_match: true, + }, + single("pome"), + ); assert!(filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); } #[test] fn test_contains_match() { - let filter = EdgeFilter.property("prop").contains("shivam"); + let filter = prop_filter(FilterOperator::Contains, single("shivam")); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam_kapoor")))); assert!(res); let res = filter.matches(None); assert!(!res); - let filter = EdgeFilter.property("prop").contains("am_ka"); + let filter = prop_filter(FilterOperator::Contains, single("am_ka")); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam_kapoor")))); assert!(res); } #[test] fn test_contains_not_match() { - let filter = NodeFilter.property("prop").not_contains("shivam"); + let filter = prop_filter(FilterOperator::NotContains, single("shivam")); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam_kapoor")))); assert!(!res); let res = filter.matches(None); @@ -81,9 +125,7 @@ mod test_composite_filters { #[test] fn test_is_in_match() { - let filter = NodeFilter - .property("prop") - .is_in(vec!["shivam".into_prop()]); + let filter = prop_filter(FilterOperator::IsIn, set(["shivam".into_prop()])); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam")))); assert!(res); let res = filter.matches(None); @@ -92,9 +134,7 @@ mod test_composite_filters { #[test] fn test_is_not_in_match() { - let filter = EdgeFilter - .property("prop") - .is_not_in(vec!["shivam".into_prop()]); + let filter = prop_filter(FilterOperator::IsNotIn, set(["shivam".into_prop()])); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam")))); assert!(!res); let res = filter.matches(None); @@ -123,10 +163,7 @@ mod test_property_semantics { use raphtory::{ db::{ api::view::{filter_ops::Filter, StaticGraphViewOps}, - graph::views::filter::model::{ - node_filter::NodeFilter, property_filter::ops::PropertyFilterOps, - PropertyExprFactory, - }, + graph::views::filter::model::{node_filter::NodeFilter, PropertyExprFactory}, }, errors::GraphError, prelude::*, @@ -410,10 +447,7 @@ mod test_property_semantics { db::{ api::view::{filter_ops::Filter, EdgeViewOps, StaticGraphViewOps}, graph::views::filter::{ - model::{ - edge_filter::EdgeFilter, property_filter::ops::PropertyFilterOps, - PropertyExprFactory, - }, + model::{edge_filter::EdgeFilter, PropertyExprFactory}, CreateFilter, }, }, @@ -1503,12 +1537,7 @@ mod test_node_filter { db::{ api::view::{filter_ops::Select, Filter}, graph::views::filter::{ - model::{ - node_filter::ops::NodeFilterOps, - not_filter::NotFilter, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - ComposableFilter, NodeViewFilterOps, PropertyExprFactory, ViewWrapOps, - }, + model::{not_filter::NotFilter, ComposableFilter, NodeViewFilterOps, ViewWrapOps}, CreateFilter, }, }, @@ -2837,11 +2866,7 @@ mod test_node_property_filter { use crate::{init_nodes_graph, IdentityGraphTransformer}; use raphtory::{ db::graph::views::filter::model::{ - graph_filter::GraphFilter, - node_filter::NodeFilter, - not_filter::NotFilter, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - windowed_filter::Windowed, + graph_filter::GraphFilter, node_filter::NodeFilter, windowed_filter::Windowed, ComposableFilter, PropertyExprFactory, ViewWrapOps, }, prelude::{EntityAggOps, EntityExprFilterOps}, @@ -4306,11 +4331,9 @@ mod composite_node_filter_tests { use crate::{init_edges_graph, init_nodes_graph, IdentityGraphTransformer}; use raphtory::{ db::graph::views::filter::model::{ - node_filter::ops::NodeFilterOps, not_filter::NotFilter, - property_filter::ops::PropertyFilterOps, ComposableFilter, NodeFilterFactory, - PropertyExprFactory, + not_filter::NotFilter, ComposableFilter, NodeFilterFactory, PropertyExprFactory, }, - prelude::NodeFilter, + prelude::{EntityExprFilterOps, NodeFilter}, }; use raphtory_tests::assertions::{ assert_filter_neighbours_results, assert_filter_nodes_results, TestVariants, @@ -4575,9 +4598,7 @@ mod test_node_property_filter_agg { db::{ api::view::StaticGraphViewOps, graph::views::filter::model::{ - node_filter::NodeFilter, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - CombinedFilter, PropertyExprFactory, + node_filter::NodeFilter, CombinedFilter, PropertyExprFactory, }, }, prelude::{ @@ -7814,29 +7835,28 @@ mod test_node_property_filter_agg { // ------ Unsupported filter operations ------ #[test] fn test_unsupported_filter_ops_agg() { + // String operators over an aggregated numeric value are rejected. + let expected: &str = "string operator requires a Str property"; + let filter = NodeFilter.property("p_u64s").sum().starts_with("abc"); - let expected: &str = "Operator STARTS_WITH is not supported with list aggregation"; apply_assertion_err(filter, expected); let filter = NodeFilter.property("p_u64s").avg().ends_with("abc"); - let expected: &str = "Operator ENDS_WITH is not supported with list aggregation"; - apply_assertion_err(filter, expected); - - let filter = NodeFilter.property("p_u64s").min().is_none(); - let expected: &str = "Operator IS_NONE is not supported with list aggregation"; - apply_assertion_err(filter, expected); - - let filter = NodeFilter.property("p_u64s").max().is_some(); - let expected: &str = "Operator IS_SOME is not supported with list aggregation"; apply_assertion_err(filter, expected); let filter = NodeFilter.property("p_u64s").len().contains("abc"); - let expected: &str = "Operator CONTAINS is not supported with list aggregation"; apply_assertion_err(filter, expected); let filter = NodeFilter.property("p_u64s").sum().not_contains("abc"); - let expected: &str = "Operator NOT_CONTAINS is not supported with list aggregation"; apply_assertion_err(filter, expected); + + // is_none / is_some after an aggregation are meaningful: the + // aggregate of an absent or empty list is absent. + let filter = NodeFilter.property("p_u64s").min().is_none(); + apply_assertion(filter, &["n6", "n7"]); + + let filter = NodeFilter.property("p_u64s").max().is_some(); + apply_assertion(filter, &["n1", "n10", "n2", "n3", "n4", "n5"]); } // --------------- OVERFLOW --------------- @@ -8017,6 +8037,63 @@ mod test_node_property_filter_agg { let expected = vec!["n4", "n10"]; apply_assertion(filter, &expected); } + + // --------------- OVERFLOW HANDLING --------------- + #[test] + fn test_max_value_agg() { + let filter = NodeFilter + .property("p_u64s_max") + .max() + .eq(Prop::U64(u64::MAX)); + let expected: Vec<&str> = vec!["n5", "n1"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u64s_min") + .min() + .eq(Prop::U64(u64::MIN)); + let expected: Vec<&str> = vec!["n5"]; + apply_assertion(filter, &expected); + + // A constant is validated by value-castability against the list's + // element type, so a sum that only exists past that type's range + // cannot be matched with a wider constant. + let filter = NodeFilter.property("p_u8s_max").sum().eq(Prop::U64(510)); + apply_assertion_err(filter, "cannot be coerced to U8"); + + let filter = NodeFilter + .property("p_u16s_max") + .sum() + .eq(Prop::U64(131070)); + apply_assertion_err(filter, "cannot be coerced to U16"); + + let filter = NodeFilter + .property("p_u32s_max") + .sum() + .eq(Prop::U64(8589934590)); + apply_assertion_err(filter, "cannot be coerced to U32"); + + let filter = NodeFilter.property("p_u64s_max").sum().gt(Prop::U64(0)); + let expected: Vec<&str> = vec!["n1", "n5"]; + apply_assertion(filter, &expected); + + // AVG is computed in f64 even if SUM overflowed. + let avg = (u64::MAX as f64 + 1.0) / 2.0; + let filter = NodeFilter.property("p_u64s_max").avg().eq(avg); + let expected = vec!["n5"]; + apply_assertion(filter, &expected); + + // Overflow is handled by promoting to Decimal which still compares + let filter = NodeFilter.property("p_i64s_max").sum().gt(Prop::I64(0)); + let expected: Vec<&str> = vec!["n5"]; + apply_assertion(filter, &expected); + + // AVG is computed in f64 even if SUM overflowed. + let avg = (i64::MAX as f64 + 1.0) / 2.0; + let filter = NodeFilter.property("p_i64s_max").avg().eq(avg); + let expected = vec!["n5"]; + apply_assertion(filter, &expected); + } } mod test_edge_filter { @@ -8025,8 +8102,8 @@ mod test_edge_filter { init_edges_graph_with_str_ids_del, init_nodes_graph, IdentityGraphTransformer, }; use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, ComposableFilter, EdgeViewFilterOps, EntityExprFilterOps, - NodeFilterFactory, PropertyExprFactory, ViewWrapOps, + edge_filter::EdgeFilter, EdgeViewFilterOps, EntityExprFilterOps, NodeFilterFactory, + PropertyExprFactory, ViewWrapOps, }; use raphtory_tests::assertions::{ assert_filter_edges_results, assert_select_edges_results, TestGraphVariants, TestVariants, @@ -8409,8 +8486,8 @@ mod test_edge_filter { #[test] fn test_filter_edges_for_dst_id_eq() { - let filter = EdgeFilter::dst().id().eq("3"); - let expected_results = vec!["2->3"]; + let _filter = EdgeFilter::dst().id().eq("3"); + let _expected_results = vec!["2->3"]; // assert_filter_edges_results( // init_edges_graph, // IdentityGraphTransformer, @@ -8996,14 +9073,52 @@ mod test_edge_filter { TestVariants::All, ); } + + #[test] + fn test_is_self_loop_edge_window() { + // window has no effect on is_self_loop and because we are using an `EdgeFilter` as the + // entrypoint, the window is only applied to the edges, not the graph + let filter = EdgeFilter.window(1, 3).is_self_loop(); + let expected_results_self_loop = vec!["Bangalore->Bangalore"]; + assert_filter_edges_results( + init_edges_graph_with_str_ids_del, + IdentityGraphTransformer, + filter.clone(), + &expected_results_self_loop, + TestVariants::All, + ); + + // window doesn't make a difference for `is_self_loop` + assert_select_edges_results( + init_edges_graph_with_str_ids_del, + IdentityGraphTransformer, + filter.clone(), + &expected_results_self_loop, + TestVariants::All, + ); + + let filter = EdgeFilter.window(1, 6).is_self_loop(); + assert_filter_edges_results( + init_edges_graph_with_str_ids_del, + IdentityGraphTransformer, + filter.clone(), + &expected_results_self_loop, + TestVariants::All, + ); + assert_select_edges_results( + init_edges_graph_with_str_ids_del, + IdentityGraphTransformer, + filter.clone(), + &expected_results_self_loop, + TestVariants::All, + ); + } } mod test_edge_property_filter { use crate::{init_edges_graph, init_edges_graph2, IdentityGraphTransformer}; use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, - property_filter::ops::{ElemQualifierOps, ListAggOps, PropertyFilterOps}, - ComposableFilter, PropertyExprFactory, ViewWrapOps, + edge_filter::EdgeFilter, ComposableFilter, PropertyExprFactory, ViewWrapOps, }; use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; @@ -10306,10 +10421,12 @@ mod test_edge_property_filter { // TODO: delete when search is dropped and graphql composite path is gone mod composite_edge_filter_tests { - use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, not_filter::NotFilter, - property_filter::ops::PropertyFilterOps, ComposableFilter, NodeFilterFactory, - PropertyExprFactory, + use raphtory::{ + db::graph::views::filter::model::{ + edge_filter::EdgeFilter, not_filter::NotFilter, ComposableFilter, NodeFilterFactory, + PropertyExprFactory, + }, + prelude::EntityExprFilterOps, }; use raphtory_tests::assertions::{ assert_filter_edges_results, TestGraphVariants, TestVariants, diff --git a/raphtory/src/db/api/state/ops/filter.rs b/raphtory/src/db/api/state/ops/filter.rs index 4669a58f72..e003c5b3fd 100644 --- a/raphtory/src/db/api/state/ops/filter.rs +++ b/raphtory/src/db/api/state/ops/filter.rs @@ -1,28 +1,15 @@ use crate::{ db::{ api::{ - state::{ - ops::{Const, Degree, IntoDynNodeOp, NodeOp, TypeId}, - Index, - }, + state::ops::{Const, IntoDynNodeOp, NodeOp, TypeId}, view::internal::{GraphView, NodeList}, }, - graph::{ - create_node_type_filter, - views::filter::model::{ - degree_filter::DegreeFilter, - filter::{FieldFilterValue, Filter}, - node_filter::NodeFilter, - property_filter::PropertyFilterValue, - FilterOperator, - }, - }, + graph::create_node_type_filter, }, - prelude::{GraphViewOps, PropertyFilter}, + prelude::GraphViewOps, }; -use raphtory_api::core::entities::{properties::prop::Prop, VID}; -use raphtory_core::entities::nodes::node_ref::AsNodeRef; -use raphtory_storage::graph::{graph::GraphStorage, nodes::node_storage_ops::NodeStorageOps}; +use raphtory_api::core::entities::VID; +use raphtory_storage::graph::graph::GraphStorage; use std::sync::Arc; #[derive(Clone, Debug)] @@ -83,199 +70,6 @@ impl NodeOp for NodeExistsOp { } } -#[derive(Debug, Clone)] -pub struct NodeIdFilterOp { - filter: Filter, -} - -impl NodeIdFilterOp { - pub(crate) fn new(filter: Filter) -> Self { - Self { filter } - } -} - -impl NodeOp for NodeIdFilterOp { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { - let node = storage.core_node(node); - self.filter.id_matches(node.id()) - } - - fn domain(&self, storage: &GraphStorage) -> NodeList { - let op = &self.filter.operator; - match op { - FilterOperator::Eq => match &self.filter.field_value { - FieldFilterValue::ID(id) => { - let vid = storage.internalise_node(id.as_node_ref()); - NodeList::List { - elems: vid.into_iter().collect(), - } - } - _ => unreachable!(), - }, - FilterOperator::IsIn => match &self.filter.field_value { - FieldFilterValue::IDSet(ids) => NodeList::List { - elems: ids - .iter() - .filter_map(|id| storage.internalise_node(id.as_node_ref())) - .collect(), - }, - _ => unreachable!(), - }, - FilterOperator::IsNone => NodeList::empty(), - _ => NodeList::All, - } - } - - fn const_value(&self) -> Option { - match &self.filter.operator { - FilterOperator::IsSome => Some(true), - _ => None, - } - } - fn const_value_in_domain(&self) -> Option { - match &self.filter.operator { - FilterOperator::Eq - | FilterOperator::IsIn - | FilterOperator::IsNone - | FilterOperator::IsSome => Some(true), - _ => None, - } - } -} - -#[derive(Debug, Clone)] -pub struct NodeNameFilterOp { - filter: Filter, -} - -impl NodeNameFilterOp { - pub(crate) fn new(filter: Filter) -> Self { - Self { filter } - } -} - -impl NodeOp for NodeNameFilterOp { - type Output = bool; - - fn domain(&self, storage: &GraphStorage) -> NodeList { - let op = &self.filter.operator; - match op { - FilterOperator::Eq => match &self.filter.field_value { - FieldFilterValue::Single(name) => { - let vid = storage.internalise_node(name.as_node_ref()); - NodeList::List { - elems: vid.into_iter().collect(), - } - } - _ => unreachable!(), - }, - FilterOperator::IsIn => match &self.filter.field_value { - FieldFilterValue::Set(names) => NodeList::List { - elems: names - .iter() - .filter_map(|name| storage.internalise_node(name.as_node_ref())) - .collect(), - }, - _ => unreachable!(), - }, - FilterOperator::IsNone => NodeList::List { - elems: Index::default(), - }, - _ => NodeList::All, - } - } - - fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { - let node_ref = storage.core_node(node); - self.filter.matches(Some(&node_ref.name())) - } - - fn const_value(&self) -> Option { - match &self.filter.operator { - FilterOperator::IsSome => Some(true), - _ => None, - } - } - fn const_value_in_domain(&self) -> Option { - match &self.filter.operator { - FilterOperator::Eq - | FilterOperator::IsIn - | FilterOperator::IsNone - | FilterOperator::IsSome => Some(true), - _ => None, - } - } -} - -#[derive(Debug, Clone)] -pub struct NodePropertyFilterOp { - graph: G, - prop_id: usize, - filter: PropertyFilter, -} - -impl NodePropertyFilterOp { - pub(crate) fn new(graph: G, prop_id: usize, filter: PropertyFilter) -> Self { - Self { - graph, - prop_id, - filter, - } - } -} - -impl NodeOp for NodePropertyFilterOp { - type Output = bool; - - fn domain(&self, _storage: &GraphStorage) -> NodeList { - self.graph.node_list() - } - - fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { - let node = storage.core_node(node); - self.filter - .matches_node(&self.graph, self.prop_id, node.as_ref()) - } -} - -#[derive(Debug, Clone)] -pub struct NodeDegreeFilterOp { - degree: Degree, - operator: FilterOperator, - value: PropertyFilterValue, -} - -impl NodeDegreeFilterOp { - pub(crate) fn new(graph: G, filter: DegreeFilter) -> Self { - let degree = Degree { - dir: filter.direction, - view: graph, - }; - Self { - degree, - operator: filter.operator, - value: filter.value, - } - } -} - -impl NodeOp for NodeDegreeFilterOp { - type Output = bool; - - fn domain(&self, storage: &GraphStorage) -> NodeList { - self.degree.domain(storage) - } - - fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { - let node_degree = self.degree.apply(storage, node); - let node_degree_prop = Prop::U64(node_degree as u64); - self.operator - .apply_to_property(&self.value, Some(&node_degree_prop)) - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct OrOp { pub(crate) left: L, diff --git a/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs deleted file mode 100644 index e9cdcbbe73..0000000000 --- a/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::{ - core::entities::LayerIds, - db::{ - api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - view::internal::{ - Immutable, InheritEdgeHistoryFilter, InheritEdgeLayerFilterOps, - InheritExplodedEdgeFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, - InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, - InheritTimeSemantics, InternalEdgeFilterOps, Static, - }, - }, - graph::views::filter::model::{edge_filter::EdgeFilter, property_filter::PropertyFilter}, - }, - prelude::GraphViewOps, -}; -use raphtory_api::inherit::Base; -use raphtory_storage::core_ops::InheritCoreGraphOps; -use storage::EdgeEntryRef; - -#[derive(Debug, Clone)] -pub struct EdgePropertyFilteredGraph { - graph: G, - prop_id: usize, - filter: PropertyFilter, -} - -impl EdgePropertyFilteredGraph { - pub(crate) fn new(graph: G, prop_id: usize, filter: PropertyFilter) -> Self { - Self { - graph, - prop_id, - filter, - } - } -} - -impl Base for EdgePropertyFilteredGraph { - type Base = G; - - fn base(&self) -> &Self::Base { - &self.graph - } -} - -impl Static for EdgePropertyFilteredGraph {} -impl Immutable for EdgePropertyFilteredGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritCoreGraphOps for EdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritStorageOps for EdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritLayerOps for EdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritListOps for EdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritMaterialize for EdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritNodeFilterOps for EdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritPropertiesOps for EdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritNodePropertySchemaOps - for EdgePropertyFilteredGraph -{ -} -impl<'graph, G: GraphViewOps<'graph>> InheritEdgePropertySchemaOps - for EdgePropertyFilteredGraph -{ -} -impl<'graph, G: GraphViewOps<'graph>> InheritTimeSemantics for EdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritNodeHistoryFilter for EdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeHistoryFilter for EdgePropertyFilteredGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritExplodedEdgeFilterOps - for EdgePropertyFilteredGraph -{ -} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeLayerFilterOps for EdgePropertyFilteredGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InternalEdgeFilterOps for EdgePropertyFilteredGraph { - #[inline] - fn internal_edge_filtered(&self) -> bool { - true - } - - #[inline] - fn internal_edge_list_trusted(&self) -> bool { - false - } - - #[inline] - fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { - self.graph.internal_filter_edge(edge, layer_ids) - && self.filter.matches_edge(&self.graph, self.prop_id, edge) - } -} diff --git a/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs b/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs index 7fe3768542..e3792cc686 100644 --- a/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs @@ -119,6 +119,12 @@ impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> if !self.graph.internal_filter_exploded_edge(eid, t, layer_ids) { return false; } + // Deletions carry no properties, so they always pass through: filtering + // them out would silently extend the previous addition's interval on a + // persistent graph. + if eid.is_deletion() { + return true; + } let edge_ref: EdgeRef = self.core_edge(Either::Left(eid.eid())).out_ref(); self.filter.apply( self.graph.core_graph(), diff --git a/raphtory/src/db/graph/views/filter/exploded_edge_property_filter.rs b/raphtory/src/db/graph/views/filter/exploded_edge_property_filter.rs deleted file mode 100644 index 597f592c04..0000000000 --- a/raphtory/src/db/graph/views/filter/exploded_edge_property_filter.rs +++ /dev/null @@ -1,133 +0,0 @@ -use crate::{ - core::entities::LayerIds, - db::{ - api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - view::internal::{ - Immutable, InheritEdgeFilterOps, InheritEdgeHistoryFilter, - InheritEdgeLayerFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, - InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, - InheritTimeSemantics, InternalExplodedEdgeFilterOps, Static, - }, - }, - graph::views::filter::model::{ - exploded_edge_filter::ExplodedEdgeFilter, property_filter::PropertyFilter, - }, - }, - prelude::GraphViewOps, -}; -use raphtory_api::{ - core::{entities::ELID, storage::timeindex::EventTime}, - inherit::Base, -}; -use raphtory_storage::core_ops::InheritCoreGraphOps; - -#[derive(Debug, Clone)] -pub struct ExplodedEdgePropertyFilteredGraph { - graph: G, - prop_id: usize, - filter: PropertyFilter, -} - -impl Static for ExplodedEdgePropertyFilteredGraph {} -impl Immutable for ExplodedEdgePropertyFilteredGraph {} - -impl<'graph, G: GraphViewOps<'graph>> ExplodedEdgePropertyFilteredGraph { - pub(crate) fn new( - graph: G, - prop_id: usize, - filter: PropertyFilter, - ) -> Self { - Self { - graph, - prop_id, - filter, - } - } -} - -impl Base for ExplodedEdgePropertyFilteredGraph { - type Base = G; - - fn base(&self) -> &Self::Base { - &self.graph - } -} - -impl<'graph, G: GraphViewOps<'graph>> InheritCoreGraphOps for ExplodedEdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritNodeHistoryFilter - for ExplodedEdgePropertyFilteredGraph -{ -} -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeHistoryFilter - for ExplodedEdgePropertyFilteredGraph -{ -} - -impl<'graph, G: GraphViewOps<'graph>> InheritStorageOps for ExplodedEdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritLayerOps for ExplodedEdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritListOps for ExplodedEdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritMaterialize for ExplodedEdgePropertyFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritPropertiesOps - for ExplodedEdgePropertyFilteredGraph -{ -} - -impl<'graph, G: GraphViewOps<'graph>> InheritNodePropertySchemaOps - for ExplodedEdgePropertyFilteredGraph -{ -} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgePropertySchemaOps - for ExplodedEdgePropertyFilteredGraph -{ -} - -impl<'graph, G: GraphViewOps<'graph>> InheritNodeFilterOps - for ExplodedEdgePropertyFilteredGraph -{ -} -impl<'graph, G: GraphViewOps<'graph>> InheritTimeSemantics - for ExplodedEdgePropertyFilteredGraph -{ -} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeFilterOps - for ExplodedEdgePropertyFilteredGraph -{ -} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeLayerFilterOps - for ExplodedEdgePropertyFilteredGraph -{ -} - -impl<'graph, G: GraphViewOps<'graph>> InternalExplodedEdgeFilterOps - for ExplodedEdgePropertyFilteredGraph -{ - fn internal_exploded_edge_filtered(&self) -> bool { - true - } - - fn internal_exploded_filter_edge_list_trusted(&self) -> bool { - false - } - - fn internal_filter_exploded_edge(&self, eid: ELID, t: EventTime, layer_ids: &LayerIds) -> bool { - self.graph.internal_filter_exploded_edge(eid, t, layer_ids) && { - if eid.is_deletion() { - true - } else { - self.filter.matches_exploded_edge( - &self.graph, - self.prop_id, - eid.eid(), - t, - eid.layer(), - ) - } - } - } -} diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 0b9e74eeb4..83cf3f116e 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -14,11 +14,9 @@ pub mod and_filtered_graph; pub mod edge_expr_filtered_graph; pub mod edge_filtered_graph; pub mod edge_node_filtered_graph; -pub mod edge_property_filtered_graph; mod exploded_edge_expr_filtered_graph; pub mod exploded_edge_filtered_graph; pub mod exploded_edge_node_filtered_graph; -pub mod exploded_edge_property_filter; pub mod model; pub mod node_filtered_graph; pub mod not_filtered_graph; diff --git a/raphtory/src/db/graph/views/filter/model/and_filter.rs b/raphtory/src/db/graph/views/filter/model/and_filter.rs index 3b6a070faf..a685fd1ced 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -5,13 +5,7 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - and_filtered_graph::AndFilteredGraph, - model::{ - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, ComposableFilter, FilterTree, - }, - CreateFilter, + and_filtered_graph::AndFilteredGraph, model::ComposableFilter, CreateFilter, }, }, errors::GraphError, diff --git a/raphtory/src/db/graph/views/filter/model/degree_filter.rs b/raphtory/src/db/graph/views/filter/model/degree_filter.rs index a8bd34684f..fe8af4c496 100644 --- a/raphtory/src/db/graph/views/filter/model/degree_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/degree_filter.rs @@ -1,42 +1,9 @@ -use crate::{ - db::{ - api::state::ops::{filter::NodeDegreeFilterOp, GraphView}, - graph::views::filter::{ - model, - model::{ - property_filter::{ - builders::PropertyExprBuilderInput, Op, PropertyFilter, PropertyFilterInput, - PropertyFilterValue, PropertyRef, - }, - CombinedFilter, ComposableFilter, CompositeNodeFilter, FilterOperator, - InternalPropertyFilterBuilder, NodeFilter, - }, - node_filtered_graph::NodeFilteredGraph, - CreateFilter, - }, - }, - errors::GraphError, +use crate::db::graph::views::filter::model::{ + property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, + ComposableFilter, FilterOperator, NodeFilter, }; -use raphtory_api::core::{ - entities::properties::prop::{Prop, PropType}, - Direction, -}; -use std::{collections::HashSet, fmt, fmt::Display, sync::Arc}; - -#[derive(Clone)] -pub struct DegreeFilterBuilder { - direction: Direction, - ops: Vec, -} - -impl DegreeFilterBuilder { - pub fn new(direction: Direction) -> Self { - Self { - direction, - ops: Vec::new(), - } - } -} +use raphtory_api::core::Direction; +use std::{fmt, fmt::Display}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct DegreeFilter { @@ -46,98 +13,6 @@ pub struct DegreeFilter { pub ops: Vec, } -impl CreateFilter for DegreeFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph>; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeDegreeFilterOp; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone(), filtered)?; - Ok(NodeFilteredGraph::new(graph, filter)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - if !self.ops.is_empty() { - return Err(GraphError::InvalidFilter( - "degree filter does not support expressions".to_string(), - )); - } - match self.operator { - FilterOperator::Eq - | FilterOperator::Ne - | FilterOperator::Gt - | FilterOperator::Ge - | FilterOperator::Lt - | FilterOperator::Le - | FilterOperator::IsIn - | FilterOperator::IsNotIn => {} - _ => { - return Err(GraphError::InvalidFilter(format!( - "degree filter does not support operator {:?}", - self.operator - ))); - } - } - let value = match self.value { - PropertyFilterValue::Single(ref prop_val) => { - let casted_val = prop_val.clone().cast(PropType::U64).ok_or_else(|| { - GraphError::InvalidFilter(format!( - "degree filter expects an integer value, got {}", - prop_val - )) - })?; - - PropertyFilterValue::Single(casted_val) - } - PropertyFilterValue::Set(ref prop_vals) => { - let casted_set = prop_vals - .iter() - .map(|val| { - val.clone().cast(PropType::U64).ok_or_else(|| { - GraphError::InvalidFilter(format!( - "degree filter expects an integer value, got {}", - val - )) - }) - }) - .collect::, GraphError>>()?; - - PropertyFilterValue::Set(Arc::new(casted_set)) - } - PropertyFilterValue::None => { - return Err(GraphError::InvalidFilter( - "degree filter requires a value".to_string(), - )); - } - }; - let mut filter = self.clone(); - filter.value = value; - Ok(NodeDegreeFilterOp::new(filtered, filter)) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - fn property_ref(direction: &Direction) -> PropertyRef { match direction { Direction::IN => PropertyRef::Property("in_degree".to_string()), @@ -146,50 +21,8 @@ fn property_ref(direction: &Direction) -> PropertyRef { } } -impl InternalPropertyFilterBuilder for DegreeFilterBuilder -where - DegreeFilter: CombinedFilter, -{ - type Filter = DegreeFilter; - type ExprBuilder = DegreeFilterBuilder; - type Marker = NodeFilter; - - fn property_ref(&self) -> PropertyRef { - property_ref(&self.direction) - } - - fn ops(&self) -> &[Op] { - &self.ops - } - - fn entity(&self) -> Self::Marker { - NodeFilter - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - DegreeFilter { - value: filter.prop_value, - direction: self.direction, - operator: filter.operator, - ops: filter.ops, - } - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - let mut filter = self.clone(); - filter.ops = builder.ops; - filter - } -} - impl ComposableFilter for DegreeFilter {} -pub trait DegreeFilterFactory { - fn in_degree(&self) -> DegreeFilterBuilder; - fn out_degree(&self) -> DegreeFilterBuilder; - fn degree(&self) -> DegreeFilterBuilder; -} - impl Display for DegreeFilter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let property_filter = PropertyFilter { 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 44004cbfbe..8b83ad6f5c 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -2,17 +2,13 @@ use crate::{ db::{ api::{ state::ops::{NodeOp, NotANodeFilter}, - view::{ - internal::{DynGraphArc, GraphView}, - BoxableGraphView, - }, + view::internal::GraphView, }, graph::views::filter::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, edge_node_filtered_graph::EdgeNodeFilteredGraph, model::{ edge_expr::{ops::EdgeEndpointNodeOp, EdgeOp}, - exploded_edge_filter::CompositeExplodedEdgeFilter, is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, @@ -20,22 +16,12 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_expr::{CreateOp, EntityExpr, EntityExprBuilder}, - node_filter::{ - builders::{InternalNodeFilterBuilder, InternalNodeIdFilterBuilder}, - CompositeNodeFilter, NodeFilter, - }, - property_filter::{ - builders::{ - MetadataFilterBuilder, PropertyExprBuilderInput, PropertyFilterBuilder, - }, - Op, PropertyFilter, PropertyFilterInput, PropertyRef, - }, + node_filter::{CompositeNodeFilter, NodeFilter}, + property_filter::PropertyFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - AndFilter, CombinedFilter, ComposableFilter, DynFilter, EdgeViewFilterOps, - EntityMarker, FilterTree, InternalPropertyFilterBuilder, - InternalPropertyFilterFactory, InternalViewWrapOps, NotFilter, OrFilter, - TemporalPropertyFilterFactory, Wrap, + CombinedFilter, ComposableFilter, DynFilter, EdgeViewFilterOps, EntityMarker, + InternalViewWrapOps, Wrap, }, CreateFilter, }, @@ -87,24 +73,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, InternalPropertyFilterFactory::entity(self)) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - MetadataFilterBuilder(property, InternalPropertyFilterFactory::entity(self)) - } -} - impl EdgeViewFilterOps for EdgeFilter { type Output = T; @@ -131,9 +99,9 @@ pub enum Endpoint { Dst, } -// Generic wrapper that pairs node-side builders with a concrete endpoint. -// The objective is to carry the endpoint through builder chain without having to change node builders -// and at the end convert into a composite node filter via TryAsCompositeFilter +// Generic wrapper that pairs a node-side expression with a concrete endpoint, +// carrying the endpoint through the chain so the compiled node op can be +// applied to the edge's src or dst node. #[derive(Debug, Clone)] pub struct EdgeEndpointWrapper { pub(crate) inner: T, @@ -162,9 +130,8 @@ impl EdgeEndpointWrapper { } impl EdgeEndpointWrapper { - /// Endpoint fields and properties are expressions: they compose with the comparison, - /// string, set and temporal operators. Nothing outside the expression tests consumed - /// the builder-returning forms these replace. + /// Endpoint fields and properties are expressions: they compose with the + /// comparison, string, set and temporal operators. #[inline] pub fn id(&self) -> EdgeEndpointWrapper { self.wrap(Id) @@ -210,68 +177,6 @@ impl Wrap for EdgeEndpointWrapper { impl ComposableFilter for EdgeEndpointWrapper where T: Clone + Send + Sync {} -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 { - self.inner.field_name() - } -} - -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, F> = EdgeNodeFilteredGraph> 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 6881682314..edb5e1c4ef 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 @@ -1,37 +1,21 @@ use crate::{ db::{ - api::{ - state::ops::NotANodeFilter, - view::{ - internal::{DynGraphArc, GraphView}, - BoxableGraphView, - }, - }, + api::{state::ops::NotANodeFilter, view::internal::GraphView}, graph::views::filter::{ exploded_edge_node_filtered_graph::ExplodedEdgeNodeFilteredGraph, model::{ - edge_filter::{CompositeEdgeFilter, Endpoint}, + edge_filter::Endpoint, is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, latest_filter::Latest, layered_filter::Layered, - node_filter::{ - builders::{InternalNodeFilterBuilder, InternalNodeIdFilterBuilder}, - CompositeNodeFilter, NodeFilter, - }, - property_filter::{ - builders::{ - MetadataFilterBuilder, PropertyExprBuilderInput, PropertyFilterBuilder, - }, - Op, PropertyFilter, PropertyFilterInput, PropertyRef, - }, + node_filter::{CompositeNodeFilter, NodeFilter}, + property_filter::PropertyFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - AndFilter, CombinedFilter, EdgeViewFilterOps, EntityMarker, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, - NotFilter, OrFilter, TemporalPropertyFilterFactory, Wrap, + CombinedFilter, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, Wrap, }, CreateFilter, }, @@ -39,7 +23,7 @@ use crate::{ errors::GraphError, }; use raphtory_api::core::storage::timeindex::EventTime; -use std::{fmt, fmt::Display, sync::Arc}; +use std::{fmt, fmt::Display}; #[derive(Clone, Debug, Copy, Default, PartialEq, Eq)] pub struct ExplodedEdgeFilter; @@ -78,24 +62,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; @@ -154,78 +120,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; - - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -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: GraphView + 'graph, F: GraphView + 'graph> = ExplodedEdgeNodeFilteredGraph> 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 1fa12c1f9a..cdb9d10b95 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -3,13 +3,11 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::filter::{ model::{ - edge_filter::CompositeEdgeFilter, latest_filter::Latest, layered_filter::Layered, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - CombinedFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, FilterTree, - InternalViewWrapOps, Wrap, + CombinedFilter, InternalViewWrapOps, Wrap, }, CreateFilter, }, 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 5a2867d5df..47b33a2023 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 @@ -3,12 +3,7 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - edge_filtered_graph::EdgeFilteredGraph, - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, - }, - CreateFilter, + edge_filtered_graph::EdgeFilteredGraph, model::ComposableFilter, CreateFilter, }, is_active_graph::IsActiveGraph, }, 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 63d869954d..b23676417d 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 @@ -2,12 +2,7 @@ use crate::{ db::{ api::state::ops::{GraphView, HistoryOp, Map, NodeOp}, graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, - }, - node_filtered_graph::NodeFilteredGraph, - CreateFilter, + model::ComposableFilter, node_filtered_graph::NodeFilteredGraph, CreateFilter, }, }, errors::GraphError, 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 b37373f10d..4a6f4b71c9 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 @@ -3,12 +3,7 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - edge_filtered_graph::EdgeFilteredGraph, - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, - }, - CreateFilter, + edge_filtered_graph::EdgeFilteredGraph, model::ComposableFilter, CreateFilter, }, is_deleted_graph::IsDeletedGraph, }, 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 e40e86eb95..4bc4c7469b 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 @@ -2,13 +2,7 @@ use crate::{ db::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ - filter::{ - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, - }, - CreateFilter, - }, + filter::{model::ComposableFilter, CreateFilter}, is_self_loop_graph::IsSelfLoopGraph, }, }, 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 64d421b273..766bbbb6ac 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 @@ -3,12 +3,7 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::{ filter::{ - edge_filtered_graph::EdgeFilteredGraph, - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, - }, - CreateFilter, + edge_filtered_graph::EdgeFilteredGraph, model::ComposableFilter, CreateFilter, }, valid_graph::ValidGraph, }, 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 f952fafda7..a8232f46b6 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -4,24 +4,12 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_expr::EdgeOp, - 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_expr::CreateOp, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateView, EdgeViewFilterOps, FilterTree, GraphViewOp, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, - InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, Wrap, + edge_expr::EdgeOp, is_active_edge_filter::IsActiveEdge, + is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, + node_expr::CreateOp, windowed_filter::Windowed, CombinedFilter, + ComposableFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, + NodeViewFilterOps, Wrap, }, CreateFilter, }, @@ -60,45 +48,6 @@ impl InternalViewWrapOps for Latest { } } -impl InternalNodeFilterBuilder for Latest { - type FilterType = T::FilterType; - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalNodeIdFilterBuilder for Latest { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalPropertyFilterBuilder for Latest { - type Filter = Latest; - type ExprBuilder = Latest; - type Marker = T::Marker; - - fn property_ref(&self) -> PropertyRef { - self.inner.property_ref() - } - - fn ops(&self) -> &[Op] { - self.inner.ops() - } - - fn entity(&self) -> Self::Marker { - self.inner.entity() - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - impl CreateFilter for Latest { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> @@ -159,26 +108,6 @@ impl Wrap for Latest { } } -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>; @@ -222,7 +151,7 @@ impl CreateView for Latest { } // ── expr layer: the latest view scopes any inner expression (per-expression view) ── -// Nesting order of chained views is pinned by the Phase-3 semantics tests. +// Nesting order of chained views is pinned by the view-semantics tests. impl CreateOp for Latest { fn create_node_op<'g, G: GraphView + 'g>( 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 4b5c2d9515..7fdbf25832 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -4,23 +4,11 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_expr::EdgeOp, - 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_expr::CreateOp, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, FilterTree, GraphViewOp, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, - InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, Wrap, + edge_expr::EdgeOp, is_active_edge_filter::IsActiveEdge, + is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, + node_expr::CreateOp, CombinedFilter, ComposableFilter, EdgeViewFilterOps, + InternalViewWrapOps, NodeViewFilterOps, Wrap, }, CreateFilter, }, @@ -75,46 +63,6 @@ impl InternalViewWrapOps for Layered { } } -impl InternalNodeFilterBuilder for Layered { - type FilterType = T::FilterType; - - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalNodeIdFilterBuilder for Layered { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalPropertyFilterBuilder for Layered { - type Filter = Layered; - type ExprBuilder = Layered; - type Marker = T::Marker; - - fn property_ref(&self) -> PropertyRef { - self.inner.property_ref() - } - - fn ops(&self) -> &[Op] { - self.inner.ops() - } - - fn entity(&self) -> Self::Marker { - self.inner.entity() - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - impl CreateFilter for Layered { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> @@ -178,26 +126,6 @@ impl Wrap for Layered { } } -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>; @@ -227,7 +155,7 @@ impl EdgeViewFilterOps for Layered { } // ── expr layer: the layer view scopes any inner expression (per-expression view) ── -// Nesting order of chained views is pinned by the Phase-3 semantics tests. +// Nesting order of chained views is pinned by the view-semantics tests. impl CreateOp for Layered { fn create_node_op<'g, G: GraphView + 'g>( diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index cc952d911b..605653e21f 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -48,10 +48,6 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_expr::{NodeMetaOp, NodePropOp}, - property_filter::{ - builders::{PropertyExprBuilder, PropertyExprBuilderInput}, - Op, PropertyFilterInput, PropertyRef, - }, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, }, @@ -315,40 +311,6 @@ impl EntityExpr for PropertyExpr { } } -pub trait PropertyFilterFactory: InternalPropertyFilterFactory { - fn property(&self, name: impl Into) -> Self::PropertyBuilder { - self.property_builder(name.into()) - } - - fn metadata(&self, name: impl Into) -> Self::MetadataBuilder { - self.metadata_builder(name.into()) - } -} - -impl PropertyFilterFactory for T {} - -pub trait TemporalPropertyFilterFactory: InternalPropertyFilterBuilder { - fn temporal(&self) -> Self::ExprBuilder { - let builder = PropertyExprBuilderInput { - prop_ref: PropertyRef::TemporalProperty(self.property_ref().name().to_string()), - ops: vec![], - }; - self.with_expr_builder(builder) - } -} - -pub trait DynTemporalPropertyFilterBuilder: DynPropertyFilterBuilder { - fn dyn_temporal(&self) -> Arc; -} - -impl DynTemporalPropertyFilterBuilder for T { - fn dyn_temporal(&self) -> Arc { - Arc::new(self.temporal()) - } -} - -impl TemporalPropertyFilterFactory for Arc {} - /// One graph-level view restriction, as data. `at`/`before`/`after` lower to /// `Window` at construction time (see `ViewWrapOps`), so they need no /// variants here. @@ -463,7 +425,6 @@ impl CreateOp for Me } /// Entry point of the expr API: selects a property or metadata column on any view expression. -/// Distinct from [`PropertyFilterFactory`], the builder-path factory it will eventually replace. pub trait PropertyExprFactory: CreateView + EntityExpr + Sized { fn property(&self, name: impl Into) -> PropertyExpr; @@ -486,14 +447,6 @@ impl PropertyExprFactory for T { } } -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; -} - pub trait DynPropertyExprFactory { fn dyn_property(&self, name: String) -> Arc; } @@ -504,74 +457,63 @@ impl DynPropertyExprFactory 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()) +impl PropertyExpr { + pub fn temporal(&self) -> TemporalPropExpr { + TemporalPropExpr { + view_expr: self.view_expr.clone(), + name: self.name.clone(), + } } +} - fn ops(&self) -> &[Op] { - &[] +/// Aggregators apply to the latest value of a property when it is list-valued; +/// scalar values are rejected at filter-build time (`require_aggregable`). +impl EntityAggOps for PropertyExpr { + fn sum(self) -> SumExpr { + SumExpr(self) } - - fn entity(&self) -> Self::Marker { - self.view_expr.clone() + fn avg(self) -> AvgExpr { + AvgExpr(self) } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - filter.with_entity(self.entity()) + fn min(self) -> MinExpr { + MinExpr(self) } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - builder.with_entity(self.entity()) + 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 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()) +/// As for [`PropertyExpr`]: aggregation over a list-valued metadata field. +impl EntityAggOps for MetadataExpr { + fn sum(self) -> SumExpr { + SumExpr(self) } - - fn ops(&self) -> &[Op] { - &[] + fn avg(self) -> AvgExpr { + AvgExpr(self) } - - fn entity(&self) -> Self::Marker { - self.view_expr.clone() + fn min(self) -> MinExpr { + MinExpr(self) } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - filter.with_entity(self.entity()) + fn max(self) -> MaxExpr { + MaxExpr(self) } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - builder.with_entity(self.entity()) + fn first(self) -> FirstExpr { + FirstExpr(self) } -} - -impl PropertyExpr { - pub fn temporal(&self) -> TemporalPropExpr { - TemporalPropExpr { - view_expr: self.view_expr.clone(), - name: self.name.clone(), - } + fn last(self) -> LastExpr { + LastExpr(self) + } + fn len(self) -> LenExpr { + LenExpr(self) } } @@ -829,14 +771,6 @@ impl CreateView for Layered { } } -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 {} - pub trait DynViewFilter: DynCreateFilter + Send + Sync + 'static { fn dyn_bounds(&self) -> (EventTime, EventTime); @@ -890,56 +824,6 @@ pub trait EdgeViewFilterOps: ViewWrapOps { fn is_self_loop(&self) -> Self::Output; } -pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps { - fn dyn_is_active(&self) -> Arc; - - fn dyn_is_valid(&self) -> Arc; - - fn dyn_is_deleted(&self) -> Arc; - - fn dyn_is_self_loop(&self) -> Arc; -} - -impl DynEdgeViewFilterOps for T { - fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) - } - - fn dyn_is_valid(&self) -> Arc { - Arc::new(self.is_valid()) - } - - fn dyn_is_deleted(&self) -> Arc { - Arc::new(self.is_deleted()) - } - - fn dyn_is_self_loop(&self) -> Arc { - Arc::new(self.is_self_loop()) - } -} - -pub type DynEdgeViewProps = Arc; - -impl EdgeViewFilterOps for DynEdgeViewProps { - type Output = Arc; - - fn is_active(&self) -> Self::Output { - self.deref().dyn_is_active() - } - - fn is_valid(&self) -> Self::Output { - self.deref().dyn_is_valid() - } - - fn is_deleted(&self) -> Self::Output { - self.deref().dyn_is_deleted() - } - - fn is_self_loop(&self) -> Self::Output { - self.deref().dyn_is_self_loop() - } -} - // ───────────────────────────────────────────────────────────────────────────── // EntityExprFilterOps — comparison and set operators on any EntityExpr // ───────────────────────────────────────────────────────────────────────────── @@ -1326,51 +1210,6 @@ pub fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result + 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 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; -} - -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 CombinedFilter: CreateFilter + Clone + Send + Sync + 'static {} pub trait NodeViewFilterOps: ViewWrapOps { @@ -1379,199 +1218,4 @@ pub trait NodeViewFilterOps: ViewWrapOps { fn is_active(&self) -> Self::Output; } -pub trait DynNodeViewFilterOps: DynInternalViewWrapPropOps { - fn dyn_is_active(&self) -> 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) - } -} - -impl InternalPropertyFilterFactory for Arc { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; - - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) - } -} - impl CombinedFilter for T {} - -impl InternalPropertyFilterFactory for Arc { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; - - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) - } -} - -impl DynNodeViewFilterOps for T { - fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) - } -} - -pub type DynNodeViewProps = Arc; - -impl NodeViewFilterOps for DynNodeViewProps { - type Output = Arc; - - fn is_active(&self) -> Self::Output { - self.deref().dyn_is_active() - } -} - -impl InternalPropertyFilterFactory for DynNodeViewProps { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; - - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() - } - - 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) - } -} - -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/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 4e07cce494..5e3800e292 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -7,8 +7,8 @@ //! - extends the chain by producing another `Arc` //! (selectors / aggregators / quantifiers). //! -//! Used by the Python `PyPropertyExprBuilder` and `PyPropertyFilterBuilder` -//! wrappers to dispatch chain calls at runtime through typed expressions. +//! Used by the Python `PyExpr` and `PyPropertyExpr` wrappers to dispatch +//! chain calls at runtime through typed expressions. //! //! ## Chain methods are currently panic stubs //! 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 bc606c7f78..163731cafb 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 @@ -886,11 +886,7 @@ use crate::db::graph::views::filter::{ exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, model::{ edge_expr::filters::qualify_edge_filter, - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, node_expr::exprs::{AllExpr, AnyExpr}, - node_filter::CompositeNodeFilter, - FilterTree, }, }; 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 deleted file mode 100644 index 22feaddc7f..0000000000 --- a/raphtory/src/db/graph/views/filter/model/node_filter/builders.rs +++ /dev/null @@ -1,123 +0,0 @@ -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}; - -// 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; -} - -impl InternalNodeFilterBuilder for Arc { - type FilterType = T::FilterType; - - fn field_name(&self) -> &'static str { - self.deref().field_name() - } -} - -impl Wrap for Name { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - -impl InternalNodeFilterBuilder for Name { - type FilterType = NodeNameFilter; - - fn field_name(&self) -> &'static str { - "node_name" - } -} - -impl Wrap for Type { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - -impl InternalNodeFilterBuilder for Type { - type FilterType = NodeTypeFilter; - - fn field_name(&self) -> &'static str { - "node_type" - } -} - -// ── restored from db_v4 (lost in a silent auto-merge) ── - -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() - } -} - -#[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; - -impl Wrap for NodeNameFilterBuilder { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - -impl InternalNodeFilterBuilder for NodeNameFilterBuilder { - type FilterType = NodeNameFilter; - - fn field_name(&self) -> &'static str { - "node_name" - } -} - -#[derive(Clone, Debug)] -pub struct NodeTypeFilterBuilder; - -impl Wrap for NodeTypeFilterBuilder { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - -impl InternalNodeFilterBuilder for NodeTypeFilterBuilder { - type FilterType = NodeTypeFilter; - fn field_name(&self) -> &'static str { - "node_type" - } -} diff --git a/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs b/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs index d78e3f1d40..e90cb95719 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 @@ -1,54 +1,28 @@ use crate::{ api::core::Direction, db::{ - api::{ - state::{ - ops::{ - filter::{ - AndOp, MaskOp, NodeIdFilterOp, NodeNameFilterOp, NodeTypeFilterOp, NotOp, - OrOp, - }, - node::{Id, Name, Type}, - NodeOp, TypeId, - }, - NodeStateValue, TypedNodeState, - }, - view::{internal::GraphView, BoxableGraphView}, + api::state::{ + ops::node::{Id, Name, Type}, + NodeStateValue, TypedNodeState, }, - graph::views::filter::{ - model::{ - degree_filter::{DegreeFilter, DegreeFilterBuilder, DegreeFilterFactory}, - edge_filter::CompositeEdgeFilter, - filter::Filter, - is_active_node_filter::IsActiveNode, - latest_filter::Latest, - layered_filter::Layered, - node_expr::{exprs::DegreeExpr, EntityExpr}, - node_filter::{ - builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, - validate::validate, - }, - node_state_filter::NodeStateBoolColOp, - property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, - snapshot_filter::{SnapshotAt, SnapshotLatest}, - windowed_filter::Windowed, - AndFilter, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CreateView, EntityMarker, InternalPropertyFilterFactory, InternalViewWrapOps, - NodeViewFilterOps, NotFilter, OrFilter, Wrap, - }, - node_filtered_graph::NodeFilteredGraph, - CreateFilter, + graph::views::filter::model::{ + degree_filter::DegreeFilter, + filter::Filter, + is_active_node_filter::IsActiveNode, + latest_filter::Latest, + layered_filter::Layered, + node_expr::{exprs::DegreeExpr, EntityExpr}, + node_state_filter::NodeStateBoolColOp, + snapshot_filter::{SnapshotAt, SnapshotLatest}, + windowed_filter::Windowed, + CombinedFilter, CreateView, EntityMarker, InternalViewWrapOps, NodeViewFilterOps, }, }, errors::GraphError, prelude::PropertyFilter, }; use raphtory_api::core::storage::timeindex::EventTime; -use std::{fmt, fmt::Display, sync::Arc}; - -pub mod builders; -pub mod ops; -mod validate; +use std::{fmt, fmt::Display}; #[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] pub struct NodeFilter; @@ -59,43 +33,6 @@ impl From for EntityMarker { } } -impl NodeFilter { - #[inline] - pub fn id() -> NodeIdFilterBuilder { - NodeIdFilterBuilder - } - - #[inline] - pub fn name() -> NodeNameFilterBuilder { - NodeNameFilterBuilder - } - - #[inline] - pub fn node_type() -> NodeTypeFilterBuilder { - NodeTypeFilterBuilder - } - - /// Build a filter from a boolean column inside a TypedNodeState. - pub fn by_column<'graph, V, G, T>( - state: &TypedNodeState<'graph, V, G, T>, - col: &str, - ) -> Result - where - V: NodeStateValue + 'graph, - T: Clone + Send + Sync + 'graph, - { - state.bool_col_filter(col) - } -} - -impl Wrap for NodeFilter { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - impl InternalViewWrapOps for NodeFilter { type Window = Windowed; @@ -104,38 +41,6 @@ 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, InternalPropertyFilterFactory::entity(self)) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - MetadataFilterBuilder(property, InternalPropertyFilterFactory::entity(self)) - } -} - -impl DegreeFilterFactory for NodeFilter { - fn degree(&self) -> DegreeFilterBuilder { - DegreeFilterBuilder::new(Direction::BOTH) - } - - fn in_degree(&self) -> DegreeFilterBuilder { - DegreeFilterBuilder::new(Direction::IN) - } - - fn out_degree(&self) -> DegreeFilterBuilder { - DegreeFilterBuilder::new(Direction::OUT) - } -} - impl NodeViewFilterOps for NodeFilter { type Output = T; @@ -144,184 +49,6 @@ impl NodeViewFilterOps for NodeFilter { } } -#[derive(Debug, Clone)] -pub struct NodeIdFilter(pub Filter); - -impl Display for NodeIdFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for NodeIdFilter { - fn from(filter: Filter) -> Self { - NodeIdFilter(filter) - } -} - -impl ComposableFilter for NodeIdFilter {} - -impl CreateFilter for NodeIdFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeIdFilterOp; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - _filtered: F, - ) -> Result, GraphError> { - validate(graph.id_type(), &self.0)?; - Ok(NodeFilteredGraph::new(graph, NodeIdFilterOp::new(self.0))) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - _filtered: F, - ) -> Result, GraphError> { - 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) - } -} - -#[derive(Debug, Clone)] -pub struct NodeNameFilter(pub Filter); - -impl Display for NodeNameFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for NodeNameFilter { - fn from(filter: Filter) -> Self { - NodeNameFilter(filter) - } -} - -impl ComposableFilter for NodeNameFilter {} - -impl CreateFilter for NodeNameFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeNameFilterOp; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - _filtered: F, - ) -> Result, GraphError> { - Ok(NodeFilteredGraph::new(graph, NodeNameFilterOp::new(self.0))) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Ok(NodeNameFilterOp::new(self.0)) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -#[derive(Debug, Clone)] -pub struct NodeTypeFilter(pub Filter); - -impl Display for NodeTypeFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for NodeTypeFilter { - fn from(filter: Filter) -> Self { - NodeTypeFilter(filter) - } -} - -impl ComposableFilter for NodeTypeFilter {} - -impl CreateFilter for NodeTypeFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeTypeFilterOp; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - _filtered: F, - ) -> Result, GraphError> { - let node_types_filter = graph - .node_meta() - .node_type_meta() - .keys() - .iter() - .map(|k| self.0.matches(Some(k))) // TODO: _default check - .collect::>(); - Ok(NodeFilteredGraph::new( - graph, - TypeId.mask(node_types_filter.into()), - )) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - _filtered: F, - ) -> Result, GraphError> { - let node_types_filter = graph - .node_meta() - .node_type_meta() - .keys() - .iter() - .map(|k| self.0.matches(Some(k))) // TODO: _default check - .collect::>(); - Ok(TypeId.mask(node_types_filter.into())) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum CompositeNodeFilter { Node(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 deleted file mode 100644 index d6c3c14037..0000000000 --- a/raphtory/src/db/graph/views/filter/model/node_filter/ops.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::db::graph::views::filter::model::{ - filter::Filter, - node_filter::{ - builders::{InternalNodeFilterBuilder, InternalNodeIdFilterBuilder}, - NodeIdFilter, - }, -}; -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); - 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); - 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); - self.wrap(NodeIdFilter(filter)) - } - - fn lt>(&self, value: V) -> Self::Wrapped { - let filter = Filter::lt(self.field_name(), value); - self.wrap(NodeIdFilter(filter)) - } - - fn le>(&self, value: V) -> Self::Wrapped { - let filter = Filter::le(self.field_name(), value); - self.wrap(NodeIdFilter(filter)) - } - - fn gt>(&self, value: V) -> Self::Wrapped { - let filter = Filter::gt(self.field_name(), value); - self.wrap(NodeIdFilter(filter)) - } - - fn ge>(&self, value: V) -> Self::Wrapped { - let filter = Filter::ge(self.field_name(), value); - self.wrap(NodeIdFilter(filter)) - } - - fn starts_with>(&self, s: S) -> Self::Wrapped { - let filter = Filter::starts_with(self.field_name(), s.into()); - self.wrap(NodeIdFilter(filter)) - } - - fn ends_with>(&self, s: S) -> Self::Wrapped { - let filter = Filter::ends_with(self.field_name(), s.into()); - self.wrap(NodeIdFilter(filter)) - } - - fn contains>(&self, s: S) -> Self::Wrapped { - let filter = Filter::contains(self.field_name(), s.into()); - self.wrap(NodeIdFilter(filter)) - } - - fn not_contains>(&self, s: S) -> Self::Wrapped { - let filter = Filter::not_contains(self.field_name(), s.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); - self.wrap(NodeIdFilter(filter)) - } -} - -impl NodeIdFilterOps for T {} - -pub trait NodeFilterOps: InternalNodeFilterBuilder { - fn eq(&self, value: impl Into) -> Self::Wrapped { - let filter = Filter::eq(self.field_name(), value); - self.wrap(filter.into()) - } - - fn ne(&self, value: impl Into) -> Self::Wrapped { - let filter = Filter::ne(self.field_name(), value); - self.wrap(filter.into()) - } - - fn is_in( - &self, - values: impl IntoIterator>, - ) -> Self::Wrapped { - let filter = Filter::is_in(self.field_name(), values); - self.wrap(filter.into()) - } - - fn is_not_in( - &self, - values: impl IntoIterator>, - ) -> Self::Wrapped { - let filter = Filter::is_not_in(self.field_name(), values); - self.wrap(filter.into()) - } - - fn starts_with(&self, value: impl Into) -> Self::Wrapped { - let filter = Filter::starts_with(self.field_name(), value); - self.wrap(filter.into()) - } - - fn ends_with(&self, value: impl Into) -> Self::Wrapped { - let filter = Filter::ends_with(self.field_name(), value); - self.wrap(filter.into()) - } - - fn contains(&self, value: impl Into) -> Self::Wrapped { - let filter = Filter::contains(self.field_name(), value); - self.wrap(filter.into()) - } - - fn not_contains(&self, value: impl Into) -> Self::Wrapped { - let filter = Filter::not_contains(self.field_name(), value.into()); - self.wrap(filter.into()) - } - - fn fuzzy_search( - &self, - value: impl Into, - levenshtein_distance: usize, - prefix_match: bool, - ) -> Self::Wrapped { - let filter = - Filter::fuzzy_search(self.field_name(), value, levenshtein_distance, prefix_match); - self.wrap(filter.into()) - } -} - -impl NodeFilterOps for T {} 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 deleted file mode 100644 index 191b974a96..0000000000 --- a/raphtory/src/db/graph/views/filter/model/node_filter/validate.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::{ - db::graph::views::filter::model::{ - filter::FieldFilterValue, - FilterOperator::{ - Contains, EndsWith, Eq, Ge, Gt, IsIn, IsNone, IsNotIn, IsSome, Le, Lt, Ne, NotContains, - StartsWith, *, - }, - }, - errors::GraphError, - prelude::Filter, -}; -use raphtory_api::core::entities::{ - GidType, - GidType::{Str, U64}, - GID, -}; - -pub fn validate(id_dtype: Option, filter: &Filter) -> Result<(), GraphError> { - let Some(kind) = id_dtype else { - return Ok(()); - }; - - fn filter_value_kind(fv: &FieldFilterValue) -> &'static str { - match fv { - 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(_))) { - "Str" - } else { - "heterogeneous id set" - } - } - FieldFilterValue::Single(_) => "Str", - FieldFilterValue::Set(_) => "Str", - } - } - - let value_matches_kind = |fv: &FieldFilterValue, expect: GidType| -> bool { - match (fv, expect) { - (FieldFilterValue::ID(GID::U64(_)), U64) => true, - (FieldFilterValue::IDSet(set), U64) => set.iter().all(|g| matches!(g, GID::U64(_))), - - (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, - } - }; - - let op_allowed = match kind { - U64 => matches!( - filter.operator, - Eq | Ne | Lt | Le | Gt | Ge | IsIn | IsNotIn - ), - Str => matches!( - filter.operator, - Eq | Ne - | StartsWith - | EndsWith - | Contains - | NotContains - | FuzzySearch { .. } - | IsIn - | IsNotIn - ), - }; - - if !op_allowed { - return Err(GraphError::InvalidGqlFilter(format!( - "Operator {} not allowed for {:?} ID", - filter.operator, kind - ))); - } - - if !value_matches_kind(&filter.field_value, kind) { - return Err(GraphError::InvalidGqlFilter(format!( - "Filter value type does not match node ID type. Expected {:?} but got {:?}", - kind, - filter_value_kind(&filter.field_value) - ))); - } - - match filter.operator { - IsIn | IsNotIn => { - if !matches!( - filter.field_value, - FieldFilterValue::IDSet(_) | FieldFilterValue::Set(_) - ) { - return Err(GraphError::InvalidGqlFilter( - "IN/NOT_IN on ID expects a set of IDs".into(), - )); - } - } - StartsWith | EndsWith | Contains | NotContains | FuzzySearch { .. } => { - if !matches!( - filter.field_value, - FieldFilterValue::ID(GID::Str(_)) | FieldFilterValue::Single(_) - ) { - return Err(GraphError::InvalidGqlFilter( - "String operators on ID expect a single string ID".into(), - )); - } - } - Lt | Le | Gt | Ge => { - if !matches!(filter.field_value, FieldFilterValue::ID(GID::U64(_))) { - return Err(GraphError::InvalidGqlFilter( - "Numeric operators on ID expect a single numeric (u64) ID".into(), - )); - } - } - IsSome | IsNone => { - return Err(GraphError::InvalidGqlFilter( - "IsSome/IsNone are not supported as filter operators".into(), - )); - } - Eq | Ne => { - // Eq/Ne already type-checked above - } - } - - Ok(()) -} diff --git a/raphtory/src/db/graph/views/filter/model/node_state_filter.rs b/raphtory/src/db/graph/views/filter/model/node_state_filter.rs index 4678e152ed..71d4a03fde 100644 --- a/raphtory/src/db/graph/views/filter/model/node_state_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/node_state_filter.rs @@ -1,12 +1,7 @@ use crate::{ - db::{ - api::{ - state::{ops::NodeOp, Index, NodeStateValue, TypedNodeState}, - view::internal::NodeList, - }, - graph::views::filter::model::{ - edge_filter::CompositeEdgeFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, - }, + db::api::{ + state::{ops::NodeOp, Index, NodeStateValue, TypedNodeState}, + view::internal::NodeList, }, errors::GraphError, }; 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 b2056eacf3..94a3d16c56 100644 --- a/raphtory/src/db/graph/views/filter/model/not_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/not_filter.rs @@ -5,13 +5,7 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, ComposableFilter, FilterTree, - }, - not_filtered_graph::NotFilteredGraph, - CreateFilter, + model::ComposableFilter, not_filtered_graph::NotFilteredGraph, CreateFilter, }, }, errors::GraphError, 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 27c58600aa..167b0a52bd 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -5,13 +5,7 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, ComposableFilter, FilterTree, - }, - or_filtered_graph::OrFilteredGraph, - CreateFilter, + model::ComposableFilter, or_filtered_graph::OrFilteredGraph, CreateFilter, }, }, errors::GraphError, 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 deleted file mode 100644 index 95f020a31c..0000000000 --- a/raphtory/src/db/graph/views/filter/model/property_filter/builders.rs +++ /dev/null @@ -1,216 +0,0 @@ -use crate::db::graph::views::filter::model::{ - property_filter::{Op, PropertyFilter, PropertyFilterInput, PropertyRef}, - CombinedFilter, EntityMarker, InternalPropertyFilterBuilder, TemporalPropertyFilterFactory, - Wrap, -}; - -#[derive(Clone)] -pub struct PropertyFilterBuilder(pub String, pub M); - -impl PropertyFilterBuilder { - pub fn new(prop: impl Into, entity: M) -> Self { - Self(prop.into(), entity) - } -} - -impl Wrap for PropertyFilterBuilder { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - -impl InternalPropertyFilterBuilder for PropertyFilterBuilder -where - M: Into + Send + Sync + Clone + 'static, - PropertyFilter: CombinedFilter, - PropertyExprBuilder: InternalPropertyFilterBuilder, -{ - type Filter = PropertyFilter; - type ExprBuilder = PropertyExprBuilder; - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - PropertyRef::Property(self.0.clone()) - } - - fn ops(&self) -> &[Op] { - &[] - } - - fn entity(&self) -> Self::Marker { - self.1.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()) - } -} - -#[derive(Clone)] -pub struct MetadataFilterBuilder(pub String, pub M); - -impl MetadataFilterBuilder { - pub fn new(prop: impl Into, entity: M) -> Self { - Self(prop.into(), entity) - } -} - -impl Wrap for MetadataFilterBuilder { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - -impl InternalPropertyFilterBuilder for MetadataFilterBuilder -where - M: Into + Send + Sync + Clone + 'static, - PropertyFilter: CombinedFilter, - PropertyExprBuilder: InternalPropertyFilterBuilder, -{ - type Filter = PropertyFilter; - type ExprBuilder = PropertyExprBuilder; - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - PropertyRef::Metadata(self.0.clone()) - } - - fn ops(&self) -> &[Op] { - &[] - } - - fn entity(&self) -> Self::Marker { - self.1.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()) - } -} - -pub struct PropertyExprBuilderInput { - pub prop_ref: PropertyRef, - pub ops: Vec, -} - -impl PropertyExprBuilderInput { - pub fn with_entity(self, entity: M) -> PropertyExprBuilder { - PropertyExprBuilder { - prop_ref: self.prop_ref, - ops: self.ops, - entity, - } - } -} - -#[derive(Clone)] -pub struct PropertyExprBuilder { - pub prop_ref: PropertyRef, - pub ops: Vec, - pub entity: M, -} - -impl PropertyExprBuilder { - pub fn with_op(mut self, op: Op) -> Self { - self.ops.push(op); - self - } - - pub fn with_ops(mut self, ops: impl IntoIterator) -> Self { - self.ops.extend(ops); - self - } - - pub fn first(self) -> Self { - self.with_op(Op::First) - } - - pub fn last(self) -> Self { - self.with_op(Op::Last) - } - - pub fn any(self) -> Self { - self.with_op(Op::Any) - } - - pub fn all(self) -> Self { - self.with_op(Op::All) - } - - pub fn len(self) -> Self { - self.with_op(Op::Len) - } - - pub fn sum(self) -> Self { - self.with_op(Op::Sum) - } - - pub fn avg(self) -> Self { - self.with_op(Op::Avg) - } - - pub fn min(self) -> Self { - self.with_op(Op::Min) - } - - pub fn max(self) -> Self { - self.with_op(Op::Max) - } -} - -impl Wrap for PropertyExprBuilder { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - -impl InternalPropertyFilterBuilder for PropertyExprBuilder -where - M: Into + Send + Sync + Clone + 'static, - PropertyFilter: CombinedFilter, -{ - type Filter = PropertyFilter; - type ExprBuilder = PropertyExprBuilder; - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - self.prop_ref.clone() - } - - fn ops(&self) -> &[Op] { - &self.ops - } - - fn entity(&self) -> Self::Marker { - self.entity.clone() - } - - 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 TemporalPropertyFilterFactory for PropertyFilterBuilder -where - T: Into + Send + Sync + Clone + 'static, - PropertyFilter: CombinedFilter, -{ -} diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index 2df06026bb..f37ad78690 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -1,4 +1,4 @@ -use crate::{db::graph::views::filter::model::Op, prelude::PropertyFilter}; +use crate::{db::graph::views::filter::model::property_filter::Op, prelude::PropertyFilter}; use raphtory_api::core::{ entities::properties::prop::{Prop, PropType}, utils::generalised_reduce, 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 ffce3805fb..9ba055ef7a 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 @@ -2,29 +2,16 @@ use crate::{ db::{ api::{ properties::{internal::InternalPropertiesOps, Metadata, Properties}, - state::ops::{filter::NodePropertyFilterOp, NotANodeFilter}, view::internal::{GraphView, NodeTimeSemanticsOps}, }, graph::{ edge::EdgeView, node::NodeView, - views::filter::{ - edge_filtered_graph::EdgeFilteredGraph, - edge_property_filtered_graph::EdgePropertyFilteredGraph, - exploded_edge_filtered_graph::ExplodedEdgeFilteredGraph, - exploded_edge_property_filter::ExplodedEdgePropertyFilteredGraph, - model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, - CompositeExplodedEdgeFilter, CompositeNodeFilter, ExplodedEdgeFilter, - FilterOperator, - }, - node_filtered_graph::NodeFilteredGraph, - CreateFilter, - }, + views::filter::model::{ComposableFilter, FilterOperator}, }, }, errors::GraphError, - prelude::{EdgeFilter, EdgeViewOps, LayerOps, NodeFilter, NodeViewOps, PropertiesOps}, + prelude::{EdgeViewOps, NodeViewOps, PropertiesOps}, }; use either::Either; use itertools::Itertools; @@ -44,9 +31,7 @@ use raphtory_storage::graph::{ }; use std::{collections::HashSet, fmt, fmt::Display, sync::Arc}; -pub mod builders; pub(crate) mod evaluate; -pub mod ops; mod validate; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -383,120 +368,4 @@ impl PropertyFilter { } } -impl CreateFilter for PropertyFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph>; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodePropertyFilterOp; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone(), filtered)?; - Ok(NodeFilteredGraph::new(graph, filter)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let prop_id = self.resolve_prop_id(graph.node_meta(), false)?; - Ok(NodePropertyFilterOp::new(filtered, prop_id, self)) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -impl CreateFilter for PropertyFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - EdgeFilteredGraph>; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let prop_id = self.resolve_prop_id(graph.edge_meta(), graph.num_layers() > 1)?; - Ok(EdgeFilteredGraph::new( - graph, - EdgePropertyFilteredGraph::new(filtered, prop_id, self), - )) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -impl CreateFilter for PropertyFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - ExplodedEdgeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let prop_id = self.resolve_prop_id(graph.edge_meta(), graph.num_layers() > 1)?; - Ok(ExplodedEdgeFilteredGraph::new( - graph, - ExplodedEdgePropertyFilteredGraph::new(filtered, prop_id, self), - )) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> 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 deleted file mode 100644 index 0e946406c8..0000000000 --- a/raphtory/src/db/graph/views/filter/model/property_filter/ops.rs +++ /dev/null @@ -1,277 +0,0 @@ -use crate::db::graph::views::filter::model::{ - property_filter::{ - builders::PropertyExprBuilderInput, Op, PropertyFilterInput, PropertyFilterValue, - }, - FilterOperator, InternalPropertyFilterBuilder, -}; -use raphtory_api::core::{entities::properties::prop::Prop, storage::arc_str::ArcStr}; -use std::sync::Arc; - -pub trait PropertyFilterOps: InternalPropertyFilterBuilder { - fn eq(&self, value: impl Into) -> Self::Filter; - fn ne(&self, value: impl Into) -> Self::Filter; - fn le(&self, value: impl Into) -> Self::Filter; - fn ge(&self, value: impl Into) -> Self::Filter; - fn lt(&self, value: impl Into) -> Self::Filter; - fn gt(&self, value: impl Into) -> Self::Filter; - fn is_in(&self, values: impl IntoIterator) -> Self::Filter; - fn is_not_in(&self, values: impl IntoIterator) -> Self::Filter; - fn is_none(&self) -> Self::Filter; - fn is_some(&self) -> Self::Filter; - fn starts_with(&self, value: impl Into) -> Self::Filter; - fn ends_with(&self, value: impl Into) -> Self::Filter; - fn contains(&self, value: impl Into) -> Self::Filter; - fn not_contains(&self, value: impl Into) -> Self::Filter; - fn fuzzy_search( - &self, - prop_value: impl Into, - levenshtein_distance: usize, - prefix_match: bool, - ) -> Self::Filter; -} - -impl PropertyFilterOps for T { - fn eq(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::Eq, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn ne(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::Ne, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn le(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::Le, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn ge(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::Ge, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn lt(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::Lt, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn gt(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::Gt, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn is_in(&self, values: impl IntoIterator) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Set(Arc::new(values.into_iter().collect())), - operator: FilterOperator::IsIn, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn is_not_in(&self, values: impl IntoIterator) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Set(Arc::new(values.into_iter().collect())), - operator: FilterOperator::IsNotIn, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn is_none(&self) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::None, - operator: FilterOperator::IsNone, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn is_some(&self) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::None, - operator: FilterOperator::IsSome, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn starts_with(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::StartsWith, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn ends_with(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::EndsWith, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn contains(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::Contains, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn not_contains(&self, value: impl Into) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(value.into()), - operator: FilterOperator::NotContains, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } - - fn fuzzy_search( - &self, - prop_value: impl Into, - levenshtein_distance: usize, - prefix_match: bool, - ) -> Self::Filter { - let filter = PropertyFilterInput { - prop_ref: self.property_ref(), - prop_value: PropertyFilterValue::Single(Prop::Str(ArcStr::from(prop_value.into()))), - operator: FilterOperator::FuzzySearch { - levenshtein_distance, - prefix_match, - }, - ops: self.ops().to_vec(), - }; - self.filter(filter) - } -} - -pub trait ElemQualifierOps: InternalPropertyFilterBuilder { - fn any(&self) -> Self::ExprBuilder - where - Self: Sized, - { - let builder = PropertyExprBuilderInput { - prop_ref: self.property_ref(), - ops: self.ops().iter().copied().chain([Op::Any]).collect(), - }; - self.with_expr_builder(builder) - } - - fn all(&self) -> Self::ExprBuilder - where - Self: Sized, - { - let builder = PropertyExprBuilderInput { - prop_ref: self.property_ref(), - ops: self.ops().iter().copied().chain([Op::All]).collect(), - }; - self.with_expr_builder(builder) - } -} - -impl ElemQualifierOps for T {} - -pub trait ListAggOps: InternalPropertyFilterBuilder { - fn len(&self) -> Self::ExprBuilder { - let builder = PropertyExprBuilderInput { - prop_ref: self.property_ref(), - ops: self.ops().iter().copied().chain([Op::Len]).collect(), - }; - self.with_expr_builder(builder) - } - - fn sum(&self) -> Self::ExprBuilder { - let builder = PropertyExprBuilderInput { - prop_ref: self.property_ref(), - ops: self.ops().iter().copied().chain([Op::Sum]).collect(), - }; - self.with_expr_builder(builder) - } - - fn avg(&self) -> Self::ExprBuilder { - let builder = PropertyExprBuilderInput { - prop_ref: self.property_ref(), - ops: self.ops().iter().copied().chain([Op::Avg]).collect(), - }; - self.with_expr_builder(builder) - } - - fn min(&self) -> Self::ExprBuilder { - let builder = PropertyExprBuilderInput { - prop_ref: self.property_ref(), - ops: self.ops().iter().copied().chain([Op::Min]).collect(), - }; - self.with_expr_builder(builder) - } - - fn max(&self) -> Self::ExprBuilder { - let builder = PropertyExprBuilderInput { - prop_ref: self.property_ref(), - ops: self.ops().iter().copied().chain([Op::Max]).collect(), - }; - self.with_expr_builder(builder) - } - - fn first(&self) -> Self::ExprBuilder { - let builder = PropertyExprBuilderInput { - prop_ref: self.property_ref(), - ops: self.ops().iter().copied().chain([Op::First]).collect(), - }; - self.with_expr_builder(builder) - } - - fn last(&self) -> Self::ExprBuilder { - let builder = PropertyExprBuilderInput { - prop_ref: self.property_ref(), - ops: self.ops().iter().copied().chain([Op::Last]).collect(), - }; - self.with_expr_builder(builder) - } -} - -impl ListAggOps for T {} diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/validate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/validate.rs index 5fc1a6916a..ba96dd4dce 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/validate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/validate.rs @@ -1,6 +1,7 @@ use crate::{ db::graph::views::filter::model::{ - property_filter::PropertyFilterValue, FilterOperator, Op, PropertyRef, + property_filter::{Op, PropertyFilterValue, PropertyRef}, + FilterOperator, }, errors::GraphError, prelude::PropertyFilter, 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 52fd566aa3..4e02597279 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -7,21 +7,12 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_expr::EdgeOp, - 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_expr::CreateOp, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateView, EdgeViewFilterOps, FilterTree, GraphViewOp, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, - InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, Wrap, + edge_expr::EdgeOp, is_active_edge_filter::IsActiveEdge, + is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, + node_expr::CreateOp, windowed_filter::Windowed, CombinedFilter, + ComposableFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, + NodeViewFilterOps, Wrap, }, CreateFilter, }, @@ -65,32 +56,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 CreateFilter for SnapshotAt { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> @@ -154,26 +119,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>; @@ -228,32 +173,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 CreateFilter for SnapshotLatest { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> @@ -313,26 +232,6 @@ impl Wrap for SnapshotLatest { } } -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>; @@ -388,7 +287,7 @@ impl CreateView for SnapshotLatest { } // ── expr layer: the snapshot-at view scopes any inner expression (per-expression view) ── -// Nesting order of chained views is pinned by the Phase-3 semantics tests. +// Nesting order of chained views is pinned by the view-semantics tests. impl CreateOp for SnapshotAt { fn create_node_op<'g, G: GraphView + 'g>( @@ -407,7 +306,7 @@ impl CreateOp for SnapshotAt { } // ── expr layer: the snapshot-latest view scopes any inner expression (per-expression view) ── -// Nesting order of chained views is pinned by the Phase-3 semantics tests. +// Nesting order of chained views is pinned by the view-semantics tests. impl CreateOp for SnapshotLatest { fn create_node_op<'g, G: GraphView + 'g>( 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 5d3df4cea6..bdaa72da4d 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -4,23 +4,11 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_expr::EdgeOp, - 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_expr::CreateOp, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, CreateView, EdgeViewFilterOps, FilterTree, GraphViewOp, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, - InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, Wrap, + edge_expr::EdgeOp, is_active_edge_filter::IsActiveEdge, + is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, + node_expr::CreateOp, CombinedFilter, ComposableFilter, CreateView, + EdgeViewFilterOps, InternalViewWrapOps, NodeViewFilterOps, Wrap, }, CreateFilter, }, @@ -86,46 +74,6 @@ impl InternalViewWrapOps for Windowed { } } -impl InternalNodeFilterBuilder for Windowed { - type FilterType = T::FilterType; - - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalNodeIdFilterBuilder for Windowed { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalPropertyFilterBuilder for Windowed { - type Filter = Windowed; - type ExprBuilder = Windowed; - type Marker = T::Marker; - - fn property_ref(&self) -> PropertyRef { - self.inner.property_ref() - } - - fn ops(&self) -> &[Op] { - self.inner.ops() - } - - fn entity(&self) -> Self::Marker { - self.inner.entity() - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - impl CreateFilter for Windowed { type EntityFiltered<'graph, G, F> = T::EntityFiltered<'graph, G, F> @@ -190,26 +138,6 @@ impl Wrap for Windowed { } } -impl InternalPropertyFilterFactory for Windowed { - type Entity = T::Entity; - type PropertyBuilder = Windowed; - type MetadataBuilder = Windowed; - - fn entity(&self) -> Self::Entity { - self.inner.entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.wrap(self.inner.property_builder(property)) - } - - 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>; @@ -253,7 +181,7 @@ impl CreateView for Windowed { } // ── expr layer: the windowed view scopes any inner expression (per-expression view) ── -// Nesting order of chained views is pinned by the Phase-3 semantics tests. +// Nesting order of chained views is pinned by the view-semantics tests. impl CreateOp for Windowed { fn create_node_op<'g, G: GraphView + 'g>( diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index f3a9a28bd8..a033967ec5 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -144,9 +144,7 @@ pub mod prelude { views::{ deletion_graph::PersistentGraph, filter::model::{ - filter::Filter, - node_filter::{ops::*, NodeFilter}, - property_filter::{ops::PropertyFilterOps, PropertyFilter}, + filter::Filter, node_filter::NodeFilter, property_filter::PropertyFilter, EdgeFilter, }, property_redacted_graph::PropertyRedaction, diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 46bb18171b..b37212e206 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -6,7 +6,6 @@ use crate::{ }, graph::views::filter::{ model::{ - edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, not_filter::NotFilter, or_filter::OrFilter, AndFilter, DynCreateFilter, FilterTree, }, CreateFilter, diff --git a/raphtory/src/python/graph/views/graph_view.rs b/raphtory/src/python/graph/views/graph_view.rs index ecfe1108fc..c682068fe8 100644 --- a/raphtory/src/python/graph/views/graph_view.rs +++ b/raphtory/src/python/graph/views/graph_view.rs @@ -16,10 +16,6 @@ use crate::{ nodes::Nodes, views::{ cached_view::CachedView, - filter::{ - edge_property_filtered_graph::EdgePropertyFilteredGraph, - exploded_edge_property_filter::ExplodedEdgePropertyFilteredGraph, - }, layer_graph::LayeredGraph, node_subgraph::{NodeSubgraph, UnfilteredSubgraph}, valid_graph::ValidGraph, @@ -145,28 +141,6 @@ impl<'py, G: StaticGraphViewOps + IntoDynamic> IntoPyObject<'py> for CachedView< } } -impl<'py, G: StaticGraphViewOps + IntoDynamic> IntoPyObject<'py> for EdgePropertyFilteredGraph { - type Target = PyGraphView; - type Output = >::Output; - type Error = >::Error; - - fn into_pyobject(self, py: Python<'py>) -> Result { - PyGraphView::from(self).into_pyobject(py) - } -} - -impl<'py, G: StaticGraphViewOps + IntoDynamic> IntoPyObject<'py> - for ExplodedEdgePropertyFilteredGraph -{ - type Target = PyGraphView; - type Output = >::Output; - type Error = >::Error; - - fn into_pyobject(self, py: Python<'py>) -> Result { - PyGraphView::from(self).into_pyobject(py) - } -} - impl<'py, G: StaticGraphViewOps + IntoDynamic> IntoPyObject<'py> for ValidGraph { type Target = PyGraphView; type Output = >::Output; From dc862ca87a1bd72989e338ea434abcfcb1151daf Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:38:14 +0100 Subject: [PATCH 113/155] narrow id-filter domains, widen sum's declared type, refresh stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node-id equality and set membership resolve their evaluation domain through the storage index instead of visiting every node, carrying the narrowing #2754 added to the removed builder op onto expressions. A constant whose type does not match the graph's id type keeps the full domain rather than guessing, and every other predicate is unchanged. A sum widens at runtime but declared its element type, so a constant beyond that element's range was rejected before the sum ran — no comparison above 255 was expressible on a list of u8. Sum now declares what the evaluator produces; reductions returning an element keep the element type. The generated stubs and the docstrings pointing at them still named builder classes that no longer exist. --- python/python/raphtory/__init__.pyi | 596 +----- .../python/raphtory/algorithms/__init__.pyi | 191 +- python/python/raphtory/filter/__init__.pyi | 1738 +---------------- python/python/raphtory/graph_gen/__init__.pyi | 12 +- .../python/raphtory/graph_loader/__init__.pyi | 11 +- python/python/raphtory/graphql/__init__.pyi | 294 +-- python/python/raphtory/iterables/__init__.pyi | 236 ++- .../python/raphtory/node_state/__init__.pyi | 331 ++-- python/python/raphtory/vectors/__init__.pyi | 101 +- raphtory-tests/tests/test_filters.rs | 19 +- .../views/filter/model/node_expr/dyn_expr.rs | 10 + .../views/filter/model/node_expr/exprs.rs | 4 + .../views/filter/model/node_expr/filters.rs | 48 +- .../graph/views/filter/model/node_expr/mod.rs | 7 + .../graph/views/filter/model/node_expr/ops.rs | 137 +- .../views/filter/model/node_expr/tests.rs | 128 +- raphtory/src/python/filter/edge_expr.rs | 15 + raphtory/src/python/filter/node_expr.rs | 34 +- 18 files changed, 857 insertions(+), 3055 deletions(-) diff --git a/python/python/raphtory/__init__.pyi b/python/python/raphtory/__init__.pyi index 9546372c67..1e878618aa 100644 --- a/python/python/raphtory/__init__.pyi +++ b/python/python/raphtory/__init__.pyi @@ -1,7 +1,6 @@ """ Raphtory graph analytics library """ - from __future__ import annotations ############################################################################### @@ -33,53 +32,8 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = [ - "GraphView", - "Graph", - "PersistentGraph", - "Node", - "Nodes", - "PathFromNode", - "PathFromGraph", - "MutableNode", - "Edge", - "Edges", - "NestedEdges", - "MutableEdge", - "SortByTime", - "NodeSortBy", - "EdgeSortBy", - "Properties", - "PyPropValueList", - "PropType", - "Metadata", - "MetadataView", - "TemporalProperties", - "PropertiesView", - "TemporalProperty", - "EventTime", - "OptionalEventTime", - "History", - "HistoryTimestamp", - "HistoryDateTime", - "HistoryEventId", - "Intervals", - "WindowSet", - "Prop", - "version", - "graphql", - "algorithms", - "graph_loader", - "graph_gen", - "vectors", - "node_state", - "filter", - "iterables", - "nullmodels", - "plottingutils", -] - -class GraphView(object): +__all__ = ['GraphView', 'Graph', 'PersistentGraph', 'Node', 'Nodes', 'PathFromNode', 'PathFromGraph', 'MutableNode', 'Edge', 'Edges', 'NestedEdges', 'MutableEdge', 'SortByTime', 'NodeSortBy', 'EdgeSortBy', 'Properties', 'PyPropValueList', 'PropType', 'Metadata', 'MetadataView', 'TemporalProperties', 'PropertiesView', 'TemporalProperty', 'EventTime', 'OptionalEventTime', 'History', 'HistoryTimestamp', 'HistoryDateTime', 'HistoryEventId', 'Intervals', 'WindowSet', 'Prop', 'version', 'graphql', 'algorithms', 'graph_loader', 'graph_gen', 'vectors', 'node_state', 'filter', 'iterables', 'nullmodels', 'plottingutils'] +class GraphView(object): """Graph view is a read-only version of a graph at a certain point in time.""" def __eq__(self, value): @@ -284,9 +238,7 @@ class GraphView(object): GraphView: The layered view """ - def expanding( - self, step: int | str, alignment_unit: str | None = None - ) -> WindowSet: + def expanding(self, step: int | str, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `step` size using an expanding window. @@ -461,12 +413,7 @@ class GraphView(object): Properties: Properties paired with their names """ - def rolling( - self, - window: int | str, - step: int | str | None = None, - alignment_unit: str | None = None, - ) -> WindowSet: + def rolling(self, window: int | str, step: int | str | None = None, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `window` size and optional `step` using a rolling window. If `alignment_unit` is not "unaligned" and a `step` larger than `window` is provided, some time entries @@ -565,14 +512,7 @@ class GraphView(object): GraphView: Returns the subgraph """ - def to_networkx( - self, - explode_edges: bool = False, - include_node_properties: bool = True, - include_edge_properties: bool = True, - include_update_history: bool = True, - include_property_history: bool = True, - ) -> nx.MultiDiGraph: + def to_networkx(self, explode_edges: bool = False, include_node_properties: bool = True, include_edge_properties: bool = True, include_update_history: bool = True, include_property_history: bool = True) -> nx.MultiDiGraph: """ Returns a graph with NetworkX. @@ -591,19 +531,7 @@ class GraphView(object): nx.MultiDiGraph: A Networkx MultiDiGraph. """ - def to_pyvis( - self, - explode_edges: bool = False, - edge_color: str = "#000000", - shape: str = "dot", - node_image: Optional[str] = None, - edge_weight: Optional[str] = None, - edge_label: Optional[str] = None, - colour_nodes_by_type: bool = False, - directed: bool = True, - notebook: bool = False, - **kwargs: Any, - ) -> pyvis.network.Network: + def to_pyvis(self, explode_edges: bool = False, edge_color: str = '#000000', shape: str = 'dot', node_image: Optional[str] = None, edge_weight: Optional[str] = None, edge_label: Optional[str] = None, colour_nodes_by_type: bool = False, directed: bool = True, notebook: bool = False, **kwargs: Any) -> pyvis.network.Network: """ Draw a graph with PyVis. Pyvis is a required dependency. If you intend to use this function make sure that you install Pyvis @@ -664,13 +592,7 @@ class GraphView(object): GraphView: The layered view """ - def vectorise( - self, - model: VectorCache, - nodes: bool | str = True, - edges: bool | str = True, - verbose: bool = False, - ) -> VectorisedGraph: + def vectorise(self, model: VectorCache, nodes: bool | str = True, edges: bool | str = True, verbose: bool = False) -> VectorisedGraph: """ Create a VectorisedGraph from the current graph. @@ -705,7 +627,7 @@ class GraphView(object): Optional[int]: """ -class Graph(GraphView): +class Graph(GraphView): """ A temporal graph with event semantics. @@ -714,21 +636,13 @@ class Graph(GraphView): config (Config, optional): The configuration options for the graph """ - def __new__( - cls, path: Optional[str | PathLike] = None, config: Optional[Config] = None - ) -> Graph: + def __new__(cls, path: Optional[str | PathLike] = None, config: Optional[Config] = None) -> Graph: """Create and return a new object. See help(type) for accurate signature.""" - def __reduce__(self): ... - def add_edge( - self, - timestamp: TimeInput, - src: str | int, - dst: str | int, - properties: Optional[PropInput] = None, - layer: Optional[str] = None, - event_id: Optional[int] = None, - ) -> MutableEdge: + def __reduce__(self): + ... + + def add_edge(self, timestamp: TimeInput, src: str|int, dst: str|int, properties: Optional[PropInput] = None, layer: Optional[str] = None, event_id: Optional[int] = None) -> MutableEdge: """ Adds a new edge with the given source and destination nodes and properties to the graph. @@ -761,15 +675,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def add_node( - self, - timestamp: TimeInput, - id: str | int, - properties: Optional[PropInput] = None, - node_type: Optional[str] = None, - event_id: Optional[int] = None, - layer: Optional[str] = None, - ) -> MutableNode: + def add_node(self, timestamp: TimeInput, id: str|int, properties: Optional[PropInput] = None, node_type: Optional[str] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> MutableNode: """ Adds a new node with the given id and properties to the graph. @@ -788,12 +694,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def add_properties( - self, - timestamp: TimeInput, - properties: PropInput, - event_id: Optional[int] = None, - ) -> None: + def add_properties(self, timestamp: TimeInput, properties: PropInput, event_id: Optional[int] = None) -> None: """ Adds properties to the graph. @@ -809,15 +710,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def create_node( - self, - timestamp: TimeInput, - id: str | int, - properties: Optional[PropInput] = None, - node_type: Optional[str] = None, - event_id: Optional[int] = None, - layer: Optional[str] = None, - ) -> MutableNode: + def create_node(self, timestamp: TimeInput, id: str|int, properties: Optional[PropInput] = None, node_type: Optional[str] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> MutableNode: """ Creates a new node with the given id and properties to the graph. It fails if the node already exists. @@ -835,14 +728,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def delete_edge( - self, - timestamp: int, - src: str | int, - dst: str | int, - layer: Optional[str] = None, - event_id: Optional[int] = None, - ) -> MutableEdge: + def delete_edge(self, timestamp: int, src: str | int, dst: str | int, layer: Optional[str] = None, event_id: Optional[int] = None) -> MutableEdge: """ Deletes an edge given the timestamp, src and dst nodes and layer (optional). @@ -872,7 +758,7 @@ class Graph(GraphView): Graph: """ - def edge(self, src: str | int, dst: str | int) -> MutableEdge: + def edge(self, src: str|int, dst: str|int) -> MutableEdge: """ Gets the edge with the specified source and destination nodes @@ -973,9 +859,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def import_edges_as( - self, edges: List[Edge], new_ids: List[Tuple[int, int]], merge: bool = False - ) -> None: + def import_edges_as(self, edges: List[Edge], new_ids: List[Tuple[int, int]], merge: bool = False) -> None: """ Import multiple edges into the graph with new ids. @@ -1010,9 +894,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def import_node_as( - self, node: Node, new_id: str | int, merge: bool = False - ) -> MutableNode: + def import_node_as(self, node: Node, new_id: str|int, merge: bool = False) -> MutableNode: """ Import a single node into the graph with new id. @@ -1047,9 +929,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def import_nodes_as( - self, nodes: List[Node], new_ids: List[str | int], merge: bool = False - ) -> None: + def import_nodes_as(self, nodes: List[Node], new_ids: List[str|int], merge: bool = False) -> None: """ Import multiple nodes into the graph with new ids. @@ -1080,11 +960,7 @@ class Graph(GraphView): """ @staticmethod - def load( - path: str | PathLike, - config: Optional[Config] = None, - read_only: Optional[bool] = False, - ) -> Graph: + def load(path: str | PathLike, config: Optional[Config] = None, read_only: Optional[bool] = False) -> Graph: """ Load a disk graph from path @@ -1100,21 +976,7 @@ class Graph(GraphView): Graph: the graph """ - def load_edge_metadata( - self, - data: Any, - src: str, - dst: str, - metadata: Optional[List[str]] = None, - shared_metadata: Optional[PropInput] = None, - layer: Optional[str] = None, - layer_col: Optional[str] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - csv_options: Optional[dict[str, str | bool]] = None, - ) -> None: + def load_edge_metadata(self, data: Any, src: str, dst: str, metadata: Optional[List[str]] = None, shared_metadata: Optional[PropInput] = None, layer: Optional[str] = None, layer_col: Optional[str] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, csv_options: Optional[dict[str, str | bool]] = None) -> None: """ Load edge metadata into the graph from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), a path to a CSV or Parquet file, or a directory containing multiple CSV or Parquet files. @@ -1139,24 +1001,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def load_edges( - self, - data: Any, - time: str, - src: str, - dst: str, - properties: Optional[List[str]] = None, - metadata: Optional[List[str]] = None, - shared_metadata: Optional[PropInput] = None, - layer: Optional[str] = None, - layer_col: Optional[str] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - csv_options: Optional[dict[str, str | bool]] = None, - event_id: Optional[str] = None, - ) -> None: + def load_edges(self, data: Any, time: str, src: str, dst: str, properties: Optional[List[str]] = None, metadata: Optional[List[str]] = None, shared_metadata: Optional[PropInput] = None, layer: Optional[str] = None, layer_col: Optional[str] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, csv_options: Optional[dict[str, str | bool]] = None, event_id: Optional[str] = None) -> None: """ Load edges into the graph from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), a path to a CSV or Parquet file, or a directory containing multiple CSV or Parquet files. @@ -1196,20 +1041,7 @@ class Graph(GraphView): Graph: """ - def load_node_metadata( - self, - data: Any, - id: str, - node_type: Optional[str] = None, - node_type_col: Optional[str] = None, - metadata: Optional[List[str]] = None, - shared_metadata: Optional[PropInput] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - csv_options: Optional[dict[str, str | bool]] = None, - ) -> None: + def load_node_metadata(self, data: Any, id: str, node_type: Optional[str] = None, node_type_col: Optional[str] = None, metadata: Optional[List[str]] = None, shared_metadata: Optional[PropInput] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, csv_options: Optional[dict[str, str | bool]] = None) -> None: """ Load node metadata into the graph from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), a path to a CSV or Parquet file, or a directory containing multiple CSV or Parquet files. @@ -1233,25 +1065,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def load_nodes( - self, - data: Any, - time: str, - id: str, - node_type: Optional[str] = None, - node_type_col: Optional[str] = None, - properties: Optional[List[str]] = None, - metadata: Optional[List[str]] = None, - shared_metadata: Optional[PropInput] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - csv_options: Optional[dict[str, str | bool]] = None, - event_id: Optional[str] = None, - layer: Optional[str] = None, - layer_col: Optional[str] = None, - ) -> None: + def load_nodes(self, data: Any, time: str, id: str, node_type: Optional[str] = None, node_type_col: Optional[str] = None, properties: Optional[List[str]] = None, metadata: Optional[List[str]] = None, shared_metadata: Optional[PropInput] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, csv_options: Optional[dict[str, str | bool]] = None, event_id: Optional[str] = None, layer: Optional[str] = None, layer_col: Optional[str] = None) -> None: """ Load nodes into the graph from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), a path to a CSV or Parquet file, or a directory containing multiple CSV or Parquet files. @@ -1280,7 +1094,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ - def node(self, id: str | int) -> MutableNode: + def node(self, id: str|int) -> MutableNode: """ Gets the node with the specified id @@ -1372,7 +1186,7 @@ class Graph(GraphView): GraphError: If the operation fails. """ -class PersistentGraph(GraphView): +class PersistentGraph(GraphView): """ A temporal graph that allows edges and nodes to be deleted. @@ -1382,21 +1196,13 @@ class PersistentGraph(GraphView): """ - def __new__( - cls, path: Optional[str | PathLike] = None, config: Optional[Config] = None - ) -> PersistentGraph: + def __new__(cls, path: Optional[str | PathLike] = None, config: Optional[Config] = None) -> PersistentGraph: """Create and return a new object. See help(type) for accurate signature.""" - def __reduce__(self): ... - def add_edge( - self, - timestamp: int, - src: str | int, - dst: str | int, - properties: Optional[PropInput] = None, - layer: Optional[str] = None, - event_id: Optional[int] = None, - ) -> None: + def __reduce__(self): + ... + + def add_edge(self, timestamp: int, src: str | int, dst: str | int, properties: Optional[PropInput] = None, layer: Optional[str] = None, event_id: Optional[int] = None) -> None: """ Adds a new edge with the given source and destination nodes and properties to the graph. @@ -1429,15 +1235,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def add_node( - self, - timestamp: TimeInput, - id: str | int, - properties: Optional[PropInput] = None, - node_type: Optional[str] = None, - event_id: Optional[int] = None, - layer: Any = None, - ) -> None: + def add_node(self, timestamp: TimeInput, id: str | int, properties: Optional[PropInput] = None, node_type: Optional[str] = None, event_id: Optional[int] = None, layer: Any = None) -> None: """ Adds a new node with the given id and properties to the graph. @@ -1456,9 +1254,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def add_properties( - self, timestamp: TimeInput, properties: dict, event_id: Optional[int] = None - ) -> None: + def add_properties(self, timestamp: TimeInput, properties: dict, event_id: Optional[int] = None) -> None: """ Adds properties to the graph. @@ -1474,15 +1270,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def create_node( - self, - timestamp: TimeInput, - id: str | int, - properties: Optional[PropInput] = None, - node_type: Optional[str] = None, - event_id: Optional[int] = None, - layer: Optional[str] = None, - ) -> MutableNode: + def create_node(self, timestamp: TimeInput, id: str | int, properties: Optional[PropInput] = None, node_type: Optional[str] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> MutableNode: """ Creates a new node with the given id and properties to the graph. It fails if the node already exists. @@ -1501,14 +1289,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def delete_edge( - self, - timestamp: int, - src: str | int, - dst: str | int, - layer: Optional[str] = None, - event_id: Optional[int] = None, - ) -> MutableEdge: + def delete_edge(self, timestamp: int, src: str | int, dst: str | int, layer: Optional[str] = None, event_id: Optional[int] = None) -> MutableEdge: """ Deletes an edge given the timestamp, src and dst nodes and layer (optional). @@ -1629,9 +1410,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def import_edges_as( - self, edges: List[Edge], new_ids: list[Tuple[GID, GID]], merge: bool = False - ) -> None: + def import_edges_as(self, edges: List[Edge], new_ids: list[Tuple[GID, GID]], merge: bool = False) -> None: """ Import multiple edges into the graph with new ids. @@ -1668,9 +1447,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def import_node_as( - self, node: Node, new_id: str | int, merge: bool = False - ) -> Node: + def import_node_as(self, node: Node, new_id: str|int, merge: bool = False) -> Node: """ Import a single node into the graph with new id. @@ -1707,9 +1484,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def import_nodes_as( - self, nodes: List[Node], new_ids: List[str | int], merge: bool = False - ) -> None: + def import_nodes_as(self, nodes: List[Node], new_ids: List[str|int], merge: bool = False) -> None: """ Import multiple nodes into the graph with new ids. @@ -1729,11 +1504,7 @@ class PersistentGraph(GraphView): """ @staticmethod - def load( - path: str | PathLike, - config: Optional[Config] = None, - read_only: Optional[bool] = False, - ) -> PersistentGraph: + def load(path: str | PathLike, config: Optional[Config] = None, read_only: Optional[bool] = False) -> PersistentGraph: """ Load a disk graph from path @@ -1749,21 +1520,7 @@ class PersistentGraph(GraphView): PersistentGraph: the graph """ - def load_edge_deletions( - self, - data: Any, - time: str, - src: str, - dst: str, - layer: Optional[str] = None, - layer_col: Optional[str] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - csv_options: Optional[dict[str, str | bool]] = None, - event_id: Optional[str] = None, - ) -> None: + def load_edge_deletions(self, data: Any, time: str, src: str, dst: str, layer: Optional[str] = None, layer_col: Optional[str] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, csv_options: Optional[dict[str, str | bool]] = None, event_id: Optional[str] = None) -> None: """ Load edge deletions into the graph from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), a path to a CSV or Parquet file, or a directory containing multiple CSV or Parquet files. @@ -1788,21 +1545,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def load_edge_metadata( - self, - data: Any, - src: str, - dst: str, - metadata: Optional[List[str]] = None, - shared_metadata: Optional[PropInput] = None, - layer: Optional[str] = None, - layer_col: Optional[str] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - csv_options: Optional[dict[str, str | bool]] = None, - ) -> None: + def load_edge_metadata(self, data: Any, src: str, dst: str, metadata: Optional[List[str]] = None, shared_metadata: Optional[PropInput] = None, layer: Optional[str] = None, layer_col: Optional[str] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, csv_options: Optional[dict[str, str | bool]] = None) -> None: """ Load edge metadata into the graph from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), a path to a CSV or Parquet file, or a directory containing multiple CSV or Parquet files. @@ -1827,24 +1570,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def load_edges( - self, - data: Any, - time: str, - src: str, - dst: str, - properties: Optional[List[str]] = None, - metadata: Optional[List[str]] = None, - shared_metadata: Optional[PropInput] = None, - layer: Optional[str] = None, - layer_col: Optional[str] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - csv_options: Optional[dict[str, str | bool]] = None, - event_id: Optional[str] = None, - ) -> None: + def load_edges(self, data: Any, time: str, src: str, dst: str, properties: Optional[List[str]] = None, metadata: Optional[List[str]] = None, shared_metadata: Optional[PropInput] = None, layer: Optional[str] = None, layer_col: Optional[str] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, csv_options: Optional[dict[str, str | bool]] = None, event_id: Optional[str] = None) -> None: """ Load edges into the graph from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), a path to a CSV or Parquet file, or a directory containing multiple CSV or Parquet files. @@ -1884,18 +1610,7 @@ class PersistentGraph(GraphView): PersistentGraph: """ - def load_graph_properties( - self, - data: Any, - time: str, - properties: Optional[List[str]] = None, - metadata: Optional[List[str]] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - event_id: Optional[str] = None, - ) -> None: + def load_graph_properties(self, data: Any, time: str, properties: Optional[List[str]] = None, metadata: Optional[List[str]] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, event_id: Optional[str] = None) -> None: """ Load graph properties from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), or a path to a Parquet file, or a directory containing multiple Parquet files. @@ -1917,20 +1632,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def load_node_metadata( - self, - data: Any, - id: str, - node_type: Optional[str] = None, - node_type_col: Optional[str] = None, - metadata: Optional[List[str]] = None, - shared_metadata: Optional[PropInput] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - csv_options: Optional[dict[str, str | bool]] = None, - ) -> None: + def load_node_metadata(self, data: Any, id: str, node_type: Optional[str] = None, node_type_col: Optional[str] = None, metadata: Optional[List[str]] = None, shared_metadata: Optional[PropInput] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, csv_options: Optional[dict[str, str | bool]] = None) -> None: """ Load node metadata into the graph from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), a path to a CSV or Parquet file, or a directory containing multiple CSV or Parquet files. @@ -1954,25 +1656,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ - def load_nodes( - self, - data: Any, - time: str, - id: str, - node_type: Optional[str] = None, - node_type_col: Optional[str] = None, - properties: Optional[List[str]] = None, - metadata: Optional[List[str]] = None, - shared_metadata: Optional[PropInput] = None, - schema: Optional[ - list[tuple[str, DataType | PropType | str]] - | dict[str, DataType | PropType | str] - ] = None, - csv_options: Optional[dict[str, str | bool]] = None, - event_id: Optional[str] = None, - layer: Optional[str] = None, - layer_col: Optional[str] = None, - ) -> None: + def load_nodes(self, data: Any, time: str, id: str, node_type: Optional[str] = None, node_type_col: Optional[str] = None, properties: Optional[List[str]] = None, metadata: Optional[List[str]] = None, shared_metadata: Optional[PropInput] = None, schema: Optional[list[tuple[str, DataType | PropType | str]] | dict[str, DataType | PropType | str]] = None, csv_options: Optional[dict[str, str | bool]] = None, event_id: Optional[str] = None, layer: Optional[str] = None, layer_col: Optional[str] = None) -> None: """ Load nodes into the graph from any data source that supports the ArrowStreamExportable protocol (by providing an __arrow_c_stream__() method), a path to a CSV or Parquet file, or a directory containing multiple CSV or Parquet files. @@ -2093,7 +1777,7 @@ class PersistentGraph(GraphView): GraphError: If the operation fails. """ -class Node(object): +class Node(object): """A node (or node) in the graph.""" def __eq__(self, value): @@ -2247,9 +1931,7 @@ class Node(object): Node: The layered view """ - def expanding( - self, step: int | str, alignment_unit: str | None = None - ) -> WindowSet: + def expanding(self, step: int | str, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `step` size using an expanding window. @@ -2300,7 +1982,7 @@ class Node(object): """ @property - def id(self) -> str | int: + def id(self) -> (str|int): """ Returns the id of the node. This is a unique identifier for the node. @@ -2455,12 +2137,7 @@ class Node(object): Properties: A list of properties. """ - def rolling( - self, - window: int | str, - step: int | str | None = None, - alignment_unit: str | None = None, - ) -> WindowSet: + def rolling(self, window: int | str, step: int | str | None = None, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `window` size and optional `step` using a rolling window. If `alignment_unit` is not "unaligned" and a `step` larger than `window` is provided, some time entries @@ -2570,7 +2247,7 @@ class Node(object): Optional[int]: """ -class Nodes(object): +class Nodes(object): """A list of nodes that can be iterated over.""" def __bool__(self): @@ -2741,9 +2418,7 @@ class Nodes(object): Nodes: The layered view """ - def expanding( - self, step: int | str, alignment_unit: str | None = None - ) -> WindowSet: + def expanding(self, step: int | str, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `step` size using an expanding window. @@ -2940,12 +2615,7 @@ class Nodes(object): PropertiesView: A view of the node properties. """ - def rolling( - self, - window: int | str, - step: int | str | None = None, - alignment_unit: str | None = None, - ) -> WindowSet: + def rolling(self, window: int | str, step: int | str | None = None, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `window` size and optional `step` using a rolling window. If `alignment_unit` is not "unaligned" and a `step` larger than `window` is provided, some time entries @@ -3034,9 +2704,7 @@ class Nodes(object): OptionalEventTime: The earliest time that this Nodes is valid or None if the Nodes is valid for all times. """ - def to_df( - self, include_property_history: bool = False, convert_datetime: bool = False - ) -> DataFrame: + def to_df(self, include_property_history: bool = False, convert_datetime: bool = False) -> DataFrame: """ Converts the graph's nodes into a Pandas DataFrame. @@ -3097,7 +2765,8 @@ class Nodes(object): Optional[int]: """ -class PathFromNode(object): +class PathFromNode(object): + def __bool__(self): """True if self else False""" @@ -3256,9 +2925,7 @@ class PathFromNode(object): PathFromNode: The layered view """ - def expanding( - self, step: int | str, alignment_unit: str | None = None - ) -> WindowSet: + def expanding(self, step: int | str, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `step` size using an expanding window. @@ -3446,12 +3113,7 @@ class PathFromNode(object): PropertiesView: """ - def rolling( - self, - window: int | str, - step: int | str | None = None, - alignment_unit: str | None = None, - ) -> WindowSet: + def rolling(self, window: int | str, step: int | str | None = None, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `window` size and optional `step` using a rolling window. If `alignment_unit` is not "unaligned" and a `step` larger than `window` is provided, some time entries @@ -3572,7 +3234,8 @@ class PathFromNode(object): Optional[int]: """ -class PathFromGraph(object): +class PathFromGraph(object): + def __bool__(self): """True if self else False""" @@ -3731,9 +3394,7 @@ class PathFromGraph(object): PathFromGraph: The layered view """ - def expanding( - self, step: int | str, alignment_unit: str | None = None - ) -> WindowSet: + def expanding(self, step: int | str, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `step` size using an expanding window. @@ -3930,12 +3591,7 @@ class PathFromGraph(object): PyNestedPropsIterable: """ - def rolling( - self, - window: int | str, - step: int | str | None = None, - alignment_unit: str | None = None, - ) -> WindowSet: + def rolling(self, window: int | str, step: int | str | None = None, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `window` size and optional `step` using a rolling window. If `alignment_unit` is not "unaligned" and a `step` larger than `window` is provided, some time entries @@ -4056,7 +3712,8 @@ class PathFromGraph(object): Optional[int]: """ -class MutableNode(Node): +class MutableNode(Node): + def __repr__(self): """Return repr(self).""" @@ -4073,13 +3730,7 @@ class MutableNode(Node): None: """ - def add_updates( - self, - t: TimeInput, - properties: Optional[PropInput] = None, - event_id: Optional[int] = None, - layer: Optional[str] = None, - ) -> None: + def add_updates(self, t: TimeInput, properties: Optional[PropInput] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> None: """ Add updates to a node in the graph at a specified time. This function allows for the addition of property updates to a node within the graph. The updates are time-stamped, meaning they are applied at the specified time. @@ -4125,7 +3776,7 @@ class MutableNode(Node): None: """ -class Edge(object): +class Edge(object): """ PyEdge is a Python class that represents an edge in the graph. An edge is a directed connection between two nodes. @@ -4275,9 +3926,7 @@ class Edge(object): Edge: The layered view """ - def expanding( - self, step: int | str, alignment_unit: str | None = None - ) -> WindowSet: + def expanding(self, step: int | str, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `step` size using an expanding window. @@ -4466,12 +4115,7 @@ class Edge(object): Properties: Properties on the Edge. """ - def rolling( - self, - window: int | str, - step: int | str | None = None, - alignment_unit: str | None = None, - ) -> WindowSet: + def rolling(self, window: int | str, step: int | str | None = None, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `window` size and optional `step` using a rolling window. If `alignment_unit` is not "unaligned" and a `step` larger than `window` is provided, some time entries @@ -4599,7 +4243,7 @@ class Edge(object): Optional[int]: """ -class Edges(object): +class Edges(object): """A list of edges that can be iterated over.""" def __bool__(self): @@ -4745,9 +4389,7 @@ class Edges(object): Edges: The layered view """ - def expanding( - self, step: int | str, alignment_unit: str | None = None - ) -> WindowSet: + def expanding(self, step: int | str, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `step` size using an expanding window. @@ -4940,12 +4582,7 @@ class Edges(object): PropertiesView: """ - def rolling( - self, - window: int | str, - step: int | str | None = None, - alignment_unit: str | None = None, - ) -> WindowSet: + def rolling(self, window: int | str, step: int | str | None = None, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `window` size and optional `step` using a rolling window. If `alignment_unit` is not "unaligned" and a `step` larger than `window` is provided, some time entries @@ -5052,12 +4689,7 @@ class Edges(object): EventTimeIterable: Iterable of `EventTime`s. """ - def to_df( - self, - include_property_history: bool = True, - convert_datetime: bool = False, - explode: bool = False, - ) -> DataFrame: + def to_df(self, include_property_history: bool = True, convert_datetime: bool = False, explode: bool = False) -> DataFrame: """ Converts the graph's edges into a Pandas DataFrame. @@ -5110,7 +4742,8 @@ class Edges(object): Optional[int]: """ -class NestedEdges(object): +class NestedEdges(object): + def __bool__(self): """True if self else False""" @@ -5254,9 +4887,7 @@ class NestedEdges(object): NestedEdges: The layered view """ - def expanding( - self, step: int | str, alignment_unit: str | None = None - ) -> WindowSet: + def expanding(self, step: int | str, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `step` size using an expanding window. @@ -5449,12 +5080,7 @@ class NestedEdges(object): PyNestedPropsIterable: """ - def rolling( - self, - window: int | str, - step: int | str | None = None, - alignment_unit: str | None = None, - ) -> WindowSet: + def rolling(self, window: int | str, step: int | str | None = None, alignment_unit: str | None = None) -> WindowSet: """ Creates a `WindowSet` with the given `window` size and optional `step` using a rolling window. If `alignment_unit` is not "unaligned" and a `step` larger than `window` is provided, some time entries @@ -5585,7 +5211,8 @@ class NestedEdges(object): Optional[int]: """ -class MutableEdge(Edge): +class MutableEdge(Edge): + def __repr__(self): """Return repr(self).""" @@ -5603,13 +5230,7 @@ class MutableEdge(Edge): None: """ - def add_updates( - self, - t: TimeInput, - properties: Optional[PropInput] = None, - layer: Optional[str] = None, - event_id: Optional[int] = None, - ) -> None: + def add_updates(self, t: TimeInput, properties: Optional[PropInput] = None, layer: Optional[str] = None, event_id: Optional[int] = None) -> None: """ Add updates to an edge in the graph at a specified time. This function allows for the addition of property updates to an edge within the graph. The updates are time-stamped, meaning they are applied at the specified time. @@ -5627,9 +5248,7 @@ class MutableEdge(Edge): GraphError: If the operation fails. """ - def delete( - self, t: TimeInput, layer: Optional[str] = None, event_id: Optional[int] = None - ) -> None: + def delete(self, t: TimeInput, layer: Optional[str] = None, event_id: Optional[int] = None) -> None: """ Mark the edge as deleted at the specified time. @@ -5659,7 +5278,7 @@ class MutableEdge(Edge): None: """ -class SortByTime(object): +class SortByTime(object): """Which time boundary of a member to sort by.""" def __eq__(self, value): @@ -5686,7 +5305,7 @@ class SortByTime(object): def __repr__(self): """Return repr(self).""" -class NodeSortBy(object): +class NodeSortBy(object): """ One entry in a `Nodes.sorted(...)` sort key list. Construct with the static factories `by_id` / `by_name` / `by_type` / `by_time` / @@ -5755,7 +5374,7 @@ class NodeSortBy(object): NodeSortBy: a sort key usable in `Nodes.sorted(...)`. """ -class EdgeSortBy(object): +class EdgeSortBy(object): """ One entry in an `Edges.sorted(...)` sort key list. Construct with the static factories `by_src` / `by_dst` / `by_neighbour` / `by_time` / @@ -5829,7 +5448,7 @@ class EdgeSortBy(object): EdgeSortBy: a sort key usable in `Edges.sorted(...)`. """ -class Properties(object): +class Properties(object): """A view of the properties of an entity""" def __contains__(self, key): @@ -5935,7 +5554,8 @@ class Properties(object): list[PropValue]: """ -class PyPropValueList(object): +class PyPropValueList(object): + def __eq__(self, value): """Return self==value.""" @@ -6050,7 +5670,7 @@ class PyPropValueList(object): PropValue: """ -class PropType(object): +class PropType(object): """ PropType provides access to the types used by Raphtory. They can be used to specify the data type of different properties, which is especially useful if one wishes to cast some input column from one type to another during ingestion. @@ -6227,7 +5847,7 @@ class PropType(object): PropType: """ -class Metadata(object): +class Metadata(object): """A view of metadata of an entity""" def __contains__(self, key): @@ -6312,7 +5932,8 @@ class Metadata(object): list[PropValue]: """ -class MetadataView(object): +class MetadataView(object): + def __contains__(self, key): """Return bool(key in self).""" @@ -6384,7 +6005,7 @@ class MetadataView(object): list[PyPropValueList]: """ -class TemporalProperties(object): +class TemporalProperties(object): """A view of the temporal properties of an entity""" def __contains__(self, key): @@ -6475,7 +6096,8 @@ class TemporalProperties(object): list[TemporalProperty]: the list of property views """ -class PropertiesView(object): +class PropertiesView(object): + def __contains__(self, key): """Return bool(key in self).""" @@ -6558,7 +6180,7 @@ class PropertiesView(object): list[list[PropValue]]: """ -class TemporalProperty(object): +class TemporalProperty(object): """A view of a temporal property""" def __eq__(self, value): @@ -6704,7 +6326,7 @@ class TemporalProperty(object): NDArray: a numpy array of values, one per update. """ -class EventTime(object): +class EventTime(object): """ Raphtory's EventTime. Represents a unique timepoint in the graph's history as (timestamp, event_id). @@ -6747,18 +6369,14 @@ class EventTime(object): def __ne__(self, value): """Return self!=value.""" - def __new__( - cls, - timestamp: int | float | datetime | str, - event_id: int | float | datetime | str | None = None, - ) -> EventTime: + def __new__(cls, timestamp: int | float | datetime | str, event_id: int | float | datetime | str | None = None) -> EventTime: """Create and return a new object. See help(type) for accurate signature.""" def __repr__(self): """Return repr(self).""" @property - def as_tuple(self) -> tuple[int, int]: + def as_tuple(self) -> tuple[int,int]: """ Return this entry as a tuple of (timestamp, event_id), where the timestamp is in milliseconds. @@ -6796,7 +6414,7 @@ class EventTime(object): int: Milliseconds since the Unix epoch. """ -class OptionalEventTime(object): +class OptionalEventTime(object): """ Raphtory’s optional EventTime type. Instances of OptionalEventTime may contain an EventTime, or be empty. This is used for functions that may not return data (such as earliest_time and latest_time) because the data is unavailable. @@ -6893,7 +6511,7 @@ class OptionalEventTime(object): Optional[int]: Milliseconds since the Unix epoch. """ -class History(object): +class History(object): """History of updates for an object. Provides access to time entries and derived views such as timestamps, datetimes, event ids, and intervals.""" def __contains__(self, key): @@ -7044,7 +6662,7 @@ class History(object): HistoryTimestamp: Timestamp (as int) view of this history. """ -class HistoryTimestamp(object): +class HistoryTimestamp(object): """History view that exposes timestamps in milliseconds since the Unix epoch.""" def __contains__(self, key): @@ -7101,7 +6719,7 @@ class HistoryTimestamp(object): NDArray[np.int64]: Timestamps in milliseconds since the Unix epoch in reverse order. """ -class HistoryDateTime(object): +class HistoryDateTime(object): """History view that exposes UTC datetimes.""" def __contains__(self, key): @@ -7167,7 +6785,7 @@ class HistoryDateTime(object): TimeError: If a timestamp cannot be converted to a datetime. """ -class HistoryEventId(object): +class HistoryEventId(object): """History view that exposes event ids of time entries. They are used for ordering within the same timestamp.""" def __contains__(self, key): @@ -7224,7 +6842,7 @@ class HistoryEventId(object): NDArray[np.uintp]: Event ids in reverse order. """ -class Intervals(object): +class Intervals(object): """View over the intervals between consecutive timestamps, expressed in milliseconds.""" def __contains__(self, key): @@ -7313,7 +6931,8 @@ class Intervals(object): Optional[int]: Minimum interval, or None if fewer than 1 interval. """ -class WindowSet(object): +class WindowSet(object): + def __iter__(self): """Implement iter(self).""" @@ -7331,7 +6950,8 @@ class WindowSet(object): Iterable: The time index. """ -class Prop(object): +class Prop(object): + def __eq__(self, value): """Return self==value.""" diff --git a/python/python/raphtory/algorithms/__init__.pyi b/python/python/raphtory/algorithms/__init__.pyi index d1601da21c..2cd84f268d 100644 --- a/python/python/raphtory/algorithms/__init__.pyi +++ b/python/python/raphtory/algorithms/__init__.pyi @@ -1,7 +1,6 @@ """ Algorithmic functions that can be run on Raphtory graphs """ - from __future__ import annotations ############################################################################### @@ -33,60 +32,8 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = [ - "dijkstra_single_source_shortest_paths", - "global_reciprocity", - "betweenness_centrality", - "all_local_reciprocity", - "triplet_count", - "local_triangle_count", - "average_degree", - "directed_graph_density", - "degree_centrality", - "alternating_mask", - "max_degree", - "min_degree", - "max_out_degree", - "max_in_degree", - "min_out_degree", - "min_in_degree", - "pagerank", - "single_source_shortest_path", - "global_clustering_coefficient", - "temporally_reachable_nodes", - "temporal_bipartite_graph_projection", - "local_clustering_coefficient", - "local_clustering_coefficient_batch", - "weakly_connected_components", - "strongly_connected_components", - "in_components", - "in_component", - "out_components", - "out_component", - "fast_rp", - "global_temporal_three_node_motif", - "global_temporal_three_node_motif_multi", - "local_temporal_three_node_motifs", - "hits", - "balance", - "label_propagation", - "k_core", - "temporal_SEIR", - "louvain", - "fruchterman_reingold", - "cohesive_fruchterman_reingold", - "max_weight_matching", - "Matching", - "Infected", -] - -def dijkstra_single_source_shortest_paths( - graph: GraphView, - source: NodeInput, - targets: list[NodeInput], - direction: Direction = "both", - weight: str = "weight", -) -> NodeStateWeightedSP: +__all__ = ['dijkstra_single_source_shortest_paths', 'global_reciprocity', 'betweenness_centrality', 'all_local_reciprocity', 'triplet_count', 'local_triangle_count', 'average_degree', 'directed_graph_density', 'degree_centrality', 'alternating_mask', 'max_degree', 'min_degree', 'max_out_degree', 'max_in_degree', 'min_out_degree', 'min_in_degree', 'pagerank', 'single_source_shortest_path', 'global_clustering_coefficient', 'temporally_reachable_nodes', 'temporal_bipartite_graph_projection', 'local_clustering_coefficient', 'local_clustering_coefficient_batch', 'weakly_connected_components', 'strongly_connected_components', 'in_components', 'in_component', 'out_components', 'out_component', 'fast_rp', 'global_temporal_three_node_motif', 'global_temporal_three_node_motif_multi', 'local_temporal_three_node_motifs', 'hits', 'balance', 'label_propagation', 'k_core', 'temporal_SEIR', 'louvain', 'fruchterman_reingold', 'cohesive_fruchterman_reingold', 'max_weight_matching', 'Matching', 'Infected'] +def dijkstra_single_source_shortest_paths(graph: GraphView, source: NodeInput, targets: list[NodeInput], direction: Direction = "both", weight: str = 'weight') -> NodeStateWeightedSP: """ Finds the shortest paths from a single source to multiple targets in a graph. @@ -116,9 +63,7 @@ def global_reciprocity(graph: GraphView) -> float: float: reciprocity of the graph between 0 and 1. """ -def betweenness_centrality( - graph: GraphView, k: Optional[int] = None, normalized: bool = True -) -> NodeStateF64: +def betweenness_centrality(graph: GraphView, k: Optional[int] = None, normalized: bool = True) -> NodeStateF64: """ Computes the betweenness centrality for nodes in a given graph. @@ -297,14 +242,7 @@ def min_in_degree(graph: GraphView) -> int: int: value of the smallest indegree """ -def pagerank( - graph: GraphView, - iter_count: int = 20, - max_diff: Optional[float] = None, - use_l2_norm: bool = True, - damping_factor: float = 0.85, - weight: Optional[str] = None, -) -> OutputNodeState: +def pagerank(graph: GraphView, iter_count: int = 20, max_diff: Optional[float] = None, use_l2_norm: bool = True, damping_factor: float = 0.85, weight: Optional[str] = None) -> OutputNodeState: """ Pagerank -- pagerank centrality value of the nodes in a graph @@ -326,9 +264,7 @@ def pagerank( OutputNodeState: NodeState mapping nodes to their pagerank score. """ -def single_source_shortest_path( - graph: GraphView, source: NodeInput, cutoff: Optional[int] = None -) -> NodeStateNodes: +def single_source_shortest_path(graph: GraphView, source: NodeInput, cutoff: Optional[int] = None) -> NodeStateNodes: """ Calculates the single source shortest paths from a given source node. @@ -359,13 +295,7 @@ def global_clustering_coefficient(graph: GraphView) -> float: [`Triplet Count`](triplet_count) """ -def temporally_reachable_nodes( - graph: GraphView, - max_hops: int, - start_time: int, - seed_nodes: list[NodeInput], - stop_nodes: Optional[list[NodeInput]] = None, -) -> NodeStateReachability: +def temporally_reachable_nodes(graph: GraphView, max_hops: int, start_time: int, seed_nodes: list[NodeInput], stop_nodes: Optional[list[NodeInput]] = None) -> NodeStateReachability: """ Temporally reachable nodes -- the nodes that are reachable by a time respecting path followed out from a set of seed nodes at a starting time. @@ -384,9 +314,7 @@ def temporally_reachable_nodes( NodeStateReachability: Mapping of nodes to their reachability history. """ -def temporal_bipartite_graph_projection( - graph: GraphView, delta: int, pivot_type: str -) -> Graph: +def temporal_bipartite_graph_projection(graph: GraphView, delta: int, pivot_type: str) -> Graph: """ Projects a temporal bipartite graph into an undirected temporal graph over the pivot node type. Let `G` be a bipartite graph with node types `A` and `B`. Given `delta > 0`, the projection graph `G'` pivoting over type `B` nodes, will make a connection between nodes `n1` and `n2` (of type `A`) at time `(t1 + t2)/2` if they respectively have an edge at time `t1`, `t2` with the same node of type `B` in `G`, and `|t2-t1| < delta`. @@ -414,9 +342,7 @@ def local_clustering_coefficient(graph: GraphView, v: NodeInput) -> float: float: the local clustering coefficient of node v in graph. """ -def local_clustering_coefficient_batch( - graph: GraphView, v: Optional[list[NodeInput]] = None -) -> OutputNodeState: +def local_clustering_coefficient_batch(graph: GraphView, v: Optional[list[NodeInput]] = None) -> OutputNodeState: """ Returns the Local clustering coefficient (batch, intersection) for each specified node in a graph. This measures the degree to which one or multiple nodes in a graph tend to cluster together. @@ -457,9 +383,7 @@ def strongly_connected_components(graph: GraphView) -> OutputNodeState: OutputNodeState: NodeState mapping nodes to their component ids """ -def in_components( - graph: GraphView, filter: Optional[filter.FilterExpr] = None -) -> NodeStateNodes: +def in_components(graph: GraphView, filter: Optional[filter.FilterExpr] = None) -> NodeStateNodes: """ In components -- Finding the "in-component" of a node in a directed graph involves identifying all nodes that can be reached following only incoming edges. @@ -471,9 +395,7 @@ def in_components( NodeStateNodes: Mapping of nodes to the nodes in their 'in-component' """ -def in_component( - node: Node, filter: Optional[filter.FilterExpr] = None -) -> NodeStateUsize: +def in_component(node: Node, filter: Optional[filter.FilterExpr] = None) -> NodeStateUsize: """ In component -- Finding the "in-component" of a node in a directed graph involves identifying all nodes that can be reached following only incoming edges. @@ -485,9 +407,7 @@ def in_component( NodeStateUsize: Mapping of nodes in the in-component to the distance from the starting node. """ -def out_components( - graph: GraphView, filter: Optional[filter.FilterExpr] = None -) -> NodeStateNodes: +def out_components(graph: GraphView, filter: Optional[filter.FilterExpr] = None) -> NodeStateNodes: """ Out components -- Finding the "out-component" of a node in a directed graph involves identifying all nodes that can be reached following only outgoing edges. @@ -499,9 +419,7 @@ def out_components( NodeStateNodes: Mapping of nodes to the nodes within their 'out-component' """ -def out_component( - node: Node, filter: Optional[filter.FilterExpr] = None -) -> NodeStateUsize: +def out_component(node: Node, filter: Optional[filter.FilterExpr] = None) -> NodeStateUsize: """ Out component -- Finding the "out-component" of a node in a directed graph involves identifying all nodes that can be reached following only outgoing edges. @@ -513,14 +431,7 @@ def out_component( NodeStateUsize: A NodeState mapping the nodes in the out-component to their distance from the starting node. """ -def fast_rp( - graph: GraphView, - embedding_dim: int, - normalization_strength: float, - iter_weights: list[float], - seed: Optional[int] = None, - threads: Optional[int] = None, -) -> NodeStateListF64: +def fast_rp(graph: GraphView, embedding_dim: int, normalization_strength: float, iter_weights: list[float], seed: Optional[int] = None, threads: Optional[int] = None) -> NodeStateListF64: """ Computes embedding vectors for each vertex of an undirected/bidirectional graph according to the Fast RP algorithm. Original Paper: https://doi.org/10.48550/arXiv.1908.11512 @@ -536,9 +447,7 @@ def fast_rp( NodeStateListF64: Mapping from nodes to embedding vectors. """ -def global_temporal_three_node_motif( - graph: GraphView, delta: int, threads: Optional[int] = None -) -> list[int]: +def global_temporal_three_node_motif(graph: GraphView, delta: int, threads: Optional[int] = None) -> list[int]: """ Computes the number of three edge, up-to-three node delta-temporal motifs in the graph, using the algorithm of Paranjape et al, Motifs in Temporal Networks (2017). We point the reader to this reference for more information on the algorithm and background, but provide a short summary below. @@ -587,9 +496,7 @@ def global_temporal_three_node_motif( """ -def global_temporal_three_node_motif_multi( - graph: GraphView, deltas: list[int], threads: Optional[int] = None -) -> list[list[int]]: +def global_temporal_three_node_motif_multi(graph: GraphView, deltas: list[int], threads: Optional[int] = None) -> list[list[int]]: """ Computes the global counts of three-edge up-to-three node temporal motifs for a range of timescales. See `global_temporal_three_node_motif` for an interpretation of each row returned. @@ -602,9 +509,7 @@ def global_temporal_three_node_motif_multi( list[list[int]]: A list of 40d arrays, each array is the motif count for a particular value of delta, returned in the order that the deltas were given as input. """ -def local_temporal_three_node_motifs( - graph: GraphView, delta: int, threads: Optional[int] = None -) -> NodeStateMotifs: +def local_temporal_three_node_motifs(graph: GraphView, delta: int, threads: Optional[int] = None) -> NodeStateMotifs: """ Computes the number of each type of motif that each node participates in. See global_temporal_three_node_motifs for a summary of the motifs involved. @@ -621,9 +526,7 @@ def local_temporal_three_node_motifs( the motif. For two node motifs, both constituent nodes count the motif. For triangles, all three constituent nodes count the motif. """ -def hits( - graph: GraphView, iter_count: int = 20, threads: Optional[int] = None -) -> NodeStateHits: +def hits(graph: GraphView, iter_count: int = 20, threads: Optional[int] = None) -> NodeStateHits: """ HITS (Hubs and Authority) Algorithm: @@ -642,9 +545,7 @@ def hits( NodeStateHits: A mapping from nodes their hub and authority scores """ -def balance( - graph: GraphView, name: str = "weight", direction: Direction = "both" -) -> NodeStateF64: +def balance(graph: GraphView, name: str = "weight", direction: Direction = "both") -> NodeStateF64: """ Sums the weights of edges in the graph based on the specified direction. @@ -663,9 +564,7 @@ def balance( """ -def label_propagation( - graph: GraphView, iter_count: int = 20, seed: Optional[bytes] = None -) -> OutputNodeState: +def label_propagation(graph: GraphView, iter_count: int = 20, seed: Optional[bytes] = None) -> OutputNodeState: """ Computes components using a label propagation algorithm @@ -679,9 +578,7 @@ def label_propagation( """ -def k_core( - graph: GraphView, k: int, iter_count: int, threads: Optional[int] = None -) -> list[Node]: +def k_core(graph: GraphView, k: int, iter_count: int, threads: Optional[int] = None) -> list[Node]: """ Determines which nodes are in the k-core for a given value of k @@ -696,15 +593,7 @@ def k_core( """ -def temporal_SEIR( - graph: GraphView, - seeds: int | float | list[NodeInput], - infection_prob: float, - initial_infection: int | str | datetime, - recovery_rate: float | None = None, - incubation_rate: float | None = None, - rng_seed: int | None = None, -) -> NodeStateSEIR: +def temporal_SEIR(graph: GraphView, seeds: int | float | list[NodeInput], infection_prob: float, initial_infection: int | str | datetime, recovery_rate: float | None = None, incubation_rate: float | None = None, rng_seed: int | None = None) -> NodeStateSEIR: """ Simulate an SEIR dynamic on the network @@ -734,13 +623,7 @@ def temporal_SEIR( """ -def louvain( - graph: GraphView, - resolution: float = 1.0, - weight_prop: str | None = None, - tol: None | float = None, - rng_seed: int | None = None, -) -> NodeStateUsize: +def louvain(graph: GraphView, resolution: float = 1.0, weight_prop: str | None = None, tol: None | float = None, rng_seed: int | None = None) -> NodeStateUsize: """ Louvain algorithm for community detection @@ -755,14 +638,7 @@ def louvain( NodeStateUsize: Mapping of nodes to their community assignment """ -def fruchterman_reingold( - graph: GraphView, - iterations: int | None = 100, - scale: float | None = 1.0, - node_start_size: float | None = 1.0, - cooloff_factor: float | None = 0.95, - dt: float | None = 0.1, -) -> NodeLayout: +def fruchterman_reingold(graph: GraphView, iterations: int | None = 100, scale: float | None = 1.0, node_start_size: float | None = 1.0, cooloff_factor: float | None = 0.95, dt: float | None = 0.1) -> NodeLayout: """ Fruchterman Reingold layout algorithm @@ -778,14 +654,7 @@ def fruchterman_reingold( NodeLayout: A mapping from nodes to their [x, y] positions """ -def cohesive_fruchterman_reingold( - graph: GraphView, - iter_count: int = 100, - scale: float = 1.0, - node_start_size: float = 1.0, - cooloff_factor: float = 0.95, - dt: float = 0.1, -) -> NodeLayout: +def cohesive_fruchterman_reingold(graph: GraphView, iter_count: int = 100, scale: float = 1.0, node_start_size: float = 1.0, cooloff_factor: float = 0.95, dt: float = 0.1) -> NodeLayout: """ Cohesive version of `fruchterman_reingold` that adds virtual edges between isolated nodes Arguments: @@ -801,12 +670,7 @@ def cohesive_fruchterman_reingold( """ -def max_weight_matching( - graph: GraphView, - weight_prop: Optional[str] = None, - max_cardinality: bool = True, - verify_optimum_flag: bool = False, -) -> Matching: +def max_weight_matching(graph: GraphView, weight_prop: Optional[str] = None, max_cardinality: bool = True, verify_optimum_flag: bool = False) -> Matching: """ Compute a maximum-weighted matching in the general undirected weighted graph given by "edges". If `max_cardinality` is true, only @@ -843,7 +707,7 @@ def max_weight_matching( Matching: The matching """ -class Matching(object): +class Matching(object): """A Matching (i.e., a set of edges that do not share any nodes)""" def __bool__(self): @@ -915,7 +779,8 @@ class Matching(object): """ -class Infected(object): +class Infected(object): + def __repr__(self): """Return repr(self).""" diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 36ba87efb2..5540ce1bfc 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -29,24 +29,9 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = [ - "FilterExpr", - "FilterOps", - "PropertyFilterOps", - "Node", - "NodeIdFilterBuilder", - "NodeNameFilterBuilder", - "NodeTypeFilterBuilder", - "Edge", - "EdgeEndpoint", - "EdgeEndpointIdFilter", - "EdgeEndpointNameFilter", - "EdgeEndpointTypeFilter", - "ExplodedEdge", - "Graph", -] +__all__ = ['FilterExpr', 'Node', 'Edge', 'EdgeEndpoint', 'ExplodedEdge', 'Graph', 'Node', 'Edge', 'ExplodedEdge', 'Graph'] +class FilterExpr(object): -class FilterExpr(object): def __and__(self, value): """Return self&value.""" @@ -62,1733 +47,62 @@ class FilterExpr(object): def __ror__(self, value): """Return value|self.""" -class FilterOps(object): +class EdgeEndpoint(object): """ - Builds property filter expressions. - - This object represents “a property access” plus optional list/aggregate - qualifiers (e.g. `first`, `len`, `sum`) and can emit a `filter.FilterExpr` via - comparisons such as `==`, `<`, `is_in`, etc. - - Returned expressions can be combined with `&`, `|`, and `~` at the - `filter.FilterExpr` level (where supported). - """ - - def __eq__(self, value): - """Return self==value.""" - - def __ge__(self, value): - """Return self>=value.""" - - def __gt__(self, value): - """Return self>value.""" - - def __le__(self, value): - """Return self<=value.""" - - def __lt__(self, value): - """Return self filter.PropertyFilterOps: - """ - Requires that **all** elements match when the underlying property is list-like. - - Returns: - filter.PropertyFilterOps: - """ - - def any(self) -> filter.PropertyFilterOps: - """ - Requires that **any** element matches when the underlying property is list-like. - - Returns: - filter.PropertyFilterOps: - """ - - def avg(self) -> filter.PropertyFilterOps: - """ - Averages list elements when the underlying property is numeric and list-like. - - Returns: - filter.PropertyFilterOps: - """ - - def contains(self, value: Prop) -> filter.FilterExpr: - """ - Checks whether the property's string representation contains the given value. - - Arguments: - value (Prop): Substring that must appear within the value. - - Returns: - filter.FilterExpr: A filter expression evaluating substring search. - """ - - def ends_with(self, value: Prop) -> filter.FilterExpr: - """ - Checks whether the property's string representation ends with the given value. - - Arguments: - value (Prop): Suffix to check for. - - Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. - """ - - def first(self) -> filter.PropertyFilterOps: - """ - Selects the first element when the underlying property is list-like. - - Returns: - filter.PropertyFilterOps: - """ - - def fuzzy_search( - self, prop_value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: - """ - Performs fuzzy matching against the property's string value. - - Uses a specified Levenshtein distance and optional prefix matching. - - Arguments: - prop_value (str): String to approximately match against. - levenshtein_distance (int): Maximum allowed Levenshtein distance. - prefix_match (bool): Whether to require a matching prefix. - - Returns: - filter.FilterExpr: A filter expression performing approximate text matching. - """ - - def is_in(self, values: list[Prop]) -> filter.FilterExpr: - """ - Checks whether the property is contained within the specified iterable of values. - - Arguments: - values (list[Prop]): Iterable of property values to match against. - - Returns: - filter.FilterExpr: A filter expression evaluating membership. - """ - - def is_none(self) -> filter.FilterExpr: - """ - Checks whether the property value is `None` / missing. - - Returns: - filter.FilterExpr: A filter expression evaluating `value is None`. - """ - - def is_not_in(self, values: list[Prop]) -> filter.FilterExpr: - """ - Checks whether the property is **not** contained within the specified iterable of values. - - Arguments: - values (list[Prop]): Iterable of property values to exclude. - - Returns: - filter.FilterExpr: A filter expression evaluating non-membership. - """ - - def is_some(self) -> filter.FilterExpr: - """ - Checks whether the property value is present (not `None`). - - Returns: - filter.FilterExpr: A filter expression evaluating `value is not None`. - """ - - def last(self) -> filter.PropertyFilterOps: - """ - Selects the last element when the underlying property is list-like. - - Returns: - filter.PropertyFilterOps: - """ - - def len(self) -> filter.PropertyFilterOps: - """ - Returns the list length when the underlying property is list-like. - - Returns: - filter.PropertyFilterOps: - """ - - def max(self) -> filter.PropertyFilterOps: - """ - Returns the maximum list element when the underlying property is list-like. - - Returns: - filter.PropertyFilterOps: - """ - - def min(self) -> filter.PropertyFilterOps: - """ - Returns the minimum list element when the underlying property is list-like. - - Returns: - filter.PropertyFilterOps: - """ - - def not_contains(self, value: Prop) -> filter.FilterExpr: - """ - Checks whether the property's string representation **does not** contain the given value. - - Arguments: - value (Prop): Substring that must not appear within the value. - - Returns: - filter.FilterExpr: A filter expression evaluating substring exclusion. - """ - - def starts_with(self, value: Prop) -> filter.FilterExpr: - """ - Checks whether the property's string representation starts with the given value. - - Arguments: - value (Prop): Prefix to check for. - - Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. - """ - - def sum(self) -> filter.PropertyFilterOps: - """ - Sums list elements when the underlying property is numeric and list-like. - - Returns: - filter.PropertyFilterOps: - """ - -class PropertyFilterOps(FilterOps): - """ - Builds property filter expressions with access to temporal qualifiers. - - Exported as: `filter.PropertyFilterOps` - - This extends `FilterOps` and provides `.temporal()` to explicitly select - temporal property evaluation semantics (where supported by the query context). - """ - - def temporal(self) -> filter.FilterOps: - """ - Selects temporal evaluation for the property. - - Returns: - filter.FilterOps: A property expression builder operating on temporal values. - """ - -class Node(object): - """ - Constructs node filter expressions. - - Each method returns either: - - a field-specific filter builder, or - - a view-restricted filter context, or - - a boolean predicate over node state. - """ - - @staticmethod - def after(time: int) -> filter.NodeViewPropsFilterBuilder: - """ - Restricts node evaluation to times strictly after the given time. - - Arguments: - time (int): Lower time bound. - - Returns: - filter.NodeViewPropsFilterBuilder: - """ - - @staticmethod - def at(time: int) -> filter.NodeViewPropsFilterBuilder: - """ - Restricts node evaluation to a single point in time. - - Arguments: - time (int): Event time. - - Returns: - filter.NodeViewPropsFilterBuilder: - """ - - @staticmethod - def before(time: int) -> filter.NodeViewPropsFilterBuilder: - """ - Restricts node evaluation to times strictly before the given time. - - Arguments: - time (int): Upper time bound. - - Returns: - filter.NodeViewPropsFilterBuilder: - """ - - @staticmethod - def by_state_column(state: OutputNodeState, col: str) -> filter.FilterExpr: - """ - Build a node filter from a boolean column of an existing node-state result. - - Arguments: - state (OutputNodeState): A pre-computed node state (e.g. from an algorithm). - col (str): Name of the boolean column on `state` whose values determine inclusion. - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def degree() -> filter.FilterOps: - """ - Selects total node degree for filtering. - - Returns: - filter.FilterOps: a builder that selects the node degree for filtering. - """ - - @staticmethod - def id() -> filter.NodeIdFilterBuilder: - """ - Selects the node ID field for filtering. - - Returns: - filter.NodeIdFilterBuilder: - """ - - @staticmethod - def in_degree() -> filter.FilterOps: - """ - Selects incoming node degree for filtering. - - Returns: - filter.FilterOps: a builder that selects the node degree for filtering. - """ - - @staticmethod - def is_active() -> filter.FilterExpr: - """ - Matches nodes that have at least one event in the current view. - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def latest() -> filter.NodeViewPropsFilterBuilder: - """ - Evaluates filters against the latest available state of each node. - - Returns: - filter.NodeViewPropsFilterBuilder: - """ - - @staticmethod - def layer(layer: str) -> filter.NodeViewPropsFilterBuilder: - """ - Restricts evaluation to nodes belonging to the given layer. - - Arguments: - layer (str): Layer name. - - Returns: - filter.NodeViewPropsFilterBuilder: - """ - - @staticmethod - def layers(layers: list[str]) -> filter.NodeViewPropsFilterBuilder: - """ - Restricts evaluation to nodes belonging to any of the given layers. - - Arguments: - layers (list[str]): Layer names. - - Returns: - filter.NodeViewPropsFilterBuilder: - """ - - @staticmethod - def metadata(name: str) -> filter.FilterOps: - """ - Filters a node metadata field by name. - - Metadata is shared across all temporal versions of a node. - - Arguments: - name (str): Metadata key. - - Returns: - filter.FilterOps: - """ - - @staticmethod - def name() -> filter.NodeNameFilterBuilder: - """ - Selects the node name field for filtering. - - Returns: - filter.NodeNameFilterBuilder: - """ - - @staticmethod - def node_type() -> filter.NodeTypeFilterBuilder: - """ - Selects the node type field for filtering. - - Returns: - filter.NodeTypeFilterBuilder: - """ - - @staticmethod - def out_degree() -> filter.FilterOps: - """ - Selects outgoing node degree for filtering. - - Returns: - filter.FilterOps: a builder that selects the node degree for filtering. - """ - - @staticmethod - def property(name: str) -> filter.PropertyFilterOps: - """ - Filters a node property by name. - - The property may be static or temporal depending on the query context. - - Arguments: - name (str): Property key. - - Returns: - filter.PropertyFilterOps: - """ - - @staticmethod - def snapshot_at(time: int) -> filter.NodeViewPropsFilterBuilder: - """ - Evaluates filters against a snapshot of the graph at a given time. - - Arguments: - time (int): Snapshot time. - - Returns: - filter.NodeViewPropsFilterBuilder: - """ - - @staticmethod - def snapshot_latest() -> filter.NodeViewPropsFilterBuilder: - """ - Evaluates filters against the most recent snapshot of the graph. - - Returns: - filter.NodeViewPropsFilterBuilder: - """ - - @staticmethod - def window(start: int, end: int) -> filter.NodeViewPropsFilterBuilder: - """ - Restricts node evaluation to the given time window. - - The window is inclusive of `start` and exclusive of `end`. - - Arguments: - start (int): Start time. - end (int): End time. - - Returns: - filter.NodeViewPropsFilterBuilder: - """ - -class NodeIdFilterBuilder(object): - """ - Filters nodes by their ID value. - - Supports numeric and string IDs and produces a `FilterExpr` - that can be used in node queries. - - Examples: - Node.id() == 1 - Node.id().is_in([1, 2, 3]) - Node.id().starts_with("user:") - """ - - def __eq__(self, value): - """Return self==value.""" - - def __ge__(self, value): - """Return self>=value.""" - - def __gt__(self, value): - """Return self>value.""" - - def __le__(self, value): - """Return self<=value.""" - - def __lt__(self, value): - """Return self filter.FilterExpr: - """ - Returns a filter expression that checks whether the string - representation of the node ID contains the given substring. - - Arguments: - value (str): Substring that must appear within the value. - - Returns: - filter.FilterExpr: A filter expression evaluating substring search. - """ - - def ends_with(self, value: str) -> filter.FilterExpr: - """ - Returns a filter expression that checks whether the string - representation of the node ID ends with the given suffix. - - Arguments: - value (str): Suffix to check for. - - Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. - """ - - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: - """ - Returns a filter expression that performs fuzzy matching - against the string representation of the node ID. - - 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. - """ - - def is_in(self, values: list[int]) -> filter.FilterExpr: - """ - Returns a filter expression that checks whether the node ID - is contained within the specified iterable of IDs. - - Arguments: - values (list[int]): Iterable of node IDs to match against. - - Returns: - filter.FilterExpr: A filter expression evaluating membership. - """ - - def is_not_in(self, values: list[int]) -> filter.FilterExpr: - """ - Returns a filter expression that checks whether the node ID - is **not** contained within the specified iterable of IDs. - - Arguments: - values (list[int]): Iterable of node IDs to exclude. - - Returns: - filter.FilterExpr: A filter expression evaluating non-membership. - """ - - def not_contains(self, value: str) -> filter.FilterExpr: - """ - Returns a filter expression that checks whether the string - representation of the node ID **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. - """ - - def starts_with(self, value: str) -> filter.FilterExpr: - """ - Returns a filter expression that checks whether the string - representation of the node ID starts with the given prefix. - - Arguments: - value (str): Prefix to check for. - - Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. - """ - -class NodeNameFilterBuilder(object): - """ - Filters nodes by their name. - - Comparisons are performed on the node's string name. - - Examples: - Node.name() == "alice" - Node.name().contains("ali") - """ - - def __eq__(self, value): - """Return self==value.""" - - def __ge__(self, value): - """Return self>=value.""" - - def __gt__(self, value): - """Return self>value.""" - - def __le__(self, value): - """Return self<=value.""" - - def __lt__(self, value): - """Return self filter.FilterExpr: - """ - 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. - """ - - def ends_with(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: - """ - 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. - """ - - def is_in(self, values: list[str]) -> filter.FilterExpr: - """ - 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. - """ - - def is_not_in(self, values: list[str]) -> filter.FilterExpr: - """ - 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. - """ - - def not_contains(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - - def starts_with(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - -class NodeTypeFilterBuilder(object): - """ - Filters nodes by their node type. - - The node type corresponds to the optional type assigned at node creation. - - Examples: - Node.node_type() == "fire_nation" - Node.node_type().is_not_in(["air_nomads"]) - """ - - def __eq__(self, value): - """Return self==value.""" - - def __ge__(self, value): - """Return self>=value.""" - - def __gt__(self, value): - """Return self>value.""" - - def __le__(self, value): - """Return self<=value.""" - - def __lt__(self, value): - """Return self filter.FilterExpr: - """ - 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. - """ - - def ends_with(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: - """ - 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. - """ - - def is_in(self, values: list[str]) -> filter.FilterExpr: - """ - 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. - """ - - def is_not_in(self, values: list[str]) -> filter.FilterExpr: - """ - 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. - """ - - def not_contains(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - - def starts_with(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - -class Edge(object): - """ - Entry point for constructing edge filter expressions. - - The `Edge` filter provides: - - endpoint filters via `src()` and `dst()`, - - property and metadata filters, - - view restrictions (time windows, snapshots, layers), - - and structural predicates over edge state (active/valid/deleted/self-loop). - - Examples: - Edge.src().id() == 1 - Edge.property("weight") > 0.5 - Edge.window(0, 10).is_active() - Edge.layer("fire_nation").is_valid() - """ - - @staticmethod - def after(time: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts edge evaluation to times strictly after the given time. - - Arguments: - time (int): Lower time bound. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def at(time: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts edge evaluation to a single point in time. - - Arguments: - time (int): Event time. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def before(time: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts edge evaluation to times strictly before the given time. - - Arguments: - time (int): Upper time bound. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def dst() -> filter.EdgeEndpoint: - """ - Selects the edge **destination endpoint** for filtering. - - Returns: - filter.EdgeEndpoint: - """ - - @staticmethod - def is_active() -> filter.FilterExpr: - """ - Matches edges that have at least one event in the current view. - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def is_deleted() -> filter.FilterExpr: - """ - Matches edges that have been deleted. - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def is_self_loop() -> filter.FilterExpr: - """ - Matches edges that are self-loops (source == destination). - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def is_valid() -> filter.FilterExpr: - """ - Matches edges that are structurally valid in the current view. - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def latest() -> filter.EdgeViewPropsFilterBuilder: - """ - Evaluates edge predicates against the latest available edge state. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def layer(layer: str) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts evaluation to edges belonging to the given layer. - - Arguments: - layer (str): Layer name. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def layers(layers: list[str]) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts evaluation to edges belonging to any of the given layers. - - Arguments: - layers (list[str]): Layer names. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def metadata(name: str) -> filter.FilterOps: - """ - Filters an edge metadata field by name. - - Metadata is shared across all temporal versions of an edge. - - Arguments: - name (str): Metadata key. - - Returns: - filter.FilterOps: - """ - - @staticmethod - def property(name: str) -> filter.PropertyFilterOps: - """ - Filters an edge property by name. - - The property may be static or temporal depending on the query context. - - Arguments: - name (str): Property key. - - Returns: - filter.PropertyFilterOps: - """ - - @staticmethod - def snapshot_at(time: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Evaluates edge predicates against a snapshot of the graph at a given time. - - Arguments: - time (int): Snapshot time. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def snapshot_latest() -> filter.EdgeViewPropsFilterBuilder: - """ - Evaluates edge predicates against the most recent snapshot of the graph. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def src() -> filter.EdgeEndpoint: - """ - Selects the edge **source endpoint** for filtering. - - Returns: - filter.EdgeEndpoint: - """ - - @staticmethod - def window(start: int, end: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts edge evaluation to the given time window. - - The window is inclusive of `start` and exclusive of `end`. - - Arguments: - start (int): Start time. - end (int): End time. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - -class EdgeEndpoint(object): - """ - Entry point for filtering an edge endpoint (source or destination). - - An `EdgeEndpoint` is obtained from `Edge.src()` or `Edge.dst()` and allows - you to filter on endpoint fields (id, name, type) as well as endpoint - properties and metadata. - - Examples: - Edge.src().id() == 1 - Edge.dst().name().starts_with("user:") - Edge.src().property("country") == "UK" - """ - - def id(self) -> filter.EdgeEndpointIdFilter: - """ - Selects the endpoint node ID field for filtering. - - Returns: - filter.EdgeEndpointIdFilter: - """ - - def metadata(self, name: str) -> filter.FilterOps: - """ - Filters an endpoint node metadata field by name. - - Metadata is shared across all temporal versions of a node. - - Arguments: - name (str): Metadata key. - - Returns: - filter.FilterOps: - """ - - def name(self) -> filter.EdgeEndpointNameFilter: - """ - Selects the endpoint node name field for filtering. - - Returns: - filter.EdgeEndpointNameFilter: - """ - - def node_type(self) -> filter.EdgeEndpointTypeFilter: - """ - Selects the endpoint node type field for filtering. - - Returns: - filter.EdgeEndpointTypeFilter: - """ - - def property(self, name: str) -> filter.PropertyFilterOps: - """ - Filters an endpoint node property by name. - - The property may be static or temporal depending on the query context. - - Arguments: - name (str): Property key. - - Returns: - filter.PropertyFilterOps: - """ - -class EdgeEndpointIdFilter(object): - """ - Filters an edge endpoint by its node ID. - - This builder produces `FilterExpr` predicates over the **source** or - **destination** endpoint of an edge (depending on where it was obtained). - - Examples: - Edge.src().id() == 1 - Edge.dst().id().is_in([1, 2, 3]) - Edge.src().id().starts_with("user:") - """ - - def __eq__(self, value): - """Return self==value.""" - - def __ge__(self, value): - """Return self>=value.""" - - def __gt__(self, value): - """Return self>value.""" - - def __le__(self, value): - """Return self<=value.""" - - def __lt__(self, value): - """Return self filter.FilterExpr: - """ - Checks whether the string representation of the endpoint ID contains the given substring. - - Arguments: - value (str): Substring to search for. - - Returns: - filter.FilterExpr: A filter expression evaluating substring search. - """ - - def ends_with(self, value: str) -> filter.FilterExpr: - """ - Checks whether the string representation of the endpoint ID ends with the given suffix. - - Arguments: - value (str): Suffix to check for. - - Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. - """ - - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: - """ - Performs fuzzy matching against the string representation of the endpoint ID. - - Uses a specified Levenshtein distance and optional prefix matching. - - Arguments: - value (str): String to approximately match against. - levenshtein_distance (int): Maximum allowed Levenshtein distance. - prefix_match (bool): Whether to require a matching prefix. - - Returns: - filter.FilterExpr: A filter expression performing approximate text matching. - """ - - def is_in(self, values: list[int]) -> filter.FilterExpr: - """ - Checks whether the endpoint ID is contained within the specified iterable of IDs. - - Arguments: - values (list[int]): Iterable of node IDs to match against. - - Returns: - filter.FilterExpr: A filter expression evaluating membership. - """ - - def is_not_in(self, values: list[int]) -> filter.FilterExpr: - """ - Checks whether the endpoint ID is **not** contained within the specified iterable of IDs. - - Arguments: - values (list[int]): Iterable of node IDs to exclude. - - Returns: - filter.FilterExpr: A filter expression evaluating non-membership. - """ - - def not_contains(self, value: str) -> filter.FilterExpr: - """ - Checks whether the string representation of the endpoint ID **does not** contain the given substring. - - Arguments: - value (str): Substring to exclude. - - Returns: - filter.FilterExpr: A filter expression evaluating substring exclusion. - """ - - def starts_with(self, value: str) -> filter.FilterExpr: - """ - Checks whether the string representation of the endpoint ID starts with the given prefix. - - Arguments: - value (str): Prefix to check for. - - Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. - """ - -class EdgeEndpointNameFilter(object): - """ - Filters an edge endpoint by its node name. - - This builder produces `FilterExpr` predicates over the **source** or - **destination** endpoint node name. - - Examples: - Edge.src().name() == "alice" - Edge.dst().name().contains("ali") - """ - - def __eq__(self, value): - """Return self==value.""" - - def __ge__(self, value): - """Return self>=value.""" - - def __gt__(self, value): - """Return self>value.""" - - def __le__(self, value): - """Return self<=value.""" - - def __lt__(self, value): - """Return self filter.FilterExpr: - """ - 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. - """ - - def ends_with(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: - """ - 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. - """ - - def is_in(self, values: list[str]) -> filter.FilterExpr: - """ - 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. - """ - - def is_not_in(self, values: list[str]) -> filter.FilterExpr: - """ - 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. - """ - - def not_contains(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - - def starts_with(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - -class EdgeEndpointTypeFilter(object): - """ - Filters an edge endpoint by its node type. - - This builder produces `FilterExpr` predicates over the **source** or - **destination** endpoint node type. - - Examples: - Edge.src().node_type() == "fire_nation" - Edge.dst().node_type().is_not_in(["air_nomads"]) - """ - - def __eq__(self, value): - """Return self==value.""" - - def __ge__(self, value): - """Return self>=value.""" - - def __gt__(self, value): - """Return self>value.""" - - def __le__(self, value): - """Return self<=value.""" - - def __lt__(self, value): - """Return self filter.FilterExpr: - """ - 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. - """ - - def ends_with(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: - """ - 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. - """ - - def is_in(self, values: list[str]) -> filter.FilterExpr: - """ - 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. - """ - - def is_not_in(self, values: list[str]) -> filter.FilterExpr: - """ - 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. - """ - - def not_contains(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - - def starts_with(self, value: str) -> filter.FilterExpr: - """ - 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. - """ - -class ExplodedEdge(object): - """ - Entry point for constructing **exploded edge** filter expressions. - - An **exploded edge** represents an edge view where temporal events are treated - as individually addressable edge instances (i.e. “event-level” edges), rather - than a single aggregated edge across time. + Entry point for filtering an edge endpoint (source or destination). - This filter provides: - - property and metadata filters, - - view restrictions (time windows, snapshots, layers), - - and structural predicates over exploded edge state (active/valid/deleted/self-loop). + An `EdgeEndpoint` is obtained from `Edge.src()` or `Edge.dst()` and allows + you to filter on endpoint fields (id, name, type) as well as endpoint + properties and metadata. Examples: - ExplodedEdge.property("weight") > 0.5 - ExplodedEdge.window(0, 10).is_active() - ExplodedEdge.layer("fire_nation").is_valid() + Edge.src().id() == 1 + Edge.dst().name().starts_with("user:") + Edge.src().property("country") == "UK" """ - @staticmethod - def after(time: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts exploded edge evaluation to times strictly after the given time. - - Arguments: - time (int): Lower time bound. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def at(time: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts exploded edge evaluation to a single point in time. - - Arguments: - time (int): Event time. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def before(time: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts exploded edge evaluation to times strictly before the given time. - - Arguments: - time (int): Upper time bound. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def is_active() -> filter.FilterExpr: - """ - Matches exploded edges that have at least one event in the current view. - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def is_deleted() -> filter.FilterExpr: - """ - Matches exploded edges that have been deleted. - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def is_self_loop() -> filter.FilterExpr: - """ - Matches exploded edges that are self-loops (source == destination). - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def is_valid() -> filter.FilterExpr: - """ - Matches exploded edges that are structurally valid in the current view. - - Returns: - filter.FilterExpr: - """ - - @staticmethod - def latest() -> filter.EdgeViewPropsFilterBuilder: - """ - Evaluates exploded edge predicates against the latest available state. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def layer(layer: str) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts evaluation to exploded edges belonging to the given layer. - - Arguments: - layer (str): Layer name. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def layers(layers: list[str]) -> filter.EdgeViewPropsFilterBuilder: + def id(self) -> filter.Expr: """ - Restricts evaluation to exploded edges belonging to any of the given layers. - - Arguments: - layers (list[str]): Layer names. + Selects the endpoint node ID field for filtering. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.Expr: """ - @staticmethod - def metadata(name: str) -> filter.FilterOps: + def metadata(self, name: str) -> filter.Expr: """ - Filters an exploded edge metadata field by name. - - Metadata is shared across all temporal versions of an exploded edge. + Filters an endpoint node metadata field by name. Arguments: name (str): Metadata key. Returns: - filter.FilterOps: - """ - - @staticmethod - def property(name: str) -> filter.PropertyFilterOps: - """ - Filters an exploded edge property by name. - - The property may be static or temporal depending on the query context. - - Arguments: - name (str): Property key. - - Returns: - filter.PropertyFilterOps: - """ - - @staticmethod - def snapshot_at(time: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Evaluates exploded edge predicates against a snapshot of the graph at a given time. - - Arguments: - time (int): Snapshot time. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def snapshot_latest() -> filter.EdgeViewPropsFilterBuilder: - """ - Evaluates exploded edge predicates against the most recent snapshot of the graph. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - - @staticmethod - def window(start: int, end: int) -> filter.EdgeViewPropsFilterBuilder: - """ - Restricts exploded edge evaluation to the given time window. - - The window is inclusive of `start` and exclusive of `end`. - - Arguments: - start (int): Start time. - end (int): End time. - - Returns: - filter.EdgeViewPropsFilterBuilder: - """ - -class Graph(object): - """ - Entry point for constructing **graph-level view filters**. - - The `Graph` filter restricts *when* and *where* the graph is evaluated, - independent of node or edge predicates. It defines the **temporal scope** - (windows, snapshots, latest state) and **layer scope** for subsequent - node and edge filters. - - All methods are static and return a `ViewFilterBuilder`, which can then - be refined further or combined with node/edge predicates. - - Examples: - Graph.window(0, 10) - Graph.at(5) - Graph.latest().layer("fire_nation") - Graph.layers(["A", "B"]).snapshot_latest() - """ - - @staticmethod - def after(time: int) -> filter.ViewFilterBuilder: - """ - Restricts evaluation to times strictly after the given time. - - Arguments: - time (int): Lower time bound. - - Returns: - filter.ViewFilterBuilder: - """ - - @staticmethod - def at(time: int) -> filter.ViewFilterBuilder: - """ - Restricts evaluation to a single point in time. - - Arguments: - time (int): Event time. - - Returns: - filter.ViewFilterBuilder: - """ - - @staticmethod - def before(time: int) -> filter.ViewFilterBuilder: - """ - Restricts evaluation to times strictly before the given time. - - Arguments: - time (int): Upper time bound. - - Returns: - filter.ViewFilterBuilder: - """ - - @staticmethod - def latest() -> filter.ViewFilterBuilder: - """ - Evaluates filters against the latest available state of the graph. - - Returns: - filter.ViewFilterBuilder: - """ - - @staticmethod - def layer(layer: str) -> filter.ViewFilterBuilder: - """ - Restricts evaluation to a single layer. - - Arguments: - layer (str): Layer name. - - Returns: - filter.ViewFilterBuilder: - """ - - @staticmethod - def layers(layers: list[str]) -> filter.ViewFilterBuilder: - """ - Restricts evaluation to any of the given layers. - - Arguments: - layers (list[str]): Layer names. - - Returns: - filter.ViewFilterBuilder: + filter.Expr: """ - @staticmethod - def snapshot_at(time: int) -> filter.ViewFilterBuilder: + def name(self) -> filter.Expr: """ - Evaluates filters against a snapshot of the graph at a given time. - - Arguments: - time (int): Snapshot time. + Selects the endpoint node name field for filtering. Returns: - filter.ViewFilterBuilder: + filter.Expr: """ - @staticmethod - def snapshot_latest() -> filter.ViewFilterBuilder: + def node_type(self) -> filter.Expr: """ - Evaluates filters against the most recent snapshot of the graph. + Selects the endpoint node type field for filtering. Returns: - filter.ViewFilterBuilder: + filter.Expr: """ - @staticmethod - def window(start: int, end: int) -> filter.ViewFilterBuilder: + def property(self, name: str) -> filter.PropertyExpr: """ - Restricts evaluation to events within a time window. - - The window is inclusive of `start` and exclusive of `end`. + Filters an endpoint node property by name. Arguments: - start (int): Start time. - end (int): End time. + name (str): Property key. Returns: - filter.ViewFilterBuilder: + filter.PropertyExpr: """ diff --git a/python/python/raphtory/graph_gen/__init__.pyi b/python/python/raphtory/graph_gen/__init__.pyi index c4daeb00b3..931f67117f 100644 --- a/python/python/raphtory/graph_gen/__init__.pyi +++ b/python/python/raphtory/graph_gen/__init__.pyi @@ -1,7 +1,6 @@ """ Generate Raphtory graphs from attachment models """ - from __future__ import annotations ############################################################################### @@ -34,11 +33,8 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = ["random_attachment", "ba_preferential_attachment"] - -def random_attachment( - g: Graph, nodes_to_add: int, edges_per_step: int, seed: Optional[list[int]] = None -) -> None: +__all__ = ['random_attachment', 'ba_preferential_attachment'] +def random_attachment(g: Graph, nodes_to_add: int, edges_per_step: int, seed: Optional[list[int]] = None) -> None: """ Generates a graph using the random attachment model @@ -56,9 +52,7 @@ def random_attachment( None: """ -def ba_preferential_attachment( - g: Graph, nodes_to_add: int, edges_per_step: int, seed: Optional[list[int]] = None -) -> None: +def ba_preferential_attachment(g: Graph, nodes_to_add: int, edges_per_step: int, seed: Optional[list[int]] = None) -> None: """ Generates a graph using the preferential attachment model. diff --git a/python/python/raphtory/graph_loader/__init__.pyi b/python/python/raphtory/graph_loader/__init__.pyi index ed9ab67785..269bd52748 100644 --- a/python/python/raphtory/graph_loader/__init__.pyi +++ b/python/python/raphtory/graph_loader/__init__.pyi @@ -1,7 +1,6 @@ """ Load and save Raphtory graphs from/to file(s) """ - from __future__ import annotations ############################################################################### @@ -34,15 +33,7 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = [ - "lotr_graph", - "lotr_graph_with_props", - "stable_coin_graph", - "reddit_hyperlink_graph", - "reddit_hyperlink_graph_local", - "karate_club_graph", -] - +__all__ = ['lotr_graph', 'lotr_graph_with_props', 'stable_coin_graph', 'reddit_hyperlink_graph', 'reddit_hyperlink_graph_local', 'karate_club_graph'] def lotr_graph() -> Graph: """ Load the Lord of the Rings dataset into a graph. diff --git a/python/python/raphtory/graphql/__init__.pyi b/python/python/raphtory/graphql/__init__.pyi index 47d04fa376..188bd74587 100644 --- a/python/python/raphtory/graphql/__init__.pyi +++ b/python/python/raphtory/graphql/__init__.pyi @@ -29,46 +29,8 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = [ - "GraphServer", - "RunningGraphServer", - "RaphtoryClient", - "RemoteGraph", - "RemoteEdge", - "RemoteNode", - "RemoteNodes", - "RemotePathFromNode", - "RemotePathFromGraph", - "RemoteEdges", - "RemoteNestedEdges", - "RemoteHistory", - "RemoteHistoryTimestamps", - "RemoteHistoryEventIds", - "RemoteHistoryDateTimes", - "RemoteIntervals", - "RemoteMetadata", - "RemoteProperties", - "RemoteMetadataView", - "RemotePropertiesView", - "RemoteTemporalProperties", - "RemoteTemporalProperty", - "RemoteGraphSchema", - "RemoteNodeSchema", - "RemoteLayerSchema", - "RemoteEdgeSchema", - "RemotePropertySchema", - "RemoteNodeAddition", - "RemoteUpdate", - "RemoteEdgeAddition", - "RemotePermissionError", - "encode_graph", - "decode_graph", - "schema", - "cli", - "has_extension", -] - -class GraphServer(object): +__all__ = ['GraphServer', 'RunningGraphServer', 'RaphtoryClient', 'RemoteGraph', 'RemoteEdge', 'RemoteNode', 'RemoteNodes', 'RemotePathFromNode', 'RemotePathFromGraph', 'RemoteEdges', 'RemoteNestedEdges', 'RemoteHistory', 'RemoteHistoryTimestamps', 'RemoteHistoryEventIds', 'RemoteHistoryDateTimes', 'RemoteIntervals', 'RemoteMetadata', 'RemoteProperties', 'RemoteMetadataView', 'RemotePropertiesView', 'RemoteTemporalProperties', 'RemoteTemporalProperty', 'RemoteGraphSchema', 'RemoteNodeSchema', 'RemoteLayerSchema', 'RemoteEdgeSchema', 'RemotePropertySchema', 'RemoteNodeAddition', 'RemoteUpdate', 'RemoteEdgeAddition', 'RemotePermissionError', 'encode_graph', 'decode_graph', 'schema', 'cli', 'has_extension'] +class GraphServer(object): """ A class for defining and running a Raphtory GraphQL server @@ -115,12 +77,7 @@ class GraphServer(object): depends on which extensions the build has. """ - def __new__( - cls, - work_dir: str | PathLike, - config_path: Optional[str | PathLike] = None, - config: Optional[dict] = None, - ) -> GraphServer: + def __new__(cls, work_dir: str | PathLike, config_path: Optional[str | PathLike] = None, config: Optional[dict] = None) -> GraphServer: """Create and return a new object. See help(type) for accurate signature.""" def config_schema(self) -> dict: @@ -144,9 +101,7 @@ class GraphServer(object): None: """ - def start( - self, port: Optional[int] = None, timeout_ms: int = 5000 - ) -> RunningGraphServer: + def start(self, port: Optional[int] = None, timeout_ms: int = 5000) -> RunningGraphServer: """ Start the server and return a handle to it. @@ -161,12 +116,7 @@ class GraphServer(object): RunningGraphServer: The running server """ - def vectorise_all_graphs( - self, - embeddings: OpenAIEmbeddings, - nodes: bool | str = True, - edges: bool | str = True, - ) -> None: + def vectorise_all_graphs(self, embeddings: OpenAIEmbeddings, nodes: bool | str = True, edges: bool | str = True) -> None: """ Vectorise all graphs in the server working directory. @@ -179,13 +129,7 @@ class GraphServer(object): None: """ - def vectorise_graph( - self, - name: list[str], - embeddings: OpenAIEmbeddings, - nodes: bool | str = True, - edges: bool | str = True, - ) -> None: + def vectorise_graph(self, name: list[str], embeddings: OpenAIEmbeddings, nodes: bool | str = True, edges: bool | str = True) -> None: """ Vectorise the graph name in the server working directory. @@ -199,11 +143,15 @@ class GraphServer(object): None: """ -class RunningGraphServer(object): +class RunningGraphServer(object): """A Raphtory server handler that also enables querying the server""" - def __enter__(self): ... - def __exit__(self, _exc_type, _exc_val, _exc_tb): ... + def __enter__(self): + ... + + def __exit__(self, _exc_type, _exc_val, _exc_tb): + ... + def get_client(self) -> RaphtoryClient: """ Get the client for the server. @@ -228,7 +176,7 @@ class RunningGraphServer(object): None: """ -class RaphtoryClient(object): +class RaphtoryClient(object): """ A client for handling GraphQL operations in the context of Raphtory. @@ -284,9 +232,7 @@ class RaphtoryClient(object): None: """ - def new_graph( - self, path: str, graph_type: Literal["EVENT", "PERSISTENT"] - ) -> RemoteGraph: + def new_graph(self, path: str, graph_type: Literal["EVENT", "PERSISTENT"]) -> RemoteGraph: """ Create a new empty Graph on the server at path @@ -299,9 +245,7 @@ class RaphtoryClient(object): """ - def query( - self, query: str, variables: Optional[dict[str, Any]] = None - ) -> dict[str, Any]: + def query(self, query: str, variables: Optional[dict[str, Any]] = None) -> dict[str, Any]: """ Make a GraphQL query against the server. @@ -339,9 +283,7 @@ class RaphtoryClient(object): """ - def send_graph( - self, path: str, graph: Graph | PersistentGraph, overwrite: bool = False - ) -> None: + def send_graph(self, path: str, graph: Graph | PersistentGraph, overwrite: bool = False) -> None: """ Send a graph to the server @@ -383,16 +325,9 @@ class RaphtoryClient(object): RaphtoryClient: a new client using the given token. """ -class RemoteGraph(object): - def add_edge( - self, - timestamp: int | str | datetime, - src: str | int, - dst: str | int, - properties: Optional[dict] = None, - layer: Optional[str] = None, - event_id: Optional[int] = None, - ) -> RemoteEdge: +class RemoteGraph(object): + + def add_edge(self, timestamp: int | str | datetime, src: str | int, dst: str | int, properties: Optional[dict] = None, layer: Optional[str] = None, event_id: Optional[int] = None) -> RemoteEdge: """ Adds a new edge with the given source and destination nodes and properties to the remote graph. @@ -431,15 +366,7 @@ class RemoteGraph(object): None: """ - def add_node( - self, - timestamp: int | str | datetime, - id: str | int, - properties: Optional[dict] = None, - node_type: Optional[str] = None, - event_id: Optional[int] = None, - layer: Optional[str] = None, - ) -> RemoteNode: + def add_node(self, timestamp: int | str | datetime, id: str | int, properties: Optional[dict] = None, node_type: Optional[str] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> RemoteNode: """ Adds a new node with the given id and properties to the remote graph. @@ -467,12 +394,7 @@ class RemoteGraph(object): None: """ - def add_properties( - self, - timestamp: int | str | datetime, - properties: dict, - event_id: Optional[int] = None, - ) -> None: + def add_properties(self, timestamp: int | str | datetime, properties: dict, event_id: Optional[int] = None) -> None: """ Adds temporal properties to the remote graph. @@ -545,15 +467,7 @@ class RemoteGraph(object): int: the number of edge updates. """ - def create_node( - self, - timestamp: int | str | datetime, - id: str | int, - properties: Optional[dict] = None, - node_type: Optional[str] = None, - event_id: Optional[int] = None, - layer: Optional[str] = None, - ) -> RemoteNode: + def create_node(self, timestamp: int | str | datetime, id: str | int, properties: Optional[dict] = None, node_type: Optional[str] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> RemoteNode: """ Create a new node with the given id and properties to the remote graph and fail if the node already exists. @@ -586,14 +500,7 @@ class RemoteGraph(object): RemoteGraph: a new view restricted to the default layer. """ - def delete_edge( - self, - timestamp: int, - src: str | int, - dst: str | int, - layer: Optional[str] = None, - event_id: Optional[int] = None, - ) -> RemoteEdge: + def delete_edge(self, timestamp: int, src: str | int, dst: str | int, layer: Optional[str] = None, event_id: Optional[int] = None) -> RemoteEdge: """ Deletes an edge in the remote graph, given the timestamp, src and dst nodes and layer (optional) @@ -1090,9 +997,7 @@ class RemoteGraph(object): RemoteGraph: a new view restricted to those valid layers. """ - def window( - self, start: int | str | datetime, end: int | str | datetime - ) -> RemoteGraph: + def window(self, start: int | str | datetime, end: int | str | datetime) -> RemoteGraph: """ Restrict the graph to a time window `[start, end)`. @@ -1119,7 +1024,7 @@ class RemoteGraph(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteEdge(object): +class RemoteEdge(object): """ A remote edge reference @@ -1128,9 +1033,7 @@ class RemoteEdge(object): and [RemoteGraph.delete_edge][raphtory.graphql.RemoteGraph.delete_edge]. """ - def add_metadata( - self, metadata: dict[str, PropValue], layer: Optional[str] = None - ) -> None: + def add_metadata(self, metadata: dict[str, PropValue], layer: Optional[str] = None) -> None: """ Add metadata to the edge within the remote graph. This function is used to add metadata to an edge that does not @@ -1144,13 +1047,7 @@ class RemoteEdge(object): None: """ - def add_updates( - self, - t: int | str | datetime, - properties: Optional[dict[str, PropValue]] = None, - layer: Optional[str] = None, - event_id: Optional[int] = None, - ) -> None: + def add_updates(self, t: int | str | datetime, properties: Optional[dict[str, PropValue]] = None, layer: Optional[str] = None, event_id: Optional[int] = None) -> None: """ Add updates to an edge in the remote graph at a specified time. @@ -1209,12 +1106,7 @@ class RemoteEdge(object): RemoteEdge: a new view restricted to the default layer. """ - def delete( - self, - t: int | str | datetime, - layer: Optional[str] = None, - event_id: Optional[int] = None, - ) -> None: + def delete(self, t: int | str | datetime, layer: Optional[str] = None, event_id: Optional[int] = None) -> None: """ Mark the edge as deleted at the specified time. @@ -1573,9 +1465,7 @@ class RemoteEdge(object): there is none. """ - def update_metadata( - self, metadata: dict[str, PropValue], layer: Optional[str] = None - ) -> None: + def update_metadata(self, metadata: dict[str, PropValue], layer: Optional[str] = None) -> None: """ Update metadata of an edge in the remote graph overwriting existing values. This function is used to add properties to an edge that does not @@ -1622,7 +1512,8 @@ class RemoteEdge(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteNode(object): +class RemoteNode(object): + def add_metadata(self, metadata: dict[str, PropValue]) -> None: """ Add metadata to a node in the remote graph. @@ -1636,13 +1527,7 @@ class RemoteNode(object): None: """ - def add_updates( - self, - t: int | str | datetime, - properties: Optional[dict[str, PropValue]] = None, - event_id: Optional[int] = None, - layer: Optional[str] = None, - ) -> None: + def add_updates(self, t: int | str | datetime, properties: Optional[dict[str, PropValue]] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> None: """ Add updates to a node in the remote graph at a specified time. This function allows for the addition of property updates to a node within the graph. The updates are time-stamped, meaning they are applied at the specified time. @@ -1832,7 +1717,7 @@ class RemoteNode(object): """ @property - def id(self) -> str | int: + def id(self) -> str|int: """ The node's id (as a string, even if the graph uses integer GIDs). Property — attribute access fires one RPC. @@ -2122,7 +2007,7 @@ class RemoteNode(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteNodes(object): +class RemoteNodes(object): """ A handle to a remote collection of nodes. @@ -2585,7 +2470,7 @@ class RemoteNodes(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemotePathFromNode(object): +class RemotePathFromNode(object): """ A handle to a "path from node" collection. @@ -3040,7 +2925,7 @@ class RemotePathFromNode(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemotePathFromGraph(object): +class RemotePathFromGraph(object): """ A handle to a "path from graph" collection. @@ -3502,7 +3387,7 @@ class RemotePathFromGraph(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteEdges(object): +class RemoteEdges(object): """ A handle to a remote collection of edges. @@ -3958,7 +3843,7 @@ class RemoteEdges(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteNestedEdges(object): +class RemoteNestedEdges(object): """ A handle to a nested edges collection. @@ -4411,7 +4296,7 @@ class RemoteNestedEdges(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteHistory(object): +class RemoteHistory(object): """ A handle to the event history of a remote node or edge. @@ -4513,9 +4398,7 @@ class RemoteHistory(object): OptionalEventTime: the latest event time, or empty. """ - def page( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[EventTime]: + def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[EventTime]: """ A page of events in ascending time order — at most `limit` items, starting `page_index * limit + offset` items in. Both `offset` and @@ -4530,9 +4413,7 @@ class RemoteHistory(object): list[EventTime]: at most `limit` events. """ - def page_rev( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[EventTime]: + def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[EventTime]: """ A page of events in descending time order. Same args as `page()`. Fires one RPC. @@ -4565,7 +4446,7 @@ class RemoteHistory(object): RemoteHistoryTimestamps: the timestamps view of this history. """ -class RemoteHistoryTimestamps(object): +class RemoteHistoryTimestamps(object): """Timestamps view of a `RemoteHistory`. Lists / pages return `list[int]`.""" def __contains__(self, key): @@ -4603,9 +4484,7 @@ class RemoteHistoryTimestamps(object): list[int]: all timestamps in descending time order. """ - def page( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[int]: + def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: """ Fires one RPC. @@ -4618,9 +4497,7 @@ class RemoteHistoryTimestamps(object): list[int]: at most `limit` timestamps, in ascending time order. """ - def page_rev( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[int]: + def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: """ Fires one RPC. @@ -4633,7 +4510,7 @@ class RemoteHistoryTimestamps(object): list[int]: at most `limit` timestamps, in descending time order. """ -class RemoteHistoryEventIds(object): +class RemoteHistoryEventIds(object): """Event-id view of a `RemoteHistory`. Lists / pages return `list[int]`.""" def __contains__(self, key): @@ -4671,9 +4548,7 @@ class RemoteHistoryEventIds(object): list[int]: all event ids in descending time order. """ - def page( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[int]: + def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: """ Fires one RPC. @@ -4686,9 +4561,7 @@ class RemoteHistoryEventIds(object): list[int]: at most `limit` event ids, in ascending time order. """ - def page_rev( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[int]: + def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: """ Fires one RPC. @@ -4701,7 +4574,7 @@ class RemoteHistoryEventIds(object): list[int]: at most `limit` event ids, in descending time order. """ -class RemoteHistoryDateTimes(object): +class RemoteHistoryDateTimes(object): """ Datetime view of a `RemoteHistory`. Lists / pages return `list[datetime]` (UTC), mirroring the local `History.dt`. @@ -4742,9 +4615,7 @@ class RemoteHistoryDateTimes(object): list[datetime]: all datetimes (UTC) in descending time order. """ - def page( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[datetime]: + def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[datetime]: """ Fires one RPC. @@ -4757,9 +4628,7 @@ class RemoteHistoryDateTimes(object): list[datetime]: at most `limit` datetimes (UTC), in ascending time order. """ - def page_rev( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[datetime]: + def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[datetime]: """ Fires one RPC. @@ -4772,7 +4641,7 @@ class RemoteHistoryDateTimes(object): list[datetime]: at most `limit` datetimes (UTC), in descending time order. """ -class RemoteIntervals(object): +class RemoteIntervals(object): """ Intervals view of a `RemoteHistory` — inter-event gaps plus summary stats (`mean`, `median`, `max`, `min`). @@ -4849,9 +4718,7 @@ class RemoteIntervals(object): Optional[int]: the smallest interval, or `None` if fewer than 2 events. """ - def page( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[int]: + def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: """ Fires one RPC. @@ -4864,9 +4731,7 @@ class RemoteIntervals(object): list[int]: at most `limit` intervals, in ascending time order. """ - def page_rev( - self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None - ) -> list[int]: + def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: """ Fires one RPC. @@ -4879,7 +4744,7 @@ class RemoteIntervals(object): list[int]: at most `limit` intervals, in descending time order. """ -class RemoteMetadata(object): +class RemoteMetadata(object): """ A handle to the metadata container of a remote graph, node, or edge — the non-temporal properties whose values don't change over the graph's @@ -4952,7 +4817,7 @@ class RemoteMetadata(object): list[PropValue]: the metadata values. """ -class RemoteProperties(object): +class RemoteProperties(object): """ A handle to the full properties container of a remote graph, node, or edge — includes both non-temporal metadata and temporal properties. @@ -5055,7 +4920,7 @@ class RemoteProperties(object): list[PropValue]: the property values. """ -class RemoteMetadataView(object): +class RemoteMetadataView(object): """ A columnar view over the non-temporal metadata of a remote node/edge collection. Every accessor returns one value per member (nested per source @@ -5116,7 +4981,7 @@ class RemoteMetadataView(object): list: one column per key, in key order. """ -class RemotePropertiesView(object): +class RemotePropertiesView(object): """ A columnar view over the properties of a remote node/edge collection (temporal properties yield their most recent value under the current view). @@ -5176,7 +5041,7 @@ class RemotePropertiesView(object): list: one column per key, in key order. """ -class RemoteTemporalProperties(object): +class RemoteTemporalProperties(object): """ A handle to the temporal-only view of a properties container. Each property has a full history over time. @@ -5268,7 +5133,7 @@ class RemoteTemporalProperties(object): list[RemoteTemporalProperty]: the temporal property handles. """ -class RemoteTemporalProperty(object): +class RemoteTemporalProperty(object): """ A handle to a single temporal property — one key with its full history of updates, plus statistical summaries and time-indexed accessors. @@ -5429,7 +5294,7 @@ class RemoteTemporalProperty(object): list[PropValue]: every value the property has taken, in temporal order. """ -class RemoteGraphSchema(object): +class RemoteGraphSchema(object): """ The full schema of a remote graph — the tree of node types, edge layers, and their observed property/metadata fields. @@ -5458,7 +5323,7 @@ class RemoteGraphSchema(object): list[RemoteNodeSchema]: one entry per node type. """ -class RemoteNodeSchema(object): +class RemoteNodeSchema(object): """Schema for nodes of a specific type.""" def __repr__(self): @@ -5491,7 +5356,7 @@ class RemoteNodeSchema(object): str: the node type name. """ -class RemoteLayerSchema(object): +class RemoteLayerSchema(object): """Schema for a single edge layer.""" def __repr__(self): @@ -5515,7 +5380,7 @@ class RemoteLayerSchema(object): str: the layer name. """ -class RemoteEdgeSchema(object): +class RemoteEdgeSchema(object): """Schema for edges between a specific `(src_type, dst_type)` pair.""" def __repr__(self): @@ -5557,7 +5422,7 @@ class RemoteEdgeSchema(object): str: the source node type. """ -class RemotePropertySchema(object): +class RemotePropertySchema(object): """ One property key on a node/edge type, with its observed property type and (for string-valued properties) the set of distinct values seen. @@ -5593,7 +5458,7 @@ class RemotePropertySchema(object): list[str]: the distinct values seen. """ -class RemoteNodeAddition(object): +class RemoteNodeAddition(object): """ Node addition update @@ -5604,16 +5469,10 @@ class RemoteNodeAddition(object): updates (list[RemoteUpdate], optional): the temporal updates """ - def __new__( - cls, - name: GID, - node_type: Optional[str] = None, - metadata: Optional[PropInput] = None, - updates: Optional[list[RemoteUpdate]] = None, - ) -> RemoteNodeAddition: + def __new__(cls, name: GID, node_type: Optional[str] = None, metadata: Optional[PropInput] = None, updates: Optional[list[RemoteUpdate]] = None) -> RemoteNodeAddition: """Create and return a new object. See help(type) for accurate signature.""" -class RemoteUpdate(object): +class RemoteUpdate(object): """ A temporal update @@ -5622,12 +5481,10 @@ class RemoteUpdate(object): properties (PropInput, optional): the properties for the update """ - def __new__( - cls, time: TimeInput, properties: Optional[PropInput] = None - ) -> RemoteUpdate: + def __new__(cls, time: TimeInput, properties: Optional[PropInput] = None) -> RemoteUpdate: """Create and return a new object. See help(type) for accurate signature.""" -class RemoteEdgeAddition(object): +class RemoteEdgeAddition(object): """ An edge update @@ -5639,17 +5496,10 @@ class RemoteEdgeAddition(object): updates (list[RemoteUpdate], optional): the temporal updates for the edge """ - def __new__( - cls, - src: GID, - dst: GID, - layer: Optional[str] = None, - metadata: Optional[PropInput] = None, - updates: Optional[list[RemoteUpdate]] = None, - ) -> RemoteEdgeAddition: + def __new__(cls, src: GID, dst: GID, layer: Optional[str] = None, metadata: Optional[PropInput] = None, updates: Optional[list[RemoteUpdate]] = None) -> RemoteEdgeAddition: """Create and return a new object. See help(type) for accurate signature.""" -class RemotePermissionError(Exception): +class RemotePermissionError(Exception): """Raised when the server denies a request for lack of permission. A denied request is distinct from a missing graph: a forbidden-but-hidden graph is reported as not found, never as this error.""" @property diff --git a/python/python/raphtory/iterables/__init__.pyi b/python/python/raphtory/iterables/__init__.pyi index 1be45bce63..bb6079cd30 100644 --- a/python/python/raphtory/iterables/__init__.pyi +++ b/python/python/raphtory/iterables/__init__.pyi @@ -29,68 +29,9 @@ from decimal import Decimal import networkx as nx # type: ignore import pyvis # type: ignore -__all__ = [ - "NestedUtcDateTimeIterable", - "NestedGIDIterable", - "GIDIterable", - "StringIterable", - "OptionArcStringIterable", - "UsizeIterable", - "OptionI64Iterable", - "NestedOptionArcStringIterable", - "NestedStringIterable", - "NestedOptionI64Iterable", - "NestedI64VecIterable", - "NestedUsizeIterable", - "BoolIterable", - "ArcStringIterable", - "NestedVecUtcDateTimeIterable", - "OptionVecUtcDateTimeIterable", - "GIDGIDIterable", - "NestedGIDGIDIterable", - "NestedBoolIterable", - "U64Iterable", - "OptionUtcDateTimeIterable", - "ArcStringVecIterable", - "NestedArcStringVecIterable", - "NestedEventTimeIterable", - "NestedArcStringIterable", - "NestedOptionEventTimeIterable", - "NestedHistoryIterable", - "EventTimeIterable", - "OptionEventTimeIterable", - "HistoryIterable", - "HistoryTimestampIterable", - "IntervalsIterable", - "HistoryEventIdIterable", - "HistoryDateTimeIterable", - "OptionUsizeIterable", - "ResultOptionUtcDateTimeIterable", - "I64Iterable", - "ResultUtcDateTimeIterable", - "NestedHistoryTimestampIterable", - "NestedIntervalsIterable", - "NestedHistoryEventIdIterable", - "NestedHistoryDateTimeIterable", - "NestedOptionUsizeIterable", - "NestedResultOptionUtcDateTimeIterable", - "NestedI64Iterable", - "NestedResultUtcDateTimeIterable", - "MetadataListList", - "PyNestedPropsIterable", - "PyPropValueListList", - "PyTemporalPropsList", - "PyTemporalPropsListList", - "PyPropHistValueList", - "PyPropHistValueListList", - "PyTemporalPropList", - "PyTemporalPropListList", - "PyPropHistItemsList", - "PyPropHistItemsListList", - "PropIterable", -] - -class NestedUtcDateTimeIterable(object): +__all__ = ['NestedUtcDateTimeIterable', 'NestedGIDIterable', 'GIDIterable', 'StringIterable', 'OptionArcStringIterable', 'UsizeIterable', 'OptionI64Iterable', 'NestedOptionArcStringIterable', 'NestedStringIterable', 'NestedOptionI64Iterable', 'NestedI64VecIterable', 'NestedUsizeIterable', 'BoolIterable', 'ArcStringIterable', 'NestedVecUtcDateTimeIterable', 'OptionVecUtcDateTimeIterable', 'GIDGIDIterable', 'NestedGIDGIDIterable', 'NestedBoolIterable', 'U64Iterable', 'OptionUtcDateTimeIterable', 'ArcStringVecIterable', 'NestedArcStringVecIterable', 'NestedEventTimeIterable', 'NestedArcStringIterable', 'NestedOptionEventTimeIterable', 'NestedHistoryIterable', 'EventTimeIterable', 'OptionEventTimeIterable', 'HistoryIterable', 'HistoryTimestampIterable', 'IntervalsIterable', 'HistoryEventIdIterable', 'HistoryDateTimeIterable', 'OptionUsizeIterable', 'ResultOptionUtcDateTimeIterable', 'I64Iterable', 'ResultUtcDateTimeIterable', 'NestedHistoryTimestampIterable', 'NestedIntervalsIterable', 'NestedHistoryEventIdIterable', 'NestedHistoryDateTimeIterable', 'NestedOptionUsizeIterable', 'NestedResultOptionUtcDateTimeIterable', 'NestedI64Iterable', 'NestedResultUtcDateTimeIterable', 'MetadataListList', 'PyNestedPropsIterable', 'PyPropValueListList', 'PyTemporalPropsList', 'PyTemporalPropsListList', 'PyPropHistValueList', 'PyPropHistValueListList', 'PyTemporalPropList', 'PyTemporalPropListList', 'PyPropHistItemsList', 'PyPropHistItemsListList', 'PropIterable'] +class NestedUtcDateTimeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -126,7 +67,8 @@ class NestedUtcDateTimeIterable(object): list[list]: """ -class NestedGIDIterable(object): +class NestedGIDIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -178,7 +120,8 @@ class NestedGIDIterable(object): Any: """ -class GIDIterable(object): +class GIDIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -230,7 +173,8 @@ class GIDIterable(object): Any: """ -class StringIterable(object): +class StringIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -266,7 +210,8 @@ class StringIterable(object): list: """ -class OptionArcStringIterable(object): +class OptionArcStringIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -302,7 +247,8 @@ class OptionArcStringIterable(object): list: """ -class UsizeIterable(object): +class UsizeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -370,7 +316,8 @@ class UsizeIterable(object): Any: """ -class OptionI64Iterable(object): +class OptionI64Iterable(object): + def __eq__(self, value): """Return self==value.""" @@ -422,7 +369,8 @@ class OptionI64Iterable(object): Any: """ -class NestedOptionArcStringIterable(object): +class NestedOptionArcStringIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -458,7 +406,8 @@ class NestedOptionArcStringIterable(object): list[list]: """ -class NestedStringIterable(object): +class NestedStringIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -494,7 +443,8 @@ class NestedStringIterable(object): list[list]: """ -class NestedOptionI64Iterable(object): +class NestedOptionI64Iterable(object): + def __eq__(self, value): """Return self==value.""" @@ -546,7 +496,8 @@ class NestedOptionI64Iterable(object): Any: """ -class NestedI64VecIterable(object): +class NestedI64VecIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -582,7 +533,8 @@ class NestedI64VecIterable(object): list[list]: """ -class NestedUsizeIterable(object): +class NestedUsizeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -650,7 +602,8 @@ class NestedUsizeIterable(object): Any: """ -class BoolIterable(object): +class BoolIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -686,7 +639,8 @@ class BoolIterable(object): list: """ -class ArcStringIterable(object): +class ArcStringIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -704,7 +658,8 @@ class ArcStringIterable(object): list: """ -class NestedVecUtcDateTimeIterable(object): +class NestedVecUtcDateTimeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -740,7 +695,8 @@ class NestedVecUtcDateTimeIterable(object): list[list]: """ -class OptionVecUtcDateTimeIterable(object): +class OptionVecUtcDateTimeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -776,7 +732,8 @@ class OptionVecUtcDateTimeIterable(object): list: """ -class GIDGIDIterable(object): +class GIDGIDIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -828,7 +785,8 @@ class GIDGIDIterable(object): Any: """ -class NestedGIDGIDIterable(object): +class NestedGIDGIDIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -880,7 +838,8 @@ class NestedGIDGIDIterable(object): Any: """ -class NestedBoolIterable(object): +class NestedBoolIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -916,7 +875,8 @@ class NestedBoolIterable(object): list[list]: """ -class U64Iterable(object): +class U64Iterable(object): + def __eq__(self, value): """Return self==value.""" @@ -984,7 +944,8 @@ class U64Iterable(object): Any: """ -class OptionUtcDateTimeIterable(object): +class OptionUtcDateTimeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1020,7 +981,8 @@ class OptionUtcDateTimeIterable(object): list: """ -class ArcStringVecIterable(object): +class ArcStringVecIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1056,7 +1018,8 @@ class ArcStringVecIterable(object): list: """ -class NestedArcStringVecIterable(object): +class NestedArcStringVecIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1092,7 +1055,8 @@ class NestedArcStringVecIterable(object): list[list]: """ -class NestedEventTimeIterable(object): +class NestedEventTimeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1174,7 +1138,8 @@ class NestedEventTimeIterable(object): NestedI64Iterable: Nested iterable of millisecond timestamps since the Unix epoch for each EventTime. """ -class NestedArcStringIterable(object): +class NestedArcStringIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1192,7 +1157,8 @@ class NestedArcStringIterable(object): list[list]: """ -class NestedOptionEventTimeIterable(object): +class NestedOptionEventTimeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1274,7 +1240,8 @@ class NestedOptionEventTimeIterable(object): NestedOptionI64Iterable: Nested iterable of millisecond timestamps since the Unix epoch for each EventTime, if available. """ -class NestedHistoryIterable(object): +class NestedHistoryIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1336,7 +1303,8 @@ class NestedHistoryIterable(object): NestedHistoryTimestampIterable: Iterable of iterables of HistoryTimestamp objects. """ -class EventTimeIterable(object): +class EventTimeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1418,7 +1386,8 @@ class EventTimeIterable(object): I64Iterable: Iterable of millisecond timestamps since the Unix epoch for each EventTime. """ -class OptionEventTimeIterable(object): +class OptionEventTimeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1500,7 +1469,8 @@ class OptionEventTimeIterable(object): OptionI64Iterable: Iterable of millisecond timestamps since the Unix epoch for each EventTime, if available. """ -class HistoryIterable(object): +class HistoryIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1562,7 +1532,8 @@ class HistoryIterable(object): HistoryTimestampIterable: Iterable of HistoryTimestamp objects, one for each item. """ -class HistoryTimestampIterable(object): +class HistoryTimestampIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1580,7 +1551,8 @@ class HistoryTimestampIterable(object): list[NDArray[np.int64]]: NumPy NDArray of timestamps in milliseconds per history. """ -class IntervalsIterable(object): +class IntervalsIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1598,7 +1570,8 @@ class IntervalsIterable(object): list[NDArray[np.int64]]: NumPy NDArray of intervals per history. """ -class HistoryEventIdIterable(object): +class HistoryEventIdIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1616,7 +1589,8 @@ class HistoryEventIdIterable(object): list[NDArray[np.uintp]]: NumPy NDArray of event ids per history. """ -class HistoryDateTimeIterable(object): +class HistoryDateTimeIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1637,7 +1611,8 @@ class HistoryDateTimeIterable(object): TimeError: If a timestamp cannot be converted to a datetime. """ -class OptionUsizeIterable(object): +class OptionUsizeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1689,7 +1664,8 @@ class OptionUsizeIterable(object): Any: """ -class ResultOptionUtcDateTimeIterable(object): +class ResultOptionUtcDateTimeIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1707,7 +1683,8 @@ class ResultOptionUtcDateTimeIterable(object): list[Optional[datetime]]: one entry per element (None where absent). """ -class I64Iterable(object): +class I64Iterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1775,7 +1752,8 @@ class I64Iterable(object): Any: """ -class ResultUtcDateTimeIterable(object): +class ResultUtcDateTimeIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1793,7 +1771,8 @@ class ResultUtcDateTimeIterable(object): list[datetime]: one datetime per element. """ -class NestedHistoryTimestampIterable(object): +class NestedHistoryTimestampIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1827,7 +1806,8 @@ class NestedHistoryTimestampIterable(object): list[int]: List of timestamps in milliseconds. """ -class NestedIntervalsIterable(object): +class NestedIntervalsIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1861,7 +1841,8 @@ class NestedIntervalsIterable(object): list[int]: List of intervals. """ -class NestedHistoryEventIdIterable(object): +class NestedHistoryEventIdIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1895,7 +1876,8 @@ class NestedHistoryEventIdIterable(object): list[int]: List of timestamps in milliseconds. """ -class NestedHistoryDateTimeIterable(object): +class NestedHistoryDateTimeIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1927,7 +1909,8 @@ class NestedHistoryDateTimeIterable(object): TimeError: If a timestamp cannot be converted to a datetime. """ -class NestedOptionUsizeIterable(object): +class NestedOptionUsizeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -1979,7 +1962,8 @@ class NestedOptionUsizeIterable(object): Any: """ -class NestedResultOptionUtcDateTimeIterable(object): +class NestedResultOptionUtcDateTimeIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -1997,7 +1981,8 @@ class NestedResultOptionUtcDateTimeIterable(object): list[list[Optional[datetime]]]: one inner list per outer element (entries are None where absent). """ -class NestedI64Iterable(object): +class NestedI64Iterable(object): + def __eq__(self, value): """Return self==value.""" @@ -2065,7 +2050,8 @@ class NestedI64Iterable(object): Any: """ -class NestedResultUtcDateTimeIterable(object): +class NestedResultUtcDateTimeIterable(object): + def __iter__(self): """Implement iter(self).""" @@ -2083,7 +2069,8 @@ class NestedResultUtcDateTimeIterable(object): list[list[datetime]]: one inner list per outer element. """ -class MetadataListList(object): +class MetadataListList(object): + def __contains__(self, key): """Return bool(key in self).""" @@ -2154,7 +2141,8 @@ class MetadataListList(object): list[PyPropValueListList]: """ -class PyNestedPropsIterable(object): +class PyNestedPropsIterable(object): + def __contains__(self, key): """Return bool(key in self).""" @@ -2235,7 +2223,8 @@ class PyNestedPropsIterable(object): list[list[list[PropValue]]]: """ -class PyPropValueListList(object): +class PyPropValueListList(object): + def __eq__(self, value): """Return self==value.""" @@ -2343,7 +2332,8 @@ class PyPropValueListList(object): list[PropValue]: """ -class PyTemporalPropsList(object): +class PyTemporalPropsList(object): + def __contains__(self, key): """Return bool(key in self).""" @@ -2422,7 +2412,8 @@ class PyTemporalPropsList(object): list[PyTemporalPropList]: """ -class PyTemporalPropsListList(object): +class PyTemporalPropsListList(object): + def __contains__(self, key): """Return bool(key in self).""" @@ -2501,7 +2492,8 @@ class PyTemporalPropsListList(object): list[PyTemporalPropListList]: """ -class PyPropHistValueList(object): +class PyPropHistValueList(object): + def __eq__(self, value): """Return self==value.""" @@ -2601,7 +2593,8 @@ class PyPropHistValueList(object): list[PropValue]: """ -class PyPropHistValueListList(object): +class PyPropHistValueListList(object): + def __eq__(self, value): """Return self==value.""" @@ -2693,7 +2686,8 @@ class PyPropHistValueListList(object): list[list[PropValue]]: """ -class PyTemporalPropList(object): +class PyTemporalPropList(object): + def __eq__(self, value): """Return self==value.""" @@ -2773,7 +2767,8 @@ class PyTemporalPropList(object): PyPropHistValueList: """ -class PyTemporalPropListList(object): +class PyTemporalPropListList(object): + def __eq__(self, value): """Return self==value.""" @@ -2861,7 +2856,8 @@ class PyTemporalPropListList(object): PyPropHistValueListList: """ -class PyPropHistItemsList(object): +class PyPropHistItemsList(object): + def __eq__(self, value): """Return self==value.""" @@ -2897,7 +2893,8 @@ class PyPropHistItemsList(object): list: """ -class PyPropHistItemsListList(object): +class PyPropHistItemsListList(object): + def __eq__(self, value): """Return self==value.""" @@ -2933,7 +2930,8 @@ class PyPropHistItemsListList(object): list[list]: """ -class PropIterable(object): +class PropIterable(object): + def __eq__(self, value): """Return self==value.""" diff --git a/python/python/raphtory/node_state/__init__.pyi b/python/python/raphtory/node_state/__init__.pyi index 16bee37fea..87af0aec5c 100644 --- a/python/python/raphtory/node_state/__init__.pyi +++ b/python/python/raphtory/node_state/__init__.pyi @@ -29,59 +29,9 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = [ - "NodeGroups", - "DegreeView", - "NodeStateUsize", - "NodeStateOptionUsize", - "NodeStateU64", - "NodeStateOptionI64", - "NodeStateOptionEventTime", - "NodeStateOptionDateTime", - "IdView", - "NodeStateGID", - "EarliestTimeView", - "EarliestTimestampView", - "EarliestEventIdView", - "EarliestDateTimeView", - "LatestTimeView", - "LatestTimestampView", - "LatestEventIdView", - "LatestDateTimeView", - "NameView", - "NodeStateString", - "HistoryView", - "HistoryTimestampView", - "HistoryDateTimeView", - "HistoryEventIdView", - "IntervalsView", - "IntervalsFloatView", - "IntervalsIntegerView", - "EdgeHistoryCountView", - "UsizeIterable", - "NodeTypeView", - "NodeStateOptionStr", - "NodeStateListDateTime", - "NodeStateWeightedSP", - "NodeStateF64", - "NodeStateOptionF64", - "NodeStateNodes", - "NodeStateReachability", - "NodeStateListF64", - "NodeStateMotifs", - "NodeStateHits", - "NodeStateHistory", - "NodeStateHistoryTimestamp", - "NodeStateHistoryDateTime", - "NodeStateHistoryEventId", - "NodeStateIntervals", - "NodeStateSEIR", - "NodeLayout", - "NodeStateF64String", - "OutputNodeState", -] - -class NodeGroups(object): +__all__ = ['NodeGroups', 'DegreeView', 'NodeStateUsize', 'NodeStateOptionUsize', 'NodeStateU64', 'NodeStateOptionI64', 'NodeStateOptionEventTime', 'NodeStateOptionDateTime', 'IdView', 'NodeStateGID', 'EarliestTimeView', 'EarliestTimestampView', 'EarliestEventIdView', 'EarliestDateTimeView', 'LatestTimeView', 'LatestTimestampView', 'LatestEventIdView', 'LatestDateTimeView', 'NameView', 'NodeStateString', 'HistoryView', 'HistoryTimestampView', 'HistoryDateTimeView', 'HistoryEventIdView', 'IntervalsView', 'IntervalsFloatView', 'IntervalsIntegerView', 'EdgeHistoryCountView', 'UsizeIterable', 'NodeTypeView', 'NodeStateOptionStr', 'NodeStateListDateTime', 'NodeStateWeightedSP', 'NodeStateF64', 'NodeStateOptionF64', 'NodeStateNodes', 'NodeStateReachability', 'NodeStateListF64', 'NodeStateMotifs', 'NodeStateHits', 'NodeStateHistory', 'NodeStateHistoryTimestamp', 'NodeStateHistoryDateTime', 'NodeStateHistoryEventId', 'NodeStateIntervals', 'NodeStateSEIR', 'NodeLayout', 'NodeStateF64String', 'OutputNodeState'] +class NodeGroups(object): + def __bool__(self): """True if self else False""" @@ -124,7 +74,7 @@ class NodeGroups(object): Iterator[Tuple[Any, GraphView]]: Iterator over subgraphs with corresponding value """ -class DegreeView(object): +class DegreeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -343,7 +293,8 @@ class DegreeView(object): Iterator[int]: Iterator over values """ -class NodeStateUsize(object): +class NodeStateUsize(object): + def __eq__(self, value): """Return self==value.""" @@ -536,7 +487,8 @@ class NodeStateUsize(object): Iterator[int]: Iterator over values """ -class NodeStateOptionUsize(object): +class NodeStateOptionUsize(object): + def __eq__(self, value): """Return self==value.""" @@ -578,9 +530,7 @@ class NodeStateOptionUsize(object): NodeStateOptionUsize: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[Optional[int]] = None - ) -> Optional[Optional[int]]: + def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: """ Get value for node @@ -714,7 +664,8 @@ class NodeStateOptionUsize(object): Iterator[Optional[int]]: Iterator over values """ -class NodeStateU64(object): +class NodeStateU64(object): + def __eq__(self, value): """Return self==value.""" @@ -899,7 +850,8 @@ class NodeStateU64(object): Iterator[int]: Iterator over values """ -class NodeStateOptionI64(object): +class NodeStateOptionI64(object): + def __eq__(self, value): """Return self==value.""" @@ -941,9 +893,7 @@ class NodeStateOptionI64(object): NodeStateOptionI64: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[Optional[int]] = None - ) -> Optional[Optional[int]]: + def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: """ Get value for node @@ -1077,7 +1027,8 @@ class NodeStateOptionI64(object): Iterator[Optional[int]]: Iterator over values """ -class NodeStateOptionEventTime(object): +class NodeStateOptionEventTime(object): + def __eq__(self, value): """Return self==value.""" @@ -1119,9 +1070,7 @@ class NodeStateOptionEventTime(object): NodeStateOptionEventTime: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[Optional[EventTime]] = None - ) -> Optional[Optional[EventTime]]: + def get(self, node: NodeInput, default: Optional[Optional[EventTime]] = None) -> Optional[Optional[EventTime]]: """ Get value for node @@ -1255,7 +1204,8 @@ class NodeStateOptionEventTime(object): Iterator[Optional[EventTime]]: Iterator over values """ -class NodeStateOptionDateTime(object): +class NodeStateOptionDateTime(object): + def __eq__(self, value): """Return self==value.""" @@ -1297,9 +1247,7 @@ class NodeStateOptionDateTime(object): NodeStateOptionDateTime: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[Optional[datetime]] = None - ) -> Optional[Optional[datetime]]: + def get(self, node: NodeInput, default: Optional[Optional[datetime]] = None) -> Optional[Optional[datetime]]: """ Get value for node @@ -1433,7 +1381,7 @@ class NodeStateOptionDateTime(object): Iterator[Optional[datetime]]: Iterator over values """ -class IdView(object): +class IdView(object): """A lazy view over node values""" def __eq__(self, value): @@ -1627,7 +1575,8 @@ class IdView(object): Iterator[GID]: Iterator over values """ -class NodeStateGID(object): +class NodeStateGID(object): + def __eq__(self, value): """Return self==value.""" @@ -1795,7 +1744,7 @@ class NodeStateGID(object): Iterator[GID]: Iterator over values """ -class EarliestTimeView(object): +class EarliestTimeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -1881,9 +1830,7 @@ class EarliestTimeView(object): EarliestEventIdView: A lazy view over the event ids of the earliest times for each node. """ - def get( - self, node: NodeInput, default: Optional[Optional[EventTime]] = None - ) -> Optional[Optional[EventTime]]: + def get(self, node: NodeInput, default: Optional[Optional[EventTime]] = None) -> Optional[Optional[EventTime]]: """ Get value for node @@ -2026,7 +1973,7 @@ class EarliestTimeView(object): Iterator[Optional[EventTime]]: Iterator over values """ -class EarliestTimestampView(object): +class EarliestTimestampView(object): """A lazy view over node values""" def __eq__(self, value): @@ -2094,9 +2041,7 @@ class EarliestTimestampView(object): NodeStateOptionI64: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[Optional[int]] = None - ) -> Optional[Optional[int]]: + def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: """ Get value for node @@ -2230,7 +2175,7 @@ class EarliestTimestampView(object): Iterator[Optional[int]]: Iterator over values """ -class EarliestEventIdView(object): +class EarliestEventIdView(object): """A lazy view over node values""" def __eq__(self, value): @@ -2298,9 +2243,7 @@ class EarliestEventIdView(object): NodeStateOptionUsize: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[Optional[int]] = None - ) -> Optional[Optional[int]]: + def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: """ Get value for node @@ -2434,7 +2377,7 @@ class EarliestEventIdView(object): Iterator[Optional[int]]: Iterator over values """ -class EarliestDateTimeView(object): +class EarliestDateTimeView(object): """A lazy view over EarliestDateTime values for each node.""" def __eq__(self, value): @@ -2510,9 +2453,7 @@ class EarliestDateTimeView(object): NodeStateOptionDateTime: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[datetime] = None - ) -> Optional[datetime]: + def get(self, node: NodeInput, default: Optional[datetime] = None) -> Optional[datetime]: """ Get value for node @@ -2677,7 +2618,7 @@ class EarliestDateTimeView(object): Iterator[datetime]: Iterator over values """ -class LatestTimeView(object): +class LatestTimeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -2763,9 +2704,7 @@ class LatestTimeView(object): LatestEventIdView: A lazy view over the event ids of the latest times for each node. """ - def get( - self, node: NodeInput, default: Optional[Optional[int]] = None - ) -> Optional[Optional[int]]: + def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: """ Get value for node @@ -2908,7 +2847,7 @@ class LatestTimeView(object): Iterator[Optional[int]]: Iterator over values """ -class LatestTimestampView(object): +class LatestTimestampView(object): """A lazy view over node values""" def __eq__(self, value): @@ -2976,9 +2915,7 @@ class LatestTimestampView(object): NodeStateOptionI64: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[Optional[int]] = None - ) -> Optional[Optional[int]]: + def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: """ Get value for node @@ -3112,7 +3049,7 @@ class LatestTimestampView(object): Iterator[Optional[int]]: Iterator over values """ -class LatestEventIdView(object): +class LatestEventIdView(object): """A lazy view over node values""" def __eq__(self, value): @@ -3180,9 +3117,7 @@ class LatestEventIdView(object): NodeStateOptionUsize: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[Optional[int]] = None - ) -> Optional[Optional[int]]: + def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: """ Get value for node @@ -3316,7 +3251,7 @@ class LatestEventIdView(object): Iterator[Optional[int]]: Iterator over values """ -class LatestDateTimeView(object): +class LatestDateTimeView(object): """A lazy view over LatestDateTime values for each node.""" def __eq__(self, value): @@ -3392,9 +3327,7 @@ class LatestDateTimeView(object): NodeStateOptionDateTime: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[datetime] = None - ) -> Optional[datetime]: + def get(self, node: NodeInput, default: Optional[datetime] = None) -> Optional[datetime]: """ Get value for node @@ -3559,7 +3492,7 @@ class LatestDateTimeView(object): Iterator[datetime]: Iterator over values """ -class NameView(object): +class NameView(object): """A lazy view over node values""" def __eq__(self, value): @@ -3761,7 +3694,8 @@ class NameView(object): Iterator[str]: Iterator over values """ -class NodeStateString(object): +class NodeStateString(object): + def __eq__(self, value): """Return self==value.""" @@ -3937,7 +3871,7 @@ class NodeStateString(object): Iterator[str]: Iterator over values """ -class HistoryView(object): +class HistoryView(object): """A lazy view over History objects for each node.""" def __eq__(self, value): @@ -4028,9 +3962,7 @@ class HistoryView(object): History: a history object containing all time entries """ - def get( - self, node: NodeInput, default: Optional[History] = None - ) -> Optional[History]: + def get(self, node: NodeInput, default: Optional[History] = None) -> Optional[History]: """ Get value for node @@ -4110,7 +4042,7 @@ class HistoryView(object): Iterator[History]: Iterator over histories """ -class HistoryTimestampView(object): +class HistoryTimestampView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4167,9 +4099,7 @@ class HistoryTimestampView(object): NodeStateHistoryTimestamp: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[HistoryTimestamp] = None - ) -> Optional[HistoryTimestamp]: + def get(self, node: NodeInput, default: Optional[HistoryTimestamp] = None) -> Optional[HistoryTimestamp]: """ Get value for node @@ -4224,7 +4154,7 @@ class HistoryTimestampView(object): Iterator[HistoryTimestamp]: Iterator over values """ -class HistoryDateTimeView(object): +class HistoryDateTimeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4281,9 +4211,7 @@ class HistoryDateTimeView(object): NodeStateHistoryDateTime: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[HistoryDateTime] = None - ) -> Optional[HistoryDateTime]: + def get(self, node: NodeInput, default: Optional[HistoryDateTime] = None) -> Optional[HistoryDateTime]: """ Get value for node @@ -4338,7 +4266,7 @@ class HistoryDateTimeView(object): Iterator[HistoryDateTime]: Iterator over values """ -class HistoryEventIdView(object): +class HistoryEventIdView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4395,9 +4323,7 @@ class HistoryEventIdView(object): NodeStateHistoryEventId: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[HistoryEventId] = None - ) -> Optional[HistoryEventId]: + def get(self, node: NodeInput, default: Optional[HistoryEventId] = None) -> Optional[HistoryEventId]: """ Get value for node @@ -4452,7 +4378,7 @@ class HistoryEventIdView(object): Iterator[HistoryEventId]: Iterator over values """ -class IntervalsView(object): +class IntervalsView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4509,9 +4435,7 @@ class IntervalsView(object): NodeStateIntervals: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[Intervals] = None - ) -> Optional[Intervals]: + def get(self, node: NodeInput, default: Optional[Intervals] = None) -> Optional[Intervals]: """ Get value for node @@ -4598,7 +4522,7 @@ class IntervalsView(object): Iterator[Intervals]: Iterator over values """ -class IntervalsFloatView(object): +class IntervalsFloatView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4666,9 +4590,7 @@ class IntervalsFloatView(object): NodeStateOptionF64: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[Optional[float]] = None - ) -> Optional[Optional[float]]: + def get(self, node: NodeInput, default: Optional[Optional[float]] = None) -> Optional[Optional[float]]: """ Get value for node @@ -4794,7 +4716,7 @@ class IntervalsFloatView(object): Iterator[Optional[float]]: Iterator over values """ -class IntervalsIntegerView(object): +class IntervalsIntegerView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4862,9 +4784,7 @@ class IntervalsIntegerView(object): NodeStateOptionI64: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[Optional[int]] = None - ) -> Optional[Optional[int]]: + def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: """ Get value for node @@ -4990,7 +4910,7 @@ class IntervalsIntegerView(object): Iterator[Optional[int]]: Iterator over values """ -class EdgeHistoryCountView(object): +class EdgeHistoryCountView(object): """A lazy view over node values""" def __eq__(self, value): @@ -5201,7 +5121,8 @@ class EdgeHistoryCountView(object): Iterator[int]: Iterator over values """ -class UsizeIterable(object): +class UsizeIterable(object): + def __eq__(self, value): """Return self==value.""" @@ -5269,7 +5190,7 @@ class UsizeIterable(object): Any: """ -class NodeTypeView(object): +class NodeTypeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -5337,9 +5258,7 @@ class NodeTypeView(object): NodeStateOptionStr: the computed `NodeState` """ - def get( - self, node: NodeInput, default: Optional[Optional[str]] = None - ) -> Optional[Optional[str]]: + def get(self, node: NodeInput, default: Optional[Optional[str]] = None) -> Optional[Optional[str]]: """ Get value for node @@ -5473,7 +5392,8 @@ class NodeTypeView(object): Iterator[Optional[str]]: Iterator over values """ -class NodeStateOptionStr(object): +class NodeStateOptionStr(object): + def __eq__(self, value): """Return self==value.""" @@ -5515,9 +5435,7 @@ class NodeStateOptionStr(object): NodeStateOptionStr: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[Optional[str]] = None - ) -> Optional[Optional[str]]: + def get(self, node: NodeInput, default: Optional[Optional[str]] = None) -> Optional[Optional[str]]: """ Get value for node @@ -5651,7 +5569,8 @@ class NodeStateOptionStr(object): Iterator[Optional[str]]: Iterator over values """ -class NodeStateListDateTime(object): +class NodeStateListDateTime(object): + def __eq__(self, value): """Return self==value.""" @@ -5693,9 +5612,7 @@ class NodeStateListDateTime(object): NodeStateListDateTime: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[list[datetime]] = None - ) -> Optional[list[datetime]]: + def get(self, node: NodeInput, default: Optional[list[datetime]] = None) -> Optional[list[datetime]]: """ Get value for node @@ -5821,7 +5738,8 @@ class NodeStateListDateTime(object): Iterator[list[datetime]]: Iterator over values """ -class NodeStateWeightedSP(object): +class NodeStateWeightedSP(object): + def __eq__(self, value): """Return self==value.""" @@ -5852,9 +5770,7 @@ class NodeStateWeightedSP(object): def __repr__(self): """Return repr(self).""" - def get( - self, node: NodeInput, default: Optional[Tuple[float, Nodes]] = None - ) -> Optional[Tuple[float, Nodes]]: + def get(self, node: NodeInput, default: Optional[Tuple[float, Nodes]] = None) -> Optional[Tuple[float, Nodes]]: """ Get value for node @@ -5909,7 +5825,8 @@ class NodeStateWeightedSP(object): Iterator[Tuple[float, Nodes]]: Iterator over values """ -class NodeStateF64(object): +class NodeStateF64(object): + def __eq__(self, value): """Return self==value.""" @@ -6094,7 +6011,8 @@ class NodeStateF64(object): Iterator[float]: Iterator over values """ -class NodeStateOptionF64(object): +class NodeStateOptionF64(object): + def __eq__(self, value): """Return self==value.""" @@ -6136,9 +6054,7 @@ class NodeStateOptionF64(object): NodeStateOptionF64: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[Optional[float]] = None - ) -> Optional[Optional[float]]: + def get(self, node: NodeInput, default: Optional[Optional[float]] = None) -> Optional[Optional[float]]: """ Get value for node @@ -6264,7 +6180,8 @@ class NodeStateOptionF64(object): Iterator[Optional[float]]: Iterator over values """ -class NodeStateNodes(object): +class NodeStateNodes(object): + def __eq__(self, value): """Return self==value.""" @@ -6350,7 +6267,8 @@ class NodeStateNodes(object): Iterator[Nodes]: Iterator over values """ -class NodeStateReachability(object): +class NodeStateReachability(object): + def __eq__(self, value): """Return self==value.""" @@ -6381,9 +6299,7 @@ class NodeStateReachability(object): def __repr__(self): """Return repr(self).""" - def get( - self, node: NodeInput, default: Optional[list[Tuple[int, str]]] = None - ) -> Optional[list[Tuple[int, str]]]: + def get(self, node: NodeInput, default: Optional[list[Tuple[int, str]]] = None) -> Optional[list[Tuple[int, str]]]: """ Get value for node @@ -6438,7 +6354,8 @@ class NodeStateReachability(object): Iterator[list[Tuple[int, str]]]: Iterator over values """ -class NodeStateListF64(object): +class NodeStateListF64(object): + def __eq__(self, value): """Return self==value.""" @@ -6469,9 +6386,7 @@ class NodeStateListF64(object): def __repr__(self): """Return repr(self).""" - def get( - self, node: NodeInput, default: Optional[list[float]] = None - ) -> Optional[list[float]]: + def get(self, node: NodeInput, default: Optional[list[float]] = None) -> Optional[list[float]]: """ Get value for node @@ -6526,7 +6441,8 @@ class NodeStateListF64(object): Iterator[list[float]]: Iterator over values """ -class NodeStateMotifs(object): +class NodeStateMotifs(object): + def __eq__(self, value): """Return self==value.""" @@ -6568,9 +6484,7 @@ class NodeStateMotifs(object): NodeStateMotifs: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[list[int]] = None - ) -> Optional[list[int]]: + def get(self, node: NodeInput, default: Optional[list[int]] = None) -> Optional[list[int]]: """ Get value for node @@ -6696,7 +6610,8 @@ class NodeStateMotifs(object): Iterator[list[int]]: Iterator over values """ -class NodeStateHits(object): +class NodeStateHits(object): + def __eq__(self, value): """Return self==value.""" @@ -6738,9 +6653,7 @@ class NodeStateHits(object): NodeStateHits: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[Tuple[float, float]] = None - ) -> Optional[Tuple[float, float]]: + def get(self, node: NodeInput, default: Optional[Tuple[float, float]] = None) -> Optional[Tuple[float, float]]: """ Get value for node @@ -6866,7 +6779,7 @@ class NodeStateHits(object): Iterator[Tuple[float, float]]: Iterator over values """ -class NodeStateHistory(object): +class NodeStateHistory(object): """A NodeState of History objects for each node.""" def __eq__(self, value): @@ -6941,9 +6854,7 @@ class NodeStateHistory(object): History: A history object containing all time entries. """ - def get( - self, node: NodeInput, default: Optional[History] = None - ) -> Optional[History]: + def get(self, node: NodeInput, default: Optional[History] = None) -> Optional[History]: """ Get History object for the node. @@ -7024,7 +6935,8 @@ class NodeStateHistory(object): Iterator[History]: Iterator over History objects. """ -class NodeStateHistoryTimestamp(object): +class NodeStateHistoryTimestamp(object): + def __eq__(self, value): """Return self==value.""" @@ -7055,9 +6967,7 @@ class NodeStateHistoryTimestamp(object): def __repr__(self): """Return repr(self).""" - def get( - self, node: NodeInput, default: Optional[HistoryTimestamp] = None - ) -> Optional[HistoryTimestamp]: + def get(self, node: NodeInput, default: Optional[HistoryTimestamp] = None) -> Optional[HistoryTimestamp]: """ Get value for node @@ -7112,7 +7022,8 @@ class NodeStateHistoryTimestamp(object): Iterator[HistoryTimestamp]: Iterator over values """ -class NodeStateHistoryDateTime(object): +class NodeStateHistoryDateTime(object): + def __eq__(self, value): """Return self==value.""" @@ -7143,9 +7054,7 @@ class NodeStateHistoryDateTime(object): def __repr__(self): """Return repr(self).""" - def get( - self, node: NodeInput, default: Optional[HistoryDateTime] = None - ) -> Optional[HistoryDateTime]: + def get(self, node: NodeInput, default: Optional[HistoryDateTime] = None) -> Optional[HistoryDateTime]: """ Get value for node @@ -7200,7 +7109,8 @@ class NodeStateHistoryDateTime(object): Iterator[HistoryDateTime]: Iterator over values """ -class NodeStateHistoryEventId(object): +class NodeStateHistoryEventId(object): + def __eq__(self, value): """Return self==value.""" @@ -7231,9 +7141,7 @@ class NodeStateHistoryEventId(object): def __repr__(self): """Return repr(self).""" - def get( - self, node: NodeInput, default: Optional[HistoryEventId] = None - ) -> Optional[HistoryEventId]: + def get(self, node: NodeInput, default: Optional[HistoryEventId] = None) -> Optional[HistoryEventId]: """ Get value for node @@ -7288,7 +7196,8 @@ class NodeStateHistoryEventId(object): Iterator[HistoryEventId]: Iterator over values """ -class NodeStateIntervals(object): +class NodeStateIntervals(object): + def __eq__(self, value): """Return self==value.""" @@ -7319,9 +7228,7 @@ class NodeStateIntervals(object): def __repr__(self): """Return repr(self).""" - def get( - self, node: NodeInput, default: Optional[Intervals] = None - ) -> Optional[Intervals]: + def get(self, node: NodeInput, default: Optional[Intervals] = None) -> Optional[Intervals]: """ Get value for node @@ -7416,7 +7323,8 @@ class NodeStateIntervals(object): Iterator[Intervals]: Iterator over values """ -class NodeStateSEIR(object): +class NodeStateSEIR(object): + def __eq__(self, value): """Return self==value.""" @@ -7458,9 +7366,7 @@ class NodeStateSEIR(object): NodeStateSEIR: The k smallest values as a node state """ - def get( - self, node: NodeInput, default: Optional[Infected] = None - ) -> Optional[Infected]: + def get(self, node: NodeInput, default: Optional[Infected] = None) -> Optional[Infected]: """ Get value for node @@ -7586,7 +7492,8 @@ class NodeStateSEIR(object): Iterator[Infected]: Iterator over values """ -class NodeLayout(object): +class NodeLayout(object): + def __eq__(self, value): """Return self==value.""" @@ -7617,9 +7524,7 @@ class NodeLayout(object): def __repr__(self): """Return repr(self).""" - def get( - self, node: NodeInput, default: Optional[list[float]] = None - ) -> Optional[list[float]]: + def get(self, node: NodeInput, default: Optional[list[float]] = None) -> Optional[list[float]]: """ Get value for node @@ -7674,7 +7579,8 @@ class NodeLayout(object): Iterator[list[float]]: Iterator over values """ -class NodeStateF64String(object): +class NodeStateF64String(object): + def __eq__(self, value): """Return self==value.""" @@ -7705,9 +7611,7 @@ class NodeStateF64String(object): def __repr__(self): """Return repr(self).""" - def get( - self, node: NodeInput, default: Optional[Tuple[float, str]] = None - ) -> Optional[Tuple[float, str]]: + def get(self, node: NodeInput, default: Optional[Tuple[float, str]] = None) -> Optional[Tuple[float, str]]: """ Get value for node @@ -7762,7 +7666,8 @@ class NodeStateF64String(object): Iterator[Tuple[float, str]]: Iterator over values """ -class OutputNodeState(object): +class OutputNodeState(object): + def __eq__(self, value): """Return self==value.""" @@ -7836,13 +7741,7 @@ class OutputNodeState(object): Iterator[Tuple[Node, Dict]]: Iterator over items """ - def merge( - self, - other: OutputNodeState, - index_merge_priority: str = "left", - default_column_merge_priority: str = "left", - column_merge_priority_map: Optional[dict] = None, - ) -> OutputNodeState: + def merge(self, other: OutputNodeState, index_merge_priority: str = "left", default_column_merge_priority: str = "left", column_merge_priority_map: Optional[dict] = None) -> OutputNodeState: """ Merge with another OutputNodeState (produces new OutputNodeState) diff --git a/python/python/raphtory/vectors/__init__.pyi b/python/python/raphtory/vectors/__init__.pyi index 96a8a83300..7033452a15 100644 --- a/python/python/raphtory/vectors/__init__.pyi +++ b/python/python/raphtory/vectors/__init__.pyi @@ -29,27 +29,11 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = [ - "VectorisedGraph", - "Document", - "Embedding", - "VectorSelection", - "OpenAIEmbeddings", - "VectorCache", - "EmbeddingServer", - "RunningEmbeddingServer", - "embedding_server", -] - -class VectorisedGraph(object): +__all__ = ['VectorisedGraph', 'Document', 'Embedding', 'VectorSelection', 'OpenAIEmbeddings', 'VectorCache', 'EmbeddingServer', 'RunningEmbeddingServer', 'embedding_server'] +class VectorisedGraph(object): """VectorisedGraph object that contains embedded documents that correspond to graph entities.""" - def edges_by_similarity( - self, - query: str | list, - limit: int, - window: Optional[Tuple[int | str, int | str]] = None, - ) -> VectorSelection: + def edges_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> VectorSelection: """ Perform a similarity search between each edge's associated document and a specified `query`. Returns a number of edges up to a specified `limit` ranked in ascending order of distance. @@ -70,12 +54,7 @@ class VectorisedGraph(object): VectorSelection: """ - def entities_by_similarity( - self, - query: str | list, - limit: int, - window: Optional[Tuple[int | str, int | str]] = None, - ) -> VectorSelection: + def entities_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> VectorSelection: """ Perform a similarity search between each entity's associated document and a specified `query`. Returns a number of entities up to a specified `limit` ranked in ascending order of distance. @@ -88,12 +67,7 @@ class VectorisedGraph(object): VectorSelection: The vector selection resulting from the search. """ - def nodes_by_similarity( - self, - query: str | list, - limit: int, - window: Optional[Tuple[int | str, int | str]] = None, - ) -> VectorSelection: + def nodes_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> VectorSelection: """ Perform a similarity search between each node's associated document and a specified `query`. Returns a number of nodes up to a specified `limit` ranked in ascending order of distance. @@ -114,7 +88,7 @@ class VectorisedGraph(object): None: """ -class Document(object): +class Document(object): """A document corresponding to a graph entity. Used to generate embeddings.""" def __repr__(self): @@ -147,7 +121,8 @@ class Document(object): Optional[Any]: """ -class Embedding(object): +class Embedding(object): + def __repr__(self): """Return repr(self).""" @@ -159,7 +134,8 @@ class Embedding(object): pyarrow.Array: """ -class VectorSelection(object): +class VectorSelection(object): + def add_edges(self, edges: list) -> None: """ Add all the documents associated with the specified `edges` to the current selection. @@ -205,9 +181,7 @@ class VectorSelection(object): list[Edge]: List of edges in the current selection. """ - def expand( - self, hops: int, window: Optional[Tuple[int | str, int | str]] = None - ) -> None: + def expand(self, hops: int, window: Optional[Tuple[int | str, int | str]] = None) -> None: """ Add all the documents a specified number of `hops` away from the selection. @@ -224,12 +198,7 @@ class VectorSelection(object): None: """ - def expand_edges_by_similarity( - self, - query: str | list, - limit: int, - window: Optional[Tuple[int | str, int | str]] = None, - ) -> None: + def expand_edges_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> None: """ Add to the selection the `limit` adjacent edges closest to `query` @@ -244,12 +213,7 @@ class VectorSelection(object): None: """ - def expand_entities_by_similarity( - self, - query: str | list, - limit: int, - window: Optional[Tuple[int | str, int | str]] = None, - ) -> None: + def expand_entities_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> None: """ Add to the selection the `limit` adjacent entities closest to `query` @@ -271,12 +235,7 @@ class VectorSelection(object): None: """ - def expand_nodes_by_similarity( - self, - query: str | list, - limit: int, - window: Optional[Tuple[int | str, int | str]] = None, - ) -> None: + def expand_nodes_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> None: """ Add to the selection the `limit` adjacent nodes closest to `query` @@ -315,7 +274,7 @@ class VectorSelection(object): list[Node]: List of nodes in the current selection. """ -class OpenAIEmbeddings(object): +class OpenAIEmbeddings(object): """ OpenAI-compatible embedding configuration. Pass an instance of this to `VectorCache(...)` to drive `vectorise(...)`. @@ -329,18 +288,10 @@ class OpenAIEmbeddings(object): dim (int, optional): Embedding dimension override. If None, the model's native dimension is used. Defaults to None. """ - def __new__( - cls, - model: str = "text-embedding-3-small", - api_base: Optional[str] = None, - api_key_env: Optional[str] = None, - org_id: Optional[str] = None, - project_id: Optional[str] = None, - dim: Optional[int] = None, - ) -> OpenAIEmbeddings: + def __new__(cls, model: str = 'text-embedding-3-small', api_base: Optional[str] = None, api_key_env: Optional[str] = None, org_id: Optional[str] = None, project_id: Optional[str] = None, dim: Optional[int] = None) -> OpenAIEmbeddings: """Create and return a new object. See help(type) for accurate signature.""" -class VectorCache(object): +class VectorCache(object): """ Cache wrapping an embedding model. Pass to `Graph.vectorise(model=...)` or other vectorisation entry points. @@ -350,12 +301,11 @@ class VectorCache(object): cache (str, optional): Path to persist the embedding cache on disk. Defaults to None. """ - def __new__( - cls, v_cache: OpenAIEmbeddings, cache: Optional[str] = None - ) -> VectorCache: + def __new__(cls, v_cache: OpenAIEmbeddings, cache: Optional[str] = None) -> VectorCache: """Create and return a new object. See help(type) for accurate signature.""" -class EmbeddingServer(object): +class EmbeddingServer(object): + def run(self, port: int, host: Optional[str] = None) -> None: """ Run the embedding server in the foreground until it's stopped. @@ -380,9 +330,14 @@ class EmbeddingServer(object): RunningEmbeddingServer: handle to stop the server. """ -class RunningEmbeddingServer(object): - def __enter__(self): ... - def __exit__(self, _exc_type, _exc_val, _exc_tb): ... +class RunningEmbeddingServer(object): + + def __enter__(self): + ... + + def __exit__(self, _exc_type, _exc_val, _exc_tb): + ... + def stop(self) -> None: """ Stop the running embedding server. diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 0386919c28..8f5f2a5315 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -8055,23 +8055,30 @@ mod test_node_property_filter_agg { let expected: Vec<&str> = vec!["n5"]; apply_assertion(filter, &expected); - // A constant is validated by value-castability against the list's - // element type, so a sum that only exists past that type's range - // cannot be matched with a wider constant. + // A sum widens past its element type, so the constant it is compared + // against is measured against the widened type. let filter = NodeFilter.property("p_u8s_max").sum().eq(Prop::U64(510)); - apply_assertion_err(filter, "cannot be coerced to U8"); + apply_assertion(filter, &["n1"]); let filter = NodeFilter .property("p_u16s_max") .sum() .eq(Prop::U64(131070)); - apply_assertion_err(filter, "cannot be coerced to U16"); + apply_assertion(filter, &["n1"]); let filter = NodeFilter .property("p_u32s_max") .sum() .eq(Prop::U64(8589934590)); - apply_assertion_err(filter, "cannot be coerced to U32"); + apply_assertion(filter, &["n1"]); + + // Reductions that return an element keep the element type, so a + // constant outside its range still cannot match. + let filter = NodeFilter.property("p_u8s_max").max().eq(Prop::U64(510)); + apply_assertion_err(filter, "cannot be coerced to U8"); + + let filter = NodeFilter.property("p_u8s_max").min().eq(Prop::U64(510)); + apply_assertion_err(filter, "cannot be coerced to U8"); let filter = NodeFilter.property("p_u64s_max").sum().gt(Prop::U64(0)); let expected: Vec<&str> = vec!["n1", "n5"]; diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 5e3800e292..10158b1f6d 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -87,6 +87,8 @@ where } pub trait DynCreateOp: DynEntityExpr { + fn dyn_selects_node_id(&self) -> bool; + fn dyn_create_node_op<'g>( &self, graph: Arc, @@ -109,6 +111,10 @@ pub trait DynCreateOp: DynEntityExpr { } impl DynCreateOp for E { + fn dyn_selects_node_id(&self) -> bool { + self.selects_node_id() + } + fn dyn_create_node_op<'g>( &self, graph: Arc, @@ -157,6 +163,10 @@ impl EntityExpr for Arc { impl EntityExprBuilder for Arc {} impl CreateOp for Arc { + fn selects_node_id(&self) -> bool { + self.as_ref().dyn_selects_node_id() + } + fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, 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 c3d484b131..dc6adc8c1c 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 @@ -125,6 +125,10 @@ impl EntityExpr for Id { impl EntityExprBuilder for Id {} impl CreateOp for Id { + fn selects_node_id(&self) -> bool { + true + } + fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, 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 163731cafb..7ab09df778 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 @@ -42,8 +42,9 @@ use super::{ ops::{ - AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, ListAwareCmpNodeOp, ListAwareSetNodeOp, - ListAwareStringNodeOp, ListAwareUnaryNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, + gid_for_id_lookup, AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, IdDomainNodeOp, + ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, ListAwareUnaryNodeOp, + PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, CreateOp, EntityExpr, EntityExprBuilder, Marker, }; @@ -75,7 +76,10 @@ use crate::{ errors::GraphError, prelude::{EdgeFilter, NodeFilter}, }; -use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use raphtory_api::core::entities::{ + properties::prop::{Prop, PropType}, + GID, +}; use std::sync::Arc; // ───────────────────────────────────────────────────────────────────────────── // BinaryCmpExpr — binary expression filter @@ -219,6 +223,7 @@ where filtered: F, ) -> Result, GraphError> { let expr_pt = self.left.prop_type(); + let id_type = filtered.id_type(); let (left, quals) = self.left.create_qualified_node_op(filtered.clone())?; let right = self.right.create_node_op(filtered)?; let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len())?; @@ -229,11 +234,22 @@ where None => validate_types_compatible(&lhs_pt, &rhs_pt)?, } if quals.is_empty() { - Ok(Arc::new(BinaryCmpNodeOp { + let cmp: Arc + 'graph> = Arc::new(BinaryCmpNodeOp { left, - right, + right: right.clone(), op: self.op, - })) + }); + if self.op == BinaryOp::Eq && self.left.selects_node_id() { + if let Some(Some(value)) = right.const_value() { + if let Some(gid) = gid_for_id_lookup(id_type, &value) { + return Ok(Arc::new(IdDomainNodeOp { + gids: Arc::from([gid]), + inner: cmp, + })); + } + } + } + Ok(cmp) } else { let elemwise = Arc::new(ListAwareCmpNodeOp { left, @@ -793,15 +809,31 @@ impl CreateFilter for PropValueSetExpr { filtered: F, ) -> Result, GraphError> { let expr_pt = self.expr.prop_type(); + let id_type = filtered.id_type(); let (inner, quals) = self.expr.create_qualified_node_op(filtered)?; let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; let values = coerce_set_values(&lhs_pt, self.values)?; if quals.is_empty() { - Ok(Arc::new(PropValueSetNodeOp { + let gids: Option> = (self.op == SetOp::IsIn && self.expr.selects_node_id()) + .then(|| { + values + .iter() + .map(|v| gid_for_id_lookup(id_type, v)) + .collect() + }) + .flatten(); + let set_op: Arc + 'graph> = Arc::new(PropValueSetNodeOp { inner, values, op: self.op, - })) + }); + match gids { + Some(gids) => Ok(Arc::new(IdDomainNodeOp { + gids: gids.into(), + inner: set_op, + })), + None => Ok(set_op), + } } else { let elemwise = Arc::new(ListAwareSetNodeOp { inner, 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 index 9f3b599096..bc3313032f 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -48,6 +48,13 @@ pub use ops::*; /// ``` /// pub trait CreateOp: EntityExpr + Clone + Send + Sync + 'static { + /// Whether this expression selects the node id field. Comparisons against + /// constants use it to narrow the evaluation domain to the named nodes + /// instead of scanning every node. + fn selects_node_id(&self) -> bool { + false + } + /// Compile the expression against a specific graph view. /// /// Any name→ID resolution (property, metadata) happens here, once. 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 index 6303899636..0391e37981 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -38,6 +38,7 @@ use super::EdgeOp; use crate::{ + core::entities::nodes::node_ref::AsNodeRef, db::{ api::{ properties::PropertiesOps, @@ -60,7 +61,7 @@ use bigdecimal::BigDecimal; use raphtory_api::core::entities::{ edges::edge_ref::EdgeRef, properties::prop::{IntoProp, Prop, PropArray, PropType}, - VID, + GidType, GID, VID, }; use raphtory_storage::graph::graph::GraphStorage; use std::sync::Arc; @@ -157,8 +158,8 @@ impl NodeOp for WithPropType { self.inner.const_value() } - fn const_value_in_domain(&self) -> Option { - self.inner.const_value_in_domain() + fn const_value_in_domain(&self, storage: &GraphStorage) -> Option { + self.inner.const_value_in_domain(storage) } fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { @@ -224,19 +225,75 @@ impl NodeOp for TemporalNodePropOp { // ───────────────────────────────────────────────────────────────────────────── /// Aggregations collapse the innermost list level; outer levels survive so a -/// pending qualifier still sees per-element results. `scalar` names the type a -/// single aggregation step produces (`None` keeps the element type, as for -/// sum/min/max/first/last). -fn agg_out_type(pt: PropType, scalar: Option) -> PropType { +/// pending qualifier still sees per-element results. `elem_out` names the type a +/// single aggregation step produces from the element type it collapses. +fn agg_out_type_with(pt: PropType, elem_out: &dyn Fn(PropType) -> PropType) -> PropType { match pt { PropType::List(inner) => match *inner { - nested @ PropType::List(_) => PropType::List(Box::new(agg_out_type(nested, scalar))), - elem => scalar.unwrap_or(elem), + nested @ PropType::List(_) => { + PropType::List(Box::new(agg_out_type_with(nested, elem_out))) + } + elem => elem_out(elem), }, other => other, } } +/// `scalar` names a fixed output type (`None` keeps the element type, as for +/// min/max/first/last). +fn agg_out_type(pt: PropType, scalar: Option) -> PropType { + agg_out_type_with(pt, &|elem| scalar.clone().unwrap_or(elem)) +} + +/// The type a sum produces from its element type. Summing widens: the +/// evaluator below accumulates every unsigned width into a `U64`, every signed +/// width into an `I64`, and either into a `Decimal` when that overflows, so a +/// narrow element type would understate what the sum can hold. Keep the arms +/// in step with the evaluator's. +fn sum_out_type(pt: PropType) -> PropType { + agg_out_type_with(pt, &|elem| match elem { + PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => PropType::U64, + PropType::I32 | PropType::I64 => PropType::I64, + PropType::F32 | PropType::F64 => PropType::F64, + other => other, + }) +} + +#[cfg(test)] +mod sum_out_type_tests { + use super::{sum_out_type, PropType}; + + fn list(inner: PropType) -> PropType { + PropType::List(Box::new(inner)) + } + + // The declared type has to hold every value the evaluator can produce, or + // a constant comparison is validated against a range the sum can exceed. + #[test] + fn narrow_numeric_elements_widen() { + for elem in [PropType::U8, PropType::U16, PropType::U32, PropType::U64] { + assert_eq!(sum_out_type(list(elem)), PropType::U64); + } + for elem in [PropType::I32, PropType::I64] { + assert_eq!(sum_out_type(list(elem)), PropType::I64); + } + for elem in [PropType::F32, PropType::F64] { + assert_eq!(sum_out_type(list(elem)), PropType::F64); + } + } + + #[test] + fn non_numeric_elements_and_scalars_are_unchanged() { + assert_eq!(sum_out_type(list(PropType::Str)), PropType::Str); + assert_eq!(sum_out_type(PropType::U8), PropType::U8); + } + + #[test] + fn only_the_innermost_list_level_collapses() { + assert_eq!(sum_out_type(list(list(PropType::U8))), list(PropType::U64)); + } +} + macro_rules! impl_agg_entity_op { ($node_name:ident, $edge_name:ident, $out_pt:expr, $body:expr) => { #[derive(Clone)] @@ -279,7 +336,7 @@ macro_rules! impl_agg_entity_op { }; } -impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |pt| agg_out_type(pt, None), |vals| { +impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |pt| sum_out_type(pt), |vals| { aggregate_list_values(vals, &|pi| { let mut vals = pi.peekable(); if vals.peek().is_none() { @@ -682,6 +739,66 @@ impl<'g, T: Comparable + Clone + Send + Sync + 'static> NodeOp for BinaryCmpNode } } +// ───────────────────────────────────────────────────────────────────────────── +// IdDomainNodeOp<'g> — id comparisons resolve their domain directly +// ───────────────────────────────────────────────────────────────────────────── + +/// The GID to look up for an id comparison against `value`, or `None` when the +/// constant's type does not match the graph's id type. `domain` must be a +/// superset of the matches, so a constant that only compares equal after value +/// coercion falls back to the unrestricted domain instead of guessing. +pub(crate) fn gid_for_id_lookup(id_type: Option, value: &Prop) -> Option { + match (id_type?, value) { + (GidType::Str, Prop::Str(s)) => Some(GID::Str(s.to_string())), + (GidType::U64, Prop::U64(n)) => Some(GID::U64(*n)), + (GidType::U64, Prop::U32(n)) => Some(GID::U64(*n as u64)), + (GidType::U64, Prop::U16(n)) => Some(GID::U64(*n as u64)), + (GidType::U64, Prop::U8(n)) => Some(GID::U64(*n as u64)), + (GidType::U64, Prop::I64(n)) => u64::try_from(*n).ok().map(GID::U64), + (GidType::U64, Prop::I32(n)) => u64::try_from(*n).ok().map(GID::U64), + _ => None, + } +} + +/// Wraps a compiled boolean filter whose only possible matches are the nodes +/// with the given ids: `domain` resolves them directly instead of scanning +/// every node. An id that does not exist simply resolves to nothing. +#[derive(Clone)] +pub struct IdDomainNodeOp<'g> { + pub(crate) gids: Arc<[GID]>, + pub(crate) inner: Arc + 'g>, +} + +impl<'g> NodeOp for IdDomainNodeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + self.inner.apply(storage, node) + } + + fn domain(&self, storage: &GraphStorage) -> NodeList { + NodeList::List { + elems: self + .gids + .iter() + .filter_map(|gid| storage.internalise_node(gid.as_node_ref())) + .collect(), + } + } + + fn const_value(&self) -> Option { + self.inner.const_value() + } + + fn const_value_in_domain(&self, storage: &GraphStorage) -> Option { + self.inner.const_value_in_domain(storage) + } + + fn prop_type(&self) -> PropType { + self.inner.prop_type() + } +} + // ───────────────────────────────────────────────────────────────────────────── // StringNodeOp<'g, T> — applies a StringOp to two NodeOp // ───────────────────────────────────────────────────────────────────────────── 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 7a8fc47b49..5e1e82b6b8 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 @@ -1,7 +1,13 @@ use super::*; use crate::{ db::{ - api::{state::ops::Id, view::filter_ops::Select}, + api::{ + state::ops::{Id, Name, NodeOp}, + view::{ + filter_ops::Select, + internal::{CoreGraphOps, NodeList}, + }, + }, graph::views::filter::{ model::{ filter_operator::BinaryOp, node_filter::NodeFilter, PropertyExprFactory, @@ -15,7 +21,7 @@ use crate::{ use raphtory_api::core::{ entities::{ properties::prop::{IntoProp, Prop}, - GID, + GID, VID, }, Direction, }; @@ -574,3 +580,121 @@ fn ordering_op_on_bool_prop_returns_error() { "expected Err for ordering op on boolean property" ); } + +// ── Domain narrowing for id comparisons ────────────────────────────────── + +/// The nodes a compiled node filter will actually visit. +fn filter_domain(filter: F, g: &Graph) -> NodeList +where + F: CreateFilter + Clone, + for<'g> F::NodeFilter<'g, Graph, F::FilteredGraph<'g, Graph>>: NodeOp, +{ + let fg = filter.clone().filter_graph_view(g.clone()).unwrap(); + let op = filter.create_node_filter(g.clone(), fg).unwrap(); + op.domain(&g.core_graph().lock()) +} + +#[test] +fn id_eq_visits_only_the_named_node() { + let g = build_test_graph(); + let vid = g.node("b").unwrap().node; + match filter_domain(Id.eq("b"), &g) { + NodeList::List { elems } => { + assert_eq!(elems.into_iter().collect::>(), vec![vid]) + } + other => panic!("id equality should narrow the domain, got {other:?}"), + } +} + +#[test] +fn id_is_in_visits_only_the_named_nodes() { + let g = build_test_graph(); + let mut expected: Vec = ["a", "c"].iter().map(|n| g.node(n).unwrap().node).collect(); + expected.sort(); + match filter_domain(Id.is_in(vec!["a".into_prop(), "c".into_prop()]), &g) { + NodeList::List { elems } => { + let mut got: Vec = elems.into_iter().collect(); + got.sort(); + assert_eq!(got, expected); + } + other => panic!("id set membership should narrow the domain, got {other:?}"), + } +} + +#[test] +fn id_eq_for_an_absent_node_visits_nothing() { + let g = build_test_graph(); + match filter_domain(Id.eq("nope"), &g) { + NodeList::List { elems } => { + assert!(elems.into_iter().next().is_none()) + } + other => panic!("an absent id should narrow to nothing, got {other:?}"), + } +} + +#[test] +fn non_id_and_inequality_filters_keep_the_full_domain() { + let g = build_test_graph(); + // Only equality and set membership name specific nodes; everything else + // has to be evaluated per node. + for (label, domain) in [ + ("id != b", filter_domain(Id.ne("b"), &g)), + ("name == b", filter_domain(Name.eq("b"), &g)), + ( + "degree == 2", + filter_domain( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter, + } + .eq(2usize), + &g, + ), + ), + ] { + assert!( + matches!(domain, NodeList::All), + "{label} must keep the full domain, got {domain:?}" + ); + } +} + +// ── Sum widens past its element type ───────────────────────────────────── + +/// A graph whose node `n` carries `prop` as a list, so a sum over it exceeds +/// the range of the element type. +fn graph_with_list(prop: &str, values: Vec) -> Graph { + let g = Graph::new(); + g.add_node(0, "n", [(prop, Prop::List(values.into()))], None, None) + .unwrap(); + g +} + +#[test] +fn sum_of_signed_elements_matches_a_wider_constant() { + let g = graph_with_list("xs", vec![Prop::I32(i32::MAX), Prop::I32(i32::MAX)]); + let filter = NodeFilter.property("xs").sum().eq(i64::from(i32::MAX) * 2); + assert_eq!(filtered_names(filter, g), vec!["n"]); +} + +#[test] +fn sum_of_float_elements_compares_in_f64() { + let g = graph_with_list("xs", vec![Prop::F32(1.5), Prop::F32(2.25)]); + let filter = NodeFilter.property("xs").sum().eq(3.75f64); + assert_eq!(filtered_names(filter, g), vec!["n"]); +} + +#[test] +fn sum_over_a_nested_list_widens_only_the_innermost_level() { + // The outer level survives for a pending qualifier, so the comparison is + // element-wise against the widened inner sums. + let g = graph_with_list( + "xs", + vec![ + Prop::List(vec![Prop::U8(u8::MAX), Prop::U8(u8::MAX)].into()), + Prop::List(vec![Prop::U8(1), Prop::U8(1)].into()), + ], + ); + let filter = NodeFilter.property("xs").sum().eq(510u64).any(); + assert_eq!(filtered_names(filter, g), vec!["n"]); +} diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index e16115a63b..ae0585b3dc 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -58,6 +58,9 @@ impl PyEdgeEndpoint { #[pymethods] impl PyEdgeEndpoint { /// Selects the endpoint node ID field for filtering. + /// + /// Returns: + /// filter.Expr: fn id(&self) -> PyExpr { PyExpr::new( Arc::new(self.0.id()), @@ -66,6 +69,9 @@ impl PyEdgeEndpoint { } /// Selects the endpoint node name field for filtering. + /// + /// Returns: + /// filter.Expr: fn name(&self) -> PyExpr { PyExpr::new( Arc::new(self.0.name()), @@ -74,6 +80,9 @@ impl PyEdgeEndpoint { } /// Selects the endpoint node type field for filtering. + /// + /// Returns: + /// filter.Expr: fn node_type(&self) -> PyExpr { PyExpr::new( Arc::new(self.0.node_type()), @@ -85,6 +94,9 @@ impl PyEdgeEndpoint { /// /// Arguments: /// name (str): Property key. + /// + /// Returns: + /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); PyPropertyExpr::new(Arc::new(self.0.property(name)), Some(lhs)) @@ -94,6 +106,9 @@ impl PyEdgeEndpoint { /// /// Arguments: /// name (str): Metadata key. + /// + /// Returns: + /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); PyExpr::new( diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index f801c5b26a..ea8264a9f8 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -512,7 +512,7 @@ impl PyNodeFilter { /// Selects the node ID field for filtering. /// /// Returns: - /// filter.NodeIdFilterBuilder: + /// filter.Expr: fn id(&self) -> PyExpr { PyExpr( self.0.dyn_id(), @@ -523,7 +523,7 @@ impl PyNodeFilter { /// Selects the node name field for filtering. /// /// Returns: - /// filter.NodeNameFilterBuilder: + /// filter.Expr: fn name(&self) -> PyExpr { PyExpr( self.0.dyn_name(), @@ -534,7 +534,7 @@ impl PyNodeFilter { /// Selects the node type field for filtering. /// /// Returns: - /// filter.NodeTypeFilterBuilder: + /// filter.Expr: fn node_type(&self) -> PyExpr { PyExpr( self.0.dyn_node_type(), @@ -545,7 +545,7 @@ impl PyNodeFilter { /// Selects incoming node degree for filtering. /// /// Returns: - /// filter.FilterOps + /// filter.Expr: fn in_degree(&self) -> PyExpr { PyExpr( self.0.dyn_in_degree(), @@ -556,7 +556,7 @@ impl PyNodeFilter { /// Selects total node degree for filtering. /// /// Returns: - /// filter.FilterOps + /// filter.Expr: fn degree(&self) -> PyExpr { PyExpr( self.0.dyn_degree(), @@ -567,7 +567,7 @@ impl PyNodeFilter { /// Selects outgoing node degree for filtering. /// /// Returns: - /// filter.FilterOps + /// filter.Expr: fn out_degree(&self) -> PyExpr { PyExpr( self.0.dyn_out_degree(), @@ -583,7 +583,7 @@ impl PyNodeFilter { /// name (str): Property key. /// /// Returns: - /// filter.PropertyFilterOps: + /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); PyPropertyExpr(self.0.dyn_property(name), Some(lhs)) @@ -597,7 +597,7 @@ impl PyNodeFilter { /// name (str): Metadata key. /// /// Returns: - /// filter.FilterOps: + /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); PyExpr(self.0.dyn_metadata(name), Some(lhs)) @@ -612,7 +612,7 @@ impl PyNodeFilter { /// end (int): End time. /// /// Returns: - /// filter.NodeViewPropsFilterBuilder: + /// filter.Node: fn window(&self, start: EventTime, end: EventTime) -> PyNodeFilter { self.wrap( self.0.clone().window(start, end), @@ -626,7 +626,7 @@ impl PyNodeFilter { /// time (int): Event time. /// /// Returns: - /// filter.NodeViewPropsFilterBuilder: + /// filter.Node: fn at(&self, time: EventTime) -> PyNodeFilter { self.wrap( self.0.clone().at(time), @@ -640,7 +640,7 @@ impl PyNodeFilter { /// time (int): Lower time bound. /// /// Returns: - /// filter.NodeViewPropsFilterBuilder: + /// filter.Node: fn after(&self, time: EventTime) -> PyNodeFilter { self.wrap( self.0.clone().after(time), @@ -657,7 +657,7 @@ impl PyNodeFilter { /// time (int): Upper time bound. /// /// Returns: - /// filter.NodeViewPropsFilterBuilder: + /// filter.Node: fn before(&self, time: EventTime) -> PyNodeFilter { self.wrap( self.0.clone().before(time), @@ -668,7 +668,7 @@ impl PyNodeFilter { /// Evaluates filters against the latest available state of each node. /// /// Returns: - /// filter.NodeViewPropsFilterBuilder: + /// filter.Node: fn latest(&self) -> PyNodeFilter { self.wrap(self.0.clone().latest(), WireView::Latest) } @@ -679,7 +679,7 @@ impl PyNodeFilter { /// time (int): Snapshot time. /// /// Returns: - /// filter.NodeViewPropsFilterBuilder: + /// filter.Node: fn snapshot_at(&self, time: EventTime) -> PyNodeFilter { self.wrap(self.0.clone().snapshot_at(time), WireView::SnapshotAt(time)) } @@ -687,7 +687,7 @@ impl PyNodeFilter { /// Evaluates filters against the most recent snapshot of the graph. /// /// Returns: - /// filter.NodeViewPropsFilterBuilder: + /// filter.Node: fn snapshot_latest(&self) -> PyNodeFilter { self.wrap(self.0.clone().snapshot_latest(), WireView::SnapshotLatest) } @@ -698,7 +698,7 @@ impl PyNodeFilter { /// layer (str): Layer name. /// /// Returns: - /// filter.NodeViewPropsFilterBuilder: + /// filter.Node: fn layer(&self, layer: String) -> PyNodeFilter { self.wrap( self.0.clone().layer(vec![layer.clone()]), @@ -712,7 +712,7 @@ impl PyNodeFilter { /// layers (list[str]): Layer names. /// /// Returns: - /// filter.NodeViewPropsFilterBuilder: + /// filter.Node: fn layers(&self, layers: FromIterable) -> PyNodeFilter { let layers = layers.to_vec(); self.wrap( From d443d44bc4b59c99aa969ea819100b4384846451 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:30:25 +0100 Subject: [PATCH 114/155] compare constants within a type family, as the property filters do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A constant was accepted whenever it could be cast into the property's type, so a numeric string or a bool compared against a number and a string property compared against a decimal. Constants now follow the rule the property filters already stated: numerics compare with one another, everything else only with its own type. Ordering returns to booleans, which are ordered. Set membership drops members of a type the property can never equal rather than refusing the filter — being absent from a set is an answer, where a comparison has no answer to give. Degree keeps casting its constant, so a count still accepts the string form a wire query sends. That cast was previously skipped, leaving the comparison to fall through to a variant ordering rather than comparing counts at all. --- .../test_filters/test_edge_filter.py | 11 ++- .../test_filters/test_exploded_edge_filter.py | 56 ++++++-------- .../test_filters/test_node_filter.py | 8 +- .../test_filters/test_node_property_filter.py | 20 +++-- .../parity/test_parity_filters.py | 28 +++++-- raphtory-tests/tests/test_filters.rs | 7 +- .../src/db/graph/views/filter/model/mod.rs | 76 ++++++++++++------- .../views/filter/model/node_expr/dyn_expr.rs | 10 +++ .../views/filter/model/node_expr/exprs.rs | 4 + .../views/filter/model/node_expr/filters.rs | 31 +++++++- .../graph/views/filter/model/node_expr/mod.rs | 9 +++ .../views/filter/model/node_expr/tests.rs | 18 +++-- 12 files changed, 178 insertions(+), 100 deletions(-) diff --git a/python/tests/test_base_install/test_filters/test_edge_filter.py b/python/tests/test_base_install/test_filters/test_edge_filter.py index 0ce1aadb4d..d660dddd87 100644 --- a/python/tests/test_base_install/test_filters/test_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_filter.py @@ -512,13 +512,12 @@ def check(graph): @with_variants(init_graph2) def test_filter_edges_with_num_ids_error(): def check(graph): - # A numeric string coerces to the numeric id type, consistent with the - # castable-constant rule used across the expression comparisons. filter_expr = filter.Edge.src().id() == "3" - assert {(e.src.id, e.dst.id) for e in graph.filter(filter_expr).edges} == { - (3, 1), - (3, 4), - } + with pytest.raises( + Exception, + match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be coerced to U64', + ): + graph.filter(filter_expr).nodes.id return check diff --git a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py index 31f444f070..09aebff87e 100644 --- a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py @@ -311,12 +311,12 @@ def test_all_property_types(GraphClass): (filter.ExplodedEdge.property("weight").is_none(), 0), ( filter.ExplodedEdge.property("weight").is_in(["1", 2]), - 4, - ), # numeric strings coerce to the property type + 2, + ), # actually does the filter ( filter.ExplodedEdge.property("weight").is_not_in(["3"]), - 4, - ), # numeric strings coerce to the property type + 6, + ), # actually does the filter # confidence (float) (filter.ExplodedEdge.property("confidence") == 0.95, 1), (filter.ExplodedEdge.property("confidence") != 0.80, 5), @@ -349,6 +349,8 @@ def test_all_property_types(GraphClass): (filter.ExplodedEdge.property("name") > "dave", 2), (filter.ExplodedEdge.property("name") <= "dave", 4), (filter.ExplodedEdge.property("name") >= "dave", 4), + (filter.ExplodedEdge.property("name").is_in([1, 2]), 0), + (filter.ExplodedEdge.property("name").is_not_in([3, "dave"]), 4), (filter.ExplodedEdge.property("name").fuzzy_search("gabe", 2, False), 2), # active (bool) (filter.ExplodedEdge.property("active") == True, 4), @@ -358,6 +360,12 @@ def test_all_property_types(GraphClass): (filter.ExplodedEdge.property("active").is_not_in([False]), 4), (filter.ExplodedEdge.property("active").is_some(), 6), (filter.ExplodedEdge.property("active").is_none(), 0), + (filter.ExplodedEdge.property("active") < True, 2), + (filter.ExplodedEdge.property("active") > False, 4), + (filter.ExplodedEdge.property("active") >= False, 6), + (filter.ExplodedEdge.property("active") <= False, 2), + (filter.ExplodedEdge.property("active").is_in([1, 2]), 0), + (filter.ExplodedEdge.property("active").is_not_in([3]), 6), # created (datetime) (filter.ExplodedEdge.property("created") == datetime(2023, 1, 1), 1), (filter.ExplodedEdge.property("created") != datetime(2023, 1, 1), 5), @@ -379,6 +387,8 @@ def test_all_property_types(GraphClass): ), (filter.ExplodedEdge.property("created").is_some(), 6), (filter.ExplodedEdge.property("created").is_none(), 0), + (filter.ExplodedEdge.property("created").is_in([1, 2]), 0), + (filter.ExplodedEdge.property("created").is_not_in([3]), 6), # tags (list of str) (filter.ExplodedEdge.property("tags") == ["team_b", "remote"], 1), (filter.ExplodedEdge.property("tags") != ["team_b", "remote"], 5), @@ -433,26 +443,6 @@ def test_all_property_types(GraphClass): len(result) == expected ), f"Test {i} failed: expected {expected}, got {len(result)}" - # Ordering operators and non-boolean set values are rejected for boolean - # properties. - for make_bad in ( - lambda: filter.ExplodedEdge.property("active") < True, - lambda: filter.ExplodedEdge.property("active") >= False, - lambda: filter.ExplodedEdge.property("active").is_in([1, 2]), - lambda: filter.ExplodedEdge.property("active").is_not_in([3]), - lambda: filter.ExplodedEdge.property("name").is_in([1, 2]), - lambda: filter.ExplodedEdge.property("name").is_not_in([3, "dave"]), - lambda: filter.ExplodedEdge.property("created").is_in([1, 2]), - lambda: filter.ExplodedEdge.property("created").is_not_in([3]), - lambda: filter.ExplodedEdge.property("tags").is_in([1, 2]), - lambda: filter.ExplodedEdge.property("tags").is_in([1, 2, ["team_a", 0]]), - lambda: filter.ExplodedEdge.property("tags").is_not_in([3]), - ): - with pytest.raises( - Exception, match=r"not valid for boolean properties|cannot be coerced" - ): - g.filter(make_bad()).edges.explode() - nonsense_filter_cases = [ # Integers (weight) ( @@ -583,13 +573,13 @@ def test_all_property_types(GraphClass): # Numeric strings coerce to the property's numeric type: each string form # matches exactly what its native-typed twin matches. + # A string constant never compares against a numeric property, whether or + # not it happens to parse as a number. for prop, val in (("weight", 2), ("weight", 3), ("confidence", 2)): for op in ("__eq__", "__ne__", "__lt__", "__gt__", "__le__", "__ge__"): - typed = getattr(filter.ExplodedEdge.property(prop), op)(val) - coerced = getattr(filter.ExplodedEdge.property(prop), op)(str(val)) - assert len(g.filter(coerced).edges.explode()) == len( - g.filter(typed).edges.explode() - ), prop + " " + op + " " + str(val) + expr = getattr(filter.ExplodedEdge.property(prop), op)(str(val)) + with pytest.raises(Exception, match=r"of type Str cannot be coerced"): + g.filter(expr).edges.explode() wrong_types = [ # Integers (weight) @@ -654,19 +644,19 @@ def test_all_property_types(GraphClass): ), ( lambda: filter.ExplodedEdge.property("active") < 3, - "not valid for boolean properties", + "cannot be coerced to Bool", ), ( lambda: filter.ExplodedEdge.property("active") > 1, - "not valid for boolean properties", + "cannot be coerced to Bool", ), ( lambda: filter.ExplodedEdge.property("active") <= 2, - "not valid for boolean properties", + "cannot be coerced to Bool", ), ( lambda: filter.ExplodedEdge.property("active") >= 3, - "not valid for boolean properties", + "cannot be coerced to Bool", ), ( lambda: filter.ExplodedEdge.property("active").contains(2), diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index 44de84533c..de660965c5 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -1132,10 +1132,12 @@ def check(graph): @with_variants(init_graph2) def test_filter_nodes_with_num_ids_error(): def check(graph): - # A numeric string coerces to the numeric id type, consistent with the - # castable-constant rule used across the expression comparisons. filter_expr = filter.Node.id() == "3" - assert sorted(graph.filter(filter_expr).nodes.id) == [3] + with pytest.raises( + Exception, + match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be coerced to U64', + ): + graph.filter(filter_expr).nodes.id return check diff --git a/python/tests/test_base_install/test_filters/test_node_property_filter.py b/python/tests/test_base_install/test_filters/test_node_property_filter.py index 6321d3fb29..060a612288 100644 --- a/python/tests/test_base_install/test_filters/test_node_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_property_filter.py @@ -787,17 +787,15 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_with_with_qualifier_on_non_string(): def check(graph): - # A numeric string coerces to the element type under the - # castable-constant rule; a non-numeric string is rejected. - filter_expr = filter.Node.property("prop8").any() == "3" - assert sorted(graph.filter(filter_expr).nodes.id) == ["a", "d"] - - filter_expr = filter.Node.property("prop8").any() == "pometry" - with pytest.raises( - Exception, - match=r"of type Str cannot be coerced to I64", - ): - graph.filter(filter_expr).nodes.id + # A string constant never compares against a numeric property, + # whether or not it happens to parse as a number. + for value in ["3", "pometry"]: + filter_expr = filter.Node.property("prop8").any() == value + with pytest.raises( + Exception, + match=r"of type Str cannot be coerced to I64", + ): + graph.filter(filter_expr).nodes.id return check diff --git a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index 5b7d5c1f82..b8771da8e2 100644 --- a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py +++ b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py @@ -710,6 +710,29 @@ def _baseline(pair, key, probe, reach): } +def test_is_in_with_a_mistyped_value_matches_nothing_on_both_sides(filter_pair): + """A set member of a type the property can never equal is absent, not an error. + + Unlike a comparison against a mistyped value — which both sides reject — + set membership asks whether a value is present, and a member of an + unrelated type simply is not. That asymmetry has to be the *same* surprise + on both sides, since a caller cannot tell "no matches" from "bad query" + otherwise. + """ + build = lambda: f.Node.property("score").is_in(["not", "numbers"]) + assert_parity( + filter_pair, lambda g: sorted(n.name for n in g.filter(build()).nodes) + ) + + for side_name, side in ( + ("local", filter_pair.local), + ("remote", filter_pair.remote), + ): + assert [n.name for n in side.filter(build()).nodes] == [], ( + f"{side_name}: a mistyped is_in matched nodes; if this now raises " + f"or filters, move the case into REJECTED_EXPRS" + ) + @pytest.mark.parametrize("name", sorted(EXPRS), ids=sorted(EXPRS)) def test_expr_discriminates(filter_pair, name): """The expression must keep some entities and drop some — on each side. @@ -1051,11 +1074,6 @@ def test_getitem_narrows_membership_where_filter_rescopes(filter_pair): "reject.unknown_property": lambda: f.Node.property("nope") > 1, "reject.unknown_metadata": lambda: f.Node.metadata("nope") > 1, "reject.degree_vs_str": lambda: f.Node.degree() > "x", - # One element on purpose: the wire carries sets unordered, so with several - # bad values local and server may name different offenders in the error. - "reject.is_in_mistyped_values": lambda: f.Node.property("score").is_in( - ["banana"] - ), # `avg` is F64 and `len` is U64, so neither accepts a plain Python int here. } diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 8f5f2a5315..3e613a0fda 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -8073,12 +8073,13 @@ mod test_node_property_filter_agg { apply_assertion(filter, &["n1"]); // Reductions that return an element keep the element type, so a - // constant outside its range still cannot match. + // constant outside that element's range is a legal numeric comparison + // that nothing can equal. let filter = NodeFilter.property("p_u8s_max").max().eq(Prop::U64(510)); - apply_assertion_err(filter, "cannot be coerced to U8"); + apply_assertion(filter, &[]); let filter = NodeFilter.property("p_u8s_max").min().eq(Prop::U64(510)); - apply_assertion_err(filter, "cannot be coerced to U8"); + apply_assertion(filter, &[]); let filter = NodeFilter.property("p_u64s_max").sum().gt(Prop::U64(0)); let expected: Vec<&str> = vec!["n1", "n5"]; diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 605653e21f..4999d8836e 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -57,7 +57,10 @@ use crate::{ prelude::LayerOps, }; use raphtory_api::core::{ - entities::{properties::prop::Prop, Layer}, + entities::{ + properties::prop::{unify_types, Prop}, + Layer, + }, storage::timeindex::{AsTime, EventTime}, utils::time::IntoTime, }; @@ -1002,12 +1005,6 @@ pub fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), Gra op, BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge ) { - if *prop_type == PropType::Bool { - return Err(GraphError::InvalidFilter(format!( - "operator {:?} is not valid for boolean properties", - op - ))); - } if matches!(prop_type, PropType::Map(_)) { return Err(GraphError::InvalidFilter(format!( "operator {:?} is not valid for map properties", @@ -1053,6 +1050,24 @@ pub fn resolved_prop_type(expr_pt: PropType, op_pt: PropType) -> PropType { /// Reject a constant RHS value whose type cannot be coerced to the LHS type. /// +/// Cast a constant to the type an expression wants to compare it as, erroring +/// when it does not convert. Used where the expression defines the comparison's +/// type rather than adopting the constant's — see [`CreateOp::const_cast_type`]. +pub fn cast_const_to(target: &PropType, value: Option<&Prop>) -> Result, GraphError> { + match value { + None => Ok(None), + Some(v) if v.dtype() == *target => Ok(Some(v.clone())), + Some(v) => v.clone().try_cast(target.clone()).map(Some).map_err(|v| { + GraphError::InvalidFilter(format!( + "value {:?} of type {} cannot be compared as {}", + v, + v.dtype(), + target + )) + }), + } +} + /// Only fires when both sides are known and the RHS is a literal/const. Defers /// to runtime when the LHS type is unknown (`PropType::Empty`) or the RHS isn't /// a const value. @@ -1079,12 +1094,18 @@ pub fn validate_const_castable( ))) }; } - if rhs.dtype() != *lhs_pt && rhs.clone().try_cast(lhs_pt.clone()).is_err() { + // A numeric constant may target any numeric property, since comparison + // widens across the numeric variants. Everything else must unify with + // the property's own type — a constant that merely *casts* into it + // (a numeric string, a bool read as an int) is a cross-type comparison + // the caller almost certainly did not mean. + let rhs_pt = rhs.dtype(); + let compatible = (lhs_pt.is_numeric() && rhs_pt.is_numeric()) + || unify_types(lhs_pt, &rhs_pt, &mut false).is_ok(); + if !compatible { return Err(GraphError::InvalidFilter(format!( "value {:?} of type {} cannot be coerced to {}", - rhs, - rhs.dtype(), - lhs_pt + rhs, rhs_pt, lhs_pt ))); } } @@ -1176,12 +1197,15 @@ pub fn require_aggregable(pt: &PropType, op: &str) -> Result<(), GraphError> { } } -/// Cast every value in an `is_in`/`is_not_in` set to the LHS type. +/// Narrow an `is_in`/`is_not_in` set to the members that could match the LHS. /// -/// If the LHS type is unknown (`PropType::Empty`), the values are returned -/// unchanged and coercion is deferred to runtime. Otherwise, any value whose -/// type cannot be coerced produces `Err(InvalidFilter)`. Successful casts are -/// substituted so the runtime set comparison sees same-typed values. +/// Set membership asks whether a value is present, so a member of a type the +/// LHS can never equal simply is not present — it is dropped rather than +/// rejected, leaving `is_in` answering "no" where a comparison would refuse the +/// question. Members that share the LHS type, or are numeric alongside a +/// numeric LHS, are kept and cast so the runtime comparison sees one type. +/// An unknown LHS type (`PropType::Empty`) defers the whole decision to +/// runtime. pub fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result, GraphError> { if *lhs_pt == PropType::Empty { return Ok(values); @@ -1192,22 +1216,18 @@ pub fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result Option; + fn dyn_selects_node_id(&self) -> bool; fn dyn_create_node_op<'g>( @@ -111,6 +113,10 @@ pub trait DynCreateOp: DynEntityExpr { } impl DynCreateOp for E { + fn dyn_const_cast_type(&self) -> Option { + self.const_cast_type() + } + fn dyn_selects_node_id(&self) -> bool { self.selects_node_id() } @@ -163,6 +169,10 @@ impl EntityExpr for Arc { impl EntityExprBuilder for Arc {} impl CreateOp for Arc { + fn const_cast_type(&self) -> Option { + self.as_ref().dyn_const_cast_type() + } + fn selects_node_id(&self) -> bool { self.as_ref().dyn_selects_node_id() } 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 dc6adc8c1c..75a7e293c2 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 @@ -479,6 +479,10 @@ impl EntityExpr for DegreeExpr impl EntityExprBuilder for DegreeExpr {} impl CreateOp for DegreeExpr { + fn const_cast_type(&self) -> Option { + Some(PropType::U64) + } + fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, 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 7ab09df778..6d949366c8 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 @@ -51,12 +51,12 @@ use super::{ use crate::{ db::{ api::{ - state::ops::NodeOp, + state::ops::{Const, NodeOp}, view::{internal::GraphView, BoxableGraphView}, }, graph::views::filter::{ model::{ - coerce_set_values, + cast_const_to, coerce_set_values, edge_expr::{ ops::{ ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, @@ -229,8 +229,17 @@ where let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len())?; let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; + let mut right = right; match right.const_value() { - Some(c) => validate_const_castable(&lhs_pt, c.as_ref())?, + Some(c) => match self.left.const_cast_type() { + // The expression fixes the comparison's type, so the constant is + // converted into it rather than compared across types. + Some(target) => { + let casted = cast_const_to(&target, c.as_ref())?; + right = Arc::new(Const(casted)); + } + None => validate_const_castable(&lhs_pt, c.as_ref())?, + }, None => validate_types_compatible(&lhs_pt, &rhs_pt)?, } if quals.is_empty() { @@ -812,7 +821,21 @@ impl CreateFilter for PropValueSetExpr { let id_type = filtered.id_type(); let (inner, quals) = self.expr.create_qualified_node_op(filtered)?; let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; - let values = coerce_set_values(&lhs_pt, self.values)?; + // An expression that fixes the comparison's type fixes it for set + // members too, so they convert into it or the filter is refused — + // unlike the general case, where a member of an unrelated type is + // simply absent from the set. + let values = match self.expr.const_cast_type() { + Some(target) => self + .values + .into_iter() + .map(|v| { + cast_const_to(&target, Some(&v)) + .map(|c| c.expect("a present value casts to a present value")) + }) + .collect::, _>>()?, + None => coerce_set_values(&lhs_pt, self.values)?, + }; if quals.is_empty() { let gids: Option> = (self.op == SetOp::IsIn && self.expr.selects_node_id()) .then(|| { 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 index bc3313032f..3ce133b6d6 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -48,6 +48,15 @@ pub use ops::*; /// ``` /// pub trait CreateOp: EntityExpr + Clone + Send + Sync + 'static { + /// The type a constant compared against this expression is cast to before + /// validation. Degree is a count, so a constant that converts to one — the + /// string `"5"` from a wire query, say — is compared as that count rather + /// than across types. `None` (the default) leaves the constant alone and + /// requires it to match the expression's own type. + fn const_cast_type(&self) -> Option { + None + } + /// Whether this expression selects the node id field. Comparisons against /// constants use it to narrow the evaluation domain to the named nodes /// instead of scanning every node. 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 5e1e82b6b8..4fba390f59 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 @@ -568,16 +568,20 @@ fn string_op_on_numeric_prop_returns_error() { } #[test] -fn ordering_op_on_bool_prop_returns_error() { +fn ordering_op_on_bool_prop_orders_false_before_true() { let g = Graph::new(); - g.add_node(0, "n", [("flag", true.into_prop())], None, None) + g.add_node(0, "t", [("flag", true.into_prop())], None, None) + .unwrap(); + g.add_node(0, "f", [("flag", false.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.clone(), g); - assert!( - result.is_err(), - "expected Err for ordering op on boolean property" + assert_eq!( + filtered_names(NodeFilter.property("flag").gt(Prop::Bool(false)), g.clone()), + vec!["t"] + ); + assert_eq!( + filtered_names(NodeFilter.property("flag").lt(Prop::Bool(true)), g), + vec!["f"] ); } From 978498d26414f4096c545fd48d7f3db8805d97aa Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:23:05 +0100 Subject: [PATCH 115/155] restore the class-with-static-methods shape of the python filter surface filter.Node, Edge, ExplodedEdge and Graph are classes of static methods again, with their view methods returning the view-scoped NodeFilter, EdgeFilter, ExplodedEdgeFilter and GraphFilter; Expr and PropertyExpr are registered so annotations and isinstance work. The stub generator no longer lets a submodule's star import shadow the classes the submodule defines, every filter method documents its arguments and return type, and the remote client's refusal of expression-rhs filters is pinned by a test. Also: wire field names are shared constants, the set-cast helper is fallible, one GID conversion serves both the wire and domain narrowing, the EntityExprBuilder marker is renamed PredicateLhs, and the explicit-false not()/is_true()/is_false() aliases are gone in favour of eq(); not() is the set complement everywhere, as in python. --- py | 1 + python/python/raphtory/filter/__init__.pyi | 1591 ++++++++++++++++- .../parity/test_parity_filters.py | 32 + raphtory-graphql/src/model/graph/filtering.rs | 14 +- raphtory-tests/tests/edge_expr_filter.rs | 4 +- raphtory-tests/tests/test_filters.rs | 29 +- .../graph/views/filter/model/edge_filter.rs | 4 +- .../src/db/graph/views/filter/model/filter.rs | 13 +- .../src/db/graph/views/filter/model/mod.rs | 70 +- .../views/filter/model/node_expr/dyn_expr.rs | 6 +- .../views/filter/model/node_expr/exprs.rs | 16 +- .../views/filter/model/node_expr/filters.rs | 21 +- .../graph/views/filter/model/node_expr/mod.rs | 6 +- .../graph/views/filter/model/node_expr/ops.rs | 37 +- .../views/filter/model/node_expr/tests.rs | 6 +- raphtory/src/python/filter/edge_expr.rs | 292 ++- .../src/python/filter/exploded_edge_expr.rs | 269 ++- raphtory/src/python/filter/graph_filter.rs | 154 +- raphtory/src/python/filter/mod.rs | 23 +- raphtory/src/python/filter/node_expr.rs | 377 +++- raphtory/src/python/filter/wire.rs | 2 +- stub_gen/stub_gen.py | 31 +- 22 files changed, 2753 insertions(+), 245 deletions(-) create mode 100644 py diff --git a/py b/py new file mode 100644 index 0000000000..55fe211008 --- /dev/null +++ b/py @@ -0,0 +1 @@ +---- InvalidFilter - exception diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 5540ce1bfc..3d2fdf3a99 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -10,7 +10,7 @@ from __future__ import annotations ############################################################################### from typing import * -from raphtory import * +from raphtory import GraphView, PersistentGraph, _GraphEncoder, Nodes, PathFromNode, PathFromGraph, MutableNode, Edges, NestedEdges, MutableEdge, SortByTime, NodeSortBy, EdgeSortBy, Properties, PyPropValueList, PropType, Metadata, MetadataView, TemporalProperties, PropertiesView, TemporalProperty, EventTime, OptionalEventTime, History, HistoryTimestamp, HistoryDateTime, HistoryEventId, Intervals, WindowSet, Prop, version, graphql, algorithms, graph_loader, graph_gen, vectors, node_state, filter, iterables, nullmodels, plottingutils from raphtory.algorithms import * from raphtory.vectors import * from raphtory.node_state import * @@ -29,7 +29,7 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = ['FilterExpr', 'Node', 'Edge', 'EdgeEndpoint', 'ExplodedEdge', 'Graph', 'Node', 'Edge', 'ExplodedEdge', 'Graph'] +__all__ = ['FilterExpr', 'Expr', 'PropertyExpr', 'Node', 'NodeFilter', 'Edge', 'EdgeFilter', 'EdgeEndpoint', 'ExplodedEdge', 'ExplodedEdgeFilter', 'Graph', 'GraphFilter'] class FilterExpr(object): def __and__(self, value): @@ -47,31 +47,337 @@ class FilterExpr(object): def __ror__(self, value): """Return value|self.""" -class EdgeEndpoint(object): - """ - Entry point for filtering an edge endpoint (source or destination). +class Expr(object): - An `EdgeEndpoint` is obtained from `Edge.src()` or `Edge.dst()` and allows - you to filter on endpoint fields (id, name, type) as well as endpoint - properties and metadata. + def __eq__(self, value): + """Return self==value.""" - Examples: - Edge.src().id() == 1 - Edge.dst().name().starts_with("user:") - Edge.src().property("country") == "UK" + def __ge__(self, value): + """Return self>=value.""" + + def __gt__(self, value): + """Return self>value.""" + + def __le__(self, value): + """Return self<=value.""" + + def __lt__(self, value): + """Return self filter.Expr: + """ + Requires that **all** elements match when the value is list-like (a temporal history or a list property). + + Returns: + filter.Expr: + """ + + def any(self) -> filter.Expr: + """ + Requires that **any** element matches when the value is list-like (a temporal history or a list property). + + Returns: + filter.Expr: + """ + + def avg(self) -> filter.Expr: + """ + Averages the elements when the value is numeric and list-like. + + Returns: + filter.Expr: + """ + + def contains(self, other: Prop | filter.Expr) -> filter.FilterExpr: + """ + Checks whether the value's string representation contains the given value. + + Arguments: + other (Prop | filter.Expr): Substring that must appear within the value. + + Returns: + filter.FilterExpr: + """ + + def ends_with(self, other: Prop | filter.Expr) -> filter.FilterExpr: + """ + Checks whether the value's string representation ends with the given value. + + Arguments: + other (Prop | filter.Expr): Suffix to check for. + + Returns: + filter.FilterExpr: + """ + + def first(self) -> filter.Expr: + """ + Selects the first element when the value is list-like. + + Returns: + filter.Expr: + """ + + def fuzzy_search(self, other: Prop | filter.Expr, levenshtein_distance: int, prefix_match: bool) -> filter.FilterExpr: + """ + Performs fuzzy matching against the value's string representation, within a Levenshtein distance and with optional prefix matching. + + Arguments: + other (Prop | filter.Expr): String to approximately match against. + levenshtein_distance (int): Maximum allowed Levenshtein distance. + prefix_match (bool): Whether to require a matching prefix. + + Returns: + filter.FilterExpr: + """ + + def is_in(self, values: list[Prop]) -> filter.FilterExpr: + """ + Checks whether the value is contained within the given values. + + Arguments: + values (list[Prop]): Values to match against. + + Returns: + filter.FilterExpr: + """ + + def is_none(self) -> filter.FilterExpr: + """ + Checks whether the value is `None` / missing. + + Returns: + filter.FilterExpr: + """ + + def is_not_in(self, values: list[Prop]) -> filter.FilterExpr: + """ + Checks whether the value is **not** contained within the given values. + + Arguments: + values (list[Prop]): Values to exclude. + + Returns: + filter.FilterExpr: + """ + + def is_some(self) -> filter.FilterExpr: + """ + Checks whether the value is present (not `None`). + + Returns: + filter.FilterExpr: + """ + + def last(self) -> filter.Expr: + """ + Selects the last element when the value is list-like. + + Returns: + filter.Expr: + """ + + def len(self) -> filter.Expr: + """ + Selects the number of elements when the value is list-like. + + Returns: + filter.Expr: + """ + + def max(self) -> filter.Expr: + """ + Selects the maximum element when the value is list-like. + + Returns: + filter.Expr: + """ + + def min(self) -> filter.Expr: + """ + Selects the minimum element when the value is list-like. + + Returns: + filter.Expr: + """ + + def not_contains(self, other: Prop | filter.Expr) -> filter.FilterExpr: + """ + Checks whether the value's string representation **does not** contain the given value. + + Arguments: + other (Prop | filter.Expr): Substring that must not appear within the value. + + Returns: + filter.FilterExpr: + """ + + def starts_with(self, other: Prop | filter.Expr) -> filter.FilterExpr: + """ + Checks whether the value's string representation starts with the given value. + + Arguments: + other (Prop | filter.Expr): Prefix to check for. + + Returns: + filter.FilterExpr: + """ + + def sum(self) -> filter.Expr: + """ + Sums the elements when the value is numeric and list-like. + + Returns: + filter.Expr: + """ + +class PropertyExpr(Expr): + + def temporal(self) -> filter.Expr: + """ + Switches from the property's latest value to its full temporal history, + unlocking the aggregate chain (`sum`, `avg`, `min`, `max`, `any`, ...). + + Returns: + filter.Expr: + """ + +class Node(object): """ + Entry point for constructing node filter expressions. - def id(self) -> filter.Expr: + Every method is static: `Node.property("age") > 30` selects nodes + directly, and the view methods (`window`, `latest`, `layer`, ...) return a + [`NodeFilter`] scoped to that view for further chaining. + """ + + @staticmethod + def after(time: int) -> filter.NodeFilter: """ - Selects the endpoint node ID field for filtering. + Restricts node evaluation to times strictly after the given time. + + Arguments: + time (int): Lower time bound. + + Returns: + filter.NodeFilter: + """ + + @staticmethod + def at(time: int) -> filter.NodeFilter: + """ + Restricts node evaluation to a single point in time. + + Arguments: + time (int): Event time. + + Returns: + filter.NodeFilter: + """ + + @staticmethod + def before(time: int) -> filter.NodeFilter: + """ + Restricts node evaluation to times strictly before the given time. + + Arguments: + time (int): Upper time bound. + + Returns: + filter.NodeFilter: + """ + + @staticmethod + def by_state_column(state: OutputNodeState, col: str) -> filter.FilterExpr: + """ + Build a node filter from a boolean column of an existing node-state result. + + Arguments: + state (OutputNodeState): A pre-computed node state (e.g. from an algorithm). + col (str): Name of the boolean column on `state` whose values determine inclusion. + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def degree() -> filter.Expr: + """ + Selects total node degree for filtering. Returns: filter.Expr: """ - def metadata(self, name: str) -> filter.Expr: + @staticmethod + def id() -> filter.Expr: """ - Filters an endpoint node metadata field by name. + Selects the node ID field for filtering. + + Returns: + filter.Expr: + """ + + @staticmethod + def in_degree() -> filter.Expr: + """ + Selects incoming node degree for filtering. + + Returns: + filter.Expr: + """ + + @staticmethod + def is_active() -> filter.FilterExpr: + """ + Matches nodes that have at least one event in the current view. + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def latest() -> filter.NodeFilter: + """ + Evaluates filters against the latest available state of each node. + + Returns: + filter.NodeFilter: + """ + + @staticmethod + def layer(layer: str) -> filter.NodeFilter: + """ + Restricts evaluation to nodes belonging to the given layer. + + Arguments: + layer (str): Layer name. + + Returns: + filter.NodeFilter: + """ + + @staticmethod + def layers(layers: list[str]) -> filter.NodeFilter: + """ + Restricts evaluation to nodes belonging to any of the given layers. + + Arguments: + layers (list[str]): Layer names. + + Returns: + filter.NodeFilter: + """ + + @staticmethod + def metadata(name: str) -> filter.Expr: + """ + Filters a node metadata field by name. + + Metadata is shared across all temporal versions of a node. Arguments: name (str): Metadata key. @@ -80,25 +386,39 @@ class EdgeEndpoint(object): filter.Expr: """ - def name(self) -> filter.Expr: + @staticmethod + def name() -> filter.Expr: """ - Selects the endpoint node name field for filtering. + Selects the node name field for filtering. Returns: filter.Expr: """ - def node_type(self) -> filter.Expr: + @staticmethod + def node_type() -> filter.Expr: """ - Selects the endpoint node type field for filtering. + Selects the node type field for filtering. Returns: filter.Expr: """ - def property(self, name: str) -> filter.PropertyExpr: + @staticmethod + def out_degree() -> filter.Expr: + """ + Selects outgoing node degree for filtering. + + Returns: + filter.Expr: """ - Filters an endpoint node property by name. + + @staticmethod + def property(name: str) -> filter.PropertyExpr: + """ + Filters a node property by name. + + The property may be static or temporal depending on the query context. Arguments: name (str): Property key. @@ -106,3 +426,1230 @@ class EdgeEndpoint(object): Returns: filter.PropertyExpr: """ + + @staticmethod + def snapshot_at(time: int) -> filter.NodeFilter: + """ + Evaluates filters against a snapshot of the graph at a given time. + + Arguments: + time (int): Snapshot time. + + Returns: + filter.NodeFilter: + """ + + @staticmethod + def snapshot_latest() -> filter.NodeFilter: + """ + Evaluates filters against the most recent snapshot of the graph. + + Returns: + filter.NodeFilter: + """ + + @staticmethod + def window(start: int, end: int) -> filter.NodeFilter: + """ + Restricts node evaluation to the given time window. + + The window is inclusive of `start` and exclusive of `end`. + + Arguments: + start (int): Start time. + end (int): End time. + + Returns: + filter.NodeFilter: + """ + +class NodeFilter(object): + """ + A node filter scoped to a view. + + Obtained from the view methods on [`Node`] (`Node.window(...)`, + `Node.latest()`, ...); its field and property methods evaluate within that + view, and its own view methods narrow it further. + """ + + def __new__(cls) -> NodeFilter: + """Create and return a new object. See help(type) for accurate signature.""" + + def after(self, time: int) -> filter.NodeFilter: + """ + Restricts node evaluation to times strictly after the given time. + + Arguments: + time (int): Lower time bound. + + Returns: + filter.NodeFilter: + """ + + def at(self, time: int) -> filter.NodeFilter: + """ + Restricts node evaluation to a single point in time. + + Arguments: + time (int): Event time. + + Returns: + filter.NodeFilter: + """ + + def before(self, time: int) -> filter.NodeFilter: + """ + Restricts node evaluation to times strictly before the given time. + + Arguments: + time (int): Upper time bound. + + Returns: + filter.NodeFilter: + """ + + def by_state_column(self, state: OutputNodeState, col: str) -> filter.FilterExpr: + """ + Build a node filter from a boolean column of an existing node-state result. + + Arguments: + state (OutputNodeState): A pre-computed node state (e.g. from an algorithm). + col (str): Name of the boolean column on `state` whose values determine inclusion. + + Returns: + filter.FilterExpr: + """ + + def degree(self) -> filter.Expr: + """ + Selects total node degree for filtering. + + Returns: + filter.Expr: + """ + + def id(self) -> filter.Expr: + """ + Selects the node ID field for filtering. + + Returns: + filter.Expr: + """ + + def in_degree(self) -> filter.Expr: + """ + Selects incoming node degree for filtering. + + Returns: + filter.Expr: + """ + + def is_active(self) -> filter.FilterExpr: + """ + Matches nodes that have at least one event in the current view. + + Returns: + filter.FilterExpr: + """ + + def latest(self) -> filter.NodeFilter: + """ + Evaluates filters against the latest available state of each node. + + Returns: + filter.NodeFilter: + """ + + def layer(self, layer: str) -> filter.NodeFilter: + """ + Restricts evaluation to nodes belonging to the given layer. + + Arguments: + layer (str): Layer name. + + Returns: + filter.NodeFilter: + """ + + def layers(self, layers: list[str]) -> filter.NodeFilter: + """ + Restricts evaluation to nodes belonging to any of the given layers. + + Arguments: + layers (list[str]): Layer names. + + Returns: + filter.NodeFilter: + """ + + def metadata(self, name: str) -> filter.Expr: + """ + Filters a node metadata field by name. + + Metadata is shared across all temporal versions of a node. + + Arguments: + name (str): Metadata key. + + Returns: + filter.Expr: + """ + + def name(self) -> filter.Expr: + """ + Selects the node name field for filtering. + + Returns: + filter.Expr: + """ + + def node_type(self) -> filter.Expr: + """ + Selects the node type field for filtering. + + Returns: + filter.Expr: + """ + + def out_degree(self) -> filter.Expr: + """ + Selects outgoing node degree for filtering. + + Returns: + filter.Expr: + """ + + def property(self, name: str) -> filter.PropertyExpr: + """ + Filters a node property by name. + + The property may be static or temporal depending on the query context. + + Arguments: + name (str): Property key. + + Returns: + filter.PropertyExpr: + """ + + def snapshot_at(self, time: int) -> filter.NodeFilter: + """ + Evaluates filters against a snapshot of the graph at a given time. + + Arguments: + time (int): Snapshot time. + + Returns: + filter.NodeFilter: + """ + + def snapshot_latest(self) -> filter.NodeFilter: + """ + Evaluates filters against the most recent snapshot of the graph. + + Returns: + filter.NodeFilter: + """ + + def window(self, start: int, end: int) -> filter.NodeFilter: + """ + Restricts node evaluation to the given time window. + + The window is inclusive of `start` and exclusive of `end`. + + Arguments: + start (int): Start time. + end (int): End time. + + Returns: + filter.NodeFilter: + """ + +class Edge(object): + """ + Entry point for constructing edge filter expressions. + + Every method is static: `Edge.src().name() == "alice"` selects edges + directly, and the view methods return an [`EdgeFilter`] scoped to that + view for further chaining. + """ + + @staticmethod + def after(time: int) -> filter.EdgeFilter: + """ + Restricts edge evaluation to times strictly after the given time. + + Arguments: + time (int): Lower time bound. + + Returns: + filter.EdgeFilter: + """ + + @staticmethod + def at(time: int) -> filter.EdgeFilter: + """ + Restricts edge evaluation to a single point in time. + + Arguments: + time (int): Event time. + + Returns: + filter.EdgeFilter: + """ + + @staticmethod + def before(time: int) -> filter.EdgeFilter: + """ + Restricts edge evaluation to times strictly before the given time. + + Arguments: + time (int): Upper time bound. + + Returns: + filter.EdgeFilter: + """ + + @staticmethod + def dst() -> filter.EdgeEndpoint: + """ + Selects the edge **destination endpoint** for filtering. + + Returns: + filter.EdgeEndpoint: + """ + + @staticmethod + def is_active() -> filter.FilterExpr: + """ + Matches edges that have at least one event in the current view. + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def is_deleted() -> filter.FilterExpr: + """ + Matches edges that have been deleted. + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def is_self_loop() -> filter.FilterExpr: + """ + Matches edges that are self-loops (source == destination). + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def is_valid() -> filter.FilterExpr: + """ + Matches edges that are structurally valid in the current view. + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def latest() -> filter.EdgeFilter: + """ + Evaluates edge predicates against the latest available edge state. + + Returns: + filter.EdgeFilter: + """ + + @staticmethod + def layer(layer: str) -> filter.EdgeFilter: + """ + Restricts evaluation to edges belonging to the given layer. + + Arguments: + layer (str): Layer name. + + Returns: + filter.EdgeFilter: + """ + + @staticmethod + def layers(layers: list[str]) -> filter.EdgeFilter: + """ + Restricts evaluation to edges belonging to any of the given layers. + + Arguments: + layers (list[str]): Layer names. + + Returns: + filter.EdgeFilter: + """ + + @staticmethod + def metadata(name: str) -> filter.Expr: + """ + Filters an edge metadata field by name. + + Arguments: + name (str): Metadata key. + + Returns: + filter.Expr: + """ + + @staticmethod + def property(name: str) -> filter.PropertyExpr: + """ + Filters an edge property by name. + + Arguments: + name (str): Property key. + + Returns: + filter.PropertyExpr: + """ + + @staticmethod + def snapshot_at(time: int) -> filter.EdgeFilter: + """ + Evaluates edge predicates against a snapshot of the graph at a given time. + + Arguments: + time (int): Snapshot time. + + Returns: + filter.EdgeFilter: + """ + + @staticmethod + def snapshot_latest() -> filter.EdgeFilter: + """ + Evaluates edge predicates against the most recent snapshot of the graph. + + Returns: + filter.EdgeFilter: + """ + + @staticmethod + def src() -> filter.EdgeEndpoint: + """ + Selects the edge **source endpoint** for filtering. + + Returns: + filter.EdgeEndpoint: + """ + + @staticmethod + def window(start: int, end: int) -> filter.EdgeFilter: + """ + Restricts edge evaluation to the given time window. + + Arguments: + start (int): Start time. + end (int): End time. + + Returns: + filter.EdgeFilter: + """ + +class EdgeFilter(object): + """ + An edge filter scoped to a view. + + Obtained from the view methods on [`Edge`] (`Edge.window(...)`, + `Edge.layer(...)`, ...); its endpoint, property and structural predicates + evaluate within that view, and its own view methods narrow it further. + """ + + def __new__(cls) -> EdgeFilter: + """Create and return a new object. See help(type) for accurate signature.""" + + def after(self, time: int) -> filter.EdgeFilter: + """ + Restricts edge evaluation to times strictly after the given time. + + Arguments: + time (int): Lower time bound. + + Returns: + filter.EdgeFilter: + """ + + def at(self, time: int) -> filter.EdgeFilter: + """ + Restricts edge evaluation to a single point in time. + + Arguments: + time (int): Event time. + + Returns: + filter.EdgeFilter: + """ + + def before(self, time: int) -> filter.EdgeFilter: + """ + Restricts edge evaluation to times strictly before the given time. + + Arguments: + time (int): Upper time bound. + + Returns: + filter.EdgeFilter: + """ + + def dst(self) -> filter.EdgeEndpoint: + """ + Selects the edge **destination endpoint** for filtering. + + Returns: + filter.EdgeEndpoint: + """ + + def is_active(self) -> filter.FilterExpr: + """ + Matches edges that have at least one event in the current view. + + Returns: + filter.FilterExpr: + """ + + def is_deleted(self) -> filter.FilterExpr: + """ + Matches edges that have been deleted. + + Returns: + filter.FilterExpr: + """ + + def is_self_loop(self) -> filter.FilterExpr: + """ + Matches edges that are self-loops (source == destination). + + Returns: + filter.FilterExpr: + """ + + def is_valid(self) -> filter.FilterExpr: + """ + Matches edges that are structurally valid in the current view. + + Returns: + filter.FilterExpr: + """ + + def latest(self) -> filter.EdgeFilter: + """ + Evaluates edge predicates against the latest available edge state. + + Returns: + filter.EdgeFilter: + """ + + def layer(self, layer: str) -> filter.EdgeFilter: + """ + Restricts evaluation to edges belonging to the given layer. + + Arguments: + layer (str): Layer name. + + Returns: + filter.EdgeFilter: + """ + + def layers(self, layers: list[str]) -> filter.EdgeFilter: + """ + Restricts evaluation to edges belonging to any of the given layers. + + Arguments: + layers (list[str]): Layer names. + + Returns: + filter.EdgeFilter: + """ + + def metadata(self, name: str) -> filter.Expr: + """ + Filters an edge metadata field by name. + + Arguments: + name (str): Metadata key. + + Returns: + filter.Expr: + """ + + def property(self, name: str) -> filter.PropertyExpr: + """ + Filters an edge property by name. + + Arguments: + name (str): Property key. + + Returns: + filter.PropertyExpr: + """ + + def snapshot_at(self, time: int) -> filter.EdgeFilter: + """ + Evaluates edge predicates against a snapshot of the graph at a given time. + + Arguments: + time (int): Snapshot time. + + Returns: + filter.EdgeFilter: + """ + + def snapshot_latest(self) -> filter.EdgeFilter: + """ + Evaluates edge predicates against the most recent snapshot of the graph. + + Returns: + filter.EdgeFilter: + """ + + def src(self) -> filter.EdgeEndpoint: + """ + Selects the edge **source endpoint** for filtering. + + Returns: + filter.EdgeEndpoint: + """ + + def window(self, start: int, end: int) -> filter.EdgeFilter: + """ + Restricts edge evaluation to the given time window. + + Arguments: + start (int): Start time. + end (int): End time. + + Returns: + filter.EdgeFilter: + """ + +class EdgeEndpoint(object): + """ + Entry point for filtering an edge endpoint (source or destination). + + An `EdgeEndpoint` is obtained from `Edge.src()` or `Edge.dst()` and allows + you to filter on endpoint fields (id, name, type) as well as endpoint + properties and metadata. + + Examples: + Edge.src().id() == 1 + Edge.dst().name().starts_with("user:") + Edge.src().property("country") == "UK" + """ + + def id(self) -> filter.Expr: + """ + Selects the endpoint node ID field for filtering. + + Returns: + filter.Expr: + """ + + def metadata(self, name: str) -> filter.Expr: + """ + Filters an endpoint node metadata field by name. + + Arguments: + name (str): Metadata key. + + Returns: + filter.Expr: + """ + + def name(self) -> filter.Expr: + """ + Selects the endpoint node name field for filtering. + + Returns: + filter.Expr: + """ + + def node_type(self) -> filter.Expr: + """ + Selects the endpoint node type field for filtering. + + Returns: + filter.Expr: + """ + + def property(self, name: str) -> filter.PropertyExpr: + """ + Filters an endpoint node property by name. + + Arguments: + name (str): Property key. + + Returns: + filter.PropertyExpr: + """ + +class ExplodedEdge(object): + """ + Entry point for constructing exploded-edge filter expressions. + + Every method is static; the view methods return an + [`ExplodedEdgeFilter`] scoped to that view for further chaining. + """ + + @staticmethod + def after(time: int) -> filter.ExplodedEdgeFilter: + """ + Restricts exploded edge evaluation to times strictly after the given time. + + Arguments: + time (int): Lower time bound. + + Returns: + filter.ExplodedEdgeFilter: + """ + + @staticmethod + def at(time: int) -> filter.ExplodedEdgeFilter: + """ + Restricts exploded edge evaluation to a single point in time. + + Arguments: + time (int): Event time. + + Returns: + filter.ExplodedEdgeFilter: + """ + + @staticmethod + def before(time: int) -> filter.ExplodedEdgeFilter: + """ + Restricts exploded edge evaluation to times strictly before the given time. + + Arguments: + time (int): Upper time bound. + + Returns: + filter.ExplodedEdgeFilter: + """ + + @staticmethod + def is_active() -> filter.FilterExpr: + """ + Matches exploded edges that have at least one event in the current view. + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def is_deleted() -> filter.FilterExpr: + """ + Matches exploded edges that have been deleted. + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def is_self_loop() -> filter.FilterExpr: + """ + Matches exploded edges that are self-loops (source == destination). + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def is_valid() -> filter.FilterExpr: + """ + Matches exploded edges that are structurally valid in the current view. + + Returns: + filter.FilterExpr: + """ + + @staticmethod + def latest() -> filter.ExplodedEdgeFilter: + """ + Evaluates exploded edge predicates against the latest available state. + + Returns: + filter.ExplodedEdgeFilter: + """ + + @staticmethod + def layer(layer: str) -> filter.ExplodedEdgeFilter: + """ + Restricts evaluation to exploded edges belonging to the given layer. + + Arguments: + layer (str): Layer name. + + Returns: + filter.ExplodedEdgeFilter: + """ + + @staticmethod + def layers(layers: list[str]) -> filter.ExplodedEdgeFilter: + """ + Restricts evaluation to exploded edges belonging to any of the given layers. + + Arguments: + layers (list[str]): Layer names. + + Returns: + filter.ExplodedEdgeFilter: + """ + + @staticmethod + def metadata(name: str) -> filter.Expr: + """ + Filters an exploded edge metadata field by name. + + Metadata is shared across all temporal versions of an exploded edge. + + Arguments: + name (str): Metadata key. + + Returns: + filter.Expr: + """ + + @staticmethod + def property(name: str) -> filter.PropertyExpr: + """ + Filters an exploded edge property by name. + + The property may be static or temporal depending on the query context. + + Arguments: + name (str): Property key. + + Returns: + filter.PropertyExpr: + """ + + @staticmethod + def snapshot_at(time: int) -> filter.ExplodedEdgeFilter: + """ + Evaluates exploded edge predicates against a snapshot of the graph at a given time. + + Arguments: + time (int): Snapshot time. + + Returns: + filter.ExplodedEdgeFilter: + """ + + @staticmethod + def snapshot_latest() -> filter.ExplodedEdgeFilter: + """ + Evaluates exploded edge predicates against the most recent snapshot of the graph. + + Returns: + filter.ExplodedEdgeFilter: + """ + + @staticmethod + def window(start: int, end: int) -> filter.ExplodedEdgeFilter: + """ + Restricts exploded edge evaluation to the given time window. + + The window is inclusive of `start` and exclusive of `end`. + + Arguments: + start (int): Start time. + end (int): End time. + + Returns: + filter.ExplodedEdgeFilter: + """ + +class ExplodedEdgeFilter(object): + """ + An exploded-edge filter scoped to a view. + + An exploded edge is one temporal event of an edge, addressed individually + rather than as the edge aggregated across time. Obtained from the view + methods on [`ExplodedEdge`]; its property and structural predicates evaluate + within that view, and its own view methods narrow it further. + """ + + def __new__(cls) -> ExplodedEdgeFilter: + """Create and return a new object. See help(type) for accurate signature.""" + + def after(self, time: int) -> filter.ExplodedEdgeFilter: + """ + Restricts exploded edge evaluation to times strictly after the given time. + + Arguments: + time (int): Lower time bound. + + Returns: + filter.ExplodedEdgeFilter: + """ + + def at(self, time: int) -> filter.ExplodedEdgeFilter: + """ + Restricts exploded edge evaluation to a single point in time. + + Arguments: + time (int): Event time. + + Returns: + filter.ExplodedEdgeFilter: + """ + + def before(self, time: int) -> filter.ExplodedEdgeFilter: + """ + Restricts exploded edge evaluation to times strictly before the given time. + + Arguments: + time (int): Upper time bound. + + Returns: + filter.ExplodedEdgeFilter: + """ + + def is_active(self) -> filter.FilterExpr: + """ + Matches exploded edges that have at least one event in the current view. + + Returns: + filter.FilterExpr: + """ + + def is_deleted(self) -> filter.FilterExpr: + """ + Matches exploded edges that have been deleted. + + Returns: + filter.FilterExpr: + """ + + def is_self_loop(self) -> filter.FilterExpr: + """ + Matches exploded edges that are self-loops (source == destination). + + Returns: + filter.FilterExpr: + """ + + def is_valid(self) -> filter.FilterExpr: + """ + Matches exploded edges that are structurally valid in the current view. + + Returns: + filter.FilterExpr: + """ + + def latest(self) -> filter.ExplodedEdgeFilter: + """ + Evaluates exploded edge predicates against the latest available state. + + Returns: + filter.ExplodedEdgeFilter: + """ + + def layer(self, layer: str) -> filter.ExplodedEdgeFilter: + """ + Restricts evaluation to exploded edges belonging to the given layer. + + Arguments: + layer (str): Layer name. + + Returns: + filter.ExplodedEdgeFilter: + """ + + def layers(self, layers: list[str]) -> filter.ExplodedEdgeFilter: + """ + Restricts evaluation to exploded edges belonging to any of the given layers. + + Arguments: + layers (list[str]): Layer names. + + Returns: + filter.ExplodedEdgeFilter: + """ + + def metadata(self, name: str) -> filter.Expr: + """ + Filters an exploded edge metadata field by name. + + Metadata is shared across all temporal versions of an exploded edge. + + Arguments: + name (str): Metadata key. + + Returns: + filter.Expr: + """ + + def property(self, name: str) -> filter.PropertyExpr: + """ + Filters an exploded edge property by name. + + The property may be static or temporal depending on the query context. + + Arguments: + name (str): Property key. + + Returns: + filter.PropertyExpr: + """ + + def snapshot_at(self, time: int) -> filter.ExplodedEdgeFilter: + """ + Evaluates exploded edge predicates against a snapshot of the graph at a given time. + + Arguments: + time (int): Snapshot time. + + Returns: + filter.ExplodedEdgeFilter: + """ + + def snapshot_latest(self) -> filter.ExplodedEdgeFilter: + """ + Evaluates exploded edge predicates against the most recent snapshot of the graph. + + Returns: + filter.ExplodedEdgeFilter: + """ + + def window(self, start: int, end: int) -> filter.ExplodedEdgeFilter: + """ + Restricts exploded edge evaluation to the given time window. + + The window is inclusive of `start` and exclusive of `end`. + + Arguments: + start (int): Start time. + end (int): End time. + + Returns: + filter.ExplodedEdgeFilter: + """ + +class Graph(object): + """ + Entry point for graph-level view filters. + + Every method is static and returns a [`GraphFilter`] carrying the view, + which composes with node and edge predicates. + """ + + @staticmethod + def after(time: int) -> filter.GraphFilter: + """ + Restricts evaluation to times strictly after the given time. + + Arguments: + time (int): Lower time bound. + + Returns: + filter.GraphFilter: + """ + + @staticmethod + def at(time: int) -> filter.GraphFilter: + """ + Restricts evaluation to a single point in time. + + Arguments: + time (int): Event time. + + Returns: + filter.GraphFilter: + """ + + @staticmethod + def before(time: int) -> filter.GraphFilter: + """ + Restricts evaluation to times strictly before the given time. + + Arguments: + time (int): Upper time bound. + + Returns: + filter.GraphFilter: + """ + + @staticmethod + def latest() -> filter.GraphFilter: + """ + Evaluates filters against the latest available state of the graph. + + Returns: + filter.GraphFilter: + """ + + @staticmethod + def layer(layer: str) -> filter.GraphFilter: + """ + Restricts evaluation to a single layer. + + Arguments: + layer (str): Layer name. + + Returns: + filter.GraphFilter: + """ + + @staticmethod + def layers(layers: list[str]) -> filter.GraphFilter: + """ + Restricts evaluation to any of the given layers. + + Arguments: + layers (list[str]): Layer names. + + Returns: + filter.GraphFilter: + """ + + @staticmethod + def snapshot_at(time: int) -> filter.GraphFilter: + """ + Evaluates filters against a snapshot of the graph at a given time. + + Arguments: + time (int): Snapshot time. + + Returns: + filter.GraphFilter: + """ + + @staticmethod + def snapshot_latest() -> filter.GraphFilter: + """ + Evaluates filters against the most recent snapshot of the graph. + + Returns: + filter.GraphFilter: + """ + + @staticmethod + def window(start: int, end: int) -> filter.GraphFilter: + """ + Restricts evaluation to events within a time window. + + The window is inclusive of `start` and exclusive of `end`. + + Arguments: + start (int): Start time. + end (int): End time. + + Returns: + filter.GraphFilter: + """ + +class GraphFilter(FilterExpr): + """ + A graph-level view scope. + + Obtained from the view methods on [`Graph`] (`Graph.window(...)`, + `Graph.latest()`, ...). It carries no node or edge predicate of its own: it + fixes the temporal and layer scope that node and edge predicates compose + with, and its own view methods narrow it further. + """ + + def after(self, time: int) -> filter.GraphFilter: + """ + Restricts evaluation to times strictly after the given time. + + Arguments: + time (int): Lower time bound. + + Returns: + filter.GraphFilter: + """ + + def at(self, time: int) -> filter.GraphFilter: + """ + Restricts evaluation to a single point in time. + + Arguments: + time (int): Event time. + + Returns: + filter.GraphFilter: + """ + + def before(self, time: int) -> filter.GraphFilter: + """ + Restricts evaluation to times strictly before the given time. + + Arguments: + time (int): Upper time bound. + + Returns: + filter.GraphFilter: + """ + + def latest(self) -> filter.GraphFilter: + """ + Evaluates filters against the latest available state of the graph. + + Returns: + filter.GraphFilter: + """ + + def layer(self, layer: str) -> filter.GraphFilter: + """ + Restricts evaluation to a single layer. + + Arguments: + layer (str): Layer name. + + Returns: + filter.GraphFilter: + """ + + def layers(self, layers: list[str]) -> filter.GraphFilter: + """ + Restricts evaluation to any of the given layers. + + Arguments: + layers (list[str]): Layer names. + + Returns: + filter.GraphFilter: + """ + + def snapshot_at(self, time: int) -> filter.GraphFilter: + """ + Evaluates filters against a snapshot of the graph at a given time. + + Arguments: + time (int): Snapshot time. + + Returns: + filter.GraphFilter: + """ + + def snapshot_latest(self) -> filter.GraphFilter: + """ + Evaluates filters against the most recent snapshot of the graph. + + Returns: + filter.GraphFilter: + """ + + def window(self, start: int, end: int) -> filter.GraphFilter: + """ + Restricts evaluation to events within a time window. + + The window is inclusive of `start` and exclusive of `end`. + + Arguments: + start (int): Start time. + end (int): End time. + + Returns: + filter.GraphFilter: + """ diff --git a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index b8771da8e2..90c5a8d629 100644 --- a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py +++ b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py @@ -1111,6 +1111,38 @@ def test_rejected_expr_parity_at_nodes_filter(filter_pair, name): ) +# Filters that compare two expressions have no wire form: the GraphQL schema +# only takes a constant on the right-hand side. Locally they run; remotely the +# client has to say so at the call, not ship a mistranslation. +EXPR_RHS_SITES = { + "graph.filter": lambda g, e: [n.name for n in g.filter(e).nodes], + "nodes.filter": lambda g, e: [n.name for n in g.nodes.filter(e)], + "nodes[expr]": lambda g, e: [n.name for n in g.nodes[e]], + "node.filter": lambda g, e: g.node("hub").filter(e) is not None, + "path.filter": lambda g, e: [n.name for n in g.node("hub").neighbours.filter(e)], +} + + +@pytest.mark.parametrize("site", sorted(EXPR_RHS_SITES), ids=sorted(EXPR_RHS_SITES)) +def test_expression_rhs_is_refused_remotely_with_the_reason(filter_pair, site): + """`degree() > in_degree()` runs locally; the remote client refuses it. + + The refusal is asserted for its reason, so a future client that silently + dropped the right-hand side (and so sent a different filter) or that + failed later with an unrelated server error would both fail here. The + local side is asserted too: the expression is meaningful and narrows, so + what the client refuses is a real filter, not an already-invalid one. + """ + read = EXPR_RHS_SITES[site] + expr = f.Node.degree() > f.Node.in_degree() + + local = read(filter_pair.local, expr) + assert local, f"{site}: the expression selects nothing locally" + + with pytest.raises(ValueError, match="no server-side form"): + read(filter_pair.remote, expr) + + # Node collections that take a `[expr]` subscript. Each must refuse an # edge-testing expression identically, so the check runs at every site rather # than only at `graph.nodes`. diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 6d8cccc6d7..21eb1b712e 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -16,7 +16,7 @@ use raphtory::{ model::{ edge_filter::CompositeEdgeFilter, exploded_edge_filter::CompositeExplodedEdgeFilter, - filter::{Filter, FilterValue}, + filter::{Filter, FilterValue, NODE_ID_FIELD, NODE_NAME_FIELD, NODE_TYPE_FIELD}, filter_operator::FilterOperator, graph_filter::GraphFilter, node_filter::CompositeNodeFilter, @@ -276,9 +276,9 @@ impl Display for NodeField { f, "{}", match self { - NodeField::NodeId => "node_id", - NodeField::NodeName => "node_name", - NodeField::NodeType => "node_type", + NodeField::NodeId => NODE_ID_FIELD, + NodeField::NodeName => NODE_NAME_FIELD, + NodeField::NodeType => NODE_TYPE_FIELD, } ) } @@ -2053,9 +2053,9 @@ fn apply_ops_to_condition(base: PropCondition, ops: &[Op]) -> PropCondition { /// (`Id` / `Name` / `NodeType`). fn filter_to_node_field(f: Filter) -> Result { let variant: fn(NodeFieldWhere) -> GqlNodeFilter = match f.field_name.as_str() { - "node_id" => GqlNodeFilter::Id, - "node_name" => GqlNodeFilter::Name, - "node_type" => GqlNodeFilter::NodeType, + NODE_ID_FIELD => GqlNodeFilter::Id, + NODE_NAME_FIELD => GqlNodeFilter::Name, + NODE_TYPE_FIELD => GqlNodeFilter::NodeType, other => { return Err(GraphError::InvalidGqlFilter(format!( "unknown node field name for wire conversion: {}", diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs index 81d0b1e16f..61a0c011b2 100644 --- a/raphtory-tests/tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -844,7 +844,7 @@ fn active_graph() -> Graph { #[test] fn test_edge_property_is_true() { let g = active_graph(); - let filter = EdgeFilter.property("active").is_true(); + let filter = EdgeFilter.property("active").eq(Prop::Bool(true)); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["A->B", "E->F"]); } @@ -852,7 +852,7 @@ fn test_edge_property_is_true() { #[test] fn test_edge_property_is_false() { let g = active_graph(); - let filter = EdgeFilter.property("active").is_false(); + let filter = EdgeFilter.property("active").eq(Prop::Bool(false)); let result = g.filter(filter).unwrap(); assert_eq!(sorted_edges(result), vec!["C->D"]); } diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 3e613a0fda..d33e0af428 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -1947,7 +1947,7 @@ mod test_node_filter { assert_filter( &graph, - NodeFilter.degree().lt(threshold).or(EntityExprFilterOps::not(NodeFilter.degree().gt(threshold + 5))), + NodeFilter.degree().lt(threshold).or(NodeFilter.degree().gt(threshold + 5).not()), Direction::BOTH, |d| d < threshold as usize || d <= (threshold + 5) as usize, &format!("BOTH < {} OR BOTH > {}", threshold, threshold + 5), @@ -1955,7 +1955,7 @@ mod test_node_filter { assert_filter( &graph, - NodeFilter.in_degree().lt(threshold).or(EntityExprFilterOps::not(NodeFilter.in_degree().gt(threshold + 5))), + NodeFilter.in_degree().lt(threshold).or(NodeFilter.in_degree().gt(threshold + 5).not()), Direction::IN, |d| d < threshold as usize || d <= (threshold + 5) as usize, &format!("IN < {} OR IN > {}", threshold, threshold + 5), @@ -1963,7 +1963,7 @@ mod test_node_filter { assert_filter( &graph, - NodeFilter.out_degree().lt(threshold).or(EntityExprFilterOps::not(NodeFilter.out_degree().gt(threshold + 5))), + NodeFilter.out_degree().lt(threshold).or(NodeFilter.out_degree().gt(threshold + 5).not()), Direction::OUT, |d| d < threshold as usize || d <= (threshold + 5) as usize, &format!("OUT < {} OR OUT > {}", threshold, threshold + 5), @@ -2510,8 +2510,7 @@ mod test_node_filter { #[test] fn test_filter_nodes_for_not_node_type() { - let filter = - EntityExprFilterOps::not(NodeFilter.node_type().is_not_in(vec!["fire_nation"])); + let filter = NodeFilter.node_type().is_not_in(vec!["fire_nation"]).not(); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3552,12 +3551,12 @@ mod test_node_property_filter { #[test] fn test_filter_nodes_for_not_property() { - // Under SQL-NULL semantics, .not() rejects nodes whose property is absent - // (None cannot satisfy a value comparison). Use "ship" so nodes 1 and 3 - // (p10 = "Paper_airplane", does not contain "ship") pass; node 2 - // (p10 = "Paper_ship") and node 4 (no p10) are rejected. - let filter = EntityExprFilterOps::not(NodeFilter.property("p10").contains("ship")); - let expected_results: Vec<&str> = vec!["1", "3"]; + // `.not()` is the set complement, the same as python's `~`: the inner + // predicate selects node 2 (p10 = "Paper_ship") only, so the complement + // keeps nodes 1 and 3 (p10 = "Paper_airplane") and node 4, which has no + // p10 and therefore was never selected. + let filter = NodeFilter.property("p10").contains("ship").not(); + let expected_results: Vec<&str> = vec!["1", "3", "4"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -8110,8 +8109,8 @@ mod test_edge_filter { init_edges_graph_with_str_ids_del, init_nodes_graph, IdentityGraphTransformer, }; use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, EdgeViewFilterOps, EntityExprFilterOps, NodeFilterFactory, - PropertyExprFactory, ViewWrapOps, + edge_filter::EdgeFilter, ComposableFilter, EdgeViewFilterOps, EntityExprFilterOps, + NodeFilterFactory, PropertyExprFactory, ViewWrapOps, }; use raphtory_tests::assertions::{ assert_filter_edges_results, assert_select_edges_results, TestGraphVariants, TestVariants, @@ -8458,7 +8457,7 @@ mod test_edge_filter { #[test] fn test_filter_edges_for_not_src() { - let filter = EntityExprFilterOps::not(EdgeFilter::src().name().is_not_in(vec!["1"])); + let filter = EdgeFilter::src().name().is_not_in(vec!["1"]).not(); let expected_results = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -9920,7 +9919,7 @@ mod test_edge_property_filter { #[test] fn test_filter_edges_for_not_property() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. Search API uses filter API internally for this filter. - let filter = EntityExprFilterOps::not(EdgeFilter.property("p2").ne(2u64)); + let filter = EdgeFilter.property("p2").ne(2u64).not(); let expected_results = vec!["2->3"]; assert_filter_edges_results( init_edges_graph, 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 8b83ad6f5c..0dfc7bdb22 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -15,7 +15,7 @@ use crate::{ is_valid_filter::IsValidEdge, latest_filter::Latest, layered_filter::Layered, - node_expr::{CreateOp, EntityExpr, EntityExprBuilder}, + node_expr::{CreateOp, EntityExpr, PredicateLhs}, node_filter::{CompositeNodeFilter, NodeFilter}, property_filter::PropertyFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, @@ -341,7 +341,7 @@ impl ComposableFilter for EdgeEndpointNodeFilter {} // ── expr layer: endpoint expressions bridge node ops into edge ops ── -impl EntityExprBuilder for EdgeEndpointWrapper {} +impl PredicateLhs for EdgeEndpointWrapper {} impl EntityExpr for EdgeEndpointWrapper { type Marker = EdgeFilter; diff --git a/raphtory/src/db/graph/views/filter/model/filter.rs b/raphtory/src/db/graph/views/filter/model/filter.rs index fb540a7dd1..5d31772d7f 100644 --- a/raphtory/src/db/graph/views/filter/model/filter.rs +++ b/raphtory/src/db/graph/views/filter/model/filter.rs @@ -240,7 +240,14 @@ impl Filter { } } -// The generic `filter_value::FilterValue` owns the short name; the composite -// machinery keeps its original spelling through this alias and both leave with -// the composite path. +// The GraphQL model spells a field's filter value `FilterValue`; the generic +// `filter_value::FilterValue` owns that name here, so the field form carries +// the longer one and this alias keeps the model's spelling valid. pub type FilterValue = FieldFilterValue; + +/// Wire names of the built-in node fields, shared by every layer that spells +/// them: the python wire recording, the GraphQL field mapping, and the +/// composite data they both produce. +pub const NODE_ID_FIELD: &str = "node_id"; +pub const NODE_NAME_FIELD: &str = "node_name"; +pub const NODE_TYPE_FIELD: &str = "node_type"; diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 4999d8836e..795426bc62 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -521,10 +521,10 @@ impl EntityAggOps for MetadataExpr { } // ───────────────────────────────────────────────────────────────────────────── -// EdgeFilterFactory — marker for edge-side filter builder types +// EdgeFilterFactory — marker for edge-side filter factory types // ───────────────────────────────────────────────────────────────────────────── -/// Marker trait for edge filter builder types (`EdgeFilter`, `Windowed`, etc.). +/// Marker trait for edge filter factory types (`EdgeFilter`, `Windowed`, etc.). /// /// Disjoint from `NodeFilterFactory`: no type implements both, so `PropertyExpr` /// can have two separate sets of comparison methods gated on each. @@ -537,7 +537,7 @@ pub trait EdgeFilterFactory: PropertyExprFactory + Clone {} use crate::db::graph::views::filter::model::{ edge_expr::ops::{EdgeMetaOp, EdgePropOp}, graph_filter::GraphFilterOps, - node_expr::{CreateOp, DynTemporal, EntityExpr, EntityExprBuilder}, + node_expr::{CreateOp, DynTemporal, EntityExpr, PredicateLhs}, }; use edge_expr::EdgeOp; use raphtory_api::core::entities::properties::prop::PropType; @@ -957,20 +957,6 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { } } - fn is_true(self) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(true), entity) - } - - fn is_false(self) -> BinaryCmpExpr { - let entity = self.entity(); - BinaryCmpExpr::new(self, BinaryOp::Eq, Prop::Bool(false), entity) - } - - fn not(self) -> BinaryCmpExpr { - self.eq(Prop::Bool(false)) - } - fn any(self) -> AnyExpr { AnyExpr(self) } @@ -980,23 +966,23 @@ pub trait EntityExprFilterOps: EntityExpr + Sized { } } -impl EntityExprFilterOps for E {} +impl EntityExprFilterOps for E {} // Concrete LHS markers -impl EntityExprBuilder for NodeFilter {} -impl EntityExprBuilder for EdgeFilter {} -impl EntityExprBuilder for ExplodedEdgeFilter {} +impl PredicateLhs for NodeFilter {} +impl PredicateLhs for EdgeFilter {} +impl PredicateLhs for ExplodedEdgeFilter {} // Property / metadata accessors -impl EntityExprBuilder for PropertyExpr {} -impl EntityExprBuilder for MetadataExpr {} +impl PredicateLhs for PropertyExpr {} +impl PredicateLhs for MetadataExpr {} -// View modifiers preserve builder-ness -impl EntityExprBuilder for Windowed {} -impl EntityExprBuilder for Layered {} -impl EntityExprBuilder for Latest {} -impl EntityExprBuilder for SnapshotAt {} -impl EntityExprBuilder for SnapshotLatest {} +// A view wrapper stands on the left-hand side whenever its inner expression does +impl PredicateLhs for Windowed {} +impl PredicateLhs for Layered {} +impl PredicateLhs for Latest {} +impl PredicateLhs for SnapshotAt {} +impl PredicateLhs for SnapshotLatest {} /// Reject ordering operators on boolean properties. //. TODO: Also check if both the types are comparable. @@ -1054,18 +1040,22 @@ pub fn resolved_prop_type(expr_pt: PropType, op_pt: PropType) -> PropType { /// when it does not convert. Used where the expression defines the comparison's /// type rather than adopting the constant's — see [`CreateOp::const_cast_type`]. pub fn cast_const_to(target: &PropType, value: Option<&Prop>) -> Result, GraphError> { - match value { - None => Ok(None), - Some(v) if v.dtype() == *target => Ok(Some(v.clone())), - Some(v) => v.clone().try_cast(target.clone()).map(Some).map_err(|v| { - GraphError::InvalidFilter(format!( - "value {:?} of type {} cannot be compared as {}", - v, - v.dtype(), - target - )) - }), + value.map(|v| cast_prop_to(target, v)).transpose() +} + +/// [`cast_const_to`] for a value that is known to be present. +pub fn cast_prop_to(target: &PropType, value: &Prop) -> Result { + if value.dtype() == *target { + return Ok(value.clone()); } + value.clone().try_cast(target.clone()).map_err(|v| { + GraphError::InvalidFilter(format!( + "value {:?} of type {} cannot be compared as {}", + v, + v.dtype(), + target + )) + }) } /// Only fires when both sides are known and the RHS is a literal/const. Defers diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index e285865ee8..5b4ba73774 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -35,8 +35,8 @@ use crate::{ edge_filter::EdgeEndpointWrapper, filter_operator::ElemQual, node_expr::{ - AvgExpr, CreateOp, EntityAggOps, EntityExpr, EntityExprBuilder, FirstExpr, - LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr, + AvgExpr, CreateOp, EntityAggOps, EntityExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, + MinExpr, PredicateLhs, SumExpr, }, CreateView, EntityMarker, PropertyExpr, }, @@ -166,7 +166,7 @@ impl EntityExpr for Arc { } } -impl EntityExprBuilder for Arc {} +impl PredicateLhs for Arc {} impl CreateOp for Arc { fn const_cast_type(&self) -> Option { 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 75a7e293c2..8ec353a9fb 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 @@ -66,8 +66,8 @@ use super::{ AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, SumNodeOp, TemporalNodePropOp, WithPropType, }, - AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, CreateOp, EntityExpr, EntityExprBuilder, - FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, SumEdgeOp, + AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, CreateOp, EntityExpr, FirstEdgeOp, + LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, PredicateLhs, SumEdgeOp, }; use crate::{ db::{ @@ -122,7 +122,7 @@ impl EntityExpr for Id { } } -impl EntityExprBuilder for Id {} +impl PredicateLhs for Id {} impl CreateOp for Id { fn selects_node_id(&self) -> bool { @@ -174,7 +174,7 @@ impl EntityExpr for Name { } } -impl EntityExprBuilder for Name {} +impl PredicateLhs for Name {} impl CreateOp for Name { fn create_node_op<'g, G: GraphView + 'g>( @@ -200,7 +200,7 @@ impl EntityExpr for Type { } } -impl EntityExprBuilder for Type {} +impl PredicateLhs for Type {} impl CreateOp for Type { fn create_node_op<'g, G: GraphView + 'g>( @@ -476,7 +476,7 @@ impl EntityExpr for DegreeExpr } } -impl EntityExprBuilder for DegreeExpr {} +impl PredicateLhs for DegreeExpr {} impl CreateOp for DegreeExpr { fn const_cast_type(&self) -> Option { @@ -525,7 +525,7 @@ impl EntityExpr for TemporalPropE } } -impl EntityExprBuilder for TemporalPropExpr {} +impl PredicateLhs for TemporalPropExpr {} impl EntityAggOps for TemporalPropExpr { fn sum(self) -> SumExpr { @@ -683,7 +683,7 @@ macro_rules! impl_agg_expr { } } - impl EntityExprBuilder for $expr {} + impl PredicateLhs for $expr {} impl EntityAggOps for $expr { fn sum(self) -> SumExpr { 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 6d949366c8..cbb614a35c 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 @@ -46,7 +46,7 @@ use super::{ ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, ListAwareUnaryNodeOp, PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, }, - CreateOp, EntityExpr, EntityExprBuilder, Marker, + CreateOp, EntityExpr, Marker, PredicateLhs, }; use crate::{ db::{ @@ -56,7 +56,7 @@ use crate::{ }, graph::views::filter::{ model::{ - cast_const_to, coerce_set_values, + cast_const_to, cast_prop_to, coerce_set_values, edge_expr::{ ops::{ ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, @@ -150,7 +150,7 @@ impl BinaryCmpExpr { impl ComposableFilter for BinaryCmpExpr {} -impl EntityExprBuilder for BinaryCmpExpr {} +impl PredicateLhs for BinaryCmpExpr {} impl EntityExpr for BinaryCmpExpr { type Marker = E; @@ -373,7 +373,7 @@ impl UnaryExpr { impl ComposableFilter for UnaryExpr {} -impl EntityExprBuilder for UnaryExpr {} +impl PredicateLhs for UnaryExpr {} impl EntityExpr for UnaryExpr { type Marker = M; @@ -564,7 +564,7 @@ impl StringExpr { impl ComposableFilter for StringExpr {} -impl EntityExprBuilder for StringExpr {} +impl PredicateLhs for StringExpr {} impl EntityExpr for StringExpr { type Marker = M; @@ -753,7 +753,7 @@ impl PropValueSetExpr { impl ComposableFilter for PropValueSetExpr {} -impl EntityExprBuilder for PropValueSetExpr {} +impl PredicateLhs for PropValueSetExpr {} impl EntityExpr for PropValueSetExpr { type Marker = M; @@ -829,10 +829,7 @@ impl CreateFilter for PropValueSetExpr { Some(target) => self .values .into_iter() - .map(|v| { - cast_const_to(&target, Some(&v)) - .map(|c| c.expect("a present value casts to a present value")) - }) + .map(|v| cast_prop_to(&target, &v)) .collect::, _>>()?, None => coerce_set_values(&lhs_pt, self.values)?, }; @@ -932,10 +929,6 @@ impl CreateFilter for PropValueSetExpr { } } -// ── The expr layer has no composite form: these filters exist only as compiled ops. ── -// The conversion is fallible by design, so "not representable" is an answer, not a lie; -// the composite path survives solely for its remaining GraphQL and grant-lowering consumers. - use crate::db::graph::views::filter::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, 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 index 3ce133b6d6..0a98e2797a 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -125,15 +125,15 @@ pub trait EntityExpr: Clone + Send + Sync + 'static { } } -/// Marker for types that initiate a filter expression chain (LHS receiver for -/// `.eq` / `.gt` / `.contains` / ...). +/// Marker for the expressions that stand on the left-hand side of a predicate +/// (`.eq` / `.gt` / `.contains` / ...). /// /// Scoped narrowly (not blanket-impl'd for every `EntityExpr`) to avoid name /// collisions with stdlib methods like `str::contains` / `PartialOrd::gt` on /// primitive `EntityExpr` types (`String`, `&str`, `usize`, numerics, `Prop`). /// /// Mirrors the same trick used by `EntityAggOps` for `min`/`max`/`sum`. -pub trait EntityExprBuilder: EntityExpr {} +pub trait PredicateLhs: EntityExpr {} /// Scopes an expression to a view chain: the inner expression is compiled against the view the /// chain constructs over the incoming graph. This is how a factory chain (window, latest, layers) 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 index 0391e37981..78ac69c3be 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -339,10 +339,7 @@ macro_rules! impl_agg_entity_op { impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |pt| sum_out_type(pt), |vals| { aggregate_list_values(vals, &|pi| { let mut vals = pi.peekable(); - if vals.peek().is_none() { - return None; - } - let inner = vals.peek().unwrap().dtype(); + let inner = vals.peek()?.dtype(); match inner { PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { let (promoted, s64, s128, _) = scan_u64_sum(vals)?; @@ -382,10 +379,7 @@ impl_agg_entity_op!( |vals| { aggregate_list_values(vals, &|pi| { let mut vals = pi.peekable(); - if vals.peek().is_none() { - return None; - } - let inner = vals.peek().unwrap().dtype(); + let inner = vals.peek()?.dtype(); match inner { PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { let (promoted, s64, s128, count) = scan_u64_sum(vals)?; @@ -748,14 +742,25 @@ impl<'g, T: Comparable + Clone + Send + Sync + 'static> NodeOp for BinaryCmpNode /// superset of the matches, so a constant that only compares equal after value /// coercion falls back to the unrestricted domain instead of guessing. pub(crate) fn gid_for_id_lookup(id_type: Option, value: &Prop) -> Option { - match (id_type?, value) { - (GidType::Str, Prop::Str(s)) => Some(GID::Str(s.to_string())), - (GidType::U64, Prop::U64(n)) => Some(GID::U64(*n)), - (GidType::U64, Prop::U32(n)) => Some(GID::U64(*n as u64)), - (GidType::U64, Prop::U16(n)) => Some(GID::U64(*n as u64)), - (GidType::U64, Prop::U8(n)) => Some(GID::U64(*n as u64)), - (GidType::U64, Prop::I64(n)) => u64::try_from(*n).ok().map(GID::U64), - (GidType::U64, Prop::I32(n)) => u64::try_from(*n).ok().map(GID::U64), + let aligned = match (id_type?, value) { + (GidType::Str, Prop::Str(_)) => true, + (GidType::U64, v) => v.is_numeric(), + _ => false, + }; + aligned.then(|| prop_as_gid(value)).flatten() +} + +/// The GID a constant names, by its own variant: a string is a string id, and +/// any integer that fits is a numeric id. +pub(crate) fn prop_as_gid(value: &Prop) -> Option { + match value { + Prop::Str(s) => Some(GID::Str(s.to_string())), + Prop::U64(n) => Some(GID::U64(*n)), + Prop::U32(n) => Some(GID::U64(*n as u64)), + Prop::U16(n) => Some(GID::U64(*n as u64)), + Prop::U8(n) => Some(GID::U64(*n as u64)), + Prop::I64(n) => u64::try_from(*n).ok().map(GID::U64), + Prop::I32(n) => u64::try_from(*n).ok().map(GID::U64), _ => None, } } 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 4fba390f59..c71f78d49c 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 @@ -532,14 +532,14 @@ fn build_bool_graph() -> Graph { #[test] fn is_true_keeps_only_true_nodes() { let g = build_bool_graph(); - let filter = NodeFilter.property("active").is_true(); + let filter = NodeFilter.property("active").eq(Prop::Bool(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(); + let filter = NodeFilter.property("active").eq(Prop::Bool(false)); assert_eq!(filtered_names(filter, g), vec!["off"]); } @@ -547,7 +547,7 @@ fn is_false_keeps_only_false_nodes() { 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 filter = NodeFilter.property("active").eq(Prop::Bool(true)); let names = filtered_names(filter, g); assert!(!names.contains(&"na".to_string())); } diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index ae0585b3dc..edbe57da4a 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -1,6 +1,7 @@ use crate::{ db::graph::views::filter::model::{ edge_filter::{CompositeEdgeFilter, EdgeEndpointWrapper, EdgeFilter, Endpoint}, + filter::{NODE_ID_FIELD, NODE_NAME_FIELD, NODE_TYPE_FIELD}, is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, @@ -64,7 +65,7 @@ impl PyEdgeEndpoint { fn id(&self) -> PyExpr { PyExpr::new( Arc::new(self.0.id()), - Some(self.lhs(WireTarget::Field("node_id"))), + Some(self.lhs(WireTarget::Field(NODE_ID_FIELD))), ) } @@ -75,7 +76,7 @@ impl PyEdgeEndpoint { fn name(&self) -> PyExpr { PyExpr::new( Arc::new(self.0.name()), - Some(self.lhs(WireTarget::Field("node_name"))), + Some(self.lhs(WireTarget::Field(NODE_NAME_FIELD))), ) } @@ -86,7 +87,7 @@ impl PyEdgeEndpoint { fn node_type(&self) -> PyExpr { PyExpr::new( Arc::new(self.0.node_type()), - Some(self.lhs(WireTarget::Field("node_type"))), + Some(self.lhs(WireTarget::Field(NODE_TYPE_FIELD))), ) } @@ -259,20 +260,12 @@ impl PyEdgeFilter { } } -/// Entry point for constructing edge filter expressions. -/// -/// The `Edge` filter provides: -/// - endpoint filters via `src()` and `dst()`, -/// - property and metadata filters, -/// - view restrictions (time windows, snapshots, layers), -/// - and structural predicates over edge state (active/valid/deleted/self-loop). +/// An edge filter scoped to a view. /// -/// Examples: -/// Edge.src().id() == 1 -/// Edge.property("weight") > 0.5 -/// Edge.window(0, 10).is_active() -/// Edge.layer("fire_nation").is_valid() -#[pyclass(frozen, name = "Edge", module = "raphtory.filter")] +/// Obtained from the view methods on [`Edge`] (`Edge.window(...)`, +/// `Edge.layer(...)`, ...); its endpoint, property and structural predicates +/// evaluate within that view, and its own view methods narrow it further. +#[pyclass(frozen, name = "EdgeFilter", module = "raphtory.filter")] pub struct PyEdgeFilter(Arc, Vec); #[pymethods] @@ -283,11 +276,17 @@ impl PyEdgeFilter { } /// Selects the edge **source endpoint** for filtering. + /// + /// Returns: + /// filter.EdgeEndpoint: fn src(&self) -> PyEdgeEndpoint { PyEdgeEndpoint(EdgeFilter::src(), Endpoint::Src, self.1.clone()) } /// Selects the edge **destination endpoint** for filtering. + /// + /// Returns: + /// filter.EdgeEndpoint: fn dst(&self) -> PyEdgeEndpoint { PyEdgeEndpoint(EdgeFilter::dst(), Endpoint::Dst, self.1.clone()) } @@ -296,6 +295,9 @@ impl PyEdgeFilter { /// /// Arguments: /// name (str): Property key. + /// + /// Returns: + /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); PyPropertyExpr::new(self.0.dyn_property(name), Some(lhs)) @@ -305,17 +307,33 @@ impl PyEdgeFilter { /// /// Arguments: /// name (str): Metadata key. + /// + /// Returns: + /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); PyExpr::new(self.0.dyn_metadata(name), Some(lhs)) } /// Restricts edge evaluation to the given time window. + /// + /// Arguments: + /// start (int): Start time. + /// end (int): End time. + /// + /// Returns: + /// filter.EdgeFilter: fn window(&self, start: EventTime, end: EventTime) -> PyEdgeFilter { self.wrap(self.0.dyn_window(start, end), WireView::Window(start, end)) } /// Restricts edge evaluation to a single point in time. + /// + /// Arguments: + /// time (int): Event time. + /// + /// Returns: + /// filter.EdgeFilter: fn at(&self, time: EventTime) -> PyEdgeFilter { self.wrap( self.0.dyn_at(time), @@ -324,6 +342,12 @@ impl PyEdgeFilter { } /// Restricts edge evaluation to times strictly after the given time. + /// + /// Arguments: + /// time (int): Lower time bound. + /// + /// Returns: + /// filter.EdgeFilter: fn after(&self, time: EventTime) -> PyEdgeFilter { self.wrap( self.0.dyn_after(time), @@ -335,6 +359,12 @@ impl PyEdgeFilter { } /// Restricts edge evaluation to times strictly before the given time. + /// + /// Arguments: + /// time (int): Upper time bound. + /// + /// Returns: + /// filter.EdgeFilter: fn before(&self, time: EventTime) -> PyEdgeFilter { self.wrap( self.0.dyn_before(time), @@ -343,21 +373,39 @@ impl PyEdgeFilter { } /// Evaluates edge predicates against the latest available edge state. + /// + /// Returns: + /// filter.EdgeFilter: fn latest(&self) -> PyEdgeFilter { self.wrap(self.0.dyn_latest(), WireView::Latest) } /// Evaluates edge predicates against a snapshot of the graph at a given time. + /// + /// Arguments: + /// time (int): Snapshot time. + /// + /// Returns: + /// filter.EdgeFilter: fn snapshot_at(&self, time: EventTime) -> PyEdgeFilter { self.wrap(self.0.dyn_snapshot_at(time), WireView::SnapshotAt(time)) } /// Evaluates edge predicates against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.EdgeFilter: fn snapshot_latest(&self) -> PyEdgeFilter { self.wrap(self.0.dyn_snapshot_latest(), WireView::SnapshotLatest) } /// Restricts evaluation to edges belonging to the given layer. + /// + /// Arguments: + /// layer (str): Layer name. + /// + /// Returns: + /// filter.EdgeFilter: fn layer(&self, layer: String) -> PyEdgeFilter { self.wrap( self.0.dyn_layer(vec![layer.clone()]), @@ -366,12 +414,21 @@ impl PyEdgeFilter { } /// Restricts evaluation to edges belonging to any of the given layers. + /// + /// Arguments: + /// layers (list[str]): Layer names. + /// + /// Returns: + /// filter.EdgeFilter: fn layers(&self, layers: FromIterable) -> PyEdgeFilter { - let layers = layers.to_vec(); + let layers: Vec = layers.into(); self.wrap(self.0.dyn_layer(layers.clone()), WireView::Layers(layers)) } /// Matches edges that have at least one event in the current view. + /// + /// Returns: + /// filter.FilterExpr: fn is_active(&self) -> PyFilterExpr { let tree = FilterTree::Edge(wrap_edge_views( CompositeEdgeFilter::IsActiveEdge(IsActiveEdge), @@ -381,6 +438,9 @@ impl PyEdgeFilter { } /// Matches edges that are structurally valid in the current view. + /// + /// Returns: + /// filter.FilterExpr: fn is_valid(&self) -> PyFilterExpr { let tree = FilterTree::Edge(wrap_edge_views( CompositeEdgeFilter::IsValidEdge(IsValidEdge), @@ -390,6 +450,9 @@ impl PyEdgeFilter { } /// Matches edges that have been deleted. + /// + /// Returns: + /// filter.FilterExpr: fn is_deleted(&self) -> PyFilterExpr { let tree = FilterTree::Edge(wrap_edge_views( CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge), @@ -399,6 +462,9 @@ impl PyEdgeFilter { } /// Matches edges that are self-loops (source == destination). + /// + /// Returns: + /// filter.FilterExpr: fn is_self_loop(&self) -> PyFilterExpr { let tree = FilterTree::Edge(wrap_edge_views( CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge), @@ -407,3 +473,195 @@ impl PyEdgeFilter { PyFilterExpr(self.0.dyn_is_self_loop(), Some(tree)) } } + +/// Entry point for constructing edge filter expressions. +/// +/// Every method is static: `Edge.src().name() == "alice"` selects edges +/// directly, and the view methods return an [`EdgeFilter`] scoped to that +/// view for further chaining. +#[pyclass(frozen, name = "Edge", module = "raphtory.filter")] +pub struct PyEdge; + +#[pymethods] +impl PyEdge { + /// Selects the edge **source endpoint** for filtering. + /// + /// Returns: + /// filter.EdgeEndpoint: + #[staticmethod] + fn src() -> PyEdgeEndpoint { + PyEdgeFilter::root().src() + } + + /// Selects the edge **destination endpoint** for filtering. + /// + /// Returns: + /// filter.EdgeEndpoint: + #[staticmethod] + fn dst() -> PyEdgeEndpoint { + PyEdgeFilter::root().dst() + } + + /// Filters an edge property by name. + /// + /// Arguments: + /// name (str): Property key. + /// + /// Returns: + /// filter.PropertyExpr: + #[staticmethod] + fn property(name: String) -> PyPropertyExpr { + PyEdgeFilter::root().property(name) + } + + /// Filters an edge metadata field by name. + /// + /// Arguments: + /// name (str): Metadata key. + /// + /// Returns: + /// filter.Expr: + #[staticmethod] + fn metadata(name: String) -> PyExpr { + PyEdgeFilter::root().metadata(name) + } + + /// Restricts edge evaluation to the given time window. + /// + /// Arguments: + /// start (int): Start time. + /// end (int): End time. + /// + /// Returns: + /// filter.EdgeFilter: + #[staticmethod] + fn window(start: EventTime, end: EventTime) -> PyEdgeFilter { + PyEdgeFilter::root().window(start, end) + } + + /// Restricts edge evaluation to a single point in time. + /// + /// Arguments: + /// time (int): Event time. + /// + /// Returns: + /// filter.EdgeFilter: + #[staticmethod] + fn at(time: EventTime) -> PyEdgeFilter { + PyEdgeFilter::root().at(time) + } + + /// Restricts edge evaluation to times strictly after the given time. + /// + /// Arguments: + /// time (int): Lower time bound. + /// + /// Returns: + /// filter.EdgeFilter: + #[staticmethod] + fn after(time: EventTime) -> PyEdgeFilter { + PyEdgeFilter::root().after(time) + } + + /// Restricts edge evaluation to times strictly before the given time. + /// + /// Arguments: + /// time (int): Upper time bound. + /// + /// Returns: + /// filter.EdgeFilter: + #[staticmethod] + fn before(time: EventTime) -> PyEdgeFilter { + PyEdgeFilter::root().before(time) + } + + /// Evaluates edge predicates against the latest available edge state. + /// + /// Returns: + /// filter.EdgeFilter: + #[staticmethod] + fn latest() -> PyEdgeFilter { + PyEdgeFilter::root().latest() + } + + /// Evaluates edge predicates against a snapshot of the graph at a given time. + /// + /// Arguments: + /// time (int): Snapshot time. + /// + /// Returns: + /// filter.EdgeFilter: + #[staticmethod] + fn snapshot_at(time: EventTime) -> PyEdgeFilter { + PyEdgeFilter::root().snapshot_at(time) + } + + /// Evaluates edge predicates against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.EdgeFilter: + #[staticmethod] + fn snapshot_latest() -> PyEdgeFilter { + PyEdgeFilter::root().snapshot_latest() + } + + /// Restricts evaluation to edges belonging to the given layer. + /// + /// Arguments: + /// layer (str): Layer name. + /// + /// Returns: + /// filter.EdgeFilter: + #[staticmethod] + fn layer(layer: String) -> PyEdgeFilter { + PyEdgeFilter::root().layer(layer) + } + + /// Restricts evaluation to edges belonging to any of the given layers. + /// + /// Arguments: + /// layers (list[str]): Layer names. + /// + /// Returns: + /// filter.EdgeFilter: + #[staticmethod] + fn layers(layers: FromIterable) -> PyEdgeFilter { + PyEdgeFilter::root().layers(layers) + } + + /// Matches edges that have at least one event in the current view. + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn is_active() -> PyFilterExpr { + PyEdgeFilter::root().is_active() + } + + /// Matches edges that are structurally valid in the current view. + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn is_valid() -> PyFilterExpr { + PyEdgeFilter::root().is_valid() + } + + /// Matches edges that have been deleted. + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn is_deleted() -> PyFilterExpr { + PyEdgeFilter::root().is_deleted() + } + + /// Matches edges that are self-loops (source == destination). + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn is_self_loop() -> PyFilterExpr { + PyEdgeFilter::root().is_self_loop() + } +} diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index 15818603ca..c7311e2854 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -22,22 +22,13 @@ use pyo3::{pyclass, pymethods}; use raphtory_api::core::storage::timeindex::{AsTime, EventTime}; use std::sync::Arc; -/// Entry point for constructing **exploded edge** filter expressions. +/// An exploded-edge filter scoped to a view. /// -/// An **exploded edge** represents an edge view where temporal events are treated -/// as individually addressable edge instances (i.e. "event-level" edges), rather -/// than a single aggregated edge across time. -/// -/// This filter provides: -/// - property and metadata filters, -/// - view restrictions (time windows, snapshots, layers), -/// - and structural predicates over exploded edge state (active/valid/deleted/self-loop). -/// -/// Examples: -/// ExplodedEdge.property("weight") > 0.5 -/// ExplodedEdge.window(0, 10).is_active() -/// ExplodedEdge.layer("fire_nation").is_valid() -#[pyclass(frozen, name = "ExplodedEdge", module = "raphtory.filter")] +/// An exploded edge is one temporal event of an edge, addressed individually +/// rather than as the edge aggregated across time. Obtained from the view +/// methods on [`ExplodedEdge`]; its property and structural predicates evaluate +/// within that view, and its own view methods narrow it further. +#[pyclass(frozen, name = "ExplodedEdgeFilter", module = "raphtory.filter")] pub struct PyExplodedEdgeFilter(Arc, Vec); impl PyExplodedEdgeFilter { @@ -75,6 +66,9 @@ impl PyExplodedEdgeFilter { /// /// Arguments: /// name (str): Property key. + /// + /// Returns: + /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); PyPropertyExpr::new(self.0.dyn_property(name), Some(lhs)) @@ -86,6 +80,9 @@ impl PyExplodedEdgeFilter { /// /// Arguments: /// name (str): Metadata key. + /// + /// Returns: + /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); PyExpr::new(self.0.dyn_metadata(name), Some(lhs)) @@ -94,11 +91,24 @@ impl PyExplodedEdgeFilter { /// Restricts exploded edge evaluation to the given time window. /// /// The window is inclusive of `start` and exclusive of `end`. + /// + /// Arguments: + /// start (int): Start time. + /// end (int): End time. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: fn window(&self, start: EventTime, end: EventTime) -> PyExplodedEdgeFilter { self.wrap(self.0.dyn_window(start, end), WireView::Window(start, end)) } /// Restricts exploded edge evaluation to a single point in time. + /// + /// Arguments: + /// time (int): Event time. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: fn at(&self, time: EventTime) -> PyExplodedEdgeFilter { self.wrap( self.0.dyn_at(time), @@ -107,6 +117,12 @@ impl PyExplodedEdgeFilter { } /// Restricts exploded edge evaluation to times strictly after the given time. + /// + /// Arguments: + /// time (int): Lower time bound. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: fn after(&self, time: EventTime) -> PyExplodedEdgeFilter { self.wrap( self.0.dyn_after(time), @@ -118,6 +134,12 @@ impl PyExplodedEdgeFilter { } /// Restricts exploded edge evaluation to times strictly before the given time. + /// + /// Arguments: + /// time (int): Upper time bound. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: fn before(&self, time: EventTime) -> PyExplodedEdgeFilter { self.wrap( self.0.dyn_before(time), @@ -126,21 +148,39 @@ impl PyExplodedEdgeFilter { } /// Evaluates exploded edge predicates against the latest available state. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: fn latest(&self) -> PyExplodedEdgeFilter { self.wrap(self.0.dyn_latest(), WireView::Latest) } /// Evaluates exploded edge predicates against a snapshot of the graph at a given time. + /// + /// Arguments: + /// time (int): Snapshot time. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: fn snapshot_at(&self, time: EventTime) -> PyExplodedEdgeFilter { self.wrap(self.0.dyn_snapshot_at(time), WireView::SnapshotAt(time)) } /// Evaluates exploded edge predicates against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: fn snapshot_latest(&self) -> PyExplodedEdgeFilter { self.wrap(self.0.dyn_snapshot_latest(), WireView::SnapshotLatest) } /// Restricts evaluation to exploded edges belonging to the given layer. + /// + /// Arguments: + /// layer (str): Layer name. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: fn layer(&self, layer: String) -> PyExplodedEdgeFilter { self.wrap( self.0.dyn_layer(vec![layer.clone()]), @@ -149,12 +189,21 @@ impl PyExplodedEdgeFilter { } /// Restricts evaluation to exploded edges belonging to any of the given layers. + /// + /// Arguments: + /// layers (list[str]): Layer names. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: fn layers(&self, layers: FromIterable) -> PyExplodedEdgeFilter { - let layers = layers.to_vec(); + let layers: Vec = layers.into(); self.wrap(self.0.dyn_layer(layers.clone()), WireView::Layers(layers)) } /// Matches exploded edges that have at least one event in the current view. + /// + /// Returns: + /// filter.FilterExpr: fn is_active(&self) -> PyFilterExpr { let tree = FilterTree::ExplodedEdge(wrap_exploded_views( CompositeExplodedEdgeFilter::IsActiveEdge(IsActiveEdge), @@ -164,6 +213,9 @@ impl PyExplodedEdgeFilter { } /// Matches exploded edges that are structurally valid in the current view. + /// + /// Returns: + /// filter.FilterExpr: fn is_valid(&self) -> PyFilterExpr { let tree = FilterTree::ExplodedEdge(wrap_exploded_views( CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge), @@ -173,6 +225,9 @@ impl PyExplodedEdgeFilter { } /// Matches exploded edges that have been deleted. + /// + /// Returns: + /// filter.FilterExpr: fn is_deleted(&self) -> PyFilterExpr { let tree = FilterTree::ExplodedEdge(wrap_exploded_views( CompositeExplodedEdgeFilter::IsDeletedEdge(IsDeletedEdge), @@ -182,6 +237,9 @@ impl PyExplodedEdgeFilter { } /// Matches exploded edges that are self-loops (source == destination). + /// + /// Returns: + /// filter.FilterExpr: fn is_self_loop(&self) -> PyFilterExpr { let tree = FilterTree::ExplodedEdge(wrap_exploded_views( CompositeExplodedEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge), @@ -190,3 +248,182 @@ impl PyExplodedEdgeFilter { PyFilterExpr(self.0.dyn_is_self_loop(), Some(tree)) } } + +/// Entry point for constructing exploded-edge filter expressions. +/// +/// Every method is static; the view methods return an +/// [`ExplodedEdgeFilter`] scoped to that view for further chaining. +#[pyclass(frozen, name = "ExplodedEdge", module = "raphtory.filter")] +pub struct PyExplodedEdge; + +#[pymethods] +impl PyExplodedEdge { + /// Filters an exploded edge property by name. + /// + /// The property may be static or temporal depending on the query context. + /// + /// Arguments: + /// name (str): Property key. + /// + /// Returns: + /// filter.PropertyExpr: + #[staticmethod] + fn property(name: String) -> PyPropertyExpr { + PyExplodedEdgeFilter::root().property(name) + } + + /// Filters an exploded edge metadata field by name. + /// + /// Metadata is shared across all temporal versions of an exploded edge. + /// + /// Arguments: + /// name (str): Metadata key. + /// + /// Returns: + /// filter.Expr: + #[staticmethod] + fn metadata(name: String) -> PyExpr { + PyExplodedEdgeFilter::root().metadata(name) + } + + /// Restricts exploded edge evaluation to the given time window. + /// + /// The window is inclusive of `start` and exclusive of `end`. + /// + /// Arguments: + /// start (int): Start time. + /// end (int): End time. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + #[staticmethod] + fn window(start: EventTime, end: EventTime) -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter::root().window(start, end) + } + + /// Restricts exploded edge evaluation to a single point in time. + /// + /// Arguments: + /// time (int): Event time. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + #[staticmethod] + fn at(time: EventTime) -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter::root().at(time) + } + + /// Restricts exploded edge evaluation to times strictly after the given time. + /// + /// Arguments: + /// time (int): Lower time bound. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + #[staticmethod] + fn after(time: EventTime) -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter::root().after(time) + } + + /// Restricts exploded edge evaluation to times strictly before the given time. + /// + /// Arguments: + /// time (int): Upper time bound. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + #[staticmethod] + fn before(time: EventTime) -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter::root().before(time) + } + + /// Evaluates exploded edge predicates against the latest available state. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + #[staticmethod] + fn latest() -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter::root().latest() + } + + /// Evaluates exploded edge predicates against a snapshot of the graph at a given time. + /// + /// Arguments: + /// time (int): Snapshot time. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + #[staticmethod] + fn snapshot_at(time: EventTime) -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter::root().snapshot_at(time) + } + + /// Evaluates exploded edge predicates against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + #[staticmethod] + fn snapshot_latest() -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter::root().snapshot_latest() + } + + /// Restricts evaluation to exploded edges belonging to the given layer. + /// + /// Arguments: + /// layer (str): Layer name. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + #[staticmethod] + fn layer(layer: String) -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter::root().layer(layer) + } + + /// Restricts evaluation to exploded edges belonging to any of the given layers. + /// + /// Arguments: + /// layers (list[str]): Layer names. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + #[staticmethod] + fn layers(layers: FromIterable) -> PyExplodedEdgeFilter { + PyExplodedEdgeFilter::root().layers(layers) + } + + /// Matches exploded edges that have at least one event in the current view. + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn is_active() -> PyFilterExpr { + PyExplodedEdgeFilter::root().is_active() + } + + /// Matches exploded edges that are structurally valid in the current view. + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn is_valid() -> PyFilterExpr { + PyExplodedEdgeFilter::root().is_valid() + } + + /// Matches exploded edges that have been deleted. + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn is_deleted() -> PyFilterExpr { + PyExplodedEdgeFilter::root().is_deleted() + } + + /// Matches exploded edges that are self-loops (source == destination). + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn is_self_loop() -> PyFilterExpr { + PyExplodedEdgeFilter::root().is_self_loop() + } +} diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index e1c808338b..74e38114c9 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -9,23 +9,14 @@ use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; use raphtory_api::core::storage::timeindex::{AsTime, EventTime}; use std::sync::Arc; -/// Entry point for constructing **graph-level view filters**. +/// A graph-level view scope. /// -/// The `Graph` filter restricts *when* and *where* the graph is evaluated, -/// independent of node or edge predicates. It defines the **temporal scope** -/// (windows, snapshots, latest state) and **layer scope** for subsequent -/// node and edge filters. -/// -/// All methods are static and return a `Graph`, which can then -/// be refined further or combined with node/edge predicates. -/// -/// Examples: -/// Graph.window(0, 10) -/// Graph.at(5) -/// Graph.latest().layer("fire_nation") -/// Graph.layers(["A", "B"]).snapshot_latest() +/// Obtained from the view methods on [`Graph`] (`Graph.window(...)`, +/// `Graph.latest()`, ...). It carries no node or edge predicate of its own: it +/// fixes the temporal and layer scope that node and edge predicates compose +/// with, and its own view methods narrow it further. #[pyclass( - name = "Graph", + name = "GraphFilter", module = "raphtory.filter", extends = PyFilterExpr, frozen @@ -55,7 +46,7 @@ impl PyGraphFilter { /// end (int): End time. /// /// Returns: - /// filter.Graph: + /// filter.GraphFilter: fn window(&self, start: EventTime, end: EventTime) -> PyGraphFilter { self.extend( self.0.clone().window(start, end), @@ -69,7 +60,7 @@ impl PyGraphFilter { /// time (int): Event time. /// /// Returns: - /// filter.Graph: + /// filter.GraphFilter: fn at(&self, time: EventTime) -> PyGraphFilter { self.extend( self.0.clone().at(time), @@ -86,7 +77,7 @@ impl PyGraphFilter { /// time (int): Lower time bound. /// /// Returns: - /// filter.Graph: + /// filter.GraphFilter: fn after(&self, time: EventTime) -> PyGraphFilter { self.extend( self.0.clone().after(time), @@ -103,7 +94,7 @@ impl PyGraphFilter { /// time (int): Upper time bound. /// /// Returns: - /// filter.Graph: + /// filter.GraphFilter: fn before(&self, time: EventTime) -> PyGraphFilter { self.extend( self.0.clone().before(time), @@ -117,7 +108,7 @@ impl PyGraphFilter { /// Evaluates filters against the latest available state of the graph. /// /// Returns: - /// filter.Graph: + /// filter.GraphFilter: fn latest(&self) -> PyGraphFilter { self.extend(Arc::new(self.0.clone().latest()), GraphViewOp::Latest) } @@ -128,7 +119,7 @@ impl PyGraphFilter { /// time (int): Snapshot time. /// /// Returns: - /// filter.Graph: + /// filter.GraphFilter: fn snapshot_at(&self, time: EventTime) -> PyGraphFilter { self.extend( Arc::new(self.0.clone().snapshot_at(time)), @@ -139,7 +130,7 @@ impl PyGraphFilter { /// Evaluates filters against the most recent snapshot of the graph. /// /// Returns: - /// filter.Graph: + /// filter.GraphFilter: fn snapshot_latest(&self) -> PyGraphFilter { self.extend( Arc::new(self.0.clone().snapshot_latest()), @@ -153,7 +144,7 @@ impl PyGraphFilter { /// layer (str): Layer name. /// /// Returns: - /// filter.Graph: + /// filter.GraphFilter: fn layer(&self, layer: String) -> PyGraphFilter { self.extend( Arc::new(self.0.clone().layer(layer.clone())), @@ -167,7 +158,7 @@ impl PyGraphFilter { /// layers (list[str]): Layer names. /// /// Returns: - /// filter.Graph: + /// filter.GraphFilter: fn layers(&self, layers: FromIterable) -> PyGraphFilter { let names: Vec = layers.into(); self.extend( @@ -177,6 +168,121 @@ impl PyGraphFilter { } } +/// Entry point for graph-level view filters. +/// +/// Every method is static and returns a [`GraphFilter`] carrying the view, +/// which composes with node and edge predicates. +#[pyclass(frozen, name = "Graph", module = "raphtory.filter")] +pub struct PyGraph; + +#[pymethods] +impl PyGraph { + /// Restricts evaluation to events within a time window. + /// + /// The window is inclusive of `start` and exclusive of `end`. + /// + /// Arguments: + /// start (int): Start time. + /// end (int): End time. + /// + /// Returns: + /// filter.GraphFilter: + #[staticmethod] + fn window(start: EventTime, end: EventTime) -> PyGraphFilter { + PyGraphFilter::root().window(start, end) + } + + /// Restricts evaluation to a single point in time. + /// + /// Arguments: + /// time (int): Event time. + /// + /// Returns: + /// filter.GraphFilter: + #[staticmethod] + fn at(time: EventTime) -> PyGraphFilter { + PyGraphFilter::root().at(time) + } + + /// Restricts evaluation to times strictly after the given time. + /// + /// Arguments: + /// time (int): Lower time bound. + /// + /// Returns: + /// filter.GraphFilter: + #[staticmethod] + fn after(time: EventTime) -> PyGraphFilter { + PyGraphFilter::root().after(time) + } + + /// Restricts evaluation to times strictly before the given time. + /// + /// Arguments: + /// time (int): Upper time bound. + /// + /// Returns: + /// filter.GraphFilter: + #[staticmethod] + fn before(time: EventTime) -> PyGraphFilter { + PyGraphFilter::root().before(time) + } + + /// Evaluates filters against the latest available state of the graph. + /// + /// Returns: + /// filter.GraphFilter: + #[staticmethod] + fn latest() -> PyGraphFilter { + PyGraphFilter::root().latest() + } + + /// Evaluates filters against a snapshot of the graph at a given time. + /// + /// Arguments: + /// time (int): Snapshot time. + /// + /// Returns: + /// filter.GraphFilter: + #[staticmethod] + fn snapshot_at(time: EventTime) -> PyGraphFilter { + PyGraphFilter::root().snapshot_at(time) + } + + /// Evaluates filters against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.GraphFilter: + #[staticmethod] + fn snapshot_latest() -> PyGraphFilter { + PyGraphFilter::root().snapshot_latest() + } + + /// Restricts evaluation to a single layer. + /// + /// Arguments: + /// layer (str): Layer name. + /// + /// Returns: + /// filter.GraphFilter: + #[staticmethod] + fn layer(layer: String) -> PyGraphFilter { + PyGraphFilter::root().layer(layer) + } + + /// Restricts evaluation to any of the given layers. + /// + /// Arguments: + /// layers (list[str]): Layer names. + /// + /// Returns: + /// filter.GraphFilter: + #[staticmethod] + fn layers(layers: FromIterable) -> PyGraphFilter { + PyGraphFilter::root().layers(layers) + } +} + impl<'py> IntoPyObject<'py> for PyGraphFilter { type Target = PyGraphFilter; type Output = Bound<'py, Self::Target>; diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index bac250ee25..622dae3e26 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -1,10 +1,10 @@ use crate::python::{ filter::{ - edge_expr::{PyEdgeEndpoint, PyEdgeFilter}, - exploded_edge_expr::PyExplodedEdgeFilter, + edge_expr::{PyEdge, PyEdgeEndpoint, PyEdgeFilter}, + exploded_edge_expr::{PyExplodedEdge, PyExplodedEdgeFilter}, filter_expr::PyFilterExpr, - graph_filter::PyGraphFilter, - node_expr::PyNodeFilter, + graph_filter::{PyGraph, PyGraphFilter}, + node_expr::{PyExpr, PyNode, PyNodeFilter, PyPropertyExpr}, }, types::iterable::FromIterable, }; @@ -31,22 +31,21 @@ pub fn base_filter_module(py: Python<'_>) -> Result, PyErr> let filter_module = PyModule::new(py, "filter")?; filter_module.add_class::()?; + filter_module.add_class::()?; + filter_module.add_class::()?; + filter_module.add_class::()?; filter_module.add_class::()?; + filter_module.add_class::()?; filter_module.add_class::()?; filter_module.add_class::()?; + filter_module.add_class::()?; filter_module.add_class::()?; - filter_module.add_class::()?; - // The entry points are instances: `filter.Edge.src()` chains through - // instance methods, so the module attributes shadow the classes with - // ready-made roots. - filter_module.add("Node", PyNodeFilter::root())?; - filter_module.add("Edge", PyEdgeFilter::root())?; - filter_module.add("ExplodedEdge", PyExplodedEdgeFilter::root())?; - filter_module.add("Graph", PyGraphFilter::root())?; + filter_module.add_class::()?; + filter_module.add_class::()?; Ok(filter_module) } diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index ea8264a9f8..934a624486 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,8 +1,8 @@ use crate::{ db::graph::views::filter::model::{ - filter::FieldFilterValue, + filter::{FieldFilterValue, NODE_ID_FIELD, NODE_NAME_FIELD, NODE_TYPE_FIELD}, is_active_node_filter::IsActiveNode, - node_expr::{CreateOp, DynCreateOp, DynEntityExpr, DynTemporal}, + node_expr::{ops::prop_as_gid, CreateOp, DynCreateOp, DynEntityExpr, DynTemporal}, node_filter::CompositeNodeFilter, node_state_filter::NodeStateBoolColOp, property_filter::{Op, PropertyFilterValue, PropertyRef}, @@ -109,7 +109,9 @@ impl PyExpr { fn wire_single(&self, v: &Prop) -> Option { let lhs = self.1.as_ref()?; Some(match &lhs.target { - WireTarget::Field("node_id") => WireValue::Field(FieldFilterValue::ID(prop_to_gid(v)?)), + WireTarget::Field(NODE_ID_FIELD) => { + WireValue::Field(FieldFilterValue::ID(prop_as_gid(v)?)) + } WireTarget::Field(_) => match v { Prop::Str(s) => WireValue::Field(FieldFilterValue::Single(s.to_string())), _ => return None, @@ -123,9 +125,9 @@ impl PyExpr { fn wire_set(&self, values: &[Prop]) -> Option { let lhs = self.1.as_ref()?; Some(match &lhs.target { - WireTarget::Field("node_id") => WireValue::Field(FieldFilterValue::IDSet(Arc::new( - values.iter().map(prop_to_gid).collect::>()?, - ))), + WireTarget::Field(NODE_ID_FIELD) => WireValue::Field(FieldFilterValue::IDSet( + Arc::new(values.iter().map(prop_as_gid).collect::>()?), + )), WireTarget::Field(_) => WireValue::Field(FieldFilterValue::Set(Arc::new( values .iter() @@ -150,17 +152,6 @@ impl PyExpr { } } -fn prop_to_gid(v: &Prop) -> Option { - match v { - Prop::Str(s) => Some(GID::Str(s.to_string())), - Prop::U64(n) => Some(GID::U64(*n)), - Prop::I64(n) => u64::try_from(*n).ok().map(GID::U64), - Prop::U32(n) => Some(GID::U64(*n as u64)), - Prop::I32(n) => u64::try_from(*n).ok().map(GID::U64), - _ => None, - } -} - #[pymethods] impl PyExpr { fn __eq__(&self, other: ExprOrValue) -> PyResult { @@ -224,6 +215,13 @@ impl PyExpr { } } + /// Checks whether the value's string representation starts with the given value. + /// + /// Arguments: + /// other (Prop | filter.Expr): Prefix to check for. + /// + /// Returns: + /// filter.FilterExpr: fn starts_with(&self, other: ExprOrValue) -> PyResult { match other { ExprOrValue::Expr(e) => Ok(PyFilterExpr( @@ -238,6 +236,13 @@ impl PyExpr { } } } + /// Checks whether the value's string representation ends with the given value. + /// + /// Arguments: + /// other (Prop | filter.Expr): Suffix to check for. + /// + /// Returns: + /// filter.FilterExpr: fn ends_with(&self, other: ExprOrValue) -> PyResult { match other { ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ends_with(e.0)), None)), @@ -249,6 +254,13 @@ impl PyExpr { } } } + /// Checks whether the value's string representation contains the given value. + /// + /// Arguments: + /// other (Prop | filter.Expr): Substring that must appear within the value. + /// + /// Returns: + /// filter.FilterExpr: fn contains(&self, other: ExprOrValue) -> PyResult { match other { ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().contains(e.0)), None)), @@ -260,6 +272,13 @@ impl PyExpr { } } } + /// Checks whether the value's string representation **does not** contain the given value. + /// + /// Arguments: + /// other (Prop | filter.Expr): Substring that must not appear within the value. + /// + /// Returns: + /// filter.FilterExpr: fn not_contains(&self, other: ExprOrValue) -> PyResult { match other { ExprOrValue::Expr(e) => Ok(PyFilterExpr( @@ -274,6 +293,15 @@ impl PyExpr { } } } + /// Performs fuzzy matching against the value's string representation, within a Levenshtein distance and with optional prefix matching. + /// + /// Arguments: + /// other (Prop | filter.Expr): String to approximately match against. + /// levenshtein_distance (int): Maximum allowed Levenshtein distance. + /// prefix_match (bool): Whether to require a matching prefix. + /// + /// Returns: + /// filter.FilterExpr: fn fuzzy_search( &self, other: ExprOrValue, @@ -310,17 +338,35 @@ impl PyExpr { }) } + /// Checks whether the value is contained within the given values. + /// + /// Arguments: + /// values (list[Prop]): Values to match against. + /// + /// Returns: + /// filter.FilterExpr: fn is_in(&self, values: FromIterable) -> PyFilterExpr { let values: Vec = values.into(); let wire = self.finish(FilterOperator::IsIn, self.wire_set(&values)); PyFilterExpr(Arc::new(self.0.clone().is_in(values)), wire) } + /// Checks whether the value is **not** contained within the given values. + /// + /// Arguments: + /// values (list[Prop]): Values to exclude. + /// + /// Returns: + /// filter.FilterExpr: fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { let values: Vec = values.into(); let wire = self.finish(FilterOperator::IsNotIn, self.wire_set(&values)); PyFilterExpr(Arc::new(self.0.clone().is_not_in(values)), wire) } + /// Checks whether the value is present (not `None`). + /// + /// Returns: + /// filter.FilterExpr: fn is_some(&self) -> PyFilterExpr { let wire = self.finish( FilterOperator::IsSome, @@ -328,6 +374,10 @@ impl PyExpr { ); PyFilterExpr(Arc::new(self.0.clone().is_some()), wire) } + /// Checks whether the value is `None` / missing. + /// + /// Returns: + /// filter.FilterExpr: fn is_none(&self) -> PyFilterExpr { let wire = self.finish( FilterOperator::IsNone, @@ -336,31 +386,67 @@ impl PyExpr { PyFilterExpr(Arc::new(self.0.clone().is_none()), wire) } + /// Requires that **any** element matches when the value is list-like (a temporal history or a list property). + /// + /// Returns: + /// filter.Expr: fn any(&self) -> Self { self.with_op(Arc::new(self.0.clone().any()), Op::Any) } + /// Requires that **all** elements match when the value is list-like (a temporal history or a list property). + /// + /// Returns: + /// filter.Expr: fn all(&self) -> Self { self.with_op(Arc::new(self.0.clone().all()), Op::All) } + /// Sums the elements when the value is numeric and list-like. + /// + /// Returns: + /// filter.Expr: fn sum(&self) -> Self { self.with_op(Arc::new(self.0.clone().sum()), Op::Sum) } + /// Averages the elements when the value is numeric and list-like. + /// + /// Returns: + /// filter.Expr: fn avg(&self) -> Self { self.with_op(Arc::new(self.0.clone().avg()), Op::Avg) } + /// Selects the minimum element when the value is list-like. + /// + /// Returns: + /// filter.Expr: fn min(&self) -> Self { self.with_op(Arc::new(self.0.clone().min()), Op::Min) } + /// Selects the maximum element when the value is list-like. + /// + /// Returns: + /// filter.Expr: fn max(&self) -> Self { self.with_op(Arc::new(self.0.clone().max()), Op::Max) } + /// Selects the first element when the value is list-like. + /// + /// Returns: + /// filter.Expr: fn first(&self) -> Self { self.with_op(Arc::new(self.0.clone().first()), Op::First) } + /// Selects the last element when the value is list-like. + /// + /// Returns: + /// filter.Expr: fn last(&self) -> Self { self.with_op(Arc::new(self.0.clone().last()), Op::Last) } + /// Selects the number of elements when the value is list-like. + /// + /// Returns: + /// filter.Expr: fn len(&self) -> Self { self.with_op(Arc::new(self.0.clone().len()), Op::Len) } @@ -471,13 +557,12 @@ impl NodeViewFilterOps for Arc { } } -/// Constructs node filter expressions. +/// A node filter scoped to a view. /// -/// Each method returns either: -/// - a field-specific filter builder, or -/// - a view-restricted filter context, or -/// - a boolean predicate over node state. -#[pyclass(frozen, name = "Node", module = "raphtory.filter")] +/// Obtained from the view methods on [`Node`] (`Node.window(...)`, +/// `Node.latest()`, ...); its field and property methods evaluate within that +/// view, and its own view methods narrow it further. +#[pyclass(frozen, name = "NodeFilter", module = "raphtory.filter")] pub struct PyNodeFilter(Arc, Vec); impl PyNodeFilter { @@ -516,7 +601,7 @@ impl PyNodeFilter { fn id(&self) -> PyExpr { PyExpr( self.0.dyn_id(), - Some(self.lhs(WireTarget::Field("node_id"))), + Some(self.lhs(WireTarget::Field(NODE_ID_FIELD))), ) } @@ -527,7 +612,7 @@ impl PyNodeFilter { fn name(&self) -> PyExpr { PyExpr( self.0.dyn_name(), - Some(self.lhs(WireTarget::Field("node_name"))), + Some(self.lhs(WireTarget::Field(NODE_NAME_FIELD))), ) } @@ -538,7 +623,7 @@ impl PyNodeFilter { fn node_type(&self) -> PyExpr { PyExpr( self.0.dyn_node_type(), - Some(self.lhs(WireTarget::Field("node_type"))), + Some(self.lhs(WireTarget::Field(NODE_TYPE_FIELD))), ) } @@ -612,7 +697,7 @@ impl PyNodeFilter { /// end (int): End time. /// /// Returns: - /// filter.Node: + /// filter.NodeFilter: fn window(&self, start: EventTime, end: EventTime) -> PyNodeFilter { self.wrap( self.0.clone().window(start, end), @@ -626,7 +711,7 @@ impl PyNodeFilter { /// time (int): Event time. /// /// Returns: - /// filter.Node: + /// filter.NodeFilter: fn at(&self, time: EventTime) -> PyNodeFilter { self.wrap( self.0.clone().at(time), @@ -640,7 +725,7 @@ impl PyNodeFilter { /// time (int): Lower time bound. /// /// Returns: - /// filter.Node: + /// filter.NodeFilter: fn after(&self, time: EventTime) -> PyNodeFilter { self.wrap( self.0.clone().after(time), @@ -657,7 +742,7 @@ impl PyNodeFilter { /// time (int): Upper time bound. /// /// Returns: - /// filter.Node: + /// filter.NodeFilter: fn before(&self, time: EventTime) -> PyNodeFilter { self.wrap( self.0.clone().before(time), @@ -668,7 +753,7 @@ impl PyNodeFilter { /// Evaluates filters against the latest available state of each node. /// /// Returns: - /// filter.Node: + /// filter.NodeFilter: fn latest(&self) -> PyNodeFilter { self.wrap(self.0.clone().latest(), WireView::Latest) } @@ -679,7 +764,7 @@ impl PyNodeFilter { /// time (int): Snapshot time. /// /// Returns: - /// filter.Node: + /// filter.NodeFilter: fn snapshot_at(&self, time: EventTime) -> PyNodeFilter { self.wrap(self.0.clone().snapshot_at(time), WireView::SnapshotAt(time)) } @@ -687,7 +772,7 @@ impl PyNodeFilter { /// Evaluates filters against the most recent snapshot of the graph. /// /// Returns: - /// filter.Node: + /// filter.NodeFilter: fn snapshot_latest(&self) -> PyNodeFilter { self.wrap(self.0.clone().snapshot_latest(), WireView::SnapshotLatest) } @@ -698,7 +783,7 @@ impl PyNodeFilter { /// layer (str): Layer name. /// /// Returns: - /// filter.Node: + /// filter.NodeFilter: fn layer(&self, layer: String) -> PyNodeFilter { self.wrap( self.0.clone().layer(vec![layer.clone()]), @@ -712,9 +797,9 @@ impl PyNodeFilter { /// layers (list[str]): Layer names. /// /// Returns: - /// filter.Node: + /// filter.NodeFilter: fn layers(&self, layers: FromIterable) -> PyNodeFilter { - let layers = layers.to_vec(); + let layers: Vec = layers.into(); self.wrap( self.0.clone().layer(layers.clone()), WireView::Layers(layers), @@ -747,3 +832,223 @@ impl PyNodeFilter { Ok(PyFilterExpr(Arc::new(op), None)) } } + +/// Entry point for constructing node filter expressions. +/// +/// Every method is static: `Node.property("age") > 30` selects nodes +/// directly, and the view methods (`window`, `latest`, `layer`, ...) return a +/// [`NodeFilter`] scoped to that view for further chaining. +#[pyclass(frozen, name = "Node", module = "raphtory.filter")] +pub struct PyNode; + +#[pymethods] +impl PyNode { + /// Selects the node ID field for filtering. + /// + /// Returns: + /// filter.Expr: + #[staticmethod] + fn id() -> PyExpr { + PyNodeFilter::root().id() + } + + /// Selects the node name field for filtering. + /// + /// Returns: + /// filter.Expr: + #[staticmethod] + fn name() -> PyExpr { + PyNodeFilter::root().name() + } + + /// Selects the node type field for filtering. + /// + /// Returns: + /// filter.Expr: + #[staticmethod] + fn node_type() -> PyExpr { + PyNodeFilter::root().node_type() + } + + /// Selects incoming node degree for filtering. + /// + /// Returns: + /// filter.Expr: + #[staticmethod] + fn in_degree() -> PyExpr { + PyNodeFilter::root().in_degree() + } + + /// Selects total node degree for filtering. + /// + /// Returns: + /// filter.Expr: + #[staticmethod] + fn degree() -> PyExpr { + PyNodeFilter::root().degree() + } + + /// Selects outgoing node degree for filtering. + /// + /// Returns: + /// filter.Expr: + #[staticmethod] + fn out_degree() -> PyExpr { + PyNodeFilter::root().out_degree() + } + + /// Filters a node property by name. + /// + /// The property may be static or temporal depending on the query context. + /// + /// Arguments: + /// name (str): Property key. + /// + /// Returns: + /// filter.PropertyExpr: + #[staticmethod] + fn property(name: String) -> PyPropertyExpr { + PyNodeFilter::root().property(name) + } + + /// Filters a node metadata field by name. + /// + /// Metadata is shared across all temporal versions of a node. + /// + /// Arguments: + /// name (str): Metadata key. + /// + /// Returns: + /// filter.Expr: + #[staticmethod] + fn metadata(name: String) -> PyExpr { + PyNodeFilter::root().metadata(name) + } + + /// Restricts node evaluation to the given time window. + /// + /// The window is inclusive of `start` and exclusive of `end`. + /// + /// Arguments: + /// start (int): Start time. + /// end (int): End time. + /// + /// Returns: + /// filter.NodeFilter: + #[staticmethod] + fn window(start: EventTime, end: EventTime) -> PyNodeFilter { + PyNodeFilter::root().window(start, end) + } + + /// Restricts node evaluation to a single point in time. + /// + /// Arguments: + /// time (int): Event time. + /// + /// Returns: + /// filter.NodeFilter: + #[staticmethod] + fn at(time: EventTime) -> PyNodeFilter { + PyNodeFilter::root().at(time) + } + + /// Restricts node evaluation to times strictly after the given time. + /// + /// Arguments: + /// time (int): Lower time bound. + /// + /// Returns: + /// filter.NodeFilter: + #[staticmethod] + fn after(time: EventTime) -> PyNodeFilter { + PyNodeFilter::root().after(time) + } + + /// Restricts node evaluation to times strictly before the given time. + /// + /// Arguments: + /// time (int): Upper time bound. + /// + /// Returns: + /// filter.NodeFilter: + #[staticmethod] + fn before(time: EventTime) -> PyNodeFilter { + PyNodeFilter::root().before(time) + } + + /// Evaluates filters against the latest available state of each node. + /// + /// Returns: + /// filter.NodeFilter: + #[staticmethod] + fn latest() -> PyNodeFilter { + PyNodeFilter::root().latest() + } + + /// Evaluates filters against a snapshot of the graph at a given time. + /// + /// Arguments: + /// time (int): Snapshot time. + /// + /// Returns: + /// filter.NodeFilter: + #[staticmethod] + fn snapshot_at(time: EventTime) -> PyNodeFilter { + PyNodeFilter::root().snapshot_at(time) + } + + /// Evaluates filters against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.NodeFilter: + #[staticmethod] + fn snapshot_latest() -> PyNodeFilter { + PyNodeFilter::root().snapshot_latest() + } + + /// Restricts evaluation to nodes belonging to the given layer. + /// + /// Arguments: + /// layer (str): Layer name. + /// + /// Returns: + /// filter.NodeFilter: + #[staticmethod] + fn layer(layer: String) -> PyNodeFilter { + PyNodeFilter::root().layer(layer) + } + + /// Restricts evaluation to nodes belonging to any of the given layers. + /// + /// Arguments: + /// layers (list[str]): Layer names. + /// + /// Returns: + /// filter.NodeFilter: + #[staticmethod] + fn layers(layers: FromIterable) -> PyNodeFilter { + PyNodeFilter::root().layers(layers) + } + + /// Matches nodes that have at least one event in the current view. + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn is_active() -> PyFilterExpr { + PyNodeFilter::root().is_active() + } + + /// Build a node filter from a boolean column of an existing node-state result. + /// + /// Arguments: + /// state (OutputNodeState): A pre-computed node state (e.g. from an algorithm). + /// col (str): Name of the boolean column on `state` whose values determine inclusion. + /// + /// Returns: + /// filter.FilterExpr: + #[staticmethod] + fn by_state_column(state: &PyOutputNodeState, col: String) -> PyResult { + PyNodeFilter::root().by_state_column(state, col) + } +} diff --git a/raphtory/src/python/filter/wire.rs b/raphtory/src/python/filter/wire.rs index c50062a938..5d64e7f950 100644 --- a/raphtory/src/python/filter/wire.rs +++ b/raphtory/src/python/filter/wire.rs @@ -12,7 +12,7 @@ use crate::db::graph::views::filter::model::{ degree_filter::DegreeFilter, edge_filter::{CompositeEdgeFilter, EdgeFilter, Endpoint}, exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, - filter::{FieldFilterValue, Filter}, + filter::{FieldFilterValue, Filter, NODE_ID_FIELD, NODE_NAME_FIELD, NODE_TYPE_FIELD}, latest_filter::Latest, layered_filter::Layered, node_filter::{CompositeNodeFilter, NodeFilter}, diff --git a/stub_gen/stub_gen.py b/stub_gen/stub_gen.py index 4fbd9dd6cd..9bf9030406 100644 --- a/stub_gen/stub_gen.py +++ b/stub_gen/stub_gen.py @@ -1,5 +1,7 @@ import ast import builtins +import importlib +import re import inspect import logging import textwrap @@ -428,6 +430,31 @@ def not_this_module_import(line: str, full_name: str) -> bool: ) +def shadow_free_import(line: str, module: ModuleType, defined: list[str]) -> str: + """A `from X import *` that carries a name this module binds to a different + object is a redefinition to the type checker, which keeps the imported one + and types every use of the local class as the foreign one. Such an import + is spelled out without the clashing names. A name both modules bind to the + same object (a class re-exported from two places) is not a clash. + """ + star = re.fullmatch(r"from ([\w.]+) import \*", line) + if star is None: + return line + source = importlib.import_module(star[1]) + exported = getattr(source, "__all__", None) + if exported is None: + return line + clashing = { + n + for n in defined + if n in exported and getattr(source, n, None) is not getattr(module, n) + } + if not clashing: + return line + kept = ", ".join(n for n in exported if n not in clashing) + return f"from {star[1]} import {kept}" + + def gen_module( module: ModuleType, name: str, path: Path, log_path, full_name=None ) -> None: @@ -458,7 +485,9 @@ def gen_module( if loader is None or isinstance(loader, ExtensionFileLoader): modules.append((obj, obj_name)) valid_imports = ( - f"{line}" for line in imports if not_this_module_import(line, full_name) + shadow_free_import(line, module, all_names) + for line in imports + if not_this_module_import(line, full_name) ) doc_str = getattr(module, "__doc__", None) if doc_str: From 7b82b119440c8a08a8c52414ce3655d7657d2dde Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:47:09 +0100 Subject: [PATCH 116/155] show a filter's wire form in its python repr FilterExpr prints the FilterTree it carries in the notation the composite filters already use, or says it is local-only when there is none; layer selections print as names rather than debug output. --- python/tests/test_base_install/test_repr.py | 23 +++++++++ .../views/filter/model/layered_filter.rs | 18 ++++++- .../src/db/graph/views/filter/model/mod.rs | 48 ++++++++++++++++++- raphtory/src/python/filter/filter_expr.rs | 10 ++++ 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/python/tests/test_base_install/test_repr.py b/python/tests/test_base_install/test_repr.py index 0cbd495b6e..5cbd71cc27 100644 --- a/python/tests/test_base_install/test_repr.py +++ b/python/tests/test_base_install/test_repr.py @@ -1,4 +1,5 @@ from raphtory import Graph, PersistentGraph +from raphtory import filter from io import StringIO import unittest from unittest import TestCase @@ -62,3 +63,25 @@ def test_persistent_graph(self): if __name__ == "__main__": unittest.main() + + +class FilterExprReprTest(TestCase): + """`repr` shows the wire form a filter carries, so what runs locally and what + would be sent to a server can be read off the object.""" + + def test_repr_shows_the_recorded_wire_form(self): + expr = filter.Node.window(0, 5).property("score") > 4 + self.assertEqual(repr(expr), "FilterExpr(WINDOW[0..5](score > 4))") + + def test_repr_shows_temporal_ops_and_combinators(self): + expr = (filter.Node.property("score").temporal().sum() > 10) & ~( + filter.Node.name() == "carol" + ) + self.assertEqual( + repr(expr), + "FilterExpr((sum(temporal(score)) > 10 AND NOT(node_name == carol)))", + ) + + def test_repr_of_a_local_only_filter_says_so(self): + expr = filter.Node.degree() > filter.Node.in_degree() + self.assertEqual(repr(expr), "FilterExpr()") 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 7fdbf25832..49d4c07c61 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -32,7 +32,23 @@ pub struct Layered { impl Display for Layered { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "LAYER[{:?}]({})", self.layer, self.inner) + write!(f, "LAYER[{}]({})", layer_label(&self.layer), self.inner) + } +} + +/// The layer selection as a reader would write it: the names themselves, +/// `*` for every layer, `none` for no layer. +pub(crate) fn layer_label(layer: &Layer) -> String { + match layer { + Layer::All => "*".to_string(), + Layer::None => "none".to_string(), + Layer::Default => "_default".to_string(), + Layer::One(name) => name.to_string(), + Layer::Multiple(names) => names + .iter() + .map(|n| n.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 795426bc62..a40232b316 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -46,7 +46,7 @@ use crate::{ is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, latest_filter::Latest, - layered_filter::Layered, + layered_filter::{layer_label, Layered}, node_expr::{NodeMetaOp, NodePropOp}, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, @@ -64,7 +64,11 @@ use raphtory_api::core::{ storage::timeindex::{AsTime, EventTime}, utils::time::IntoTime, }; -use std::{ops::Deref, sync::Arc}; +use std::{ + fmt::{self, Display}, + ops::Deref, + sync::Arc, +}; pub mod and_filter; pub mod degree_filter; @@ -343,6 +347,46 @@ pub enum FilterTree { Not(Box), } +impl Display for GraphViewOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GraphViewOp::Window { start, end } => write!(f, "WINDOW[{}..{}]", start.t(), end.t()), + GraphViewOp::Latest => write!(f, "LATEST"), + GraphViewOp::SnapshotAt(time) => write!(f, "SNAPSHOT_AT[{}]", time.t()), + GraphViewOp::SnapshotLatest => write!(f, "SNAPSHOT_LATEST"), + GraphViewOp::Layers(layer) => write!(f, "LAYER[{}]", layer_label(layer)), + } + } +} + +/// The wire form as text, in the same notation the composite filters print +/// themselves in: predicates as `lhs op value`, views as `KIND[args](inner)`, +/// combinators as `(a AND b)`, `(a OR b)` and `NOT(a)`. A view chain lists its +/// ops in application order. +impl Display for FilterTree { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let joined = |items: &[FilterTree], sep: &str| -> String { + items + .iter() + .map(|t| t.to_string()) + .collect::>() + .join(sep) + }; + match self { + FilterTree::Node(inner) => write!(f, "{inner}"), + FilterTree::Edge(inner) => write!(f, "{inner}"), + FilterTree::ExplodedEdge(inner) => write!(f, "{inner}"), + FilterTree::View(ops) => { + let ops = ops.iter().map(|op| op.to_string()).collect::>(); + write!(f, "VIEW({})", ops.join(" . ")) + } + FilterTree::And(items) => write!(f, "({})", joined(items, " AND ")), + FilterTree::Or(items) => write!(f, "({})", joined(items, " OR ")), + FilterTree::Not(inner) => write!(f, "NOT({inner})"), + } + } +} + impl FilterTree { /// Whether any part of this expression tests edges. /// diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index b37212e206..00c1949628 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -67,6 +67,16 @@ impl PyFilterExpr { let wire = self.1.clone().map(|t| FilterTree::Not(Box::new(t))); PyFilterExpr(Arc::new(NotFilter(self.0.clone())), wire) } + + /// Shows the filter as it would be sent to a server. A filter with no + /// server-side form (an expression on both sides of a comparison) says so + /// instead; it still runs locally. + fn __repr__(&self) -> String { + match &self.1 { + Some(tree) => format!("FilterExpr({tree})"), + None => "FilterExpr()".to_string(), + } + } } impl CreateFilter for PyFilterExpr { From 3c14bfbc5caf142ba478cddf471c35009e2df63a Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:51:42 +0100 Subject: [PATCH 117/155] scope endpoint reads by the views applied to the edge Edge.window(0, 5).src().property("p") read the source node's property outside the window: the endpoint was built from an unscoped node filter, so the view chain that reached src()/dst() was dropped locally while the server applied it. The endpoint now carries the same views onto the node it reads from, and a parity test pins both sides agreeing. --- .../parity/test_parity_filters.py | 27 ++++++++++ .../graph/views/filter/model/edge_filter.rs | 5 ++ .../views/filter/model/node_expr/dyn_expr.rs | 11 +++++ raphtory/src/python/filter/edge_expr.rs | 49 +++++++++++++------ 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index 90c5a8d629..50179f13c1 100644 --- a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py +++ b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py @@ -1292,3 +1292,30 @@ def test_by_state_column_needs_a_boolean_state_column(): with pytest.raises(ValueError): f.Node.by_state_column(state, "pagerank_score") + + +def test_edge_views_scope_endpoint_reads_on_both_sides(): + """A view applied before `src()`/`dst()` scopes the endpoint read, locally and + remotely. + + `alice.score` is 3 until t=5 and 9 after; the edge alice→bob has events at + t=1 and t=6. Inside [0, 5) alice's score is 3, so asking for 9 there must + match nothing on either side. The local engine used to read the endpoint + outside the window and keep the edge. + """ + + def build(g): + g.add_node(0, "alice", properties={"score": 3}) + g.add_node(5, "alice", properties={"score": 9}) + g.add_node(0, "bob", properties={"score": 1}) + g.add_edge(1, "alice", "bob") + g.add_edge(6, "alice", "bob") + + late = f.Edge.window(0, 5).src().property("score") == 9 + early = f.Edge.window(0, 5).src().property("score") == 3 + read = lambda g, e: sorted((x.src.name, x.dst.name) for x in g.filter(e).edges) + with graph_pair(build) as pair: + assert read(pair.local, late) == [] + assert read(pair.local, early) == [("alice", "bob")] + assert_parity(pair, lambda g: read(g, late)) + assert_parity(pair, lambda g: read(g, early)) 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 0dfc7bdb22..ec8e69d9d7 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -120,6 +120,11 @@ impl EdgeEndpointWrapper { Self { inner, endpoint } } + #[inline] + pub fn endpoint(&self) -> Endpoint { + self.endpoint + } + #[inline] pub fn map(self, f: impl FnOnce(T) -> U) -> EdgeEndpointWrapper { EdgeEndpointWrapper { diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 5b4ba73774..3bbf5af18d 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -86,6 +86,17 @@ where } } +/// An endpoint read built from an erased node value: switching to the history +/// happens on the node side, and the result is read through the same endpoint. +impl DynTemporal for EdgeEndpointWrapper> { + fn temporal(&self) -> Arc { + Arc::new(EdgeEndpointWrapper::new( + self.inner.temporal(), + self.endpoint(), + )) + } +} + pub trait DynCreateOp: DynEntityExpr { fn dyn_const_cast_type(&self) -> Option; diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index edbe57da4a..f7b6eec246 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -10,14 +10,14 @@ use crate::{ node_filter::NodeFilter, property_filter::PropertyRef, windowed_filter::Windowed, - CombinedFilter, CreateView, DynCreateFilter, DynCreateView, EdgeFilterFactory, - EdgeViewFilterOps, EntityMarker, FilterTree, InternalViewWrapOps, PropertyExprFactory, - ViewWrapOps, Wrap, + CombinedFilter, CreateView, DynCreateFilter, DynCreateView, DynPropertyExprFactory, + EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, FilterTree, InternalViewWrapOps, + PropertyExprFactory, ViewWrapOps, }, python::{ filter::{ filter_expr::PyFilterExpr, - node_expr::{PyExpr, PyPropertyExpr}, + node_expr::{DynNodeFilterFactory, PyExpr, PyPropertyExpr}, wire::{wrap_edge_views, WireEntity, WireLhs, WireTarget, WireView}, }, types::iterable::FromIterable, @@ -39,11 +39,28 @@ use std::sync::Arc; /// Edge.src().property("country") == "UK" #[pyclass(frozen, name = "EdgeEndpoint", module = "raphtory.filter")] pub struct PyEdgeEndpoint( - pub EdgeEndpointWrapper, + pub(crate) Arc, pub(crate) Endpoint, pub(crate) Vec, ); +/// The node an endpoint read evaluates on, scoped by the same views as the +/// edge chain that reached it: `Edge.window(0, 5).src().property("p")` reads +/// the source node's property inside the window. +fn node_scope(views: &[WireView]) -> Arc { + let mut node: Arc = Arc::new(NodeFilter); + for view in views { + node = match view { + WireView::Window(start, end) => node.window(*start, *end), + WireView::Latest => Arc::new(node.latest()), + WireView::SnapshotAt(time) => Arc::new(node.snapshot_at(*time)), + WireView::SnapshotLatest => Arc::new(node.snapshot_latest()), + WireView::Layers(names) => Arc::new(node.layer(names.clone())), + }; + } + node +} + impl PyEdgeEndpoint { fn lhs(&self, target: WireTarget) -> WireLhs { WireLhs { @@ -64,7 +81,7 @@ impl PyEdgeEndpoint { /// filter.Expr: fn id(&self) -> PyExpr { PyExpr::new( - Arc::new(self.0.id()), + Arc::new(EdgeEndpointWrapper::new(self.0.dyn_id(), self.1)), Some(self.lhs(WireTarget::Field(NODE_ID_FIELD))), ) } @@ -75,7 +92,7 @@ impl PyEdgeEndpoint { /// filter.Expr: fn name(&self) -> PyExpr { PyExpr::new( - Arc::new(self.0.name()), + Arc::new(EdgeEndpointWrapper::new(self.0.dyn_name(), self.1)), Some(self.lhs(WireTarget::Field(NODE_NAME_FIELD))), ) } @@ -86,7 +103,7 @@ impl PyEdgeEndpoint { /// filter.Expr: fn node_type(&self) -> PyExpr { PyExpr::new( - Arc::new(self.0.node_type()), + Arc::new(EdgeEndpointWrapper::new(self.0.dyn_node_type(), self.1)), Some(self.lhs(WireTarget::Field(NODE_TYPE_FIELD))), ) } @@ -100,7 +117,10 @@ impl PyEdgeEndpoint { /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); - PyPropertyExpr::new(Arc::new(self.0.property(name)), Some(lhs)) + PyPropertyExpr::new( + Arc::new(EdgeEndpointWrapper::new(self.0.dyn_property(name), self.1)), + Some(lhs), + ) } /// Filters an endpoint node metadata field by name. @@ -113,10 +133,7 @@ impl PyEdgeEndpoint { fn metadata(&self, name: String) -> PyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); PyExpr::new( - Arc::new( - self.0 - .wrap(PropertyExprFactory::metadata(&NodeFilter, name)), - ), + Arc::new(EdgeEndpointWrapper::new(self.0.dyn_metadata(name), self.1)), Some(lhs), ) } @@ -280,7 +297,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeEndpoint: fn src(&self) -> PyEdgeEndpoint { - PyEdgeEndpoint(EdgeFilter::src(), Endpoint::Src, self.1.clone()) + PyEdgeEndpoint(node_scope(&self.1), Endpoint::Src, self.1.clone()) } /// Selects the edge **destination endpoint** for filtering. @@ -288,7 +305,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeEndpoint: fn dst(&self) -> PyEdgeEndpoint { - PyEdgeEndpoint(EdgeFilter::dst(), Endpoint::Dst, self.1.clone()) + PyEdgeEndpoint(node_scope(&self.1), Endpoint::Dst, self.1.clone()) } /// Filters an edge property by name. @@ -300,7 +317,7 @@ impl PyEdgeFilter { /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); - PyPropertyExpr::new(self.0.dyn_property(name), Some(lhs)) + PyPropertyExpr::new(self.0.as_ref().dyn_property(name), Some(lhs)) } /// Filters an edge metadata field by name. From 292597749e85a1ac92b8b466715b7e73fd266c3d Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:02:03 +0100 Subject: [PATCH 118/155] compare numbers by value, as the property filters always did A compiled comparison cast its constant to the property's type and, when the cast did not fit, fell back to an ordering that only knows same-type pairs, so a u64 above i64::MAX against an i64 property matched nothing. Prop::compare and Prop::equals are what the property filters used: exact through i128 and Decimal, f64 when a float is involved, structural for everything else. --- .../views/filter/model/filter_operator.rs | 30 +++++-------- .../views/filter/model/node_expr/tests.rs | 44 +++++++++++++++++++ 2 files changed, 55 insertions(+), 19 deletions(-) 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 6f021d80f6..c71c6405b5 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -5,7 +5,7 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, GidRef, GID}, storage::arc_str::ArcStr, }; -use std::{collections::HashSet, fmt, fmt::Display, ops::Deref}; +use std::{cmp::Ordering, collections::HashSet, fmt, fmt::Display, ops::Deref}; use strsim::levenshtein; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -385,27 +385,19 @@ impl Comparable for usize { } impl Comparable for Prop { + /// Numbers compare by value whatever their width or sign — `i64(3)` is + /// below `u64::MAX`, `1i64` equals `1.0f64` — exactly, through `i128` and + /// `Decimal`, the way the property filters always have. Every other type + /// compares structurally. fn binary_cmp(op: &BinaryOp, left: &Prop, right: &Prop) -> bool { use std::cmp::Ordering::*; - - // Try casting right to left's type for cross-type numeric comparisons - // (e.g. Prop::I32(1) vs Prop::U64(1), or Prop::F64(3.0) vs Prop::U64(3)). - let right_casted = right.clone().try_cast(left.dtype()); - let right = right_casted.as_ref().unwrap_or(right); - 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), + BinaryOp::Eq => left.equals(right), + BinaryOp::Ne => !left.equals(right), + BinaryOp::Lt => left.compare(right) == Some(Less), + BinaryOp::Le => matches!(left.compare(right), Some(Less | Equal)), + BinaryOp::Gt => left.compare(right) == Some(Greater), + BinaryOp::Ge => matches!(left.compare(right), Some(Greater | Equal)), } } } 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 c71f78d49c..4af1cbb7a8 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 @@ -702,3 +702,47 @@ fn sum_over_a_nested_list_widens_only_the_innermost_level() { let filter = NodeFilter.property("xs").sum().eq(510u64).any(); assert_eq!(filtered_names(filter, g), vec!["n"]); } + +// ── Numbers compare by value across widths and signedness ────────────────── + +/// A constant that does not fit the property's own type still compares by +/// value: `u64::MAX` is above every `i64`, and a negative `i64` is below every +/// `u64`. Declaring them incomparable made `risk <= 2^63+5` match nothing. +#[test] +fn numeric_constants_beyond_the_property_width_compare_by_value() { + let g = Graph::new(); + g.add_node(0, "low", [("risk", 3i64.into_prop())], None, None) + .unwrap(); + g.add_node(0, "neg", [("risk", (-4i64).into_prop())], None, None) + .unwrap(); + let all = vec!["low".to_string(), "neg".to_string()]; + assert_eq!( + filtered_names( + NodeFilter.property("risk").le(Prop::U64(u64::MAX)), + g.clone() + ), + all + ); + assert_eq!( + filtered_names( + NodeFilter.property("risk").le(Prop::U64(1u64 << 63 | 5)), + g.clone() + ), + all + ); + assert_eq!( + filtered_names( + NodeFilter.property("risk").gt(Prop::U64(1u64 << 63)), + g.clone() + ), + Vec::::new() + ); + assert_eq!( + filtered_names(NodeFilter.property("risk").lt(Prop::U64(0)), g.clone()), + vec!["neg"] + ); + assert_eq!( + filtered_names(NodeFilter.property("risk").le(Prop::U64(3)), g), + all + ); +} From b2dae04404d66966098bb0c6c46bd9e051829848 Mon Sep 17 00:00:00 2001 From: Shivam <4599890+shivamka1@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:17:17 +0100 Subject: [PATCH 119/155] The filter tree is the only filter grammar (#2785) The expression tree is the single form of a filter: Rust compiles it, Python holds it, GraphQL accepts it as FilterExpr, the remote client sends it and permission grants store it. The per-entity GraphQL grammar and its lowering are removed; view collections take one filter entry; a view leg of an and applies first and composes only with and; numeric comparisons compare by value; endpoint reads keep the edge's views; the filtering user guide is rewritten. --- docs/user-guide/views/6_filtering.md | 202 +- graphql-bench/src/bench.ts | 9 +- py | 1 - python/python/raphtory/filter/__init__.pyi | 13 + python/python/raphtory/graphql/__init__.pyi | 31 +- .../test_edges_collection_filter.py | 111 +- .../test_filters/test_node_filter.py | 4 +- .../test_nodes_collection_filter.py | 58 +- .../parity/test_parity_filters.py | 63 +- .../test_graphql/test_apply_views.py | 23 +- .../test_graphql/test_component_filter.py | 61 +- .../test_graphql/test_filter_expr_grammar.py | 183 ++ .../test_filters/test_edge_filter_gql.py | 46 +- .../test_graph_edges_property_filter.py | 486 +-- .../test_graph_nodes_property_filter.py | 246 +- .../test_filters/test_neighbours_filter.py | 148 +- .../test_filters/test_node_filter_gql.py | 251 +- .../test_nodes_property_filter.py | 698 +++-- .../test_graphql/test_gql_history.py | 96 +- .../test_graphql/test_nodes.py | 5 +- python/tests/test_base_install/test_repr.py | 12 +- raphtory-graphql/schema.graphql | 1155 ++----- .../src/client/graphql_transport.rs | 121 +- raphtory-graphql/src/client/op.rs | 10 +- .../src/client/remote/remote_edge.rs | 2 +- .../src/client/remote/remote_edges.rs | 2 +- .../src/client/remote/remote_graph.rs | 2 +- .../src/client/remote/remote_nested_edges.rs | 2 +- .../src/client/remote/remote_node.rs | 2 +- .../src/client/remote/remote_nodes.rs | 2 +- .../client/remote/remote_path_from_graph.rs | 2 +- .../client/remote/remote_path_from_node.rs | 2 +- raphtory-graphql/src/data.rs | 71 +- raphtory-graphql/src/lib.rs | 38 +- .../src/model/algorithms/resolvers.rs | 10 +- .../tests/components/in_component.rs | 1 - .../tests/components/out_component.rs | 155 +- raphtory-graphql/src/model/graph/edge.rs | 7 +- raphtory-graphql/src/model/graph/edges.rs | 7 +- .../src/model/graph/expr_lowering.rs | 483 --- .../src/model/graph/filter_expr_input.rs | 826 +++++ raphtory-graphql/src/model/graph/filtering.rs | 2688 ++--------------- raphtory-graphql/src/model/graph/graph.rs | 25 +- raphtory-graphql/src/model/graph/mod.rs | 2 +- .../src/model/graph/namespace_filtering.rs | 2 +- .../src/model/graph/nested_edges.rs | 7 +- raphtory-graphql/src/model/graph/node.rs | 37 +- raphtory-graphql/src/model/graph/nodes.rs | 43 +- .../src/model/graph/path_from_graph.rs | 18 +- .../src/model/graph/path_from_node.rs | 18 +- raphtory-graphql/src/python/client/mod.rs | 16 +- .../src/python/client/remote_edge.rs | 9 +- .../src/python/client/remote_edges.rs | 17 +- .../src/python/client/remote_graph.rs | 10 +- .../src/python/client/remote_nested_edges.rs | 17 +- .../src/python/client/remote_node.rs | 10 +- .../src/python/client/remote_nodes.rs | 13 +- .../python/client/remote_path_from_graph.rs | 13 +- .../python/client/remote_path_from_node.rs | 13 +- raphtory-tests/tests/qp3.rs | 30 - raphtory/src/db/api/state/ops/filter.rs | 2 +- .../graph/views/filter/model/degree_filter.rs | 37 - .../graph/views/filter/model/dyn_factory.rs | 227 ++ .../graph/views/filter/model/edge_filter.rs | 48 +- .../filter/model/exploded_edge_filter.rs | 61 +- .../src/db/graph/views/filter/model/filter.rs | 7 - .../views/filter/model/filter_operator.rs | 2 +- .../src/db/graph/views/filter/model/mod.rs | 140 +- .../views/filter/model/node_expr/dyn_expr.rs | 28 +- .../views/filter/model/node_expr/exprs.rs | 4 +- .../views/filter/model/node_filter/mod.rs | 39 - .../src/db/graph/views/filter/model/tree.rs | 1257 ++++++++ raphtory/src/python/filter/edge_expr.rs | 295 +- .../src/python/filter/exploded_edge_expr.rs | 110 +- raphtory/src/python/filter/filter_expr.rs | 90 +- raphtory/src/python/filter/graph_filter.rs | 74 +- raphtory/src/python/filter/mod.rs | 1 - raphtory/src/python/filter/node_expr.rs | 617 ++-- raphtory/src/python/filter/wire.rs | 210 -- 79 files changed, 5229 insertions(+), 6655 deletions(-) delete mode 100644 py create mode 100644 python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py delete mode 100644 raphtory-graphql/src/model/graph/expr_lowering.rs create mode 100644 raphtory-graphql/src/model/graph/filter_expr_input.rs delete mode 100644 raphtory-tests/tests/qp3.rs delete mode 100644 raphtory/src/db/graph/views/filter/model/degree_filter.rs create mode 100644 raphtory/src/db/graph/views/filter/model/dyn_factory.rs create mode 100644 raphtory/src/db/graph/views/filter/model/tree.rs delete mode 100644 raphtory/src/python/filter/wire.rs diff --git a/docs/user-guide/views/6_filtering.md b/docs/user-guide/views/6_filtering.md index d09b8f4118..135542a582 100644 --- a/docs/user-guide/views/6_filtering.md +++ b/docs/user-guide/views/6_filtering.md @@ -1,42 +1,203 @@ # Filtering -The `filter` module provides a variety of functions prefixed with 'filter' that take a [filter expression][raphtory.filter.FilterExpr] and return a corresponding view. +A filter picks out part of a graph: the nodes with a high score, the edges that start at a given +node, the updates inside a time window. You describe the part you want as a +[filter expression][raphtory.filter.FilterExpr] and hand it to `filter()` on a graph, a node +collection or a node. The result is a view, so nothing is copied. -The following functions can be called on a `graph` or `node`: +A filter expression is a small tree: *read something* (a name, a degree, a property), *compare it* +to a value or to another read, and *combine* comparisons with `&`, `|` and `~`. The same tree runs +locally and is what a remote graph sends to a server, and `repr()` prints it, so what you see is +what runs. -- [filter_edges][raphtory.GraphView.filter_edges] -- [filter_exploded_edges][raphtory.GraphView.filter_exploded_edges] -- [filter_nodes][raphtory.GraphView.filter_nodes] +The examples below use this graph: -Filter expressions allow you to create complex logical queries to select a narrower set of your data based on multiple criteria. This is useful when you already have some knowledge of the subset you want to isolate. +/// tab | :fontawesome-brands-python: Python + +```python +from raphtory import Graph, filter + +g = Graph() +g.add_node(0, "alice", properties={"score": 3.0}) +g.add_node(2, "alice", properties={"score": 7.0}) +g.add_node(1, "bob", properties={"score": 5.0}) +g.add_node(0, "carol") +g.add_edge(1, "alice", "bob", layer="knows") +g.add_edge(2, "bob", "carol", layer="works") +``` +/// + +## Where a filter starts + +Every expression starts from one of four entry points. The entry point says what kind of thing is +being tested, and the rest of the expression is checked against it as you build it. + +| start with | tests | example | +|---|---|---| +| [filter.Node][raphtory.filter.Node] | one node at a time | `filter.Node.property("score") > 4` | +| [filter.Edge][raphtory.filter.Edge] | one edge at a time | `filter.Edge.src().name() == "alice"` | +| [filter.ExplodedEdge][raphtory.filter.ExplodedEdge] | one edge update at a time | `filter.ExplodedEdge.property("weight") > 1` | +| [filter.Graph][raphtory.filter.Graph] | nothing; it is a view (window, layer, snapshot) | `filter.Graph.window(0, 2)` | + +## What you can read + +From a node, or from the end of an edge (`filter.Edge.src()` and `filter.Edge.dst()`): + +| read | gives | +|---|---| +| `.name()`, `.id()`, `.node_type()` | the built-in fields | +| `.degree()`, `.in_degree()`, `.out_degree()` | how many neighbours the node has (nodes only, not edge ends) | +| `.property("score")` | the latest value of a temporal property | +| `.metadata("owner")` | a metadata (constant) value | + +Edges and exploded edges read `.property(...)` and `.metadata(...)` too, and have yes/no tests of +their own: `.is_valid()`, `.is_deleted()`, `.is_active()`, `.is_self_loop()`. + +## How you compare + +A read is an [Expr][raphtory.filter.Expr]. Comparing it gives a `FilterExpr`. + +| compare with | meaning | +|---|---| +| `==`, `!=`, `<`, `<=`, `>`, `>=` | the usual comparisons; the value must match the property's type family (a number for a number, a string for a string) | +| `.is_in([...])`, `.is_not_in([...])` | membership in a list of values | +| `.starts_with(s)`, `.ends_with(s)`, `.contains(s)`, `.not_contains(s)` | string tests | +| `.fuzzy_search(s, levenshtein_distance, prefix_match)` | approximate string match | +| `.is_some()`, `.is_none()` | whether the property has a value at all | + +The right-hand side can be another read. `filter.Node.degree() > filter.Node.in_degree()` selects +nodes with a neighbour that does not point back at them. + +/// tab | :fontawesome-brands-python: Python + +```{.python continuation} +high = filter.Node.property("score") > 4 +assert sorted(n.name for n in g.filter(high).nodes) == ["alice", "bob"] + +missing = filter.Node.property("score").is_none() +assert [n.name for n in g.filter(missing).nodes] == ["carol"] + +more_out_than_in = filter.Node.degree() > filter.Node.in_degree() +assert sorted(n.name for n in g.filter(more_out_than_in).nodes) == ["alice", "bob"] +``` +/// + +## Combining filters + +Use the bitwise operators: `&` for *and*, `|` for *or*, `~` for *not*. Python's `and`, `or` and +`not` do not work on filter expressions. + +`~f` selects everything `f` did not select. A node without the property is not selected by +`property("score") > 4`, so it *is* selected by `~(property("score") > 4)`. + +/// tab | :fontawesome-brands-python: Python + +```{.python continuation} +assert [n.name for n in g.filter(~high).nodes] == ["carol"] + +not_bob = high & ~(filter.Node.name() == "bob") +assert [n.name for n in g.filter(not_bob).nodes] == ["alice"] + +either = (filter.Node.name() == "carol") | (filter.Node.property("score") > 6) +assert sorted(n.name for n in g.filter(either).nodes) == ["alice", "carol"] +``` +/// + +## Reading through a view + +A view can sit in front of a read. `filter.Node.window(0, 2).property("score")` reads the score +*as it was inside the window*, so alice's latest score there is 3, not 7. The same works for +`.layer(...)`, `.layers(...)`, `.latest()`, `.at(t)`, `.before(t)`, `.after(t)`, `.snapshot_at(t)` +and `.snapshot_latest()`, on nodes, edges and exploded edges, and they can be chained. + +/// tab | :fontawesome-brands-python: Python -To construct a filter expression you can call [filter.Node()][raphtory.filter.Node], [filter.Edge()][raphtory.filter.Edge], [filter.Property()][raphtory.filter.Property], [filter.Metadata()][raphtory.filter.Metadata], or one of their methods. This creates a corresponding builder that requires a comparison to a specified value or values to produce a valid filter expression. +```{.python continuation} +early_high = filter.Node.window(0, 2).property("score") > 4 +assert [n.name for n in g.filter(early_high).nodes] == ["bob"] +``` +/// -The available operators for comparisons depend on if you are looking at a [node][raphtory.filter.NodeFilterBuilder], [edge][raphtory.filter.EdgeFilterOp], or [property][raphtory.filter.PropertyFilterOps] and metadata have the same operators available as properties. Additionally, you can use any of the normal Python logical operators to combine filter expressions. +## Using a property's history -For example: +`.temporal()` switches a property read from its latest value to its whole history. An aggregate +then turns the history back into one value: `.sum()`, `.avg()`, `.min()`, `.max()`, `.first()`, +`.last()`, `.len()`. `.any()` and `.all()` ask whether the comparison holds for any, or every, +value in the history. /// tab | :fontawesome-brands-python: Python -```{.python notest} +```{.python continuation} +total = filter.Node.property("score").temporal().sum() > 8 +assert [n.name for n in g.filter(total).nodes] == ["alice"] + +ever_low = filter.Node.property("score").temporal().any() < 4 +assert [n.name for n in g.filter(ever_low).nodes] == ["alice"] +``` +/// -# Compare a numerical value -filter1 = filter.Property("uptime_days") <= 60 -# Check equality of stings -filter2 = filter.Property("OS_version") == "Ubuntu 20.04" -# Create a single filter from a logical combination using Python bitwise or -filter3 = (filter.Property("primary_function").is_not_in(["Database", "Backup"])) | (filter.Property("uptime_days") < 45) +## Filtering edges +An edge filter can look at the edge itself or at either end of it. + +/// tab | :fontawesome-brands-python: Python + +```{.python continuation} +from_alice = filter.Edge.src().name() == "alice" +assert [(e.src.name, e.dst.name) for e in g.filter(from_alice).edges] == [("alice", "bob")] + +in_works = filter.Edge.layer("works").is_active() +assert [(e.src.name, e.dst.name) for e in g.filter(in_works).edges] == [("bob", "carol")] ``` +/// -!!! Note - As the above example shows, you must use the bitwise operators `&` and `|` instead of `and` and `or` when combining filter expressions. For more information on logical operators consult the [Python Mapping of Operators to Functions](https://docs.python.org/3/library/operator.html#mapping-operators-to-functions). +## Applying a filter + +| call | what you get back | +|---|---| +| `graph.filter(expr)` | a graph view with only the matching nodes, or only the matching edges. A node filter keeps the edges between the remaining nodes; an edge filter keeps every node. | +| `graph.filter(filter.Graph.window(0, 2))` | the graph seen through the view; the same as `graph.window(0, 2)` | +| `graph.filter(filter.Graph.window(0, 2) & expr)` | the view first, then `expr` inside it: the same as `graph.window(0, 2).filter(expr)`. A view can be combined with `&` but not with `\|` or `~` | +| `graph.nodes.filter(expr)` | every node stays, but each node's edges and neighbours are narrowed to the ones that match | +| `node.filter(expr)` | the node with its edges and neighbours narrowed the same way | + +/// tab | :fontawesome-brands-python: Python + +```{.python continuation} +assert g.filter(filter.Graph.window(0, 2)).count_edges() == 1 + +narrowed = g.nodes.filter(filter.Node.name() != "carol") +assert [n.name for n in narrowed] == ["alice", "bob", "carol"] +assert [n.degree() for n in narrowed] == [1, 1, 1] +``` +/// + +## Seeing what a filter will do + +`repr()` prints the tree. It is the same tree a remote graph sends, so there is no separate +server-side form to check. + +/// tab | :fontawesome-brands-python: Python + +```{.python continuation} +print(repr(filter.Node.window(0, 2).property("score") > 4)) +``` +/// + +!!! output + + ``` + FilterExpr(WINDOW[0..2](score) > 4) + ``` ## Cybersecurity scenario -Consider a cybersecurity team investigating the impact of a CVE on your companies servers. They might use Raphtory to filter for nodes which function as public facing servers and that have a specific operating system. This would give the security team a view that contains only nodes that might be vulnerable. +Consider a cybersecurity team investigating the impact of a CVE on your company's servers. They +might use Raphtory to filter for nodes that are public-facing servers running a specific operating +system. This gives the security team a view that contains only the nodes that might be vulnerable. -Using the traffic dataset you can explore this scenario by using `filter_nodes()` to create a new `GraphView` that contains only the nodes that match the CVE description: +Using the traffic dataset you can explore this scenario with `filter()`, which creates a new +`GraphView` containing only the nodes that match the CVE description: /// tab | :fontawesome-brands-python: Python @@ -78,6 +239,7 @@ cve_view = traffic_graph.filter(my_filter) print(cve_view.nodes) ``` +/// You can print the nodes in the filtered view to see which machines you should investigate. diff --git a/graphql-bench/src/bench.ts b/graphql-bench/src/bench.ts index b5f2792b85..0793e67734 100644 --- a/graphql-bench/src/bench.ts +++ b/graphql-bench/src/bench.ts @@ -354,7 +354,14 @@ export function heavyNameScan() { graph: { __args: { path: "big" }, nodes: { - __args: { select: { name: { where: { contains: { str: "99999" } } } } }, + __args: { + select: { + contains: { + lhs: { read: { entity: "NODE", target: { field: "NAME" } } }, + rhs: { const: { str: "99999" } }, + }, + }, + }, count: true, }, }, diff --git a/py b/py deleted file mode 100644 index 55fe211008..0000000000 --- a/py +++ /dev/null @@ -1 +0,0 @@ ----- InvalidFilter - exception diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 3d2fdf3a99..d889a5a2d6 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -31,6 +31,10 @@ from raphtory.iterables import * __all__ = ['FilterExpr', 'Expr', 'PropertyExpr', 'Node', 'NodeFilter', 'Edge', 'EdgeFilter', 'EdgeEndpoint', 'ExplodedEdge', 'ExplodedEdgeFilter', 'Graph', 'GraphFilter'] class FilterExpr(object): + """ + A filter as a tree. The same tree runs locally, is sent to a server, and is + what `repr` prints, so there is nothing to keep in step. + """ def __and__(self, value): """Return self&value.""" @@ -44,10 +48,18 @@ class FilterExpr(object): def __rand__(self, value): """Return value&self.""" + def __repr__(self): + """Return repr(self).""" + def __ror__(self, value): """Return value|self.""" class Expr(object): + """ + A value expression: a field, degree, property, metadata entry or an + aggregate over one. Comparing it to a value or to another expression gives + a [`FilterExpr`]. + """ def __eq__(self, value): """Return self==value.""" @@ -235,6 +247,7 @@ class Expr(object): """ class PropertyExpr(Expr): + """A property read, which can switch to the property's history with `temporal()`.""" def temporal(self) -> filter.Expr: """ diff --git a/python/python/raphtory/graphql/__init__.pyi b/python/python/raphtory/graphql/__init__.pyi index 188bd74587..5f1031a8cd 100644 --- a/python/python/raphtory/graphql/__init__.pyi +++ b/python/python/raphtory/graphql/__init__.pyi @@ -657,8 +657,8 @@ class RemoteGraph(object): RemoteGraph: a new filtered graph view. Raises: - ValueError: if the filter cannot be represented as a GraphQL - `NodeFilter` or `EdgeFilter`. + ValueError: if the filter has no server-side form because it reads + in-process state (`by_state_column`). """ def get_all_node_types(self) -> list[str]: @@ -1238,7 +1238,8 @@ class RemoteEdge(object): RemoteEdge: a new filtered edge view. Raises: - ValueError: if the filter cannot be represented remotely. + ValueError: if the filter has no server-side form because it reads + in-process state (`by_state_column`). """ def has_layer(self, name: str) -> bool: @@ -1689,8 +1690,8 @@ class RemoteNode(object): RemoteNode: a new filtered node view. Raises: - ValueError: if the filter cannot be represented as a GraphQL - `NodeFilter` (e.g. references edge fields). + ValueError: if the filter has no server-side form because it reads + in-process state (`by_state_column`). """ def has_layer(self, name: str) -> bool: @@ -2189,8 +2190,8 @@ class RemoteNodes(object): RemoteNodes: a new collection with the filter applied. Raises: - ValueError: if the filter cannot be represented as a GraphQL - `NodeFilter` (e.g. references edge fields). + ValueError: if the filter has no server-side form because it reads + in-process state (`by_state_column`). """ def has_layer(self, name: str) -> bool: @@ -2658,8 +2659,8 @@ class RemotePathFromNode(object): RemotePathFromNode: a new collection with the filter applied. Raises: - ValueError: if the filter cannot be represented as a GraphQL - `NodeFilter`. + ValueError: if the filter has no server-side form because it reads + in-process state (`by_state_column`). """ def has_layer(self, name: str) -> bool: @@ -3115,8 +3116,8 @@ class RemotePathFromGraph(object): RemotePathFromGraph: a new collection with the filter applied. Raises: - ValueError: if the filter cannot be represented as a GraphQL - `NodeFilter`. + ValueError: if the filter has no server-side form because it reads + in-process state (`by_state_column`). """ def has_layer(self, name: str) -> bool: @@ -3570,8 +3571,8 @@ class RemoteEdges(object): RemoteEdges: a new collection with the filter applied. Raises: - ValueError: if the filter cannot be represented as a GraphQL - `EdgeFilter` (e.g. references node-only fields). + ValueError: if the filter has no server-side form because it reads + in-process state (`by_state_column`). """ def has_layer(self, name: str) -> bool: @@ -4029,8 +4030,8 @@ class RemoteNestedEdges(object): RemoteNestedEdges: a new collection with the filter applied. Raises: - ValueError: if the filter cannot be represented as a GraphQL - `EdgeFilter`. + ValueError: if the filter has no server-side form because it reads + in-process state (`by_state_column`). """ def has_layer(self, name: str) -> bool: diff --git a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py index eecc3dd66e..9cd28cc84c 100644 --- a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py +++ b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py @@ -9,6 +9,8 @@ from itertools import combinations +import pytest + from raphtory import filter from utils import with_variants @@ -61,25 +63,36 @@ def _kind(name): def _and_is_broken(a, b): - # A time view combined with anything via `and` is silently ignored. - return a in TIME_VIEWS or b in TIME_VIEWS + # `view & X` is not set algebra: the view applies first and `X` runs inside it + # (`test_a_view_applies_first_under_and`), so it has no set-derived expectation here. + return a in VIEWS or b in VIEWS + + +def _or_is_refused(a, b): + # A view under `|` has no meaning the engine can give it, so it is refused when written + # (`test_a_view_under_or_or_not_is_refused`). + return a in VIEWS or b in VIEWS def _or_is_broken(a, b): - # An `or` involving any graph view, or mixing edge- and node-kind operands, returns every edge. - return a in VIEWS or b in VIEWS or _kind(a) != _kind(b) + # An `or` mixing edge- and node-kind operands returns every edge. + return _or_is_refused(a, b) or _kind(a) != _kind(b) + + +def _not_is_refused(a): + return a in VIEWS def _not_is_broken(a): - # `~view` returns every edge; `~node-filter` distributes the negation into the endpoints - # instead of complementing the matching edge set. - return a in VIEWS or a in NODE_KIND + # `~node-filter` distributes the negation into the endpoints instead of complementing the + # matching edge set. + return _not_is_refused(a) or a in NODE_KIND def _not_composite_is_broken(a, b): - # `~(A & B)` and `~(A | B)`: negating a *composite* reaches the same wrappers - # through the negation, so `~(A & view)` degenerates to `~A` and loses the - # view entirely. Only a composite of two edge predicates survives. + # `~(A & B)` and `~(A | B)`: a composite with a view is refused under `~`; negating a + # composite with a node-kind operand reaches the endpoint wrappers. Only a composite of + # two edge predicates survives. return _kind(a) != "edge" or _kind(b) != "edge" @@ -362,6 +375,61 @@ def check(graph): return check +@with_variants(_init) +def test_a_view_applies_first_under_and(): + """`view & X` means `graph.().filter(X)`: the view is applied first and `X` is + evaluated inside it, on both the subscript and the `filter()` path. Two views chain. + """ + + def check(graph): + atoms, views = _atoms(), _view_references(graph) + mismatches = [] + for v, viewed in views.items(): + for name, atom in atoms.items(): + if name in VIEWS: + continue + want = _ids(viewed.edges[atom]) + for path, got in ( + ("edges[]", _ids(graph.edges[atoms[v] & atom])), + ("filter()", _ids(graph.filter(atoms[v] & atom).edges)), + ): + if got != want: + mismatches.append( + f"[{path}] {v} & {name}: got {sorted(got)} want {sorted(want)}" + ) + # Two views: the second applies inside the first. + want = _ids(graph.window(3, 12).layers(["work"]).edges) + got = _ids(graph.filter(atoms["window"] & atoms["layer"]).edges) + if got != want: + mismatches.append( + f"[filter()] window & layer: got {sorted(got)} want {sorted(want)}" + ) + assert not mismatches, "\n".join(mismatches) + + return check + + +@with_variants(_init) +def test_a_view_under_or_or_not_is_refused(): + """A view applies to the whole filter, so it composes with `&` only. Under `|` or `~` the + engine has no meaning to give it, and the expression is refused where it is written. + """ + + def check(graph): + atoms = _atoms() + for label, build in { + "edge_prop | layer": lambda: atoms["edge_prop"] | atoms["layer"], + "~layer": lambda: ~atoms["layer"], + "~(edge_prop & layer)": lambda: ~(atoms["edge_prop"] & atoms["layer"]), + "(edge_prop & layer) | src": lambda: (atoms["edge_prop"] & atoms["layer"]) + | atoms["src"], + }.items(): + with pytest.raises(TypeError, match="view"): + build() + + return check + + @with_variants(_init) def test_broken_combination_classes_are_still_broken(): """One discriminating representative per known-broken class. When a class is fixed this fails: @@ -377,37 +445,14 @@ def check(graph): ["window", "layer", "edge_prop", "node_prop", "node_name"], ) representatives = { - "and drops a time view": ( - atoms["window"] & atoms["edge_prop"], - single["window"] & single["edge_prop"], - ), - "or with a view returns every edge": ( - atoms["edge_prop"] | atoms["layer"], - single["edge_prop"] | single["layer"], - ), "or of mixed kinds returns every edge": ( atoms["edge_prop"] | atoms["node_prop"], single["edge_prop"] | single["node_prop"], ), - "not of a view returns every edge": ( - ~atoms["layer"], - every - single["layer"], - ), "not of a node filter is not the complement": ( ~atoms["node_name"], every - single["node_name"], ), - # Negating a composite that contains a view: the pairwise rules - # above only ever negate a single atom, so these shapes need their - # own representatives. - "not of an and containing a view loses the view": ( - ~(atoms["edge_prop"] & atoms["layer"]), - every - (single["edge_prop"] & single["layer"]), - ), - "not of an or containing a view returns every edge": ( - ~(atoms["edge_prop"] | atoms["layer"]), - every - (single["edge_prop"] | single["layer"]), - ), } fixed = [] for label, (expr, want) in representatives.items(): diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index de660965c5..c9212a6039 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -393,7 +393,9 @@ def test_degree_filter_with_invalid_expressions(): ] for make_filter in invalid_filters: - with pytest.raises(Exception, match=r"Invalid filter|not comparable"): + with pytest.raises( + Exception, match=r"Invalid filter|not comparable|always has a value" + ): graph.filter(make_filter()).nodes.id diff --git a/python/tests/test_base_install/test_filters/test_nodes_collection_filter.py b/python/tests/test_base_install/test_filters/test_nodes_collection_filter.py index b0b7fb4492..80985c6fcc 100644 --- a/python/tests/test_base_install/test_filters/test_nodes_collection_filter.py +++ b/python/tests/test_base_install/test_filters/test_nodes_collection_filter.py @@ -5,6 +5,8 @@ from itertools import combinations +import pytest + from raphtory import filter from utils import with_variants @@ -86,40 +88,46 @@ def check(graph): assert set(single) == set(atoms), "every atom needs an independent reference" every = frozenset(graph.nodes.name) cases = [] + views = {"window", "before", "layer"} + # A view composes with `&` only, and `view & X` is `graph.().filter(X)` rather + # than set algebra (pinned below); under `|` or `~` a view is refused where it is written. for a, b in combinations(atoms, 2): + if a in views or b in views: + continue cases.append((f"{a} & {b}", atoms[a] & atoms[b], single[a] & single[b])) cases.append((f"{a} | {b}", atoms[a] | atoms[b], single[a] | single[b])) for a in atoms: - cases.append((f"~{a}", ~atoms[a], every - single[a])) - views = {"window", "before", "layer"} - - def filter_path_reliable(label): - # `graph.filter()` goes through the entity-filter path, which fails open on the same - # composite classes as edge collections: `|` with a view, view & view, and `~view`. - # `nodes[...]` is immune, so it is asserted for everything. - if label.startswith("~"): - return label[1:] not in views - a, op, b = label.split(" ") - if op == "|": - return a not in views and b not in views - return not (a in views and b in views) + if a not in views: + cases.append((f"~{a}", ~atoms[a], every - single[a])) mismatches = [] for label, expr, want in cases: if frozenset(graph.nodes[expr].name) != want: mismatches.append(f"[nodes[]] {label}") - if filter_path_reliable(label): - if frozenset(graph.filter(expr).nodes.name) != want: - mismatches.append(f"[filter()] {label}") + if frozenset(graph.filter(expr).nodes.name) != want: + mismatches.append(f"[filter()] {label}") assert not mismatches, mismatches - # Pin the skip: when the entity path is fixed these fire — delete `filter_path_reliable`. - assert ( - frozenset(graph.filter(~atoms["window"]).nodes.name) - != every - single["window"] - ) - assert ( - frozenset(graph.filter(atoms["name"] | atoms["window"]).nodes.name) - != single["name"] | single["window"] - ) + # `view & X`: the view first, then `X` inside it. + view_ref = { + "window": graph.window(3, 12), + "before": graph.before(12), + "layer": graph.layer("work"), + } + for v, viewed in view_ref.items(): + for name in ("name", "prop"): + want = frozenset(viewed.nodes[atoms[name]].name) + assert ( + frozenset(graph.nodes[atoms[v] & atoms[name]].name) == want + ), f"{v} & {name}" + assert ( + frozenset(graph.filter(atoms[v] & atoms[name]).nodes.name) == want + ), f"{v} & {name}" + assert frozenset( + graph.filter(atoms["window"] & atoms["layer"]).nodes.name + ) == frozenset(graph.window(3, 12).layer("work").nodes.name) + with pytest.raises(TypeError, match="view"): + ~atoms["window"] + with pytest.raises(TypeError, match="view"): + atoms["name"] | atoms["window"] return check diff --git a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index 50179f13c1..cfa7f29512 100644 --- a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py +++ b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py @@ -3,7 +3,7 @@ Filtering is the one place where the local `Graph` and `RemoteGraph` are asked to agree on a *program*, not just a call. Locally a `raphtory.filter` expression is handed straight to the engine; remotely the very same Python -object has to be lowered to a GraphQL `GqlFilter`, shipped, re-parsed and +object has to be lowered to a GraphQL `FilterExpr`, shipped, re-parsed and re-planned on the server. Every step of that lowering can drop a conjunct, confuse a property source (metadata vs temporal), invert a comparison, or attach a view scope to the wrong subtree — and still return a plausible answer. @@ -697,16 +697,6 @@ def _baseline(pair, key, probe, reach): "a node OR an edge predicate: each branch leaves the other entity " "type unconstrained, so the disjunction admits everything", ), - "universal.view_or": ( - lambda: f.Graph.at(3) | f.Graph.at(5), - "a disjunction of two view scopes widens rather than narrows", - ), - "universal.view_not": ( - lambda: ~f.Graph.layer("knows"), - "negating a view scope does not exclude entities from the result; the " - "intended semantics are undecided (#2718), so this pins today's no-op " - "rather than endorsing it", - ), } @@ -733,6 +723,7 @@ def test_is_in_with_a_mistyped_value_matches_nothing_on_both_sides(filter_pair): f"or filters, move the case into REJECTED_EXPRS" ) + @pytest.mark.parametrize("name", sorted(EXPRS), ids=sorted(EXPRS)) def test_expr_discriminates(filter_pair, name): """The expression must keep some entities and drop some — on each side. @@ -1074,6 +1065,9 @@ def test_getitem_narrows_membership_where_filter_rescopes(filter_pair): "reject.unknown_property": lambda: f.Node.property("nope") > 1, "reject.unknown_metadata": lambda: f.Node.metadata("nope") > 1, "reject.degree_vs_str": lambda: f.Node.degree() > "x", + # A view applies to the whole filter: it composes with `&` only (#2718 decided). + "reject.view_or": lambda: f.Graph.at(3) | f.Graph.at(5), + "reject.view_not": lambda: ~f.Graph.layer("knows"), # `avg` is F64 and `len` is U64, so neither accepts a plain Python int here. } @@ -1111,36 +1105,39 @@ def test_rejected_expr_parity_at_nodes_filter(filter_pair, name): ) -# Filters that compare two expressions have no wire form: the GraphQL schema -# only takes a constant on the right-hand side. Locally they run; remotely the -# client has to say so at the call, not ship a mistranslation. +# Filters that compare two expressions travel as the same tree the local +# engine compiles, so every application site must agree with the local answer. +# The deferred sites (`nodes.filter`, `node.filter`, `path.filter`) keep every +# member and narrow what each one sees, so they are read through degrees, which +# the filter changes; membership alone would be the same for any filter. EXPR_RHS_SITES = { - "graph.filter": lambda g, e: [n.name for n in g.filter(e).nodes], - "nodes.filter": lambda g, e: [n.name for n in g.nodes.filter(e)], - "nodes[expr]": lambda g, e: [n.name for n in g.nodes[e]], - "node.filter": lambda g, e: g.node("hub").filter(e) is not None, - "path.filter": lambda g, e: [n.name for n in g.node("hub").neighbours.filter(e)], + "graph.filter": lambda g, e: sorted(n.name for n in g.filter(e).nodes), + "nodes.filter": lambda g, e: sorted( + (n.name, n.degree()) for n in g.nodes.filter(e) + ), + "nodes[expr]": lambda g, e: sorted(n.name for n in g.nodes[e]), + "node.filter": lambda g, e: g.node("hub").filter(e).degree(), + "path.filter": lambda g, e: sorted( + (n.name, n.degree()) for n in g.node("hub").neighbours.filter(e) + ), } @pytest.mark.parametrize("site", sorted(EXPR_RHS_SITES), ids=sorted(EXPR_RHS_SITES)) -def test_expression_rhs_is_refused_remotely_with_the_reason(filter_pair, site): - """`degree() > in_degree()` runs locally; the remote client refuses it. - - The refusal is asserted for its reason, so a future client that silently - dropped the right-hand side (and so sent a different filter) or that - failed later with an unrelated server error would both fail here. The - local side is asserted too: the expression is meaningful and narrows, so - what the client refuses is a real filter, not an already-invalid one. - """ +def test_expression_rhs_agrees_on_both_sides(filter_pair, site): + """`degree() > in_degree()` has no constant on the right, which the old + wire grammar could not say. It is a tree now, so it runs remotely and must + give the local answer. The local side is asserted to differ from a filter + every node passes, so what is compared is a real filter.""" read = EXPR_RHS_SITES[site] expr = f.Node.degree() > f.Node.in_degree() + everything = f.Node.degree() >= 0 local = read(filter_pair.local, expr) - assert local, f"{site}: the expression selects nothing locally" - - with pytest.raises(ValueError, match="no server-side form"): - read(filter_pair.remote, expr) + assert local != read( + filter_pair.local, everything + ), f"{site}: the expression narrows nothing" + assert_parity(filter_pair, lambda g: read(g, expr)) # Node collections that take a `[expr]` subscript. Each must refuse an @@ -1190,7 +1187,7 @@ def test_edge_expr_in_a_node_subscript_is_refused_the_same_way( # `[expr]` with general (non-kind-typed) expressions: select on the wire now -# takes GqlFilter, so graph-view / node / mixed expressions narrow membership +# takes FilterExpr, so graph-view / node / mixed expressions narrow membership # the same way local core select does. SUBSCRIPT_GENERAL_EXPRS = [ ( diff --git a/python/tests/test_base_install/test_graphql/test_apply_views.py b/python/tests/test_base_install/test_graphql/test_apply_views.py index e1f1206fb2..f244ca1aae 100644 --- a/python/tests/test_base_install/test_graphql/test_apply_views.py +++ b/python/tests/test_base_install/test_graphql/test_apply_views.py @@ -2072,10 +2072,10 @@ def test_apply_view_node_filter(): graph(path: "g") { applyViews(views: [ { - nodeFilter: { - property: { - name: "where" - where: { eq: { str: "Berlin" } } + filter: { + eq: { + lhs: { read: { entity: NODE, target: { property: "where" } } } + rhs: { const: { str: "Berlin" } } } } } @@ -2101,10 +2101,10 @@ def test_apply_view_edge_filter(): graph(path: "g") { applyViews(views: [ { - edgeFilter: { - property: { - name: "where" - where: { eq: { str: "fishbowl" } } + filter: { + eq: { + lhs: { read: { entity: EDGE, target: { property: "where" } } } + rhs: { const: { str: "fishbowl" } } } } } @@ -2263,7 +2263,12 @@ def test_apply_view_a_lot_of_views(): applyViews(views: [ { window: { start: 1735689600000, end: 1735862400000 } }, { layers: ["follows"] }, - { nodeFilter: { property: { name: "where", where: { eq: { str: "Berlin" } } } } } + { filter: { + eq: { + lhs: { read: { entity: NODE, target: { property: "where" } } } + rhs: { const: { str: "Berlin" } } + } + } } ]) { list { name diff --git a/python/tests/test_base_install/test_graphql/test_component_filter.py b/python/tests/test_base_install/test_graphql/test_component_filter.py index 54e9affc9e..f881803b5e 100644 --- a/python/tests/test_base_install/test_graphql/test_component_filter.py +++ b/python/tests/test_base_install/test_graphql/test_component_filter.py @@ -2,7 +2,7 @@ while returning full-graph nodes, so their other-layer neighbours remain queryable. All tests are read-only over the same graph, so one module-scoped server serves them all. -Filter selects use `expr: {isValid: true}` as a pass-all edge expression. +Filter selects use `{ isValid: { entity: EDGE } }` as a pass-all edge expression. """ import pytest @@ -38,14 +38,14 @@ def test_out_component_scoped_by_edge_layer(client): client, "a", "outComponent", - '{edge: {layers: {names: ["owns"], expr: {isValid: true}}}}', + '{isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}', ) == ["b", "c"] # scope to `has` -> only a's own satellite (owns edges are not followed) assert _names( client, "a", "outComponent", - '{edge: {layers: {names: ["has"], expr: {isValid: true}}}}', + '{isValid: {entity: EDGE, views: [{layers: ["has"]}]}}', ) == ["x"] @@ -55,18 +55,17 @@ def test_out_component_scoped_by_node_filter(client): client, "a", "outComponent", - '{node: {name: {where: {isIn: {list: [{str: "b"}, {str: "c"}]}}}}}', + '{isIn: {expr: {read: {entity: NODE, target: {field: NAME}}}, values: {list: [{str: "b"}, {str: "c"}]}}}', ) == ["b", "c"] def test_out_component_scoped_by_graph_layer_filter(client): # The graph-level layer filter (the `filter.Graph.layer(...)` equivalent). - assert _names( - client, "a", "outComponent", '{graph: {layers: {names: ["owns"]}}}' - ) == ["b", "c"] - assert _names( - client, "a", "outComponent", '{graph: {layers: {names: ["has"]}}}' - ) == ["x"] + assert _names(client, "a", "outComponent", '{view: [{layers: ["owns"]}]}') == [ + "b", + "c", + ] + assert _names(client, "a", "outComponent", '{view: [{layers: ["has"]}]}') == ["x"] def test_component_filter_and_or_combinators(client): @@ -76,31 +75,28 @@ def test_component_filter_and_or_combinators(client): client, "a", "outComponent", - '{node: {or: [{name: {where: {eq: {str: "b"}}}}, ' - '{name: {where: {eq: {str: "c"}}}}]}}', + '{or: [{eq: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "b"}}}}, {eq: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "c"}}}}]}', ) == ["b", "c"] # node AND: step through nodes that are neither x nor y assert _names( client, "a", "outComponent", - '{node: {and: [{name: {where: {ne: {str: "x"}}}}, ' - '{name: {where: {ne: {str: "y"}}}}]}}', + '{and: [{ne: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "x"}}}}, {ne: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "y"}}}}]}', ) == ["b", "c"] # edge OR: follow owns OR has edges -> everything downstream assert _names( client, "a", "outComponent", - '{edge: {or: [{layers: {names: ["owns"], expr: {isValid: true}}}, ' - '{layers: {names: ["has"], expr: {isValid: true}}}]}}', + '{or: [{isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}, {isValid: {entity: EDGE, views: [{layers: ["has"]}]}}]}', ) == ["b", "c", "x", "y"] # edge AND: owns AND valid assert _names( client, "a", "outComponent", - '{edge: {and: [{layers: {names: ["owns"], expr: {isValid: true}}}, {isValid: true}]}}', + '{and: [{isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}, {isValid: {entity: EDGE}}]}', ) == ["b", "c"] @@ -111,16 +107,14 @@ def test_component_top_level_and_or_across_kinds(client): client, "a", "outComponent", - '{and: [{graph: {layers: {names: ["owns"]}}}, ' - '{node: {name: {where: {ne: {str: "c"}}}}}]}', + '{and: [{view: [{layers: ["owns"]}]}, {ne: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "c"}}}}]}', ) == ["b"] - # graph(has layer) OR edge(owns layer) -> everything downstream + # edge(has layer) OR edge(owns layer) -> everything downstream assert _names( client, "a", "outComponent", - '{or: [{graph: {layers: {names: ["has"]}}}, ' - '{edge: {layers: {names: ["owns"], expr: {isValid: true}}}}]}', + '{or: [{isValid: {entity: EDGE, views: [{layers: ["has"]}]}}, {isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}]}', ) == ["b", "c", "x", "y"] @@ -131,30 +125,30 @@ def test_in_component_scoped_by_filters(client): client, "c", "inComponent", - '{node: {name: {where: {isIn: {list: [{str: "a"}, {str: "b"}]}}}}}', + '{isIn: {expr: {read: {entity: NODE, target: {field: NAME}}}, values: {list: [{str: "a"}, {str: "b"}]}}}', ) == ["a", "b"] # edge filter assert _names( client, "c", "inComponent", - '{edge: {layers: {names: ["owns"], expr: {isValid: true}}}}', + '{isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}', ) == ["a", "b"] # graph (layer) filter - assert _names( - client, "c", "inComponent", '{graph: {layers: {names: ["owns"]}}}' - ) == ["a", "b"] + assert _names(client, "c", "inComponent", '{view: [{layers: ["owns"]}]}') == [ + "a", + "b", + ] # no incoming `has` edges into c - assert ( - _names(client, "c", "inComponent", '{graph: {layers: {names: ["has"]}}}') == [] - ) + assert _names(client, "c", "inComponent", '{view: [{layers: ["has"]}]}') == [] def test_component_respects_an_external_graph_filter(client): # A graph-level filter (here removing `x`) applied before the walk must be honoured — the # returned nodes are over that already-filtered graph. q = ( - '{ graph(path: "g") { filterNodes: filter(expr: {node: {name: {where: {ne: {str: "x"}}}}}) ' + '{ graph(path: "g") { filterNodes: filter(expr: { ne: { lhs: { read: { entity: NODE, target: { field: NAME } } }, ' + 'rhs: { const: { str: "x" } } } }) ' '{ node(name: "a") { outComponent { list { name } } } } } }' ) got = client.query(q)["graph"]["filterNodes"]["node"]["outComponent"]["list"] @@ -165,8 +159,9 @@ def test_component_external_graph_filter_composed_with_select(client): # External graph filter (remove `c`) AND a component `select` (owns layer) compose: the # owns walk from `a` would reach b, c — but c is filtered out, leaving only b. q = ( - '{ graph(path: "g") { filterNodes: filter(expr: {node: {name: {where: {ne: {str: "c"}}}}}) ' - '{ node(name: "a") { outComponent(select: {edge: {layers: {names: ["owns"], expr: {isValid: true}}}}) ' + '{ graph(path: "g") { filterNodes: filter(expr: { ne: { lhs: { read: { entity: NODE, target: { field: NAME } } }, ' + 'rhs: { const: { str: "c" } } } }) ' + '{ node(name: "a") { outComponent(select: { isValid: { entity: EDGE, views: [{ layers: ["owns"] }] } }) ' "{ list { name } } } } } }" ) got = client.query(q)["graph"]["filterNodes"]["node"]["outComponent"]["list"] diff --git a/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py b/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py new file mode 100644 index 0000000000..3b8da8f013 --- /dev/null +++ b/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py @@ -0,0 +1,183 @@ +"""The filter tree as a GraphQL input. + +`FilterExpr` is the same tree the local engine compiles: one grammar for +nodes, edges and views, with an expression on *both* sides of a comparison. +These tests send trees as JSON variables and check the answers against the +same graph read locally, so the wire grammar is pinned by results, not by +shape. +""" + +import pytest +from raphtory import Graph, filter as f +from utils import graphql_client + + +def build(): + """alice.score 3@0 7@2 9@6 · bob.score 5@1 2@7 · carol none · dave.score 1@2 1@3 + alice→bob [knows] @1 @4 · bob→carol [works] @2 · carol→dave [knows] @6""" + g = Graph() + for t, name, score in [ + (0, "alice", 3.0), + (2, "alice", 7.0), + (6, "alice", 9.0), + (1, "bob", 5.0), + (7, "bob", 2.0), + (2, "dave", 1.0), + (3, "dave", 1.0), + ]: + g.add_node(t, name, properties={"score": score}) + g.add_node(0, "carol") + g.add_edge(1, "alice", "bob", layer="knows") + g.add_edge(4, "alice", "bob", layer="knows") + g.add_edge(2, "bob", "carol", layer="works") + g.add_edge(6, "carol", "dave", layer="knows") + return g + + +# `graph.filter(expr)` narrows membership; `nodes.filter(expr)` is the deferred +# form that keeps every node and narrows what each one sees, so membership +# questions are asked at the graph. +NODES = """ +query($f: FilterExpr!) { + graph(path: "g") { filter(expr: $f) { nodes { list { name } } } } +} +""" + +EDGES = """ +query($f: FilterExpr!) { + graph(path: "g") { filter(expr: $f) { edges { list { src { name } dst { name } } } } } +} +""" + + +def read(t): + return {"read": {"entity": "NODE", "target": t}} + + +def const(v): + return {"const": v} + + +def node_names(client, tree): + out = client.query(NODES, {"f": tree}) + return sorted(n["name"] for n in out["graph"]["filter"]["nodes"]["list"]) + + +def edge_pairs(client, tree): + out = client.query(EDGES, {"f": tree}) + return sorted( + (e["src"]["name"], e["dst"]["name"]) + for e in out["graph"]["filter"]["edges"]["list"] + ) + + +def test_both_sides_of_a_comparison_are_expressions(): + """`degree > in_degree` has no constant on either side, which a + constant-only grammar could not say. The tree can, and the server answers + what the local engine answers.""" + g = build() + tree = {"gt": {"lhs": read({"degree": "BOTH"}), "rhs": read({"degree": "IN"})}} + with graphql_client(g) as client: + assert node_names(client, tree) == ["alice", "bob", "carol"] + local = sorted(n.name for n in g.filter(f.Node.degree() > f.Node.in_degree()).nodes) + assert local == ["alice", "bob", "carol"] + + +def test_views_belong_to_the_read(): + """Inside [0, 5) alice's latest score is 7 and bob's is 5.""" + g = build() + windowed = { + "read": { + "entity": "NODE", + "views": [{"window": {"start": 0, "end": 5}}], + "target": {"property": "score"}, + } + } + tree = {"gt": {"lhs": windowed, "rhs": const({"f64": 4.0})}} + plain = {"gt": {"lhs": read({"property": "score"}), "rhs": const({"f64": 4.0})}} + with graphql_client(g) as client: + assert node_names(client, tree) == ["alice", "bob"] + assert node_names(client, plain) == ["alice"] + + +def test_temporal_aggregates_and_qualifiers(): + g = build() + history = {"temporal": read({"property": "score"})} + total = {"gt": {"lhs": {"sum": history}, "rhs": const({"f64": 10.0})}} + any_high = {"gt": {"lhs": {"any": history}, "rhs": const({"f64": 4.0})}} + two_updates = {"eq": {"lhs": {"len": history}, "rhs": const({"u64": 2})}} + with graphql_client(g) as client: + assert node_names(client, total) == ["alice"] + assert node_names(client, any_high) == ["alice", "bob"] + assert node_names(client, two_updates) == ["bob", "dave"] + + +def test_edge_reads_through_an_endpoint_keep_the_edge_views(): + """The window on the edge scopes the source node's score: inside [0, 5) + alice's latest score is 7, so asking for the later 9 matches nothing.""" + g = build() + src_score = { + "read": { + "entity": "EDGE", + "endpoint": "SRC", + "views": [{"window": {"start": 0, "end": 5}}], + "target": {"property": "score"}, + } + } + late = {"eq": {"lhs": src_score, "rhs": const({"f64": 9.0})}} + early = {"eq": {"lhs": src_score, "rhs": const({"f64": 7.0})}} + with graphql_client(g) as client: + assert edge_pairs(client, late) == [] + assert edge_pairs(client, early) == [("alice", "bob")] + + +def test_a_view_leg_restricts_the_whole_filter(): + """`and: [view, predicate]` applies the view first and the predicate inside it, + like `graph.window(0, 2).filter(predicate)`: dave's updates at 2 and 3 fall + outside [0, 2), so he is gone before the predicate runs.""" + g = build() + window = {"view": [{"window": {"start": 0, "end": 2}}]} + has_score = {"isSome": read({"property": "score"})} + with graphql_client(g) as client: + assert node_names(client, {"and": [window, has_score]}) == ["alice", "bob"] + assert node_names(client, has_score) == ["alice", "bob", "dave"] + for shape in ({"or": [window, has_score]}, {"not": window}): + with pytest.raises(Exception, match="view"): + node_names(client, shape) + + +def test_structural_predicates_and_views(): + g = build() + works = {"isActive": {"entity": "EDGE", "views": [{"layers": ["works"]}]}} + window_then_latest = { + "view": [{"window": {"start": 0, "end": 5}}, {"latest": True}] + } + with graphql_client(g) as client: + assert edge_pairs(client, works) == [("bob", "carol")] + assert edge_pairs(client, window_then_latest) == [("alice", "bob")] + + +def test_combinators_presence_and_membership(): + g = build() + tree = { + "and": [ + {"isSome": read({"property": "score"})}, + { + "not": { + "startsWith": { + "lhs": read({"field": "NAME"}), + "rhs": const({"str": "a"}), + } + } + }, + ] + } + members = { + "isIn": { + "expr": read({"field": "NAME"}), + "values": {"list": [{"str": "alice"}, {"str": "dave"}]}, + } + } + with graphql_client(g) as client: + assert node_names(client, tree) == ["bob", "dave"] + assert node_names(client, members) == ["alice", "dave"] diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py index a16097e18a..2acab1f716 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py @@ -12,13 +12,12 @@ def test_filter_edges_with_str_ids_for_node_id_eq_gql(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - src: { - id: { - where: { eq: { str: "3" } } - } + filterEdges: filter(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: SRC } } + rhs: { const: { str: "3" } } } - } }) { + }) { edges { list { src { name } @@ -53,13 +52,12 @@ def test_filter_edges_with_num_ids_for_node_id_eq_gql(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - src: { - id: { - where: { eq: { u64: 1 } } - } + filterEdges: filter(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: SRC } } + rhs: { const: { u64: 1 } } } - } }) { + }) { edges { list { src { name } @@ -86,12 +84,18 @@ def test_edges_chained_selection_with_edge_filter(graph): query { graph(path: "g") { edges { - select(expr: { edge: { dst: { - id: { - where: { eq: { u64: 2 } } + select(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: DST } } + rhs: { const: { u64: 2 } } } - } } }) { - select(expr: { edge: { property: { name: "p2", where: { gt:{ i64: 2 } } } } }) { + }) { + select(expr: { + gt: { + lhs: { read: { entity: EDGE, target: { property: "p2" } } } + rhs: { const: { i64: 2 } } + } + }) { list { src { name } dst { name } } } } @@ -121,7 +125,9 @@ def test_edges_filter_window_is_active(graph): query { graph(path: "g") { edges { - select(expr: { edge: {window: {start: 1, end: 4, expr: {isActive: true}}} }) { + select(expr: { + isActive: { entity: EDGE, views: [{ window: { start: 1, end: 4 } }] } + }) { list { src { name @@ -170,7 +176,9 @@ def test_edges_filter_window_is_deleted(graph, expected_edges): query { graph(path: "g") { edges { - select(expr: { edge: {window: {start: 1, end: 5, expr: {isDeleted: true}}} }) { + select(expr: { + isDeleted: { entity: EDGE, views: [{ window: { start: 1, end: 5 } }] } + }) { list { src { name diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py index 813a562d3f..8233b656c0 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py @@ -17,12 +17,12 @@ def test_graph_edge_property_filter_equal(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "eprop5" - where: { eq: { list: [{i64: 1},{i64: 2},{i64: 3}] } } - } - } }) { + filterEdges: filter(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { property: "eprop5" } } } + rhs: { const: { list: [{ i64: 1 }, { i64: 2 }, { i64: 3 }] } } + } + }) { edges { list { src { name } dst { name } } } } } @@ -43,12 +43,12 @@ def test_graph_edge_property_filter_equal_type_error(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "eprop5" - where: { eq: { i64: 1 } } - } - } }) { + filterEdges: filter(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { property: "eprop5" } } } + rhs: { const: { i64: 1 } } + } + }) { nodes { list { name } } } } @@ -65,12 +65,12 @@ def test_graph_edge_property_filter_not_equal(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "eprop4" - where: { ne: { bool: true } } - } - } }) { + filterEdges: filter(expr: { + ne: { + lhs: { read: { entity: EDGE, target: { property: "eprop4" } } } + rhs: { const: { bool: true } } + } + }) { edges { list { src { name } dst { name } } } } } @@ -91,12 +91,12 @@ def test_graph_edge_property_filter_not_equal_type_error(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "eprop4" - where: { ne: { i64: 1 } } - } - } }) { + filterEdges: filter(expr: { + ne: { + lhs: { read: { entity: EDGE, target: { property: "eprop4" } } } + rhs: { const: { i64: 1 } } + } + }) { edges { list { src { name } dst { name } } } } } @@ -113,12 +113,12 @@ def test_graph_edge_property_filter_greater_than_or_equal(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "eprop1" - where: { ge: { i64: 60 } } - } - } }) { + filterEdges: filter(expr: { + ge: { + lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } + rhs: { const: { i64: 60 } } + } + }) { edges { list { src { name } dst { name } } } } } @@ -139,20 +139,18 @@ def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "eprop1" - where: { ge: { str: "shivam" } } - } - } }) { + filterEdges: filter(expr: { + ge: { + lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { edges { list { src { name } dst { name } } } } } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -161,12 +159,12 @@ def test_graph_edge_property_filter_less_than_or_equal(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "eprop1" - where: { le: { i64: 30 } } - } - } }) { + filterEdges: filter(expr: { + le: { + lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } + rhs: { const: { i64: 30 } } + } + }) { edges { list { src { name } dst { name } } } } } @@ -192,15 +190,18 @@ def test_graph_edge_property_filter_less_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { le: { str: "shivam" } } } } }) { + filterEdges: filter(expr: { + le: { + lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { edges { list { src { name } dst { name } } } } } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -209,7 +210,12 @@ def test_graph_edge_property_filter_greater_than(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { gt: { i64: 30 } } } } }) { + filterEdges: filter(expr: { + gt: { + lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } + rhs: { const: { i64: 30 } } + } + }) { edges { list { src { name } dst { name } } } } } @@ -230,15 +236,18 @@ def test_graph_edge_property_filter_greater_than_type_error(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { gt: { str: "shivam" } } } } }) { + filterEdges: filter(expr: { + gt: { + lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { edges { list { src { name } dst { name } } } } } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -247,7 +256,12 @@ def test_graph_edge_property_filter_less_than(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { lt: { i64: 30 } } } } }) { + filterEdges: filter(expr: { + lt: { + lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } + rhs: { const: { i64: 30 } } + } + }) { edges { list { src { name } dst { name } } } } } @@ -268,15 +282,18 @@ def test_graph_edge_property_filter_less_than_type_error(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { lt: { str: "shivam" } } } } }) { + filterEdges: filter(expr: { + lt: { + lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { edges { list { src { name } dst { name } } } } } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -285,7 +302,7 @@ def test_graph_edge_property_filter_is_none(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop5", where: { isNone: true } } } }) { + filterEdges: filter(expr: { isNone: { read: { entity: EDGE, target: { property: "eprop5" } } } }) { edges { list { src { name } dst { name } } } } } @@ -300,7 +317,7 @@ def test_graph_edge_property_filter_is_some(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop5", where: { isSome: true } } } }) { + filterEdges: filter(expr: { isSome: { read: { entity: EDGE, target: { property: "eprop5" } } } }) { edges { list { src { name } dst { name } } } } } @@ -327,7 +344,12 @@ def test_graph_edge_property_filter_is_in(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { isIn: { list: [{i64: 10},{i64: 20},{i64: 30}] } } } } }) { + filterEdges: filter(expr: { + isIn: { + expr: { read: { entity: EDGE, target: { property: "eprop1" } } } + values: { list: [{ i64: 10 }, { i64: 20 }, { i64: 30 }] } + } + }) { edges { list { src { name } dst { name } } } } } @@ -353,7 +375,12 @@ def test_graph_edge_property_filter_is_empty_list(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { isIn: { list: [] } } } } }) { + filterEdges: filter(expr: { + isIn: { + expr: { read: { entity: EDGE, target: { property: "eprop1" } } } + values: { list: [] } + } + }) { edges { list { src { name } dst { name } } } } } @@ -368,7 +395,12 @@ def test_graph_edge_property_filter_is_in_type_error(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { isIn: { str: "shivam" } } } } }) { + filterEdges: filter(expr: { + isIn: { + expr: { read: { entity: EDGE, target: { property: "eprop1" } } } + values: { str: "shivam" } + } + }) { edges { list { src { name } dst { name } } } } } @@ -385,7 +417,12 @@ def test_graph_edge_property_filter_is_not_in(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { isNotIn: { list: [{i64: 10},{i64: 20},{i64: 30}] } } } } }) { + filterEdges: filter(expr: { + isNotIn: { + expr: { read: { entity: EDGE, target: { property: "eprop1" } } } + values: { list: [{ i64: 10 }, { i64: 20 }, { i64: 30 }] } + } + }) { edges { list { src { name } dst { name } } } } } @@ -406,7 +443,12 @@ def test_graph_edge_property_filter_is_not_in_empty_list(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { isNotIn: { list: [] } } } } }) { + filterEdges: filter(expr: { + isNotIn: { + expr: { read: { entity: EDGE, target: { property: "eprop1" } } } + values: { list: [] } + } + }) { edges { list { src { name } dst { name } } } } } @@ -433,7 +475,12 @@ def test_graph_edge_property_filter_is_not_in_type_error(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { property: { name: "eprop1", where: { isNotIn: { str: "shivam" } } } } }) { + filterEdges: filter(expr: { + isNotIn: { + expr: { read: { entity: EDGE, target: { property: "eprop1" } } } + values: { str: "shivam" } + } + }) { edges { list { src { name } dst { name } } } } } @@ -450,14 +497,14 @@ def test_graph_edge_not_property_filter(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - not: { - property: { - name: "eprop5" - where: { eq: { list: [{i64: 1},{i64: 2}] } } - } + filterEdges: filter(expr: { + not: { + eq: { + lhs: { read: { entity: EDGE, target: { property: "eprop5" } } } + rhs: { const: { list: [{ i64: 1 }, { i64: 2 }] } } } - } }) { + } + }) { edges { list { src { name } dst { name } } } } } @@ -484,12 +531,12 @@ def test_edges_property_filter_starts_with(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "eprop3" - where: { startsWith: { str: "xyz" } } + filterEdges: filter(expr: { + startsWith: { + lhs: { read: { entity: EDGE, target: { property: "eprop3" } } } + rhs: { const: { str: "xyz" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -516,12 +563,12 @@ def test_edges_property_filter_ends_with(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "eprop3" - where: { endsWith: { str: "123" } } + filterEdges: filter(expr: { + endsWith: { + lhs: { read: { entity: EDGE, target: { property: "eprop3" } } } + rhs: { const: { str: "123" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -552,7 +599,12 @@ def test_edges_selection(graph): query = """ query { graph(path: "g") { - edges(select: { property: { name: "p2", where: { gt: { i64: 3 } } } }) { + edges(select: { + gt: { + lhs: { read: { entity: EDGE, target: { property: "p2" } } } + rhs: { const: { i64: 3 } } + } + }) { list { src { name } dst { name } } } } @@ -579,10 +631,18 @@ def test_edges_selection_edges_filter_paired(graph): query = """ query { graph(path: "g") { - edges(select: { property: { name: "p2", where: { gt: { i64: 3 } } } }) { - filter(expr: { edge: { - property: { name: "p3", where: { eq:{ i64: 5 } } } - } }) { + edges(select: { + gt: { + lhs: { read: { entity: EDGE, target: { property: "p2" } } } + rhs: { const: { i64: 3 } } + } + }) { + filter(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { property: "p3" } } } + rhs: { const: { i64: 5 } } + } + }) { list { src { name } dst { name } } } } @@ -611,15 +671,24 @@ def test_edges_chained_selection_edges_filter_paired(graph): query = """ query { graph(path: "g") { - edges(select: { property: { name: "p2", where: { gt: { i64: 3 } } } }) { - select(expr: { edge: { property: { name: "p2", where: { lt: { i64: 5 } } } } }) { - filter(expr: { edge: { - dst: { - id: { - where: { eq: { u64: 2 } } - } + edges(select: { + gt: { + lhs: { read: { entity: EDGE, target: { property: "p2" } } } + rhs: { const: { i64: 3 } } + } + }) { + select(expr: { + lt: { + lhs: { read: { entity: EDGE, target: { property: "p2" } } } + rhs: { const: { i64: 5 } } + } + }) { + filter(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: DST } } + rhs: { const: { u64: 2 } } } - } }) { + }) { list { src { name } dst { name } } } } @@ -645,15 +714,24 @@ def test_edges_chained_selection_edges_filter_paired_ver2(graph): query { graph(path: "g") { edges { - select(expr: { edge: { property: { name: "p2", where: { gt: { i64: 3 } } } } }) { - select(expr: { edge: { property: { name: "p2", where: { lt: { i64: 5 } } } } }) { - filter(expr: { edge: { - dst: { - id: { - where: { eq: { u64: 2 } } - } + select(expr: { + gt: { + lhs: { read: { entity: EDGE, target: { property: "p2" } } } + rhs: { const: { i64: 3 } } + } + }) { + select(expr: { + lt: { + lhs: { read: { entity: EDGE, target: { property: "p2" } } } + rhs: { const: { i64: 5 } } + } + }) { + filter(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: DST } } + rhs: { const: { u64: 2 } } } - } }) { + }) { list { src { name } dst { name } } } } @@ -683,17 +761,18 @@ def test_edge_temporal_property_filter_empty_layers(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - layers: { - names: [] - expr: { - temporalProperty: { - name: "p2" - where: { avg: { lt: { f64: 1.0 } } } + filterEdges: filter(expr: { + lt: { + lhs: { + avg: { + temporal: { + read: { entity: EDGE, target: { property: "p2" }, views: [{ layers: [] }] } + } } } + rhs: { const: { f64: 1.0 } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -709,17 +788,22 @@ def test_edges_temporal_property_last_with_single_layer(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - layers: { - names: ["air_nomads"] - expr: { - temporalProperty: { - name: "p10" - where: { last: { eq: { str: "Paper_ship" } } } + filterEdges: filter(expr: { + eq: { + lhs: { + last: { + temporal: { + read: { + entity: EDGE + target: { property: "p10" } + views: [{ layers: ["air_nomads"] }] + } + } } } + rhs: { const: { str: "Paper_ship" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -743,17 +827,22 @@ def test_edges_temporal_property_last_with_multiple_layers(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - layers: { - names: ["fire_nation", "air_nomads"] - expr: { - temporalProperty: { - name: "p10" - where: { last: { eq: { str: "Paper_airplane" } } } + filterEdges: filter(expr: { + eq: { + lhs: { + last: { + temporal: { + read: { + entity: EDGE + target: { property: "p10" } + views: [{ layers: ["fire_nation", "air_nomads"] }] + } + } } } + rhs: { const: { str: "Paper_airplane" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -776,17 +865,22 @@ def test_edges_temporal_property_last_with_default_layer(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - layers: { - names: ["_default"] - expr: { - temporalProperty: { - name: "p10" - where: { last: { eq: { str: "Paper_airplane" } } } + filterEdges: filter(expr: { + eq: { + lhs: { + last: { + temporal: { + read: { + entity: EDGE + target: { property: "p10" } + views: [{ layers: ["_default"] }] + } + } } } + rhs: { const: { str: "Paper_airplane" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -809,17 +903,18 @@ def test_edges_at_temporal_last(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - at: { - time: 1 - expr: { - temporalProperty: { - name: "p10" - where: { last: { eq: { str: "Paper_airplane" } } } + filterEdges: filter(expr: { + eq: { + lhs: { + last: { + temporal: { + read: { entity: EDGE, target: { property: "p10" }, views: [{ at: 1 }] } + } } } + rhs: { const: { str: "Paper_airplane" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -841,17 +936,18 @@ def test_edges_before_temporal_last(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - before: { - time: 2 - expr: { - temporalProperty: { - name: "p10" - where: { last: { eq: { str: "Paper_airplane" } } } + filterEdges: filter(expr: { + eq: { + lhs: { + last: { + temporal: { + read: { entity: EDGE, target: { property: "p10" }, views: [{ before: 2 }] } + } } } + rhs: { const: { str: "Paper_airplane" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -873,17 +969,18 @@ def test_edges_after_temporal_last(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - after: { - time: 2 - expr: { - temporalProperty: { - name: "p10" - where: { last: { eq: { str: "Paper_ship" } } } + filterEdges: filter(expr: { + eq: { + lhs: { + last: { + temporal: { + read: { entity: EDGE, target: { property: "p10" }, views: [{ after: 2 }] } + } } } + rhs: { const: { str: "Paper_ship" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -905,16 +1002,18 @@ def test_edges_latest_temporal_last(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - latest: { - expr: { - temporalProperty: { - name: "p10" - where: { last: { eq: { str: "Paper_ship" } } } + filterEdges: filter(expr: { + eq: { + lhs: { + last: { + temporal: { + read: { entity: EDGE, target: { property: "p10" }, views: [{ latest: true }] } + } } } + rhs: { const: { str: "Paper_ship" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -935,17 +1034,22 @@ def test_edges_snapshot_at_temporal_last(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - snapshotAt: { - time: 2 - expr: { - temporalProperty: { - name: "p10" - where: { last: { eq: { str: "Paper_ship" } } } + filterEdges: filter(expr: { + eq: { + lhs: { + last: { + temporal: { + read: { + entity: EDGE + target: { property: "p10" } + views: [{ snapshotAt: 2 }] + } + } } } + rhs: { const: { str: "Paper_ship" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -966,16 +1070,22 @@ def test_edges_snapshot_latest_temporal_last(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { edge: { - snapshotLatest: { - expr: { - temporalProperty: { - name: "p10" - where: { last: { eq: { str: "Paper_ship" } } } + filterEdges: filter(expr: { + eq: { + lhs: { + last: { + temporal: { + read: { + entity: EDGE + target: { property: "p10" } + views: [{ snapshotLatest: true }] + } + } } } + rhs: { const: { str: "Paper_ship" } } } - } }) { + }) { edges { list { src { name } dst { name } } } } } @@ -997,15 +1107,7 @@ def test_edges_graph_filter_gql(graph): query { graph(path: "g") { filter(expr: { - window: { - start: 1 - end: 4 - expr: { - layers: { - names: ["fire_nation"] - } - } - } + view: [{ layers: ["fire_nation"] }, { window: { start: 1, end: 4 } }] }) { nodes { diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index f7d79882e0..eb3bd1590f 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -12,12 +12,12 @@ def test_graph_node_property_filter_equal(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop5" - where: { eq: { list: [ {i64: 1}, {i64: 2}, {i64: 3} ] } } - } - } }) { + filterNodes: filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "prop5" } } } + rhs: { const: { list: [{ i64: 1 }, { i64: 2 }, { i64: 3 }] } } + } + }) { nodes { list { name } } } } @@ -32,12 +32,12 @@ def test_graph_node_property_filter_equal_type_error(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop5" - where: { eq: { i64: 1 } } - } - } }) { + filterNodes: filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "prop5" } } } + rhs: { const: { i64: 1 } } + } + }) { nodes { list { name } } } } @@ -54,12 +54,12 @@ def test_graph_node_property_filter_not_equal(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop4" - where: { ne: { bool: true } } - } - } }) { + filterNodes: filter(expr: { + ne: { + lhs: { read: { entity: NODE, target: { property: "prop4" } } } + rhs: { const: { bool: true } } + } + }) { nodes { list { name } } } } @@ -76,12 +76,12 @@ def test_graph_node_property_filter_not_equal_type_error(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop4" - where: { ne: { i64: 1 } } - } - } }) { + filterNodes: filter(expr: { + ne: { + lhs: { read: { entity: NODE, target: { property: "prop4" } } } + rhs: { const: { i64: 1 } } + } + }) { nodes { list { name } } } } @@ -98,12 +98,12 @@ def test_graph_node_property_filter_greater_than_or_equal(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop1" - where: { ge: { i64: 60 } } - } - } }) { + filterNodes: filter(expr: { + ge: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 60 } } + } + }) { nodes { list { name } } } } @@ -118,20 +118,18 @@ def test_graph_node_property_filter_greater_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop1" - where: { ge: { str: "shivam" } } - } - } }) { + filterNodes: filter(expr: { + ge: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { nodes { list { name } } } } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -140,9 +138,12 @@ def test_graph_node_property_filter_less_than_or_equal(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { name: "prop1", where: { le: { i64: 30 } } } - } }) { + filterNodes: filter(expr: { + le: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 30 } } + } + }) { nodes { list { name } } } } @@ -163,15 +164,18 @@ def test_graph_node_property_filter_less_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { le: { str: "shivam" } } } } }) { + filterNodes: filter(expr: { + le: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { nodes { list { name } } } } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -180,7 +184,12 @@ def test_graph_node_property_filter_greater_than(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { gt: { i64: 30 } } } } }) { + filterNodes: filter(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 30 } } + } + }) { nodes { list { name } } } } @@ -195,15 +204,18 @@ def test_graph_node_property_filter_greater_than_type_error(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { gt: { str: "shivam" } } } } }) { + filterNodes: filter(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { nodes { list { name } } } } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -212,7 +224,12 @@ def test_graph_node_property_filter_less_than(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { lt: { i64: 30 } } } } }) { + filterNodes: filter(expr: { + lt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 30 } } + } + }) { nodes { list { name } } } } @@ -229,15 +246,18 @@ def test_graph_node_property_filter_less_than_type_error(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { lt: { str: "shivam" } } } } }) { + filterNodes: filter(expr: { + lt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { nodes { list { name } } } } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -246,7 +266,7 @@ def test_graph_node_property_filter_is_none(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop5", where: { isNone: true } } } }) { + filterNodes: filter(expr: { isNone: { read: { entity: NODE, target: { property: "prop5" } } } }) { nodes { list { name } } } } @@ -263,7 +283,7 @@ def test_graph_node_property_filter_is_some(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop5", where: { isSome: true } } } }) { + filterNodes: filter(expr: { isSome: { read: { entity: NODE, target: { property: "prop5" } } } }) { nodes { list { name } } } } @@ -280,7 +300,12 @@ def test_graph_node_property_filter_is_in(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { isIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } } }) { + filterNodes: filter(expr: { + isIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [{ i64: 10 }, { i64: 30 }, { i64: 50 }, { i64: 70 }] } + } + }) { nodes { list { name } } } } @@ -298,7 +323,12 @@ def test_node_property_filter_is_in_empty_list(graph): query { graph(path: "g") { nodes { - select(expr: { node: { property: { name: "prop1", where: { isIn: { list: [] } } } } } + select(expr: { + isIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [] } + } + } ) { list { name } } @@ -316,7 +346,12 @@ def test_graph_node_property_filter_is_in_no_value(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { isIn: { list: [] } } } } }) { + filterNodes: filter(expr: { + isIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [] } + } + }) { nodes { list { name } } } } @@ -331,7 +366,12 @@ def test_graph_node_property_filter_is_in_type_error(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { isIn: { str: "shivam" } } } } }) { + filterNodes: filter(expr: { + isIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { str: "shivam" } + } + }) { nodes { list { name } } } } @@ -348,7 +388,12 @@ def test_graph_node_property_filter_is_not_in_any(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { isNotIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } } }) { + filterNodes: filter(expr: { + isNotIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [{ i64: 10 }, { i64: 30 }, { i64: 50 }, { i64: 70 }] } + } + }) { nodes { list { name } } } } @@ -366,7 +411,12 @@ def test_node_property_filter_not_is_not_in_empty_list(graph): query { graph(path: "g") { nodes { - filter(expr: { node: { property: { name: "prop1", where: { isNotIn: { list: [] } } } } }) { + filter(expr: { + isNotIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [] } + } + }) { list { name } } } @@ -390,7 +440,12 @@ def test_graph_node_property_filter_is_not_in_type_error(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { property: { name: "prop1", where: { isNotIn: { str: "shivam" } } } } }) { + filterNodes: filter(expr: { + isNotIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { str: "shivam" } + } + }) { nodes { list { name } } } } @@ -407,14 +462,14 @@ def test_graph_node_not_property_filter(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - not: { - property: { - name: "prop5" - where: { eq: { list: [ {i64: 1}, {i64: 2} ] } } - } + filterNodes: filter(expr: { + not: { + eq: { + lhs: { read: { entity: NODE, target: { property: "prop5" } } } + rhs: { const: { list: [{ i64: 1 }, { i64: 2 }] } } } - } }) { + } + }) { nodes { list { name } } } } @@ -438,21 +493,22 @@ def test_graph_node_type_and_property_filter(graph): query { graph(path: "g") { nodes { - select(expr: { node: { + select(expr: { and: [ { - nodeType: { - where: { isIn: { list: [ {str: "fire_nation"}, {str: "water_tribe"} ] } } + isIn: { + expr: { read: { entity: NODE, target: { field: NODE_TYPE } } } + values: { list: [{ str: "fire_nation" }, { str: "water_tribe" }] } } }, { - property: { - name: "prop2", - where: { gt: { f64: 1 } } + gt: { + lhs: { read: { entity: NODE, target: { property: "prop2" } } } + rhs: { const: { f64: 1 } } } } ] - } }) { + }) { count list { name } } @@ -478,12 +534,12 @@ def test_graph_nodes_property_filter_starts_with(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop3" - where: { startsWith: { str: "abc" } } + filterNodes: filter(expr: { + startsWith: { + lhs: { read: { entity: NODE, target: { property: "prop3" } } } + rhs: { const: { str: "abc" } } } - } }) { + }) { nodes { list { name } } } } @@ -506,12 +562,12 @@ def test_graph_nodes_property_filter_ends_with(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop3" - where: { endsWith: { str: "123" } } + filterNodes: filter(expr: { + endsWith: { + lhs: { read: { entity: NODE, target: { property: "prop3" } } } + rhs: { const: { str: "123" } } } - } }) { + }) { nodes { list { name } } } } @@ -526,12 +582,14 @@ def test_graph_nodes_property_filter_starts_with_temporal_any(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - temporalProperty: { - name: "prop3", - where: { any: { startsWith: { str: "abc1" } } } + filterNodes: filter(expr: { + startsWith: { + lhs: { + any: { temporal: { read: { entity: NODE, target: { property: "prop3" } } } } } - } }) { + rhs: { const: { str: "abc1" } } + } + }) { nodes { list { name } } } } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py index 79ed77b1ff..47f4b40adf 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py @@ -13,21 +13,22 @@ def test_out_neighbours_found(graph): query { graph(path: "g") { node(name: "a") { - filter(expr: { node: { + filter(expr: { and: [ { - name: { - where: { eq: { str: "d" } } + eq: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "d" } } } }, { - property: { - name: "prop1" - where: { gt: { i64: 10 } } + gt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 10 } } } } ] - } }) { + }) { outNeighbours { list { name } } @@ -48,25 +49,27 @@ def test_out_neighbours_found_select(graph): query { graph(path: "g") { node(name: "a") { - filter(expr: { node: { + filter(expr: { and: [ { - name: { - where: { eq: { str: "d" } } + eq: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "d" } } } }, { - property: { - name: "prop1" - where: { gt: { i64: 10 } } + gt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 10 } } } } ] - } }) { + }) { outNeighbours(select: { - name: { - where: { eq: { str: "d" } } - } + eq: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "d" } } + } }) { list { name } } @@ -87,11 +90,12 @@ def test_out_neighbours_not_found(graph): query { graph(path: "g") { node(name: "a") { - filter(expr: { node: { - name: { - where: { eq: { str: "e" } } + filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "e" } } } - } }) { + }) { outNeighbours { list { name } } @@ -110,12 +114,12 @@ def test_in_neighbours_found(graph): query { graph(path: "g") { node(name: "d") { - filter(expr: { node: { - property: { - name: "prop1" - where: { gt: { i64: 10 } } + filter(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 10 } } } - } }) { + }) { inNeighbours { list { name } } @@ -140,16 +144,17 @@ def test_in_neighbours_found_select(graph): query { graph(path: "g") { node(name: "d") { - filter(expr: { node: { - property: { - name: "prop1" - where: { gt: { i64: 10 } } + filter(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 10 } } } - } }) { + }) { inNeighbours(select: { - name: { - where: { eq: { str: "c" } } - } + eq: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "c" } } + } }) { list { name } } @@ -170,11 +175,12 @@ def test_in_neighbours_not_found(graph): query { graph(path: "g") { node(name: "d") { - filter(expr: { node: { - name: { - where: { eq: { str: "e" } } + filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "e" } } } - } }) { + }) { inNeighbours { list { name } } @@ -193,11 +199,12 @@ def test_neighbours_found(graph): query { graph(path: "g") { node(name: "d") { - filter(expr: { node: { - name: { - where: { ne: { str: "a" } } + filter(expr: { + ne: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "a" } } } - } }) { + }) { neighbours { list { name } } @@ -220,15 +227,17 @@ def test_neighbours_found_select(graph): query { graph(path: "g") { node(name: "d") { - filter(expr: { node: { - name: { - where: { ne: { str: "a" } } + filter(expr: { + ne: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "a" } } } - } }) { + }) { neighbours(select: { - name: { - where: { eq: { str: "b" } } - } + eq: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "b" } } + } }) { list { name } } @@ -249,11 +258,12 @@ def test_neighbours_not_found(graph): query { graph(path: "g") { node(name: "d") { - filter(expr: { node: { - name: { - where: { eq: { str: "e" } } + filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { field: NAME } } } + rhs: { const: { str: "e" } } } - } }) { + }) { neighbours { list { name } } @@ -275,12 +285,20 @@ def test_neighbours_selection(graph): query = """ query { graph(path: "g") { - nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + nodes(select: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p100" } } } + rhs: { const: { i64: 30 } } + } + }) { list { neighbours { - select(expr: { node: { - property: { name: "p2", where: { gt: { i64: 3 } } } - } }) { + select(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p2" } } } + rhs: { const: { i64: 3 } } + } + }) { list { name } @@ -309,13 +327,21 @@ def test_neighbours_neighbours_filtering(graph): query = """ query { graph(path: "g") { - nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + nodes(select: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p100" } } } + rhs: { const: { i64: 30 } } + } + }) { list { name neighbours { - filter(expr: { node: { - property: { name: "p2", where: { gt: { i64: 3 } } } - } }) { + filter(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p2" } } } + rhs: { const: { i64: 3 } } + } + }) { list { name neighbours { diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py index f2a571a518..e53d872bb4 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py @@ -21,11 +21,12 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - id: { - where: { eq: { str: "1" } } - } - } }) { + filterNodes: filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { field: ID } } } + rhs: { const: { str: "1" } } + } + }) { nodes { list { name } } @@ -49,7 +50,8 @@ def test_sort_key_with_no_or_several_fields_is_rejected(graph): nodes { sorted(sortBys: %s) { list { name } } } } } - """ % keys, + """ + % keys, "exactly one", graph, ) @@ -60,11 +62,12 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql2(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - id: { - where: { eq: { u64: 1 } } - } - } }) { + filterNodes: filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { field: ID } } } + rhs: { const: { u64: 1 } } + } + }) { nodes { list { name } } @@ -87,11 +90,12 @@ def test_filter_nodes_with_num_ids_for_node_id_eq_gql(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - id: { - where: { eq: { u64: 1 } } - } - } }) { + filterNodes: filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { field: ID } } } + rhs: { const: { u64: 1 } } + } + }) { nodes { list { name } } @@ -109,13 +113,24 @@ def test_nodes_chained_selection_with_node_filter(graph): query { graph(path: "g") { nodes { - select(expr: { node: { nodeType: { - where: { eq: { str: "fire_nation" } } - } } }) { - select(expr: { node: { property: { name: "p9", where: { eq:{ i64: 5 } } } } }) { - filter(expr: { node: { - property: { name: "p100", where: { gt: { i64: 30 } } } - } }) { + select(expr: { + eq: { + lhs: { read: { entity: NODE, target: { field: NODE_TYPE } } } + rhs: { const: { str: "fire_nation" } } + } + }) { + select(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "p9" } } } + rhs: { const: { i64: 5 } } + } + }) { + filter(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p100" } } } + rhs: { const: { i64: 30 } } + } + }) { list { name } @@ -140,7 +155,9 @@ def test_nodes_filter_windowed_is_active(graph): query { graph(path: "g") { nodes { - select(expr: {node: {window: {start: 1, end: 4, expr: {isActive: true}}}}) { + select(expr: { + isActive: { entity: NODE, views: [{ window: { start: 1, end: 4 } }] } + }) { list { name } @@ -168,7 +185,11 @@ def test_nodes_filter_windowed_is_not_active(graph): query { graph(path: "g") { nodes { - select(expr: {node: {window: {start: 1, end: 4, expr: {isActive: false}}}}) { + select(expr: { + not: { + isActive: { entity: NODE, views: [{ window: { start: 1, end: 4 } }] } + } + }) { list { name } @@ -215,11 +236,25 @@ def _expected_degree_select_names(graph, direction, predicate): ) +def _degree(direction, op, value=None, over=None): + """A degree predicate in the tree grammar: `degree(direction) value`, the degree + optionally wrapped in an aggregate or qualifier (`over`) so invalid chains can be spelled. + """ + lhs = f"{{ read: {{ entity: NODE, target: {{ degree: {direction} }} }} }}" + if over: + lhs = f"{{ {over}: {lhs} }}" + if op in ("isSome", "isNone"): + return f"{{ {op}: {lhs} }}" + if op in ("isIn", "isNotIn"): + return f"{{ {op}: {{ expr: {lhs}, values: {value} }} }}" + return f"{{ {op}: {{ lhs: {lhs}, rhs: {{ const: {value} }} }} }}" + + def _degree_filter_nodes_query_expected_pair(expr, expected_names): query = f""" query {{ graph(path: "g") {{ - filterNodes: filter(expr: {{ node: {{ {expr} }} }}) {{ + filterNodes: filter(expr: {expr}) {{ nodes {{ list {{ name }} }} @@ -243,7 +278,7 @@ def _degree_select_nodes_query_expected_pair(expr, expected_names): query {{ graph(path: "g") {{ nodes {{ - select(expr: {{ node: {{ {expr} }} }}) {{ + select(expr: {expr}) {{ list {{ name }} }} }} @@ -267,7 +302,7 @@ def test_filter_nodes_degree_ops_and_gql(graph): for direction in ["BOTH", "IN", "OUT"]: queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ lt: {{ u64: {threshold} }} }} }}", + _degree(direction, "lt", f"{{ u64: {threshold} }}"), _expected_degree_select_names( graph, direction, lambda d: d < threshold ), @@ -275,13 +310,13 @@ def test_filter_nodes_degree_ops_and_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ lt: {{ u64: {threshold} }} }} }}", + _degree(direction, "lt", f"{{ u64: {threshold} }}"), _expected_degree_names(graph, direction, lambda d: d < threshold), ) ) queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ le: {{ u64: {threshold} }} }} }}", + _degree(direction, "le", f"{{ u64: {threshold} }}"), _expected_degree_select_names( graph, direction, lambda d: d <= threshold ), @@ -289,13 +324,13 @@ def test_filter_nodes_degree_ops_and_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ le: {{ u64: {threshold} }} }} }}", + _degree(direction, "le", f"{{ u64: {threshold} }}"), _expected_degree_names(graph, direction, lambda d: d <= threshold), ) ) queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ eq: {{ u64: {threshold} }} }} }}", + _degree(direction, "eq", f"{{ u64: {threshold} }}"), _expected_degree_select_names( graph, direction, lambda d: d == threshold ), @@ -303,13 +338,13 @@ def test_filter_nodes_degree_ops_and_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ eq: {{ u64: {threshold} }} }} }}", + _degree(direction, "eq", f"{{ u64: {threshold} }}"), _expected_degree_names(graph, direction, lambda d: d == threshold), ) ) queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ ne: {{ u64: {threshold} }} }} }}", + _degree(direction, "ne", f"{{ u64: {threshold} }}"), _expected_degree_select_names( graph, direction, lambda d: d != threshold ), @@ -317,13 +352,13 @@ def test_filter_nodes_degree_ops_and_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ ne: {{ u64: {threshold} }} }} }}", + _degree(direction, "ne", f"{{ u64: {threshold} }}"), _expected_degree_names(graph, direction, lambda d: d != threshold), ) ) queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ ge: {{ u64: {threshold} }} }} }}", + _degree(direction, "ge", f"{{ u64: {threshold} }}"), _expected_degree_select_names( graph, direction, lambda d: d >= threshold ), @@ -331,13 +366,13 @@ def test_filter_nodes_degree_ops_and_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ ge: {{ u64: {threshold} }} }} }}", + _degree(direction, "ge", f"{{ u64: {threshold} }}"), _expected_degree_names(graph, direction, lambda d: d >= threshold), ) ) queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ gt: {{ u64: {threshold} }} }} }}", + _degree(direction, "gt", f"{{ u64: {threshold} }}"), _expected_degree_select_names( graph, direction, lambda d: d > threshold ), @@ -345,7 +380,7 @@ def test_filter_nodes_degree_ops_and_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ gt: {{ u64: {threshold} }} }} }}", + _degree(direction, "gt", f"{{ u64: {threshold} }}"), _expected_degree_names(graph, direction, lambda d: d > threshold), ) ) @@ -360,12 +395,16 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): queries_and_expected_outputs = [] for direction in ["BOTH", "IN", "OUT"]: + above = _degree(direction, "gt", f"{{ u64: {threshold} }}") + below_upper = _degree(direction, "lt", f"{{ u64: {upper} }}") + below = _degree(direction, "lt", f"{{ u64: {threshold} }}") + above_upper = _degree(direction, "gt", f"{{ u64: {upper} }}") queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - "and: [" - f"{{ degree: {{ direction: {direction}, where: {{ gt: {{ u64: {threshold} }} }} }} }}," - f"{{ degree: {{ direction: {direction}, where: {{ lt: {{ u64: {upper} }} }} }} }}" - "]", + f"{{ and: [" + f"{above}," + f"{below_upper}" + "] }", _expected_degree_select_names( graph, direction, lambda d: d > threshold and d < upper ), @@ -373,10 +412,10 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - "and: [" - f"{{ degree: {{ direction: {direction}, where: {{ gt: {{ u64: {threshold} }} }} }} }}," - f"{{ degree: {{ direction: {direction}, where: {{ lt: {{ u64: {upper} }} }} }} }}" - "]", + f"{{ and: [" + f"{above}," + f"{below_upper}" + "] }", _expected_degree_names( graph, direction, lambda d: d > threshold and d < upper ), @@ -385,10 +424,10 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - "or: [" - f"{{ degree: {{ direction: {direction}, where: {{ lt: {{ u64: {threshold} }} }} }} }}," - f"{{ degree: {{ direction: {direction}, where: {{ gt: {{ u64: {upper} }} }} }} }}" - "]", + f"{{ or: [" + f"{below}," + f"{above_upper}" + "] }", _expected_degree_select_names( graph, direction, lambda d: d < threshold or d > upper ), @@ -396,10 +435,10 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - "or: [" - f"{{ degree: {{ direction: {direction}, where: {{ lt: {{ u64: {threshold} }} }} }} }}," - f"{{ degree: {{ direction: {direction}, where: {{ gt: {{ u64: {upper} }} }} }} }}" - "]", + f"{{ or: [" + f"{below}," + f"{above_upper}" + "] }", _expected_degree_names( graph, direction, lambda d: d < threshold or d > upper ), @@ -408,12 +447,12 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - "or: [" - f"{{ degree: {{ direction: {direction}, where: {{ lt: {{ u64: {threshold} }} }} }} }}," - "{ not: " - f"{{ degree: {{ direction: {direction}, where: {{ gt: {{ u64: {upper} }} }} }} }}" - " }" - "]", + f"{{ or: [" + f"{below}," + f"{{ not: " + f"{above_upper}" + f" }}" + "] }", _expected_degree_select_names( graph, direction, lambda d: d < threshold or d <= upper ), @@ -421,12 +460,12 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - "or: [" - f"{{ degree: {{ direction: {direction}, where: {{ lt: {{ u64: {threshold} }} }} }} }}," - "{ not: " - f"{{ degree: {{ direction: {direction}, where: {{ gt: {{ u64: {upper} }} }} }} }}" - " }" - "]", + f"{{ or: [" + f"{below}," + f"{{ not: " + f"{above_upper}" + f" }}" + "] }", _expected_degree_names( graph, direction, lambda d: d < threshold or d <= upper ), @@ -435,7 +474,11 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ isIn: {{ list: [{{u64: {threshold}}}, {{u64: {threshold + 1}}}] }} }} }}", + _degree( + direction, + "isIn", + f"{{ list: [{{u64: {threshold}}}, {{u64: {threshold + 1}}}] }}", + ), _expected_degree_select_names( graph, direction, lambda d: d in [threshold, threshold + 1] ), @@ -443,7 +486,11 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ isIn: {{ list: [{{u64: {threshold}}}, {{u64: {threshold + 1}}}] }} }} }}", + _degree( + direction, + "isIn", + f"{{ list: [{{u64: {threshold}}}, {{u64: {threshold + 1}}}] }}", + ), _expected_degree_names( graph, direction, lambda d: d in [threshold, threshold + 1] ), @@ -452,7 +499,11 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ isNotIn: {{ list: [{{u64: {threshold}}}, {{u64: {threshold + 1}}}] }} }} }}", + _degree( + direction, + "isNotIn", + f"{{ list: [{{u64: {threshold}}}, {{u64: {threshold + 1}}}] }}", + ), _expected_degree_select_names( graph, direction, lambda d: d not in [threshold, threshold + 1] ), @@ -460,7 +511,11 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ isNotIn: {{ list: [{{u64: {threshold}}}, {{u64: {threshold + 1}}}] }} }} }}", + _degree( + direction, + "isNotIn", + f"{{ list: [{{u64: {threshold}}}, {{u64: {threshold + 1}}}] }}", + ), _expected_degree_names( graph, direction, lambda d: d not in [threshold, threshold + 1] ), @@ -479,7 +534,7 @@ def test_filter_nodes_degree_numeric_coercion_gql(graph): for direction in ["BOTH", "IN", "OUT"]: queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f'degree: {{ direction: {direction}, where: {{ eq: {{ str: "{threshold_str}" }} }} }}', + _degree(direction, "eq", f'{{ str: "{threshold_str}" }}'), _expected_degree_select_names( graph, direction, lambda d: d == int(threshold_str) ), @@ -487,7 +542,7 @@ def test_filter_nodes_degree_numeric_coercion_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f'degree: {{ direction: {direction}, where: {{ eq: {{ str: "{threshold_str}" }} }} }}', + _degree(direction, "eq", f'{{ str: "{threshold_str}" }}'), _expected_degree_names( graph, direction, lambda d: d == int(threshold_str) ), @@ -496,7 +551,7 @@ def test_filter_nodes_degree_numeric_coercion_gql(graph): queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ ge: {{ f64: {threshold_float} }} }} }}", + _degree(direction, "ge", f"{{ f64: {threshold_float} }}"), _expected_degree_select_names( graph, direction, lambda d: d >= int(threshold_float) ), @@ -504,7 +559,7 @@ def test_filter_nodes_degree_numeric_coercion_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"degree: {{ direction: {direction}, where: {{ ge: {{ f64: {threshold_float} }} }} }}", + _degree(direction, "ge", f"{{ f64: {threshold_float} }}"), _expected_degree_names( graph, direction, lambda d: d >= int(threshold_float) ), @@ -513,13 +568,13 @@ def test_filter_nodes_degree_numeric_coercion_gql(graph): queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f'degree: {{ direction: {direction}, where: {{ isIn: {{ list: [{{str: "3"}}, {{f64: 4.9}}] }} }} }}', + _degree(direction, "isIn", f'{{ list: [{{str: "3"}}, {{f64: 4.9}}] }}'), _expected_degree_select_names(graph, direction, lambda d: d in [3, 4]), ) ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f'degree: {{ direction: {direction}, where: {{ isIn: {{ list: [{{str: "3"}}, {{f64: 4.9}}] }} }} }}', + _degree(direction, "isIn", f'{{ list: [{{str: "3"}}, {{f64: 4.9}}] }}'), _expected_degree_names(graph, direction, lambda d: d in [3, 4]), ) ) @@ -530,17 +585,17 @@ def test_filter_nodes_degree_numeric_coercion_gql(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_filter_nodes_degree_invalid_non_numeric_string_values_gql(graph): invalid_exprs = [ - 'degree: { direction: BOTH, where: { lt: { str: "foo" } } }', - 'degree: { direction: IN, where: { eq: { str: "bar" } } }', - 'degree: { direction: OUT, where: { isIn: { list: [{str: "a"}, {str: "b"}] } } }', - 'degree: { direction: BOTH, where: { isNotIn: { list: [{str: "x"}, {str: "y"}] } } }', + _degree("BOTH", "lt", '{ str: "foo" }'), + _degree("IN", "eq", '{ str: "bar" }'), + _degree("OUT", "isIn", '{ list: [{str: "a"}, {str: "b"}] }'), + _degree("BOTH", "isNotIn", '{ list: [{str: "x"}, {str: "y"}] }'), ] for expr in invalid_exprs: filter_nodes_query = f""" query {{ graph(path: "g") {{ - filterNodes: filter(expr: {{ node: {{ {expr} }} }}) {{ + filterNodes: filter(expr: {expr}) {{ nodes {{ list {{ name }} }} @@ -553,7 +608,7 @@ def test_filter_nodes_degree_invalid_non_numeric_string_values_gql(graph): query {{ graph(path: "g") {{ nodes {{ - select(expr: {{ node: {{ {expr} }} }}) {{ + select(expr: {expr}) {{ list {{ name }} }} }} @@ -568,26 +623,26 @@ def test_filter_nodes_degree_invalid_non_numeric_string_values_gql(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_filter_nodes_degree_invalid_expressions_gql(graph): invalid_exprs = [ - "degree: { direction: BOTH, where: { isNone: true } }", - "degree: { direction: IN, where: { isSome: true } }", - 'degree: { direction: OUT, where: { startsWith: { str: "1" } } }', - 'degree: { direction: BOTH, where: { endsWith: { str: "1" } } }', - 'degree: { direction: IN, where: { contains: { str: "1" } } }', - 'degree: { direction: OUT, where: { notContains: { str: "1" } } }', - "degree: { direction: BOTH, where: { any: { eq: { u64: 1 } } } }", - "degree: { direction: IN, where: { all: { eq: { u64: 1 } } } }", - "degree: { direction: OUT, where: { len: { gt: { u64: 0 } } } }", - "degree: { direction: BOTH, where: { sum: { eq: { u64: 1 } } } }", - "degree: { direction: IN, where: { avg: { eq: { u64: 1 } } } }", - "degree: { direction: OUT, where: { first: { eq: { u64: 1 } } } }", - "degree: { direction: BOTH, where: { last: { eq: { u64: 1 } } } }", + _degree("BOTH", "isNone", "true"), + _degree("IN", "isSome", "true"), + _degree("OUT", "startsWith", '{ str: "1" }'), + _degree("BOTH", "endsWith", '{ str: "1" }'), + _degree("IN", "contains", '{ str: "1" }'), + _degree("OUT", "notContains", '{ str: "1" }'), + _degree("BOTH", "eq", "{ u64: 1 }", over="any"), + _degree("IN", "eq", "{ u64: 1 }", over="all"), + _degree("OUT", "gt", "{ u64: 0 }", over="len"), + _degree("BOTH", "eq", "{ u64: 1 }", over="sum"), + _degree("IN", "eq", "{ u64: 1 }", over="avg"), + _degree("OUT", "eq", "{ u64: 1 }", over="first"), + _degree("BOTH", "eq", "{ u64: 1 }", over="last"), ] for expr in invalid_exprs: filter_nodes_query = f""" query {{ graph(path: "g") {{ - filterNodes: filter(expr: {{ node: {{ {expr} }} }}) {{ + filterNodes: filter(expr: {expr}) {{ nodes {{ list {{ name }} }} @@ -600,7 +655,7 @@ def test_filter_nodes_degree_invalid_expressions_gql(graph): query {{ graph(path: "g") {{ nodes {{ - select(expr: {{ node: {{ {expr} }} }}) {{ + select(expr: {expr}) {{ list {{ name }} }} }} diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py index 3cc27b66bc..aba4c397d7 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py @@ -23,14 +23,12 @@ def test_node_property_filter_equal2(graph): query { graph(path: "g") { nodes { - filter(expr: { node: { - property: { - name: "prop5" - where: { - eq: { list: [ {i64: 1}, {i64: 2}, {i64: 3} ] } - } - } - } }) { + filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "prop5" } } } + rhs: { const: { list: [{ i64: 1 }, { i64: 2 }, { i64: 3 }] } } + } + }) { list { name neighbours { @@ -67,14 +65,12 @@ def test_node_property_filter_equal3(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop5" - where: { - eq: { list: [ {i64: 1}, {i64: 2}, {i64: 3} ] } - } - } - } } + select(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "prop5" } } } + rhs: { const: { list: [{ i64: 1 }, { i64: 2 }, { i64: 3 }] } } + } + } ) { list { name } } @@ -92,14 +88,12 @@ def test_node_property_filter_equal_type_error(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop5" - where: { - eq: { i64: 1 } - } - } - } } + select(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "prop5" } } } + rhs: { const: { i64: 1 } } + } + } ) { list { name @@ -121,14 +115,12 @@ def test_node_property_filter_not_equal(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop4" - where: { - ne: { bool: true } - } - } - } } + select(expr: { + ne: { + lhs: { read: { entity: NODE, target: { property: "prop4" } } } + rhs: { const: { bool: true } } + } + } ) { list { name @@ -150,14 +142,12 @@ def test_node_property_filter_not_equal_type_error(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop4" - where: { - ne: { i64: 1 } - } - } - } } + select(expr: { + ne: { + lhs: { read: { entity: NODE, target: { property: "prop4" } } } + rhs: { const: { i64: 1 } } + } + } ) { list { name @@ -179,14 +169,12 @@ def test_node_property_filter_greater_than_or_equal(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { - ge: { i64: 60 } - } - } - } } + select(expr: { + ge: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 60 } } + } + } ) { list { name @@ -206,14 +194,12 @@ def test_node_property_filter_greater_than_or_equal_type_error(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { - ge: { str: "shivam" } - } - } - } } + select(expr: { + ge: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { str: "shivam" } } + } + } ) { list { name @@ -223,9 +209,7 @@ def test_node_property_filter_greater_than_or_equal_type_error(graph): } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -235,14 +219,12 @@ def test_node_property_filter_less_than_or_equal(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { - le: { i64: 30 } - } - } - } } + select(expr: { + le: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 30 } } + } + } ) { list { name @@ -266,12 +248,12 @@ def test_node_property_filter_less_than_or_equal_type_error(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { le: { str: "shivam" } } - } - } } + select(expr: { + le: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { str: "shivam" } } + } + } ) { list { name } } @@ -279,9 +261,7 @@ def test_node_property_filter_less_than_or_equal_type_error(graph): } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -291,12 +271,12 @@ def test_node_property_filter_greater_than(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { gt: { i64: 30 } } - } - } } + select(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 30 } } + } + } ) { list { name } } @@ -314,12 +294,12 @@ def test_node_property_filter_greater_than_type_error(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { gt: { str: "shivam" } } - } - } } + select(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { str: "shivam" } } + } + } ) { list { name } } @@ -327,9 +307,7 @@ def test_node_property_filter_greater_than_type_error(graph): } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -339,12 +317,12 @@ def test_node_property_filter_less_than(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { lt: { i64: 30 } } - } - } } + select(expr: { + lt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { i64: 30 } } + } + } ) { list { name } } @@ -364,12 +342,12 @@ def test_node_property_filter_less_than_type_error(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { lt: { str: "shivam" } } - } - } } + select(expr: { + lt: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { str: "shivam" } } + } + } ) { list { name } } @@ -377,9 +355,7 @@ def test_node_property_filter_less_than_type_error(graph): } } """ - expected_error_message = ( - "Invalid filter: value Str(ArcStr(\"shivam\")) of type Str cannot be coerced to I64" - ) + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' run_graphql_error_test(query, expected_error_message, graph) @@ -389,12 +365,7 @@ def test_node_property_filter_is_none(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop5" - where: { isNone: true } - } - } } + select(expr: { isNone: { read: { entity: NODE, target: { property: "prop5" } } } } ) { list { name } } @@ -414,12 +385,7 @@ def test_node_property_filter_is_some(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop5" - where: { isSome: true } - } - } } + select(expr: { isSome: { read: { entity: NODE, target: { property: "prop5" } } } } ) { list { name } } @@ -439,12 +405,12 @@ def test_node_property_filter_is_in(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { isIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } - } - } } + select(expr: { + isIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [{ i64: 10 }, { i64: 30 }, { i64: 50 }, { i64: 70 }] } + } + } ) { list { name } } @@ -464,12 +430,12 @@ def test_node_property_filter_is_in_empty_list(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { isIn: { list: [] } } - } - } } + select(expr: { + isIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [] } + } + } ) { list { name } } @@ -488,12 +454,12 @@ def test_node_property_filter_is_in_no_value(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { isIn: { list: [{i64: 100}] } } - } - } } + select(expr: { + isIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [{ i64: 100 }] } + } + } ) { list { name } } @@ -511,12 +477,12 @@ def test_node_property_filter_is_in_type_error(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { isIn: { str: "shivam" } } - } - } } + select(expr: { + isIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { str: "shivam" } + } + } ) { list { name } } @@ -536,12 +502,12 @@ def test_node_property_filter_is_not_in(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { isNotIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } - } - } } + select(expr: { + isNotIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [{ i64: 10 }, { i64: 30 }, { i64: 50 }, { i64: 70 }] } + } + } ) { list { name } } @@ -561,12 +527,12 @@ def test_node_property_filter_is_not_in_empty_list(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { isNotIn: { list: [] } } - } - } } + select(expr: { + isNotIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { list: [] } + } + } ) { list { name } } @@ -592,12 +558,12 @@ def test_node_property_filter_is_not_in_type_error(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop1" - where: { isNotIn: { str: "shivam" } } - } - } } + select(expr: { + isNotIn: { + expr: { read: { entity: NODE, target: { property: "prop1" } } } + values: { str: "shivam" } + } + } ) { list { name } } @@ -616,12 +582,12 @@ def test_node_property_filter_contains_wrong_value_type_error(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "p10" - where: { contains: { u64: 2 } } + filterNodes: filter(expr: { + contains: { + lhs: { read: { entity: NODE, target: { property: "p10" } } } + rhs: { const: { u64: 2 } } } - } }) { + }) { nodes { list { name } } @@ -639,12 +605,12 @@ def test_nodes_property_filter_starts_with(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop3" - where: { startsWith: { str: "abc" } } + select(expr: { + startsWith: { + lhs: { read: { entity: NODE, target: { property: "prop3" } } } + rhs: { const: { str: "abc" } } } - } }) { + }) { list { name } } } @@ -669,12 +635,12 @@ def test_nodes_property_filter_ends_with(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - property: { - name: "prop3" - where: { endsWith: { str: "333" } } + select(expr: { + endsWith: { + lhs: { read: { entity: NODE, target: { property: "prop3" } } } + rhs: { const: { str: "333" } } } - } }) { + }) { list { name } } } @@ -691,12 +657,14 @@ def test_nodes_property_filter_temporal_first_starts_with(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - temporalProperty: { - name: "prop3" - where: { first: { startsWith: { str: "abc" } } } + select(expr: { + startsWith: { + lhs: { + first: { temporal: { read: { entity: NODE, target: { property: "prop3" } } } } + } + rhs: { const: { str: "abc" } } } - } }) { + }) { list { name } } } @@ -721,12 +689,14 @@ def test_nodes_property_filter_temporal_all_starts_with(graph): query { graph(path: "g") { nodes { - select(expr: { node: { - temporalProperty: { - name: "prop3" - where: { any: { startsWith: { str: "abc1" } } } + select(expr: { + startsWith: { + lhs: { + any: { temporal: { read: { entity: NODE, target: { property: "prop3" } } } } + } + rhs: { const: { str: "abc1" } } } - } }) { + }) { list { name } } } @@ -743,12 +713,12 @@ def test_nodes_property_filter_list_agg(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop5" - where: { sum: { eq: { i64: 6 } } } + filterNodes: filter(expr: { + eq: { + lhs: { sum: { read: { entity: NODE, target: { property: "prop5" } } } } + rhs: { const: { i64: 6 } } } - } }) { + }) { nodes { list { name } } } } @@ -763,12 +733,12 @@ def test_nodes_property_filter_list_qualifier(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "prop5" - where: { any: { eq: { i64: 6 } } } + filterNodes: filter(expr: { + eq: { + lhs: { any: { read: { entity: NODE, target: { property: "prop5" } } } } + rhs: { const: { i64: 6 } } } - } }) { + }) { nodes { list { name } } } } @@ -787,12 +757,14 @@ def test_nodes_temporal_property_filter_agg(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - temporalProperty: { - name: "p2" - where: { avg: { lt: { f64: 10.0 } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { temporal: { read: { entity: NODE, target: { property: "p2" } } } } + } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -814,12 +786,16 @@ def test_nodes_temporal_property_filter_any_avg(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { temporal: { read: { entity: NODE, target: { property: "prop5" } } } } + } + } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -840,7 +816,12 @@ def test_nodes_neighbours_selection_with_prop_filter(graph): query = """ query { graph(path: "g") { - nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + nodes(select: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p100" } } } + rhs: { const: { i64: 30 } } + } + }) { list { name neighbours { @@ -879,7 +860,12 @@ def test_nodes_selection(graph): query = """ query { graph(path: "g") { - nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + nodes(select: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p100" } } } + rhs: { const: { i64: 30 } } + } + }) { list { name } @@ -897,10 +883,18 @@ def test_nodes_selection_nodes_filter_paired(graph): query = """ query { graph(path: "g") { - nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { - filter(expr: { node: { - property: { name: "p9", where: { eq:{ i64: 5 } } } - } }) { + nodes(select: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p100" } } } + rhs: { const: { i64: 30 } } + } + }) { + filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "p9" } } } + rhs: { const: { i64: 5 } } + } + }) { list { name } @@ -921,10 +915,18 @@ def test_nodes_selection_nodes_filter_paired2(graph): query = """ query { graph(path: "g") { - nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { - filter(expr: { node: { - property: { name: "p9", where: { eq:{ i64: 5 } } } - } }) { + nodes(select: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p100" } } } + rhs: { const: { i64: 30 } } + } + }) { + filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "p9" } } } + rhs: { const: { i64: 5 } } + } + }) { list { neighbours { list { @@ -957,13 +959,24 @@ def test_nodes_chained_selection_node_filter_paired(graph): query = """ query { graph(path: "g") { - nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { - select(expr: { node: { property: { name: "p9", where: { eq:{ i64: 5 } } } } }) { - filter(expr: { node: { - nodeType: { - where: { eq: { str: "fire_nation" } } + nodes(select: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p100" } } } + rhs: { const: { i64: 30 } } + } + }) { + select(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "p9" } } } + rhs: { const: { i64: 5 } } + } + }) { + filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { field: NODE_TYPE } } } + rhs: { const: { str: "fire_nation" } } } - } }) { + }) { list { name } @@ -985,13 +998,24 @@ def test_nodes_chained_selection_node_filter_paired_ver2(graph): query { graph(path: "g") { nodes { - select(expr: { node: { property: { name: "p100", where: { gt: { i64: 30 } } } } }) { - select(expr: { node: { property: { name: "p9", where: { eq:{ i64: 5 } } } } }) { - filter(expr: { node: { - nodeType: { - where: { eq: { str: "fire_nation" } } + select(expr: { + gt: { + lhs: { read: { entity: NODE, target: { property: "p100" } } } + rhs: { const: { i64: 30 } } + } + }) { + select(expr: { + eq: { + lhs: { read: { entity: NODE, target: { property: "p9" } } } + rhs: { const: { i64: 5 } } + } + }) { + filter(expr: { + eq: { + lhs: { read: { entity: NODE, target: { field: NODE_TYPE } } } + rhs: { const: { str: "fire_nation" } } } - } }) { + }) { list { name } @@ -1019,18 +1043,24 @@ def test_nodes_temporal_property_filter_any_avg_with_window(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - window: { - start: 1 - end: 3 - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { + entity: NODE + target: { property: "prop5" } + views: [{ window: { start: 1, end: 3 } }] + } + } + } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -1048,17 +1078,24 @@ def test_node_property_layer_filter_not_supported(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - layers: { - names: ["air_nomads"] - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { + entity: NODE + target: { property: "prop5" } + views: [{ layers: ["air_nomads"] }] + } + } + } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -1078,17 +1115,20 @@ def test_nodes_at_temporal_property(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - at: { - time: 2 - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { entity: NODE, target: { property: "prop5" }, views: [{ at: 2 }] } + } + } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -1103,17 +1143,20 @@ def test_nodes_before_temporal_property(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - before: { - time: 3 - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { entity: NODE, target: { property: "prop5" }, views: [{ before: 3 }] } + } + } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -1130,17 +1173,20 @@ def test_nodes_after_temporal_property(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - after: { - time: 2 - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { entity: NODE, target: { property: "prop5" }, views: [{ after: 2 }] } + } + } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -1157,16 +1203,24 @@ def test_nodes_latest_temporal_property(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - latest: { - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { + entity: NODE + target: { property: "prop5" } + views: [{ latest: true }] + } + } + } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -1181,17 +1235,24 @@ def test_nodes_snapshot_at_temporal_property(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - snapshotAt: { - time: 2 - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { + entity: NODE + target: { property: "prop5" } + views: [{ snapshotAt: 2 }] + } + } + } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -1208,16 +1269,24 @@ def test_nodes_snapshot_latest_temporal_property(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - snapshotLatest: { - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { + entity: NODE + target: { property: "prop5" } + views: [{ snapshotLatest: true }] + } + } + } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -1229,26 +1298,32 @@ def test_nodes_snapshot_latest_temporal_property(graph): run_graphql_test(query, expected, graph) +# Both orders give the same answer on this fixture; the pair pins that either order is +# accepted on a read. Order itself is pinned by `window_then_latest` in +# test_filter_expr_grammar.py. @pytest.mark.parametrize("graph", [EVENT_GRAPH]) def test_nodes_layer_then_latest(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - latest: { - expr: { - layers: { - names: ["_default"] - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { + entity: NODE + target: { property: "prop5" } + views: [{ layers: ["_default"] }, { latest: true }] + } } } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } @@ -1263,21 +1338,24 @@ def test_nodes_latest_then_layer(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { node: { - layers: { - names: ["_default"] - expr: { - latest: { - expr: { - temporalProperty: { - name: "prop5" - where: { any: { avg: { lt: { f64: 10.0 } } } } + filterNodes: filter(expr: { + lt: { + lhs: { + avg: { + any: { + temporal: { + read: { + entity: NODE + target: { property: "prop5" } + views: [{ latest: true }, { layers: ["_default"] }] + } } } } } + rhs: { const: { f64: 10.0 } } } - } }) { + }) { nodes { list { name } } } } diff --git a/python/tests/test_base_install/test_graphql/test_gql_history.py b/python/tests/test_base_install/test_graphql/test_gql_history.py index 8b8746d245..dfe633890b 100644 --- a/python/tests/test_base_install/test_graphql/test_gql_history.py +++ b/python/tests/test_base_install/test_graphql/test_gql_history.py @@ -243,16 +243,12 @@ def test_history(): query_1 = """ { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "weight" - where: { - eq: { - f64: 0.9 - } - } - } - } }) { + filterEdges: filter(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { property: "weight" } } } + rhs: { const: { f64: 0.9 } } + } + }) { edge(src: "Dumbledore", dst: "Harry") { history { timestamps { @@ -276,16 +272,12 @@ def test_history(): query_2 = """ { graph(path: "g") { - filterEdges: filter(expr: { edge: { - property: { - name: "weight" - where: { - eq: { - f64: 0.7 - } - } - } - } }) { + filterEdges: filter(expr: { + eq: { + lhs: { read: { entity: EDGE, target: { property: "weight" } } } + rhs: { const: { f64: 0.7 } } + } + }) { edge(src: "Dumbledore", dst: "Harry") { history { timestamps { @@ -303,16 +295,12 @@ def test_history(): query_1 = """ { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "Age" - where: { - lt: { - i64: 51 - } - } - } - } }) { + filterNodes: filter(expr: { + lt: { + lhs: { read: { entity: NODE, target: { property: "Age" } } } + rhs: { const: { i64: 51 } } + } + }) { node(name: "Dumbledore") { history { timestamps { @@ -329,16 +317,12 @@ def test_history(): query_2 = """ { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "Age" - where: { - ge: { - i64: 51 - } - } - } - } }) { + filterNodes: filter(expr: { + ge: { + lhs: { read: { entity: NODE, target: { property: "Age" } } } + rhs: { const: { i64: 51 } } + } + }) { node(name: "Dumbledore") { history { timestamps { @@ -361,16 +345,12 @@ def test_history(): query_3 = """ { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "Age" - where: { - lt: { - i64: 21 - } - } - } - } }) { + filterNodes: filter(expr: { + lt: { + lhs: { read: { entity: NODE, target: { property: "Age" } } } + rhs: { const: { i64: 21 } } + } + }) { node(name: "Harry") { history { timestamps { @@ -387,16 +367,12 @@ def test_history(): query_4 = """ { graph(path: "g") { - filterNodes: filter(expr: { node: { - property: { - name: "Age" - where: { - ge: { - i64: 21 - } - } - } - } }) { + filterNodes: filter(expr: { + ge: { + lhs: { read: { entity: NODE, target: { property: "Age" } } } + rhs: { const: { i64: 21 } } + } + }) { node(name: "Harry") { history { timestamps { diff --git a/python/tests/test_base_install/test_graphql/test_nodes.py b/python/tests/test_base_install/test_graphql/test_nodes.py index 967c7a6c0e..bc2081bfda 100644 --- a/python/tests/test_base_install/test_graphql/test_nodes.py +++ b/python/tests/test_base_install/test_graphql/test_nodes.py @@ -13,8 +13,9 @@ def test_graph_node_sort_by_nothing(graph): query = """{ graph(path: "g") { nodes(select: { - id: { - where: { eq: { u64: 1 } } + eq: { + lhs: { read: { entity: NODE, target: { field: ID } } } + rhs: { const: { u64: 1 } } } }) { list { diff --git a/python/tests/test_base_install/test_repr.py b/python/tests/test_base_install/test_repr.py index 5cbd71cc27..7bff9a43b0 100644 --- a/python/tests/test_base_install/test_repr.py +++ b/python/tests/test_base_install/test_repr.py @@ -66,12 +66,12 @@ def test_persistent_graph(self): class FilterExprReprTest(TestCase): - """`repr` shows the wire form a filter carries, so what runs locally and what - would be sent to a server can be read off the object.""" + """`repr` shows the filter tree, which is both what runs locally and what a + server receives.""" def test_repr_shows_the_recorded_wire_form(self): expr = filter.Node.window(0, 5).property("score") > 4 - self.assertEqual(repr(expr), "FilterExpr(WINDOW[0..5](score > 4))") + self.assertEqual(repr(expr), "FilterExpr(WINDOW[0..5](score) > 4)") def test_repr_shows_temporal_ops_and_combinators(self): expr = (filter.Node.property("score").temporal().sum() > 10) & ~( @@ -79,9 +79,9 @@ def test_repr_shows_temporal_ops_and_combinators(self): ) self.assertEqual( repr(expr), - "FilterExpr((sum(temporal(score)) > 10 AND NOT(node_name == carol)))", + "FilterExpr((sum(temporal(score)) > 10 AND NOT(name == carol)))", ) - def test_repr_of_a_local_only_filter_says_so(self): + def test_repr_shows_expressions_on_both_sides(self): expr = filter.Node.degree() > filter.Node.in_degree() - self.assertEqual(repr(expr), "FilterExpr()") + self.assertEqual(repr(expr), "FilterExpr(degree > in_degree)") diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 0f3c0e518d..ac6f362b7a 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -75,9 +75,9 @@ type Algorithms { """ inComponents( """ - Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view. + Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view. """ - filter: GqlFilter, + filter: FilterExpr, """ Number of threads to use. Defaults to all available. """ @@ -88,9 +88,9 @@ type Algorithms { """ outComponents( """ - Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view. + Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view. """ - filter: GqlFilter, + filter: FilterExpr, """ Number of threads to use. Defaults to all available. """ @@ -105,9 +105,9 @@ type Algorithms { """ node: NodeId!, """ - Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view. + Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view. """ - filter: GqlFilter + filter: FilterExpr ): NodeState! """ Returns the out component of a single node (nodes it can reach, with their distance). @@ -118,9 +118,9 @@ type Algorithms { """ node: NodeId!, """ - Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view. + Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view. """ - filter: GqlFilter + filter: FilterExpr ): NodeState! """ Returns the local triangle count of a single node (0 if it has degree < 2), or null if @@ -522,6 +522,14 @@ enum AlignmentUnit { YEAR } +""" +Two expressions to compare. +""" +input Cmp { + lhs: Expr! + rhs: Expr! +} + """ Collection of items """ @@ -625,19 +633,7 @@ type CollectionOfNamespacedItem { } """ -Filters nodes by computed degree with a directional scope. - -`DegreeFilterNew` lets callers filter on: -- inbound degree (`IN`), -- outbound degree (`OUT`), -- or total degree (`BOTH`). - -The selected degree is compared using the `where` condition. - -Example (GraphQL): -```graphql -{ Degree: { direction: BOTH, where: { Gt: 10 } } } -``` +The direction a node degree counts. """ enum DegreeDirection { IN @@ -645,11 +641,6 @@ enum DegreeDirection { BOTH } -input DegreeFilterNew { - direction: DegreeDirection! - where: PropCondition! -} - """ Edge direction to follow during traversal. """ @@ -1013,7 +1004,7 @@ type Edge { """ Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). """ - expr: GqlFilter! + expr: FilterExpr! ): Edge! """ Pin this edge to a single event — the exploded instance recorded at @@ -1069,187 +1060,6 @@ input EdgeAddition { updates: [TemporalPropertyInput!] } -""" -GraphQL input type for filtering edges. - -`EdgeFilter` represents a composable boolean expression evaluated -against edges in a graph. Filters can target: - -- edge **endpoints** (source / destination nodes), -- edge **properties** and **metadata**, -- **temporal scope** (windows, snapshots, latest), -- **layer membership**, -- and **structural edge state** (active, valid, deleted, self-loop). - -Filters can be combined recursively using logical operators -(`And`, `Or`, `Not`). - -Examples (GraphQL): -```graphql -{ -edges(filter: { -And: [ -{ IsActive: true }, -{ Property: { name: "weight", gt: 0.5 } } -] -}) { -src -dst -} -} -``` -""" -input EdgeFilter @oneOf { - """ - Applies a filter to the **source node** of the edge. - - The nested `NodeFilter` is evaluated against the source endpoint. - - Example: - `{ Src: { Name: { contains: "alice" } } }` - """ - src: NodeFilter - """ - Applies a filter to the **destination node** of the edge. - - The nested `NodeFilter` is evaluated against the destination endpoint. - - Example: - `{ Dst: { Id: { eq: 42 } } }` - """ - dst: NodeFilter - """ - Filters an edge **property** by name and value. - - Applies to static or temporal properties depending on context. - - Example: - `{ Property: { name: "weight", gt: 0.5 } }` - """ - property: PropertyFilterNew - """ - Filters an edge **metadata field**. - - Metadata is shared across all temporal versions of an edge. - - Example: - `{ Metadata: { name: "source", eq: "imported" } }` - """ - metadata: PropertyFilterNew - """ - Filters a **temporal edge property**. - - Used when the property value varies over time and must be - evaluated within a temporal context. - - Example: - `{ TemporalProperty: { name: "status", eq: "active" } }` - """ - temporalProperty: PropertyFilterNew - """ - Logical **AND** over multiple edge filters. - - All nested filters must evaluate to `true`. - - Example: - `{ And: [ { IsActive: true }, { IsValid: true } ] }` - """ - and: [EdgeFilter!] - """ - Logical **OR** over multiple edge filters. - - At least one nested filter must evaluate to `true`. - - Example: - `{ Or: [ { IsDeleted: true }, { IsSelfLoop: true } ] }` - """ - or: [EdgeFilter!] - """ - Logical **NOT** over a nested edge filter. - - Negates the result of the wrapped filter. - - Example: - `{ Not: { IsDeleted: true } }` - """ - not: EdgeFilter - """ - Restricts edge evaluation to a **time window**. - - The window is inclusive of `start` and exclusive of `end`. - """ - window: EdgeWindowExpr - """ - Restricts edge evaluation to a **single point in time**. - """ - at: EdgeTimeExpr - """ - Restricts edge evaluation to times **strictly before** a given time. - """ - before: EdgeTimeExpr - """ - Restricts edge evaluation to times **strictly after** a given time. - """ - after: EdgeTimeExpr - """ - Evaluates edge predicates against the **latest available state**. - """ - latest: EdgeUnaryExpr - """ - Evaluates edge predicates against a **snapshot** of the graph - at a specific time. - """ - snapshotAt: EdgeTimeExpr - """ - Evaluates edge predicates against the **most recent snapshot** - of the graph. - """ - snapshotLatest: EdgeUnaryExpr - """ - Restricts evaluation to edges belonging to one or more **layers**. - - Example: - `{ Layers: { values: ["fire_nation", "air_nomads"] } }` - """ - layers: EdgeLayersExpr - """ - Matches edges that have at least one event in the current view/window. - - When `true`, only active edges are matched. - """ - isActive: Boolean - """ - Matches edges that are structurally valid (i.e. not deleted) - in the current view/window. - """ - isValid: Boolean - """ - Matches edges that have been deleted in the current view/window. - """ - isDeleted: Boolean - """ - Matches edges that are **self-loops** - (source node == destination node). - """ - isSelfLoop: Boolean -} - -""" -Restricts edge evaluation to one or more layers and applies a nested `EdgeFilter`. - -Used by `GqlEdgeFilter::Layers`. -""" -input EdgeLayersExpr { - """ - Layer names to include. - """ - names: [String!]! - """ - Filter evaluated within the layer-restricted view. - """ - expr: EdgeFilter! -} - """ Describes edges between a specific pair of node types — the property and metadata keys seen on such edges, along with their observed value types. @@ -1305,34 +1115,6 @@ input EdgeSortBy { property: String } -""" -Restricts edge evaluation to a single time bound and applies a nested `EdgeFilter`. - -Used by `At`, `Before`, and `After` edge filters. -""" -input EdgeTimeExpr { - """ - Reference time for the operation. - """ - time: TimeInput! - """ - Filter evaluated within the restricted time scope. - """ - expr: EdgeFilter! -} - -""" -Applies a unary edge-view operation and then evaluates a nested `EdgeFilter`. - -Used by `Latest` and `SnapshotLatest` edge filters. -""" -input EdgeUnaryExpr { - """ - Filter evaluated after applying the unary operation. - """ - expr: EdgeFilter! -} - input EdgeViewCollection @oneOf { """ Contains only the default layer. @@ -1387,31 +1169,9 @@ input EdgeViewCollection @oneOf { """ shrinkEnd: TimeInput """ - Edge filter - """ - edgeFilter: EdgeFilter -} - -""" -Restricts edge evaluation to a time window and applies a nested `EdgeFilter`. - -Used by `GqlEdgeFilter::Window`. - -The window is inclusive of `start` and exclusive of `end`. -""" -input EdgeWindowExpr { - """ - Window start time (inclusive). - """ - start: TimeInput! + A filter tree; the entity it tests is written in the tree. """ - Window end time (exclusive). - """ - end: TimeInput! - """ - Filter evaluated within the restricted window. - """ - expr: EdgeFilter! + filter: FilterExpr } """ @@ -1742,7 +1502,7 @@ type Edges { """ Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). """ - expr: GqlFilter! + expr: FilterExpr! ): Edges! """ Narrow the collection to edges matching `expr`, but only at this step — @@ -1768,7 +1528,7 @@ type Edges { """ Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). """ - expr: GqlFilter! + expr: FilterExpr! ): Edges! } @@ -1826,9 +1586,9 @@ input EdgesViewCollection @oneOf { """ shrinkEnd: TimeInput """ - Edge filter + A filter tree; the entity it tests is written in the tree. """ - edgeFilter: EdgeFilter + filter: FilterExpr } """ @@ -1877,6 +1637,23 @@ input EmbeddingModel @oneOf { openAI: OpenAIConfig } +""" +Which end of an edge a read looks at. +""" +enum Endpoint { + SRC + DST +} + +""" +The kind of thing a filter tests: a node, an edge, or one edge update. +""" +enum Entity { + NODE + EDGE + EXPLODED_EDGE +} + """ Raphtory’s EventTime. Represents a unique timepoint in the graph’s history as (timestamp, event_id). @@ -1914,187 +1691,79 @@ type EventTime { } """ -GraphQL input type for filtering **exploded edges** — edge views where each -temporal event is an individually addressable edge instance, rather than -one aggregated edge across time. - -Predicates are evaluated **per event**: a property condition keeps the -individual updates that match it (and the edges carrying them), where the -plain `EdgeFilter` evaluates one aggregated value per edge. - -Filters can target edge endpoints, properties/metadata, temporal scope, -layer membership, and structural edge state, and can be combined -recursively with `And`/`Or`/`Not` — mirroring `EdgeFilter`. +A value: what stands on either side of a comparison. """ -input ExplodedEdgeFilter @oneOf { - """ - Applies a filter to the **source node** of the exploded edge. - """ - src: NodeFilter - """ - Applies a filter to the **destination node** of the exploded edge. - """ - dst: NodeFilter - """ - Filters an exploded-edge **property** by name and value, evaluated - per event. - - Example: - `{ Property: { name: "weight", gt: 0.5 } }` - """ - property: PropertyFilterNew - """ - Filters an exploded-edge **metadata field**. - - Metadata is shared across all temporal versions of an edge. - """ - metadata: PropertyFilterNew - """ - Filters a **temporal exploded-edge property**, evaluated within a - temporal context per event. - """ - temporalProperty: PropertyFilterNew - """ - Logical **AND** over multiple exploded-edge filters. - """ - and: [ExplodedEdgeFilter!] - """ - Logical **OR** over multiple exploded-edge filters. - """ - or: [ExplodedEdgeFilter!] - """ - Logical **NOT** over a nested exploded-edge filter. - """ - not: ExplodedEdgeFilter - """ - Restricts exploded-edge evaluation to a **time window** - (inclusive start, exclusive end). - """ - window: ExplodedEdgeWindowExpr - """ - Restricts exploded-edge evaluation to a **single point in time**. - """ - at: ExplodedEdgeTimeExpr - """ - Restricts exploded-edge evaluation to times **strictly before** a - given time. - """ - before: ExplodedEdgeTimeExpr +input Expr @oneOf { """ - Restricts exploded-edge evaluation to times **strictly after** a - given time. + A literal. """ - after: ExplodedEdgeTimeExpr + const: Value """ - Evaluates exploded-edge predicates against the **latest available - state**. + A field, degree, property or metadata read from an entity. """ - latest: ExplodedEdgeUnaryExpr + read: Read """ - Evaluates exploded-edge predicates against a **snapshot** of the graph - at a specific time. + The full history of a property instead of its latest value. """ - snapshotAt: ExplodedEdgeTimeExpr + temporal: Expr + sum: Expr + avg: Expr + min: Expr + max: Expr + first: Expr + last: Expr + len: Expr """ - Evaluates exploded-edge predicates against the **most recent - snapshot** of the graph. + The predicate holds if it holds for any element. """ - snapshotLatest: ExplodedEdgeUnaryExpr + any: Expr """ - Restricts evaluation to exploded edges belonging to one or more - **layers**. + The predicate holds if it holds for every element. """ - layers: ExplodedEdgeLayersExpr - """ - Matches exploded edges that have at least one event in the current - view/window. - """ - isActive: Boolean - """ - Matches exploded edges that are structurally valid (i.e. not deleted) - in the current view/window. - """ - isValid: Boolean - """ - Matches exploded edges that have been deleted in the current - view/window. - """ - isDeleted: Boolean - """ - Matches exploded edges that are **self-loops** - (source node == destination node). - """ - isSelfLoop: Boolean -} - -""" -Restricts exploded-edge evaluation to one or more layers and applies a -nested `ExplodedEdgeFilter`. - -Used by `GqlExplodedEdgeFilter::Layers`. -""" -input ExplodedEdgeLayersExpr { - """ - Layer names to include. - """ - names: [String!]! - """ - Filter evaluated within the layer-restricted view. - """ - expr: ExplodedEdgeFilter! + all: Expr } """ -Restricts exploded-edge evaluation to a single time bound and applies a -nested `ExplodedEdgeFilter`. - -Used by `At`, `Before`, `After`, and `SnapshotAt` exploded-edge filters. -""" -input ExplodedEdgeTimeExpr { - """ - Reference time for the operation. - """ - time: TimeInput! - """ - Filter evaluated within the restricted time scope. - """ - expr: ExplodedEdgeFilter! +The filter itself: a yes/no over an entity. +""" +input FilterExpr @oneOf { + eq: Cmp + ne: Cmp + lt: Cmp + le: Cmp + gt: Cmp + ge: Cmp + startsWith: Cmp + endsWith: Cmp + contains: Cmp + notContains: Cmp + fuzzySearch: FuzzyCmp + isSome: Expr + isNone: Expr + isIn: Membership + isNotIn: Membership + isActive: Scope + isValid: Scope + isDeleted: Scope + isSelfLoop: Scope + """ + A graph-level view with no predicate: the result is the view. + """ + view: [ViewOp!] + and: [FilterExpr!] + or: [FilterExpr!] + not: FilterExpr } """ -Applies a unary edge-view operation and then evaluates a nested -`ExplodedEdgeFilter`. - -Used by `Latest` and `SnapshotLatest` exploded-edge filters. -""" -input ExplodedEdgeUnaryExpr { - """ - Filter evaluated after applying the unary operation. - """ - expr: ExplodedEdgeFilter! -} - +A fuzzy string match: `lhs` is within `levenshteinDistance` edits of +`rhs`, optionally matching by prefix. """ -Restricts exploded-edge evaluation to a time window and applies a nested -`ExplodedEdgeFilter`. - -Used by `GqlExplodedEdgeFilter::Window`. - -The window is inclusive of `start` and exclusive of `end`. -""" -input ExplodedEdgeWindowExpr { - """ - Window start time (inclusive). - """ - start: TimeInput! - """ - Window end time (exclusive). - """ - end: TimeInput! - """ - Filter evaluated within the restricted window. - """ - expr: ExplodedEdgeFilter! +input FuzzyCmp { + lhs: Expr! + rhs: Expr! + levenshteinDistance: Int! + prefixMatch: Boolean! } """ @@ -2117,81 +1786,6 @@ input FuzzySearchExpr { prefixMatch: Boolean! } -""" -A general filter expression — a node filter (`node`), an edge filter (`edge`), a graph/view -filter (`graph`, e.g. a layer or window restriction), or an `and`/`or` combination of these -(which may mix kinds). Used where an operation accepts any filter, such as scoping a component -walk. -""" -input GqlFilter @oneOf { - """ - Filter by node properties, fields, or temporal state. - """ - node: NodeFilter - """ - Filter by edge properties, source/destination, or temporal state. - (Persisted filters may use the legacy `edge` key.) - """ - edge: EdgeFilter - """ - Filter exploded edges — per-event edge instances — by properties, - endpoints, or temporal state, evaluated per event. - """ - explodedEdge: ExplodedEdgeFilter - """ - Apply a graph-level view (window, snapshot, layer restriction, …). - """ - graph: GraphFilter - """ - All sub-filters must pass (intersection). - """ - and: [GqlFilter!] - """ - At least one sub-filter must pass (union). - Cross-type sub-filters (e.g. `node` and `edge` together) produce a - proper graph union: a node is visible if it matches the node filter or - has a visible edge, and an edge is visible if it matches the edge - filter or both its endpoints are visible. - """ - or: [GqlFilter!] - """ - Inverts the nested filter. - """ - not: GqlFilter - """ - Restrict evaluation to a time window (inclusive start, exclusive end). - """ - window: GraphWindowExpr - """ - Restrict evaluation to a single point in time. - """ - at: GraphTimeExpr - """ - Restrict evaluation to times strictly before the given time. - """ - before: GraphTimeExpr - """ - Restrict evaluation to times strictly after the given time. - """ - after: GraphTimeExpr - """ - Evaluate against the latest available state. - """ - latest: GraphUnaryExpr - """ - Evaluate against a snapshot of the graph at a given time. - """ - snapshotAt: GraphTimeExpr - """ - Evaluate against the most recent snapshot of the graph. - """ - snapshotLatest: GraphUnaryExpr - """ - Restrict evaluation to one or more layers. - """ - layers: GraphLayersExpr -} - """ A view of a Raphtory graph. Every field here returns either data from the view or a derived view (`window`, `layer`, `at`, `filter`, ...) that you can @@ -2528,9 +2122,9 @@ type Graph { """ nodes( """ - Optional node filter (by name, property, type, etc.). If omitted, every node in the view is returned. + Optional filter expression made of node predicates, graph views, or and/or/not combinations (and is an intersection). Expressions that test edges are rejected. If omitted, every node in the view is returned. """ - select: NodeFilter + select: FilterExpr ): Nodes! """ Look up a single edge by its endpoint ids. Returns null if no edge exists @@ -2551,9 +2145,9 @@ type Graph { """ edges( """ - Optional edge filter (by property, layer, src/dst, etc.). If omitted, every edge in the view is returned. + Optional filter expression made of edge predicates (including src/dst reads), graph views, or and/or/not combinations (and is an intersection). If omitted, every edge in the view is returned. """ - select: EdgeFilter + select: FilterExpr ): Edges! """ Returns the properties of the graph. @@ -2615,9 +2209,9 @@ type Graph { """ filter( """ - Optional filter expression: node/edge predicates, graph views (window, layer, ...), or and/or/not combinations of them. `and` is an intersection: each leg is evaluated independently and the results intersect — to evaluate a predicate *inside* a view, scope the predicate itself (e.g. a windowed property condition). If omitted, applies the identity filter. + Optional filter expression made of node/edge predicates, graph views (window, layer, ...), or and/or/not combinations of them. `and` is an intersection, each leg evaluated independently and the results intersected. A `view` leg applies first and the other legs run inside it, like `graph.window(..).filter(expr)`; it must stand alone or in the top-level `and` (not under `or` or `not`). If omitted, applies the identity filter. """ - expr: GqlFilter + expr: FilterExpr ): Graph! """ Apply a list of view operations in the given order and return the @@ -2632,95 +2226,11 @@ type Graph { ): Graph! } -""" -GraphQL input type for restricting a graph view. - -`GraphFilter` controls the **evaluation scope** for subsequent node/edge filters: -- time windows (`Window`) -- time points (`At`) -- open-ended ranges (`Before`, `After`) -- latest evaluation (`Latest`) -- snapshots (`SnapshotAt`, `SnapshotLatest`) -- layer membership (`Layers`) - -These filters can be nested via the `expr` field on the corresponding -`*Expr` input objects to form pipelines. -""" -input GraphFilter @oneOf { - """ - Restrict evaluation to a time window (inclusive start, exclusive end). - """ - window: GraphWindowExpr - """ - Restrict evaluation to a single point in time. - """ - at: GraphTimeExpr - """ - Restrict evaluation to times strictly before the given time. - """ - before: GraphTimeExpr - """ - Restrict evaluation to times strictly after the given time. - """ - after: GraphTimeExpr - """ - Evaluate against the latest available state. - """ - latest: GraphUnaryExpr - """ - Evaluate against a snapshot of the graph at a given time. - """ - snapshotAt: GraphTimeExpr - """ - Evaluate against the most recent snapshot of the graph. - """ - snapshotLatest: GraphUnaryExpr - """ - Restrict evaluation to one or more layers. - """ - layers: GraphLayersExpr -} - -""" -Graph view restriction by layer membership, optionally chaining another `GraphFilter`. - -Used by `GqlGraphFilter::Layers`. -""" -input GraphLayersExpr { - """ - Layer names to include. - """ - names: [String!]! - """ - Optional nested filter applied after the layer restriction. - """ - expr: GraphFilter -} - type GraphSchema { nodes: [NodeSchema!]! layers: [LayerSchema!]! } -""" -Graph view restriction to a single time bound, optionally chaining another `GraphFilter`. - -Used by `At`, `Before`, and `After` graph filters. - -Example: -`{ At: { time: 5, expr: { Layers: { names: ["L1"] } } } }` -""" -input GraphTimeExpr { - """ - Reference time for the operation. - """ - time: TimeInput! - """ - Optional nested filter applied after the time restriction. - """ - expr: GraphFilter -} - enum GraphType { """ Persistent. @@ -2732,18 +2242,6 @@ enum GraphType { EVENT } -""" -Graph view restriction that takes only a nested expression. - -Used for unary view operations like `Latest` and `SnapshotLatest`. -""" -input GraphUnaryExpr { - """ - Optional nested filter applied after the unary operation. - """ - expr: GraphFilter -} - input GraphViewCollection @oneOf { """ Contains only the default layer. @@ -2814,41 +2312,9 @@ input GraphViewCollection @oneOf { """ shrinkEnd: TimeInput """ - Node filter. - """ - nodeFilter: NodeFilter - """ - Edge filter. - """ - edgeFilter: EdgeFilter -} - -""" -Graph view restriction to a time window, optionally chaining another `GraphFilter`. - -Used by `GqlGraphFilter::Window`. - -- `start` and `end` define the window (inclusive start, exclusive end). -- `expr` optionally nests another graph filter to apply *within* this window. - -Example (GraphQL): -```graphql -{ Window: { start: 0, end: 10, expr: { Layers: { names: ["A"] } } } } -``` -""" -input GraphWindowExpr { - """ - Window start time (inclusive). - """ - start: TimeInput! - """ - Window end time (exclusive). + A filter tree; the entity it tests is written in the tree. """ - end: TimeInput! - """ - Optional nested filter applied after the window restriction. - """ - expr: GraphFilter + filter: FilterExpr } """ @@ -3383,6 +2849,15 @@ type Matching { ): Boolean! } +""" +A membership test. `values` is a list; a policy may also leave a single +placeholder here (`{"var": …}`) that resolves to the list per caller. +""" +input Membership { + expr: Expr! + values: Value! +} + """ Lightweight summary of a stored graph — its name, path, counts, and filesystem timestamps — served without deserializing the full graph. @@ -3507,7 +2982,7 @@ enum MetaGraphField { """ Narrows a namespace's graph listing. -Composes the same way as the graph/node/edge filters: leaves test one +Composes like a filter expression: leaves test one attribute or metadata key, and `and` / `or` / `not` combine them. """ input MetaGraphFilter @oneOf { @@ -4607,7 +4082,7 @@ type NestedEdges { """ Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). """ - expr: GqlFilter! + expr: FilterExpr! ): NestedEdges! """ Narrow the edge set to edges matching `expr`, but only at this hop — @@ -4619,7 +4094,7 @@ type NestedEdges { """ Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). """ - expr: GqlFilter! + expr: FilterExpr! ): NestedEdges! } @@ -4886,43 +4361,43 @@ type Node { steps through. The returned nodes are on the full graph so their other-layer neighbours stay queryable. """ - inComponent(select: GqlFilter): Nodes! + inComponent(select: FilterExpr): Nodes! """ Nodes reachable from this one via out-edges. `select` is a general filter expression — a node filter, an edge filter, or a graph (layer/window) filter — scoping which nodes/edges the walk steps through. The returned nodes are on the full (unfiltered) graph, so their other-layer neighbours remain queryable. """ - outComponent(select: GqlFilter): Nodes! + outComponent(select: FilterExpr): Nodes! """ Returns all connected edges. """ - edges(select: EdgeFilter): Edges! + edges(select: FilterExpr): Edges! """ Returns outgoing edges. """ - outEdges(select: EdgeFilter): Edges! + outEdges(select: FilterExpr): Edges! """ Returns incoming edges. """ - inEdges(select: EdgeFilter): Edges! + inEdges(select: FilterExpr): Edges! """ Returns neighbouring nodes. """ - neighbours(select: NodeFilter): PathFromNode! + neighbours(select: FilterExpr): PathFromNode! """ Returns the number of neighbours that have at least one in-going edge to this node. """ - inNeighbours(select: NodeFilter): PathFromNode! + inNeighbours(select: FilterExpr): PathFromNode! """ Returns the number of neighbours that have at least one out-going edge from this node. """ - outNeighbours(select: NodeFilter): PathFromNode! + outNeighbours(select: FilterExpr): PathFromNode! filter( """ Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). """ - expr: GqlFilter! + expr: FilterExpr! ): Node! } @@ -4950,183 +4425,12 @@ input NodeAddition { } """ -Boolean expression over a built-in node field (ID, name, or type). - -This is used by `NodeFieldWhere.where_` when filtering a specific -built-in field. - -Supports comparisons, string predicates, and set membership. -(Presence checks and aggregations are handled via property filters instead.) -""" -input NodeFieldCondition @oneOf { - """ - Equality. - """ - eq: Value - """ - Inequality. - """ - ne: Value - """ - Greater-than. - """ - gt: Value - """ - Greater-than-or-equal. - """ - ge: Value - """ - Less-than. - """ - lt: Value - """ - Less-than-or-equal. - """ - le: Value - """ - String prefix match. - """ - startsWith: Value - """ - String suffix match. - """ - endsWith: Value - """ - Substring match. - """ - contains: Value - """ - Negated substring match. - """ - notContains: Value - """ - Fuzzy string match (Levenshtein distance, optional prefix matching). - """ - fuzzySearch: FuzzySearchExpr - """ - Set membership. - """ - isIn: Value - """ - Negated set membership. - """ - isNotIn: Value -} - -""" -A condition on one specific built-in field — the payload of the per-field -filter variants (`{ id: { where: ... } }`, `{ name: { where: ... } }`, -`{ nodeType: { where: ... } }`). -""" -input NodeFieldWhere { - """ - Condition applied to the field. - - Exposed as `where` in GraphQL. - """ - where: NodeFieldCondition! -} - -""" -GraphQL input type for filtering nodes. - -`NodeFilter` represents a composable boolean expression evaluated -against nodes in a graph. Filters can target: - -- built-in node fields (`Id` / `Name` / `NodeType`), -- node properties and metadata, -- temporal properties, -- temporal scope (windows, snapshots, latest), -- and layer membership, -- plus node state predicates (e.g. `IsActive`). - -Filters can be combined recursively using logical operators -(`And`, `Or`, `Not`). +A built-in node field. """ -input NodeFilter @oneOf { - """ - Filters the node id: `{ id: { where: ... } }`. - """ - id: NodeFieldWhere - """ - Filters the node name: `{ name: { where: ... } }`. - """ - name: NodeFieldWhere - """ - Filters the node type: `{ nodeType: { where: ... } }`. - """ - nodeType: NodeFieldWhere - """ - Filters a node property by name and condition. - """ - property: PropertyFilterNew - """ - Filters a node's degree (in, out, or total) by a condition. - """ - degree: DegreeFilterNew - """ - Filters a node metadata field by name and condition. - - Metadata is shared across all temporal versions of a node. - """ - metadata: PropertyFilterNew - """ - Filters a temporal node property by name and condition. - - Used when the property value varies over time and must be evaluated - within a temporal context. - """ - temporalProperty: PropertyFilterNew - """ - Logical AND over multiple node filters. - """ - and: [NodeFilter!] - """ - Logical OR over multiple node filters. - """ - or: [NodeFilter!] - """ - Logical NOT over a nested node filter. - """ - not: NodeFilter - """ - Restricts evaluation to a time window (inclusive start, exclusive end). - """ - window: NodeWindowExpr - """ - Restricts evaluation to a single point in time. - """ - at: NodeTimeExpr - """ - Restricts evaluation to times strictly before the given time. - """ - before: NodeTimeExpr - """ - Restricts evaluation to times strictly after the given time. - """ - after: NodeTimeExpr - """ - Evaluates predicates against the latest available node state. - """ - latest: NodeUnaryExpr - """ - Evaluates predicates against a snapshot of the graph at a given time. - """ - snapshotAt: NodeTimeExpr - """ - Evaluates predicates against the most recent snapshot of the graph. - """ - snapshotLatest: NodeUnaryExpr - """ - Restricts evaluation to nodes belonging to one or more layers. - """ - layers: NodeLayersExpr - """ - Matches nodes that have at least one event in the current view/window. - - When `true`, only active nodes are matched. - """ - isActive: Boolean +enum NodeFieldName { + ID + NAME + NODE_TYPE } """ @@ -5136,22 +4440,6 @@ when nodes were added. """ scalar NodeId -""" -Restricts node evaluation to one or more layers and applies a nested `NodeFilter`. - -Used by `GqlNodeFilter::Layers`. -""" -input NodeLayersExpr { - """ - Layer names to include. - """ - names: [String!]! - """ - Filter evaluated within the layer-restricted view. - """ - expr: NodeFilter! -} - """ Describes nodes of a specific type in a graph — its property keys and observed value types (and, for string-valued properties, the set of @@ -5484,34 +4772,6 @@ node, or a collection of nodes. """ union NodeStateValue = NodeStateProp | Node | Nodes -""" -Restricts node evaluation to a single time bound and applies a nested `NodeFilter`. - -Used by `At`, `Before`, and `After` node filters. -""" -input NodeTimeExpr { - """ - Reference time for the operation. - """ - time: TimeInput! - """ - Filter evaluated within the restricted time scope. - """ - expr: NodeFilter! -} - -""" -Applies a unary node-view operation and then evaluates a nested `NodeFilter`. - -Used by `Latest` and `SnapshotLatest` node filters. -""" -input NodeUnaryExpr { - """ - Filter evaluated after applying the unary operation. - """ - expr: NodeFilter! -} - input NodeViewCollection @oneOf { """ Contains only the default layer. @@ -5566,31 +4826,9 @@ input NodeViewCollection @oneOf { """ shrinkEnd: TimeInput """ - Node filter. + A filter tree; the entity it tests is written in the tree. """ - nodeFilter: NodeFilter -} - -""" -Restricts node evaluation to a time window and applies a nested `NodeFilter`. - -Used by `GqlNodeFilter::Window`. - -The window is inclusive of `start` and exclusive of `end`. -""" -input NodeWindowExpr { - """ - Window start time (inclusive). - """ - start: TimeInput! - """ - Window end time (exclusive). - """ - end: TimeInput! - """ - Filter evaluated within the restricted window. - """ - expr: NodeFilter! + filter: FilterExpr } """ @@ -5925,7 +5163,7 @@ type Nodes { """ Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). """ - expr: GqlFilter! + expr: FilterExpr! ): Nodes! """ Narrow the collection to nodes matching `expr`, but only at this step — @@ -5951,32 +5189,32 @@ type Nodes { """ Filter expression: node predicates, graph views, or and/or/not combinations (and = intersection). Expressions that test edges are rejected. """ - expr: GqlFilter! + expr: FilterExpr! ): Nodes! """ Returns the neighbouring nodes of each node in the collection. """ - neighbours(select: NodeFilter): PathFromGraph! + neighbours(select: FilterExpr): PathFromGraph! """ Returns the in-neighbours of each node in the collection. """ - inNeighbours(select: NodeFilter): PathFromGraph! + inNeighbours(select: FilterExpr): PathFromGraph! """ Returns the out-neighbours of each node in the collection. """ - outNeighbours(select: NodeFilter): PathFromGraph! + outNeighbours(select: FilterExpr): PathFromGraph! """ Returns the incident edges (both directions) of each node in the collection. """ - edges(select: EdgeFilter): NestedEdges! + edges(select: FilterExpr): NestedEdges! """ Returns the incoming edges of each node in the collection. """ - inEdges(select: EdgeFilter): NestedEdges! + inEdges(select: FilterExpr): NestedEdges! """ Returns the outgoing edges of each node in the collection. """ - outEdges(select: EdgeFilter): NestedEdges! + outEdges(select: FilterExpr): NestedEdges! } input NodesViewCollection @oneOf { @@ -6033,9 +5271,9 @@ input NodesViewCollection @oneOf { """ shrinkEnd: TimeInput """ - Node filter. + A filter tree; the entity it tests is written in the tree. """ - nodeFilter: NodeFilter + filter: FilterExpr """ List of types. """ @@ -6345,7 +5583,7 @@ type PathFromGraph { """ Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). """ - expr: GqlFilter! + expr: FilterExpr! ): PathFromGraph! """ Narrow the neighbour set to nodes matching `expr`, but only at this hop @@ -6357,23 +5595,23 @@ type PathFromGraph { """ Filter expression: node predicates, graph views, or and/or/not combinations (and = intersection). Expressions that test edges are rejected. """ - expr: GqlFilter! + expr: FilterExpr! ): PathFromGraph! """ Returns the neighbouring nodes reachable one further hop from each source path (both directions), as a nested `PathFromGraph`. """ - neighbours(select: NodeFilter): PathFromGraph! + neighbours(select: FilterExpr): PathFromGraph! """ Returns the in-neighbours reachable one further hop from each source path, as a nested `PathFromGraph`. """ - inNeighbours(select: NodeFilter): PathFromGraph! + inNeighbours(select: FilterExpr): PathFromGraph! """ Returns the out-neighbours reachable one further hop from each source path, as a nested `PathFromGraph`. """ - outNeighbours(select: NodeFilter): PathFromGraph! + outNeighbours(select: FilterExpr): PathFromGraph! """ Returns the incident edges (both directions) of each source path, as a nested `NestedEdges` collection. @@ -6677,7 +5915,7 @@ type PathFromNode { """ Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). """ - expr: GqlFilter! + expr: FilterExpr! ): PathFromNode! """ Narrow the neighbour set to nodes matching `expr`, but only at this hop @@ -6700,23 +5938,23 @@ type PathFromNode { """ Filter expression: node predicates, graph views, or and/or/not combinations (and = intersection). Expressions that test edges are rejected. """ - expr: GqlFilter! + expr: FilterExpr! ): PathFromNode! """ Returns the neighbouring nodes reachable one further hop from this path (both directions), as a flat `PathFromNode`. """ - neighbours(select: NodeFilter): PathFromNode! + neighbours(select: FilterExpr): PathFromNode! """ Returns the in-neighbours reachable one further hop from this path, as a flat `PathFromNode`. """ - inNeighbours(select: NodeFilter): PathFromNode! + inNeighbours(select: FilterExpr): PathFromNode! """ Returns the out-neighbours reachable one further hop from this path, as a flat `PathFromNode`. """ - outNeighbours(select: NodeFilter): PathFromNode! + outNeighbours(select: FilterExpr): PathFromNode! """ Returns the incident edges (both directions) of the nodes in this path, as a flat `Edges` collection. @@ -6827,8 +6065,8 @@ type PathFromNodeWindowSet { """ Boolean expression over a property value. -`PropCondition` is used inside `PropertyFilterNew.where` to describe -how a property’s value should be matched. +`PropCondition` is the `where` of a namespace metagraph filter: how one +graph-level metadata value, or a graph field, should be matched. It supports: - comparisons (`Eq`, `Gt`, `Le`, …), @@ -7043,34 +6281,6 @@ type Property { dtype: PropertyType! } -""" -Filters an entity property or metadata field by name and condition. - -This input is used by both node and edge filters when targeting -a specific property key (or metadata key) and applying a `PropCondition`. - -Fields: -- `name`: The property key to query. -- `where_`: The condition to apply to that property’s value. - -Example (GraphQL): -```graphql -{ Property: { name: "weight", where: { Gt: 0.5 } } } -``` -""" -input PropertyFilterNew { - """ - Property (or metadata) key. - """ - name: String! - """ - Condition applied to the property value. - - Exposed as `where` in GraphQL. - """ - where: PropCondition! -} - input PropertyInput { """ Key. @@ -7266,6 +6476,26 @@ type QueryRoot { ): VectorisedGraph } +""" +A value read from an entity. +""" +input Read { + entity: Entity! + views: [ViewOp!] + endpoint: Endpoint + target: Target! +} + +""" +Where a value is read: the entity, the views to read it through, and for an +edge optionally one of its endpoint nodes. +""" +input Scope { + entity: Entity! + views: [ViewOp!] + endpoint: Endpoint +} + """ How the initially infected nodes are chosen. """ @@ -7307,6 +6537,28 @@ enum SpecialFloat { NEG_INFINITY } +""" +What a read selects on its entity. +""" +input Target @oneOf { + """ + A built-in node field. + """ + field: NodeFieldName + """ + A node degree in a direction. + """ + degree: DegreeDirection + """ + A property, by name. + """ + property: String + """ + A metadata entry, by name. + """ + metadata: String +} + input Template @oneOf { """ The default template. @@ -7766,6 +7018,20 @@ input VectorisedGraphWindow { end: TimeInput! } +""" +One view restriction, applied in list order. +""" +input ViewOp @oneOf { + window: Window + at: TimeInput + after: TimeInput + before: TimeInput + latest: Boolean + snapshotAt: TimeInput + snapshotLatest: Boolean + layers: [String!] +} + input Window { """ Window start time. @@ -7810,3 +7076,4 @@ schema { query: QueryRoot mutation: MutRoot } + diff --git a/raphtory-graphql/src/client/graphql_transport.rs b/raphtory-graphql/src/client/graphql_transport.rs index 7d4e4502a2..c813287e48 100644 --- a/raphtory-graphql/src/client/graphql_transport.rs +++ b/raphtory-graphql/src/client/graphql_transport.rs @@ -19,7 +19,7 @@ use crate::{ ClientError, }, model::graph::{ - filtering::GqlFilter, + filter_expr_input::GqlFilter, property::{gql_to_prop, parse_special_float}, }, }; @@ -588,7 +588,7 @@ impl GraphqlTransport { struct VarCollector { vars: serde_json::Map, /// Accumulated variable declarations, already comma-joined - /// (`"$f0: NodeFilter!, $f1: EdgeFilter!"`) — appended in place rather than + /// (`"$f0: FilterExpr!, $f1: FilterExpr!"`) — appended in place rather than /// collected into a `Vec` and joined at the end. decls: String, counter: usize, @@ -596,7 +596,7 @@ struct VarCollector { impl VarCollector { fn add_filter(&mut self, f: &GqlFilter) -> Result { - self.add("GqlFilter!", f) + self.add("FilterExpr!", f) } /// Serialize `value`, register it as `$fN: `, and return `$fN`. @@ -1254,7 +1254,7 @@ fn render_read_into( )?; } ReadExpr::Filtered { input, filter } => { - // Unified server field `filter(expr: GqlFilter!)` — the same field + // Unified server field `filter(expr: FilterExpr!)` — the same field // on Graph, Node, Edge, and every collection. Applies to this view // AND propagates to downstream traversals (contrast `select`, // which narrows membership at one step only). @@ -1262,14 +1262,14 @@ fn render_read_into( write!(out, " {{ filter(expr: {})", vars.add_filter(filter)?)?; } ReadExpr::SelectNodes { input, filter } => { - // Server field `select(expr: GqlFilter!)`: narrows the current + // Server field `select(expr: FilterExpr!)`: narrows the current // collection's membership only; downstream traversals see the // unfiltered graph. render_read_into(input, vars, out)?; write!(out, " {{ select(expr: {})", vars.add_filter(filter)?)?; } ReadExpr::SelectEdges { input, filter } => { - // Server field `select(expr: GqlFilter!)` on `Edges`: narrows the + // Server field `select(expr: FilterExpr!)` on `Edges`: narrows the // current collection's membership only. render_read_into(input, vars, out)?; write!(out, " {{ select(expr: {})", vars.add_filter(filter)?)?; @@ -4296,22 +4296,40 @@ mod tests { use crate::{ data::GqlGraphType, model::graph::{ - filtering::{GqlNodeFilter, PropCondition, PropertyFilterNew}, + filter_expr_input::{GqlCmp, GqlEntity, GqlExpr, GqlRead, GqlTarget}, property::Value as GqlValue, }, server::GraphServer, }; use raphtory::{ - db::graph::views::filter::model::node_filter::CompositeNodeFilter, + db::graph::views::filter::model::tree::FilterExpr, prelude::{Args, NO_PROPS}, }; use raphtory_api::core::storage::timeindex::AsTime; use reqwest::Url; - use std::{collections::HashMap as Map, str::FromStr, sync::Arc}; + use std::{str::FromStr, sync::Arc}; use tempfile::tempdir; // ============ Unit tests for the read pipeline ============ + /// A node-property comparison in the tree grammar: `property(name) value`. + fn node_prop(name: &str, op: fn(GqlCmp) -> GqlFilter, value: GqlValue) -> GqlFilter { + op(GqlCmp { + lhs: GqlExpr::Read(GqlRead { + entity: GqlEntity::Node, + views: None, + endpoint: None, + target: GqlTarget::Property(name.into()), + }), + rhs: GqlExpr::Const(value), + }) + } + + /// The tree a client hands to `filter`, as python does. + fn tree(filter: GqlFilter) -> FilterExpr { + FilterExpr::try_from(filter).unwrap() + } + #[test] fn render_read_produces_nested_graphql() { let expr = ReadExpr::Degree { @@ -4673,14 +4691,15 @@ mod tests { // A filter with a quote-bearing string value: it must be shipped as a // `$fN` JSON variable (escaping inherent, no query-string splicing to // break out of), not rendered into the query text. - let filter = GqlFilter::Node(GqlNodeFilter::Property(PropertyFilterNew { - name: "score".into(), - where_: PropCondition::Eq(GqlValue::Str("O\"Brien".into())), - })); + let filter = node_prop( + "score".into(), + GqlFilter::Eq, + GqlValue::Str("O\"Brien".into()), + ); let mut vars = VarCollector::default(); let reference = vars.add_filter(&filter).unwrap(); assert_eq!(reference, "$f0"); - assert_eq!(vars.decls, "$f0: GqlFilter!"); + assert_eq!(vars.decls, "$f0: FilterExpr!"); // The value lives in the variables map as JSON data, quote intact. let json = serde_json::to_string(&vars.vars["f0"]).unwrap(); assert!( @@ -4726,10 +4745,7 @@ mod tests { #[test] fn property_key_rides_json_variable_intact() { // A quote-bearing property KEY is carried as JSON data too. - let filter = GqlFilter::Node(GqlNodeFilter::Property(PropertyFilterNew { - name: "wei\"rd".into(), - where_: PropCondition::Eq(GqlValue::Str("v".into())), - })); + let filter = node_prop("wei\"rd".into(), GqlFilter::Eq, GqlValue::Str("v".into())); let mut vars = VarCollector::default(); vars.add_filter(&filter).unwrap(); let json = serde_json::to_string(&vars.vars["f0"]).unwrap(); @@ -4744,12 +4760,8 @@ mod tests { // Two filters in one composed read must render as two declarations // with each field arg referencing its own variable — the payloads must // not collide or swap. - let prop_filter = |name: &str| { - GqlNodeFilter::Property(PropertyFilterNew { - name: name.into(), - where_: PropCondition::Eq(GqlValue::Str("x".into())), - }) - }; + let prop_filter = + |name: &str| node_prop(name.into(), GqlFilter::Eq, GqlValue::Str("x".into())); let expr = ReadExpr::Ids { input: Arc::new(ReadExpr::Filtered { input: Arc::new(ReadExpr::Filtered { @@ -4759,15 +4771,15 @@ mod tests { graph_type: None, }), }), - filter: Arc::new(GqlFilter::Node(prop_filter("inner"))), + filter: Arc::new(prop_filter("inner")), }), - filter: Arc::new(GqlFilter::Node(prop_filter("outer"))), + filter: Arc::new(prop_filter("outer")), }), }; let (query, vars) = render_read(&expr).unwrap(); assert!( - query.contains("$f0: GqlFilter!") && query.contains("$f1: GqlFilter!"), + query.contains("$f0: FilterExpr!") && query.contains("$f1: FilterExpr!"), "missing declarations in: {query}" ); assert!( @@ -4803,10 +4815,7 @@ mod tests { GqlValue::F64(f64::INFINITY), GqlValue::F32(f32::NEG_INFINITY), ] { - let filter = GqlFilter::Node(GqlNodeFilter::Property(PropertyFilterNew { - name: "x".into(), - where_: PropCondition::Eq(bad), - })); + let filter = node_prop("x".into(), GqlFilter::Eq, bad); let mut vars = VarCollector::default(); assert!(matches!( vars.add_filter(&filter), @@ -4815,10 +4824,7 @@ mod tests { } // A finite float serializes fine. - let filter = GqlFilter::Node(GqlNodeFilter::Property(PropertyFilterNew { - name: "x".into(), - where_: PropCondition::Eq(GqlValue::F64(1.5)), - })); + let filter = node_prop("x".into(), GqlFilter::Eq, GqlValue::F64(1.5)); let mut vars = VarCollector::default(); assert!(vars.add_filter(&filter).is_ok()); } @@ -4954,10 +4960,8 @@ mod tests { use raphtory::{ db::{ api::storage::storage::Config, - graph::views::filter::model::{ - node_filter::{CompositeNodeFilter, NodeFilter}, - property_filter::{PropertyFilter, PropertyFilterValue, PropertyRef}, - FilterOperator, + graph::views::filter::model::tree::{ + CmpOp, Entity, Expr, FilterExpr, Scope, Target, }, }, prelude::Prop, @@ -4990,14 +4994,11 @@ mod tests { rg.add_edge(2i64, "b", "c", NO_PROPS, None).await.unwrap(); rg.add_edge(3i64, "c", "a", NO_PROPS, None).await.unwrap(); - let score_gt_15 = GqlNodeFilter::Property(PropertyFilterNew { - name: "score".into(), - where_: PropCondition::Gt(GqlValue::I64(15)), - }); + let score_gt_15 = node_prop("score".into(), GqlFilter::Gt, GqlValue::I64(15)); // Membership: filter keeps every node addressable — including `a`, // which fails the filter itself. - let filtered = rg.nodes().filter(score_gt_15.clone()).unwrap(); + let filtered = rg.nodes().filter(tree(score_gt_15.clone())).unwrap(); let mut ids = filtered.id().await.unwrap(); ids.sort(); assert_eq!( @@ -5054,16 +5055,17 @@ mod tests { ); // select() narrows membership only — handles see the unfiltered graph. - // Passed as a composite to pin that kind-typed callers still satisfy - // the widened `TryInto` bound. - let score_gt_15_composite = CompositeNodeFilter::Property(PropertyFilter { - prop_ref: PropertyRef::Property("score".into()), - prop_value: PropertyFilterValue::Single(Prop::I64(15)), - operator: FilterOperator::Gt, - ops: vec![], - entity: NodeFilter, - }); - let selected = rg.nodes().select(score_gt_15_composite).unwrap(); + // Passed as a tree to pin that tree-typed callers satisfy the widened + // `TryInto` bound. + let score_gt_15_tree = FilterExpr::Cmp { + op: CmpOp::Gt, + lhs: Expr::Read { + scope: Scope::new(Entity::Node), + target: Target::Property("score".into()), + }, + rhs: Expr::Const(Prop::I64(15)), + }; + let selected = rg.nodes().select(score_gt_15_tree).unwrap(); let mut selected_ids = selected.id().await.unwrap(); selected_ids.sort(); assert_eq!( @@ -5087,7 +5089,7 @@ mod tests { .await .unwrap() .unwrap() - .filter(score_gt_15) + .filter(tree(score_gt_15)) .unwrap(); let c_handles = b.neighbours().collect().await.unwrap(); assert_eq!(c_handles.len(), 1); @@ -5103,10 +5105,11 @@ mod tests { // is b-c, and its src (b) still evaluates under f. let nested = rg .nodes() - .filter(GqlNodeFilter::Property(PropertyFilterNew { - name: "score".into(), - where_: PropCondition::Gt(GqlValue::I64(15)), - })) + .filter(tree(node_prop( + "score".into(), + GqlFilter::Gt, + GqlValue::I64(15), + ))) .unwrap(); let rows = nested.edges().collect().await.unwrap(); let ids_in_order = nested.id().await.unwrap(); diff --git a/raphtory-graphql/src/client/op.rs b/raphtory-graphql/src/client/op.rs index b220faeaea..4c4fdbaaab 100644 --- a/raphtory-graphql/src/client/op.rs +++ b/raphtory-graphql/src/client/op.rs @@ -4,7 +4,9 @@ //! hands it to the transport. This module is the single source of truth for //! what "an operation" means on the wire. -use crate::{client::properties_to_input, data::GqlGraphType, model::graph::filtering::GqlFilter}; +use crate::{ + client::properties_to_input, data::GqlGraphType, model::graph::filter_expr_input::GqlFilter, +}; use raphtory_api::core::entities::{properties::prop::Prop, GID}; // Re-exported so the client transport wrappers import the op tree's time type // from one place (`op::InputTime`), same as `ReadExpr`/`WriteOp`. @@ -193,7 +195,7 @@ pub enum ReadExpr { /// Filter this view by a general filter expression (node/edge predicates, /// graph views, and/or/not combinations). The restriction propagates to /// downstream traversals. One variant serves Graph, Node, Edge, and every - /// collection — they all expose the same `filter(expr: GqlFilter!)` field. + /// collection — they all expose the same `filter(expr: FilterExpr!)` field. Filtered { input: Arc, filter: Arc, @@ -201,7 +203,7 @@ pub enum ReadExpr { /// Narrow a `Nodes` collection's membership by a filter expression /// (node predicates, graph views, and/or/not combinations — edge tests /// are rejected server-side). Returns `Nodes`. Server field: - /// `select(expr: GqlFilter!)` on `Nodes`. + /// `select(expr: FilterExpr!)` on `Nodes`. /// /// Applies the filter only to this step; downstream traversals from /// the matching nodes see the unfiltered graph. @@ -211,7 +213,7 @@ pub enum ReadExpr { }, /// Narrow an `Edges` collection's membership by a filter expression /// (node/edge predicates, graph views, and/or/not combinations). - /// Returns `Edges`. Server field: `select(expr: GqlFilter!)` on + /// Returns `Edges`. Server field: `select(expr: FilterExpr!)` on /// `Edges`. /// /// Applies the filter only to this step; downstream traversals from diff --git a/raphtory-graphql/src/client/remote/remote_edge.rs b/raphtory-graphql/src/client/remote/remote_edge.rs index a1e88e1e66..9022788d20 100644 --- a/raphtory-graphql/src/client/remote/remote_edge.rs +++ b/raphtory-graphql/src/client/remote/remote_edge.rs @@ -17,7 +17,7 @@ use crate::{ }, ClientError, }, - model::graph::filtering::GqlFilter, + model::graph::filter_expr_input::GqlFilter, }; use raphtory::errors::GraphError; use raphtory_api::core::{ diff --git a/raphtory-graphql/src/client/remote/remote_edges.rs b/raphtory-graphql/src/client/remote/remote_edges.rs index 8baa416fe6..117a8f44b5 100644 --- a/raphtory-graphql/src/client/remote/remote_edges.rs +++ b/raphtory-graphql/src/client/remote/remote_edges.rs @@ -13,7 +13,7 @@ use crate::{ }, ClientError, }, - model::graph::filtering::GqlFilter, + model::graph::filter_expr_input::GqlFilter, }; use raphtory::errors::GraphError; use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; diff --git a/raphtory-graphql/src/client/remote/remote_graph.rs b/raphtory-graphql/src/client/remote/remote_graph.rs index 080ac6733d..3cfd1a53ae 100644 --- a/raphtory-graphql/src/client/remote/remote_graph.rs +++ b/raphtory-graphql/src/client/remote/remote_graph.rs @@ -22,7 +22,7 @@ use crate::{ ClientError, }, data::GqlGraphType, - model::graph::filtering::GqlFilter, + model::graph::filter_expr_input::GqlFilter, }; use raphtory::errors::GraphError; use raphtory_api::core::{ diff --git a/raphtory-graphql/src/client/remote/remote_nested_edges.rs b/raphtory-graphql/src/client/remote/remote_nested_edges.rs index 44df0837bf..4b50d04ae2 100644 --- a/raphtory-graphql/src/client/remote/remote_nested_edges.rs +++ b/raphtory-graphql/src/client/remote/remote_nested_edges.rs @@ -13,7 +13,7 @@ use crate::{ }, ClientError, }, - model::graph::filtering::GqlFilter, + model::graph::filter_expr_input::GqlFilter, }; use raphtory::errors::GraphError; use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; diff --git a/raphtory-graphql/src/client/remote/remote_node.rs b/raphtory-graphql/src/client/remote/remote_node.rs index b4ecad2a47..355f1e57cf 100644 --- a/raphtory-graphql/src/client/remote/remote_node.rs +++ b/raphtory-graphql/src/client/remote/remote_node.rs @@ -18,7 +18,7 @@ use crate::{ }, ClientError, }, - model::graph::filtering::GqlFilter, + model::graph::filter_expr_input::GqlFilter, }; use raphtory::errors::GraphError; use raphtory_api::core::{ diff --git a/raphtory-graphql/src/client/remote/remote_nodes.rs b/raphtory-graphql/src/client/remote/remote_nodes.rs index 1e52e623e3..a4ac29f8d7 100644 --- a/raphtory-graphql/src/client/remote/remote_nodes.rs +++ b/raphtory-graphql/src/client/remote/remote_nodes.rs @@ -13,7 +13,7 @@ use crate::{ }, ClientError, }, - model::graph::filtering::GqlFilter, + model::graph::filter_expr_input::GqlFilter, }; use raphtory::errors::GraphError; use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; diff --git a/raphtory-graphql/src/client/remote/remote_path_from_graph.rs b/raphtory-graphql/src/client/remote/remote_path_from_graph.rs index 37801f9a04..dad747f620 100644 --- a/raphtory-graphql/src/client/remote/remote_path_from_graph.rs +++ b/raphtory-graphql/src/client/remote/remote_path_from_graph.rs @@ -15,7 +15,7 @@ use crate::{ }, ClientError, }, - model::graph::filtering::GqlFilter, + model::graph::filter_expr_input::GqlFilter, }; use raphtory::errors::GraphError; use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; diff --git a/raphtory-graphql/src/client/remote/remote_path_from_node.rs b/raphtory-graphql/src/client/remote/remote_path_from_node.rs index 60deb0f54e..d5f44f2044 100644 --- a/raphtory-graphql/src/client/remote/remote_path_from_node.rs +++ b/raphtory-graphql/src/client/remote/remote_path_from_node.rs @@ -13,7 +13,7 @@ use crate::{ }, ClientError, }, - model::graph::filtering::GqlFilter, + model::graph::filter_expr_input::GqlFilter, }; use raphtory::errors::GraphError; use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; diff --git a/raphtory-graphql/src/data.rs b/raphtory-graphql/src/data.rs index 130ca9c8be..79eba965e4 100644 --- a/raphtory-graphql/src/data.rs +++ b/raphtory-graphql/src/data.rs @@ -7,7 +7,8 @@ use crate::{ model::{ blocking_io, graph::{ - filtering::{GqlFilter, GraphAccessFilter, HiddenKeys}, + filter_expr_input::GqlFilter, + filtering::{GraphAccessFilter, HiddenKeys}, namespace::Namespace, namespaced_item::NamespacedItem, }, @@ -27,7 +28,7 @@ use raphtory::{ storage::storage::Args, view::{DynamicGraph, Filter, GraphViewOps, IntoDynamic, MaterializedGraph}, }, - graph::views::{filter::model::DynFilter, property_redacted_graph::PropertyRedaction}, + graph::views::property_redacted_graph::PropertyRedaction, }, errors::GraphError, prelude::AdditionOps, @@ -374,6 +375,28 @@ impl Data { .await } + /// Whether `filter` can be applied to the graph at `path`. `Ok(Err(_))` says the + /// filter itself does not fit that graph (a value of the wrong type for a property, + /// say); `Err(_)` says the graph could not be loaded. Policies use it to tell a + /// per-caller value that cannot be compared from a grant that is wrong. + /// + /// # ⚠ Does no permission check — the caller must already have authorised `path`. + /// Loading and error reporting here would otherwise reveal whether a graph exists. + pub async fn access_filter_applies( + &self, + path: &str, + filter: &GqlFilter, + ) -> Result, GQLError> { + let graph = self + .get_graph_unchecked(path) + .await? + .graph() + .clone() + .into_dynamic(); + let filter = filter.clone(); + Ok(blocking_compute(move || compile_row_filter(graph, filter).map(|_| ())).await) + } + /// Test-only: direct graph load without permission checks. #[cfg(test)] pub(crate) async fn get_graph_for_test( @@ -905,35 +928,25 @@ fn apply_row_filter_sync( graph: DynamicGraph, filter: GqlFilter, ) -> async_graphql::Result { - // And sub-filters are applied sequentially so that DynView (window/snapshot/layer) - // sub-filters wrap the graph view before subsequent node/edge predicate filters run. - if let GqlFilter::And(filters) = filter { - // An empty `and` folds to the graph unchanged — i.e. no restriction at all. Fail closed - // rather than serve every row, matching `DynFilter::try_from`'s rejection of an empty - // combinator (which this shortcut path otherwise never reaches). - if filters.is_empty() { - error!("empty 'and' access filter restricts nothing"); - return Err(async_graphql::Error::new( - "access filter could not be applied; the grant is misconfigured", - )); + compile_row_filter(graph, filter).map_err(|e| { + // The stage is logged with the engine's own message; the caller only learns that + // the grant is at fault, never what the filter said. + match e { + GraphError::InvalidGqlFilter(_) | GraphError::InvalidFilter(_) => { + error!(error = %e, "access filter conversion failed") + } + _ => error!(error = %e, "access filter application failed"), } - return filters - .into_iter() - .try_fold(graph, |g, f| apply_row_filter_sync(g, f)); - } - let dyn_filter = DynFilter::try_from(filter).map_err(|e| { - error!(error = %e, "access filter conversion failed"); async_graphql::Error::new("access filter could not be applied; the grant is misconfigured") - })?; - Ok(graph - .filter(dyn_filter) - .map_err(|e| { - error!(error = %e, "access filter application failed"); - async_graphql::Error::new( - "access filter could not be applied; the grant is misconfigured", - ) - })? - .into_dynamic()) + }) +} + +/// The graph under a row filter, or the reason the filter cannot be applied to it. +/// +/// The filter means what it means everywhere else: `and` is an intersection, and a +/// predicate that should be evaluated inside a view carries that view on its read. +fn compile_row_filter(graph: DynamicGraph, filter: GqlFilter) -> Result { + Ok(graph.filter(filter)?.into_dynamic()) } fn build_redaction(filter: &GraphAccessFilter) -> PropertyRedaction { diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs index 93ea5e743a..5eeb2b8da6 100644 --- a/raphtory-graphql/src/lib.rs +++ b/raphtory-graphql/src/lib.rs @@ -4,7 +4,7 @@ pub use crate::{ auth::{ Access, KeyResolver, ReadOnly, Roles, RolesMissing, StaticKeyResolver, TokenClaimValues, }, - model::graph::{filtering::GraphAccessFilter, property::Value}, + model::graph::{filter_expr_input::GqlFilter, filtering::GraphAccessFilter, property::Value}, server::GraphServer, }; @@ -558,7 +558,23 @@ mod graphql_test { { graph(path: "g") { filterNodes: filter( - expr: { node: { degree: { direction: BOTH, where: { gt: { u64: 0 } } } } } + expr: { + gt: { + lhs: { + read: { + entity: NODE + target: { + degree: BOTH + } + } + } + rhs: { + const: { + u64: 0 + } + } + } + } ) { nodes { list { @@ -567,7 +583,23 @@ mod graphql_test { } } nodes { - select(expr: { node: { degree: { direction: BOTH, where: { gt: { u64: 0 } } } } }) { + select(expr: { + gt: { + lhs: { + read: { + entity: NODE + target: { + degree: BOTH + } + } + } + rhs: { + const: { + u64: 0 + } + } + } + }) { list { name } diff --git a/raphtory-graphql/src/model/algorithms/resolvers.rs b/raphtory-graphql/src/model/algorithms/resolvers.rs index 597723b4a6..67652edc69 100644 --- a/raphtory-graphql/src/model/algorithms/resolvers.rs +++ b/raphtory-graphql/src/model/algorithms/resolvers.rs @@ -7,7 +7,7 @@ use crate::{ outputs::{GqlMatching, GqlMotifCounts}, }, graph::{ - filtering::GqlFilter, node_id::GqlNodeId, node_state::GqlNodeState, + filter_expr_input::GqlFilter, node_id::GqlNodeId, node_state::GqlNodeState, timeindex::GqlTimeInput, WindowDuration, }, }, @@ -183,7 +183,7 @@ impl GqlAlgorithms { pub async fn in_components( &self, #[graphql( - desc = "Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view." + desc = "Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view." )] filter: Option, #[graphql(desc = "Number of threads to use. Defaults to all available.")] threads: Option< @@ -203,7 +203,7 @@ impl GqlAlgorithms { pub async fn out_components( &self, #[graphql( - desc = "Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view." + desc = "Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view." )] filter: Option, #[graphql(desc = "Number of threads to use. Defaults to all available.")] threads: Option< @@ -224,7 +224,7 @@ impl GqlAlgorithms { &self, #[graphql(desc = "Node id.")] node: GqlNodeId, #[graphql( - desc = "Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view." + desc = "Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view." )] filter: Option, ) -> Result { @@ -245,7 +245,7 @@ impl GqlAlgorithms { &self, #[graphql(desc = "Node id.")] node: GqlNodeId, #[graphql( - desc = "Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view." + desc = "Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view." )] filter: Option, ) -> Result { diff --git a/raphtory-graphql/src/model/algorithms/tests/components/in_component.rs b/raphtory-graphql/src/model/algorithms/tests/components/in_component.rs index 690c8c36e8..d8ac7a08b6 100644 --- a/raphtory-graphql/src/model/algorithms/tests/components/in_component.rs +++ b/raphtory-graphql/src/model/algorithms/tests/components/in_component.rs @@ -1,6 +1,5 @@ use crate::{graphql_test, test_support::setup_with_graphs}; use async_graphql::Request; -use serde_json::json; use tempfile::tempdir; #[tokio::test] diff --git a/raphtory-graphql/src/model/algorithms/tests/components/out_component.rs b/raphtory-graphql/src/model/algorithms/tests/components/out_component.rs index c66ada7548..096ae37ff8 100644 --- a/raphtory-graphql/src/model/algorithms/tests/components/out_component.rs +++ b/raphtory-graphql/src/model/algorithms/tests/components/out_component.rs @@ -69,7 +69,23 @@ async fn test_algorithm_out_component_filtered() { { graph(path: "g") { algorithm { - outComponent(node: "a", filter: { node: { name: { where: { ne: { str: "c" } } } } }) { + outComponent(node: "a", filter: { + ne: { + lhs: { + read: { + entity: NODE + target: { + field: NAME + } + } + } + rhs: { + const: { + str: "c" + } + } + } + }) { nodes { list { id } } } } @@ -100,12 +116,44 @@ async fn test_algorithm_out_component_node_filter_composed() { { graph(path: "g") { algorithm { - outComponent(node: "a", filter: { node: { + outComponent(node: "a", filter: { and: [ - { name: { where: { ne: { str: "b" } } } }, - { name: { where: { ne: { str: "c" } } } } + { + ne: { + lhs: { + read: { + entity: NODE + target: { + field: NAME + } + } + } + rhs: { + const: { + str: "b" + } + } + } + }, + { + ne: { + lhs: { + read: { + entity: NODE + target: { + field: NAME + } + } + } + rhs: { + const: { + str: "c" + } + } + } + } ] - } }) { + }) { nodes { list { id } } } } @@ -135,12 +183,46 @@ async fn test_algorithm_out_component_edge_filter_composed() { { graph(path: "g") { algorithm { - outComponent(node: "a", filter: { edge: { + outComponent(node: "a", filter: { and: [ - { dst: { name: { where: { ne: { str: "b" } } } } }, - { dst: { name: { where: { ne: { str: "c" } } } } } + { + ne: { + lhs: { + read: { + entity: EDGE + target: { + field: NAME + } + endpoint: DST + } + } + rhs: { + const: { + str: "b" + } + } + } + }, + { + ne: { + lhs: { + read: { + entity: EDGE + target: { + field: NAME + } + endpoint: DST + } + } + rhs: { + const: { + str: "c" + } + } + } + } ] - } }) { + }) { nodes { list { id } } } } @@ -176,9 +258,19 @@ async fn test_algorithm_out_component_graph_filter_composed() { { graph(path: "g") { algorithm { - outComponent(node: "a", filter: { graph: { - window: { start: 1, end: 3, expr: { before: { time: 2 } } } - } }) { + outComponent(node: "a", filter: { + view: [ + { + before: 2 + }, + { + window: { + start: 1 + end: 3 + } + } + ] + }) { nodes { list { id } } } } @@ -211,9 +303,23 @@ async fn test_algorithm_out_component_filter_equivalence() { { graph(path: "g") { algorithm { - outComponent(node: "a", filter: { node: { - name: { where: { ne: { str: "c" } } } - } }) { + outComponent(node: "a", filter: { + ne: { + lhs: { + read: { + entity: NODE + target: { + field: NAME + } + } + } + rhs: { + const: { + str: "c" + } + } + } + }) { rows { node { id } entries { @@ -230,8 +336,23 @@ async fn test_algorithm_out_component_filter_equivalence() { let pre_filtered = r#" { graph(path: "g") { - filter(expr: { node: { - name: { where: { ne: { str: "c" } } }} }) { + filter(expr: { + ne: { + lhs: { + read: { + entity: NODE + target: { + field: NAME + } + } + } + rhs: { + const: { + str: "c" + } + } + } + }) { algorithm { outComponent(node: "a") { rows { diff --git a/raphtory-graphql/src/model/graph/edge.rs b/raphtory-graphql/src/model/graph/edge.rs index 99b66fba29..7501115c0c 100644 --- a/raphtory-graphql/src/model/graph/edge.rs +++ b/raphtory-graphql/src/model/graph/edge.rs @@ -1,7 +1,8 @@ use crate::{ model::graph::{ edges::GqlEdges, - filtering::{EdgeViewCollection, GqlFilter}, + filter_expr_input::GqlFilter, + filtering::EdgeViewCollection, history::GqlHistory, node::GqlNode, node_id::GqlNodeId, @@ -303,9 +304,7 @@ impl GqlEdge { EdgeViewCollection::After(time) => return_view.after(time).await, EdgeViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, EdgeViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - EdgeViewCollection::EdgeFilter(filter) => { - return_view.filter(GqlFilter::Edge(filter)).await? - } + EdgeViewCollection::Filter(filter) => return_view.filter(filter).await?, } } Ok(return_view) diff --git a/raphtory-graphql/src/model/graph/edges.rs b/raphtory-graphql/src/model/graph/edges.rs index f9c4329e94..740aca5ca1 100644 --- a/raphtory-graphql/src/model/graph/edges.rs +++ b/raphtory-graphql/src/model/graph/edges.rs @@ -3,7 +3,8 @@ use crate::{ graph::{ collection::{check_list_allowed, check_page_limit}, edge::GqlEdge, - filtering::{EdgesViewCollection, GqlFilter}, + filter_expr_input::GqlFilter, + filtering::EdgesViewCollection, path_from_node::GqlPathFromNode, timeindex::{GqlEventTime, GqlTimeInput}, windowset::GqlEdgesWindowSet, @@ -289,9 +290,7 @@ impl GqlEdges { EdgesViewCollection::After(time) => return_view.after(time).await, EdgesViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, EdgesViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - EdgesViewCollection::EdgeFilter(filter) => { - return_view.filter(GqlFilter::Edge(filter)).await? - } + EdgesViewCollection::Filter(filter) => return_view.filter(filter).await?, } } diff --git a/raphtory-graphql/src/model/graph/expr_lowering.rs b/raphtory-graphql/src/model/graph/expr_lowering.rs deleted file mode 100644 index 7646d1772e..0000000000 --- a/raphtory-graphql/src/model/graph/expr_lowering.rs +++ /dev/null @@ -1,483 +0,0 @@ -//! Lowers the GraphQL filter wire types onto expression filters. -//! -//! The wire schema (`GqlNodeFilter` and friends) is unchanged; only the -//! target changes: instead of the composite filter enums, each condition -//! builds the corresponding typed expression and is erased to a [`DynFilter`]. - -use crate::model::graph::filtering::{ - translate_node_field_where, translate_prop_leaf_to_filter, GqlEdgeFilter, - GqlExplodedEdgeFilter, GqlNodeFilter, NodeField, NodeFieldCondition, PropCondition, -}; -use raphtory::{ - db::graph::views::filter::model::{ - edge_filter::{EdgeEndpointNodeFilter, Endpoint}, - exploded_edge_filter::ExplodedEdgeFilter, - filter::FilterValue, - latest_filter::Latest as LatestWrap, - layered_filter::Layered, - node_expr::{DynCreateOp, EntityAggOps}, - property_filter::PropertyFilterValue, - snapshot_filter::{SnapshotAt as SnapshotAtWrap, SnapshotLatest as SnapshotLatestWrap}, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, DynFilter, EdgeViewFilterOps, FilterOperator, - NodeViewFilterOps, PropertyExprFactory, - }, - errors::GraphError, - prelude::{EdgeFilter, EntityExprFilterOps, Layer, NodeFilter, NodeFilterFactory}, -}; -use raphtory_api::core::{ - entities::properties::prop::{IntoProp, Prop}, - storage::timeindex::{AsTime, EventTime}, - utils::time::IntoTime, -}; -use std::{ops::Deref, sync::Arc}; - -fn erased(f: F) -> DynFilter { - Arc::new(f) as DynFilter -} - -fn combine_all( - filters: impl IntoIterator>, - or: bool, - what: &str, -) -> Result { - let mut it = filters.into_iter(); - let first = it.next().transpose()?.ok_or_else(|| { - GraphError::InvalidGqlFilter(format!("Filter '{what}' requires non-empty list")) - })?; - it.try_fold(first, |acc, next| { - Ok::<_, GraphError>(if or { - Arc::new(acc.or(next?)) as DynFilter - } else { - Arc::new(acc.and(next?)) as DynFilter - }) - }) -} - -/// Applies one translated leaf predicate to a value expression. -fn apply_leaf( - lhs: Arc, - op: FilterOperator, - value: PropertyFilterValue, -) -> Result { - use FilterOperator as FO; - Ok(match (op, value) { - (FO::Eq, PropertyFilterValue::Single(v)) => erased(lhs.eq(v)), - (FO::Ne, PropertyFilterValue::Single(v)) => erased(lhs.ne(v)), - (FO::Gt, PropertyFilterValue::Single(v)) => erased(lhs.gt(v)), - (FO::Ge, PropertyFilterValue::Single(v)) => erased(lhs.ge(v)), - (FO::Lt, PropertyFilterValue::Single(v)) => erased(lhs.lt(v)), - (FO::Le, PropertyFilterValue::Single(v)) => erased(lhs.le(v)), - (FO::StartsWith, PropertyFilterValue::Single(v)) => erased(lhs.starts_with(v)), - (FO::EndsWith, PropertyFilterValue::Single(v)) => erased(lhs.ends_with(v)), - (FO::Contains, PropertyFilterValue::Single(v)) => erased(lhs.contains(v)), - (FO::NotContains, PropertyFilterValue::Single(v)) => erased(lhs.not_contains(v)), - ( - FO::FuzzySearch { - levenshtein_distance, - prefix_match, - }, - PropertyFilterValue::Single(v), - ) => erased(lhs.fuzzy_search(v, levenshtein_distance, prefix_match)), - (FO::IsIn, PropertyFilterValue::Set(values)) => { - erased(lhs.is_in(values.deref().iter().cloned())) - } - (FO::IsNotIn, PropertyFilterValue::Set(values)) => { - erased(lhs.is_not_in(values.deref().iter().cloned())) - } - (FO::IsSome, PropertyFilterValue::None) => erased(lhs.is_some()), - (FO::IsNone, PropertyFilterValue::None) => erased(lhs.is_none()), - (op, _) => { - return Err(GraphError::InvalidGqlFilter(format!( - "operator {op:?} received an incompatible value shape" - ))) - } - }) -} - -/// Walks a property condition tree over a value expression: wrapper conditions -/// extend the expression (leading form, outermost applied first), boolean -/// combinators branch, leaves become predicates. -fn lower_prop_condition( - lhs: Arc, - name_for_errors: &str, - cond: &PropCondition, -) -> Result { - use PropCondition::*; - match cond { - And(list) => combine_all( - list.iter() - .map(|c| lower_prop_condition(lhs.clone(), name_for_errors, c)), - false, - "and", - ), - Or(list) => combine_all( - list.iter() - .map(|c| lower_prop_condition(lhs.clone(), name_for_errors, c)), - true, - "or", - ), - Not(inner) => Ok( - Arc::new(lower_prop_condition(lhs, name_for_errors, inner.deref())?.not()) as DynFilter, - ), - First(inner) => lower_prop_condition( - Arc::new(EntityAggOps::first(lhs)), - name_for_errors, - inner.deref(), - ), - Last(inner) => lower_prop_condition( - Arc::new(EntityAggOps::last(lhs)), - name_for_errors, - inner.deref(), - ), - Any(inner) => lower_prop_condition( - Arc::new(EntityExprFilterOps::any(lhs)), - name_for_errors, - inner.deref(), - ), - All(inner) => lower_prop_condition( - Arc::new(EntityExprFilterOps::all(lhs)), - name_for_errors, - inner.deref(), - ), - Sum(inner) => lower_prop_condition( - Arc::new(EntityAggOps::sum(lhs)), - name_for_errors, - inner.deref(), - ), - Avg(inner) => lower_prop_condition( - Arc::new(EntityAggOps::avg(lhs)), - name_for_errors, - inner.deref(), - ), - Min(inner) => lower_prop_condition( - Arc::new(EntityAggOps::min(lhs)), - name_for_errors, - inner.deref(), - ), - Max(inner) => lower_prop_condition( - Arc::new(EntityAggOps::max(lhs)), - name_for_errors, - inner.deref(), - ), - Len(inner) => lower_prop_condition( - Arc::new(EntityAggOps::len(lhs)), - name_for_errors, - inner.deref(), - ), - leaf => { - let (op, value) = translate_prop_leaf_to_filter(name_for_errors, leaf)?; - apply_leaf(lhs, op, value) - } - } -} - -/// Applies one translated built-in-field predicate to a field expression. -fn apply_field_leaf( - lhs: Arc, - op: FilterOperator, - value: FilterValue, -) -> Result { - use FilterOperator as FO; - let single = |v: FilterValue| -> Result { - Ok(match v { - FilterValue::ID(gid) => gid.into_prop(), - FilterValue::Single(s) => Prop::str(s), - other => { - return Err(GraphError::InvalidGqlFilter(format!( - "expected a single value, got {other:?}" - ))) - } - }) - }; - let set = |v: FilterValue| -> Result, GraphError> { - Ok(match v { - FilterValue::IDSet(gids) => gids.iter().map(|g| g.clone().into_prop()).collect(), - FilterValue::Set(strings) => strings.iter().map(|s| Prop::str(s.to_string())).collect(), - other => { - return Err(GraphError::InvalidGqlFilter(format!( - "expected a list of values, got {other:?}" - ))) - } - }) - }; - Ok(match op { - FO::Eq => erased(lhs.eq(single(value)?)), - FO::Ne => erased(lhs.ne(single(value)?)), - FO::Gt => erased(lhs.gt(single(value)?)), - FO::Ge => erased(lhs.ge(single(value)?)), - FO::Lt => erased(lhs.lt(single(value)?)), - FO::Le => erased(lhs.le(single(value)?)), - FO::StartsWith => erased(lhs.starts_with(single(value)?)), - FO::EndsWith => erased(lhs.ends_with(single(value)?)), - FO::Contains => erased(lhs.contains(single(value)?)), - FO::NotContains => erased(lhs.not_contains(single(value)?)), - FO::FuzzySearch { - levenshtein_distance, - prefix_match, - } => erased(lhs.fuzzy_search(single(value)?, levenshtein_distance, prefix_match)), - FO::IsIn => erased(lhs.is_in(set(value)?)), - FO::IsNotIn => erased(lhs.is_not_in(set(value)?)), - FO::IsSome => erased(lhs.is_some()), - FO::IsNone => erased(lhs.is_none()), - }) -} - -fn node_field_lhs(field: NodeField) -> Arc { - match field { - NodeField::NodeId => Arc::new(NodeFilter.id()), - NodeField::NodeName => Arc::new(NodeFilter.name()), - NodeField::NodeType => Arc::new(NodeFilter.node_type()), - } -} - -fn node_field_filter(field: NodeField, cond: &NodeFieldCondition) -> Result { - let (_, value, op) = translate_node_field_where(field, cond)?; - apply_field_leaf(node_field_lhs(field), op, value) -} - -pub(crate) fn lower_node_filter(filter: &GqlNodeFilter) -> Result { - use GqlNodeFilter::*; - Ok(match filter { - Id(f) => node_field_filter(NodeField::NodeId, &f.where_)?, - Name(f) => node_field_filter(NodeField::NodeName, &f.where_)?, - NodeType(f) => node_field_filter(NodeField::NodeType, &f.where_)?, - Degree(degree) => { - let lhs: Arc = match degree.direction.into() { - raphtory_api::core::Direction::BOTH => Arc::new(NodeFilter.degree()), - raphtory_api::core::Direction::IN => Arc::new(NodeFilter.in_degree()), - raphtory_api::core::Direction::OUT => Arc::new(NodeFilter.out_degree()), - }; - let field_name: String = degree.direction.into(); - lower_prop_condition(lhs, &field_name, °ree.where_)? - } - Property(prop) => lower_prop_condition( - Arc::new(PropertyExprFactory::property(&NodeFilter, &prop.name)), - &prop.name, - &prop.where_, - )?, - Metadata(prop) => lower_prop_condition( - Arc::new(PropertyExprFactory::metadata(&NodeFilter, &prop.name)), - &prop.name, - &prop.where_, - )?, - TemporalProperty(prop) => { - let temporal = PropertyExprFactory::property(&NodeFilter, &prop.name).temporal(); - lower_prop_condition(Arc::new(temporal), &prop.name, &prop.where_)? - } - And(filters) => combine_all(filters.iter().map(lower_node_filter), false, "and")?, - Or(filters) => combine_all(filters.iter().map(lower_node_filter), true, "or")?, - Not(inner) => Arc::new(lower_node_filter(inner.deref())?.not()) as DynFilter, - Window(w) => erased(Windowed::new( - w.start.clone().into_time(), - w.end.clone().into_time(), - lower_node_filter(w.expr.deref())?, - )), - At(t) => { - let et = t.time.clone().into_time(); - erased(Windowed::new( - et, - EventTime::end(et.t().saturating_add(1)), - lower_node_filter(t.expr.deref())?, - )) - } - Before(t) => erased(Windowed::new( - EventTime::start(i64::MIN), - EventTime::end(t.time.clone().into_time().t()), - lower_node_filter(t.expr.deref())?, - )), - After(t) => erased(Windowed::new( - EventTime::start(t.time.clone().into_time().t().saturating_add(1)), - EventTime::end(i64::MAX), - lower_node_filter(t.expr.deref())?, - )), - Latest(u) => erased(LatestWrap::new(lower_node_filter(u.expr.deref())?)), - SnapshotAt(t) => erased(SnapshotAtWrap::new( - t.time.clone().into_time(), - lower_node_filter(t.expr.deref())?, - )), - SnapshotLatest(u) => erased(SnapshotLatestWrap::new(lower_node_filter(u.expr.deref())?)), - Layers(l) => erased(Layered::new( - Layer::from(l.names.clone()), - lower_node_filter(l.expr.deref())?, - )), - IsActive(true) => erased(NodeFilter.is_active()), - IsActive(false) => Arc::new(erased(NodeFilter.is_active()).not()) as DynFilter, - }) -} - -fn edge_prop_lhs(exploded: bool, kind: PropKind, name: &str) -> Arc { - match (exploded, kind) { - (false, PropKind::Property) => Arc::new(PropertyExprFactory::property(&EdgeFilter, name)), - (false, PropKind::Metadata) => Arc::new(PropertyExprFactory::metadata(&EdgeFilter, name)), - (false, PropKind::Temporal) => { - Arc::new(PropertyExprFactory::property(&EdgeFilter, name).temporal()) - } - (true, PropKind::Property) => { - Arc::new(PropertyExprFactory::property(&ExplodedEdgeFilter, name)) - } - (true, PropKind::Metadata) => { - Arc::new(PropertyExprFactory::metadata(&ExplodedEdgeFilter, name)) - } - (true, PropKind::Temporal) => { - Arc::new(PropertyExprFactory::property(&ExplodedEdgeFilter, name).temporal()) - } - } -} - -#[derive(Clone, Copy)] -enum PropKind { - Property, - Metadata, - Temporal, -} - -pub(crate) fn lower_edge_filter(filter: &GqlEdgeFilter) -> Result { - use GqlEdgeFilter::*; - Ok(match filter { - Src(inner) => erased(EdgeEndpointNodeFilter { - endpoint: Endpoint::Src, - inner: lower_node_filter(inner.deref())?, - }), - Dst(inner) => erased(EdgeEndpointNodeFilter { - endpoint: Endpoint::Dst, - inner: lower_node_filter(inner.deref())?, - }), - Property(prop) => lower_prop_condition( - edge_prop_lhs(false, PropKind::Property, &prop.name), - &prop.name, - &prop.where_, - )?, - Metadata(prop) => lower_prop_condition( - edge_prop_lhs(false, PropKind::Metadata, &prop.name), - &prop.name, - &prop.where_, - )?, - TemporalProperty(prop) => lower_prop_condition( - edge_prop_lhs(false, PropKind::Temporal, &prop.name), - &prop.name, - &prop.where_, - )?, - And(filters) => combine_all(filters.iter().map(lower_edge_filter), false, "and")?, - Or(filters) => combine_all(filters.iter().map(lower_edge_filter), true, "or")?, - Not(inner) => Arc::new(lower_edge_filter(inner.deref())?.not()) as DynFilter, - Window(w) => erased(Windowed::new( - w.start.clone().into_time(), - w.end.clone().into_time(), - lower_edge_filter(w.expr.deref())?, - )), - At(t) => { - let et = t.time.clone().into_time(); - erased(Windowed::new( - et, - EventTime::end(et.t().saturating_add(1)), - lower_edge_filter(t.expr.deref())?, - )) - } - Before(t) => erased(Windowed::new( - EventTime::start(i64::MIN), - EventTime::end(t.time.clone().into_time().t()), - lower_edge_filter(t.expr.deref())?, - )), - After(t) => erased(Windowed::new( - EventTime::start(t.time.clone().into_time().t().saturating_add(1)), - EventTime::end(i64::MAX), - lower_edge_filter(t.expr.deref())?, - )), - Latest(u) => erased(LatestWrap::new(lower_edge_filter(u.expr.deref())?)), - SnapshotAt(t) => erased(SnapshotAtWrap::new( - t.time.clone().into_time(), - lower_edge_filter(t.expr.deref())?, - )), - SnapshotLatest(u) => erased(SnapshotLatestWrap::new(lower_edge_filter(u.expr.deref())?)), - Layers(l) => erased(Layered::new( - Layer::from(l.names.clone()), - lower_edge_filter(l.expr.deref())?, - )), - IsActive(v) => bool_leaf(erased(EdgeFilter.is_active()), *v), - IsValid(v) => bool_leaf(erased(EdgeFilter.is_valid()), *v), - IsDeleted(v) => bool_leaf(erased(EdgeFilter.is_deleted()), *v), - IsSelfLoop(v) => bool_leaf(erased(EdgeFilter.is_self_loop()), *v), - }) -} - -fn bool_leaf(filter: DynFilter, wanted: bool) -> DynFilter { - if wanted { - filter - } else { - Arc::new(filter.not()) as DynFilter - } -} - -pub(crate) fn lower_exploded_edge_filter( - filter: &GqlExplodedEdgeFilter, -) -> Result { - use GqlExplodedEdgeFilter::*; - Ok(match filter { - Src(inner) => erased(EdgeEndpointNodeFilter { - endpoint: Endpoint::Src, - inner: lower_node_filter(inner.deref())?, - }), - Dst(inner) => erased(EdgeEndpointNodeFilter { - endpoint: Endpoint::Dst, - inner: lower_node_filter(inner.deref())?, - }), - Property(prop) => lower_prop_condition( - edge_prop_lhs(true, PropKind::Property, &prop.name), - &prop.name, - &prop.where_, - )?, - Metadata(prop) => lower_prop_condition( - edge_prop_lhs(true, PropKind::Metadata, &prop.name), - &prop.name, - &prop.where_, - )?, - TemporalProperty(prop) => lower_prop_condition( - edge_prop_lhs(true, PropKind::Temporal, &prop.name), - &prop.name, - &prop.where_, - )?, - And(filters) => combine_all(filters.iter().map(lower_exploded_edge_filter), false, "and")?, - Or(filters) => combine_all(filters.iter().map(lower_exploded_edge_filter), true, "or")?, - Not(inner) => Arc::new(lower_exploded_edge_filter(inner.deref())?.not()) as DynFilter, - Window(w) => erased(Windowed::new( - w.start.clone().into_time(), - w.end.clone().into_time(), - lower_exploded_edge_filter(w.expr.deref())?, - )), - At(t) => { - let et = t.time.clone().into_time(); - erased(Windowed::new( - et, - EventTime::end(et.t().saturating_add(1)), - lower_exploded_edge_filter(t.expr.deref())?, - )) - } - Before(t) => erased(Windowed::new( - EventTime::start(i64::MIN), - EventTime::end(t.time.clone().into_time().t()), - lower_exploded_edge_filter(t.expr.deref())?, - )), - After(t) => erased(Windowed::new( - EventTime::start(t.time.clone().into_time().t().saturating_add(1)), - EventTime::end(i64::MAX), - lower_exploded_edge_filter(t.expr.deref())?, - )), - Latest(u) => erased(LatestWrap::new(lower_exploded_edge_filter(u.expr.deref())?)), - SnapshotAt(t) => erased(SnapshotAtWrap::new( - t.time.clone().into_time(), - lower_exploded_edge_filter(t.expr.deref())?, - )), - SnapshotLatest(u) => erased(SnapshotLatestWrap::new(lower_exploded_edge_filter( - u.expr.deref(), - )?)), - Layers(l) => erased(Layered::new( - Layer::from(l.names.clone()), - lower_exploded_edge_filter(l.expr.deref())?, - )), - IsActive(v) => bool_leaf(erased(ExplodedEdgeFilter.is_active()), *v), - IsValid(v) => bool_leaf(erased(ExplodedEdgeFilter.is_valid()), *v), - IsDeleted(v) => bool_leaf(erased(ExplodedEdgeFilter.is_deleted()), *v), - IsSelfLoop(v) => bool_leaf(erased(ExplodedEdgeFilter.is_self_loop()), *v), - }) -} diff --git a/raphtory-graphql/src/model/graph/filter_expr_input.rs b/raphtory-graphql/src/model/graph/filter_expr_input.rs new file mode 100644 index 0000000000..e012b3d27b --- /dev/null +++ b/raphtory-graphql/src/model/graph/filter_expr_input.rs @@ -0,0 +1,826 @@ +//! The GraphQL form of the filter tree. +//! +//! One input type per node of [`tree::FilterExpr`] / [`tree::Expr`], named the +//! same way, so a filter written in python, rust or a GraphQL document is the +//! same tree spelled in three syntaxes. Both sides of a comparison are +//! expressions; a constant is the `const` expression. +//! +//! ```graphql +//! filter(expr: { gt: { lhs: { read: { entity: NODE, target: { degree: BOTH } } }, +//! rhs: { read: { entity: NODE, target: { degree: IN } } } } }) +//! ``` + +use crate::model::graph::{ + filtering::{Window, Wrapped}, + property::Value, + timeindex::GqlTimeInput, +}; +use dynamic_graphql::{Enum, InputObject, OneOfInput}; +use raphtory::{ + db::{ + api::{ + state::NodeOp, + view::internal::{DynGraphArc, GraphView}, + }, + graph::views::filter::{ + model::{ + edge_filter::Endpoint, + tree::{ + self, Agg, CmpOp, Entity, Field, FilterExpr, Qual, Scope, StrOp, Structural, + Target, ViewOp, OPAQUE_FILTER_ERROR, + }, + DynFilter, + }, + CreateFilter, + }, + }, + errors::GraphError, +}; +use raphtory_api::core::{ + entities::properties::prop::Prop, + storage::timeindex::EventTime, + utils::time::{InputTime, IntoTime}, + Direction, +}; +use serde::{Deserialize, Serialize}; +use std::{ops::Deref, sync::Arc}; + +/// The kind of thing a filter tests: a node, an edge, or one edge update. +#[derive(Enum, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[graphql(name = "Entity")] +pub enum GqlEntity { + Node, + Edge, + ExplodedEdge, +} + +/// Which end of an edge a read looks at. +#[derive(Enum, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[graphql(name = "Endpoint")] +pub enum GqlEndpoint { + Src, + Dst, +} + +/// A built-in node field. +#[derive(Enum, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[graphql(name = "NodeFieldName")] +pub enum GqlNodeField { + Id, + Name, + NodeType, +} + +/// One view restriction, applied in list order. +#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[graphql(name = "ViewOp")] +pub enum GqlViewOp { + Window(Window), + At(GqlTimeInput), + After(GqlTimeInput), + Before(GqlTimeInput), + Latest(bool), + SnapshotAt(GqlTimeInput), + SnapshotLatest(bool), + Layers(Vec), +} + +/// The direction a node degree counts. +#[derive(Enum, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DegreeDirection { + In, + Out, + Both, +} + +impl From for Direction { + fn from(d: DegreeDirection) -> Self { + match d { + DegreeDirection::In => Direction::IN, + DegreeDirection::Out => Direction::OUT, + DegreeDirection::Both => Direction::BOTH, + } + } +} + +/// What a read selects on its entity. +#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[graphql(name = "Target")] +pub enum GqlTarget { + /// A built-in node field. + Field(GqlNodeField), + /// A node degree in a direction. + Degree(DegreeDirection), + /// A property, by name. + Property(String), + /// A metadata entry, by name. + Metadata(String), +} + +/// Where a value is read: the entity, the views to read it through, and for an +/// edge optionally one of its endpoint nodes. +#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] +#[graphql(name = "Scope")] +pub struct GqlScope { + pub entity: GqlEntity, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub views: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub endpoint: Option, +} + +/// A value read from an entity. +#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] +#[graphql(name = "Read")] +pub struct GqlRead { + pub entity: GqlEntity, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub views: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub endpoint: Option, + pub target: GqlTarget, +} + +/// A value: what stands on either side of a comparison. +#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[graphql(name = "Expr")] +pub enum GqlExpr { + /// A literal. + Const(Value), + /// A field, degree, property or metadata read from an entity. + Read(GqlRead), + /// The full history of a property instead of its latest value. + Temporal(Wrapped), + Sum(Wrapped), + Avg(Wrapped), + Min(Wrapped), + Max(Wrapped), + First(Wrapped), + Last(Wrapped), + Len(Wrapped), + /// The predicate holds if it holds for any element. + Any(Wrapped), + /// The predicate holds if it holds for every element. + All(Wrapped), +} + +/// Two expressions to compare. +#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] +#[graphql(name = "Cmp")] +pub struct GqlCmp { + pub lhs: GqlExpr, + pub rhs: GqlExpr, +} + +/// A fuzzy string match: `lhs` is within `levenshteinDistance` edits of +/// `rhs`, optionally matching by prefix. +#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] +#[graphql(name = "FuzzyCmp")] +pub struct GqlFuzzyCmp { + pub lhs: GqlExpr, + pub rhs: GqlExpr, + pub levenshtein_distance: usize, + pub prefix_match: bool, +} + +/// A membership test. `values` is a list; a policy may also leave a single +/// placeholder here (`{"var": …}`) that resolves to the list per caller. +#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] +#[graphql(name = "Membership")] +pub struct GqlMembership { + pub expr: GqlExpr, + pub values: Value, +} + +/// The filter itself: a yes/no over an entity. +#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[graphql(name = "FilterExpr")] +pub enum GqlFilter { + Eq(GqlCmp), + Ne(GqlCmp), + Lt(GqlCmp), + Le(GqlCmp), + Gt(GqlCmp), + Ge(GqlCmp), + StartsWith(GqlCmp), + EndsWith(GqlCmp), + Contains(GqlCmp), + NotContains(GqlCmp), + FuzzySearch(GqlFuzzyCmp), + IsSome(Wrapped), + IsNone(Wrapped), + IsIn(GqlMembership), + IsNotIn(GqlMembership), + IsActive(GqlScope), + IsValid(GqlScope), + IsDeleted(GqlScope), + IsSelfLoop(GqlScope), + /// A graph-level view with no predicate: the result is the view. + View(Vec), + And(Vec), + Or(Vec), + Not(Wrapped), +} + +// ── GraphQL → tree ─────────────────────────────────────────────────────────── + +fn invalid(msg: impl Into) -> GraphError { + GraphError::InvalidGqlFilter(msg.into()) +} + +impl From for Entity { + fn from(e: GqlEntity) -> Self { + match e { + GqlEntity::Node => Entity::Node, + GqlEntity::Edge => Entity::Edge, + GqlEntity::ExplodedEdge => Entity::ExplodedEdge, + } + } +} + +impl From for Endpoint { + fn from(e: GqlEndpoint) -> Self { + match e { + GqlEndpoint::Src => Endpoint::Src, + GqlEndpoint::Dst => Endpoint::Dst, + } + } +} + +impl From for Field { + fn from(f: GqlNodeField) -> Self { + match f { + GqlNodeField::Id => Field::Id, + GqlNodeField::Name => Field::Name, + GqlNodeField::NodeType => Field::NodeType, + } + } +} + +impl TryFrom for ViewOp { + type Error = GraphError; + + /// `latest: false` and `snapshotLatest: false` are refused rather than + /// ignored: a view op that is not applied has no place in a view list. + fn try_from(op: GqlViewOp) -> Result { + Ok(match op { + GqlViewOp::Window(w) => ViewOp::Window { + start: w.start.into_time(), + end: w.end.into_time(), + }, + GqlViewOp::At(t) => ViewOp::At(t.into_time()), + GqlViewOp::After(t) => ViewOp::After(t.into_time()), + GqlViewOp::Before(t) => ViewOp::Before(t.into_time()), + GqlViewOp::Latest(true) => ViewOp::Latest, + GqlViewOp::Latest(false) => return Err(invalid("latest: false is not a view")), + GqlViewOp::SnapshotAt(t) => ViewOp::SnapshotAt(t.into_time()), + GqlViewOp::SnapshotLatest(true) => ViewOp::SnapshotLatest, + GqlViewOp::SnapshotLatest(false) => { + return Err(invalid("snapshotLatest: false is not a view")) + } + GqlViewOp::Layers(names) => ViewOp::Layers(names), + }) + } +} + +fn view_ops(views: Option>) -> Result, GraphError> { + views + .unwrap_or_default() + .into_iter() + .map(ViewOp::try_from) + .collect() +} + +fn scope( + entity: GqlEntity, + views: Option>, + endpoint: Option, +) -> Result { + Ok(Scope { + entity: entity.into(), + views: view_ops(views)?, + endpoint: endpoint.map(Endpoint::from), + }) +} + +impl TryFrom for Scope { + type Error = GraphError; + + fn try_from(s: GqlScope) -> Result { + scope(s.entity, s.views, s.endpoint) + } +} + +impl From for Target { + fn from(t: GqlTarget) -> Self { + match t { + GqlTarget::Field(f) => Target::Field(f.into()), + GqlTarget::Degree(d) => Target::Degree(d.into()), + GqlTarget::Property(name) => Target::Property(name), + GqlTarget::Metadata(name) => Target::Metadata(name), + } + } +} + +fn prop(value: Value) -> Result { + Prop::try_from(value).map_err(|e| invalid(format!("invalid constant: {e}"))) +} + +fn inner(expr: &Wrapped) -> Result, GraphError> { + Ok(Box::new(tree::Expr::try_from(expr.deref().clone())?)) +} + +impl TryFrom for tree::Expr { + type Error = GraphError; + + fn try_from(expr: GqlExpr) -> Result { + use tree::Expr as E; + Ok(match expr { + GqlExpr::Const(value) => E::Const(prop(value)?), + GqlExpr::Read(read) => E::Read { + scope: scope(read.entity, read.views, read.endpoint)?, + target: read.target.into(), + }, + GqlExpr::Temporal(e) => E::Temporal(inner(&e)?), + GqlExpr::Sum(e) => E::Agg(Agg::Sum, inner(&e)?), + GqlExpr::Avg(e) => E::Agg(Agg::Avg, inner(&e)?), + GqlExpr::Min(e) => E::Agg(Agg::Min, inner(&e)?), + GqlExpr::Max(e) => E::Agg(Agg::Max, inner(&e)?), + GqlExpr::First(e) => E::Agg(Agg::First, inner(&e)?), + GqlExpr::Last(e) => E::Agg(Agg::Last, inner(&e)?), + GqlExpr::Len(e) => E::Agg(Agg::Len, inner(&e)?), + GqlExpr::Any(e) => E::Qual(Qual::Any, inner(&e)?), + GqlExpr::All(e) => E::Qual(Qual::All, inner(&e)?), + }) + } +} + +fn cmp(op: CmpOp, c: GqlCmp) -> Result { + Ok(tree::FilterExpr::Cmp { + op, + lhs: c.lhs.try_into()?, + rhs: c.rhs.try_into()?, + }) +} + +fn str_op(op: StrOp, c: GqlCmp) -> Result { + Ok(tree::FilterExpr::Str { + op, + lhs: c.lhs.try_into()?, + rhs: c.rhs.try_into()?, + }) +} + +fn membership(m: GqlMembership, negated: bool) -> Result { + let op = if negated { "isNotIn" } else { "isIn" }; + let values = match m.values { + Value::List(items) => items.into_iter().map(prop).collect::, _>>()?, + other => return Err(invalid(format!("{op} requires a list value, got {other}"))), + }; + Ok(tree::FilterExpr::In { + expr: m.expr.try_into()?, + values, + negated, + }) +} + +fn structural(s: GqlScope, pred: Structural) -> Result { + Ok(tree::FilterExpr::Structural { + scope: s.try_into()?, + pred, + }) +} + +impl TryFrom for tree::FilterExpr { + type Error = GraphError; + + fn try_from(filter: GqlFilter) -> Result { + use tree::FilterExpr as F; + Ok(match filter { + GqlFilter::Eq(c) => cmp(CmpOp::Eq, c)?, + GqlFilter::Ne(c) => cmp(CmpOp::Ne, c)?, + GqlFilter::Lt(c) => cmp(CmpOp::Lt, c)?, + GqlFilter::Le(c) => cmp(CmpOp::Le, c)?, + GqlFilter::Gt(c) => cmp(CmpOp::Gt, c)?, + GqlFilter::Ge(c) => cmp(CmpOp::Ge, c)?, + GqlFilter::StartsWith(c) => str_op(StrOp::StartsWith, c)?, + GqlFilter::EndsWith(c) => str_op(StrOp::EndsWith, c)?, + GqlFilter::Contains(c) => str_op(StrOp::Contains, c)?, + GqlFilter::NotContains(c) => str_op(StrOp::NotContains, c)?, + GqlFilter::FuzzySearch(f) => F::Str { + op: StrOp::FuzzySearch { + levenshtein_distance: f.levenshtein_distance, + prefix_match: f.prefix_match, + }, + lhs: f.lhs.try_into()?, + rhs: f.rhs.try_into()?, + }, + GqlFilter::IsSome(e) => F::IsSome(e.deref().clone().try_into()?), + GqlFilter::IsNone(e) => F::IsNone(e.deref().clone().try_into()?), + GqlFilter::IsIn(m) => membership(m, false)?, + GqlFilter::IsNotIn(m) => membership(m, true)?, + GqlFilter::IsActive(s) => structural(s, Structural::IsActive)?, + GqlFilter::IsValid(s) => structural(s, Structural::IsValid)?, + GqlFilter::IsDeleted(s) => structural(s, Structural::IsDeleted)?, + GqlFilter::IsSelfLoop(s) => structural(s, Structural::IsSelfLoop)?, + GqlFilter::View(ops) => F::View(view_ops(Some(ops))?), + GqlFilter::And(items) => F::And( + items + .into_iter() + .map(F::try_from) + .collect::, _>>()?, + ), + GqlFilter::Or(items) => F::Or( + items + .into_iter() + .map(F::try_from) + .collect::, _>>()?, + ), + GqlFilter::Not(inner) => F::Not(Box::new(inner.deref().clone().try_into()?)), + }) + } +} + +// ── tree → GraphQL ─────────────────────────────────────────────────────────── +// +// Clients build trees and send them; this is the spelling they send. + +impl From for GqlEntity { + fn from(e: Entity) -> Self { + match e { + Entity::Node => GqlEntity::Node, + Entity::Edge => GqlEntity::Edge, + Entity::ExplodedEdge => GqlEntity::ExplodedEdge, + } + } +} + +impl From for GqlEndpoint { + fn from(e: Endpoint) -> Self { + match e { + Endpoint::Src => GqlEndpoint::Src, + Endpoint::Dst => GqlEndpoint::Dst, + } + } +} + +impl From for GqlNodeField { + fn from(f: Field) -> Self { + match f { + Field::Id => GqlNodeField::Id, + Field::Name => GqlNodeField::Name, + Field::NodeType => GqlNodeField::NodeType, + } + } +} + +fn time(t: EventTime) -> GqlTimeInput { + GqlTimeInput(InputTime::Indexed(t.0, t.1)) +} + +impl From<&ViewOp> for GqlViewOp { + fn from(op: &ViewOp) -> Self { + match op { + ViewOp::Window { start, end } => GqlViewOp::Window(Window { + start: time(*start), + end: time(*end), + }), + ViewOp::At(t) => GqlViewOp::At(time(*t)), + ViewOp::After(t) => GqlViewOp::After(time(*t)), + ViewOp::Before(t) => GqlViewOp::Before(time(*t)), + ViewOp::Latest => GqlViewOp::Latest(true), + ViewOp::SnapshotAt(t) => GqlViewOp::SnapshotAt(time(*t)), + ViewOp::SnapshotLatest => GqlViewOp::SnapshotLatest(true), + ViewOp::Layers(names) => GqlViewOp::Layers(names.clone()), + } + } +} + +fn views(v: &[ViewOp]) -> Option> { + (!v.is_empty()).then(|| v.iter().map(GqlViewOp::from).collect()) +} + +impl From<&Scope> for GqlScope { + fn from(s: &Scope) -> Self { + GqlScope { + entity: s.entity.into(), + views: views(&s.views), + endpoint: s.endpoint.map(GqlEndpoint::from), + } + } +} + +fn direction(d: Direction) -> DegreeDirection { + match d { + Direction::IN => DegreeDirection::In, + Direction::OUT => DegreeDirection::Out, + Direction::BOTH => DegreeDirection::Both, + } +} + +impl From<&Target> for GqlTarget { + fn from(t: &Target) -> Self { + match t { + Target::Field(f) => GqlTarget::Field((*f).into()), + Target::Degree(d) => GqlTarget::Degree(direction(*d)), + Target::Property(name) => GqlTarget::Property(name.clone()), + Target::Metadata(name) => GqlTarget::Metadata(name.clone()), + } + } +} + +fn value(p: &Prop) -> Result { + Value::try_from(p).map_err(|e| invalid(format!("constant has no wire form: {e}"))) +} + +fn wrapped(e: &tree::Expr) -> Result, GraphError> { + Ok(Wrapped::from(GqlExpr::try_from(e)?)) +} + +impl TryFrom<&tree::Expr> for GqlExpr { + type Error = GraphError; + + fn try_from(expr: &tree::Expr) -> Result { + use tree::Expr as E; + Ok(match expr { + E::Const(p) => GqlExpr::Const(value(p)?), + E::Read { scope, target } => GqlExpr::Read(GqlRead { + entity: scope.entity.into(), + views: views(&scope.views), + endpoint: scope.endpoint.map(GqlEndpoint::from), + target: target.into(), + }), + E::Temporal(e) => GqlExpr::Temporal(wrapped(e)?), + E::Agg(Agg::Sum, e) => GqlExpr::Sum(wrapped(e)?), + E::Agg(Agg::Avg, e) => GqlExpr::Avg(wrapped(e)?), + E::Agg(Agg::Min, e) => GqlExpr::Min(wrapped(e)?), + E::Agg(Agg::Max, e) => GqlExpr::Max(wrapped(e)?), + E::Agg(Agg::First, e) => GqlExpr::First(wrapped(e)?), + E::Agg(Agg::Last, e) => GqlExpr::Last(wrapped(e)?), + E::Agg(Agg::Len, e) => GqlExpr::Len(wrapped(e)?), + E::Qual(Qual::Any, e) => GqlExpr::Any(wrapped(e)?), + E::Qual(Qual::All, e) => GqlExpr::All(wrapped(e)?), + }) + } +} + +fn gql_cmp(lhs: &tree::Expr, rhs: &tree::Expr) -> Result { + Ok(GqlCmp { + lhs: lhs.try_into()?, + rhs: rhs.try_into()?, + }) +} + +impl TryFrom<&tree::FilterExpr> for GqlFilter { + type Error = GraphError; + + fn try_from(filter: &tree::FilterExpr) -> Result { + use tree::FilterExpr as F; + Ok(match filter { + F::Opaque(_) => return Err(invalid(OPAQUE_FILTER_ERROR)), + F::Cmp { op, lhs, rhs } => { + let c = gql_cmp(lhs, rhs)?; + match op { + CmpOp::Eq => GqlFilter::Eq(c), + CmpOp::Ne => GqlFilter::Ne(c), + CmpOp::Lt => GqlFilter::Lt(c), + CmpOp::Le => GqlFilter::Le(c), + CmpOp::Gt => GqlFilter::Gt(c), + CmpOp::Ge => GqlFilter::Ge(c), + } + } + F::Str { op, lhs, rhs } => match op { + StrOp::StartsWith => GqlFilter::StartsWith(gql_cmp(lhs, rhs)?), + StrOp::EndsWith => GqlFilter::EndsWith(gql_cmp(lhs, rhs)?), + StrOp::Contains => GqlFilter::Contains(gql_cmp(lhs, rhs)?), + StrOp::NotContains => GqlFilter::NotContains(gql_cmp(lhs, rhs)?), + StrOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => GqlFilter::FuzzySearch(GqlFuzzyCmp { + lhs: lhs.try_into()?, + rhs: rhs.try_into()?, + levenshtein_distance: *levenshtein_distance, + prefix_match: *prefix_match, + }), + }, + F::IsSome(e) => GqlFilter::IsSome(wrapped(e)?), + F::IsNone(e) => GqlFilter::IsNone(wrapped(e)?), + F::In { + expr, + values, + negated, + } => { + let m = GqlMembership { + expr: expr.try_into()?, + values: Value::List(values.iter().map(value).collect::, _>>()?), + }; + if *negated { + GqlFilter::IsNotIn(m) + } else { + GqlFilter::IsIn(m) + } + } + F::Structural { scope, pred } => { + let s = GqlScope::from(scope); + match pred { + Structural::IsActive => GqlFilter::IsActive(s), + Structural::IsValid => GqlFilter::IsValid(s), + Structural::IsDeleted => GqlFilter::IsDeleted(s), + Structural::IsSelfLoop => GqlFilter::IsSelfLoop(s), + } + } + F::View(ops) => GqlFilter::View(ops.iter().map(GqlViewOp::from).collect()), + F::And(items) => GqlFilter::And( + items + .iter() + .map(GqlFilter::try_from) + .collect::, _>>()?, + ), + F::Or(items) => GqlFilter::Or( + items + .iter() + .map(GqlFilter::try_from) + .collect::, _>>()?, + ), + F::Not(inner) => GqlFilter::Not(Wrapped::from(GqlFilter::try_from(inner.deref())?)), + }) + } +} + +impl TryFrom for GqlFilter { + type Error = GraphError; + + fn try_from(tree: FilterExpr) -> Result { + GqlFilter::try_from(&tree) + } +} + +/// The compiled filter, for callers that apply one filter to several handles. +impl TryFrom for DynFilter { + type Error = GraphError; + + fn try_from(value: GqlFilter) -> Result { + FilterExpr::try_from(value)?.compile() + } +} + +impl CreateFilter for GqlFilter { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = DynGraphArc<'graph> + where + Self: 'graph; + + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = Arc + 'graph> + where + Self: 'graph; + + type FilteredGraph<'graph, G> + = DynGraphArc<'graph> + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + FilterExpr::try_from(self)?.create_filter(graph, filtered) + } + + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + FilterExpr::try_from(self)?.create_node_filter(graph, filtered) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + FilterExpr::try_from(self.clone())?.filter_graph_view(graph) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn read(target: Target) -> tree::Expr { + tree::Expr::Read { + scope: Scope::new(Entity::Node), + target, + } + } + + #[test] + fn a_tree_survives_the_trip_through_the_wire_type_and_json() { + let tree = tree::FilterExpr::And(vec![ + tree::FilterExpr::Cmp { + op: CmpOp::Gt, + lhs: tree::Expr::Agg( + Agg::Sum, + Box::new(tree::Expr::Temporal(Box::new(read(Target::Property( + "score".into(), + ))))), + ), + rhs: tree::Expr::Const(Prop::F64(10.0)), + }, + tree::FilterExpr::Cmp { + op: CmpOp::Gt, + lhs: read(Target::Degree(Direction::BOTH)), + rhs: read(Target::Degree(Direction::IN)), + }, + tree::FilterExpr::Structural { + scope: Scope::new(Entity::Edge) + .with_view(ViewOp::Window { + start: EventTime::from(0), + end: EventTime::from(5), + }) + .with_view(ViewOp::Layers(vec!["knows".into()])), + pred: Structural::IsActive, + }, + tree::FilterExpr::Not(Box::new(tree::FilterExpr::In { + expr: tree::Expr::Read { + scope: Scope::new(Entity::Edge).through(Endpoint::Src), + target: Target::Field(Field::Name), + }, + values: vec![Prop::str("alice"), Prop::str("bob")], + negated: false, + })), + tree::FilterExpr::View(vec![ViewOp::Latest]), + ]); + + let wire = GqlFilter::try_from(&tree).unwrap(); + let json = serde_json::to_string(&wire).unwrap(); + let wire_back: GqlFilter = serde_json::from_str(&json).unwrap(); + let tree_back = tree::FilterExpr::try_from(wire_back).unwrap(); + assert_eq!(tree_back, tree); + } + + #[test] + fn the_json_spelling_is_the_documented_one() { + let tree = tree::FilterExpr::Cmp { + op: CmpOp::Gt, + lhs: read(Target::Degree(Direction::BOTH)), + rhs: read(Target::Degree(Direction::IN)), + }; + let wire = GqlFilter::try_from(&tree).unwrap(); + assert_eq!( + serde_json::to_value(&wire).unwrap(), + serde_json::json!({ + "gt": { + "lhs": { "read": { "entity": "NODE", "target": { "degree": "BOTH" } } }, + "rhs": { "read": { "entity": "NODE", "target": { "degree": "IN" } } } + } + }) + ); + } + + #[test] + fn an_opaque_filter_has_no_wire_form() { + let compiled = tree::FilterExpr::View(vec![ViewOp::Latest]) + .compile() + .unwrap(); + let opaque = tree::FilterExpr::Opaque(tree::OpaqueFilter(compiled)); + let err = GqlFilter::try_from(&opaque).unwrap_err(); + assert!(err.to_string().contains(OPAQUE_FILTER_ERROR), "{err}"); + } + + #[test] + fn a_view_op_that_is_not_applied_is_refused() { + for (op, name) in [ + (GqlViewOp::Latest(false), "latest"), + (GqlViewOp::SnapshotLatest(false), "snapshotLatest"), + ] { + let err = ViewOp::try_from(op).unwrap_err(); + assert!(err.to_string().contains(name), "{err}"); + } + assert_eq!( + ViewOp::try_from(GqlViewOp::Latest(true)).unwrap(), + ViewOp::Latest + ); + } +} diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 21eb1b712e..fd22d28165 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -1,47 +1,23 @@ -use crate::model::graph::{node_id::GqlNodeId, property::Value, timeindex::GqlTimeInput}; +use crate::model::graph::{ + filter_expr_input::GqlFilter, node_id::GqlNodeId, property::Value, timeindex::GqlTimeInput, +}; use async_graphql::dynamic::ValueAccessor; use dynamic_graphql::{ internal::{ FromValue, GetInputTypeRef, InputTypeName, InputValueResult, Register, Registry, TypeName, }, - Enum, InputObject, OneOfInput, + InputObject, OneOfInput, }; use raphtory::{ - db::{ - api::{ - state::NodeOp, - view::internal::{DynGraphArc, GraphView}, - }, - graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, - filter::{Filter, FilterValue, NODE_ID_FIELD, NODE_NAME_FIELD, NODE_TYPE_FIELD}, - filter_operator::FilterOperator, - graph_filter::GraphFilter, - node_filter::CompositeNodeFilter, - property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, - ComposableFilter, DynFilter, DynView, FilterTree, GraphViewOp, ViewWrapOps, - }, - CreateFilter, - }, + db::graph::views::filter::model::{ + filter_operator::FilterOperator, + property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, }, errors::GraphError, }; -use raphtory_api::core::{ - entities::{properties::prop::Prop, Layer, GID}, - storage::timeindex::{AsTime, EventTime}, - Direction, -}; +use raphtory_api::core::entities::properties::prop::Prop; use serde::{Deserialize, Serialize}; -use std::{ - borrow::Cow, - collections::HashSet, - fmt, - fmt::{Display, Formatter}, - ops::Deref, - sync::Arc, -}; +use std::{borrow::Cow, ops::Deref, sync::Arc}; #[derive(InputObject, Clone, Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -88,10 +64,8 @@ pub enum GraphViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// Node filter. - NodeFilter(GqlNodeFilter), - /// Edge filter. - EdgeFilter(GqlEdgeFilter), + /// A filter tree; the entity it tests is written in the tree. + Filter(GqlFilter), } #[derive(OneOfInput, Clone, Debug)] @@ -122,8 +96,8 @@ pub enum NodesViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// Node filter. - NodeFilter(GqlNodeFilter), + /// A filter tree; the entity it tests is written in the tree. + Filter(GqlFilter), /// List of types. TypeFilter(Vec), } @@ -156,8 +130,8 @@ pub enum NodeViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// Node filter. - NodeFilter(GqlNodeFilter), + /// A filter tree; the entity it tests is written in the tree. + Filter(GqlFilter), } #[derive(OneOfInput, Clone, Debug)] @@ -188,8 +162,8 @@ pub enum EdgesViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// Edge filter - EdgeFilter(GqlEdgeFilter), + /// A filter tree; the entity it tests is written in the tree. + Filter(GqlFilter), } #[derive(OneOfInput, Clone, Debug)] @@ -220,8 +194,8 @@ pub enum EdgeViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// Edge filter - EdgeFilter(GqlEdgeFilter), + /// A filter tree; the entity it tests is written in the tree. + Filter(GqlFilter), } #[derive(OneOfInput, Clone, Debug)] @@ -252,119 +226,10 @@ pub enum PathFromNodeViewCollection { ShrinkEnd(GqlTimeInput), } -// The node field a filter targets, as a GraphQL enum value (`NODE_ID`/`NODE_NAME`/`NODE_TYPE`). -#[derive(Enum, Copy, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum NodeField { - /// Node ID field. - /// - /// Represents the graph’s node identifier (numeric or string-backed in the API). - NodeId, - /// Node name field. - /// - /// Represents the human-readable node name (string). - NodeName, - /// Node type field. - /// - /// Represents the optional node type assigned at node creation (string). - NodeType, -} - -impl Display for NodeField { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - NodeField::NodeId => NODE_ID_FIELD, - NodeField::NodeName => NODE_NAME_FIELD, - NodeField::NodeType => NODE_TYPE_FIELD, - } - ) - } -} - -/// Filters an entity property or metadata field by name and condition. -/// -/// This input is used by both node and edge filters when targeting -/// a specific property key (or metadata key) and applying a `PropCondition`. -/// -/// Fields: -/// - `name`: The property key to query. -/// - `where_`: The condition to apply to that property’s value. -/// -/// Example (GraphQL): -/// ```graphql -/// { Property: { name: "weight", where: { Gt: 0.5 } } } -/// ``` -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct PropertyFilterNew { - /// Property (or metadata) key. - pub name: String, - /// Condition applied to the property value. - /// - /// Exposed as `where` in GraphQL. - #[graphql(name = "where")] - #[serde(rename = "where")] - pub where_: PropCondition, -} - -/// Filters nodes by computed degree with a directional scope. -/// -/// `DegreeFilterNew` lets callers filter on: -/// - inbound degree (`IN`), -/// - outbound degree (`OUT`), -/// - or total degree (`BOTH`). -/// -/// The selected degree is compared using the `where` condition. -/// -/// Example (GraphQL): -/// ```graphql -/// { Degree: { direction: BOTH, where: { Gt: 10 } } } -/// ``` - -#[derive(Enum, Copy, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "UPPERCASE")] -pub enum DegreeDirection { - In, - Out, - Both, -} - -impl From for Direction { - fn from(d: DegreeDirection) -> Self { - match d { - DegreeDirection::In => Direction::IN, - DegreeDirection::Out => Direction::OUT, - DegreeDirection::Both => Direction::BOTH, - } - } -} - -impl From for String { - fn from(d: DegreeDirection) -> Self { - match d { - DegreeDirection::In => "in_degree".to_string(), - DegreeDirection::Out => "out_degree".to_string(), - DegreeDirection::Both => "degree".to_string(), - } - } -} - -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct DegreeFilterNew { - pub direction: DegreeDirection, - #[graphql(name = "where")] - #[serde(rename = "where")] - pub where_: PropCondition, -} - /// Boolean expression over a property value. /// -/// `PropCondition` is used inside `PropertyFilterNew.where` to describe -/// how a property’s value should be matched. +/// `PropCondition` is the `where` of a namespace metagraph filter: how one +/// graph-level metadata value, or a graph field, should be matched. /// /// It supports: /// - comparisons (`Eq`, `Gt`, `Le`, …), @@ -449,7 +314,7 @@ pub enum PropCondition { } impl PropCondition { - pub fn op_name(&self) -> &'static str { + fn op_name(&self) -> &'static str { use PropCondition::*; match self { Eq(_) => "eq", @@ -489,1325 +354,190 @@ impl PropCondition { } } -/// Graph view restriction to a time window, optionally chaining another `GraphFilter`. -/// -/// Used by `GqlGraphFilter::Window`. -/// -/// - `start` and `end` define the window (inclusive start, exclusive end). -/// - `expr` optionally nests another graph filter to apply *within* this window. -/// -/// Example (GraphQL): -/// ```graphql -/// { Window: { start: 0, end: 10, expr: { Layers: { names: ["A"] } } } } -/// ``` -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct GraphWindowExpr { - /// Window start time (inclusive). - pub start: GqlTimeInput, - /// Window end time (exclusive). - pub end: GqlTimeInput, - /// Optional nested filter applied after the window restriction. - pub expr: Option>, -} +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Wrapped(Box); -/// Graph view restriction to a single time bound, optionally chaining another `GraphFilter`. -/// -/// Used by `At`, `Before`, and `After` graph filters. -/// -/// Example: -/// `{ At: { time: 5, expr: { Layers: { names: ["L1"] } } } }` -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct GraphTimeExpr { - /// Reference time for the operation. - pub time: GqlTimeInput, - /// Optional nested filter applied after the time restriction. - pub expr: Option>, +impl From for Wrapped { + fn from(inner: T) -> Self { + Wrapped(Box::new(inner)) + } } -/// Graph view restriction that takes only a nested expression. -/// -/// Used for unary view operations like `Latest` and `SnapshotLatest`. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct GraphUnaryExpr { - /// Optional nested filter applied after the unary operation. - pub expr: Option>, +impl Deref for Wrapped { + type Target = T; + fn deref(&self) -> &Self::Target { + self.0.deref() + } } -/// Graph view restriction by layer membership, optionally chaining another `GraphFilter`. -/// -/// Used by `GqlGraphFilter::Layers`. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] +/// Fuzzy string match: passes when the candidate is within `levenshteinDistance` +/// edits of `value` (optionally also matching by prefix). Mirrors the local +/// `fuzzy_search(value, levenshtein_distance, prefix_match)` builder. +#[derive(InputObject, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct GraphLayersExpr { - /// Layer names to include. - pub names: Vec, - /// Optional nested filter applied after the layer restriction. - pub expr: Option>, -} - -/// GraphQL input type for restricting a graph view. -/// -/// `GraphFilter` controls the **evaluation scope** for subsequent node/edge filters: -/// - time windows (`Window`) -/// - time points (`At`) -/// - open-ended ranges (`Before`, `After`) -/// - latest evaluation (`Latest`) -/// - snapshots (`SnapshotAt`, `SnapshotLatest`) -/// - layer membership (`Layers`) -/// -/// These filters can be nested via the `expr` field on the corresponding -/// `*Expr` input objects to form pipelines. -#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] -#[graphql(name = "GraphFilter")] -#[serde(rename_all = "camelCase")] -pub enum GqlGraphFilter { - /// Restrict evaluation to a time window (inclusive start, exclusive end). - Window(GraphWindowExpr), - /// Restrict evaluation to a single point in time. - At(GraphTimeExpr), - /// Restrict evaluation to times strictly before the given time. - Before(GraphTimeExpr), - /// Restrict evaluation to times strictly after the given time. - After(GraphTimeExpr), - - /// Evaluate against the latest available state. - Latest(GraphUnaryExpr), - /// Evaluate against a snapshot of the graph at a given time. - SnapshotAt(GraphTimeExpr), - /// Evaluate against the most recent snapshot of the graph. - SnapshotLatest(GraphUnaryExpr), - - /// Restrict evaluation to one or more layers. - Layers(GraphLayersExpr), -} - -/// A general filter expression — a node filter (`node`), an edge filter (`edge`), a graph/view -/// filter (`graph`, e.g. a layer or window restriction), or an `and`/`or` combination of these -/// (which may mix kinds). Used where an operation accepts any filter, such as scoping a component -/// walk. -#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub enum GqlFilter { - /// Filter by node properties, fields, or temporal state. - Node(GqlNodeFilter), - /// Filter by edge properties, source/destination, or temporal state. - /// (Persisted filters may use the legacy `edge` key.) - #[serde(alias = "edge")] - Edge(GqlEdgeFilter), - /// Filter exploded edges — per-event edge instances — by properties, - /// endpoints, or temporal state, evaluated per event. - ExplodedEdge(GqlExplodedEdgeFilter), - /// Apply a graph-level view (window, snapshot, layer restriction, …). - Graph(GqlGraphFilter), - /// All sub-filters must pass (intersection). - And(Vec), - /// At least one sub-filter must pass (union). - /// Cross-type sub-filters (e.g. `node` and `edge` together) produce a - /// proper graph union: a node is visible if it matches the node filter or - /// has a visible edge, and an edge is visible if it matches the edge - /// filter or both its endpoints are visible. - Or(Vec), - /// Inverts the nested filter. - Not(Wrapped), - - // Flat graph-view spellings — equivalent to wrapping the same expression - // in `graph: {...}`; kept top-level so pre-existing `Graph.filter` - // documents (e.g. `filter(expr: {window: ...})`) remain valid. - /// Restrict evaluation to a time window (inclusive start, exclusive end). - Window(GraphWindowExpr), - /// Restrict evaluation to a single point in time. - At(GraphTimeExpr), - /// Restrict evaluation to times strictly before the given time. - Before(GraphTimeExpr), - /// Restrict evaluation to times strictly after the given time. - After(GraphTimeExpr), - /// Evaluate against the latest available state. - Latest(GraphUnaryExpr), - /// Evaluate against a snapshot of the graph at a given time. - SnapshotAt(GraphTimeExpr), - /// Evaluate against the most recent snapshot of the graph. - SnapshotLatest(GraphUnaryExpr), - /// Restrict evaluation to one or more layers. - Layers(GraphLayersExpr), -} - -impl TryFrom for GqlFilter { - type Error = GraphError; - fn try_from(f: GqlNodeFilter) -> Result { - Ok(GqlFilter::Node(f)) - } +pub struct FuzzySearchExpr { + /// The string to match against. + pub value: String, + /// Maximum Levenshtein edit distance for a match. + pub levenshtein_distance: usize, + /// Whether a prefix match within the distance also passes. + pub prefix_match: bool, } -impl TryFrom for GqlFilter { - type Error = GraphError; - fn try_from(f: GqlEdgeFilter) -> Result { - Ok(GqlFilter::Edge(f)) +impl Register for Wrapped { + fn register(registry: Registry) -> Registry { + registry.register::() } } -impl TryFrom for GqlFilter { - type Error = GraphError; - fn try_from(f: GqlExplodedEdgeFilter) -> Result { - Ok(GqlFilter::ExplodedEdge(f)) +impl FromValue for Wrapped { + fn from_value(value: async_graphql::Result) -> InputValueResult { + T::from_value(value) + .map(|v| Wrapped(Box::new(v))) + .map_err(|e| e.propagate()) } } -impl TryFrom for GqlFilter { - type Error = GraphError; - fn try_from(f: GqlGraphFilter) -> Result { - Ok(GqlFilter::Graph(f)) +impl TypeName for Wrapped { + fn get_type_name() -> Cow<'static, str> { + T::get_type_name() } } +impl InputTypeName for Wrapped {} -impl CreateFilter for GqlFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = DynGraphArc<'graph> - where - Self: 'graph; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = DynGraphArc<'graph> - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - DynFilter::try_from(self)?.create_filter(graph, filtered) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - DynFilter::try_from(self)?.create_node_filter(graph, filtered) - } +fn peel_prop_wrappers_and_collect_ops<'a>( + cond: &'a PropCondition, + ops: &mut Vec, +) -> Option<&'a PropCondition> { + use PropCondition::*; - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - DynFilter::try_from(self.clone())?.filter_graph_view(graph) + match cond { + First(inner) => { + ops.push(Op::First); + Some(inner.deref()) + } + Last(inner) => { + ops.push(Op::Last); + Some(inner.deref()) + } + Any(inner) => { + ops.push(Op::Any); + Some(inner.deref()) + } + All(inner) => { + ops.push(Op::All); + Some(inner.deref()) + } + Sum(inner) => { + ops.push(Op::Sum); + Some(inner.deref()) + } + Avg(inner) => { + ops.push(Op::Avg); + Some(inner.deref()) + } + Min(inner) => { + ops.push(Op::Min); + Some(inner.deref()) + } + Max(inner) => { + ops.push(Op::Max); + Some(inner.deref()) + } + Len(inner) => { + ops.push(Op::Len); + Some(inner.deref()) + } + _ => None, } } -impl TryFrom for GqlFilter { - type Error = GraphError; - fn try_from(f: CompositeNodeFilter) -> Result { - Ok(GqlFilter::Node(f.try_into()?)) +fn require_string_value(op: &str, v: &Value) -> Result { + if let Value::Str(s) = v { + Ok(s.clone()) + } else { + Err(GraphError::InvalidGqlFilter(format!( + "{op} requires a string value, got {v}" + ))) } } -impl TryFrom for GqlFilter { - type Error = GraphError; - fn try_from(f: CompositeEdgeFilter) -> Result { - Ok(GqlFilter::Edge(f.try_into()?)) +fn require_prop_list_value(op: &str, v: &Value) -> Result { + if let Value::List(vs) = v { + let props = vs + .iter() + .cloned() + .map(Prop::try_from) + .collect::, _>>()?; + Ok(PropertyFilterValue::Set(Arc::new( + props.into_iter().collect(), + ))) + } else { + Err(GraphError::InvalidGqlFilter(format!( + "{op} requires a list value, got {v}" + ))) } } -impl TryFrom for GqlFilter { - type Error = GraphError; - fn try_from(f: CompositeExplodedEdgeFilter) -> Result { - Ok(GqlFilter::ExplodedEdge(f.try_into()?)) - } -} +fn translate_prop_leaf_to_filter( + name_for_errors: &str, + cmp: &PropCondition, +) -> Result<(FilterOperator, PropertyFilterValue), GraphError> { + use FilterOperator as FO; + use PropCondition::*; -/// Build the nested wire form of a graph-view chain: ops arrive in -/// application order and each later op wraps outside the previous one, so the -/// outermost `expr` level is the last op applied. `Layer::All` ops restrict -/// nothing and are dropped. -fn view_ops_to_graph_filter(ops: Vec) -> Result { - let time_input = |t: EventTime| { - GqlTimeInput(raphtory_api::core::utils::time::InputTime::Indexed( - t.t(), - t.i(), - )) + let single = |v: &Value| -> Result { + Ok(PropertyFilterValue::Single(Prop::try_from(v.clone())?)) }; - let mut acc: Option = None; - for op in ops { - let expr = acc.take().map(wrap); - let next = match op { - GraphViewOp::Window { start, end } => GqlGraphFilter::Window(GraphWindowExpr { - start: time_input(start), - end: time_input(end), - expr, - }), - GraphViewOp::Latest => GqlGraphFilter::Latest(GraphUnaryExpr { expr }), - GraphViewOp::SnapshotAt(t) => GqlGraphFilter::SnapshotAt(GraphTimeExpr { - time: time_input(t), - expr, - }), - GraphViewOp::SnapshotLatest => GqlGraphFilter::SnapshotLatest(GraphUnaryExpr { expr }), - GraphViewOp::Layers(layer) => { - if matches!(layer, Layer::All) { - // No restriction — skip the op, keep the accumulated chain. - acc = expr.map(|w| w.deref().clone()); - continue; - } - GqlGraphFilter::Layers(GraphLayersExpr { - names: layer_to_names(&layer)?, - expr, - }) - } - }; - acc = Some(next); - } - acc.ok_or_else(|| GraphError::InvalidGqlFilter("graph-view filter with no restrictions".into())) -} -impl TryFrom for GqlFilter { - type Error = GraphError; - - fn try_from(tree: FilterTree) -> Result { - Ok(match tree { - FilterTree::Node(f) => GqlFilter::Node(f.try_into()?), - FilterTree::Edge(f) => GqlFilter::Edge(f.try_into()?), - FilterTree::ExplodedEdge(f) => GqlFilter::ExplodedEdge(f.try_into()?), - FilterTree::View(ops) => GqlFilter::Graph(view_ops_to_graph_filter(ops)?), - FilterTree::And(items) => GqlFilter::And( - items - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()?, - ), - FilterTree::Or(items) => GqlFilter::Or( - items - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()?, - ), - FilterTree::Not(inner) => GqlFilter::Not(wrap((*inner).try_into()?)), - }) - } -} + Ok(match cmp { + Eq(v) => (FO::Eq, single(v)?), + Ne(v) => (FO::Ne, single(v)?), + Gt(v) => (FO::Gt, single(v)?), + Ge(v) => (FO::Ge, single(v)?), + Lt(v) => (FO::Lt, single(v)?), + Le(v) => (FO::Le, single(v)?), -impl TryFrom for DynFilter { - type Error = GraphError; + StartsWith(v) => ( + FO::StartsWith, + PropertyFilterValue::Single(Prop::Str(require_string_value(cmp.op_name(), v)?.into())), + ), + EndsWith(v) => ( + FO::EndsWith, + PropertyFilterValue::Single(Prop::Str(require_string_value(cmp.op_name(), v)?.into())), + ), - fn try_from(value: GqlFilter) -> Result { - let filter = match value { - GqlFilter::Node(f) => super::expr_lowering::lower_node_filter(&f)?, - GqlFilter::Edge(f) => super::expr_lowering::lower_edge_filter(&f)?, - GqlFilter::ExplodedEdge(f) => super::expr_lowering::lower_exploded_edge_filter(&f)?, - GqlFilter::Graph(f) => DynView::try_from(f)?, - GqlFilter::And(filters) => { - let mut filters = filters.into_iter().map(DynFilter::try_from); - // An empty combinator is almost always a caller bug (a filter - // list built from an empty source). Reject it rather than - // guessing an identity — for `or` in particular, the previous - // fallback (match-everything) inverted the caller's intent, - // which is a fail-open when the filter scopes access control. - // Matches the composite conversions' convention above. - let first = filters.next().transpose()?.ok_or_else(|| { - GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) - })?; - filters.try_fold(first, |combined, filter| { - Ok::<_, GraphError>(Arc::new(combined.and(filter?)) as DynFilter) - })? - } - GqlFilter::Or(filters) => { - let mut filters = filters.into_iter().map(DynFilter::try_from); - let first = filters.next().transpose()?.ok_or_else(|| { - GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) - })?; - filters.try_fold(first, |combined, filter| { - Ok::<_, GraphError>(Arc::new(combined.or(filter?)) as DynFilter) - })? - } - GqlFilter::Not(inner) => { - let inner = DynFilter::try_from(inner.deref().clone())?; - Arc::new(inner.not()) as DynFilter - } - // Flat view spellings delegate to the graph-filter conversion. - GqlFilter::Window(w) => DynView::try_from(GqlGraphFilter::Window(w))?, - GqlFilter::At(t) => DynView::try_from(GqlGraphFilter::At(t))?, - GqlFilter::Before(t) => DynView::try_from(GqlGraphFilter::Before(t))?, - GqlFilter::After(t) => DynView::try_from(GqlGraphFilter::After(t))?, - GqlFilter::Latest(u) => DynView::try_from(GqlGraphFilter::Latest(u))?, - GqlFilter::SnapshotAt(t) => DynView::try_from(GqlGraphFilter::SnapshotAt(t))?, - GqlFilter::SnapshotLatest(u) => DynView::try_from(GqlGraphFilter::SnapshotLatest(u))?, - GqlFilter::Layers(l) => DynView::try_from(GqlGraphFilter::Layers(l))?, - }; - Ok(filter) - } -} + Contains(v) => (FO::Contains, single(v)?), + NotContains(v) => (FO::NotContains, single(v)?), -/// Boolean expression over a built-in node field (ID, name, or type). -/// -/// This is used by `NodeFieldWhere.where_` when filtering a specific -/// built-in field. -/// -/// Supports comparisons, string predicates, and set membership. -/// (Presence checks and aggregations are handled via property filters instead.) -#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum NodeFieldCondition { - /// Equality. - Eq(Value), - /// Inequality. - Ne(Value), - /// Greater-than. - Gt(Value), - /// Greater-than-or-equal. - Ge(Value), - /// Less-than. - Lt(Value), - /// Less-than-or-equal. - Le(Value), + IsIn(v) => (FO::IsIn, require_prop_list_value(cmp.op_name(), v)?), + IsNotIn(v) => (FO::IsNotIn, require_prop_list_value(cmp.op_name(), v)?), - /// String prefix match. - StartsWith(Value), - /// String suffix match. - EndsWith(Value), - /// Substring match. - Contains(Value), - /// Negated substring match. - NotContains(Value), + IsSome(true) => (FO::IsSome, PropertyFilterValue::None), + IsNone(true) => (FO::IsNone, PropertyFilterValue::None), + // `isSome: false` is exactly `isNone: true` (and vice versa) — lower + // to the dual operator instead of rejecting. + IsSome(false) => (FO::IsNone, PropertyFilterValue::None), + IsNone(false) => (FO::IsSome, PropertyFilterValue::None), - /// Fuzzy string match (Levenshtein distance, optional prefix matching). - FuzzySearch(FuzzySearchExpr), + FuzzySearch(f) => ( + FO::FuzzySearch { + levenshtein_distance: f.levenshtein_distance, + prefix_match: f.prefix_match, + }, + PropertyFilterValue::Single(Prop::Str(f.value.clone().into())), + ), - /// Set membership. - IsIn(Value), - /// Negated set membership. - IsNotIn(Value), -} - -impl NodeFieldCondition { - pub fn op_name(&self) -> &'static str { - use NodeFieldCondition::*; - match self { - Eq(_) => "eq", - Ne(_) => "ne", - Gt(_) => "gt", - Ge(_) => "ge", - Lt(_) => "lt", - Le(_) => "le", - StartsWith(_) => "startsWith", - EndsWith(_) => "endsWith", - Contains(_) => "contains", - NotContains(_) => "notContains", - FuzzySearch(_) => "fuzzySearch", - IsIn(_) => "isIn", - IsNotIn(_) => "isNotIn", - } - } -} - -/// A condition on one specific built-in field — the payload of the per-field -/// filter variants (`{ id: { where: ... } }`, `{ name: { where: ... } }`, -/// `{ nodeType: { where: ... } }`). -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct NodeFieldWhere { - /// Condition applied to the field. - /// - /// Exposed as `where` in GraphQL. - #[graphql(name = "where")] - #[serde(rename = "where")] - pub where_: NodeFieldCondition, -} - -/// Restricts node evaluation to a time window and applies a nested `NodeFilter`. -/// -/// Used by `GqlNodeFilter::Window`. -/// -/// The window is inclusive of `start` and exclusive of `end`. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct NodeWindowExpr { - /// Window start time (inclusive). - pub start: GqlTimeInput, - /// Window end time (exclusive). - pub end: GqlTimeInput, - /// Filter evaluated within the restricted window. - pub expr: Wrapped, -} - -/// Restricts node evaluation to a single time bound and applies a nested `NodeFilter`. -/// -/// Used by `At`, `Before`, and `After` node filters. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct NodeTimeExpr { - /// Reference time for the operation. - pub time: GqlTimeInput, - /// Filter evaluated within the restricted time scope. - pub expr: Wrapped, -} - -/// Applies a unary node-view operation and then evaluates a nested `NodeFilter`. -/// -/// Used by `Latest` and `SnapshotLatest` node filters. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct NodeUnaryExpr { - /// Filter evaluated after applying the unary operation. - pub expr: Wrapped, -} - -/// Restricts node evaluation to one or more layers and applies a nested `NodeFilter`. -/// -/// Used by `GqlNodeFilter::Layers`. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct NodeLayersExpr { - /// Layer names to include. - pub names: Vec, - /// Filter evaluated within the layer-restricted view. - pub expr: Wrapped, -} - -/// GraphQL input type for filtering nodes. -/// -/// `NodeFilter` represents a composable boolean expression evaluated -/// against nodes in a graph. Filters can target: -/// -/// - built-in node fields (`Id` / `Name` / `NodeType`), -/// - node properties and metadata, -/// - temporal properties, -/// - temporal scope (windows, snapshots, latest), -/// - and layer membership, -/// - plus node state predicates (e.g. `IsActive`). -/// -/// Filters can be combined recursively using logical operators -/// (`And`, `Or`, `Not`). -#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] -#[graphql(name = "NodeFilter")] -#[serde(rename_all = "camelCase")] -pub enum GqlNodeFilter { - /// Filters the node id: `{ id: { where: ... } }`. - Id(NodeFieldWhere), - - /// Filters the node name: `{ name: { where: ... } }`. - Name(NodeFieldWhere), - - /// Filters the node type: `{ nodeType: { where: ... } }`. - NodeType(NodeFieldWhere), - - /// Filters a node property by name and condition. - Property(PropertyFilterNew), - - /// Filters a node's degree (in, out, or total) by a condition. - Degree(DegreeFilterNew), - - /// Filters a node metadata field by name and condition. - /// - /// Metadata is shared across all temporal versions of a node. - Metadata(PropertyFilterNew), - - /// Filters a temporal node property by name and condition. - /// - /// Used when the property value varies over time and must be evaluated - /// within a temporal context. - TemporalProperty(PropertyFilterNew), - - /// Logical AND over multiple node filters. - And(Vec), - - /// Logical OR over multiple node filters. - Or(Vec), - - /// Logical NOT over a nested node filter. - Not(Wrapped), - - /// Restricts evaluation to a time window (inclusive start, exclusive end). - Window(NodeWindowExpr), - /// Restricts evaluation to a single point in time. - At(NodeTimeExpr), - /// Restricts evaluation to times strictly before the given time. - Before(NodeTimeExpr), - /// Restricts evaluation to times strictly after the given time. - After(NodeTimeExpr), - /// Evaluates predicates against the latest available node state. - Latest(NodeUnaryExpr), - /// Evaluates predicates against a snapshot of the graph at a given time. - SnapshotAt(NodeTimeExpr), - /// Evaluates predicates against the most recent snapshot of the graph. - SnapshotLatest(NodeUnaryExpr), - /// Restricts evaluation to nodes belonging to one or more layers. - Layers(NodeLayersExpr), - - /// Matches nodes that have at least one event in the current view/window. - /// - /// When `true`, only active nodes are matched. - IsActive(bool), -} - -/// Restricts edge evaluation to a time window and applies a nested `EdgeFilter`. -/// -/// Used by `GqlEdgeFilter::Window`. -/// -/// The window is inclusive of `start` and exclusive of `end`. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EdgeWindowExpr { - /// Window start time (inclusive). - pub start: GqlTimeInput, - /// Window end time (exclusive). - pub end: GqlTimeInput, - /// Filter evaluated within the restricted window. - pub expr: Wrapped, -} - -/// Restricts edge evaluation to a single time bound and applies a nested `EdgeFilter`. -/// -/// Used by `At`, `Before`, and `After` edge filters. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EdgeTimeExpr { - /// Reference time for the operation. - pub time: GqlTimeInput, - /// Filter evaluated within the restricted time scope. - pub expr: Wrapped, -} - -/// Applies a unary edge-view operation and then evaluates a nested `EdgeFilter`. -/// -/// Used by `Latest` and `SnapshotLatest` edge filters. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EdgeUnaryExpr { - /// Filter evaluated after applying the unary operation. - pub expr: Wrapped, -} - -/// Restricts edge evaluation to one or more layers and applies a nested `EdgeFilter`. -/// -/// Used by `GqlEdgeFilter::Layers`. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EdgeLayersExpr { - /// Layer names to include. - pub names: Vec, - /// Filter evaluated within the layer-restricted view. - pub expr: Wrapped, -} - -/// GraphQL input type for filtering edges. -/// -/// `EdgeFilter` represents a composable boolean expression evaluated -/// against edges in a graph. Filters can target: -/// -/// - edge **endpoints** (source / destination nodes), -/// - edge **properties** and **metadata**, -/// - **temporal scope** (windows, snapshots, latest), -/// - **layer membership**, -/// - and **structural edge state** (active, valid, deleted, self-loop). -/// -/// Filters can be combined recursively using logical operators -/// (`And`, `Or`, `Not`). -/// -/// Examples (GraphQL): -/// ```graphql -/// { -/// edges(filter: { -/// And: [ -/// { IsActive: true }, -/// { Property: { name: "weight", gt: 0.5 } } -/// ] -/// }) { -/// src -/// dst -/// } -/// } -/// ``` -#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] -#[graphql(name = "EdgeFilter")] -#[serde(rename_all = "camelCase")] -pub enum GqlEdgeFilter { - /// Applies a filter to the **source node** of the edge. - /// - /// The nested `NodeFilter` is evaluated against the source endpoint. - /// - /// Example: - /// `{ Src: { Name: { contains: "alice" } } }` - Src(Wrapped), - - /// Applies a filter to the **destination node** of the edge. - /// - /// The nested `NodeFilter` is evaluated against the destination endpoint. - /// - /// Example: - /// `{ Dst: { Id: { eq: 42 } } }` - Dst(Wrapped), - - /// Filters an edge **property** by name and value. - /// - /// Applies to static or temporal properties depending on context. - /// - /// Example: - /// `{ Property: { name: "weight", gt: 0.5 } }` - Property(PropertyFilterNew), - - /// Filters an edge **metadata field**. - /// - /// Metadata is shared across all temporal versions of an edge. - /// - /// Example: - /// `{ Metadata: { name: "source", eq: "imported" } }` - Metadata(PropertyFilterNew), - - /// Filters a **temporal edge property**. - /// - /// Used when the property value varies over time and must be - /// evaluated within a temporal context. - /// - /// Example: - /// `{ TemporalProperty: { name: "status", eq: "active" } }` - TemporalProperty(PropertyFilterNew), - - /// Logical **AND** over multiple edge filters. - /// - /// All nested filters must evaluate to `true`. - /// - /// Example: - /// `{ And: [ { IsActive: true }, { IsValid: true } ] }` - And(Vec), - - /// Logical **OR** over multiple edge filters. - /// - /// At least one nested filter must evaluate to `true`. - /// - /// Example: - /// `{ Or: [ { IsDeleted: true }, { IsSelfLoop: true } ] }` - Or(Vec), - - /// Logical **NOT** over a nested edge filter. - /// - /// Negates the result of the wrapped filter. - /// - /// Example: - /// `{ Not: { IsDeleted: true } }` - Not(Wrapped), - - /// Restricts edge evaluation to a **time window**. - /// - /// The window is inclusive of `start` and exclusive of `end`. - Window(EdgeWindowExpr), - - /// Restricts edge evaluation to a **single point in time**. - At(EdgeTimeExpr), - - /// Restricts edge evaluation to times **strictly before** a given time. - Before(EdgeTimeExpr), - - /// Restricts edge evaluation to times **strictly after** a given time. - After(EdgeTimeExpr), - - /// Evaluates edge predicates against the **latest available state**. - Latest(EdgeUnaryExpr), - - /// Evaluates edge predicates against a **snapshot** of the graph - /// at a specific time. - SnapshotAt(EdgeTimeExpr), - - /// Evaluates edge predicates against the **most recent snapshot** - /// of the graph. - SnapshotLatest(EdgeUnaryExpr), - - /// Restricts evaluation to edges belonging to one or more **layers**. - /// - /// Example: - /// `{ Layers: { values: ["fire_nation", "air_nomads"] } }` - Layers(EdgeLayersExpr), - - /// Matches edges that have at least one event in the current view/window. - /// - /// When `true`, only active edges are matched. - IsActive(bool), - - /// Matches edges that are structurally valid (i.e. not deleted) - /// in the current view/window. - IsValid(bool), - - /// Matches edges that have been deleted in the current view/window. - IsDeleted(bool), - - /// Matches edges that are **self-loops** - /// (source node == destination node). - IsSelfLoop(bool), -} - -/// Restricts exploded-edge evaluation to a time window and applies a nested -/// `ExplodedEdgeFilter`. -/// -/// Used by `GqlExplodedEdgeFilter::Window`. -/// -/// The window is inclusive of `start` and exclusive of `end`. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ExplodedEdgeWindowExpr { - /// Window start time (inclusive). - pub start: GqlTimeInput, - /// Window end time (exclusive). - pub end: GqlTimeInput, - /// Filter evaluated within the restricted window. - pub expr: Wrapped, -} - -/// Restricts exploded-edge evaluation to a single time bound and applies a -/// nested `ExplodedEdgeFilter`. -/// -/// Used by `At`, `Before`, `After`, and `SnapshotAt` exploded-edge filters. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ExplodedEdgeTimeExpr { - /// Reference time for the operation. - pub time: GqlTimeInput, - /// Filter evaluated within the restricted time scope. - pub expr: Wrapped, -} - -/// Applies a unary edge-view operation and then evaluates a nested -/// `ExplodedEdgeFilter`. -/// -/// Used by `Latest` and `SnapshotLatest` exploded-edge filters. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ExplodedEdgeUnaryExpr { - /// Filter evaluated after applying the unary operation. - pub expr: Wrapped, -} - -/// Restricts exploded-edge evaluation to one or more layers and applies a -/// nested `ExplodedEdgeFilter`. -/// -/// Used by `GqlExplodedEdgeFilter::Layers`. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ExplodedEdgeLayersExpr { - /// Layer names to include. - pub names: Vec, - /// Filter evaluated within the layer-restricted view. - pub expr: Wrapped, -} - -/// GraphQL input type for filtering **exploded edges** — edge views where each -/// temporal event is an individually addressable edge instance, rather than -/// one aggregated edge across time. -/// -/// Predicates are evaluated **per event**: a property condition keeps the -/// individual updates that match it (and the edges carrying them), where the -/// plain `EdgeFilter` evaluates one aggregated value per edge. -/// -/// Filters can target edge endpoints, properties/metadata, temporal scope, -/// layer membership, and structural edge state, and can be combined -/// recursively with `And`/`Or`/`Not` — mirroring `EdgeFilter`. -#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] -#[graphql(name = "ExplodedEdgeFilter")] -#[serde(rename_all = "camelCase")] -pub enum GqlExplodedEdgeFilter { - /// Applies a filter to the **source node** of the exploded edge. - Src(Wrapped), - - /// Applies a filter to the **destination node** of the exploded edge. - Dst(Wrapped), - - /// Filters an exploded-edge **property** by name and value, evaluated - /// per event. - /// - /// Example: - /// `{ Property: { name: "weight", gt: 0.5 } }` - Property(PropertyFilterNew), - - /// Filters an exploded-edge **metadata field**. - /// - /// Metadata is shared across all temporal versions of an edge. - Metadata(PropertyFilterNew), - - /// Filters a **temporal exploded-edge property**, evaluated within a - /// temporal context per event. - TemporalProperty(PropertyFilterNew), - - /// Logical **AND** over multiple exploded-edge filters. - And(Vec), - - /// Logical **OR** over multiple exploded-edge filters. - Or(Vec), - - /// Logical **NOT** over a nested exploded-edge filter. - Not(Wrapped), - - /// Restricts exploded-edge evaluation to a **time window** - /// (inclusive start, exclusive end). - Window(ExplodedEdgeWindowExpr), - - /// Restricts exploded-edge evaluation to a **single point in time**. - At(ExplodedEdgeTimeExpr), - - /// Restricts exploded-edge evaluation to times **strictly before** a - /// given time. - Before(ExplodedEdgeTimeExpr), - - /// Restricts exploded-edge evaluation to times **strictly after** a - /// given time. - After(ExplodedEdgeTimeExpr), - - /// Evaluates exploded-edge predicates against the **latest available - /// state**. - Latest(ExplodedEdgeUnaryExpr), - - /// Evaluates exploded-edge predicates against a **snapshot** of the graph - /// at a specific time. - SnapshotAt(ExplodedEdgeTimeExpr), - - /// Evaluates exploded-edge predicates against the **most recent - /// snapshot** of the graph. - SnapshotLatest(ExplodedEdgeUnaryExpr), - - /// Restricts evaluation to exploded edges belonging to one or more - /// **layers**. - Layers(ExplodedEdgeLayersExpr), - - /// Matches exploded edges that have at least one event in the current - /// view/window. - IsActive(bool), - - /// Matches exploded edges that are structurally valid (i.e. not deleted) - /// in the current view/window. - IsValid(bool), - - /// Matches exploded edges that have been deleted in the current - /// view/window. - IsDeleted(bool), - - /// Matches exploded edges that are **self-loops** - /// (source node == destination node). - IsSelfLoop(bool), -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(transparent)] -pub struct Wrapped(Box); - -impl From for Wrapped { - fn from(inner: T) -> Self { - Wrapped(Box::new(inner)) - } -} - -impl Deref for Wrapped { - type Target = T; - fn deref(&self) -> &Self::Target { - self.0.deref() - } -} - -/// Fuzzy string match: passes when the candidate is within `levenshteinDistance` -/// edits of `value` (optionally also matching by prefix). Mirrors the local -/// `fuzzy_search(value, levenshtein_distance, prefix_match)` builder. -#[derive(InputObject, Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -pub struct FuzzySearchExpr { - /// The string to match against. - pub value: String, - /// Maximum Levenshtein edit distance for a match. - pub levenshtein_distance: usize, - /// Whether a prefix match within the distance also passes. - pub prefix_match: bool, -} - -impl Register for Wrapped { - fn register(registry: Registry) -> Registry { - registry.register::() - } -} - -impl FromValue for Wrapped { - fn from_value(value: async_graphql::Result) -> InputValueResult { - T::from_value(value) - .map(|v| Wrapped(Box::new(v))) - .map_err(|e| e.propagate()) - } -} - -impl TypeName for Wrapped { - fn get_type_name() -> Cow<'static, str> { - T::get_type_name() - } -} -impl InputTypeName for Wrapped {} - -fn peel_prop_wrappers_and_collect_ops<'a>( - cond: &'a PropCondition, - ops: &mut Vec, -) -> Option<&'a PropCondition> { - use PropCondition::*; - - match cond { - First(inner) => { - ops.push(Op::First); - Some(inner.deref()) - } - Last(inner) => { - ops.push(Op::Last); - Some(inner.deref()) - } - Any(inner) => { - ops.push(Op::Any); - Some(inner.deref()) - } - All(inner) => { - ops.push(Op::All); - Some(inner.deref()) - } - Sum(inner) => { - ops.push(Op::Sum); - Some(inner.deref()) - } - Avg(inner) => { - ops.push(Op::Avg); - Some(inner.deref()) - } - Min(inner) => { - ops.push(Op::Min); - Some(inner.deref()) - } - Max(inner) => { - ops.push(Op::Max); - Some(inner.deref()) - } - Len(inner) => { - ops.push(Op::Len); - Some(inner.deref()) - } - _ => None, - } -} - -fn require_string_value(op: &str, v: &Value) -> Result { - if let Value::Str(s) = v { - Ok(s.clone()) - } else { - Err(GraphError::InvalidGqlFilter(format!( - "{op} requires a string value, got {v}" - ))) - } -} - -fn require_prop_list_value(op: &str, v: &Value) -> Result { - if let Value::List(vs) = v { - let props = vs - .iter() - .cloned() - .map(Prop::try_from) - .collect::, _>>()?; - Ok(PropertyFilterValue::Set(Arc::new( - props.into_iter().collect(), - ))) - } else { - Err(GraphError::InvalidGqlFilter(format!( - "{op} requires a list value, got {v}" - ))) - } -} - -fn parse_node_id_scalar(op: &str, v: &Value) -> Result { - match v { - Value::U64(i) => Ok(FilterValue::ID(GID::U64(*i))), - Value::Str(s) => Ok(FilterValue::ID(GID::Str(s.clone()))), - other => Err(GraphError::InvalidGqlFilter(format!( - "{op} requires int or str, got {other}" - ))), - } -} - -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}" - ))); - }; - - let all_u64 = vs.iter().all(|v| matches!(v, Value::U64(_))); - let all_str = vs.iter().all(|v| matches!(v, Value::Str(_))); - if !(all_u64 || all_str) { - return Err(GraphError::InvalidGqlFilter(format!( - "{op} requires a homogeneous list of ints or strings" - ))); - } - - let mut set = HashSet::with_capacity(vs.len()); - if all_u64 { - for v in vs { - if let Value::U64(i) = v { - set.insert(GID::U64(*i)); - } - } - } else { - for v in vs { - if let Value::Str(s) = v { - set.insert(GID::Str(s.clone())); - } - } - } - Ok(FilterValue::IDSet(Arc::new(set))) -} - -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}" - ))); - }; - - let strings = vs - .iter() - .map(|v| { - if let Value::Str(s) = v { - Ok(s.clone()) - } else { - Err(GraphError::InvalidGqlFilter(format!( - "Expected list of strings for {op}, got {v}" - ))) - } - }) - .collect::, _>>()?; - - Ok(FilterValue::Set(Arc::new(strings.into_iter().collect()))) -} - -pub(crate) fn translate_node_field_where( - field: NodeField, - cond: &NodeFieldCondition, -) -> Result<(String, FilterValue, FilterOperator), GraphError> { - use FilterOperator as FO; - use NodeField::*; - use NodeFieldCondition::*; - - let field_name = field.to_string(); - let op = cond.op_name(); - - Ok(match (field, cond) { - (NodeId, Eq(v)) => (field_name, parse_node_id_scalar(op, v)?, FO::Eq), - (NodeId, Ne(v)) => (field_name, parse_node_id_scalar(op, v)?, FO::Ne), - (NodeId, Gt(v)) => (field_name, parse_node_id_scalar(op, v)?, FO::Gt), - (NodeId, Ge(v)) => (field_name, parse_node_id_scalar(op, v)?, FO::Ge), - (NodeId, Lt(v)) => (field_name, parse_node_id_scalar(op, v)?, FO::Lt), - (NodeId, Le(v)) => (field_name, parse_node_id_scalar(op, v)?, FO::Le), - - (NodeId, StartsWith(v)) => ( - field_name, - FilterValue::ID(GID::Str(require_string_value(op, v)?)), - FO::StartsWith, - ), - (NodeId, EndsWith(v)) => ( - field_name, - FilterValue::ID(GID::Str(require_string_value(op, v)?)), - FO::EndsWith, - ), - (NodeId, Contains(v)) => ( - field_name, - FilterValue::ID(GID::Str(require_string_value(op, v)?)), - FO::Contains, - ), - (NodeId, NotContains(v)) => ( - field_name, - FilterValue::ID(GID::Str(require_string_value(op, v)?)), - FO::NotContains, - ), - - (NodeId, IsIn(v)) => (field_name, parse_node_id_list(op, v)?, FO::IsIn), - (NodeId, IsNotIn(v)) => (field_name, parse_node_id_list(op, v)?, FO::IsNotIn), - - (NodeId, FuzzySearch(f)) => ( - field_name, - FilterValue::ID(GID::Str(f.value.clone())), - FO::FuzzySearch { - levenshtein_distance: f.levenshtein_distance, - prefix_match: f.prefix_match, - }, - ), - (NodeName, FuzzySearch(f)) | (NodeType, FuzzySearch(f)) => ( - field_name, - FilterValue::Single(f.value.clone()), - FO::FuzzySearch { - levenshtein_distance: f.levenshtein_distance, - prefix_match: f.prefix_match, - }, - ), - - (NodeName, Eq(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Eq, - ), - (NodeName, Ne(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Ne, - ), - (NodeName, Gt(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Gt, - ), - (NodeName, Ge(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Ge, - ), - (NodeName, Lt(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Lt, - ), - (NodeName, Le(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Le, - ), - - (NodeName, StartsWith(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::StartsWith, - ), - (NodeName, EndsWith(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::EndsWith, - ), - (NodeName, Contains(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Contains, - ), - (NodeName, NotContains(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::NotContains, - ), - - (NodeName, IsIn(v)) => (field_name, parse_string_list(op, v)?, FO::IsIn), - (NodeName, IsNotIn(v)) => (field_name, parse_string_list(op, v)?, FO::IsNotIn), - - (NodeType, Eq(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Eq, - ), - (NodeType, Ne(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Ne, - ), - (NodeType, Gt(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Gt, - ), - (NodeType, Ge(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Ge, - ), - (NodeType, Lt(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Lt, - ), - (NodeType, Le(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Le, - ), - - (NodeType, StartsWith(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::StartsWith, - ), - (NodeType, EndsWith(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::EndsWith, - ), - (NodeType, Contains(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::Contains, - ), - (NodeType, NotContains(v)) => ( - field_name, - FilterValue::Single(require_string_value(op, v)?), - FO::NotContains, - ), - - (NodeType, IsIn(v)) => (field_name, parse_string_list(op, v)?, FO::IsIn), - (NodeType, IsNotIn(v)) => (field_name, parse_string_list(op, v)?, FO::IsNotIn), - }) -} - -pub(crate) fn translate_prop_leaf_to_filter( - name_for_errors: &str, - cmp: &PropCondition, -) -> Result<(FilterOperator, PropertyFilterValue), GraphError> { - use FilterOperator as FO; - use PropCondition::*; - - let single = |v: &Value| -> Result { - Ok(PropertyFilterValue::Single(Prop::try_from(v.clone())?)) - }; - - Ok(match cmp { - Eq(v) => (FO::Eq, single(v)?), - Ne(v) => (FO::Ne, single(v)?), - Gt(v) => (FO::Gt, single(v)?), - Ge(v) => (FO::Ge, single(v)?), - Lt(v) => (FO::Lt, single(v)?), - Le(v) => (FO::Le, single(v)?), - - StartsWith(v) => ( - FO::StartsWith, - PropertyFilterValue::Single(Prop::Str(require_string_value(cmp.op_name(), v)?.into())), - ), - EndsWith(v) => ( - FO::EndsWith, - PropertyFilterValue::Single(Prop::Str(require_string_value(cmp.op_name(), v)?.into())), - ), - - Contains(v) => (FO::Contains, single(v)?), - NotContains(v) => (FO::NotContains, single(v)?), - - IsIn(v) => (FO::IsIn, require_prop_list_value(cmp.op_name(), v)?), - IsNotIn(v) => (FO::IsNotIn, require_prop_list_value(cmp.op_name(), v)?), - - IsSome(true) => (FO::IsSome, PropertyFilterValue::None), - IsNone(true) => (FO::IsNone, PropertyFilterValue::None), - // `isSome: false` is exactly `isNone: true` (and vice versa) — lower - // to the dual operator instead of rejecting. - IsSome(false) => (FO::IsNone, PropertyFilterValue::None), - IsNone(false) => (FO::IsSome, PropertyFilterValue::None), - - FuzzySearch(f) => ( - FO::FuzzySearch { - levenshtein_distance: f.levenshtein_distance, - prefix_match: f.prefix_match, - }, - PropertyFilterValue::Single(Prop::Str(f.value.clone().into())), - ), - - And(_) | Or(_) | Not(_) | First(_) | Last(_) | Any(_) | All(_) | Sum(_) | Avg(_) - | Min(_) | Max(_) | Len(_) => { - let op = cmp.op_name(); - return Err(GraphError::InvalidGqlFilter(format!( - "Expected comparison at leaf for {name_for_errors}; got '{op}'" - ))); - } - }) + And(_) | Or(_) | Not(_) | First(_) | Last(_) | Any(_) | All(_) | Sum(_) | Avg(_) + | Min(_) | Max(_) | Len(_) => { + let op = cmp.op_name(); + return Err(GraphError::InvalidGqlFilter(format!( + "Expected comparison at leaf for {name_for_errors}; got '{op}'" + ))); + } + }) } pub(crate) fn build_property_filter_from_condition_with_entity( @@ -1830,73 +560,6 @@ pub(crate) fn build_property_filter_from_condition_with_entity for DynView { - type Error = GraphError; - - fn try_from(f: GqlGraphFilter) -> Result { - let default_inner: DynView = Arc::new(GraphFilter); - - Ok(match f { - GqlGraphFilter::Window(w) => { - let inner: DynView = match w.expr { - Some(e) => e.deref().clone().try_into()?, - None => default_inner, - }; - inner.window(w.start, w.end) - } - GqlGraphFilter::At(t) => { - let inner: DynView = match t.expr { - Some(e) => e.deref().clone().try_into()?, - None => default_inner, - }; - inner.at(t.time) - } - GqlGraphFilter::Before(t) => { - let inner: DynView = match t.expr { - Some(e) => e.deref().clone().try_into()?, - None => default_inner, - }; - inner.before(t.time) - } - GqlGraphFilter::After(t) => { - let inner: DynView = match t.expr { - Some(e) => e.deref().clone().try_into()?, - None => default_inner, - }; - inner.after(t.time) - } - GqlGraphFilter::Latest(u) => { - let inner: DynView = match u.expr { - Some(e) => e.deref().clone().try_into()?, - None => default_inner, - }; - Arc::new(inner.latest()) - } - GqlGraphFilter::SnapshotAt(t) => { - let inner: DynView = match t.expr { - Some(e) => e.deref().clone().try_into()?, - None => default_inner, - }; - Arc::new(inner.snapshot_at(t.time)) - } - GqlGraphFilter::SnapshotLatest(u) => { - let inner: DynView = match u.expr { - Some(e) => e.deref().clone().try_into()?, - None => default_inner, - }; - Arc::new(inner.snapshot_latest()) - } - GqlGraphFilter::Layers(l) => { - let inner: DynView = match l.expr { - Some(e) => e.deref().clone().try_into()?, - None => default_inner, - }; - Arc::new(inner.layer(l.names)) - } - }) - } -} - /// Property/metadata keys to hide per entity type. #[derive(InputObject, Clone, Debug, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -1928,1016 +591,3 @@ pub struct GraphAccessFilter { #[serde(default, skip_serializing_if = "Option::is_none")] pub hidden_metadata: Option, } - -// ============ Reverse conversion: engine filter → wire filter ============ -// -// Used by the RemoteGraph Python client, which builds filters via the local -// `PyFilterExpr` API (producing a `Composite*Filter`) and sends them as the -// unified `GqlFilter` — the per-kind conversions here are the first half of -// that, with `Composite*Filter -> GqlFilter` wrapping them. The forward path -// (`TryFrom for CompositeNodeFilter`, above) already exists. -// -// Not every engine filter has a lossless wire counterpart, and those cases -// surface as `GraphError::InvalidGqlFilter` rather than being silently -// dropped. (A `Layer::All` view op is not one of them: it restricts nothing, -// so it is skipped while the rest of the chain is kept.) - -fn wrap(t: T) -> Wrapped { - Wrapped(Box::new(t)) -} - -/// `FilterValue` (used by field filters) → wire `Value`. -fn filter_value_to_value(v: &FilterValue) -> Result { - Ok(match v { - FilterValue::Single(s) => Value::Str(s.clone()), - FilterValue::Set(strs) => { - // Set semantics — element order is irrelevant on the wire. - Value::List(strs.iter().map(|s| Value::Str(s.clone())).collect()) - } - FilterValue::ID(GID::Str(s)) => Value::Str(s.clone()), - FilterValue::ID(GID::U64(u)) => Value::U64(*u), - FilterValue::IDSet(gids) => { - let items: Vec = gids - .iter() - .map(|g| match g { - GID::Str(s) => Value::Str(s.clone()), - GID::U64(u) => Value::U64(*u), - }) - .collect(); - Value::List(items) - } - }) -} - -/// `PropertyFilterValue` → wire `Value` — used inside `PropCondition`. -/// For `None` (used only with `IsSome`/`IsNone`) callers should route -/// separately since `PropCondition::IsSome`/`IsNone` take `bool`, not -/// `Value`. -fn prop_filter_value_to_value(v: &PropertyFilterValue) -> Result { - match v { - PropertyFilterValue::Single(p) => Value::try_from(p), - PropertyFilterValue::Set(ps) => { - // Set semantics — element order is irrelevant on the wire. - let items: Vec = ps.iter().map(Value::try_from).collect::>()?; - Ok(Value::List(items)) - } - PropertyFilterValue::None => Err(GraphError::InvalidGqlFilter( - "cannot render PropertyFilterValue::None as a wire Value".into(), - )), - } -} - -/// Build a base `PropCondition` from an operator + value (no `ops` wrapping). -fn build_base_prop_condition( - operator: FilterOperator, - value: &PropertyFilterValue, -) -> Result { - use FilterOperator as FO; - Ok(match operator { - FO::Eq => PropCondition::Eq(prop_filter_value_to_value(value)?), - FO::Ne => PropCondition::Ne(prop_filter_value_to_value(value)?), - FO::Gt => PropCondition::Gt(prop_filter_value_to_value(value)?), - FO::Ge => PropCondition::Ge(prop_filter_value_to_value(value)?), - FO::Lt => PropCondition::Lt(prop_filter_value_to_value(value)?), - FO::Le => PropCondition::Le(prop_filter_value_to_value(value)?), - FO::StartsWith => PropCondition::StartsWith(prop_filter_value_to_value(value)?), - FO::EndsWith => PropCondition::EndsWith(prop_filter_value_to_value(value)?), - FO::Contains => PropCondition::Contains(prop_filter_value_to_value(value)?), - FO::NotContains => PropCondition::NotContains(prop_filter_value_to_value(value)?), - FO::IsIn => PropCondition::IsIn(prop_filter_value_to_value(value)?), - FO::IsNotIn => PropCondition::IsNotIn(prop_filter_value_to_value(value)?), - FO::IsSome => PropCondition::IsSome(true), - FO::IsNone => PropCondition::IsNone(true), - FO::FuzzySearch { - levenshtein_distance, - prefix_match, - } => { - let PropertyFilterValue::Single(Prop::Str(v)) = value else { - return Err(GraphError::InvalidGqlFilter( - "fuzzySearch requires a string value".into(), - )); - }; - PropCondition::FuzzySearch(FuzzySearchExpr { - value: v.to_string(), - levenshtein_distance, - prefix_match, - }) - } - }) -} - -/// Rebuild the wire tree from `ops`. Both the peel (`peel_prop_wrappers_and_ -/// collect_ops`) and core eval (`evaluate.rs`) treat the OUTERMOST tree node -/// as the FIRST-applied op: tree `First(Sum(x))` ⇔ ops `[First, Sum]` ⇔ chain -/// `.first().sum()`. Since folding wraps inside-out (each wrap becomes the new -/// outermost), we iterate `ops` in REVERSE so that `ops[0]` ends up outermost. -/// -/// Beware: core's `Display` prints the OPPOSITE nesting (`[First, Sum]` prints -/// as `"sum(first(x))"`) — don't validate this mapping against Display strings. -fn apply_ops_to_condition(base: PropCondition, ops: &[Op]) -> PropCondition { - // Fold reversed so `ops[0]` becomes the outermost wrapper (see doc comment). - ops.iter().rev().fold(base, |acc, op| match op { - Op::First => PropCondition::First(wrap(acc)), - Op::Last => PropCondition::Last(wrap(acc)), - Op::Len => PropCondition::Len(wrap(acc)), - Op::Sum => PropCondition::Sum(wrap(acc)), - Op::Avg => PropCondition::Avg(wrap(acc)), - Op::Min => PropCondition::Min(wrap(acc)), - Op::Max => PropCondition::Max(wrap(acc)), - Op::Any => PropCondition::Any(wrap(acc)), - Op::All => PropCondition::All(wrap(acc)), - }) -} - -/// Map a `Filter` (built-in node field filter) → the per-field wire variant -/// (`Id` / `Name` / `NodeType`). -fn filter_to_node_field(f: Filter) -> Result { - let variant: fn(NodeFieldWhere) -> GqlNodeFilter = match f.field_name.as_str() { - NODE_ID_FIELD => GqlNodeFilter::Id, - NODE_NAME_FIELD => GqlNodeFilter::Name, - NODE_TYPE_FIELD => GqlNodeFilter::NodeType, - other => { - return Err(GraphError::InvalidGqlFilter(format!( - "unknown node field name for wire conversion: {}", - other - ))) - } - }; - let val = filter_value_to_value(&f.field_value)?; - let where_ = match f.operator { - FilterOperator::Eq => NodeFieldCondition::Eq(val), - FilterOperator::Ne => NodeFieldCondition::Ne(val), - FilterOperator::Gt => NodeFieldCondition::Gt(val), - FilterOperator::Ge => NodeFieldCondition::Ge(val), - FilterOperator::Lt => NodeFieldCondition::Lt(val), - FilterOperator::Le => NodeFieldCondition::Le(val), - FilterOperator::StartsWith => NodeFieldCondition::StartsWith(val), - FilterOperator::EndsWith => NodeFieldCondition::EndsWith(val), - FilterOperator::Contains => NodeFieldCondition::Contains(val), - FilterOperator::NotContains => NodeFieldCondition::NotContains(val), - FilterOperator::IsIn => NodeFieldCondition::IsIn(val), - FilterOperator::IsNotIn => NodeFieldCondition::IsNotIn(val), - FilterOperator::FuzzySearch { - levenshtein_distance, - prefix_match, - } => { - let Value::Str(v) = val else { - return Err(GraphError::InvalidGqlFilter( - "fuzzySearch requires a string value".into(), - )); - }; - NodeFieldCondition::FuzzySearch(FuzzySearchExpr { - value: v, - levenshtein_distance, - prefix_match, - }) - } - other => { - return Err(GraphError::InvalidGqlFilter(format!( - "unsupported operator for node field: {:?}", - other - ))) - } - }; - Ok(variant(NodeFieldWhere { where_ })) -} - -/// Map a `Layer` (engine) → `Vec` names for the wire. -fn layer_to_names(layer: &Layer) -> Result, GraphError> { - match layer { - Layer::One(name) => Ok(vec![name.to_string()]), - Layer::Multiple(names) => Ok(names.iter().map(|s| s.to_string()).collect()), - Layer::Default => Ok(vec!["_default".to_string()]), - // No layers — the empty name list (`Layer::from_iter([])` maps back - // to `Layer::None`, so the round-trip is exact). - Layer::None => Ok(vec![]), - // All layers is no restriction at all — callers drop the layer - // wrapper entirely instead of rendering it. - Layer::All => Err(GraphError::InvalidGqlFilter( - "Layer::All is no layer restriction — omit the layer wrapper".into(), - )), - } -} - -impl TryFrom for GqlNodeFilter { - type Error = GraphError; - fn try_from(f: CompositeNodeFilter) -> Result { - Ok(match f { - CompositeNodeFilter::Node(filter) => filter_to_node_field(filter)?, - - CompositeNodeFilter::Property(pf) => { - let base = build_base_prop_condition(pf.operator, &pf.prop_value)?; - let where_ = apply_ops_to_condition(base, &pf.ops); - let name = pf.prop_ref.name().to_string(); - match pf.prop_ref { - PropertyRef::Property(_) => { - GqlNodeFilter::Property(PropertyFilterNew { name, where_ }) - } - PropertyRef::Metadata(_) => { - GqlNodeFilter::Metadata(PropertyFilterNew { name, where_ }) - } - PropertyRef::TemporalProperty(_) => { - GqlNodeFilter::TemporalProperty(PropertyFilterNew { name, where_ }) - } - } - } - - CompositeNodeFilter::Degree(df) => { - let direction = match df.direction { - Direction::IN => DegreeDirection::In, - Direction::OUT => DegreeDirection::Out, - Direction::BOTH => DegreeDirection::Both, - }; - let base = build_base_prop_condition(df.operator, &df.value)?; - let where_ = apply_ops_to_condition(base, &df.ops); - GqlNodeFilter::Degree(DegreeFilterNew { direction, where_ }) - } - - CompositeNodeFilter::IsActiveNode(_) => GqlNodeFilter::IsActive(true), - - CompositeNodeFilter::And(l, r) => { - GqlNodeFilter::And(vec![(*l).try_into()?, (*r).try_into()?]) - } - CompositeNodeFilter::Or(l, r) => { - GqlNodeFilter::Or(vec![(*l).try_into()?, (*r).try_into()?]) - } - CompositeNodeFilter::Not(inner) => GqlNodeFilter::Not(wrap((*inner).try_into()?)), - - CompositeNodeFilter::Windowed(w) => GqlNodeFilter::Window(NodeWindowExpr { - start: w.start.t().into(), - end: w.end.t().into(), - expr: wrap(w.inner.try_into()?), - }), - - CompositeNodeFilter::Latest(l) => GqlNodeFilter::Latest(NodeUnaryExpr { - expr: wrap(l.inner.try_into()?), - }), - - CompositeNodeFilter::SnapshotAt(s) => GqlNodeFilter::SnapshotAt(NodeTimeExpr { - time: s.time.t().into(), - expr: wrap(s.inner.try_into()?), - }), - - CompositeNodeFilter::SnapshotLatest(s) => { - GqlNodeFilter::SnapshotLatest(NodeUnaryExpr { - expr: wrap(s.inner.try_into()?), - }) - } - - CompositeNodeFilter::Layered(l) => { - if matches!(l.layer, Layer::All) { - // Restricting to ALL layers restricts nothing — drop the - // wrapper and convert the inner filter directly. - l.inner.try_into()? - } else { - GqlNodeFilter::Layers(NodeLayersExpr { - names: layer_to_names(&l.layer)?, - expr: wrap(l.inner.try_into()?), - }) - } - } - }) - } -} - -impl TryFrom for GqlEdgeFilter { - type Error = GraphError; - fn try_from(f: CompositeEdgeFilter) -> Result { - Ok(match f { - // Endpoint filters recurse into the node converter — an edge - // filter on src/dst wraps a full node filter. - CompositeEdgeFilter::Src(nf) => GqlEdgeFilter::Src(wrap(nf.try_into()?)), - CompositeEdgeFilter::Dst(nf) => GqlEdgeFilter::Dst(wrap(nf.try_into()?)), - - CompositeEdgeFilter::Property(pf) => { - let base = build_base_prop_condition(pf.operator, &pf.prop_value)?; - let where_ = apply_ops_to_condition(base, &pf.ops); - let name = pf.prop_ref.name().to_string(); - match pf.prop_ref { - PropertyRef::Property(_) => { - GqlEdgeFilter::Property(PropertyFilterNew { name, where_ }) - } - PropertyRef::Metadata(_) => { - GqlEdgeFilter::Metadata(PropertyFilterNew { name, where_ }) - } - PropertyRef::TemporalProperty(_) => { - GqlEdgeFilter::TemporalProperty(PropertyFilterNew { name, where_ }) - } - } - } - - CompositeEdgeFilter::IsActiveEdge(_) => GqlEdgeFilter::IsActive(true), - CompositeEdgeFilter::IsValidEdge(_) => GqlEdgeFilter::IsValid(true), - CompositeEdgeFilter::IsDeletedEdge(_) => GqlEdgeFilter::IsDeleted(true), - CompositeEdgeFilter::IsSelfLoopEdge(_) => GqlEdgeFilter::IsSelfLoop(true), - - CompositeEdgeFilter::And(l, r) => { - GqlEdgeFilter::And(vec![(*l).try_into()?, (*r).try_into()?]) - } - CompositeEdgeFilter::Or(l, r) => { - GqlEdgeFilter::Or(vec![(*l).try_into()?, (*r).try_into()?]) - } - CompositeEdgeFilter::Not(inner) => GqlEdgeFilter::Not(wrap((*inner).try_into()?)), - - CompositeEdgeFilter::Windowed(w) => GqlEdgeFilter::Window(EdgeWindowExpr { - start: w.start.t().into(), - end: w.end.t().into(), - expr: wrap(w.inner.try_into()?), - }), - - CompositeEdgeFilter::Latest(l) => GqlEdgeFilter::Latest(EdgeUnaryExpr { - expr: wrap(l.inner.try_into()?), - }), - - CompositeEdgeFilter::SnapshotAt(s) => GqlEdgeFilter::SnapshotAt(EdgeTimeExpr { - time: s.time.t().into(), - expr: wrap(s.inner.try_into()?), - }), - - CompositeEdgeFilter::SnapshotLatest(s) => { - GqlEdgeFilter::SnapshotLatest(EdgeUnaryExpr { - expr: wrap(s.inner.try_into()?), - }) - } - - CompositeEdgeFilter::Layered(l) => { - if matches!(l.layer, Layer::All) { - l.inner.try_into()? - } else { - GqlEdgeFilter::Layers(EdgeLayersExpr { - names: layer_to_names(&l.layer)?, - expr: wrap(l.inner.try_into()?), - }) - } - } - }) - } -} - -impl TryFrom for GqlExplodedEdgeFilter { - type Error = GraphError; - fn try_from(f: CompositeExplodedEdgeFilter) -> Result { - Ok(match f { - // Endpoint filters recurse into the node converter — an - // exploded-edge filter on src/dst wraps a full node filter. - CompositeExplodedEdgeFilter::Src(nf) => { - GqlExplodedEdgeFilter::Src(wrap(nf.try_into()?)) - } - CompositeExplodedEdgeFilter::Dst(nf) => { - GqlExplodedEdgeFilter::Dst(wrap(nf.try_into()?)) - } - - CompositeExplodedEdgeFilter::Property(pf) => { - let base = build_base_prop_condition(pf.operator, &pf.prop_value)?; - let where_ = apply_ops_to_condition(base, &pf.ops); - let name = pf.prop_ref.name().to_string(); - match pf.prop_ref { - PropertyRef::Property(_) => { - GqlExplodedEdgeFilter::Property(PropertyFilterNew { name, where_ }) - } - PropertyRef::Metadata(_) => { - GqlExplodedEdgeFilter::Metadata(PropertyFilterNew { name, where_ }) - } - PropertyRef::TemporalProperty(_) => { - GqlExplodedEdgeFilter::TemporalProperty(PropertyFilterNew { name, where_ }) - } - } - } - - CompositeExplodedEdgeFilter::IsActiveEdge(_) => GqlExplodedEdgeFilter::IsActive(true), - CompositeExplodedEdgeFilter::IsValidEdge(_) => GqlExplodedEdgeFilter::IsValid(true), - CompositeExplodedEdgeFilter::IsDeletedEdge(_) => GqlExplodedEdgeFilter::IsDeleted(true), - CompositeExplodedEdgeFilter::IsSelfLoopEdge(_) => { - GqlExplodedEdgeFilter::IsSelfLoop(true) - } - - CompositeExplodedEdgeFilter::And(l, r) => { - GqlExplodedEdgeFilter::And(vec![(*l).try_into()?, (*r).try_into()?]) - } - CompositeExplodedEdgeFilter::Or(l, r) => { - GqlExplodedEdgeFilter::Or(vec![(*l).try_into()?, (*r).try_into()?]) - } - CompositeExplodedEdgeFilter::Not(inner) => { - GqlExplodedEdgeFilter::Not(wrap((*inner).try_into()?)) - } - - CompositeExplodedEdgeFilter::Windowed(w) => { - GqlExplodedEdgeFilter::Window(ExplodedEdgeWindowExpr { - start: w.start.t().into(), - end: w.end.t().into(), - expr: wrap(w.inner.try_into()?), - }) - } - - CompositeExplodedEdgeFilter::Latest(l) => { - GqlExplodedEdgeFilter::Latest(ExplodedEdgeUnaryExpr { - expr: wrap(l.inner.try_into()?), - }) - } - - CompositeExplodedEdgeFilter::SnapshotAt(s) => { - GqlExplodedEdgeFilter::SnapshotAt(ExplodedEdgeTimeExpr { - time: s.time.t().into(), - expr: wrap(s.inner.try_into()?), - }) - } - - CompositeExplodedEdgeFilter::SnapshotLatest(s) => { - GqlExplodedEdgeFilter::SnapshotLatest(ExplodedEdgeUnaryExpr { - expr: wrap(s.inner.try_into()?), - }) - } - - CompositeExplodedEdgeFilter::Layered(l) => { - if matches!(l.layer, Layer::All) { - l.inner.try_into()? - } else { - GqlExplodedEdgeFilter::Layers(ExplodedEdgeLayersExpr { - names: layer_to_names(&l.layer)?, - expr: wrap(l.inner.try_into()?), - }) - } - } - }) - } -} - -#[cfg(test)] -mod op_chain_tests { - use super::*; - - #[test] - fn multi_op_prop_condition_round_trips() { - // Tree `Sum(First(leaf))`: the OUTERMOST node (Sum) is the first-applied - // op. Peeling outermost-first yields ops `[Sum, First]`, and core eval - // runs ops[0] first — so this tree is the chain `.sum().first()`. - let tree = PropCondition::Sum(wrap(PropCondition::First(wrap(PropCondition::IsSome( - true, - ))))); - - // Decompose exactly as the wire encoder does — peel outermost-first. - let mut ops = Vec::new(); - let mut cursor = &tree; - while let Some(inner) = peel_prop_wrappers_and_collect_ops(cursor, &mut ops) { - cursor = inner; - } - - // Reconstruct: with the fold-in-reverse fix this round-trips. Before the - // fix it produced the inverted `First(Sum(leaf))` (i.e. `.first().sum()`). - let rebuilt = apply_ops_to_condition(cursor.clone(), &ops); - assert_eq!( - format!("{tree:?}"), - format!("{rebuilt:?}"), - "op chain did not round-trip — nesting inverted" - ); - } - - #[test] - fn apply_ops_pins_explicit_nesting_and_is_direction_sensitive() { - // A round-trip alone is self-consistent even if decompose+reconstruct - // were both wrong, so pin the exact tree and assert the two orderings - // genuinely differ — otherwise a future edit could silently re-invert. - let leaf = || PropCondition::IsSome(true); - - // ops = [Sum, First] (peeled outermost-first from tree `Sum(First(leaf))`, - // the chain `.sum().first()`) must reconstruct as `Sum(First(leaf))`, not - // `First(Sum(leaf))`. - let rebuilt = apply_ops_to_condition(leaf(), &[Op::Sum, Op::First]); - let expected = PropCondition::Sum(wrap(PropCondition::First(wrap(leaf())))); - assert_eq!(format!("{expected:?}"), format!("{rebuilt:?}")); - - // The reverse op order produces a genuinely different tree. - let reversed = apply_ops_to_condition(leaf(), &[Op::First, Op::Sum]); - assert_ne!( - format!("{rebuilt:?}"), - format!("{reversed:?}"), - "op ordering must be direction-sensitive" - ); - } -} - -#[cfg(test)] -mod filter_serde_goldens { - use super::*; - - // The wire format is the single source of truth — async-graphql input - // coercion, the persisted auth-store `GraphAccessFilter`, and the client - // all depend on these EXACT shapes. Pin them so a stray `#[serde(rename)]` - // is caught here, not at e2e time or by an invalidated permission store. - #[test] - fn per_field_filter_golden() { - let f = GqlNodeFilter::Name(NodeFieldWhere { - where_: NodeFieldCondition::Eq(Value::Str("alice".into())), - }); - assert_eq!( - serde_json::to_value(&f).unwrap(), - serde_json::json!({"name": {"where": {"eq": {"str": "alice"}}}}) - ); - } - - #[test] - fn property_filter_golden() { - let f = GqlNodeFilter::Property(PropertyFilterNew { - name: "score".into(), - where_: PropCondition::Gt(Value::F64(6.0)), - }); - assert_eq!( - serde_json::to_value(&f).unwrap(), - serde_json::json!({"property": {"name": "score", "where": {"gt": {"f64": 6.0}}}}) - ); - } - - #[test] - fn logical_and_golden() { - let f = GqlNodeFilter::And(vec![GqlNodeFilter::IsActive(true)]); - assert_eq!( - serde_json::to_value(&f).unwrap(), - serde_json::json!({"and": [{"isActive": true}]}) - ); - } - - #[test] - fn datetime_value_golden() { - // Serialization uses the schema field name `dtime`... - let v = Value::DTime("2020-01-01T00:00:00Z".into()); - assert_eq!( - serde_json::to_value(&v).unwrap(), - serde_json::json!({"dtime": "2020-01-01T00:00:00Z"}) - ); - } -} - -#[cfg(test)] -mod empty_combinator_tests { - use super::*; - - // Empty `and`/`or` lists are rejected in every conversion — critically for - // `or`, whose previous fallback (match-everything) inverted the caller's - // intent and was a fail-open where these filters scope access control - // (`GraphRowFilter` feeds the stored `GraphAccessFilter`). - #[test] - fn empty_combinators_are_rejected() { - for (name, filter) in [ - ("and", GqlFilter::And(vec![])), - ("or", GqlFilter::Or(vec![])), - ] { - let Err(err) = DynFilter::try_from(filter) else { - panic!("GqlFilter {name}: empty combinator must be rejected"); - }; - assert!( - err.to_string().contains("requires non-empty list"), - "GqlFilter {name}: unexpected error {err}" - ); - } - } - - // Single-element combinators still convert — the rejection is only about - // empty lists, not about unary composition. - #[test] - fn single_element_combinators_convert() { - let node_filter = || { - GqlNodeFilter::Property(PropertyFilterNew { - name: "x".into(), - where_: PropCondition::Eq(Value::I64(1)), - }) - }; - assert!(DynFilter::try_from(GqlFilter::And(vec![GqlFilter::Node(node_filter())])).is_ok()); - assert!(DynFilter::try_from(GqlFilter::Or(vec![GqlFilter::Node(node_filter())])).is_ok()); - } -} - -#[cfg(test)] -mod gql_filter_serde_tests { - use super::*; - - fn node_prop_eq(name: &str, v: i64) -> GqlNodeFilter { - GqlNodeFilter::Property(PropertyFilterNew { - name: name.into(), - where_: PropCondition::Eq(Value::I64(v)), - }) - } - - // Golden fixtures: `GqlFilter`'s serde output IS the wire contract (GraphQL - // variables) and the future stored-filter shape — it must match what - // async-graphql's OneOfInput coercion accepts (externally tagged, - // camelCase). A rename or tagging change here breaks the wire and any - // persisted filter; these tests make that a compile-time-adjacent failure - // instead of a production incident. - #[test] - fn serializes_to_the_oneof_wire_shape() { - let cases = [ - ( - GqlFilter::Node(node_prop_eq("x", 1)), - r#"{"node":{"property":{"name":"x","where":{"eq":{"i64":1}}}}}"#, - ), - ( - GqlFilter::And(vec![GqlFilter::Node(node_prop_eq("x", 1))]), - r#"{"and":[{"node":{"property":{"name":"x","where":{"eq":{"i64":1}}}}}]}"#, - ), - ( - GqlFilter::Or(vec![GqlFilter::Node(node_prop_eq("x", 1))]), - r#"{"or":[{"node":{"property":{"name":"x","where":{"eq":{"i64":1}}}}}]}"#, - ), - ( - GqlFilter::Not(wrap(GqlFilter::Node(node_prop_eq("x", 1)))), - r#"{"not":{"node":{"property":{"name":"x","where":{"eq":{"i64":1}}}}}}"#, - ), - ]; - for (filter, expected) in cases { - assert_eq!(serde_json::to_string(&filter).unwrap(), expected); - } - } - - #[test] - fn round_trips_through_serde() { - let filter = GqlFilter::And(vec![ - GqlFilter::Node(node_prop_eq("a", 1)), - GqlFilter::Not(wrap(GqlFilter::Or(vec![GqlFilter::Node(node_prop_eq( - "b", 2, - ))]))), - ]); - let json = serde_json::to_string(&filter).unwrap(); - let back: GqlFilter = serde_json::from_str(&json).unwrap(); - assert_eq!(serde_json::to_string(&back).unwrap(), json); - } - - // `not` composes end-to-end into a core filter. - #[test] - fn not_variant_converts_to_dyn_filter() { - let filter = GqlFilter::Not(wrap(GqlFilter::Node(node_prop_eq("x", 1)))); - assert!(DynFilter::try_from(filter).is_ok()); - } -} - -#[cfg(test)] -mod fuzzy_search_tests { - use super::*; - - // The wire shape is externally tagged camelCase, like every other condition. - #[test] - fn serializes_to_the_wire_shape() { - let cond = PropCondition::FuzzySearch(FuzzySearchExpr { - value: "shivam".into(), - levenshtein_distance: 2, - prefix_match: false, - }); - assert_eq!( - serde_json::to_string(&cond).unwrap(), - r#"{"fuzzySearch":{"value":"shivam","levenshteinDistance":2,"prefixMatch":false}}"# - ); - } - - // Wire condition → core (operator, value) and back — the remote client's - // round-trip for property fuzzy matching. - #[test] - fn property_fuzzy_round_trips_through_the_conversions() { - let cond = PropCondition::FuzzySearch(FuzzySearchExpr { - value: "graph enthusiast".into(), - levenshtein_distance: 3, - prefix_match: true, - }); - - let (operator, value) = translate_prop_leaf_to_filter("bio", &cond).unwrap(); - assert_eq!( - operator, - FilterOperator::FuzzySearch { - levenshtein_distance: 3, - prefix_match: true, - } - ); - - let back = build_base_prop_condition(operator, &value).unwrap(); - let PropCondition::FuzzySearch(f) = back else { - panic!("expected fuzzySearch back, got something else"); - }; - assert_eq!( - (f.value.as_str(), f.levenshtein_distance, f.prefix_match), - ("graph enthusiast", 3, true) - ); - } - - // Local node-name filter → wire condition (the reverse conversion the - // Python remote client rides) preserves the fuzzy parameters. - #[test] - fn node_name_fuzzy_round_trips_through_the_wire() { - let core = Filter::fuzzy_search("node_name", "ben", 1, true); - let GqlNodeFilter::Name(wire) = filter_to_node_field(core).unwrap() else { - panic!("expected the per-field name variant"); - }; - let NodeFieldCondition::FuzzySearch(ref f) = wire.where_ else { - panic!("expected fuzzySearch condition, got {:?}", wire.where_); - }; - assert_eq!( - (f.value.as_str(), f.levenshtein_distance, f.prefix_match), - ("ben", 1, true) - ); - } -} - -#[cfg(test)] -mod conversion_hole_tests { - use super::*; - use raphtory::db::graph::views::filter::model::layered_filter::Layered; - - // `isSome: false` lowers to the IsNone operator (and vice versa) instead - // of erroring — the two spellings are the same predicate. - #[test] - fn is_some_false_lowers_to_the_dual_operator() { - let (op, _) = translate_prop_leaf_to_filter("p", &PropCondition::IsSome(false)).unwrap(); - assert_eq!(op, FilterOperator::IsNone); - let (op, _) = translate_prop_leaf_to_filter("p", &PropCondition::IsNone(false)).unwrap(); - assert_eq!(op, FilterOperator::IsSome); - } - - // Node-id ordering comparisons accept string GIDs, matching the local - // expression api's `V: Into` bound. - #[test] - fn node_id_ordering_accepts_string_gids() { - let filter = GqlNodeFilter::Id(NodeFieldWhere { - where_: NodeFieldCondition::Gt(Value::Str("m".into())), - }); - assert!(super::super::expr_lowering::lower_node_filter(&filter).is_ok()); - } - - // Aggregation ops on a degree filter fail with a clear message — degree is - // a scalar, so an op chain over it is meaningless. - #[test] - fn degree_rejects_aggregation_ops() { - use raphtory::{db::api::view::Filter as _, prelude::Graph}; - - let filter = GqlNodeFilter::Degree(DegreeFilterNew { - direction: DegreeDirection::Both, - where_: PropCondition::Sum(wrap(PropCondition::Eq(Value::I64(3)))), - }); - let result = super::super::expr_lowering::lower_node_filter(&filter) - .and_then(|f| Graph::new().filter(f).map(|_| ())); - let Err(err) = result else { - panic!("degree with an op chain must be rejected"); - }; - assert!( - err.to_string().contains("is not valid on a scalar"), - "unexpected error: {err}" - ); - } - - // Layer round-trip semantics: `None` is the empty name list (exact - // round-trip via `Layer::from_iter([])`); `All` is no restriction, so the - // reverse conversion drops the wrapper entirely. - #[test] - fn layer_none_and_all_normalize() { - assert_eq!(layer_to_names(&Layer::None).unwrap(), Vec::::new()); - - let inner = CompositeNodeFilter::Node(Filter::eq("node_name", "a")); - let layered = CompositeNodeFilter::Layered(Box::new(Layered { - layer: Layer::All, - inner, - })); - let gql = GqlNodeFilter::try_from(layered).unwrap(); - assert!( - matches!(gql, GqlNodeFilter::Name(_)), - "Layer::All should drop the layer wrapper, got {gql:?}" - ); - } -} - -#[cfg(test)] -mod exploded_edge_filter_tests { - use super::*; - use raphtory::{ - db::graph::views::filter::model::{ - is_valid_filter::IsValidEdge, layered_filter::Layered, windowed_filter::Windowed, - ExplodedEdgeFilter, FilterOperator, - }, - prelude::Prop, - }; - use raphtory_api::core::storage::timeindex::EventTime; - - fn exploded_prop_gt(name: &str, v: i64) -> GqlExplodedEdgeFilter { - GqlExplodedEdgeFilter::Property(PropertyFilterNew { - name: name.into(), - where_: PropCondition::Gt(Value::I64(v)), - }) - } - - // The wire shape follows the OneOfInput convention of every other filter: - // externally tagged, camelCase. - #[test] - fn serializes_to_the_oneof_wire_shape() { - let cases = [ - ( - GqlFilter::ExplodedEdge(exploded_prop_gt("w", 1)), - r#"{"explodedEdge":{"property":{"name":"w","where":{"gt":{"i64":1}}}}}"#, - ), - ( - GqlFilter::ExplodedEdge(GqlExplodedEdgeFilter::Metadata(PropertyFilterNew { - name: "kind".into(), - where_: PropCondition::Eq(Value::Str("strong".into())), - })), - r#"{"explodedEdge":{"metadata":{"name":"kind","where":{"eq":{"str":"strong"}}}}}"#, - ), - ( - GqlFilter::ExplodedEdge(GqlExplodedEdgeFilter::And(vec![ - exploded_prop_gt("w", 1), - GqlExplodedEdgeFilter::IsValid(true), - ])), - r#"{"explodedEdge":{"and":[{"property":{"name":"w","where":{"gt":{"i64":1}}}},{"isValid":true}]}}"#, - ), - ]; - for (filter, expected) in cases { - assert_eq!(serde_json::to_string(&filter).unwrap(), expected); - } - } - - #[test] - fn round_trips_through_serde() { - let filter = GqlFilter::ExplodedEdge(GqlExplodedEdgeFilter::Not(wrap( - GqlExplodedEdgeFilter::Or(vec![ - exploded_prop_gt("w", 1), - GqlExplodedEdgeFilter::TemporalProperty(PropertyFilterNew { - name: "w".into(), - where_: PropCondition::Any(wrap(PropCondition::Eq(Value::I64(3)))), - }), - ]), - ))); - let json = serde_json::to_string(&filter).unwrap(); - let back: GqlFilter = serde_json::from_str(&json).unwrap(); - assert_eq!(serde_json::to_string(&back).unwrap(), json); - } - - // Composite → wire → composite is exact for every variant family the - // Python builder can produce (property/metadata/temporal, view wrappers, - // combinators, predicates, endpoints). - #[test] - fn composite_wire_forms_lower_onto_expressions() { - let pf = |prop_ref, value, operator| PropertyFilter { - prop_ref, - prop_value: PropertyFilterValue::Single(value), - operator, - ops: vec![], - entity: ExplodedEdgeFilter, - }; - let prop = || { - CompositeExplodedEdgeFilter::Property(pf( - PropertyRef::Property("w".into()), - Prop::I64(1), - FilterOperator::Gt, - )) - }; - let cases = vec![ - prop(), - CompositeExplodedEdgeFilter::Property(pf( - PropertyRef::Metadata("kind".into()), - Prop::str("strong"), - FilterOperator::Eq, - )), - CompositeExplodedEdgeFilter::Windowed(Box::new(Windowed::new( - EventTime::start(2), - EventTime::end(4), - prop(), - ))), - CompositeExplodedEdgeFilter::Layered(Box::new(Layered::from_layers( - vec!["knows".to_string()], - prop(), - ))), - CompositeExplodedEdgeFilter::And( - Box::new(prop()), - Box::new(CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge)), - ), - CompositeExplodedEdgeFilter::Not(Box::new(prop())), - CompositeExplodedEdgeFilter::Src(CompositeNodeFilter::Node(Filter::eq( - "node_name", - "a", - ))), - ]; - for original in cases { - let gql = GqlExplodedEdgeFilter::try_from(original.clone()).unwrap(); - assert!( - super::super::expr_lowering::lower_exploded_edge_filter(&gql).is_ok(), - "wire form no longer lowers: {original}" - ); - } - } - - // The composite converts to a DynFilter, so the server can evaluate it - // through the same `graph.filter(...)` machinery as node/edge filters. - #[test] - fn converts_to_dyn_filter() { - let filter = GqlFilter::ExplodedEdge(exploded_prop_gt("w", 1)); - assert!(DynFilter::try_from(filter).is_ok()); - } - - // The exploded FilterTree kind flows into the wire enum — the client's - // transport path. - #[test] - fn filter_tree_converts_to_the_wire_variant() { - let prop_tree = || { - FilterTree::ExplodedEdge(CompositeExplodedEdgeFilter::Property(PropertyFilter { - prop_ref: PropertyRef::Property("w".into()), - prop_value: PropertyFilterValue::Single(Prop::I64(1)), - operator: FilterOperator::Gt, - ops: vec![], - entity: ExplodedEdgeFilter, - })) - }; - let gql = GqlFilter::try_from(prop_tree()).unwrap(); - assert!( - matches!(gql, GqlFilter::ExplodedEdge(_)), - "expected ExplodedEdges, got {gql:?}" - ); - - // A mixed node∧exploded tree keeps both kinds through the conversion. - let n = FilterTree::Node(CompositeNodeFilter::Property(PropertyFilter { - prop_ref: PropertyRef::Property("x".into()), - prop_value: PropertyFilterValue::Single(Prop::I64(1)), - operator: FilterOperator::Eq, - ops: vec![], - entity: raphtory::db::graph::views::filter::model::node_filter::NodeFilter, - })); - let tree = FilterTree::And(vec![n, prop_tree()]); - let gql = GqlFilter::try_from(tree).unwrap(); - let GqlFilter::And(items) = gql else { - panic!("expected GqlFilter::And"); - }; - assert!(matches!(items[0], GqlFilter::Node(_))); - assert!(matches!(items[1], GqlFilter::ExplodedEdge(_))); - } - - // Empty combinators are rejected like everywhere else in this module. - #[test] - fn empty_combinators_are_rejected() { - for (name, filter) in [ - ("and", GqlExplodedEdgeFilter::And(vec![])), - ("or", GqlExplodedEdgeFilter::Or(vec![])), - ] { - let Err(err) = super::super::expr_lowering::lower_exploded_edge_filter(&filter) else { - panic!("ExplodedEdgeFilter {name}: empty combinator must be rejected"); - }; - assert!( - err.to_string().contains("requires non-empty list"), - "ExplodedEdgeFilter {name}: unexpected error {err}" - ); - } - } -} - -#[cfg(test)] -mod filter_tree_tests { - use super::*; - use raphtory::{ - db::graph::views::filter::model::{node_filter::NodeFilter, FilterOperator}, - prelude::{EdgeFilter, Layer, Prop}, - }; - use raphtory_api::core::storage::timeindex::EventTime; - - fn node_prop_eq(name: &str, v: i64) -> FilterTree { - FilterTree::Node(CompositeNodeFilter::Property(PropertyFilter { - prop_ref: PropertyRef::Property(name.into()), - prop_value: PropertyFilterValue::Single(Prop::I64(v)), - operator: FilterOperator::Eq, - ops: vec![], - entity: NodeFilter, - })) - } - - // A mixed node∧edge tree converts arm by arm to the wire form. - #[test] - fn mixed_and_converts_structurally() { - let edge = FilterTree::Edge(CompositeEdgeFilter::Property(PropertyFilter { - prop_ref: PropertyRef::Property("w".into()), - prop_value: PropertyFilterValue::Single(Prop::I64(2)), - operator: FilterOperator::Eq, - ops: vec![], - entity: EdgeFilter, - })); - let tree = FilterTree::And(vec![node_prop_eq("x", 1), edge]); - - let gql = GqlFilter::try_from(tree).unwrap(); - let GqlFilter::And(items) = gql else { - panic!("expected GqlFilter::And"); - }; - assert!(matches!(items[0], GqlFilter::Node(_))); - assert!(matches!(items[1], GqlFilter::Edge(_))); - } - - // A view chain nests into the wire form in application order: the last op - // applied becomes the outermost `expr` level. - #[test] - fn graph_view_chain_converts_to_the_nested_wire_form() { - let tree = FilterTree::View(vec![ - GraphViewOp::Layers(Layer::from("x")), - GraphViewOp::Window { - start: EventTime::start(1), - end: EventTime::end(5), - }, - ]); - - let gql = GqlFilter::try_from(tree).unwrap(); - let GqlFilter::Graph(GqlGraphFilter::Window(ref w)) = gql else { - panic!("expected Graph(Window), got {gql:?}"); - }; - let Some(GqlGraphFilter::Layers(ref l)) = w.expr.as_deref() else { - panic!("expected inner Layers, got {:?}", w.expr); - }; - assert_eq!(l.names, vec!["x"]); - } -} diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index 0959574b0b..bdef0bf007 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -6,7 +6,8 @@ use crate::{ graph::{ edge::GqlEdge, edges::GqlEdges, - filtering::{GqlEdgeFilter, GqlFilter, GqlNodeFilter, GraphViewCollection}, + filter_expr_input::GqlFilter, + filtering::GraphViewCollection, node::GqlNode, node_id::GqlNodeId, nodes::GqlNodes, @@ -535,14 +536,14 @@ impl GqlGraph { pub async fn nodes( &self, #[graphql( - desc = "Optional node filter (by name, property, type, etc.). If omitted, every node in the view is returned." + desc = "Optional filter expression made of node predicates, graph views, or and/or/not combinations (and is an intersection). Expressions that test edges are rejected. If omitted, every node in the view is returned." )] - select: Option, + select: Option, ) -> Result { let nn = self.graph.nodes(); if let Some(sel) = select { - let nf = GqlFilter::Node(sel); + let nf = sel; let narrowed = blocking_compute({ let nn_clone = nn.clone(); move || nn_clone.select(nf) @@ -569,15 +570,14 @@ impl GqlGraph { pub async fn edges<'a>( &self, #[graphql( - desc = "Optional edge filter (by property, layer, src/dst, etc.). If omitted, every edge in the view is returned." + desc = "Optional filter expression made of edge predicates (including src/dst reads), graph views, or and/or/not combinations (and is an intersection). If omitted, every edge in the view is returned." )] - select: Option, + select: Option, ) -> Result { let base = self.graph.edges_unlocked(); if let Some(sel) = select { - let ef = GqlFilter::Edge(sel); - let narrowed = blocking_compute(move || base.select(ef)).await?; + let narrowed = blocking_compute(move || base.select(sel)).await?; return Ok(GqlEdges::new(narrowed)); } @@ -717,7 +717,7 @@ impl GqlGraph { pub async fn filter( &self, #[graphql( - desc = "Optional filter expression: node/edge predicates, graph views (window, layer, ...), or and/or/not combinations of them. `and` is an intersection: each leg is evaluated independently and the results intersect — to evaluate a predicate *inside* a view, scope the predicate itself (e.g. a windowed property condition). If omitted, applies the identity filter." + desc = "Optional filter expression made of node/edge predicates, graph views (window, layer, ...), or and/or/not combinations of them. `and` is an intersection, each leg evaluated independently and the results intersected. A `view` leg applies first and the other legs run inside it, like `graph.window(..).filter(expr)`; it must stand alone or in the top-level `and` (not under `or` or `not`). If omitted, applies the identity filter." )] expr: Option, ) -> Result { @@ -796,12 +796,7 @@ impl GqlGraph { GraphViewCollection::After(after) => return_view.after(after).await, GraphViewCollection::ShrinkStart(start) => return_view.shrink_start(start).await, GraphViewCollection::ShrinkEnd(end) => return_view.shrink_end(end).await, - GraphViewCollection::NodeFilter(filter) => { - return_view.filter(Some(GqlFilter::Node(filter))).await? - } - GraphViewCollection::EdgeFilter(filter) => { - return_view.filter(Some(GqlFilter::Edge(filter))).await? - } + GraphViewCollection::Filter(filter) => return_view.filter(Some(filter)).await?, }; } Ok(return_view) diff --git a/raphtory-graphql/src/model/graph/mod.rs b/raphtory-graphql/src/model/graph/mod.rs index 0e4114b00e..548df3ef25 100644 --- a/raphtory-graphql/src/model/graph/mod.rs +++ b/raphtory-graphql/src/model/graph/mod.rs @@ -5,7 +5,7 @@ use raphtory_api::core::utils::time::ParseTimeError; pub mod collection; pub mod edge; pub mod edges; -pub(crate) mod expr_lowering; +pub mod filter_expr_input; pub mod filtering; pub mod graph; pub mod history; diff --git a/raphtory-graphql/src/model/graph/namespace_filtering.rs b/raphtory-graphql/src/model/graph/namespace_filtering.rs index d5cde3f28a..a6efb1d539 100644 --- a/raphtory-graphql/src/model/graph/namespace_filtering.rs +++ b/raphtory-graphql/src/model/graph/namespace_filtering.rs @@ -60,7 +60,7 @@ pub enum MetaGraphField { /// Narrows a namespace's graph listing. /// -/// Composes the same way as the graph/node/edge filters: leaves test one +/// Composes like a filter expression: leaves test one /// attribute or metadata key, and `and` / `or` / `not` combine them. #[derive(OneOfInput, Clone, Debug)] pub enum MetaGraphFilter { diff --git a/raphtory-graphql/src/model/graph/nested_edges.rs b/raphtory-graphql/src/model/graph/nested_edges.rs index 405a4a5992..7efa031af1 100644 --- a/raphtory-graphql/src/model/graph/nested_edges.rs +++ b/raphtory-graphql/src/model/graph/nested_edges.rs @@ -2,7 +2,8 @@ use crate::{ model::graph::{ collection::{check_list_allowed, check_page_limit}, edges::GqlEdges, - filtering::{EdgesViewCollection, GqlFilter}, + filter_expr_input::GqlFilter, + filtering::EdgesViewCollection, path_from_graph::GqlPathFromGraph, timeindex::{GqlEventTime, GqlTimeInput}, }, @@ -226,9 +227,7 @@ impl GqlNestedEdges { EdgesViewCollection::After(time) => return_view.after(time).await, EdgesViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, EdgesViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - EdgesViewCollection::EdgeFilter(filter) => { - return_view.filter(GqlFilter::Edge(filter)).await? - } + EdgesViewCollection::Filter(filter) => return_view.filter(filter).await?, } } diff --git a/raphtory-graphql/src/model/graph/node.rs b/raphtory-graphql/src/model/graph/node.rs index 5e0b297cd9..2207a9c1f8 100644 --- a/raphtory-graphql/src/model/graph/node.rs +++ b/raphtory-graphql/src/model/graph/node.rs @@ -1,7 +1,8 @@ use crate::{ model::graph::{ edges::GqlEdges, - filtering::{GqlEdgeFilter, GqlFilter, GqlNodeFilter, NodeViewCollection}, + filter_expr_input::GqlFilter, + filtering::NodeViewCollection, history::GqlHistory, node_id::GqlNodeId, nodes::GqlNodes, @@ -286,9 +287,7 @@ impl GqlNode { NodeViewCollection::After(time) => return_view.after(time).await, NodeViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, NodeViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - NodeViewCollection::NodeFilter(filter) => { - return_view.filter(GqlFilter::Node(filter)).await? - } + NodeViewCollection::Filter(filter) => return_view.filter(filter).await?, } } Ok(return_view) @@ -450,33 +449,30 @@ impl GqlNode { } /// Returns all connected edges. - pub async fn edges(&self, select: Option) -> Result { + pub async fn edges(&self, select: Option) -> Result { let base = self.vv.edges(); if let Some(sel) = select { - let ef = GqlFilter::Edge(sel); - let narrowed = blocking_compute(move || base.select(ef)).await?; + let narrowed = blocking_compute(move || base.select(sel)).await?; return Ok(GqlEdges::new(narrowed)); } Ok(GqlEdges::new(base)) } /// Returns outgoing edges. - pub async fn out_edges(&self, select: Option) -> Result { + pub async fn out_edges(&self, select: Option) -> Result { let base = self.vv.out_edges(); if let Some(sel) = select { - let ef = GqlFilter::Edge(sel); - let narrowed = blocking_compute(move || base.select(ef)).await?; + let narrowed = blocking_compute(move || base.select(sel)).await?; return Ok(GqlEdges::new(narrowed)); } Ok(GqlEdges::new(base)) } /// Returns incoming edges. - pub async fn in_edges(&self, select: Option) -> Result { + pub async fn in_edges(&self, select: Option) -> Result { let base = self.vv.in_edges(); if let Some(sel) = select { - let ef = GqlFilter::Edge(sel); - let narrowed = blocking_compute(move || base.select(ef)).await?; + let narrowed = blocking_compute(move || base.select(sel)).await?; return Ok(GqlEdges::new(narrowed)); } Ok(GqlEdges::new(base)) @@ -485,12 +481,11 @@ impl GqlNode { /// Returns neighbouring nodes. pub async fn neighbours<'a>( &self, - select: Option, + select: Option, ) -> Result { let base = self.vv.neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromNode::new(narrowed)); } Ok(GqlPathFromNode::new(base)) @@ -499,12 +494,11 @@ impl GqlNode { /// Returns the number of neighbours that have at least one in-going edge to this node. pub async fn in_neighbours<'a>( &self, - select: Option, + select: Option, ) -> Result { let base = self.vv.in_neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromNode::new(narrowed)); } Ok(GqlPathFromNode::new(base)) @@ -513,12 +507,11 @@ impl GqlNode { /// Returns the number of neighbours that have at least one out-going edge from this node. pub async fn out_neighbours( &self, - select: Option, + select: Option, ) -> Result { let base = self.vv.out_neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromNode::new(narrowed)); } Ok(GqlPathFromNode::new(base)) diff --git a/raphtory-graphql/src/model/graph/nodes.rs b/raphtory-graphql/src/model/graph/nodes.rs index b6b66533ce..face7b1633 100644 --- a/raphtory-graphql/src/model/graph/nodes.rs +++ b/raphtory-graphql/src/model/graph/nodes.rs @@ -2,7 +2,8 @@ use crate::{ model::{ graph::{ collection::{check_list_allowed, check_page_limit}, - filtering::{GqlEdgeFilter, GqlFilter, GqlNodeFilter, NodesViewCollection}, + filter_expr_input::GqlFilter, + filtering::NodesViewCollection, nested_edges::GqlNestedEdges, node::GqlNode, path_from_graph::GqlPathFromGraph, @@ -306,9 +307,7 @@ impl GqlNodes { NodesViewCollection::After(time) => return_view.after(time).await, NodesViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, NodesViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - NodesViewCollection::NodeFilter(node_filter) => { - return_view.filter(GqlFilter::Node(node_filter)).await? - } + NodesViewCollection::Filter(node_filter) => return_view.filter(node_filter).await?, NodesViewCollection::TypeFilter(types) => return_view.type_filter(types).await, } } @@ -572,12 +571,11 @@ impl GqlNodes { /// Returns the neighbouring nodes of each node in the collection. pub async fn neighbours( &self, - select: Option, + select: Option, ) -> Result { let base = self.nn.neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } Ok(GqlPathFromGraph::new(base)) @@ -586,12 +584,11 @@ impl GqlNodes { /// Returns the in-neighbours of each node in the collection. pub async fn in_neighbours( &self, - select: Option, + select: Option, ) -> Result { let base = self.nn.in_neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } Ok(GqlPathFromGraph::new(base)) @@ -600,51 +597,41 @@ impl GqlNodes { /// Returns the out-neighbours of each node in the collection. pub async fn out_neighbours( &self, - select: Option, + select: Option, ) -> Result { let base = self.nn.out_neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } Ok(GqlPathFromGraph::new(base)) } /// Returns the incident edges (both directions) of each node in the collection. - pub async fn edges(&self, select: Option) -> Result { + pub async fn edges(&self, select: Option) -> Result { let base = self.nn.edges(); if let Some(expr) = select { - let ef = GqlFilter::Edge(expr); - let narrowed = blocking_compute(move || base.select(ef)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlNestedEdges::new(narrowed)); } Ok(GqlNestedEdges::new(base)) } /// Returns the incoming edges of each node in the collection. - pub async fn in_edges( - &self, - select: Option, - ) -> Result { + pub async fn in_edges(&self, select: Option) -> Result { let base = self.nn.in_edges(); if let Some(expr) = select { - let ef = GqlFilter::Edge(expr); - let narrowed = blocking_compute(move || base.select(ef)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlNestedEdges::new(narrowed)); } Ok(GqlNestedEdges::new(base)) } /// Returns the outgoing edges of each node in the collection. - pub async fn out_edges( - &self, - select: Option, - ) -> Result { + pub async fn out_edges(&self, select: Option) -> Result { let base = self.nn.out_edges(); if let Some(expr) = select { - let ef = GqlFilter::Edge(expr); - let narrowed = blocking_compute(move || base.select(ef)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlNestedEdges::new(narrowed)); } Ok(GqlNestedEdges::new(base)) diff --git a/raphtory-graphql/src/model/graph/path_from_graph.rs b/raphtory-graphql/src/model/graph/path_from_graph.rs index e9559fb1ed..34274323ca 100644 --- a/raphtory-graphql/src/model/graph/path_from_graph.rs +++ b/raphtory-graphql/src/model/graph/path_from_graph.rs @@ -1,7 +1,8 @@ use crate::{ model::graph::{ collection::{check_list_allowed, check_page_limit}, - filtering::{GqlFilter, GqlNodeFilter, PathFromNodeViewCollection}, + filter_expr_input::GqlFilter, + filtering::PathFromNodeViewCollection, history::GqlHistory, nested_edges::GqlNestedEdges, path_from_node::GqlPathFromNode, @@ -540,11 +541,10 @@ impl GqlPathFromGraph { /// Returns the neighbouring nodes reachable one further hop from each source /// path (both directions), as a nested `PathFromGraph`. - pub async fn neighbours(&self, select: Option) -> Result { + pub async fn neighbours(&self, select: Option) -> Result { let base = self.nn.neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } Ok(GqlPathFromGraph::new(base)) @@ -552,11 +552,10 @@ impl GqlPathFromGraph { /// Returns the in-neighbours reachable one further hop from each source /// path, as a nested `PathFromGraph`. - pub async fn in_neighbours(&self, select: Option) -> Result { + pub async fn in_neighbours(&self, select: Option) -> Result { let base = self.nn.in_neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } Ok(GqlPathFromGraph::new(base)) @@ -564,11 +563,10 @@ impl GqlPathFromGraph { /// Returns the out-neighbours reachable one further hop from each source /// path, as a nested `PathFromGraph`. - pub async fn out_neighbours(&self, select: Option) -> Result { + pub async fn out_neighbours(&self, select: Option) -> Result { let base = self.nn.out_neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromGraph::new(narrowed)); } Ok(GqlPathFromGraph::new(base)) diff --git a/raphtory-graphql/src/model/graph/path_from_node.rs b/raphtory-graphql/src/model/graph/path_from_node.rs index dc4ae62860..c4e2bab2a4 100644 --- a/raphtory-graphql/src/model/graph/path_from_node.rs +++ b/raphtory-graphql/src/model/graph/path_from_node.rs @@ -2,7 +2,8 @@ use crate::{ model::graph::{ collection::{check_list_allowed, check_page_limit}, edges::GqlEdges, - filtering::{GqlFilter, GqlNodeFilter, PathFromNodeViewCollection}, + filter_expr_input::GqlFilter, + filtering::PathFromNodeViewCollection, history::GqlHistory, node::GqlNode, timeindex::{GqlEventTime, GqlTimeInput}, @@ -528,12 +529,11 @@ impl GqlPathFromNode { /// (both directions), as a flat `PathFromNode`. pub async fn neighbours( &self, - select: Option, + select: Option, ) -> Result { let base = self.nn.neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromNode::new(narrowed)); } Ok(GqlPathFromNode::new(base)) @@ -543,12 +543,11 @@ impl GqlPathFromNode { /// flat `PathFromNode`. pub async fn in_neighbours( &self, - select: Option, + select: Option, ) -> Result { let base = self.nn.in_neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromNode::new(narrowed)); } Ok(GqlPathFromNode::new(base)) @@ -558,12 +557,11 @@ impl GqlPathFromNode { /// flat `PathFromNode`. pub async fn out_neighbours( &self, - select: Option, + select: Option, ) -> Result { let base = self.nn.out_neighbours(); if let Some(expr) = select { - let nf = GqlFilter::Node(expr); - let narrowed = blocking_compute(move || base.select(nf)).await?; + let narrowed = blocking_compute(move || base.select(expr)).await?; return Ok(GqlPathFromNode::new(narrowed)); } Ok(GqlPathFromNode::new(base)) diff --git a/raphtory-graphql/src/python/client/mod.rs b/raphtory-graphql/src/python/client/mod.rs index fff224ff03..47505d76b8 100644 --- a/raphtory-graphql/src/python/client/mod.rs +++ b/raphtory-graphql/src/python/client/mod.rs @@ -7,7 +7,7 @@ use crate::{ }; use pyo3::{exceptions::PyValueError, prelude::*, pyclass, pymethods}; use raphtory::{ - db::graph::views::filter::model::FilterTree, errors::GraphError, + db::graph::views::filter::model::tree::FilterExpr, errors::GraphError, python::filter::filter_expr::PyFilterExpr, }; use raphtory_api::{ @@ -44,10 +44,8 @@ pub(crate) mod view_ops; /// local `Nodes.__getitem__` raises, so one `except` clause catches it on /// either backend — and at the same moment: locally the rejection happens at /// subscript time, not at first read. -pub(crate) fn node_subscript(filter: &PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; +pub(crate) fn node_subscript(filter: &PyFilterExpr) -> PyResult { + let tree = filter.tree().clone(); if tree.tests_edges() { return Err(adapt_err_value(&GraphError::NotNodeFilter)); } @@ -162,12 +160,16 @@ impl PyEdgeAddition { // Takes care of the ClientError -> PyException conversion. // A permission denial maps to the distinct `RemotePermissionError` type so -// callers can catch it specifically; everything else (including a missing graph) -// stays a generic exception. +// callers can catch it specifically; a filter that has no server-side form is +// a `ValueError`, as the filter methods document; everything else (including +// a missing graph) stays a generic exception. impl From for PyErr { fn from(err: ClientError) -> Self { match &err { ClientError::PermissionDenied(msg) => RemotePermissionError::new_err(msg.clone()), + ClientError::Graph(GraphError::InvalidGqlFilter(msg)) => { + PyValueError::new_err(msg.clone()) + } _ => adapt_err_value(&err), } } diff --git a/raphtory-graphql/src/python/client/remote_edge.rs b/raphtory-graphql/src/python/client/remote_edge.rs index f247915ee6..4a35d3f94a 100644 --- a/raphtory-graphql/src/python/client/remote_edge.rs +++ b/raphtory-graphql/src/python/client/remote_edge.rs @@ -8,7 +8,7 @@ use crate::{ remote_node::PyRemoteNode, }, }; -use pyo3::{exceptions::PyValueError, pyclass, pymethods, PyResult}; +use pyo3::{pyclass, pymethods, PyResult}; use raphtory::python::{filter::filter_expr::PyFilterExpr, utils::execute_async_task}; use raphtory_api::{ core::{ @@ -51,11 +51,10 @@ impl PyRemoteEdge { /// RemoteEdge: a new filtered edge view. /// /// Raises: - /// ValueError: if the filter cannot be represented remotely. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemoteEdge::new(self.edge.filter(tree)?)) } diff --git a/raphtory-graphql/src/python/client/remote_edges.rs b/raphtory-graphql/src/python/client/remote_edges.rs index a1eeef9e38..6b07b35799 100644 --- a/raphtory-graphql/src/python/client/remote_edges.rs +++ b/raphtory-graphql/src/python/client/remote_edges.rs @@ -7,7 +7,7 @@ use crate::{ remote_path_from_node::PyRemotePathFromNode, }, }; -use pyo3::{exceptions::PyValueError, pyclass, pymethods, PyRef, PyRefMut, PyResult}; +use pyo3::{pyclass, pymethods, PyRef, PyRefMut, PyResult}; use raphtory::python::{ filter::filter_expr::PyFilterExpr, graph::sorting::PyEdgeSortBy, utils::execute_async_task, }; @@ -84,12 +84,10 @@ impl PyRemoteEdges { /// RemoteEdges: a new collection with the filter applied. /// /// Raises: - /// ValueError: if the filter cannot be represented as a GraphQL - /// `EdgeFilter` (e.g. references node-only fields). + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemoteEdges::new(self.edges.filter(tree)?)) } @@ -105,11 +103,10 @@ impl PyRemoteEdges { /// RemoteEdges: a new collection narrowed to matching edges. /// /// Raises: - /// ValueError: if the filter cannot be sent over the wire. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemoteEdges::new(self.edges.select(tree)?)) } diff --git a/raphtory-graphql/src/python/client/remote_graph.rs b/raphtory-graphql/src/python/client/remote_graph.rs index c77ee00f41..feb47d8b48 100644 --- a/raphtory-graphql/src/python/client/remote_graph.rs +++ b/raphtory-graphql/src/python/client/remote_graph.rs @@ -17,7 +17,7 @@ use crate::{ PyEdgeAddition, PyNodeAddition, }, }; -use pyo3::{exceptions::PyValueError, pyclass, pymethods, PyResult}; +use pyo3::{pyclass, pymethods, PyResult}; use raphtory::python::{filter::filter_expr::PyFilterExpr, utils::execute_async_task}; use raphtory_api::{ core::{ @@ -68,12 +68,10 @@ impl PyRemoteGraph { /// RemoteGraph: a new filtered graph view. /// /// Raises: - /// ValueError: if the filter cannot be represented as a GraphQL - /// `NodeFilter` or `EdgeFilter`. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemoteGraph { graph: Arc::new(self.graph.filter(tree)?), }) diff --git a/raphtory-graphql/src/python/client/remote_nested_edges.rs b/raphtory-graphql/src/python/client/remote_nested_edges.rs index 2fdc12b646..82e5e264d4 100644 --- a/raphtory-graphql/src/python/client/remote_nested_edges.rs +++ b/raphtory-graphql/src/python/client/remote_nested_edges.rs @@ -7,7 +7,7 @@ use crate::{ remote_path_from_graph::PyRemotePathFromGraph, }, }; -use pyo3::{exceptions::PyValueError, pyclass, pymethods, PyRef, PyRefMut, PyResult}; +use pyo3::{pyclass, pymethods, PyRef, PyRefMut, PyResult}; use raphtory::python::{filter::filter_expr::PyFilterExpr, utils::execute_async_task}; use raphtory_api::{ core::{entities::GID, storage::timeindex::EventTime, utils::time::InputTime}, @@ -57,12 +57,10 @@ impl PyRemoteNestedEdges { /// RemoteNestedEdges: a new collection with the filter applied. /// /// Raises: - /// ValueError: if the filter cannot be represented as a GraphQL - /// `EdgeFilter`. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemoteNestedEdges::new(self.edges.filter(tree)?)) } @@ -78,11 +76,10 @@ impl PyRemoteNestedEdges { /// RemoteNestedEdges: a new collection narrowed to matching edges. /// /// Raises: - /// ValueError: if the filter cannot be sent over the wire. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemoteNestedEdges::new(self.edges.select(tree)?)) } diff --git a/raphtory-graphql/src/python/client/remote_node.rs b/raphtory-graphql/src/python/client/remote_node.rs index 166981c6d2..9a0b0c4c5c 100644 --- a/raphtory-graphql/src/python/client/remote_node.rs +++ b/raphtory-graphql/src/python/client/remote_node.rs @@ -9,7 +9,7 @@ use crate::{ remote_path_from_node::PyRemotePathFromNode, }, }; -use pyo3::{exceptions::PyValueError, pyclass, pymethods, PyResult}; +use pyo3::{pyclass, pymethods, PyResult}; use raphtory::python::{filter::filter_expr::PyFilterExpr, utils::execute_async_task}; use raphtory_api::{ core::{ @@ -55,12 +55,10 @@ impl PyRemoteNode { /// RemoteNode: a new filtered node view. /// /// Raises: - /// ValueError: if the filter cannot be represented as a GraphQL - /// `NodeFilter` (e.g. references edge fields). + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemoteNode::new(self.node.filter(tree)?)) } diff --git a/raphtory-graphql/src/python/client/remote_nodes.rs b/raphtory-graphql/src/python/client/remote_nodes.rs index b59bd5121d..c98a75613d 100644 --- a/raphtory-graphql/src/python/client/remote_nodes.rs +++ b/raphtory-graphql/src/python/client/remote_nodes.rs @@ -9,7 +9,7 @@ use crate::{ remote_path_from_graph::PyRemotePathFromGraph, }, }; -use pyo3::{exceptions::PyValueError, pyclass, pymethods, PyRef, PyRefMut, PyResult}; +use pyo3::{pyclass, pymethods, PyRef, PyRefMut, PyResult}; use raphtory::python::{ filter::filter_expr::PyFilterExpr, graph::sorting::PyNodeSortBy, utils::execute_async_task, }; @@ -68,12 +68,10 @@ impl PyRemoteNodes { /// RemoteNodes: a new collection with the filter applied. /// /// Raises: - /// ValueError: if the filter cannot be represented as a GraphQL - /// `NodeFilter` (e.g. references edge fields). + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemoteNodes::new(self.nodes.filter(tree)?)) } @@ -92,7 +90,8 @@ impl PyRemoteNodes { /// Raises: /// Exception: if the expression tests edges rather than nodes — the /// same error the local `Nodes.__getitem__` raises. - /// ValueError: if the filter cannot be sent over the wire. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { Ok(PyRemoteNodes::new( self.nodes.select(node_subscript(&filter)?)?, diff --git a/raphtory-graphql/src/python/client/remote_path_from_graph.rs b/raphtory-graphql/src/python/client/remote_path_from_graph.rs index 1fb29161a2..22b1f4112d 100644 --- a/raphtory-graphql/src/python/client/remote_path_from_graph.rs +++ b/raphtory-graphql/src/python/client/remote_path_from_graph.rs @@ -10,7 +10,7 @@ use crate::{ remote_path_from_node::PyRemotePathFromNode, }, }; -use pyo3::{exceptions::PyValueError, pyclass, pymethods, PyRef, PyRefMut, PyResult}; +use pyo3::{pyclass, pymethods, PyRef, PyRefMut, PyResult}; use raphtory::python::{filter::filter_expr::PyFilterExpr, utils::execute_async_task}; use raphtory_api::{ core::{entities::GID, storage::timeindex::EventTime, utils::time::InputTime}, @@ -58,12 +58,10 @@ impl PyRemotePathFromGraph { /// RemotePathFromGraph: a new collection with the filter applied. /// /// Raises: - /// ValueError: if the filter cannot be represented as a GraphQL - /// `NodeFilter`. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemotePathFromGraph::new(self.path.filter(tree)?)) } @@ -82,7 +80,8 @@ impl PyRemotePathFromGraph { /// Raises: /// Exception: if the expression tests edges rather than nodes — the /// same error the local `PathFromGraph.__getitem__` raises. - /// ValueError: if the filter cannot be sent over the wire. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { Ok(PyRemotePathFromGraph::new( self.path.select(node_subscript(&filter)?)?, diff --git a/raphtory-graphql/src/python/client/remote_path_from_node.rs b/raphtory-graphql/src/python/client/remote_path_from_node.rs index e1f903adbc..6d7755aea2 100644 --- a/raphtory-graphql/src/python/client/remote_path_from_node.rs +++ b/raphtory-graphql/src/python/client/remote_path_from_node.rs @@ -9,7 +9,7 @@ use crate::{ remote_node::PyRemoteNode, }, }; -use pyo3::{exceptions::PyValueError, pyclass, pymethods, PyRef, PyRefMut, PyResult}; +use pyo3::{pyclass, pymethods, PyRef, PyRefMut, PyResult}; use raphtory::python::{filter::filter_expr::PyFilterExpr, utils::execute_async_task}; use raphtory_api::{ core::{entities::GID, storage::timeindex::EventTime, utils::time::InputTime}, @@ -56,12 +56,10 @@ impl PyRemotePathFromNode { /// RemotePathFromNode: a new collection with the filter applied. /// /// Raises: - /// ValueError: if the filter cannot be represented as a GraphQL - /// `NodeFilter`. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter - .try_as_filter_tree() - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let tree = filter.tree().clone(); Ok(PyRemotePathFromNode::new(self.path.filter(tree)?)) } @@ -80,7 +78,8 @@ impl PyRemotePathFromNode { /// Raises: /// Exception: if the expression tests edges rather than nodes — the /// same error the local `PathFromNode.__getitem__` raises. - /// ValueError: if the filter cannot be sent over the wire. + /// ValueError: if the filter has no server-side form because it reads + /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { Ok(PyRemotePathFromNode::new( self.path.select(node_subscript(&filter)?)?, diff --git a/raphtory-tests/tests/qp3.rs b/raphtory-tests/tests/qp3.rs deleted file mode 100644 index 68ab30e6dc..0000000000 --- a/raphtory-tests/tests/qp3.rs +++ /dev/null @@ -1,30 +0,0 @@ -use raphtory::{ - db::{ - api::view::Filter, - graph::views::filter::model::{node_expr::DynCreateOp, DynCreateFilter}, - }, - prelude::*, -}; -use std::sync::Arc; - -#[test] -fn edge_endpoint_id_validation() { - let g = Graph::new(); - g.add_edge(0, "a", "b", NO_PROPS, None).unwrap(); - - // typed - let f = EntityExprFilterOps::eq(EdgeFilter::src().id(), Prop::I64(3)); - match g.filter(f) { - Ok(_) => println!("typed: NO RAISE"), - Err(e) => println!("typed: raised {e}"), - } - - // dyn (python path) - let id_expr: Arc = Arc::new(EdgeFilter::src().id()); - let cmp = EntityExprFilterOps::eq(id_expr, Prop::I64(3)); - let dynf: Arc = Arc::new(cmp); - match g.filter(dynf) { - Ok(_) => println!("dyn: NO RAISE"), - Err(e) => println!("dyn: raised {e}"), - } -} diff --git a/raphtory/src/db/api/state/ops/filter.rs b/raphtory/src/db/api/state/ops/filter.rs index 4bb8be5537..b3bb6da90f 100644 --- a/raphtory/src/db/api/state/ops/filter.rs +++ b/raphtory/src/db/api/state/ops/filter.rs @@ -1,7 +1,7 @@ use crate::{ db::{ api::{ - state::ops::{Const, IntoDynNodeOp, NodeOp, TypeId}, + state::ops::{Const, IntoDynNodeOp, NodeOp}, view::internal::{GraphView, NodeList}, }, graph::create_node_type_filter, diff --git a/raphtory/src/db/graph/views/filter/model/degree_filter.rs b/raphtory/src/db/graph/views/filter/model/degree_filter.rs deleted file mode 100644 index fe8af4c496..0000000000 --- a/raphtory/src/db/graph/views/filter/model/degree_filter.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::db::graph::views::filter::model::{ - property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, - ComposableFilter, FilterOperator, NodeFilter, -}; -use raphtory_api::core::Direction; -use std::{fmt, fmt::Display}; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct DegreeFilter { - pub direction: Direction, - pub operator: FilterOperator, - pub value: PropertyFilterValue, - pub ops: Vec, -} - -fn property_ref(direction: &Direction) -> PropertyRef { - match direction { - Direction::IN => PropertyRef::Property("in_degree".to_string()), - Direction::OUT => PropertyRef::Property("out_degree".to_string()), - Direction::BOTH => PropertyRef::Property("degree".to_string()), - } -} - -impl ComposableFilter for DegreeFilter {} - -impl Display for DegreeFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let property_filter = PropertyFilter { - prop_ref: property_ref(&self.direction), - prop_value: self.value.clone(), - operator: self.operator, - ops: self.ops.clone(), - entity: NodeFilter, - }; - property_filter.fmt(f) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/dyn_factory.rs b/raphtory/src/db/graph/views/filter/model/dyn_factory.rs new file mode 100644 index 0000000000..9325ac52ef --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/dyn_factory.rs @@ -0,0 +1,227 @@ +//! Type-erased filter factories. +//! +//! The typed factories (`NodeFilter`, `EdgeFilter`, and their view wrappers) +//! form an open family of generic types. Anything that builds a filter from +//! runtime data — the python bindings, a deserialised filter tree — needs one +//! type to hold whichever factory the data names, so each family is erased +//! behind a trait object here. Views wrap the *erased* factory, which keeps +//! the set of concrete types finite; wrapping the typed factory would ask the +//! compiler for a vtable per wrapper combination. + +use crate::db::graph::views::filter::model::{ + after_bounds, at_bounds, before_bounds, + is_active_edge_filter::IsActiveEdge, + is_active_node_filter::IsActiveNode, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + node_expr::{DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr}, + windowed_filter::Windowed, + CombinedFilter, CreateView, DynCreateFilter, DynCreateView, DynPropertyExprFactory, + EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, NodeFilterFactory, + NodeViewFilterOps, PropertyExprFactory, ViewWrapOps, +}; +use raphtory_api::core::storage::timeindex::EventTime; +use std::sync::Arc; + +pub trait DynNodeFilterFactory: + DynPropertyExprFactory + DynEntityExpr + DynCreateView + Send + Sync + 'static +{ + fn dyn_id(&self) -> Arc; + fn dyn_name(&self) -> Arc; + fn dyn_node_type(&self) -> Arc; + fn dyn_degree(&self) -> Arc; + fn dyn_in_degree(&self) -> Arc; + fn dyn_out_degree(&self) -> Arc; + fn dyn_is_active(&self) -> Arc; + fn dyn_metadata(&self, name: String) -> Arc; + + fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc; + + fn dyn_bounds(&self) -> (EventTime, EventTime); +} + +impl InternalViewWrapOps for Arc { + type Window = Arc; + + // Both calls dispatch through the vtable explicitly: plain method syntax + // would select the DynNodeFilterFactory blanket on Arc itself and loop. + fn bounds(&self) -> (EventTime, EventTime) { + self.as_ref().dyn_bounds() + } + + fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { + self.as_ref().dyn_build_window(start, end) + } +} + +impl DynNodeFilterFactory for T +where + T: NodeFilterFactory + NodeViewFilterOps + Send + Sync + 'static, +{ + fn dyn_id(&self) -> Arc { + Arc::new(self.id()) + } + fn dyn_name(&self) -> Arc { + Arc::new(self.name()) + } + fn dyn_node_type(&self) -> Arc { + Arc::new(self.node_type()) + } + + fn dyn_degree(&self) -> Arc { + Arc::new(self.degree()) + } + fn dyn_in_degree(&self) -> Arc { + Arc::new(self.in_degree()) + } + fn dyn_out_degree(&self) -> Arc { + Arc::new(self.out_degree()) + } + + fn dyn_is_active(&self) -> Arc { + Arc::new(self.is_active()) + } + + fn dyn_metadata(&self, name: String) -> Arc { + Arc::new(PropertyExprFactory::metadata(self, name)) + } + + fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc { + Arc::new(self.clone().build_window(start, end)) + } + + fn dyn_bounds(&self) -> (EventTime, EventTime) { + self.bounds() + } +} + +impl NodeFilterFactory for Arc { + type NodeWindow = Self::Window; +} + +impl NodeViewFilterOps for Arc { + type Output = Arc; + + fn is_active(&self) -> Self::Output { + self.as_ref().dyn_is_active() + } +} + +pub trait DynEdgeFilterFactory: DynEntityExpr + DynCreateView + Send + Sync + 'static { + fn dyn_property(&self, name: String) -> Arc; + fn dyn_metadata(&self, name: String) -> Arc; + + fn dyn_is_active(&self) -> Arc; + fn dyn_is_valid(&self) -> Arc; + fn dyn_is_deleted(&self) -> Arc; + fn dyn_is_self_loop(&self) -> Arc; + + fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc; + fn dyn_at(&self, time: EventTime) -> Arc; + fn dyn_after(&self, time: EventTime) -> Arc; + fn dyn_before(&self, time: EventTime) -> Arc; + fn dyn_latest(&self) -> Arc; + fn dyn_snapshot_at(&self, time: EventTime) -> Arc; + fn dyn_snapshot_latest(&self) -> Arc; + fn dyn_layer(&self, layers: Vec) -> Arc; +} + +impl EdgeFilterFactory for Arc {} + +impl EdgeViewFilterOps for Arc { + type Output = Arc; + + fn is_active(&self) -> Self::Output { + self.as_ref().dyn_is_active() + } + + fn is_valid(&self) -> Self::Output { + self.as_ref().dyn_is_valid() + } + + fn is_deleted(&self) -> Self::Output { + self.as_ref().dyn_is_deleted() + } + + fn is_self_loop(&self) -> Self::Output { + self.as_ref().dyn_is_self_loop() + } +} + +impl InternalViewWrapOps for Arc { + type Window = Arc; + + fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { + self.as_ref().dyn_window(start, end) + } +} + +impl DynEdgeFilterFactory for T +where + T: EdgeFilterFactory + EdgeViewFilterOps + ViewWrapOps + CreateView + EntityExpr + Clone, + T: Send + Sync + 'static, + ::Marker: Into, +{ + fn dyn_property(&self, name: String) -> Arc { + Arc::new(PropertyExprFactory::property(self, name)) + } + fn dyn_metadata(&self, name: String) -> Arc { + Arc::new(PropertyExprFactory::metadata(self, name)) + } + + fn dyn_is_active(&self) -> Arc { + Arc::new(self.is_active()) + } + fn dyn_is_valid(&self) -> Arc { + Arc::new(self.is_valid()) + } + fn dyn_is_deleted(&self) -> Arc { + Arc::new(self.is_deleted()) + } + fn dyn_is_self_loop(&self) -> Arc { + Arc::new(self.is_self_loop()) + } + + // The window wrapper is constructed over the erased factory directly: + // routing through ViewWrapOps::window would dispatch straight back into + // this method through the erased build_window. + fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { + let dyn_self: Arc = Arc::new(self.clone()); + let (old_start, old_end) = self.bounds(); + let end = end.min(old_end); + let start = start.max(old_start).min(end); + Arc::new(Windowed::new(start, end, dyn_self)) + } + fn dyn_at(&self, time: EventTime) -> Arc { + let (start, end) = at_bounds(time); + self.dyn_window(start, end) + } + fn dyn_after(&self, time: EventTime) -> Arc { + let (start, end) = after_bounds(time); + self.dyn_window(start, end) + } + fn dyn_before(&self, time: EventTime) -> Arc { + let (start, end) = before_bounds(time); + self.dyn_window(start, end) + } + // Same erasure trick as dyn_window: wrapping the erased factory keeps the + // set of vtable-instantiated types finite; wrapping `self` directly would + // materialise a vtable for every wrapper combination. + fn dyn_latest(&self) -> Arc { + let dyn_self: Arc = Arc::new(self.clone()); + Arc::new(dyn_self.latest()) + } + fn dyn_snapshot_at(&self, time: EventTime) -> Arc { + let dyn_self: Arc = Arc::new(self.clone()); + Arc::new(dyn_self.snapshot_at(time)) + } + fn dyn_snapshot_latest(&self) -> Arc { + let dyn_self: Arc = Arc::new(self.clone()); + Arc::new(dyn_self.snapshot_latest()) + } + fn dyn_layer(&self, layers: Vec) -> Arc { + let dyn_self: Arc = Arc::new(self.clone()); + Arc::new(dyn_self.layer(layers)) + } +} 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 ec8e69d9d7..6adfaeefb9 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -16,8 +16,7 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_expr::{CreateOp, EntityExpr, PredicateLhs}, - node_filter::{CompositeNodeFilter, NodeFilter}, - property_filter::PropertyFilter, + node_filter::NodeFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, CombinedFilter, ComposableFilter, DynFilter, EdgeViewFilterOps, EntityMarker, @@ -33,6 +32,7 @@ use raphtory_api::core::{ storage::timeindex::EventTime, }; use raphtory_storage::graph::graph::GraphStorage; +use serde::{Deserialize, Serialize}; use std::{fmt, fmt::Display, sync::Arc}; // User facing entry for building edge filters. @@ -93,7 +93,8 @@ impl EdgeViewFilterOps for EdgeFilter { } } -#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum Endpoint { Src, Dst, @@ -228,47 +229,6 @@ impl CreateFilter for EdgeEndpointWrapper } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CompositeEdgeFilter { - Src(CompositeNodeFilter), - Dst(CompositeNodeFilter), - Property(PropertyFilter), - Windowed(Box>), - Latest(Box>), - SnapshotAt(Box>), - SnapshotLatest(Box>), - IsActiveEdge(IsActiveEdge), - IsValidEdge(IsValidEdge), - IsDeletedEdge(IsDeletedEdge), - IsSelfLoopEdge(IsSelfLoopEdge), - Layered(Box>), - And(Box, Box), - Or(Box, Box), - Not(Box), -} - -impl Display for CompositeEdgeFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CompositeEdgeFilter::Src(filter) => write!(f, "SRC({})", filter), - CompositeEdgeFilter::Dst(filter) => write!(f, "DST({})", filter), - CompositeEdgeFilter::Property(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::Windowed(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::Latest(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::SnapshotAt(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::SnapshotLatest(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::IsActiveEdge(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::IsValidEdge(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::IsDeletedEdge(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::IsSelfLoopEdge(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::Layered(filter) => write!(f, "{}", filter), - CompositeEdgeFilter::And(left, right) => write!(f, "({} AND {})", left, right), - CompositeEdgeFilter::Or(left, right) => write!(f, "({} OR {})", left, right), - CompositeEdgeFilter::Not(filter) => write!(f, "(NOT {})", filter), - } - } -} - // ── expr layer: a full node filter evaluated on an edge endpoint ── /// Evaluates an erased node filter against the src or dst node of each edge. 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 edb5e1c4ef..3ab096c1fe 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 @@ -4,17 +4,9 @@ use crate::{ graph::views::filter::{ exploded_edge_node_filtered_graph::ExplodedEdgeNodeFilteredGraph, model::{ - edge_filter::Endpoint, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - latest_filter::Latest, - layered_filter::Layered, - node_filter::{CompositeNodeFilter, NodeFilter}, - property_filter::PropertyFilter, - snapshot_filter::{SnapshotAt, SnapshotLatest}, - windowed_filter::Windowed, + edge_filter::Endpoint, is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, node_filter::NodeFilter, windowed_filter::Windowed, CombinedFilter, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, Wrap, }, CreateFilter, @@ -170,50 +162,3 @@ impl CreateFilter for ExplodedEdgeEndpointWra self.inner.filter_graph_view(graph) } } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CompositeExplodedEdgeFilter { - Src(CompositeNodeFilter), - Dst(CompositeNodeFilter), - Property(PropertyFilter), - Windowed(Box>), - Latest(Box>), - SnapshotAt(Box>), - SnapshotLatest(Box>), - Layered(Box>), - IsActiveEdge(IsActiveEdge), - IsValidEdge(IsValidEdge), - IsDeletedEdge(IsDeletedEdge), - IsSelfLoopEdge(IsSelfLoopEdge), - And( - Box, - Box, - ), - Or( - Box, - Box, - ), - Not(Box), -} - -impl Display for CompositeExplodedEdgeFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CompositeExplodedEdgeFilter::Src(filter) => write!(f, "SRC({})", filter), - CompositeExplodedEdgeFilter::Dst(filter) => write!(f, "DST({})", filter), - CompositeExplodedEdgeFilter::Property(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::Windowed(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::Latest(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::SnapshotAt(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::SnapshotLatest(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::IsActiveEdge(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::IsValidEdge(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::IsDeletedEdge(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::IsSelfLoopEdge(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::Layered(filter) => write!(f, "{}", filter), - CompositeExplodedEdgeFilter::And(left, right) => write!(f, "({} AND {})", left, right), - CompositeExplodedEdgeFilter::Or(left, right) => write!(f, "({} OR {})", left, right), - CompositeExplodedEdgeFilter::Not(filter) => write!(f, "(NOT {})", filter), - } - } -} diff --git a/raphtory/src/db/graph/views/filter/model/filter.rs b/raphtory/src/db/graph/views/filter/model/filter.rs index 5d31772d7f..01d13af43f 100644 --- a/raphtory/src/db/graph/views/filter/model/filter.rs +++ b/raphtory/src/db/graph/views/filter/model/filter.rs @@ -244,10 +244,3 @@ impl Filter { // `filter_value::FilterValue` owns that name here, so the field form carries // the longer one and this alias keeps the model's spelling valid. pub type FilterValue = FieldFilterValue; - -/// Wire names of the built-in node fields, shared by every layer that spells -/// them: the python wire recording, the GraphQL field mapping, and the -/// composite data they both produce. -pub const NODE_ID_FIELD: &str = "node_id"; -pub const NODE_NAME_FIELD: &str = "node_name"; -pub const NODE_TYPE_FIELD: &str = "node_type"; 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 c71c6405b5..fb1a8bc8a7 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -5,7 +5,7 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, GidRef, GID}, storage::arc_str::ArcStr, }; -use std::{cmp::Ordering, collections::HashSet, fmt, fmt::Display, ops::Deref}; +use std::{collections::HashSet, fmt, fmt::Display, ops::Deref}; use strsim::levenshtein; #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index a40232b316..7bc2a544be 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -39,14 +39,13 @@ use crate::{ }, graph::views::{ filter::model::{ - edge_filter::CompositeEdgeFilter, is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, latest_filter::Latest, - layered_filter::{layer_label, Layered}, + layered_filter::Layered, node_expr::{NodeMetaOp, NodePropOp}, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, @@ -64,14 +63,10 @@ use raphtory_api::core::{ storage::timeindex::{AsTime, EventTime}, utils::time::IntoTime, }; -use std::{ - fmt::{self, Display}, - ops::Deref, - sync::Arc, -}; +use std::{ops::Deref, sync::Arc}; pub mod and_filter; -pub mod degree_filter; +pub mod dyn_factory; pub mod edge_expr; pub mod edge_filter; pub mod exploded_edge_filter; @@ -88,13 +83,12 @@ pub mod latest_filter; pub mod layered_filter; pub mod node_expr; pub mod node_filter; -pub use exploded_edge_filter::CompositeExplodedEdgeFilter; -pub use node_filter::CompositeNodeFilter; pub mod node_state_filter; pub mod not_filter; pub mod or_filter; pub mod property_filter; pub mod snapshot_filter; +pub mod tree; pub mod windowed_filter; #[derive(Debug, Copy, Clone)] @@ -318,94 +312,6 @@ impl EntityExpr for PropertyExpr { } } -/// One graph-level view restriction, as data. `at`/`before`/`after` lower to -/// `Window` at construction time (see `ViewWrapOps`), so they need no -/// variants here. -#[derive(Clone, Debug, PartialEq)] -pub enum GraphViewOp { - Window { start: EventTime, end: EventTime }, - Latest, - SnapshotAt(EventTime), - SnapshotLatest, - Layers(Layer), -} - -/// Kind-tagged, owned export of a filter tree — the transportable form of a -/// composed filter, referencing no in-process state. `View` is an -/// chain of graph-level restrictions in application order (each later op -/// wraps outside the previous one). Recorded at construction on the python -/// side; filters that inherently reference in-process state (e.g. node-state -/// columns) carry no tree. -#[derive(Clone, Debug)] -pub enum FilterTree { - Node(CompositeNodeFilter), - Edge(CompositeEdgeFilter), - ExplodedEdge(CompositeExplodedEdgeFilter), - View(Vec), - And(Vec), - Or(Vec), - Not(Box), -} - -impl Display for GraphViewOp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - GraphViewOp::Window { start, end } => write!(f, "WINDOW[{}..{}]", start.t(), end.t()), - GraphViewOp::Latest => write!(f, "LATEST"), - GraphViewOp::SnapshotAt(time) => write!(f, "SNAPSHOT_AT[{}]", time.t()), - GraphViewOp::SnapshotLatest => write!(f, "SNAPSHOT_LATEST"), - GraphViewOp::Layers(layer) => write!(f, "LAYER[{}]", layer_label(layer)), - } - } -} - -/// The wire form as text, in the same notation the composite filters print -/// themselves in: predicates as `lhs op value`, views as `KIND[args](inner)`, -/// combinators as `(a AND b)`, `(a OR b)` and `NOT(a)`. A view chain lists its -/// ops in application order. -impl Display for FilterTree { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let joined = |items: &[FilterTree], sep: &str| -> String { - items - .iter() - .map(|t| t.to_string()) - .collect::>() - .join(sep) - }; - match self { - FilterTree::Node(inner) => write!(f, "{inner}"), - FilterTree::Edge(inner) => write!(f, "{inner}"), - FilterTree::ExplodedEdge(inner) => write!(f, "{inner}"), - FilterTree::View(ops) => { - let ops = ops.iter().map(|op| op.to_string()).collect::>(); - write!(f, "VIEW({})", ops.join(" . ")) - } - FilterTree::And(items) => write!(f, "({})", joined(items, " AND ")), - FilterTree::Or(items) => write!(f, "({})", joined(items, " OR ")), - FilterTree::Not(inner) => write!(f, "NOT({inner})"), - } - } -} - -impl FilterTree { - /// Whether any part of this expression tests edges. - /// - /// An edge test says nothing about which nodes belong in a node - /// collection, so a node-collection subscript refuses such an expression. - /// Lives here next to the enum so a new variant has to answer the question - /// rather than silently defaulting somewhere else. - pub fn tests_edges(&self) -> bool { - match self { - FilterTree::Edge(_) | FilterTree::ExplodedEdge(_) => true, - FilterTree::Node(_) | FilterTree::View(_) => false, - FilterTree::And(items) | FilterTree::Or(items) => { - items.iter().any(FilterTree::tests_edges) - } - FilterTree::Not(inner) => inner.tests_edges(), - } - } -} - #[derive(Clone)] pub struct MetadataExpr { view_expr: E, @@ -630,6 +536,24 @@ impl InternalViewWrapOps for Arc { } } +/// The window `at(t)` means: the single instant `t`. +pub(crate) fn at_bounds(t: EventTime) -> (EventTime, EventTime) { + (t, EventTime::from(t.t().saturating_add(1))) +} + +/// The window `after(t)` means: everything strictly after `t`. +pub(crate) fn after_bounds(t: EventTime) -> (EventTime, EventTime) { + ( + EventTime::start(t.t().saturating_add(1)), + EventTime::end(i64::MAX), + ) +} + +/// The window `before(t)` means: everything strictly before `t`. +pub(crate) fn before_bounds(t: EventTime) -> (EventTime, EventTime) { + (EventTime::start(i64::MIN), EventTime::end(t.t())) +} + pub trait ViewWrapOps: InternalViewWrapOps + Sized { #[inline] fn window(self, start: S, end: E) -> Self::Window { @@ -641,22 +565,20 @@ pub trait ViewWrapOps: InternalViewWrapOps + Sized { #[inline] fn at(self, time: T) -> Self::Window { - let t = time.into_time(); - self.window(t, t.t().saturating_add(1)) + let (start, end) = at_bounds(time.into_time()); + self.window(start, end) } #[inline] fn after(self, time: T) -> Self::Window { - let start = time.into_time().t().saturating_add(1); - self.window(EventTime::start(start), EventTime::end(i64::MAX)) + let (start, end) = after_bounds(time.into_time()); + self.window(start, end) } #[inline] fn before(self, time: T) -> Self::Window { - self.window( - EventTime::start(i64::MIN), - EventTime::end(time.into_time().t()), - ) + let (start, end) = before_bounds(time.into_time()); + self.window(start, end) } #[inline] @@ -877,9 +799,9 @@ pub trait EdgeViewFilterOps: ViewWrapOps { /// Comparison, string, set, and presence operators on any [`CreateOp`]. /// -/// `.any()` / `.all()` are terminal: they wrap `self` in `AnyExpr`/`AllExpr` and compare the -/// result to `Bool(true)`. For element-wise comparison before reduction, chain in order: -/// `.gt(10i64).any()` not `.any().gt(10i64)`. +/// `.any()` / `.all()` are qualifiers on a list-valued expression: the comparison that follows +/// is applied to each element and the results are reduced, so `.any().gt(10i64)` holds when any +/// element is greater than ten. /// /// ```rust,ignore /// NodeFilter.degree().gt(2usize) diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 3bbf5af18d..9be87b7d23 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -1,28 +1,6 @@ -//! Dyn-dispatch traits for property/aggregator/quantifier expression chains. -//! -//! `DynPropertyExpr` is the type-erased equivalent of the chained -//! `EntityExpr` → `EntityAggOps` → `EntityExprFilterOps` API. Each method either: -//! - terminates the chain by producing an `Arc` -//! (comparators / string ops / set ops / unary), or -//! - extends the chain by producing another `Arc` -//! (selectors / aggregators / quantifiers). -//! -//! Used by the Python `PyExpr` and `PyPropertyExpr` wrappers to dispatch -//! chain calls at runtime through typed expressions. -//! -//! ## Chain methods are currently panic stubs -//! -//! Implementing the chain methods (`dyn_first`, `dyn_sum`, …) properly -//! requires distinguishing node-side vs edge-side expressions at the type -//! level. The blanket impl below only bounds `E: EntityExpr`, which is not -//! enough — `BinaryCmpExpr<_, _, M>: CreateFilter` requires `L: NodeExpr` -//! (or `L: EdgeExpr`) depending on `M`. Splitting the blanket into Node / -//! Edge versions creates coherence overlap because primitive types (`Prop`, -//! `u32`, …) impl both `NodeExpr` and `EdgeExpr`. -//! -//! Chain methods are left as panicking default impls until a working -//! resolution is in place. The Python `.sum()`, `.first()`, `.any()`, etc. -//! calls will panic at runtime. +//! Type-erased forms of the expression traits, so a filter compiled from a +//! tree can hold any entity expression behind one `Arc` and still +//! report its static type and whether it can be missing. use crate::{ db::{ 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 8ec353a9fb..6b67dfd964 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 @@ -207,8 +207,8 @@ impl CreateOp for Type { &self, _graph: G, ) -> Result> + 'g>, GraphError> { - // Untyped nodes carry the storage's default type key, matching how the - // composite path builds its type mask over the node-type meta keys. + // Untyped nodes carry the storage's default type key, so a type test + // sees the same key the node-type mask is built over. Ok(Arc::new(WithPropType { inner: Type.map(|a| Some(a.map_or_else(|| Prop::str("_default"), |b| b.into_prop()))), pt: PropType::Str, 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 e90cb95719..d23521654a 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 @@ -6,8 +6,6 @@ use crate::{ NodeStateValue, TypedNodeState, }, graph::views::filter::model::{ - degree_filter::DegreeFilter, - filter::Filter, is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, @@ -19,10 +17,8 @@ use crate::{ }, }, errors::GraphError, - prelude::PropertyFilter, }; use raphtory_api::core::storage::timeindex::EventTime; -use std::{fmt, fmt::Display}; #[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] pub struct NodeFilter; @@ -49,41 +45,6 @@ impl NodeViewFilterOps for NodeFilter { } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CompositeNodeFilter { - Node(Filter), - Property(PropertyFilter), - Degree(DegreeFilter), - Windowed(Box>), - Latest(Box>), - SnapshotAt(Box>), - SnapshotLatest(Box>), - Layered(Box>), - IsActiveNode(IsActiveNode), - And(Box, Box), - Or(Box, Box), - Not(Box), -} - -impl Display for CompositeNodeFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CompositeNodeFilter::Property(filter) => write!(f, "{}", filter), - CompositeNodeFilter::Windowed(filter) => write!(f, "{}", filter), - CompositeNodeFilter::Degree(filter) => write!(f, "{}", filter), - CompositeNodeFilter::Layered(filter) => write!(f, "{}", filter), - CompositeNodeFilter::Latest(filter) => write!(f, "{}", filter), - 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::And(left, right) => write!(f, "({} AND {})", left, right), - CompositeNodeFilter::Or(left, right) => write!(f, "({} OR {})", left, right), - CompositeNodeFilter::Not(filter) => write!(f, "NOT({})", filter), - } - } -} - // ── expr-layer factory ── pub trait NodeFilterFactory: diff --git a/raphtory/src/db/graph/views/filter/model/tree.rs b/raphtory/src/db/graph/views/filter/model/tree.rs new file mode 100644 index 0000000000..a2b1df4863 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/tree.rs @@ -0,0 +1,1257 @@ +//! A filter as data. +//! +//! The typed expression API (`NodeFilter.property("score").gt(4)`) is what +//! rust callers write and what the engine compiles. Everything that has to +//! *carry* a filter — a python object, a GraphQL request, a stored permission +//! grant — needs the same filter as plain data instead. [`Expr`] and +//! [`FilterExpr`] are that data: one node per operation of the typed API, with +//! both sides of a comparison allowed to be expressions. +//! +//! A tree is built wherever the filter is written and compiled wherever it +//! runs, by [`FilterExpr::compile`], which replays the tree onto the typed +//! factories. Because the tree mirrors the API one to one, "what runs" and +//! "what is sent" cannot mean different things. + +use crate::{ + db::{ + api::{ + state::NodeOp, + view::internal::{DynGraphArc, GraphView}, + }, + graph::views::filter::{ + model::{ + and_filter::AndFilter, + dyn_factory::{DynEdgeFilterFactory, DynNodeFilterFactory}, + edge_filter::{EdgeEndpointNodeFilter, EdgeEndpointWrapper, EdgeFilter, Endpoint}, + exploded_edge_filter::ExplodedEdgeFilter, + graph_filter::GraphFilter, + layered_filter::layer_label, + node_expr::{DynCreateOp, DynTemporal}, + node_filter::NodeFilter, + not_filter::NotFilter, + or_filter::OrFilter, + DynCreateFilter, DynView, ViewWrapOps, + }, + CreateFilter, + }, + }, + errors::GraphError, + prelude::{EntityAggOps, EntityExprFilterOps, Layer}, +}; +use raphtory_api::core::{ + entities::properties::prop::Prop, + storage::timeindex::{AsTime, EventTime}, + Direction, +}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + sync::Arc, +}; + +/// Which kind of entity an expression reads from. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Entity { + Node, + Edge, + ExplodedEdge, +} + +/// One view restriction, in the order it was applied. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ViewOp { + Window { start: EventTime, end: EventTime }, + At(EventTime), + After(EventTime), + Before(EventTime), + Latest, + SnapshotAt(EventTime), + SnapshotLatest, + Layers(Vec), +} + +/// A built-in node field. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Field { + Id, + Name, + NodeType, +} + +/// A reduction over a list-valued expression. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Agg { + Sum, + Avg, + Min, + Max, + First, + Last, + Len, +} + +/// How the elements of a list-valued expression must satisfy a predicate. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Qual { + Any, + All, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CmpOp { + Eq, + Ne, + Lt, + Le, + Gt, + Ge, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum StrOp { + StartsWith, + EndsWith, + Contains, + NotContains, + FuzzySearch { + levenshtein_distance: usize, + prefix_match: bool, + }, +} + +/// A predicate on the state of an entity rather than on a value it holds. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Structural { + IsActive, + IsValid, + IsDeleted, + IsSelfLoop, +} + +/// Where a value is read: which entity, through which views, and for an edge +/// optionally through one of its endpoint nodes. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Scope { + pub entity: Entity, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub views: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub endpoint: Option, +} + +impl Scope { + pub fn new(entity: Entity) -> Self { + Scope { + entity, + views: Vec::new(), + endpoint: None, + } + } + + pub fn with_view(mut self, op: ViewOp) -> Self { + self.views.push(op); + self + } + + pub fn through(mut self, endpoint: Endpoint) -> Self { + self.endpoint = Some(endpoint); + self + } +} + +/// What a leaf reads from its scope. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Target { + Field(Field), + Degree(Direction), + Property(String), + Metadata(String), +} + +/// A value: what stands on either side of a comparison. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Expr { + Const(Prop), + Read { scope: Scope, target: Target }, + Temporal(Box), + Agg(Agg, Box), + Qual(Qual, Box), +} + +/// A yes/no: the filter itself. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FilterExpr { + Cmp { + op: CmpOp, + lhs: Expr, + rhs: Expr, + }, + Str { + op: StrOp, + lhs: Expr, + rhs: Expr, + }, + IsSome(Expr), + IsNone(Expr), + In { + expr: Expr, + values: Vec, + negated: bool, + }, + Structural { + scope: Scope, + pred: Structural, + }, + /// A graph-level view with no predicate: the result *is* the view. + View(Vec), + And(Vec), + Or(Vec), + Not(Box), + /// A filter over in-process state (a node-state column) that has no wire + /// form: it runs where it was built and cannot be sent anywhere. + Opaque(OpaqueFilter), +} + +/// An already compiled filter carried inside a tree. It exists for filters +/// built from data that lives only in this process, so it runs but does not +/// serialise: asking for its wire form is an error, not a guess. +#[derive(Clone)] +pub struct OpaqueFilter(pub Arc); + +pub const OPAQUE_FILTER_ERROR: &str = + "this filter has no server-side form; it was built from in-process state"; + +impl fmt::Debug for OpaqueFilter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("OpaqueFilter") + } +} + +impl PartialEq for OpaqueFilter { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Serialize for OpaqueFilter { + fn serialize(&self, _: S) -> Result { + Err(serde::ser::Error::custom(OPAQUE_FILTER_ERROR)) + } +} + +impl<'de> Deserialize<'de> for OpaqueFilter { + fn deserialize>(_: D) -> Result { + Err(serde::de::Error::custom(OPAQUE_FILTER_ERROR)) + } +} + +impl FilterExpr { + /// Whether a view appears anywhere in this filter. + pub fn has_view(&self) -> bool { + match self { + FilterExpr::View(_) => true, + FilterExpr::And(items) | FilterExpr::Or(items) => items.iter().any(Self::has_view), + FilterExpr::Not(inner) => inner.has_view(), + _ => false, + } + } + + /// Whether any part of this filter tests edges. An edge test says nothing + /// about which nodes belong in a node collection, so a node-collection + /// subscript refuses such a filter. + pub fn tests_edges(&self) -> bool { + let edge_scope = |scope: &Scope| scope.entity != Entity::Node; + fn expr_tests_edges(expr: &Expr, edge_scope: &dyn Fn(&Scope) -> bool) -> bool { + match expr { + Expr::Const(_) => false, + Expr::Read { scope, .. } => edge_scope(scope), + Expr::Temporal(e) | Expr::Agg(_, e) | Expr::Qual(_, e) => { + expr_tests_edges(e, edge_scope) + } + } + } + match self { + FilterExpr::Cmp { lhs, rhs, .. } | FilterExpr::Str { lhs, rhs, .. } => { + expr_tests_edges(lhs, &edge_scope) || expr_tests_edges(rhs, &edge_scope) + } + FilterExpr::IsSome(e) | FilterExpr::IsNone(e) | FilterExpr::In { expr: e, .. } => { + expr_tests_edges(e, &edge_scope) + } + FilterExpr::Structural { scope, .. } => edge_scope(scope), + FilterExpr::View(_) | FilterExpr::Opaque(_) => false, + FilterExpr::And(items) | FilterExpr::Or(items) => items.iter().any(Self::tests_edges), + FilterExpr::Not(inner) => inner.tests_edges(), + } + } +} + +// ── compiling ──────────────────────────────────────────────────────────────── + +/// A partly compiled value. Property reads keep the ability to switch to +/// their history until an aggregate or qualifier is applied. +enum Compiled { + Op(Arc), + Property(Arc), +} + +impl Compiled { + fn op(self) -> Arc { + match self { + Compiled::Op(op) => op, + Compiled::Property(prop) => prop, + } + } + + /// An endpoint read is a node read the edge evaluates on the node at that + /// end. The wrapping happens here, at the read, so that qualifiers and + /// aggregates applied above it see an edge expression and are compiled the + /// way an edge filter compiles them. + fn through(self, endpoint: Endpoint) -> Self { + match self { + Compiled::Op(op) => Compiled::Op(Arc::new(EdgeEndpointWrapper::new(op, endpoint))), + Compiled::Property(prop) => { + Compiled::Property(Arc::new(EdgeEndpointWrapper::new(prop, endpoint))) + } + } + } + + fn map_op(self, f: impl FnOnce(Arc) -> Arc) -> Self { + Compiled::Op(f(self.op())) + } +} + +fn invalid(msg: impl Into) -> GraphError { + GraphError::InvalidFilter(msg.into()) +} + +fn node_factory(views: &[ViewOp]) -> Arc { + let mut f: Arc = Arc::new(NodeFilter); + for op in views { + f = match op { + ViewOp::Window { start, end } => f.window(*start, *end), + ViewOp::At(t) => f.at(*t), + ViewOp::After(t) => f.after(*t), + ViewOp::Before(t) => f.before(*t), + ViewOp::Latest => Arc::new(f.latest()), + ViewOp::SnapshotAt(t) => Arc::new(f.snapshot_at(*t)), + ViewOp::SnapshotLatest => Arc::new(f.snapshot_latest()), + ViewOp::Layers(names) => Arc::new(f.layer(names.clone())), + }; + } + f +} + +fn edge_factory(entity: Entity, views: &[ViewOp]) -> Arc { + let mut f: Arc = match entity { + Entity::ExplodedEdge => Arc::new(ExplodedEdgeFilter), + Entity::Edge | Entity::Node => Arc::new(EdgeFilter), + }; + for op in views { + f = match op { + ViewOp::Window { start, end } => f.dyn_window(*start, *end), + ViewOp::At(t) => f.dyn_at(*t), + ViewOp::After(t) => f.dyn_after(*t), + ViewOp::Before(t) => f.dyn_before(*t), + ViewOp::Latest => f.dyn_latest(), + ViewOp::SnapshotAt(t) => f.dyn_snapshot_at(*t), + ViewOp::SnapshotLatest => f.dyn_snapshot_latest(), + ViewOp::Layers(names) => f.dyn_layer(names.clone()), + }; + } + f +} + +fn read_node(f: &Arc, target: &Target) -> Compiled { + match target { + Target::Field(Field::Id) => Compiled::Op(f.dyn_id()), + Target::Field(Field::Name) => Compiled::Op(f.dyn_name()), + Target::Field(Field::NodeType) => Compiled::Op(f.dyn_node_type()), + Target::Degree(Direction::BOTH) => Compiled::Op(f.dyn_degree()), + Target::Degree(Direction::IN) => Compiled::Op(f.dyn_in_degree()), + Target::Degree(Direction::OUT) => Compiled::Op(f.dyn_out_degree()), + Target::Property(name) => Compiled::Property(f.dyn_property(name.clone())), + Target::Metadata(name) => Compiled::Op(f.dyn_metadata(name.clone())), + } +} + +impl Expr { + /// The erased, compilable form of this value. + pub fn compile(&self) -> Result, GraphError> { + Ok(self.compile_inner()?.op()) + } + + fn compile_inner(&self) -> Result { + match self { + Expr::Const(value) => Ok(Compiled::Op(Arc::new(value.clone()))), + Expr::Read { scope, target } => match (scope.entity, scope.endpoint) { + (Entity::Node, Some(_)) => { + Err(invalid("a node expression has no src()/dst() endpoint")) + } + (Entity::Node, None) => Ok(read_node(&node_factory(&scope.views), target)), + // An endpoint read is a node read, scoped by the same views, + // that the edge evaluates on the node at that end. It is an + // edge's read: an edge update has no endpoint wrapper of its own. + (Entity::ExplodedEdge, Some(_)) => { + Err(invalid("an exploded edge has no src()/dst() endpoint")) + } + (Entity::Edge, Some(endpoint)) => { + Ok(read_node(&node_factory(&scope.views), target).through(endpoint)) + } + (entity, None) => { + let f = edge_factory(entity, &scope.views); + Ok(match target { + Target::Property(name) => Compiled::Property(f.dyn_property(name.clone())), + Target::Metadata(name) => Compiled::Op(f.dyn_metadata(name.clone())), + Target::Field(_) | Target::Degree(_) => { + return Err(invalid( + "an edge has no fields or degree; read them through src() or dst()", + )) + } + }) + } + }, + Expr::Temporal(inner) => match inner.compile_inner()? { + Compiled::Property(prop) => Ok(Compiled::Op(prop.temporal())), + Compiled::Op(_) => Err(invalid("temporal() applies to a property")), + }, + Expr::Agg(agg, inner) => Ok(inner.compile_inner()?.map_op(|op| match agg { + Agg::Sum => Arc::new(op.sum()), + Agg::Avg => Arc::new(op.avg()), + Agg::Min => Arc::new(op.min()), + Agg::Max => Arc::new(op.max()), + Agg::First => Arc::new(op.first()), + Agg::Last => Arc::new(op.last()), + Agg::Len => Arc::new(op.len()), + })), + Expr::Qual(qual, inner) => Ok(inner.compile_inner()?.map_op(|op| match qual { + Qual::Any => Arc::new(op.any()), + Qual::All => Arc::new(op.all()), + })), + } + } +} + +/// Compile a comparison whose right-hand side is either a constant or an +/// expression; both go through the same typed method, a constant being the +/// expression of its own value. +macro_rules! binary { + ($lhs:expr, $rhs:expr, $method:ident $(, $arg:expr)*) => {{ + let lhs = $lhs.compile()?; + let rhs: Arc = $rhs.compile()?; + let filter: Arc = Arc::new(lhs.$method(rhs $(, $arg)*)); + filter + }}; +} + +impl FilterExpr { + /// The erased, applicable form of this filter. + /// + /// A view (`View`) applies first: the graph is seen through it and the other + /// legs run inside it, reads included, the way `graph.window(..).filter(expr)` + /// does. A view therefore stands alone or is a leg of the top-level `and` + /// (nested `and`s count as top level); under `or` or `not` it has no meaning the + /// engine can give it and is refused. + pub fn compile(&self) -> Result, GraphError> { + let (views, predicates, saw_view) = self.split_top_views(); + if saw_view && views.is_empty() { + return Err(invalid("a view filter needs at least one view")); + } + if views.is_empty() { + return self.compile_nested(); + } + let inner: Arc = if predicates.is_empty() { + Arc::new(GraphFilter) + } else { + combine( + predicates.iter().map(|p| p.compile_nested()), + "and", + |left, right| Arc::new(AndFilter { left, right }), + )? + }; + Ok(Arc::new(Viewed { views, inner })) + } + + /// The view ops at the top of the filter, in order, and the predicates beside + /// them. `and` nests flatten; anything else is a predicate. The flag says whether + /// a `View` node was seen at all, so an empty one can be told from none. + fn split_top_views(&self) -> (Vec, Vec<&FilterExpr>, bool) { + fn walk<'a>( + filter: &'a FilterExpr, + views: &mut Vec, + predicates: &mut Vec<&'a FilterExpr>, + saw_view: &mut bool, + ) { + match filter { + FilterExpr::View(ops) => { + *saw_view = true; + views.extend(ops.iter().cloned()); + } + FilterExpr::And(items) => { + for item in items { + walk(item, views, predicates, saw_view); + } + } + other => predicates.push(other), + } + } + let (mut views, mut predicates, mut saw_view) = (Vec::new(), Vec::new(), false); + walk(self, &mut views, &mut predicates, &mut saw_view); + (views, predicates, saw_view) + } + + /// A filter below the top level: every node but a view. + fn compile_nested(&self) -> Result, GraphError> { + Ok(match self { + FilterExpr::Cmp { op, lhs, rhs } => match op { + CmpOp::Eq => binary!(lhs, rhs, eq), + CmpOp::Ne => binary!(lhs, rhs, ne), + CmpOp::Lt => binary!(lhs, rhs, lt), + CmpOp::Le => binary!(lhs, rhs, le), + CmpOp::Gt => binary!(lhs, rhs, gt), + CmpOp::Ge => binary!(lhs, rhs, ge), + }, + FilterExpr::Str { op, lhs, rhs } => match op { + StrOp::StartsWith => binary!(lhs, rhs, starts_with), + StrOp::EndsWith => binary!(lhs, rhs, ends_with), + StrOp::Contains => binary!(lhs, rhs, contains), + StrOp::NotContains => binary!(lhs, rhs, not_contains), + StrOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => binary!(lhs, rhs, fuzzy_search, *levenshtein_distance, *prefix_match), + }, + FilterExpr::IsSome(expr) => Arc::new(expr.compile()?.is_some()), + FilterExpr::IsNone(expr) => Arc::new(expr.compile()?.is_none()), + FilterExpr::In { + expr, + values, + negated, + } => { + let lhs = expr.compile()?; + if *negated { + Arc::new(lhs.is_not_in(values.clone())) + } else { + Arc::new(lhs.is_in(values.clone())) + } + } + FilterExpr::Structural { scope, pred } => { + // Through an endpoint, the predicate is a node predicate + // evaluated on the node at that end of the edge. + if let Some(endpoint) = scope.endpoint { + if scope.entity == Entity::Node { + return Err(invalid("a node has no src()/dst() endpoint")); + } + if scope.entity == Entity::ExplodedEdge { + return Err(invalid("an exploded edge has no src()/dst() endpoint")); + } + if *pred != Structural::IsActive { + return Err(invalid(format!("{pred} is an edge predicate"))); + } + return Ok(Arc::new(EdgeEndpointNodeFilter { + endpoint, + inner: node_factory(&scope.views).dyn_is_active(), + })); + } + match (scope.entity, pred) { + (Entity::Node, Structural::IsActive) => { + node_factory(&scope.views).dyn_is_active() + } + (Entity::Node, other) => { + return Err(invalid(format!("{other} is an edge predicate"))) + } + (entity, pred) => { + let f = edge_factory(entity, &scope.views); + match pred { + Structural::IsActive => f.dyn_is_active(), + Structural::IsValid => f.dyn_is_valid(), + Structural::IsDeleted => f.dyn_is_deleted(), + Structural::IsSelfLoop => f.dyn_is_self_loop(), + } + } + } + } + FilterExpr::View(_) => { + return Err(invalid( + "a view applies to the whole filter: use it alone or as a leg of the \ + top-level `and`, not under `or` or `not`", + )) + } + FilterExpr::And(items) => combine( + items.iter().map(Self::compile_nested), + "and", + |left, right| Arc::new(AndFilter { left, right }), + )?, + FilterExpr::Or(items) => combine( + items.iter().map(Self::compile_nested), + "or", + |left, right| Arc::new(OrFilter { left, right }), + )?, + FilterExpr::Not(inner) => Arc::new(NotFilter(inner.compile_nested()?)), + FilterExpr::Opaque(filter) => filter.0.clone(), + }) + } +} + +/// The graph-level view a chain of view ops describes, applied in order. +fn compile_view(views: &[ViewOp]) -> DynView { + let mut v: DynView = Arc::new(GraphFilter); + for op in views { + v = match op { + ViewOp::Window { start, end } => v.window(*start, *end), + ViewOp::At(t) => v.at(*t), + ViewOp::After(t) => v.after(*t), + ViewOp::Before(t) => v.before(*t), + ViewOp::Latest => Arc::new(v.latest()), + ViewOp::SnapshotAt(t) => Arc::new(v.snapshot_at(*t)), + ViewOp::SnapshotLatest => Arc::new(v.snapshot_latest()), + ViewOp::Layers(names) => Arc::new(v.layer(Layer::from(names.clone()))), + }; + } + v +} + +/// A filter applied inside a view: the graph is seen through `views` first and +/// `inner` runs on that graph, reads included, so `and: [view, pred]` is +/// `graph.view(..).filter(pred)`. +#[derive(Clone)] +struct Viewed { + views: Vec, + inner: Arc, +} + +impl Viewed { + fn view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + compile_view(&self.views).dyn_filter_graph_view(Arc::new(graph)) + } +} + +impl CreateFilter for Viewed { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = DynGraphArc<'graph> + where + Self: 'graph; + + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = Arc + 'graph> + where + Self: 'graph; + + type FilteredGraph<'graph, G> + = DynGraphArc<'graph> + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + let viewed = self.view(graph)?; + self.inner.create_dyn_filter(viewed, Arc::new(filtered)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + let viewed = self.view(graph)?; + self.inner + .create_dyn_node_filter(viewed, Arc::new(filtered)) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + let viewed = self.view(graph)?; + self.inner.dyn_filter_graph_view(viewed) + } +} + +/// Fold compiled operands pairwise, left to right. An empty list has no +/// meaning either way (`and` of nothing is not "everything", `or` of nothing +/// is not "nothing" the caller asked for), so it is refused. +fn combine( + mut compiled: impl Iterator, GraphError>>, + name: &str, + join: impl Fn(Arc, Arc) -> Arc, +) -> Result, GraphError> { + let first = compiled + .next() + .ok_or_else(|| invalid(format!("`{name}` needs at least one operand")))??; + compiled.try_fold(first, |acc, next| Ok(join(acc, next?))) +} + +/// A tree is a filter in its own right: applying it compiles it first. +impl CreateFilter for FilterExpr { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = DynGraphArc<'graph> + where + Self: 'graph; + + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc + 'graph>; + + type FilteredGraph<'graph, G> + = DynGraphArc<'graph> + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + self.compile()?.create_filter(graph, filtered) + } + + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + self.compile()?.create_node_filter(graph, filtered) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + self.compile()?.filter_graph_view(graph) + } +} + +// ── printing ───────────────────────────────────────────────────────────────── + +impl Display for ViewOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ViewOp::Window { start, end } => write!(f, "WINDOW[{}..{}]", start.t(), end.t()), + ViewOp::At(t) => write!(f, "AT[{}]", t.t()), + ViewOp::After(t) => write!(f, "AFTER[{}]", t.t()), + ViewOp::Before(t) => write!(f, "BEFORE[{}]", t.t()), + ViewOp::Latest => write!(f, "LATEST"), + ViewOp::SnapshotAt(t) => write!(f, "SNAPSHOT_AT[{}]", t.t()), + ViewOp::SnapshotLatest => write!(f, "SNAPSHOT_LATEST"), + ViewOp::Layers(names) => { + write!(f, "LAYER[{}]", layer_label(&Layer::from(names.clone()))) + } + } + } +} + +impl Display for Target { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Target::Field(Field::Id) => write!(f, "id"), + Target::Field(Field::Name) => write!(f, "name"), + Target::Field(Field::NodeType) => write!(f, "node_type"), + Target::Degree(Direction::BOTH) => write!(f, "degree"), + Target::Degree(Direction::IN) => write!(f, "in_degree"), + Target::Degree(Direction::OUT) => write!(f, "out_degree"), + Target::Property(name) => write!(f, "{name}"), + Target::Metadata(name) => write!(f, "metadata({name})"), + } + } +} + +/// Wraps `inner` in the scope's views (innermost first) and endpoint. +fn scoped(f: &mut fmt::Formatter<'_>, scope: &Scope, inner: &str) -> fmt::Result { + let mut text = inner.to_string(); + for view in &scope.views { + text = format!("{view}({text})"); + } + match scope.endpoint { + Some(Endpoint::Src) => write!(f, "SRC({text})"), + Some(Endpoint::Dst) => write!(f, "DST({text})"), + None => write!(f, "{text}"), + } +} + +impl Display for Expr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Expr::Const(value) => write!(f, "{value}"), + Expr::Read { scope, target } => scoped(f, scope, &target.to_string()), + Expr::Temporal(inner) => write!(f, "temporal({inner})"), + Expr::Agg(agg, inner) => write!(f, "{agg}({inner})"), + Expr::Qual(qual, inner) => write!(f, "{qual}({inner})"), + } + } +} + +impl Display for Agg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Agg::Sum => "sum", + Agg::Avg => "avg", + Agg::Min => "min", + Agg::Max => "max", + Agg::First => "first", + Agg::Last => "last", + Agg::Len => "len", + }) + } +} + +impl Display for Qual { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Qual::Any => "any", + Qual::All => "all", + }) + } +} + +impl Display for CmpOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + CmpOp::Eq => "==", + CmpOp::Ne => "!=", + CmpOp::Lt => "<", + CmpOp::Le => "<=", + CmpOp::Gt => ">", + CmpOp::Ge => ">=", + }) + } +} + +impl Display for StrOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StrOp::StartsWith => write!(f, "STARTS_WITH"), + StrOp::EndsWith => write!(f, "ENDS_WITH"), + StrOp::Contains => write!(f, "CONTAINS"), + StrOp::NotContains => write!(f, "NOT_CONTAINS"), + StrOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => write!(f, "FUZZY_SEARCH[{levenshtein_distance}, {prefix_match}]"), + } + } +} + +impl Display for Structural { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Structural::IsActive => "IS_ACTIVE", + Structural::IsValid => "IS_VALID", + Structural::IsDeleted => "IS_DELETED", + Structural::IsSelfLoop => "IS_SELF_LOOP", + }) + } +} + +impl Display for FilterExpr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let list = |items: &[FilterExpr], sep: &str| { + items + .iter() + .map(|i| i.to_string()) + .collect::>() + .join(sep) + }; + match self { + FilterExpr::Cmp { op, lhs, rhs } => write!(f, "{lhs} {op} {rhs}"), + FilterExpr::Str { op, lhs, rhs } => write!(f, "{lhs} {op} {rhs}"), + FilterExpr::IsSome(expr) => write!(f, "{expr} IS_SOME"), + FilterExpr::IsNone(expr) => write!(f, "{expr} IS_NONE"), + FilterExpr::In { + expr, + values, + negated, + } => { + let values = values + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(", "); + let op = if *negated { "NOT_IN" } else { "IN" }; + write!(f, "{expr} {op} [{values}]") + } + FilterExpr::Structural { scope, pred } => scoped(f, scope, &pred.to_string()), + FilterExpr::View(views) => { + let views = views.iter().map(|v| v.to_string()).collect::>(); + write!(f, "VIEW({})", views.join(" . ")) + } + FilterExpr::And(items) => write!(f, "({})", list(items, " AND ")), + FilterExpr::Or(items) => write!(f, "({})", list(items, " OR ")), + FilterExpr::Not(inner) => write!(f, "NOT({inner})"), + FilterExpr::Opaque(_) => write!(f, ""), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + db::{ + api::view::Filter, + graph::views::filter::model::{ + edge_filter::EdgeFilter, node_filter::NodeFilter, PropertyExprFactory, ViewWrapOps, + }, + }, + prelude::{AdditionOps, EdgeViewOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, + }; + use raphtory_api::core::entities::properties::prop::IntoProp; + + /// alice.score 3@0 7@2 9@6 · bob.score 5@1 2@7 · carol none · dave.score 1@2 1@3 + /// alice→bob [knows] @1 @4 · bob→carol [works] @2 · carol→dave [knows] @6 + fn graph() -> Graph { + let g = Graph::new(); + for (t, name, score) in [ + (0, "alice", 3.0), + (2, "alice", 7.0), + (6, "alice", 9.0), + (1, "bob", 5.0), + (7, "bob", 2.0), + (2, "dave", 1.0), + (3, "dave", 1.0), + ] { + g.add_node(t, name, [("score", score.into_prop())], None, None) + .unwrap(); + } + g.add_node(0, "carol", NO_PROPS, None, None).unwrap(); + g.add_edge(1, "alice", "bob", NO_PROPS, Some("knows")) + .unwrap(); + g.add_edge(4, "alice", "bob", NO_PROPS, Some("knows")) + .unwrap(); + g.add_edge(2, "bob", "carol", NO_PROPS, Some("works")) + .unwrap(); + g.add_edge(6, "carol", "dave", NO_PROPS, Some("knows")) + .unwrap(); + g + } + + fn nodes(g: &Graph, filter: Arc) -> Vec { + let mut names: Vec = g + .filter(filter) + .unwrap() + .nodes() + .iter() + .map(|n| n.name()) + .collect(); + names.sort(); + names + } + + fn edges(g: &Graph, filter: Arc) -> Vec { + let mut ids: Vec = g + .filter(filter) + .unwrap() + .edges() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + ids.sort(); + ids + } + + fn node(target: Target) -> Expr { + Expr::Read { + scope: Scope::new(Entity::Node), + target, + } + } + + fn score() -> Expr { + node(Target::Property("score".into())) + } + + fn cmp(op: CmpOp, lhs: Expr, rhs: Expr) -> FilterExpr { + FilterExpr::Cmp { op, lhs, rhs } + } + + fn c(v: impl Into) -> Expr { + Expr::Const(v.into()) + } + + #[test] + fn a_constant_comparison_matches_the_typed_api() { + let g = graph(); + let tree = cmp(CmpOp::Gt, score(), c(4.0)); + assert_eq!(nodes(&g, tree.compile().unwrap()), vec!["alice"]); + let typed: Arc = Arc::new(NodeFilter.property("score").gt(4.0)); + assert_eq!(nodes(&g, typed), vec!["alice"]); + assert_eq!(tree.to_string(), "score > 4"); + } + + #[test] + fn views_scope_the_read_not_the_result() { + let g = graph(); + let tree = cmp( + CmpOp::Gt, + Expr::Read { + scope: Scope::new(Entity::Node).with_view(ViewOp::Window { + start: EventTime::from(0), + end: EventTime::from(5), + }), + target: Target::Property("score".into()), + }, + c(4.0), + ); + // inside [0,5): alice's latest score is 7, bob's is 5 + assert_eq!(nodes(&g, tree.compile().unwrap()), vec!["alice", "bob"]); + let typed: Arc = + Arc::new(NodeFilter.window(0, 5).property("score").gt(4.0)); + assert_eq!(nodes(&g, typed), vec!["alice", "bob"]); + assert_eq!(tree.to_string(), "WINDOW[0..5](score) > 4"); + } + + #[test] + fn both_sides_may_be_expressions() { + let g = graph(); + let tree = cmp( + CmpOp::Gt, + node(Target::Degree(Direction::BOTH)), + node(Target::Degree(Direction::IN)), + ); + assert_eq!( + nodes(&g, tree.compile().unwrap()), + vec!["alice", "bob", "carol"] + ); + assert_eq!(tree.to_string(), "degree > in_degree"); + } + + #[test] + fn temporal_aggregates_and_qualifiers() { + let g = graph(); + let sum = cmp( + CmpOp::Gt, + Expr::Agg(Agg::Sum, Box::new(Expr::Temporal(Box::new(score())))), + c(10.0), + ); + assert_eq!(nodes(&g, sum.compile().unwrap()), vec!["alice"]); + assert_eq!(sum.to_string(), "sum(temporal(score)) > 10"); + + let any = cmp( + CmpOp::Gt, + Expr::Qual(Qual::Any, Box::new(Expr::Temporal(Box::new(score())))), + c(4.0), + ); + assert_eq!(nodes(&g, any.compile().unwrap()), vec!["alice", "bob"]); + + let all = cmp( + CmpOp::Gt, + Expr::Qual(Qual::All, Box::new(Expr::Temporal(Box::new(score())))), + c(4.0), + ); + assert!(nodes(&g, all.compile().unwrap()).is_empty()); + + let len = cmp( + CmpOp::Eq, + Expr::Agg(Agg::Len, Box::new(Expr::Temporal(Box::new(score())))), + c(2u64), + ); + assert_eq!(nodes(&g, len.compile().unwrap()), vec!["bob", "dave"]); + } + + #[test] + fn temporal_needs_a_property() { + let tree = Expr::Temporal(Box::new(node(Target::Degree(Direction::BOTH)))); + assert!(tree.compile().is_err()); + } + + #[test] + fn combinators_and_presence() { + let g = graph(); + let tree = FilterExpr::And(vec![ + FilterExpr::IsSome(score()), + FilterExpr::Not(Box::new(FilterExpr::Str { + op: StrOp::StartsWith, + lhs: node(Target::Field(Field::Name)), + rhs: c("a"), + })), + ]); + assert_eq!(nodes(&g, tree.compile().unwrap()), vec!["bob", "dave"]); + assert_eq!( + tree.to_string(), + "(score IS_SOME AND NOT(name STARTS_WITH a))" + ); + + let none = FilterExpr::IsNone(score()); + assert_eq!(nodes(&g, none.compile().unwrap()), vec!["carol"]); + + let set = FilterExpr::In { + expr: node(Target::Field(Field::Name)), + values: vec!["alice".into(), "dave".into()], + negated: false, + }; + assert_eq!(nodes(&g, set.compile().unwrap()), vec!["alice", "dave"]); + assert!(FilterExpr::And(vec![]).compile().is_err()); + assert!(FilterExpr::Or(vec![]).compile().is_err()); + } + + #[test] + fn edge_endpoints_and_structure() { + let g = graph(); + let src_name = FilterExpr::Cmp { + op: CmpOp::Eq, + lhs: Expr::Read { + scope: Scope::new(Entity::Edge).through(Endpoint::Src), + target: Target::Field(Field::Name), + }, + rhs: c("alice"), + }; + assert_eq!(edges(&g, src_name.compile().unwrap()), vec!["alice->bob"]); + let typed: Arc = Arc::new(EdgeFilter::src().name().eq("alice")); + assert_eq!(edges(&g, typed), vec!["alice->bob"]); + assert_eq!(src_name.to_string(), "SRC(name) == alice"); + + let endpoint_score = FilterExpr::Cmp { + op: CmpOp::Gt, + lhs: Expr::Read { + scope: Scope::new(Entity::Edge).through(Endpoint::Dst), + target: Target::Property("score".into()), + }, + rhs: Expr::Read { + scope: Scope::new(Entity::Edge).through(Endpoint::Src), + target: Target::Property("score".into()), + }, + }; + // dst score > src score: alice(9)->bob(2) no; bob(2)->carol(none) no; carol(none)->dave no + assert!(edges(&g, endpoint_score.compile().unwrap()).is_empty()); + + let layered = FilterExpr::Structural { + scope: Scope::new(Entity::Edge).with_view(ViewOp::Layers(vec!["works".into()])), + pred: Structural::IsActive, + }; + assert_eq!(edges(&g, layered.compile().unwrap()), vec!["bob->carol"]); + assert_eq!(layered.to_string(), "LAYER[works](IS_ACTIVE)"); + } + + #[test] + fn a_view_alone_is_the_result() { + let g = graph(); + let tree = FilterExpr::View(vec![ + ViewOp::Window { + start: EventTime::from(0), + end: EventTime::from(5), + }, + ViewOp::Latest, + ]); + assert_eq!(tree.to_string(), "VIEW(WINDOW[0..5] . LATEST)"); + assert_eq!(edges(&g, tree.compile().unwrap()), vec!["alice->bob"]); + assert!(FilterExpr::View(vec![]).compile().is_err()); + } + + #[test] + fn trees_round_trip_through_json() { + let tree = FilterExpr::And(vec![ + cmp( + CmpOp::Gt, + Expr::Agg(Agg::Sum, Box::new(Expr::Temporal(Box::new(score())))), + c(10.0), + ), + FilterExpr::Structural { + scope: Scope::new(Entity::Edge).with_view(ViewOp::Layers(vec!["works".into()])), + pred: Structural::IsActive, + }, + FilterExpr::View(vec![ViewOp::Latest]), + ]); + let json = serde_json::to_string(&tree).unwrap(); + let back: FilterExpr = serde_json::from_str(&json).unwrap(); + assert_eq!(back, tree); + } + + #[test] + fn an_opaque_filter_refuses_to_serialise() { + let compiled = FilterExpr::View(vec![ViewOp::Latest]).compile().unwrap(); + let opaque = FilterExpr::Opaque(OpaqueFilter(compiled)); + let err = serde_json::to_string(&opaque).unwrap_err(); + assert!(err.to_string().contains(OPAQUE_FILTER_ERROR), "{err}"); + assert!(!opaque.tests_edges()); + } + + #[test] + fn an_exploded_edge_has_no_endpoint() { + let scope = Scope::new(Entity::ExplodedEdge).through(Endpoint::Src); + let read = FilterExpr::Cmp { + op: CmpOp::Eq, + lhs: Expr::Read { + scope: scope.clone(), + target: Target::Field(Field::Name), + }, + rhs: Expr::Const(Prop::str("alice")), + }; + assert!(read.compile().is_err()); + let active = FilterExpr::Structural { + scope, + pred: Structural::IsActive, + }; + assert!(active.compile().is_err()); + } + + #[test] + fn a_view_leg_restricts_the_whole_filter() { + // alice's only update (t=6) and bob's (t=1) are inside [0, 7); dave's (t=8) is not. + let g = Graph::new(); + g.add_node(6, "alice", [("score", Prop::F64(9.0))], None, None) + .unwrap(); + g.add_node(1, "bob", [("score", Prop::F64(5.0))], None, None) + .unwrap(); + g.add_node(8, "dave", [("score", Prop::F64(10.0))], None, None) + .unwrap(); + g.add_node(0, "carol", NO_PROPS, None, None).unwrap(); + let score_gt_4 = FilterExpr::Cmp { + op: CmpOp::Gt, + lhs: Expr::Read { + scope: Scope::new(Entity::Node), + target: Target::Property("score".into()), + }, + rhs: Expr::Const(Prop::F64(4.0)), + }; + let window = FilterExpr::View(vec![ViewOp::Window { + start: EventTime::start(0), + end: EventTime::end(7), + }]); + // The view applies first and the predicate runs inside it. + let tree = FilterExpr::And(vec![window.clone(), score_gt_4.clone()]); + assert_eq!(nodes(&g, tree.compile().unwrap()), ["alice", "bob"]); + // A predicate every node passes still leaves the view's members only. + let named = FilterExpr::IsSome(Expr::Read { + scope: Scope::new(Entity::Node), + target: Target::Field(Field::Name), + }); + let all_in_window = FilterExpr::And(vec![window.clone(), named]); + assert_eq!( + nodes(&g, all_in_window.compile().unwrap()), + ["alice", "bob", "carol"] + ); + // Nested `and`s flatten, so the view still reaches the top. + let nested = FilterExpr::And(vec![ + FilterExpr::And(vec![window.clone()]), + score_gt_4.clone(), + ]); + assert_eq!(nodes(&g, nested.compile().unwrap()), ["alice", "bob"]); + // Under `or` or `not` a view has no meaning the engine can give it. + assert!(FilterExpr::Or(vec![window.clone(), score_gt_4.clone()]) + .compile() + .is_err()); + assert!(FilterExpr::Not(Box::new(window.clone())).compile().is_err()); + assert!(FilterExpr::Or(vec![ + FilterExpr::And(vec![window, score_gt_4.clone()]), + score_gt_4 + ]) + .compile() + .is_err()); + } +} diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index f7b6eec246..4ae60b387f 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -1,31 +1,18 @@ use crate::{ db::graph::views::filter::model::{ - edge_filter::{CompositeEdgeFilter, EdgeEndpointWrapper, EdgeFilter, Endpoint}, - filter::{NODE_ID_FIELD, NODE_NAME_FIELD, NODE_TYPE_FIELD}, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - node_expr::{DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr}, - node_filter::NodeFilter, - property_filter::PropertyRef, - windowed_filter::Windowed, - CombinedFilter, CreateView, DynCreateFilter, DynCreateView, DynPropertyExprFactory, - EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, FilterTree, InternalViewWrapOps, - PropertyExprFactory, ViewWrapOps, + edge_filter::Endpoint, + tree::{Entity, Expr, Field, FilterExpr, Scope, Structural, Target, ViewOp}, }, python::{ filter::{ filter_expr::PyFilterExpr, - node_expr::{DynNodeFilterFactory, PyExpr, PyPropertyExpr}, - wire::{wrap_edge_views, WireEntity, WireLhs, WireTarget, WireView}, + node_expr::{PyExpr, PyPropertyExpr}, }, types::iterable::FromIterable, }, }; use pyo3::{pyclass, pymethods}; -use raphtory_api::core::storage::timeindex::{AsTime, EventTime}; -use std::sync::Arc; +use raphtory_api::core::storage::timeindex::EventTime; /// Entry point for filtering an edge endpoint (source or destination). /// @@ -38,37 +25,13 @@ use std::sync::Arc; /// Edge.dst().name().starts_with("user:") /// Edge.src().property("country") == "UK" #[pyclass(frozen, name = "EdgeEndpoint", module = "raphtory.filter")] -pub struct PyEdgeEndpoint( - pub(crate) Arc, - pub(crate) Endpoint, - pub(crate) Vec, -); - -/// The node an endpoint read evaluates on, scoped by the same views as the -/// edge chain that reached it: `Edge.window(0, 5).src().property("p")` reads -/// the source node's property inside the window. -fn node_scope(views: &[WireView]) -> Arc { - let mut node: Arc = Arc::new(NodeFilter); - for view in views { - node = match view { - WireView::Window(start, end) => node.window(*start, *end), - WireView::Latest => Arc::new(node.latest()), - WireView::SnapshotAt(time) => Arc::new(node.snapshot_at(*time)), - WireView::SnapshotLatest => Arc::new(node.snapshot_latest()), - WireView::Layers(names) => Arc::new(node.layer(names.clone())), - }; - } - node -} +pub struct PyEdgeEndpoint(pub(crate) Scope); impl PyEdgeEndpoint { - fn lhs(&self, target: WireTarget) -> WireLhs { - WireLhs { - entity: WireEntity::Edge, - endpoint: Some(self.1), + fn read(&self, target: Target) -> Expr { + Expr::Read { + scope: self.0.clone(), target, - ops: Vec::new(), - views: self.2.clone(), } } } @@ -80,10 +43,7 @@ impl PyEdgeEndpoint { /// Returns: /// filter.Expr: fn id(&self) -> PyExpr { - PyExpr::new( - Arc::new(EdgeEndpointWrapper::new(self.0.dyn_id(), self.1)), - Some(self.lhs(WireTarget::Field(NODE_ID_FIELD))), - ) + PyExpr(self.read(Target::Field(Field::Id))) } /// Selects the endpoint node name field for filtering. @@ -91,10 +51,7 @@ impl PyEdgeEndpoint { /// Returns: /// filter.Expr: fn name(&self) -> PyExpr { - PyExpr::new( - Arc::new(EdgeEndpointWrapper::new(self.0.dyn_name(), self.1)), - Some(self.lhs(WireTarget::Field(NODE_NAME_FIELD))), - ) + PyExpr(self.read(Target::Field(Field::Name))) } /// Selects the endpoint node type field for filtering. @@ -102,10 +59,7 @@ impl PyEdgeEndpoint { /// Returns: /// filter.Expr: fn node_type(&self) -> PyExpr { - PyExpr::new( - Arc::new(EdgeEndpointWrapper::new(self.0.dyn_node_type(), self.1)), - Some(self.lhs(WireTarget::Field(NODE_TYPE_FIELD))), - ) + PyExpr(self.read(Target::Field(Field::NodeType))) } /// Filters an endpoint node property by name. @@ -116,11 +70,7 @@ impl PyEdgeEndpoint { /// Returns: /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { - let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); - PyPropertyExpr::new( - Arc::new(EdgeEndpointWrapper::new(self.0.dyn_property(name), self.1)), - Some(lhs), - ) + PyPropertyExpr(self.read(Target::Property(name))) } /// Filters an endpoint node metadata field by name. @@ -131,148 +81,23 @@ impl PyEdgeEndpoint { /// Returns: /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { - let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); - PyExpr::new( - Arc::new(EdgeEndpointWrapper::new(self.0.dyn_metadata(name), self.1)), - Some(lhs), - ) - } -} - -pub trait DynEdgeFilterFactory: DynEntityExpr + DynCreateView + Send + Sync + 'static { - fn dyn_property(&self, name: String) -> Arc; - fn dyn_metadata(&self, name: String) -> Arc; - - fn dyn_is_active(&self) -> Arc; - fn dyn_is_valid(&self) -> Arc; - fn dyn_is_deleted(&self) -> Arc; - fn dyn_is_self_loop(&self) -> Arc; - - fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc; - fn dyn_at(&self, time: EventTime) -> Arc; - fn dyn_after(&self, time: EventTime) -> Arc; - fn dyn_before(&self, time: EventTime) -> Arc; - fn dyn_latest(&self) -> Arc; - fn dyn_snapshot_at(&self, time: EventTime) -> Arc; - fn dyn_snapshot_latest(&self) -> Arc; - fn dyn_layer(&self, layers: Vec) -> Arc; -} - -impl EdgeFilterFactory for Arc {} - -impl EdgeViewFilterOps for Arc { - type Output = Arc; - - fn is_active(&self) -> Self::Output { - self.as_ref().dyn_is_active() - } - - fn is_valid(&self) -> Self::Output { - self.as_ref().dyn_is_valid() - } - - fn is_deleted(&self) -> Self::Output { - self.as_ref().dyn_is_deleted() - } - - fn is_self_loop(&self) -> Self::Output { - self.as_ref().dyn_is_self_loop() - } -} - -impl InternalViewWrapOps for Arc { - type Window = Arc; - - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - self.as_ref().dyn_window(start, end) - } -} - -impl DynEdgeFilterFactory for T -where - T: EdgeFilterFactory + EdgeViewFilterOps + ViewWrapOps + CreateView + EntityExpr + Clone, - T: Send + Sync + 'static, - ::Marker: Into, -{ - fn dyn_property(&self, name: String) -> Arc { - Arc::new(PropertyExprFactory::property(self, name)) - } - fn dyn_metadata(&self, name: String) -> Arc { - Arc::new(PropertyExprFactory::metadata(self, name)) - } - - fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) - } - fn dyn_is_valid(&self) -> Arc { - Arc::new(self.is_valid()) - } - fn dyn_is_deleted(&self) -> Arc { - Arc::new(self.is_deleted()) - } - fn dyn_is_self_loop(&self) -> Arc { - Arc::new(self.is_self_loop()) - } - - // The window wrapper is constructed over the erased factory directly: - // routing through ViewWrapOps::window would dispatch straight back into - // this method through the erased build_window. - fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - let (old_start, old_end) = self.bounds(); - let end = end.min(old_end); - let start = start.max(old_start).min(end); - Arc::new(Windowed::new(start, end, dyn_self)) - } - fn dyn_at(&self, time: EventTime) -> Arc { - self.dyn_window(time, EventTime::from(time.t().saturating_add(1))) - } - fn dyn_after(&self, time: EventTime) -> Arc { - let start = time.t().saturating_add(1); - self.dyn_window(EventTime::start(start), EventTime::end(i64::MAX)) - } - fn dyn_before(&self, time: EventTime) -> Arc { - self.dyn_window(EventTime::start(i64::MIN), EventTime::end(time.t())) - } - // Same erasure trick as dyn_window: wrapping the erased factory keeps the - // set of vtable-instantiated types finite; wrapping `self` directly would - // materialise a vtable for every wrapper combination. - fn dyn_latest(&self) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - Arc::new(dyn_self.latest()) - } - fn dyn_snapshot_at(&self, time: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - Arc::new(dyn_self.snapshot_at(time)) - } - fn dyn_snapshot_latest(&self) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - Arc::new(dyn_self.snapshot_latest()) - } - fn dyn_layer(&self, layers: Vec) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - Arc::new(dyn_self.layer(layers)) + PyExpr(self.read(Target::Metadata(name))) } } impl PyEdgeFilter { pub(crate) fn root() -> Self { - PyEdgeFilter(Arc::new(EdgeFilter), Vec::new()) + PyEdgeFilter(Scope::new(Entity::Edge)) } - fn wrap(&self, factory: Arc, view: WireView) -> Self { - let mut views = self.1.clone(); - views.push(view); - PyEdgeFilter(factory, views) + fn with_view(&self, view: ViewOp) -> Self { + PyEdgeFilter(self.0.clone().with_view(view)) } - fn lhs(&self, target: WireTarget) -> WireLhs { - WireLhs { - entity: WireEntity::Edge, - endpoint: None, + fn read(&self, target: Target) -> Expr { + Expr::Read { + scope: self.0.clone(), target, - ops: Vec::new(), - views: self.1.clone(), } } } @@ -283,7 +108,7 @@ impl PyEdgeFilter { /// `Edge.layer(...)`, ...); its endpoint, property and structural predicates /// evaluate within that view, and its own view methods narrow it further. #[pyclass(frozen, name = "EdgeFilter", module = "raphtory.filter")] -pub struct PyEdgeFilter(Arc, Vec); +pub struct PyEdgeFilter(pub(crate) Scope); #[pymethods] impl PyEdgeFilter { @@ -297,7 +122,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeEndpoint: fn src(&self) -> PyEdgeEndpoint { - PyEdgeEndpoint(node_scope(&self.1), Endpoint::Src, self.1.clone()) + PyEdgeEndpoint(self.0.clone().through(Endpoint::Src)) } /// Selects the edge **destination endpoint** for filtering. @@ -305,7 +130,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeEndpoint: fn dst(&self) -> PyEdgeEndpoint { - PyEdgeEndpoint(node_scope(&self.1), Endpoint::Dst, self.1.clone()) + PyEdgeEndpoint(self.0.clone().through(Endpoint::Dst)) } /// Filters an edge property by name. @@ -316,8 +141,7 @@ impl PyEdgeFilter { /// Returns: /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { - let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); - PyPropertyExpr::new(self.0.as_ref().dyn_property(name), Some(lhs)) + PyPropertyExpr(self.read(Target::Property(name))) } /// Filters an edge metadata field by name. @@ -328,8 +152,7 @@ impl PyEdgeFilter { /// Returns: /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { - let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); - PyExpr::new(self.0.dyn_metadata(name), Some(lhs)) + PyExpr(self.read(Target::Metadata(name))) } /// Restricts edge evaluation to the given time window. @@ -341,7 +164,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeFilter: fn window(&self, start: EventTime, end: EventTime) -> PyEdgeFilter { - self.wrap(self.0.dyn_window(start, end), WireView::Window(start, end)) + self.with_view(ViewOp::Window { start, end }) } /// Restricts edge evaluation to a single point in time. @@ -352,10 +175,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeFilter: fn at(&self, time: EventTime) -> PyEdgeFilter { - self.wrap( - self.0.dyn_at(time), - WireView::Window(time, EventTime::end(time.t().saturating_add(1))), - ) + self.with_view(ViewOp::At(time)) } /// Restricts edge evaluation to times strictly after the given time. @@ -366,13 +186,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeFilter: fn after(&self, time: EventTime) -> PyEdgeFilter { - self.wrap( - self.0.dyn_after(time), - WireView::Window( - EventTime::start(time.t().saturating_add(1)), - EventTime::end(i64::MAX), - ), - ) + self.with_view(ViewOp::After(time)) } /// Restricts edge evaluation to times strictly before the given time. @@ -383,10 +197,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeFilter: fn before(&self, time: EventTime) -> PyEdgeFilter { - self.wrap( - self.0.dyn_before(time), - WireView::Window(EventTime::start(i64::MIN), EventTime::end(time.t())), - ) + self.with_view(ViewOp::Before(time)) } /// Evaluates edge predicates against the latest available edge state. @@ -394,7 +205,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeFilter: fn latest(&self) -> PyEdgeFilter { - self.wrap(self.0.dyn_latest(), WireView::Latest) + self.with_view(ViewOp::Latest) } /// Evaluates edge predicates against a snapshot of the graph at a given time. @@ -405,7 +216,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeFilter: fn snapshot_at(&self, time: EventTime) -> PyEdgeFilter { - self.wrap(self.0.dyn_snapshot_at(time), WireView::SnapshotAt(time)) + self.with_view(ViewOp::SnapshotAt(time)) } /// Evaluates edge predicates against the most recent snapshot of the graph. @@ -413,7 +224,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeFilter: fn snapshot_latest(&self) -> PyEdgeFilter { - self.wrap(self.0.dyn_snapshot_latest(), WireView::SnapshotLatest) + self.with_view(ViewOp::SnapshotLatest) } /// Restricts evaluation to edges belonging to the given layer. @@ -424,10 +235,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeFilter: fn layer(&self, layer: String) -> PyEdgeFilter { - self.wrap( - self.0.dyn_layer(vec![layer.clone()]), - WireView::Layers(vec![layer]), - ) + self.with_view(ViewOp::Layers(vec![layer])) } /// Restricts evaluation to edges belonging to any of the given layers. @@ -438,8 +246,7 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeFilter: fn layers(&self, layers: FromIterable) -> PyEdgeFilter { - let layers: Vec = layers.into(); - self.wrap(self.0.dyn_layer(layers.clone()), WireView::Layers(layers)) + self.with_view(ViewOp::Layers(layers.into())) } /// Matches edges that have at least one event in the current view. @@ -447,11 +254,10 @@ impl PyEdgeFilter { /// Returns: /// filter.FilterExpr: fn is_active(&self) -> PyFilterExpr { - let tree = FilterTree::Edge(wrap_edge_views( - CompositeEdgeFilter::IsActiveEdge(IsActiveEdge), - &self.1, - )); - PyFilterExpr(self.0.dyn_is_active(), Some(tree)) + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsActive, + }) } /// Matches edges that are structurally valid in the current view. @@ -459,11 +265,10 @@ impl PyEdgeFilter { /// Returns: /// filter.FilterExpr: fn is_valid(&self) -> PyFilterExpr { - let tree = FilterTree::Edge(wrap_edge_views( - CompositeEdgeFilter::IsValidEdge(IsValidEdge), - &self.1, - )); - PyFilterExpr(self.0.dyn_is_valid(), Some(tree)) + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsValid, + }) } /// Matches edges that have been deleted. @@ -471,11 +276,10 @@ impl PyEdgeFilter { /// Returns: /// filter.FilterExpr: fn is_deleted(&self) -> PyFilterExpr { - let tree = FilterTree::Edge(wrap_edge_views( - CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge), - &self.1, - )); - PyFilterExpr(self.0.dyn_is_deleted(), Some(tree)) + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsDeleted, + }) } /// Matches edges that are self-loops (source == destination). @@ -483,11 +287,10 @@ impl PyEdgeFilter { /// Returns: /// filter.FilterExpr: fn is_self_loop(&self) -> PyFilterExpr { - let tree = FilterTree::Edge(wrap_edge_views( - CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge), - &self.1, - )); - PyFilterExpr(self.0.dyn_is_self_loop(), Some(tree)) + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsSelfLoop, + }) } } diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index c7311e2854..8c82eaa746 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -1,26 +1,17 @@ use crate::{ - db::graph::views::filter::model::{ - exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - property_filter::PropertyRef, - FilterTree, + db::graph::views::filter::model::tree::{ + Entity, Expr, FilterExpr, Scope, Structural, Target, ViewOp, }, python::{ filter::{ - edge_expr::DynEdgeFilterFactory, filter_expr::PyFilterExpr, node_expr::{PyExpr, PyPropertyExpr}, - wire::{wrap_exploded_views, WireEntity, WireLhs, WireTarget, WireView}, }, types::iterable::FromIterable, }, }; use pyo3::{pyclass, pymethods}; -use raphtory_api::core::storage::timeindex::{AsTime, EventTime}; -use std::sync::Arc; +use raphtory_api::core::storage::timeindex::EventTime; /// An exploded-edge filter scoped to a view. /// @@ -29,26 +20,21 @@ use std::sync::Arc; /// methods on [`ExplodedEdge`]; its property and structural predicates evaluate /// within that view, and its own view methods narrow it further. #[pyclass(frozen, name = "ExplodedEdgeFilter", module = "raphtory.filter")] -pub struct PyExplodedEdgeFilter(Arc, Vec); +pub struct PyExplodedEdgeFilter(pub(crate) Scope); impl PyExplodedEdgeFilter { pub(crate) fn root() -> Self { - PyExplodedEdgeFilter(Arc::new(ExplodedEdgeFilter), Vec::new()) + PyExplodedEdgeFilter(Scope::new(Entity::ExplodedEdge)) } - fn wrap(&self, factory: Arc, view: WireView) -> Self { - let mut views = self.1.clone(); - views.push(view); - PyExplodedEdgeFilter(factory, views) + fn with_view(&self, view: ViewOp) -> Self { + PyExplodedEdgeFilter(self.0.clone().with_view(view)) } - fn lhs(&self, target: WireTarget) -> WireLhs { - WireLhs { - entity: WireEntity::ExplodedEdge, - endpoint: None, + fn read(&self, target: Target) -> Expr { + Expr::Read { + scope: self.0.clone(), target, - ops: Vec::new(), - views: self.1.clone(), } } } @@ -70,8 +56,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { - let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); - PyPropertyExpr::new(self.0.dyn_property(name), Some(lhs)) + PyPropertyExpr(self.read(Target::Property(name))) } /// Filters an exploded edge metadata field by name. @@ -84,8 +69,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { - let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); - PyExpr::new(self.0.dyn_metadata(name), Some(lhs)) + PyExpr(self.read(Target::Metadata(name))) } /// Restricts exploded edge evaluation to the given time window. @@ -99,7 +83,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.ExplodedEdgeFilter: fn window(&self, start: EventTime, end: EventTime) -> PyExplodedEdgeFilter { - self.wrap(self.0.dyn_window(start, end), WireView::Window(start, end)) + self.with_view(ViewOp::Window { start, end }) } /// Restricts exploded edge evaluation to a single point in time. @@ -110,10 +94,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.ExplodedEdgeFilter: fn at(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.wrap( - self.0.dyn_at(time), - WireView::Window(time, EventTime::end(time.t().saturating_add(1))), - ) + self.with_view(ViewOp::At(time)) } /// Restricts exploded edge evaluation to times strictly after the given time. @@ -124,13 +105,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.ExplodedEdgeFilter: fn after(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.wrap( - self.0.dyn_after(time), - WireView::Window( - EventTime::start(time.t().saturating_add(1)), - EventTime::end(i64::MAX), - ), - ) + self.with_view(ViewOp::After(time)) } /// Restricts exploded edge evaluation to times strictly before the given time. @@ -141,10 +116,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.ExplodedEdgeFilter: fn before(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.wrap( - self.0.dyn_before(time), - WireView::Window(EventTime::start(i64::MIN), EventTime::end(time.t())), - ) + self.with_view(ViewOp::Before(time)) } /// Evaluates exploded edge predicates against the latest available state. @@ -152,7 +124,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.ExplodedEdgeFilter: fn latest(&self) -> PyExplodedEdgeFilter { - self.wrap(self.0.dyn_latest(), WireView::Latest) + self.with_view(ViewOp::Latest) } /// Evaluates exploded edge predicates against a snapshot of the graph at a given time. @@ -163,7 +135,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.ExplodedEdgeFilter: fn snapshot_at(&self, time: EventTime) -> PyExplodedEdgeFilter { - self.wrap(self.0.dyn_snapshot_at(time), WireView::SnapshotAt(time)) + self.with_view(ViewOp::SnapshotAt(time)) } /// Evaluates exploded edge predicates against the most recent snapshot of the graph. @@ -171,7 +143,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.ExplodedEdgeFilter: fn snapshot_latest(&self) -> PyExplodedEdgeFilter { - self.wrap(self.0.dyn_snapshot_latest(), WireView::SnapshotLatest) + self.with_view(ViewOp::SnapshotLatest) } /// Restricts evaluation to exploded edges belonging to the given layer. @@ -182,10 +154,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.ExplodedEdgeFilter: fn layer(&self, layer: String) -> PyExplodedEdgeFilter { - self.wrap( - self.0.dyn_layer(vec![layer.clone()]), - WireView::Layers(vec![layer]), - ) + self.with_view(ViewOp::Layers(vec![layer])) } /// Restricts evaluation to exploded edges belonging to any of the given layers. @@ -196,8 +165,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.ExplodedEdgeFilter: fn layers(&self, layers: FromIterable) -> PyExplodedEdgeFilter { - let layers: Vec = layers.into(); - self.wrap(self.0.dyn_layer(layers.clone()), WireView::Layers(layers)) + self.with_view(ViewOp::Layers(layers.into())) } /// Matches exploded edges that have at least one event in the current view. @@ -205,11 +173,10 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.FilterExpr: fn is_active(&self) -> PyFilterExpr { - let tree = FilterTree::ExplodedEdge(wrap_exploded_views( - CompositeExplodedEdgeFilter::IsActiveEdge(IsActiveEdge), - &self.1, - )); - PyFilterExpr(self.0.dyn_is_active(), Some(tree)) + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsActive, + }) } /// Matches exploded edges that are structurally valid in the current view. @@ -217,11 +184,10 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.FilterExpr: fn is_valid(&self) -> PyFilterExpr { - let tree = FilterTree::ExplodedEdge(wrap_exploded_views( - CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge), - &self.1, - )); - PyFilterExpr(self.0.dyn_is_valid(), Some(tree)) + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsValid, + }) } /// Matches exploded edges that have been deleted. @@ -229,11 +195,10 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.FilterExpr: fn is_deleted(&self) -> PyFilterExpr { - let tree = FilterTree::ExplodedEdge(wrap_exploded_views( - CompositeExplodedEdgeFilter::IsDeletedEdge(IsDeletedEdge), - &self.1, - )); - PyFilterExpr(self.0.dyn_is_deleted(), Some(tree)) + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsDeleted, + }) } /// Matches exploded edges that are self-loops (source == destination). @@ -241,11 +206,10 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.FilterExpr: fn is_self_loop(&self) -> PyFilterExpr { - let tree = FilterTree::ExplodedEdge(wrap_exploded_views( - CompositeExplodedEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge), - &self.1, - )); - PyFilterExpr(self.0.dyn_is_self_loop(), Some(tree)) + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsSelfLoop, + }) } } diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 00c1949628..29d1e0eab8 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -1,21 +1,18 @@ use crate::{ db::{ api::{ - state::ops::NodeOp, - view::{internal::GraphView, BoxableGraphView}, - }, - graph::views::filter::{ - model::{ - not_filter::NotFilter, or_filter::OrFilter, AndFilter, DynCreateFilter, FilterTree, - }, - CreateFilter, + state::NodeOp, + view::internal::{DynGraphArc, GraphView}, }, + graph::views::filter::{model::tree::FilterExpr, CreateFilter}, }, errors::GraphError, }; -use pyo3::prelude::*; +use pyo3::{exceptions::PyTypeError, prelude::*}; use std::sync::Arc; +/// A filter as a tree. The same tree runs locally, is sent to a server, and is +/// what `repr` prints, so there is nothing to keep in step. #[pyclass( frozen, name = "FilterExpr", @@ -24,64 +21,57 @@ use std::sync::Arc; from_py_object )] #[derive(Clone)] -pub struct PyFilterExpr(pub Arc, pub Option); +pub struct PyFilterExpr(pub FilterExpr); impl PyFilterExpr { - /// The wire form recorded at construction; filters built in ways the wire - /// schema cannot express (an expression on both sides of a comparison) - /// carry none and cannot be sent to a server. - pub fn try_as_filter_tree(&self) -> Result { - self.1.clone().ok_or_else(|| { - GraphError::InvalidFilter( - "this filter has no server-side form; use plain values rather than \ - expressions on the right-hand side of comparisons" - .to_string(), - ) - }) + pub fn tree(&self) -> &FilterExpr { + &self.0 } } #[pymethods] impl PyFilterExpr { pub fn __and__(&self, other: &Self) -> Self { - let left = self.0.clone(); - let right = other.0.clone(); - let wire = match (&self.1, &other.1) { - (Some(a), Some(b)) => Some(FilterTree::And(vec![a.clone(), b.clone()])), - _ => None, - }; - PyFilterExpr(Arc::new(AndFilter { left, right }), wire) + PyFilterExpr(FilterExpr::And(vec![self.0.clone(), other.0.clone()])) } - pub fn __or__(&self, other: &Self) -> Self { - let left = self.0.clone(); - let right = other.0.clone(); - let wire = match (&self.1, &other.1) { - (Some(a), Some(b)) => Some(FilterTree::Or(vec![a.clone(), b.clone()])), - _ => None, - }; - PyFilterExpr(Arc::new(OrFilter { left, right }), wire) + pub fn __or__(&self, other: &Self) -> PyResult { + no_view(&self.0)?; + no_view(&other.0)?; + Ok(PyFilterExpr(FilterExpr::Or(vec![ + self.0.clone(), + other.0.clone(), + ]))) } - fn __invert__(&self) -> Self { - let wire = self.1.clone().map(|t| FilterTree::Not(Box::new(t))); - PyFilterExpr(Arc::new(NotFilter(self.0.clone())), wire) + fn __invert__(&self) -> PyResult { + no_view(&self.0)?; + Ok(PyFilterExpr(FilterExpr::Not(Box::new(self.0.clone())))) } - /// Shows the filter as it would be sent to a server. A filter with no - /// server-side form (an expression on both sides of a comparison) says so - /// instead; it still runs locally. + /// Shows the filter tree: what runs locally and what a server receives. fn __repr__(&self) -> String { - match &self.1 { - Some(tree) => format!("FilterExpr({tree})"), - None => "FilterExpr()".to_string(), - } + format!("FilterExpr({})", self.0) + } +} + +/// A view applies to the whole filter, so it can be `&`-ed with predicates or applied +/// alone, but has no meaning under `|` or `~`. Refused where it is written, as the +/// engine would refuse it when applied. +fn no_view(filter: &FilterExpr) -> PyResult<()> { + if filter.has_view() { + return Err(PyTypeError::new_err( + "a view (filter.Graph...) applies to the whole filter: combine it with `&` or apply it alone, not with `|` or `~`", + )); } + Ok(()) } impl CreateFilter for PyFilterExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc; + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = DynGraphArc<'graph> + where + Self: 'graph; type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = Arc + 'graph> @@ -89,7 +79,7 @@ impl CreateFilter for PyFilterExpr { Self: 'graph; type FilteredGraph<'graph, G> - = Arc + = DynGraphArc<'graph> where Self: 'graph, G: GraphView + 'graph; @@ -114,6 +104,6 @@ impl CreateFilter for PyFilterExpr { &self, graph: G, ) -> Result, GraphError> { - self.0.dyn_filter_graph_view(Arc::new(graph)) + self.0.filter_graph_view(graph) } } diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index 74e38114c9..5706f65c8d 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -1,13 +1,9 @@ use crate::{ - db::graph::views::filter::model::{ - graph_filter::GraphFilter, DynView, FilterTree, GraphViewOp, ViewWrapOps, - }, - prelude::Layer, + db::graph::views::filter::model::tree::{FilterExpr, ViewOp}, python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; -use raphtory_api::core::storage::timeindex::{AsTime, EventTime}; -use std::sync::Arc; +use raphtory_api::core::storage::timeindex::EventTime; /// A graph-level view scope. /// @@ -21,17 +17,17 @@ use std::sync::Arc; extends = PyFilterExpr, frozen )] -pub struct PyGraphFilter(pub(crate) DynView, pub(crate) Vec); +pub struct PyGraphFilter(pub(crate) Vec); impl PyGraphFilter { pub(crate) fn root() -> Self { - PyGraphFilter(Arc::new(GraphFilter), Vec::new()) + PyGraphFilter(Vec::new()) } - fn extend(&self, view: DynView, op: GraphViewOp) -> Self { - let mut ops = self.1.clone(); - ops.push(op); - PyGraphFilter(view, ops) + fn with_view(&self, view: ViewOp) -> Self { + let mut ops = self.0.clone(); + ops.push(view); + PyGraphFilter(ops) } } @@ -48,10 +44,7 @@ impl PyGraphFilter { /// Returns: /// filter.GraphFilter: fn window(&self, start: EventTime, end: EventTime) -> PyGraphFilter { - self.extend( - self.0.clone().window(start, end), - GraphViewOp::Window { start, end }, - ) + self.with_view(ViewOp::Window { start, end }) } /// Restricts evaluation to a single point in time. @@ -62,13 +55,7 @@ impl PyGraphFilter { /// Returns: /// filter.GraphFilter: fn at(&self, time: EventTime) -> PyGraphFilter { - self.extend( - self.0.clone().at(time), - GraphViewOp::Window { - start: time, - end: EventTime::end(time.t().saturating_add(1)), - }, - ) + self.with_view(ViewOp::At(time)) } /// Restricts evaluation to times strictly after the given time. @@ -79,13 +66,7 @@ impl PyGraphFilter { /// Returns: /// filter.GraphFilter: fn after(&self, time: EventTime) -> PyGraphFilter { - self.extend( - self.0.clone().after(time), - GraphViewOp::Window { - start: EventTime::start(time.t().saturating_add(1)), - end: EventTime::end(i64::MAX), - }, - ) + self.with_view(ViewOp::After(time)) } /// Restricts evaluation to times strictly before the given time. @@ -96,13 +77,7 @@ impl PyGraphFilter { /// Returns: /// filter.GraphFilter: fn before(&self, time: EventTime) -> PyGraphFilter { - self.extend( - self.0.clone().before(time), - GraphViewOp::Window { - start: EventTime::start(i64::MIN), - end: EventTime::end(time.t()), - }, - ) + self.with_view(ViewOp::Before(time)) } /// Evaluates filters against the latest available state of the graph. @@ -110,7 +85,7 @@ impl PyGraphFilter { /// Returns: /// filter.GraphFilter: fn latest(&self) -> PyGraphFilter { - self.extend(Arc::new(self.0.clone().latest()), GraphViewOp::Latest) + self.with_view(ViewOp::Latest) } /// Evaluates filters against a snapshot of the graph at a given time. @@ -121,10 +96,7 @@ impl PyGraphFilter { /// Returns: /// filter.GraphFilter: fn snapshot_at(&self, time: EventTime) -> PyGraphFilter { - self.extend( - Arc::new(self.0.clone().snapshot_at(time)), - GraphViewOp::SnapshotAt(time), - ) + self.with_view(ViewOp::SnapshotAt(time)) } /// Evaluates filters against the most recent snapshot of the graph. @@ -132,10 +104,7 @@ impl PyGraphFilter { /// Returns: /// filter.GraphFilter: fn snapshot_latest(&self) -> PyGraphFilter { - self.extend( - Arc::new(self.0.clone().snapshot_latest()), - GraphViewOp::SnapshotLatest, - ) + self.with_view(ViewOp::SnapshotLatest) } /// Restricts evaluation to a single layer. @@ -146,10 +115,7 @@ impl PyGraphFilter { /// Returns: /// filter.GraphFilter: fn layer(&self, layer: String) -> PyGraphFilter { - self.extend( - Arc::new(self.0.clone().layer(layer.clone())), - GraphViewOp::Layers(Layer::from(layer)), - ) + self.with_view(ViewOp::Layers(vec![layer])) } /// Restricts evaluation to any of the given layers. @@ -160,11 +126,7 @@ impl PyGraphFilter { /// Returns: /// filter.GraphFilter: fn layers(&self, layers: FromIterable) -> PyGraphFilter { - let names: Vec = layers.into(); - self.extend( - Arc::new(self.0.clone().layer(names.clone())), - GraphViewOp::Layers(Layer::from(names)), - ) + self.with_view(ViewOp::Layers(layers.into())) } } @@ -289,7 +251,7 @@ impl<'py> IntoPyObject<'py> for PyGraphFilter { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyFilterExpr(self.0.clone(), Some(FilterTree::View(self.1.clone()))); + let parent = PyFilterExpr(FilterExpr::View(self.0.clone())); Bound::new(py, (self, parent)) } } diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 622dae3e26..08d7b0e67e 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -19,7 +19,6 @@ pub mod exploded_edge_expr; pub mod filter_expr; pub mod graph_filter; pub mod node_expr; -pub(crate) mod wire; impl From> for Layer { fn from(iter: FromIterable) -> Self { diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 934a624486..0ac14eda07 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,47 +1,50 @@ use crate::{ db::graph::views::filter::model::{ - filter::{FieldFilterValue, NODE_ID_FIELD, NODE_NAME_FIELD, NODE_TYPE_FIELD}, - is_active_node_filter::IsActiveNode, - node_expr::{ops::prop_as_gid, CreateOp, DynCreateOp, DynEntityExpr, DynTemporal}, - node_filter::CompositeNodeFilter, node_state_filter::NodeStateBoolColOp, - property_filter::{Op, PropertyFilterValue, PropertyRef}, - CombinedFilter, DynCreateFilter, DynCreateView, DynPropertyExprFactory, EntityMarker, - FilterOperator, FilterTree, InternalViewWrapOps, NodeViewFilterOps, PropertyExprFactory, - ViewWrapOps, + tree::{ + Agg, CmpOp, Entity, Expr, Field, FilterExpr, OpaqueFilter, Qual, Scope, StrOp, + Structural, Target, ViewOp, + }, }, - prelude::{EntityAggOps, EntityExprFilterOps, NodeFilter, NodeFilterFactory}, python::{ - filter::{ - filter_expr::PyFilterExpr, - wire::{wrap_node_views, WireEntity, WireLhs, WireTarget, WireValue, WireView}, - }, - graph::node_state::PyOutputNodeState, + filter::filter_expr::PyFilterExpr, graph::node_state::PyOutputNodeState, types::iterable::FromIterable, }, }; use pyo3::{ - exceptions::PyTypeError, pyclass, pymethods, Bound, FromPyObject, IntoPyObject, PyErr, - PyResult, Python, + exceptions::{PyTypeError, PyValueError}, + pyclass, pymethods, Bound, FromPyObject, IntoPyObject, PyErr, PyResult, Python, }; use raphtory_api::core::{ - entities::{ - properties::prop::{Prop, PropType}, - GID, - }, - storage::timeindex::{AsTime, EventTime}, + entities::properties::prop::{Prop, PropType}, + storage::timeindex::EventTime, Direction, }; use std::sync::Arc; -// filter.Node.neighbours.is_active.all -#[pyclass(frozen, subclass, name = "Expr", module = "raphtory.filter")] +/// A value expression: a field, degree, property, metadata entry or an +/// aggregate over one. Comparing it to a value or to another expression gives +/// a [`FilterExpr`]. +#[pyclass( + frozen, + subclass, + name = "Expr", + module = "raphtory.filter", + from_py_object +)] #[derive(Clone)] -pub struct PyExpr(Arc, pub(crate) Option); - -#[pyclass(frozen, extends = PyExpr, name = "PropertyExpr", module = "raphtory.filter")] +pub struct PyExpr(pub(crate) Expr); + +/// A property read, which can switch to the property's history with `temporal()`. +#[pyclass( + frozen, + extends = PyExpr, + name = "PropertyExpr", + module = "raphtory.filter", + from_py_object +)] #[derive(Clone)] -pub struct PyPropertyExpr(Arc, pub(crate) Option); +pub struct PyPropertyExpr(pub(crate) Expr); impl<'py> IntoPyObject<'py> for PyPropertyExpr { type Target = PyPropertyExpr; @@ -49,26 +52,13 @@ impl<'py> IntoPyObject<'py> for PyPropertyExpr { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyExpr(self.0.clone(), self.1.clone()); - let child = self; - Bound::new(py, (child, parent)) - } -} - -impl>> From for PyExpr { - fn from(value: E) -> Self { - PyExpr(Arc::new(value), None) + let parent = PyExpr(self.0.clone()); + Bound::new(py, (self, parent)) } } -impl From> for PyPropertyExpr { - fn from(value: Arc) -> Self { - PyPropertyExpr(value, None) - } -} - -/// Accepts either another expression or a plain python value (extracted as a -/// `Prop` constant) on the rhs of comparison and string operators. +/// Accepts either another expression or a plain python value (a constant) on +/// the right-hand side of comparison and string operators. #[derive(FromPyObject)] enum ExprOrValue { Expr(PyExpr), @@ -78,19 +68,12 @@ enum ExprOrValue { /// Values are checked against the expression's statically known type at the /// comparison itself, so a mistyped literal fails where it is written instead /// of at some later `filter()` call. Unknown types defer to filter time. -/// String operators require a string-castable operand whatever the lhs type. -fn check_str_value(v: &Prop) -> PyResult<()> { - if v.dtype() != PropType::Str && v.clone().try_cast(PropType::Str).is_err() { - return Err(PyTypeError::new_err(format!( - "value {v:?} of type {} is not a valid string operand", - v.dtype() - ))); - } - Ok(()) +fn static_type(lhs: &Expr) -> PyResult { + Ok(lhs.compile()?.dyn_prop_type()) } -fn check_value(lhs: &Arc, v: &Prop) -> PyResult<()> { - let pt = lhs.dyn_prop_type(); +fn check_value(lhs: &Expr, v: &Prop) -> PyResult<()> { + let pt = static_type(lhs)?; if pt != PropType::Empty && v.dtype() != pt && v.clone().try_cast(pt.clone()).is_err() { return Err(PyTypeError::new_err(format!( "value {v:?} of type {} is not comparable with an expression of type {pt}", @@ -100,119 +83,104 @@ fn check_value(lhs: &Arc, v: &Prop) -> PyResult<()> { Ok(()) } -impl PyExpr { - pub(crate) fn new(op: Arc, wire: Option) -> Self { - PyExpr(op, wire) +/// Every member of a set is checked the way a single constant is. +fn checked_values(lhs: &Expr, values: FromIterable) -> PyResult> { + let values: Vec = values.into(); + for v in &values { + check_value(lhs, v)?; } + Ok(values) +} - /// A value in the shape the recorded lhs target expects on the wire. - fn wire_single(&self, v: &Prop) -> Option { - let lhs = self.1.as_ref()?; - Some(match &lhs.target { - WireTarget::Field(NODE_ID_FIELD) => { - WireValue::Field(FieldFilterValue::ID(prop_as_gid(v)?)) - } - WireTarget::Field(_) => match v { - Prop::Str(s) => WireValue::Field(FieldFilterValue::Single(s.to_string())), - _ => return None, - }, - WireTarget::Prop(_) | WireTarget::Degree(_) => { - WireValue::Prop(PropertyFilterValue::Single(v.clone())) - } - }) +/// Presence tests only mean something on an expression that can be missing. +fn check_nullable(lhs: &Expr, op: &str) -> PyResult<()> { + if !lhs.compile()?.dyn_nullable() { + return Err(PyTypeError::new_err(format!( + "{op}() is not valid on an expression that always has a value" + ))); } + Ok(()) +} - fn wire_set(&self, values: &[Prop]) -> Option { - let lhs = self.1.as_ref()?; - Some(match &lhs.target { - WireTarget::Field(NODE_ID_FIELD) => WireValue::Field(FieldFilterValue::IDSet( - Arc::new(values.iter().map(prop_as_gid).collect::>()?), - )), - WireTarget::Field(_) => WireValue::Field(FieldFilterValue::Set(Arc::new( - values - .iter() - .map(|v| match v { - Prop::Str(s) => Some(s.to_string()), - _ => None, - }) - .collect::>()?, - ))), - WireTarget::Prop(_) | WireTarget::Degree(_) => WireValue::Prop( - PropertyFilterValue::Set(Arc::new(values.iter().cloned().collect())), - ), - }) +/// String operators require a string-castable operand whatever the lhs type. +fn check_str_value(v: &Prop) -> PyResult<()> { + if v.dtype() != PropType::Str && v.clone().try_cast(PropType::Str).is_err() { + return Err(PyTypeError::new_err(format!( + "value {v:?} of type {} is not a valid string operand", + v.dtype() + ))); } + Ok(()) +} - fn finish(&self, operator: FilterOperator, value: Option) -> Option { - self.1.clone()?.finish(operator, value?) - } +/// The right-hand side of a comparison, with a constant checked against the lhs. +fn compared(lhs: &Expr, other: ExprOrValue) -> PyResult { + Ok(match other { + ExprOrValue::Expr(e) => e.0, + ExprOrValue::Value(v) => { + check_value(lhs, &v)?; + Expr::Const(v) + } + }) +} - fn with_op(&self, expr: Arc, op: Op) -> Self { - PyExpr(expr, self.1.clone().map(|w| w.with_op(op))) - } +/// The right-hand side of a string operator. +fn string_operand(lhs: &Expr, other: ExprOrValue, typed: bool) -> PyResult { + Ok(match other { + ExprOrValue::Expr(e) => e.0, + ExprOrValue::Value(v) => { + check_str_value(&v)?; + if typed { + check_value(lhs, &v)?; + } + Expr::Const(v) + } + }) } #[pymethods] impl PyExpr { fn __eq__(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().eq(e.0)), None)), - ExprOrValue::Value(v) => { - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::Eq, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().eq(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Eq, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) } fn __ne__(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ne(e.0)), None)), - ExprOrValue::Value(v) => { - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::Ne, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().ne(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Ne, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) } fn __lt__(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().lt(e.0)), None)), - ExprOrValue::Value(v) => { - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::Lt, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().lt(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Lt, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) } fn __le__(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().le(e.0)), None)), - ExprOrValue::Value(v) => { - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::Le, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().le(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Le, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) } fn __gt__(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().gt(e.0)), None)), - ExprOrValue::Value(v) => { - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::Gt, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().gt(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Gt, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) } fn __ge__(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ge(e.0)), None)), - ExprOrValue::Value(v) => { - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::Ge, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().ge(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Ge, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) } /// Checks whether the value's string representation starts with the given value. @@ -223,18 +191,11 @@ impl PyExpr { /// Returns: /// filter.FilterExpr: fn starts_with(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr( - Arc::new(self.0.clone().starts_with(e.0)), - None, - )), - ExprOrValue::Value(v) => { - check_str_value(&v)?; - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::StartsWith, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().starts_with(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Str { + op: StrOp::StartsWith, + lhs: self.0.clone(), + rhs: string_operand(&self.0, other, true)?, + })) } /// Checks whether the value's string representation ends with the given value. /// @@ -244,15 +205,11 @@ impl PyExpr { /// Returns: /// filter.FilterExpr: fn ends_with(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().ends_with(e.0)), None)), - ExprOrValue::Value(v) => { - check_str_value(&v)?; - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::EndsWith, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().ends_with(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Str { + op: StrOp::EndsWith, + lhs: self.0.clone(), + rhs: string_operand(&self.0, other, true)?, + })) } /// Checks whether the value's string representation contains the given value. /// @@ -262,15 +219,11 @@ impl PyExpr { /// Returns: /// filter.FilterExpr: fn contains(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr(Arc::new(self.0.clone().contains(e.0)), None)), - ExprOrValue::Value(v) => { - check_str_value(&v)?; - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::Contains, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().contains(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Str { + op: StrOp::Contains, + lhs: self.0.clone(), + rhs: string_operand(&self.0, other, true)?, + })) } /// Checks whether the value's string representation **does not** contain the given value. /// @@ -280,18 +233,11 @@ impl PyExpr { /// Returns: /// filter.FilterExpr: fn not_contains(&self, other: ExprOrValue) -> PyResult { - match other { - ExprOrValue::Expr(e) => Ok(PyFilterExpr( - Arc::new(self.0.clone().not_contains(e.0)), - None, - )), - ExprOrValue::Value(v) => { - check_str_value(&v)?; - check_value(&self.0, &v)?; - let wire = self.finish(FilterOperator::NotContains, self.wire_single(&v)); - Ok(PyFilterExpr(Arc::new(self.0.clone().not_contains(v)), wire)) - } - } + Ok(PyFilterExpr(FilterExpr::Str { + op: StrOp::NotContains, + lhs: self.0.clone(), + rhs: string_operand(&self.0, other, true)?, + })) } /// Performs fuzzy matching against the value's string representation, within a Levenshtein distance and with optional prefix matching. /// @@ -308,34 +254,14 @@ impl PyExpr { levenshtein_distance: usize, prefix_match: bool, ) -> PyResult { - Ok(match other { - ExprOrValue::Expr(e) => PyFilterExpr( - Arc::new( - self.0 - .clone() - .fuzzy_search(e.0, levenshtein_distance, prefix_match), - ), - None, - ), - ExprOrValue::Value(v) => { - check_str_value(&v)?; - let wire = self.finish( - FilterOperator::FuzzySearch { - levenshtein_distance, - prefix_match, - }, - self.wire_single(&v), - ); - PyFilterExpr( - Arc::new( - self.0 - .clone() - .fuzzy_search(v, levenshtein_distance, prefix_match), - ), - wire, - ) - } - }) + Ok(PyFilterExpr(FilterExpr::Str { + op: StrOp::FuzzySearch { + levenshtein_distance, + prefix_match, + }, + lhs: self.0.clone(), + rhs: string_operand(&self.0, other, false)?, + })) } /// Checks whether the value is contained within the given values. @@ -345,10 +271,12 @@ impl PyExpr { /// /// Returns: /// filter.FilterExpr: - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - let values: Vec = values.into(); - let wire = self.finish(FilterOperator::IsIn, self.wire_set(&values)); - PyFilterExpr(Arc::new(self.0.clone().is_in(values)), wire) + fn is_in(&self, values: FromIterable) -> PyResult { + Ok(PyFilterExpr(FilterExpr::In { + expr: self.0.clone(), + values: checked_values(&self.0, values)?, + negated: false, + })) } /// Checks whether the value is **not** contained within the given values. /// @@ -357,33 +285,29 @@ impl PyExpr { /// /// Returns: /// filter.FilterExpr: - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - let values: Vec = values.into(); - let wire = self.finish(FilterOperator::IsNotIn, self.wire_set(&values)); - PyFilterExpr(Arc::new(self.0.clone().is_not_in(values)), wire) + fn is_not_in(&self, values: FromIterable) -> PyResult { + Ok(PyFilterExpr(FilterExpr::In { + expr: self.0.clone(), + values: checked_values(&self.0, values)?, + negated: true, + })) } /// Checks whether the value is present (not `None`). /// /// Returns: /// filter.FilterExpr: - fn is_some(&self) -> PyFilterExpr { - let wire = self.finish( - FilterOperator::IsSome, - Some(WireValue::Prop(PropertyFilterValue::None)), - ); - PyFilterExpr(Arc::new(self.0.clone().is_some()), wire) + fn is_some(&self) -> PyResult { + check_nullable(&self.0, "is_some")?; + Ok(PyFilterExpr(FilterExpr::IsSome(self.0.clone()))) } /// Checks whether the value is `None` / missing. /// /// Returns: /// filter.FilterExpr: - fn is_none(&self) -> PyFilterExpr { - let wire = self.finish( - FilterOperator::IsNone, - Some(WireValue::Prop(PropertyFilterValue::None)), - ); - PyFilterExpr(Arc::new(self.0.clone().is_none()), wire) + fn is_none(&self) -> PyResult { + check_nullable(&self.0, "is_none")?; + Ok(PyFilterExpr(FilterExpr::IsNone(self.0.clone()))) } /// Requires that **any** element matches when the value is list-like (a temporal history or a list property). @@ -391,14 +315,14 @@ impl PyExpr { /// Returns: /// filter.Expr: fn any(&self) -> Self { - self.with_op(Arc::new(self.0.clone().any()), Op::Any) + PyExpr(Expr::Qual(Qual::Any, Box::new(self.0.clone()))) } /// Requires that **all** elements match when the value is list-like (a temporal history or a list property). /// /// Returns: /// filter.Expr: fn all(&self) -> Self { - self.with_op(Arc::new(self.0.clone().all()), Op::All) + PyExpr(Expr::Qual(Qual::All, Box::new(self.0.clone()))) } /// Sums the elements when the value is numeric and list-like. @@ -406,55 +330,49 @@ impl PyExpr { /// Returns: /// filter.Expr: fn sum(&self) -> Self { - self.with_op(Arc::new(self.0.clone().sum()), Op::Sum) + PyExpr(Expr::Agg(Agg::Sum, Box::new(self.0.clone()))) } /// Averages the elements when the value is numeric and list-like. /// /// Returns: /// filter.Expr: fn avg(&self) -> Self { - self.with_op(Arc::new(self.0.clone().avg()), Op::Avg) + PyExpr(Expr::Agg(Agg::Avg, Box::new(self.0.clone()))) } /// Selects the minimum element when the value is list-like. /// /// Returns: /// filter.Expr: fn min(&self) -> Self { - self.with_op(Arc::new(self.0.clone().min()), Op::Min) + PyExpr(Expr::Agg(Agg::Min, Box::new(self.0.clone()))) } /// Selects the maximum element when the value is list-like. /// /// Returns: /// filter.Expr: fn max(&self) -> Self { - self.with_op(Arc::new(self.0.clone().max()), Op::Max) + PyExpr(Expr::Agg(Agg::Max, Box::new(self.0.clone()))) } /// Selects the first element when the value is list-like. /// /// Returns: /// filter.Expr: fn first(&self) -> Self { - self.with_op(Arc::new(self.0.clone().first()), Op::First) + PyExpr(Expr::Agg(Agg::First, Box::new(self.0.clone()))) } /// Selects the last element when the value is list-like. /// /// Returns: /// filter.Expr: fn last(&self) -> Self { - self.with_op(Arc::new(self.0.clone().last()), Op::Last) + PyExpr(Expr::Agg(Agg::Last, Box::new(self.0.clone()))) } /// Selects the number of elements when the value is list-like. /// /// Returns: /// filter.Expr: fn len(&self) -> Self { - self.with_op(Arc::new(self.0.clone().len()), Op::Len) - } -} - -impl PyPropertyExpr { - pub(crate) fn new(expr: Arc, wire: Option) -> Self { - PyPropertyExpr(expr, wire) + PyExpr(Expr::Agg(Agg::Len, Box::new(self.0.clone()))) } } @@ -466,94 +384,7 @@ impl PyPropertyExpr { /// Returns: /// filter.Expr: fn temporal(&self) -> PyExpr { - PyExpr( - self.0.temporal(), - self.1.clone().and_then(WireLhs::temporal), - ) - } -} - -pub trait DynNodeFilterFactory: - DynPropertyExprFactory + DynEntityExpr + DynCreateView + Send + Sync + 'static -{ - fn dyn_id(&self) -> Arc; - fn dyn_name(&self) -> Arc; - fn dyn_node_type(&self) -> Arc; - fn dyn_degree(&self) -> Arc; - fn dyn_in_degree(&self) -> Arc; - fn dyn_out_degree(&self) -> Arc; - fn dyn_is_active(&self) -> Arc; - fn dyn_metadata(&self, name: String) -> Arc; - - fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc; - - fn dyn_bounds(&self) -> (EventTime, EventTime); -} - -impl InternalViewWrapOps for Arc { - type Window = Arc; - - // Both calls dispatch through the vtable explicitly: plain method syntax - // would select the DynNodeFilterFactory blanket on Arc itself and loop. - fn bounds(&self) -> (EventTime, EventTime) { - self.as_ref().dyn_bounds() - } - - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - self.as_ref().dyn_build_window(start, end) - } -} - -impl DynNodeFilterFactory for T -where - T: NodeFilterFactory + NodeViewFilterOps + Send + Sync + 'static, -{ - fn dyn_id(&self) -> Arc { - Arc::new(self.id()) - } - fn dyn_name(&self) -> Arc { - Arc::new(self.name()) - } - fn dyn_node_type(&self) -> Arc { - Arc::new(self.node_type()) - } - - fn dyn_degree(&self) -> Arc { - Arc::new(self.degree()) - } - fn dyn_in_degree(&self) -> Arc { - Arc::new(self.in_degree()) - } - fn dyn_out_degree(&self) -> Arc { - Arc::new(self.out_degree()) - } - - fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) - } - - fn dyn_metadata(&self, name: String) -> Arc { - Arc::new(PropertyExprFactory::metadata(self, name)) - } - - fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc { - Arc::new(self.clone().build_window(start, end)) - } - - fn dyn_bounds(&self) -> (EventTime, EventTime) { - self.bounds() - } -} - -impl NodeFilterFactory for Arc { - type NodeWindow = Self::Window; -} - -impl NodeViewFilterOps for Arc { - type Output = Arc; - - fn is_active(&self) -> Self::Output { - self.as_ref().dyn_is_active() + PyExpr(Expr::Temporal(Box::new(self.0.clone()))) } } @@ -563,26 +394,21 @@ impl NodeViewFilterOps for Arc { /// `Node.latest()`, ...); its field and property methods evaluate within that /// view, and its own view methods narrow it further. #[pyclass(frozen, name = "NodeFilter", module = "raphtory.filter")] -pub struct PyNodeFilter(Arc, Vec); +pub struct PyNodeFilter(pub(crate) Scope); impl PyNodeFilter { pub(crate) fn root() -> Self { - PyNodeFilter(Arc::new(NodeFilter), Vec::new()) + PyNodeFilter(Scope::new(Entity::Node)) } - fn wrap(&self, filter: T, view: WireView) -> Self { - let mut views = self.1.clone(); - views.push(view); - Self(Arc::new(filter), views) + fn with_view(&self, view: ViewOp) -> Self { + PyNodeFilter(self.0.clone().with_view(view)) } - fn lhs(&self, target: WireTarget) -> WireLhs { - WireLhs { - entity: WireEntity::Node, - endpoint: None, + fn read(&self, target: Target) -> Expr { + Expr::Read { + scope: self.0.clone(), target, - ops: Vec::new(), - views: self.1.clone(), } } } @@ -599,10 +425,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn id(&self) -> PyExpr { - PyExpr( - self.0.dyn_id(), - Some(self.lhs(WireTarget::Field(NODE_ID_FIELD))), - ) + PyExpr(self.read(Target::Field(Field::Id))) } /// Selects the node name field for filtering. @@ -610,10 +433,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn name(&self) -> PyExpr { - PyExpr( - self.0.dyn_name(), - Some(self.lhs(WireTarget::Field(NODE_NAME_FIELD))), - ) + PyExpr(self.read(Target::Field(Field::Name))) } /// Selects the node type field for filtering. @@ -621,10 +441,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn node_type(&self) -> PyExpr { - PyExpr( - self.0.dyn_node_type(), - Some(self.lhs(WireTarget::Field(NODE_TYPE_FIELD))), - ) + PyExpr(self.read(Target::Field(Field::NodeType))) } /// Selects incoming node degree for filtering. @@ -632,10 +449,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn in_degree(&self) -> PyExpr { - PyExpr( - self.0.dyn_in_degree(), - Some(self.lhs(WireTarget::Degree(Direction::IN))), - ) + PyExpr(self.read(Target::Degree(Direction::IN))) } /// Selects total node degree for filtering. @@ -643,10 +457,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn degree(&self) -> PyExpr { - PyExpr( - self.0.dyn_degree(), - Some(self.lhs(WireTarget::Degree(Direction::BOTH))), - ) + PyExpr(self.read(Target::Degree(Direction::BOTH))) } /// Selects outgoing node degree for filtering. @@ -654,10 +465,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn out_degree(&self) -> PyExpr { - PyExpr( - self.0.dyn_out_degree(), - Some(self.lhs(WireTarget::Degree(Direction::OUT))), - ) + PyExpr(self.read(Target::Degree(Direction::OUT))) } /// Filters a node property by name. @@ -670,8 +478,7 @@ impl PyNodeFilter { /// Returns: /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { - let lhs = self.lhs(WireTarget::Prop(PropertyRef::Property(name.clone()))); - PyPropertyExpr(self.0.dyn_property(name), Some(lhs)) + PyPropertyExpr(self.read(Target::Property(name))) } /// Filters a node metadata field by name. @@ -684,8 +491,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { - let lhs = self.lhs(WireTarget::Prop(PropertyRef::Metadata(name.clone()))); - PyExpr(self.0.dyn_metadata(name), Some(lhs)) + PyExpr(self.read(Target::Metadata(name))) } /// Restricts node evaluation to the given time window. @@ -699,10 +505,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeFilter: fn window(&self, start: EventTime, end: EventTime) -> PyNodeFilter { - self.wrap( - self.0.clone().window(start, end), - WireView::Window(start, end), - ) + self.with_view(ViewOp::Window { start, end }) } /// Restricts node evaluation to a single point in time. @@ -713,10 +516,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeFilter: fn at(&self, time: EventTime) -> PyNodeFilter { - self.wrap( - self.0.clone().at(time), - WireView::Window(time, EventTime::end(time.t().saturating_add(1))), - ) + self.with_view(ViewOp::At(time)) } /// Restricts node evaluation to times strictly after the given time. @@ -727,13 +527,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeFilter: fn after(&self, time: EventTime) -> PyNodeFilter { - self.wrap( - self.0.clone().after(time), - WireView::Window( - EventTime::start(time.t().saturating_add(1)), - EventTime::end(i64::MAX), - ), - ) + self.with_view(ViewOp::After(time)) } /// Restricts node evaluation to times strictly before the given time. @@ -744,10 +538,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeFilter: fn before(&self, time: EventTime) -> PyNodeFilter { - self.wrap( - self.0.clone().before(time), - WireView::Window(EventTime::start(i64::MIN), EventTime::end(time.t())), - ) + self.with_view(ViewOp::Before(time)) } /// Evaluates filters against the latest available state of each node. @@ -755,7 +546,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeFilter: fn latest(&self) -> PyNodeFilter { - self.wrap(self.0.clone().latest(), WireView::Latest) + self.with_view(ViewOp::Latest) } /// Evaluates filters against a snapshot of the graph at a given time. @@ -766,7 +557,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeFilter: fn snapshot_at(&self, time: EventTime) -> PyNodeFilter { - self.wrap(self.0.clone().snapshot_at(time), WireView::SnapshotAt(time)) + self.with_view(ViewOp::SnapshotAt(time)) } /// Evaluates filters against the most recent snapshot of the graph. @@ -774,7 +565,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeFilter: fn snapshot_latest(&self) -> PyNodeFilter { - self.wrap(self.0.clone().snapshot_latest(), WireView::SnapshotLatest) + self.with_view(ViewOp::SnapshotLatest) } /// Restricts evaluation to nodes belonging to the given layer. @@ -785,10 +576,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeFilter: fn layer(&self, layer: String) -> PyNodeFilter { - self.wrap( - self.0.clone().layer(vec![layer.clone()]), - WireView::Layers(vec![layer]), - ) + self.with_view(ViewOp::Layers(vec![layer])) } /// Restricts evaluation to nodes belonging to any of the given layers. @@ -799,11 +587,7 @@ impl PyNodeFilter { /// Returns: /// filter.NodeFilter: fn layers(&self, layers: FromIterable) -> PyNodeFilter { - let layers: Vec = layers.into(); - self.wrap( - self.0.clone().layer(layers.clone()), - WireView::Layers(layers), - ) + self.with_view(ViewOp::Layers(layers.into())) } /// Matches nodes that have at least one event in the current view. @@ -811,11 +595,10 @@ impl PyNodeFilter { /// Returns: /// filter.FilterExpr: fn is_active(&self) -> PyFilterExpr { - let tree = FilterTree::Node(wrap_node_views( - CompositeNodeFilter::IsActiveNode(IsActiveNode), - &self.1, - )); - PyFilterExpr(self.0.dyn_is_active(), Some(tree)) + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsActive, + }) } /// Build a node filter from a boolean column of an existing node-state result. @@ -828,8 +611,8 @@ impl PyNodeFilter { /// filter.FilterExpr: fn by_state_column(&self, state: &PyOutputNodeState, col: String) -> PyResult { let op = NodeStateBoolColOp::new(&state.inner, &col) - .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; - Ok(PyFilterExpr(Arc::new(op), None)) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(PyFilterExpr(FilterExpr::Opaque(OpaqueFilter(Arc::new(op))))) } } diff --git a/raphtory/src/python/filter/wire.rs b/raphtory/src/python/filter/wire.rs deleted file mode 100644 index 5d64e7f950..0000000000 --- a/raphtory/src/python/filter/wire.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! Wire-side recording for python filter construction. -//! -//! A python filter object does two jobs: run locally (the compiled -//! expression) and travel to a server (the GraphQL wire JSON). Expressions -//! cannot be faithfully reverse-engineered into JSON, so each construction -//! step records its wire fragment alongside the expression it builds. The -//! remote client reads the finished [`FilterTree`]; constructions with no -//! wire equivalent (an expression on both sides of a comparison) simply -//! carry no tree and are rejected at the remote boundary with a clear error. - -use crate::db::graph::views::filter::model::{ - degree_filter::DegreeFilter, - edge_filter::{CompositeEdgeFilter, EdgeFilter, Endpoint}, - exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, - filter::{FieldFilterValue, Filter, NODE_ID_FIELD, NODE_NAME_FIELD, NODE_TYPE_FIELD}, - latest_filter::Latest, - layered_filter::Layered, - node_filter::{CompositeNodeFilter, NodeFilter}, - property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, - snapshot_filter::{SnapshotAt, SnapshotLatest}, - windowed_filter::Windowed, - FilterOperator, FilterTree, -}; -use raphtory_api::core::{storage::timeindex::EventTime, Direction}; - -/// Which entity's tree a finished predicate belongs to. -#[derive(Clone, Copy, Debug)] -pub(crate) enum WireEntity { - Node, - Edge, - ExplodedEdge, -} - -/// A view restriction recorded from a factory chain. -#[derive(Clone, Debug)] -pub(crate) enum WireView { - Window(EventTime, EventTime), - Latest, - SnapshotAt(EventTime), - SnapshotLatest, - Layers(Vec), -} - -/// What the value expression selects, before its predicate. -#[derive(Clone, Debug)] -pub(crate) enum WireTarget { - /// A built-in field (node id / name / type), by its wire field name. - Field(&'static str), - /// A property, metadata or temporal-property column. - Prop(PropertyRef), - /// A node degree in a direction. - Degree(Direction), -} - -/// The recorded lhs of a comparison: entity + target + op chain + views, and -/// an optional endpoint when the chain went through `src()`/`dst()`. -#[derive(Clone, Debug)] -pub(crate) struct WireLhs { - pub entity: WireEntity, - pub endpoint: Option, - pub target: WireTarget, - pub ops: Vec, - pub views: Vec, -} - -impl WireLhs { - pub(crate) fn with_op(mut self, op: Op) -> Self { - self.ops.push(op); - self - } - - /// `.temporal()` switches a property target to its temporal column. - pub(crate) fn temporal(mut self) -> Option { - match self.target { - WireTarget::Prop(PropertyRef::Property(name)) => { - self.target = WireTarget::Prop(PropertyRef::TemporalProperty(name)); - Some(self) - } - _ => None, - } - } - - /// Finish the lhs with a predicate, producing the wire tree. - pub(crate) fn finish(self, operator: FilterOperator, value: WireValue) -> Option { - let node_leaf = |lhs: &WireLhs| -> Option { - Some(match (&lhs.target, &value) { - (WireTarget::Field(name), WireValue::Field(v)) => { - CompositeNodeFilter::Node(Filter { - field_name: name.to_string(), - field_value: v.clone(), - operator: operator.clone(), - }) - } - (WireTarget::Prop(prop_ref), WireValue::Prop(v)) => { - CompositeNodeFilter::Property(PropertyFilter { - prop_ref: prop_ref.clone(), - prop_value: v.clone(), - operator: operator.clone(), - ops: lhs.ops.clone(), - entity: NodeFilter, - }) - } - (WireTarget::Degree(dir), WireValue::Prop(v)) => { - if !lhs.ops.is_empty() { - return None; - } - CompositeNodeFilter::Degree(DegreeFilter { - direction: *dir, - operator: operator.clone(), - value: v.clone(), - ops: Vec::new(), - }) - } - _ => return None, - }) - }; - - match (self.entity, &self.endpoint) { - (WireEntity::Node, Some(_)) => None, - (WireEntity::Node, None) => { - let leaf = node_leaf(&self)?; - Some(FilterTree::Node(wrap_node_views(leaf, &self.views))) - } - (WireEntity::Edge, Some(endpoint)) => { - let leaf = node_leaf(&self)?; - let ep = match endpoint { - Endpoint::Src => CompositeEdgeFilter::Src(leaf), - Endpoint::Dst => CompositeEdgeFilter::Dst(leaf), - }; - Some(FilterTree::Edge(wrap_edge_views(ep, &self.views))) - } - (WireEntity::Edge, None) => { - let leaf = match (&self.target, &value) { - (WireTarget::Prop(prop_ref), WireValue::Prop(v)) => { - CompositeEdgeFilter::Property(PropertyFilter { - prop_ref: prop_ref.clone(), - prop_value: v.clone(), - operator, - ops: self.ops.clone(), - entity: EdgeFilter, - }) - } - _ => return None, - }; - Some(FilterTree::Edge(wrap_edge_views(leaf, &self.views))) - } - (WireEntity::ExplodedEdge, endpoint) => { - let leaf = match (endpoint, &self.target, &value) { - (Some(ep), _, _) => { - let n = node_leaf(&self)?; - match ep { - Endpoint::Src => CompositeExplodedEdgeFilter::Src(n), - Endpoint::Dst => CompositeExplodedEdgeFilter::Dst(n), - } - } - (None, WireTarget::Prop(prop_ref), WireValue::Prop(v)) => { - CompositeExplodedEdgeFilter::Property(PropertyFilter { - prop_ref: prop_ref.clone(), - prop_value: v.clone(), - operator, - ops: self.ops.clone(), - entity: ExplodedEdgeFilter, - }) - } - _ => return None, - }; - Some(FilterTree::ExplodedEdge(wrap_exploded_views( - leaf, - &self.views, - ))) - } - } - } -} - -/// The rhs of a wire predicate. -#[derive(Clone, Debug)] -pub(crate) enum WireValue { - Field(FieldFilterValue), - Prop(PropertyFilterValue), -} - -macro_rules! wrap_views_fn { - ($name:ident, $composite:ident) => { - pub(crate) fn $name(leaf: $composite, views: &[WireView]) -> $composite { - // Views are recorded in application order and each later view - // wraps outside the previous one: the outermost wrapper is the - // last view applied, matching the local expression engine. - views.iter().fold(leaf, |acc, view| match view { - WireView::Window(start, end) => { - $composite::Windowed(Box::new(Windowed::new(*start, *end, acc))) - } - WireView::Latest => $composite::Latest(Box::new(Latest::new(acc))), - WireView::SnapshotAt(t) => { - $composite::SnapshotAt(Box::new(SnapshotAt::new(*t, acc))) - } - WireView::SnapshotLatest => { - $composite::SnapshotLatest(Box::new(SnapshotLatest::new(acc))) - } - WireView::Layers(names) => { - $composite::Layered(Box::new(Layered::from_layers(names.clone(), acc))) - } - }) - } - }; -} - -wrap_views_fn!(wrap_node_views, CompositeNodeFilter); -wrap_views_fn!(wrap_edge_views, CompositeEdgeFilter); -wrap_views_fn!(wrap_exploded_views, CompositeExplodedEdgeFilter); From e2f4e20ac40b3d3683d647051069fc553680da23 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 17 Sep 2026 16:08:17 +0000 Subject: [PATCH 120/155] chore: apply tidy-public auto-fixes --- docs/reference/graphql/graphql_API.md | 3061 +++++------------ python/python/raphtory/filter/__init__.pyi | 88 +- python/python/raphtory/graph_gen/__init__.pyi | 12 +- .../python/raphtory/graph_loader/__init__.pyi | 11 +- python/python/raphtory/graphql/__init__.pyi | 294 +- python/python/raphtory/iterables/__init__.pyi | 236 +- .../python/raphtory/node_state/__init__.pyi | 331 +- python/python/raphtory/vectors/__init__.pyi | 101 +- .../test_filters/test_exploded_edge_filter.py | 24 +- .../test_filters/test_node_property_filter.py | 20 +- .../test_graphdb/test_graphdb.py | 1 - .../test_filters/test_node_filter_gql.py | 37 +- raphtory-graphql/schema.graphql | 1 + 13 files changed, 1669 insertions(+), 2548 deletions(-) diff --git a/docs/reference/graphql/graphql_API.md b/docs/reference/graphql/graphql_API.md index 4608135352..9574546ecf 100644 --- a/docs/reference/graphql/graphql_API.md +++ b/docs/reference/graphql/graphql_API.md @@ -998,10 +998,10 @@ Returns the in component (all nodes that can reach it following out-edges) of ev filter -GqlFilter +FilterExpr -Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view. +Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view. @@ -1025,10 +1025,10 @@ Returns the out component (all reachable nodes following out-edges) of every nod filter -GqlFilter +FilterExpr -Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view. +Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view. @@ -1061,10 +1061,10 @@ Node id. filter -GqlFilter +FilterExpr -Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view. +Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view. @@ -1088,10 +1088,10 @@ Node id. filter -GqlFilter +FilterExpr -Optional composite filter (node, edge, and graph-view); the algorithm runs on the resulting view. +Optional filter expression (node/edge predicates, graph views, or and/or/not combinations); the algorithm runs on the resulting view. @@ -2813,7 +2813,7 @@ metadata / history are restricted to the matching subset. expr -GqlFilter! +FilterExpr! Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). @@ -3573,7 +3573,7 @@ Contrast with `select`, which applies here and is not carried through. expr -GqlFilter! +FilterExpr! Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). @@ -3608,7 +3608,7 @@ Contrast with `filter`, which persists the scope through subsequent ops. expr -GqlFilter! +FilterExpr! Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). @@ -4424,10 +4424,10 @@ All nodes in this view, optionally narrowed by a filter. select -NodeFilter +FilterExpr -Optional node filter (by name, property, type, etc.). If omitted, every node in the view is returned. +Optional filter expression made of node predicates, graph views, or and/or/not combinations (and is an intersection). Expressions that test edges are rejected. If omitted, every node in the view is returned. @@ -4470,10 +4470,10 @@ All edges in this view, optionally narrowed by a filter. select -EdgeFilter +FilterExpr -Optional edge filter (by property, layer, src/dst, etc.). If omitted, every edge in the view is returned. +Optional filter expression made of edge predicates (including src/dst reads), graph views, or and/or/not combinations (and is an intersection). If omitted, every edge in the view is returned. @@ -4601,10 +4601,10 @@ expression and narrows nodes, edges, and their properties to what matches. expr -GqlFilter +FilterExpr -Optional filter expression: node/edge predicates, graph views (window, layer, ...), or and/or/not combinations of them. `and` is an intersection: each leg is evaluated independently and the results intersect — to evaluate a predicate *inside* a view, scope the predicate itself (e.g. a windowed property condition). If omitted, applies the identity filter. +Optional filter expression made of node/edge predicates, graph views (window, layer, ...), or and/or/not combinations of them. `and` is an intersection, each leg evaluated independently and the results intersected. A `view` leg applies first and the other legs run inside it, like `graph.window(..).filter(expr)`; it must stand alone or in the top-level `and` (not under `or` or `not`). If omitted, applies the identity filter. @@ -7349,7 +7349,7 @@ Contrast with `select`, which applies here and is not carried through. expr -GqlFilter! +FilterExpr! Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). @@ -7370,7 +7370,7 @@ Contrast with `filter`, which persists the scope through subsequent ops. expr -GqlFilter! +FilterExpr! Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). @@ -7906,7 +7906,7 @@ queryable. select -GqlFilter +FilterExpr @@ -7923,7 +7923,7 @@ neighbours remain queryable. select -GqlFilter +FilterExpr @@ -7937,7 +7937,7 @@ Returns all connected edges. select -EdgeFilter +FilterExpr @@ -7951,7 +7951,7 @@ Returns outgoing edges. select -EdgeFilter +FilterExpr @@ -7965,7 +7965,7 @@ Returns incoming edges. select -EdgeFilter +FilterExpr @@ -7979,7 +7979,7 @@ Returns neighbouring nodes. select -NodeFilter +FilterExpr @@ -7993,7 +7993,7 @@ Returns the number of neighbours that have at least one in-going edge to this no select -NodeFilter +FilterExpr @@ -8007,7 +8007,7 @@ Returns the number of neighbours that have at least one out-going edge from this select -NodeFilter +FilterExpr @@ -8017,7 +8017,7 @@ Returns the number of neighbours that have at least one out-going edge from this expr -GqlFilter! +FilterExpr! Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). @@ -9317,7 +9317,7 @@ Contrast with `select`, which applies here and is not carried through. expr -GqlFilter! +FilterExpr! Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). @@ -9352,7 +9352,7 @@ Contrast with `filter`, which persists the scope through subsequent ops. expr -GqlFilter! +FilterExpr! Filter expression: node predicates, graph views, or and/or/not combinations (and = intersection). Expressions that test edges are rejected. @@ -9370,7 +9370,7 @@ Returns the neighbouring nodes of each node in the collection. select -NodeFilter +FilterExpr @@ -9384,7 +9384,7 @@ Returns the in-neighbours of each node in the collection. select -NodeFilter +FilterExpr @@ -9398,7 +9398,7 @@ Returns the out-neighbours of each node in the collection. select -NodeFilter +FilterExpr @@ -9412,7 +9412,7 @@ Returns the incident edges (both directions) of each node in the collection. select -EdgeFilter +FilterExpr @@ -9426,7 +9426,7 @@ Returns the incoming edges of each node in the collection. select -EdgeFilter +FilterExpr @@ -9440,7 +9440,7 @@ Returns the outgoing edges of each node in the collection. select -EdgeFilter +FilterExpr @@ -10014,7 +10014,7 @@ Contrast with `select`, which applies here and is not carried through. expr -GqlFilter! +FilterExpr! Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). @@ -10035,7 +10035,7 @@ Contrast with `filter`, which persists the scope through subsequent ops. expr -GqlFilter! +FilterExpr! Filter expression: node predicates, graph views, or and/or/not combinations (and = intersection). Expressions that test edges are rejected. @@ -10054,7 +10054,7 @@ path (both directions), as a nested `PathFromGraph`. select -NodeFilter +FilterExpr @@ -10069,7 +10069,7 @@ path, as a nested `PathFromGraph`. select -NodeFilter +FilterExpr @@ -10084,7 +10084,7 @@ path, as a nested `PathFromGraph`. select -NodeFilter +FilterExpr @@ -10683,7 +10683,7 @@ Contrast with `select`, which applies here and is not carried through. expr -GqlFilter! +FilterExpr! Filter expression: node/edge predicates, graph views, or and/or/not combinations (and = intersection). @@ -10715,7 +10715,7 @@ Contrast with `filter`, which persists the scope through subsequent ops. expr -GqlFilter! +FilterExpr! Filter expression: node predicates, graph views, or and/or/not combinations (and = intersection). Expressions that test edges are rejected. @@ -10734,7 +10734,7 @@ Returns the neighbouring nodes reachable one further hop from this path select -NodeFilter +FilterExpr @@ -10749,7 +10749,7 @@ flat `PathFromNode`. select -NodeFilter +FilterExpr @@ -10764,7 +10764,7 @@ flat `PathFromNode`. select -NodeFilter +FilterExpr @@ -11804,7 +11804,9 @@ Optional `{start, end}` to restrict matches to edges active in that interval. ## Inputs -### DegreeFilterNew +### Cmp + +Two expressions to compare. @@ -11816,13 +11818,13 @@ Optional `{start, end}` to restrict matches to edges active in that interval. - - + + - - + + @@ -11883,36 +11885,7 @@ Metadata.
directionDegreeDirection!lhsExpr!
wherePropCondition!rhsExpr!
-### EdgeFilter - -GraphQL input type for filtering edges. - -`EdgeFilter` represents a composable boolean expression evaluated -against edges in a graph. Filters can target: - -- edge **endpoints** (source / destination nodes), -- edge **properties** and **metadata**, -- **temporal scope** (windows, snapshots, latest), -- **layer membership**, -- and **structural edge state** (active, valid, deleted, self-loop). - -Filters can be combined recursively using logical operators -(`And`, `Or`, `Not`). - -Examples (GraphQL): -```graphql -{ -edges(filter: { -And: [ -{ IsActive: true }, -{ Property: { name: "weight", gt: 0.5 } } -] -}) { -src -dst -} -} -``` +### EdgeSortBy @@ -11924,245 +11897,207 @@ dst - - + + - - + + - - + + - - + + - - + + - - + + + + +
srcNodeFilterreverseBoolean -Applies a filter to the **source node** of the edge. - -The nested `NodeFilter` is evaluated against the source endpoint. - -Example: -`{ Src: { Name: { contains: "alice" } } }` +Reverse order. Applies to the `time` / `property` keys; the node keys +(`src` / `dst` / `neighbour`) carry their own `reverse` inside the +nested `NodeSortBy` and ignore this flag.
dstNodeFiltersrcNodeSortBy -Applies a filter to the **destination node** of the edge. - -The nested `NodeFilter` is evaluated against the destination endpoint. - -Example: -`{ Dst: { Id: { eq: 42 } } }` +Sort by the source node.
propertyPropertyFilterNewdstNodeSortBy -Filters an edge **property** by name and value. - -Applies to static or temporal properties depending on context. - -Example: -`{ Property: { name: "weight", gt: 0.5 } }` +Sort by the destination node.
metadataPropertyFilterNewneighbourNodeSortBy -Filters an edge **metadata field**. - -Metadata is shared across all temporal versions of an edge. - -Example: -`{ Metadata: { name: "source", eq: "imported" } }` +Sort by the neighbour node: the endpoint that is NOT the node these +edges were traversed from (the destination for a graph-level edge +collection).
temporalPropertyPropertyFilterNewtimeSortByTime -Filters a **temporal edge property**. - -Used when the property value varies over time and must be -evaluated within a temporal context. - -Example: -`{ TemporalProperty: { name: "status", eq: "active" } }` +Time
and[EdgeFilter!]propertyString -Logical **AND** over multiple edge filters. +Property -All nested filters must evaluate to `true`. +
-Example: -`{ And: [ { IsActive: true }, { IsValid: true } ] }` +### EdgeViewCollection - + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + +
FieldTypeDescription
or[EdgeFilter!]defaultLayerBoolean -Logical **OR** over multiple edge filters. - -At least one nested filter must evaluate to `true`. - -Example: -`{ Or: [ { IsDeleted: true }, { IsSelfLoop: true } ] }` +Contains only the default layer.
notEdgeFilterlatestBoolean -Logical **NOT** over a nested edge filter. - -Negates the result of the wrapped filter. - -Example: -`{ Not: { IsDeleted: true } }` +Latest time.
windowEdgeWindowExprsnapshotLatestBoolean -Restricts edge evaluation to a **time window**. - -The window is inclusive of `start` and exclusive of `end`. +Snapshot at latest time.
atEdgeTimeExprsnapshotAtTimeInput -Restricts edge evaluation to a **single point in time**. +Snapshot at specified time.
beforeEdgeTimeExprlayers[String!] -Restricts edge evaluation to times **strictly before** a given time. +List of included layers.
afterEdgeTimeExprexcludeLayers[String!] -Restricts edge evaluation to times **strictly after** a given time. +List of excluded layers.
latestEdgeUnaryExprexcludeLayerString -Evaluates edge predicates against the **latest available state**. +Single excluded layer.
snapshotAtEdgeTimeExprwindowWindow -Evaluates edge predicates against a **snapshot** of the graph -at a specific time. +Window between a start and end time.
snapshotLatestEdgeUnaryExpratTimeInput -Evaluates edge predicates against the **most recent snapshot** -of the graph. +View at a specified time.
layersEdgeLayersExprbeforeTimeInput -Restricts evaluation to edges belonging to one or more **layers**. - -Example: -`{ Layers: { values: ["fire_nation", "air_nomads"] } }` +View before a specified time (end exclusive).
isActiveBooleanafterTimeInput -Matches edges that have at least one event in the current view/window. - -When `true`, only active edges are matched. +View after a specified time (start exclusive).
isValidBooleanshrinkStartTimeInput -Matches edges that are structurally valid (i.e. not deleted) -in the current view/window. +Set the window start to a specified time.
isDeletedBooleanshrinkEndTimeInput -Matches edges that have been deleted in the current view/window. +Set the window end to a specified time.
isSelfLoopBooleanfilterFilterExpr -Matches edges that are **self-loops** -(source node == destination node). +A filter tree; the entity it tests is written in the tree.
-### EdgeLayersExpr - -Restricts edge evaluation to one or more layers and applies a nested `EdgeFilter`. - -Used by `GqlEdgeFilter::Layers`. +### EdgesViewCollection @@ -12174,139 +12109,135 @@ Used by `GqlEdgeFilter::Layers`. - - + + - - + + - -
names[String!]!defaultLayerBoolean -Layer names to include. +Contains only the default layer.
exprEdgeFilter!latestBoolean -Filter evaluated within the layer-restricted view. +Latest time.
+ +snapshotLatest +Boolean + -### EdgeSortBy +Snapshot at latest time. - - - - - - + - - - - + + - - + + - - + + - - + + - - + + - - + + - -
FieldTypeDescription
reverseBooleansnapshotAtTimeInput -Reverse order. Applies to the `time` / `property` keys; the node keys -(`src` / `dst` / `neighbour`) carry their own `reverse` inside the -nested `NodeSortBy` and ignore this flag. +Snapshot at specified time.
srcNodeSortBylayers[String!] -Sort by the source node. +List of included layers.
dstNodeSortByexcludeLayers[String!] -Sort by the destination node. +List of excluded layers.
neighbourNodeSortByexcludeLayerString -Sort by the neighbour node: the endpoint that is NOT the node these -edges were traversed from (the destination for a graph-level edge -collection). +Single excluded layer.
timeSortByTimewindowWindow -Time +Window between a start and end time.
propertyStringatTimeInput -Property +View at a specified time.
+ +before +TimeInput + -### EdgeTimeExpr +View before a specified time (end exclusive). -Restricts edge evaluation to a single time bound and applies a nested `EdgeFilter`. + + + +after +TimeInput + -Used by `At`, `Before`, and `After` edge filters. +View after a specified time (start exclusive). - - + + - - - + + + - - - - + + - - + +
FieldTypeDescriptionshrinkStartTimeInput + +Set the window start to a specified time. + +
timeTimeInput!shrinkEndTimeInput -Reference time for the operation. +Set the window end to a specified time.
exprEdgeFilter!filterFilterExpr -Filter evaluated within the restricted time scope. +A filter tree; the entity it tests is written in the tree.
-### EdgeUnaryExpr - -Applies a unary edge-view operation and then evaluates a nested `EdgeFilter`. - -Used by `Latest` and `SnapshotLatest` edge filters. +### EmbeddingModel @@ -12318,18 +12249,20 @@ Used by `Latest` and `SnapshotLatest` edge filters. - - + +
exprEdgeFilter!openAIOpenAIConfig -Filter evaluated after applying the unary operation. +OpenAI embedding models or compatible providers
-### EdgeViewCollection +### Expr + +A value: what stands on either side of a comparison. @@ -12341,1528 +12274,91 @@ Filter evaluated after applying the unary operation. - - + + - - + + - - + + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
defaultLayerBooleanconstValue -Contains only the default layer. +A literal.
latestBooleanreadRead -Latest time. +A field, degree, property or metadata read from an entity.
snapshotLatestBooleantemporalExpr -Snapshot at latest time. +The full history of a property instead of its latest value.
snapshotAtTimeInput - -Snapshot at specified time. - -sumExpr
layers[String!]avgExpr
minExpr
maxExpr
firstExpr
lastExpr
lenExpr
anyExpr -List of included layers. +The predicate holds if it holds for any element.
excludeLayers[String!]allExpr -List of excluded layers. - -
excludeLayerString - -Single excluded layer. - -
windowWindow - -Window between a start and end time. - -
atTimeInput - -View at a specified time. - -
beforeTimeInput - -View before a specified time (end exclusive). - -
afterTimeInput - -View after a specified time (start exclusive). - -
shrinkStartTimeInput - -Set the window start to a specified time. - -
shrinkEndTimeInput - -Set the window end to a specified time. - -
edgeFilterEdgeFilter - -Edge filter - -
- -### EdgeWindowExpr - -Restricts edge evaluation to a time window and applies a nested `EdgeFilter`. - -Used by `GqlEdgeFilter::Window`. - -The window is inclusive of `start` and exclusive of `end`. - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
startTimeInput! - -Window start time (inclusive). - -
endTimeInput! - -Window end time (exclusive). - -
exprEdgeFilter! - -Filter evaluated within the restricted window. - -
- -### EdgesViewCollection - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
defaultLayerBoolean - -Contains only the default layer. - -
latestBoolean - -Latest time. - -
snapshotLatestBoolean - -Snapshot at latest time. - -
snapshotAtTimeInput - -Snapshot at specified time. - -
layers[String!] - -List of included layers. - -
excludeLayers[String!] - -List of excluded layers. - -
excludeLayerString - -Single excluded layer. - -
windowWindow - -Window between a start and end time. - -
atTimeInput - -View at a specified time. - -
beforeTimeInput - -View before a specified time (end exclusive). - -
afterTimeInput - -View after a specified time (start exclusive). - -
shrinkStartTimeInput - -Set the window start to a specified time. - -
shrinkEndTimeInput - -Set the window end to a specified time. - -
edgeFilterEdgeFilter - -Edge filter - -
- -### EmbeddingModel - - - - - - - - - - - - - - - - -
FieldTypeDescription
openAIOpenAIConfig - -OpenAI embedding models or compatible providers - -
- -### ExplodedEdgeFilter - -GraphQL input type for filtering **exploded edges** — edge views where each -temporal event is an individually addressable edge instance, rather than -one aggregated edge across time. - -Predicates are evaluated **per event**: a property condition keeps the -individual updates that match it (and the edges carrying them), where the -plain `EdgeFilter` evaluates one aggregated value per edge. - -Filters can target edge endpoints, properties/metadata, temporal scope, -layer membership, and structural edge state, and can be combined -recursively with `And`/`Or`/`Not` — mirroring `EdgeFilter`. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
srcNodeFilter - -Applies a filter to the **source node** of the exploded edge. - -
dstNodeFilter - -Applies a filter to the **destination node** of the exploded edge. - -
propertyPropertyFilterNew - -Filters an exploded-edge **property** by name and value, evaluated -per event. - -Example: -`{ Property: { name: "weight", gt: 0.5 } }` - -
metadataPropertyFilterNew - -Filters an exploded-edge **metadata field**. - -Metadata is shared across all temporal versions of an edge. - -
temporalPropertyPropertyFilterNew - -Filters a **temporal exploded-edge property**, evaluated within a -temporal context per event. - -
and[ExplodedEdgeFilter!] - -Logical **AND** over multiple exploded-edge filters. - -
or[ExplodedEdgeFilter!] - -Logical **OR** over multiple exploded-edge filters. - -
notExplodedEdgeFilter - -Logical **NOT** over a nested exploded-edge filter. - -
windowExplodedEdgeWindowExpr - -Restricts exploded-edge evaluation to a **time window** -(inclusive start, exclusive end). - -
atExplodedEdgeTimeExpr - -Restricts exploded-edge evaluation to a **single point in time**. - -
beforeExplodedEdgeTimeExpr - -Restricts exploded-edge evaluation to times **strictly before** a -given time. - -
afterExplodedEdgeTimeExpr - -Restricts exploded-edge evaluation to times **strictly after** a -given time. - -
latestExplodedEdgeUnaryExpr - -Evaluates exploded-edge predicates against the **latest available -state**. - -
snapshotAtExplodedEdgeTimeExpr - -Evaluates exploded-edge predicates against a **snapshot** of the graph -at a specific time. - -
snapshotLatestExplodedEdgeUnaryExpr - -Evaluates exploded-edge predicates against the **most recent -snapshot** of the graph. - -
layersExplodedEdgeLayersExpr - -Restricts evaluation to exploded edges belonging to one or more -**layers**. - -
isActiveBoolean - -Matches exploded edges that have at least one event in the current -view/window. - -
isValidBoolean - -Matches exploded edges that are structurally valid (i.e. not deleted) -in the current view/window. - -
isDeletedBoolean - -Matches exploded edges that have been deleted in the current -view/window. - -
isSelfLoopBoolean - -Matches exploded edges that are **self-loops** -(source node == destination node). - -
- -### ExplodedEdgeLayersExpr - -Restricts exploded-edge evaluation to one or more layers and applies a -nested `ExplodedEdgeFilter`. - -Used by `GqlExplodedEdgeFilter::Layers`. - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
names[String!]! - -Layer names to include. - -
exprExplodedEdgeFilter! - -Filter evaluated within the layer-restricted view. - -
- -### ExplodedEdgeTimeExpr - -Restricts exploded-edge evaluation to a single time bound and applies a -nested `ExplodedEdgeFilter`. - -Used by `At`, `Before`, `After`, and `SnapshotAt` exploded-edge filters. - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
timeTimeInput! - -Reference time for the operation. - -
exprExplodedEdgeFilter! - -Filter evaluated within the restricted time scope. - -
- -### ExplodedEdgeUnaryExpr - -Applies a unary edge-view operation and then evaluates a nested -`ExplodedEdgeFilter`. - -Used by `Latest` and `SnapshotLatest` exploded-edge filters. - - - - - - - - - - - - - - - - -
FieldTypeDescription
exprExplodedEdgeFilter! - -Filter evaluated after applying the unary operation. - -
- -### ExplodedEdgeWindowExpr - -Restricts exploded-edge evaluation to a time window and applies a nested -`ExplodedEdgeFilter`. - -Used by `GqlExplodedEdgeFilter::Window`. - -The window is inclusive of `start` and exclusive of `end`. - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
startTimeInput! - -Window start time (inclusive). - -
endTimeInput! - -Window end time (exclusive). - -
exprExplodedEdgeFilter! - -Filter evaluated within the restricted window. - -
- -### FuzzySearchExpr - -Fuzzy string match: passes when the candidate is within `levenshteinDistance` -edits of `value` (optionally also matching by prefix). Mirrors the local -`fuzzy_search(value, levenshtein_distance, prefix_match)` builder. - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
valueString! - -The string to match against. - -
levenshteinDistanceInt! - -Maximum Levenshtein edit distance for a match. - -
prefixMatchBoolean! - -Whether a prefix match within the distance also passes. - -
- -### GqlFilter - -A general filter expression — a node filter (`node`), an edge filter (`edge`), a graph/view -filter (`graph`, e.g. a layer or window restriction), or an `and`/`or` combination of these -(which may mix kinds). Used where an operation accepts any filter, such as scoping a component -walk. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
nodeNodeFilter - -Filter by node properties, fields, or temporal state. - -
edgeEdgeFilter - -Filter by edge properties, source/destination, or temporal state. -(Persisted filters may use the legacy `edge` key.) - -
explodedEdgeExplodedEdgeFilter - -Filter exploded edges — per-event edge instances — by properties, -endpoints, or temporal state, evaluated per event. - -
graphGraphFilter - -Apply a graph-level view (window, snapshot, layer restriction, …). - -
and[GqlFilter!] - -All sub-filters must pass (intersection). - -
or[GqlFilter!] - -At least one sub-filter must pass (union). -Cross-type sub-filters (e.g. `node` and `edge` together) produce a -proper graph union: a node is visible if it matches the node filter or -has a visible edge, and an edge is visible if it matches the edge -filter or both its endpoints are visible. - -
notGqlFilter - -Inverts the nested filter. - -
windowGraphWindowExpr - -Restrict evaluation to a time window (inclusive start, exclusive end). - -
atGraphTimeExpr - -Restrict evaluation to a single point in time. - -
beforeGraphTimeExpr - -Restrict evaluation to times strictly before the given time. - -
afterGraphTimeExpr - -Restrict evaluation to times strictly after the given time. - -
latestGraphUnaryExpr - -Evaluate against the latest available state. - -
snapshotAtGraphTimeExpr - -Evaluate against a snapshot of the graph at a given time. - -
snapshotLatestGraphUnaryExpr - -Evaluate against the most recent snapshot of the graph. - -
layersGraphLayersExpr - -Restrict evaluation to one or more layers. - -
- -### GraphFilter - -GraphQL input type for restricting a graph view. - -`GraphFilter` controls the **evaluation scope** for subsequent node/edge filters: -- time windows (`Window`) -- time points (`At`) -- open-ended ranges (`Before`, `After`) -- latest evaluation (`Latest`) -- snapshots (`SnapshotAt`, `SnapshotLatest`) -- layer membership (`Layers`) - -These filters can be nested via the `expr` field on the corresponding -`*Expr` input objects to form pipelines. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
windowGraphWindowExpr - -Restrict evaluation to a time window (inclusive start, exclusive end). - -
atGraphTimeExpr - -Restrict evaluation to a single point in time. - -
beforeGraphTimeExpr - -Restrict evaluation to times strictly before the given time. - -
afterGraphTimeExpr - -Restrict evaluation to times strictly after the given time. - -
latestGraphUnaryExpr - -Evaluate against the latest available state. - -
snapshotAtGraphTimeExpr - -Evaluate against a snapshot of the graph at a given time. - -
snapshotLatestGraphUnaryExpr - -Evaluate against the most recent snapshot of the graph. - -
layersGraphLayersExpr - -Restrict evaluation to one or more layers. - -
- -### GraphLayersExpr - -Graph view restriction by layer membership, optionally chaining another `GraphFilter`. - -Used by `GqlGraphFilter::Layers`. - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
names[String!]! - -Layer names to include. - -
exprGraphFilter - -Optional nested filter applied after the layer restriction. - -
- -### GraphTimeExpr - -Graph view restriction to a single time bound, optionally chaining another `GraphFilter`. - -Used by `At`, `Before`, and `After` graph filters. - -Example: -`{ At: { time: 5, expr: { Layers: { names: ["L1"] } } } }` - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
timeTimeInput! - -Reference time for the operation. - -
exprGraphFilter - -Optional nested filter applied after the time restriction. - -
- -### GraphUnaryExpr - -Graph view restriction that takes only a nested expression. - -Used for unary view operations like `Latest` and `SnapshotLatest`. - - - - - - - - - - - - - - - - -
FieldTypeDescription
exprGraphFilter - -Optional nested filter applied after the unary operation. - -
- -### GraphViewCollection - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
defaultLayerBoolean - -Contains only the default layer. - -
layers[String!] - -List of included layers. - -
excludeLayers[String!] - -List of excluded layers. - -
excludeLayerString - -Single excluded layer. - -
subgraph[NodeId!] - -Subgraph nodes. - -
subgraphNodeTypes[String!] - -Subgraph node types. - -
excludeNodes[NodeId!] - -List of excluded nodes. - -
validBoolean - -Valid state. - -
windowWindow - -Window between a start and end time. - -
atTimeInput - -View at a specified time. - -
latestBoolean - -View at the latest time. - -
snapshotAtTimeInput - -Snapshot at specified time. - -
snapshotLatestBoolean - -Snapshot at latest time. - -
beforeTimeInput - -View before a specified time (end exclusive). - -
afterTimeInput - -View after a specified time (start exclusive). - -
shrinkStartTimeInput - -Set the window start to a specified time. - -
shrinkEndTimeInput - -Set the window end to a specified time. - -
nodeFilterNodeFilter - -Node filter. - -
edgeFilterEdgeFilter - -Edge filter. - -
- -### GraphWindowExpr - -Graph view restriction to a time window, optionally chaining another `GraphFilter`. - -Used by `GqlGraphFilter::Window`. - -- `start` and `end` define the window (inclusive start, exclusive end). -- `expr` optionally nests another graph filter to apply *within* this window. - -Example (GraphQL): -```graphql -{ Window: { start: 0, end: 10, expr: { Layers: { names: ["A"] } } } } -``` - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
startTimeInput! - -Window start time (inclusive). - -
endTimeInput! - -Window end time (exclusive). - -
exprGraphFilter - -Optional nested filter applied after the window restriction. - -
- -### InputEdge - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
srcNodeId! - -Source node id (string or non-negative integer). - -
dstNodeId! - -Destination node id (string or non-negative integer). - -
- -### MetaGraphCondition - -One condition on a graph, testing either a built-in attribute or a metadata -key. Set exactly one of `field` / `metadataKey`, as for `MetaGraphSort`. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
fieldMetaGraphField - -Built-in attribute to test. - -
metadataKeyString - -Metadata key to test. - -
wherePropCondition! - -Condition applied to the value, using the same grammar as property -filters elsewhere in the schema. Names, paths and metadata strings are -tested as strings; counts and timestamps as integers. - -
matchesIfAbsentBoolean - -Overrides the result when the graph has no value for the target. - -Set this when absence should read as a default rather than a non-match — -e.g. treating a graph with no `archived` key as not archived, so -`archived == false` still selects it. Applies to `field` too, since a -graph may have no name. - -Left unset, the condition itself decides, which is what you want for -conditions already about absence (`isNone`, `ne`). - -
caseSensitiveBoolean - -Compare strings case-sensitively (defaults to false, matching -`NamespaceFilter`). Only affects string comparisons: numbers, booleans -and the string-encoded temporal/decimal values are unaffected. - -
- -### MetaGraphFilter - -Narrows a namespace's graph listing. - -Composes the same way as the graph/node/edge filters: leaves test one -attribute or metadata key, and `and` / `or` / `not` combine them. - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
conditionMetaGraphCondition - -Condition on a built-in attribute or a metadata key. - -
and[MetaGraphFilter!] - -Logical AND over nested filters. - -
or[MetaGraphFilter!] - -Logical OR over nested filters. - -
notMetaGraphFilter - -Logical NOT over a nested filter. +The predicate holds if it holds for every element.
-### MetaGraphSort +### FilterExpr -One sort key for a graph listing. Set exactly one of `field` or -`metadataKey`. Keys are applied in order, each breaking ties left by the -previous one; graphs missing the sort value sort last. +The filter itself: a yes/no over an entity. @@ -13874,51 +12370,131 @@ previous one; graphs missing the sort value sort last. - - - + + + - - - + + + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
fieldMetaGraphField - -Sort on a built-in attribute. - -eqCmp
metadataKeyString - -Sort on the value of this metadata key. - -neCmp
valueOrder[Value!] - -Explicit ordering for the values of `metadataKey`, lowest first. Values -not listed sort after every listed one, among themselves by their natural -order. Use for keys holding a small vocabulary whose meaningful order -isn't alphabetical. - -ltCmp
reverseBooleanleCmp
gtCmp
geCmp
startsWithCmp
endsWithCmp
containsCmp
notContainsCmp
fuzzySearchFuzzyCmp
isSomeExpr
isNoneExpr
isInMembership
isNotInMembership
isActiveScope
isValidScope
isDeletedScope
isSelfLoopScope
view[ViewOp!] -Reverse this key's direction (default ascending). +A graph-level view with no predicate: the result is the view.
and[FilterExpr!]
or[FilterExpr!]
notFilterExpr
-### NamespaceFilter +### FuzzyCmp -Narrows a namespace's sub-namespace listing. Sub-namespaces carry no metadata -of their own, so only their path can be matched. +A fuzzy string match: `lhs` is within `levenshteinDistance` edits of +`rhs`, optionally matching by prefix. @@ -13930,29 +12506,33 @@ of their own, so only their path can be matched. - - - + + + - - - + + + + + + + + + + + + +
pathContainsString - -Substring match against the namespace's path. - -lhsExpr!
caseSensitiveBoolean - -Match `pathContains` case-sensitively (defaults to false). - -rhsExpr!
levenshteinDistanceInt!
prefixMatchBoolean!
-### NamespaceSort +### FuzzySearchExpr -One sort key for a sub-namespace listing. +Fuzzy string match: passes when the candidate is within `levenshteinDistance` +edits of `value` (optionally also matching by prefix). Mirrors the local +`fuzzy_search(value, levenshtein_distance, prefix_match)` builder. @@ -13964,54 +12544,36 @@ One sort key for a sub-namespace listing. - - + + - -
reverseBooleanvalueString! -Reverse the path ordering (default ascending). +The string to match against.
- -### NamespacedItemFilter - -Narrows a namespace's heterogeneous `items` listing. Each half applies to the -matching kind of item; an item whose kind has no filter is kept. To list only -one kind, query `graphs` or `children` instead. - - - - - - - - - - - - + + - - + +
FieldTypeDescription
graphsMetaGraphFilterlevenshteinDistanceInt! -Applied to the graphs in the collection. +Maximum Levenshtein edit distance for a match.
namespacesNamespaceFilterprefixMatchBoolean! -Applied to the sub-namespaces in the collection. +Whether a prefix match within the distance also passes.
-### NodeAddition +### GraphViewCollection @@ -14023,197 +12585,171 @@ Applied to the sub-namespaces in the collection. - - + + - - + + - - + + - - + + - - + + - -
nameNodeId!defaultLayerBoolean -Node id (string or non-negative integer). +Contains only the default layer.
nodeTypeStringlayers[String!] -Node type. +List of included layers.
metadata[PropertyInput!]excludeLayers[String!] -Metadata. +List of excluded layers.
updates[TemporalPropertyInput!]excludeLayerString -Updates. +Single excluded layer.
layerStringsubgraph[NodeId!] -Layer. +Subgraph nodes.
- -### NodeFieldCondition - -Boolean expression over a built-in node field (ID, name, or type). - -This is used by `NodeFieldWhere.where_` when filtering a specific -built-in field. - -Supports comparisons, string predicates, and set membership. -(Presence checks and aggregations are handled via property filters instead.) - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + +
FieldTypeDescription
eqValuesubgraphNodeTypes[String!] -Equality. +Subgraph node types.
neValueexcludeNodes[NodeId!] -Inequality. +List of excluded nodes.
gtValuevalidBoolean -Greater-than. +Valid state.
geValuewindowWindow -Greater-than-or-equal. +Window between a start and end time.
ltValueatTimeInput -Less-than. +View at a specified time.
leValuelatestBoolean -Less-than-or-equal. +View at the latest time.
startsWithValuesnapshotAtTimeInput -String prefix match. +Snapshot at specified time.
endsWithValuesnapshotLatestBoolean -String suffix match. +Snapshot at latest time.
containsValuebeforeTimeInput -Substring match. +View before a specified time (end exclusive).
notContainsValueafterTimeInput -Negated substring match. +View after a specified time (start exclusive).
fuzzySearchFuzzySearchExprshrinkStartTimeInput -Fuzzy string match (Levenshtein distance, optional prefix matching). +Set the window start to a specified time.
isInValueshrinkEndTimeInput -Set membership. +Set the window end to a specified time.
isNotInValuefilterFilterExpr -Negated set membership. +A filter tree; the entity it tests is written in the tree.
-### NodeFieldWhere - -A condition on one specific built-in field — the payload of the per-field -filter variants (`{ id: { where: ... } }`, `{ name: { where: ... } }`, -`{ nodeType: { where: ... } }`). +### InputEdge @@ -14225,35 +12761,57 @@ filter variants (`{ id: { where: ... } }`, `{ name: { where: ... } }`, - - + + + + + + +
whereNodeFieldCondition!srcNodeId! -Condition applied to the field. +Source node id (string or non-negative integer). + +
dstNodeId! -Exposed as `where` in GraphQL. +Destination node id (string or non-negative integer).
-### NodeFilter +### Membership -GraphQL input type for filtering nodes. +A membership test. `values` is a list; a policy may also leave a single +placeholder here (`{"var": …}`) that resolves to the list per caller. -`NodeFilter` represents a composable boolean expression evaluated -against nodes in a graph. Filters can target: + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
exprExpr!
valuesValue!
-- built-in node fields (`Id` / `Name` / `NodeType`), -- node properties and metadata, -- temporal properties, -- temporal scope (windows, snapshots, latest), -- and layer membership, -- plus node state predicates (e.g. `IsActive`). +### MetaGraphCondition -Filters can be combined recursively using logical operators -(`And`, `Or`, `Not`). +One condition on a graph, testing either a built-in attribute or a metadata +key. Set exactly one of `field` / `metadataKey`, as for `MetaGraphSort`. @@ -14265,191 +12823,242 @@ Filters can be combined recursively using logical operators - - + + - - + + - - + + - - + + - - + + - - - - +
idNodeFieldWherefieldMetaGraphField -Filters the node id: `{ id: { where: ... } }`. +Built-in attribute to test.
nameNodeFieldWheremetadataKeyString -Filters the node name: `{ name: { where: ... } }`. +Metadata key to test.
nodeTypeNodeFieldWherewherePropCondition! -Filters the node type: `{ nodeType: { where: ... } }`. +Condition applied to the value, using the same grammar as property +filters elsewhere in the schema. Names, paths and metadata strings are +tested as strings; counts and timestamps as integers.
propertyPropertyFilterNewmatchesIfAbsentBoolean -Filters a node property by name and condition. +Overrides the result when the graph has no value for the target. + +Set this when absence should read as a default rather than a non-match — +e.g. treating a graph with no `archived` key as not archived, so +`archived == false` still selects it. Applies to `field` too, since a +graph may have no name. + +Left unset, the condition itself decides, which is what you want for +conditions already about absence (`isNone`, `ne`).
degreeDegreeFilterNewcaseSensitiveBoolean -Filters a node's degree (in, out, or total) by a condition. +Compare strings case-sensitively (defaults to false, matching +`NamespaceFilter`). Only affects string comparisons: numbers, booleans +and the string-encoded temporal/decimal values are unaffected.
metadataPropertyFilterNew +
+ +### MetaGraphFilter -Filters a node metadata field by name and condition. +Narrows a namespace's graph listing. -Metadata is shared across all temporal versions of a node. +Composes like a filter expression: leaves test one +attribute or metadata key, and `and` / `or` / `not` combine them. - + + + + + + + + - - + + - - + + - - + + - - + + - - - - +
FieldTypeDescription
temporalPropertyPropertyFilterNewconditionMetaGraphCondition -Filters a temporal node property by name and condition. - -Used when the property value varies over time and must be evaluated -within a temporal context. +Condition on a built-in attribute or a metadata key.
and[NodeFilter!]and[MetaGraphFilter!] -Logical AND over multiple node filters. +Logical AND over nested filters.
or[NodeFilter!]or[MetaGraphFilter!] -Logical OR over multiple node filters. +Logical OR over nested filters.
notNodeFilternotMetaGraphFilter -Logical NOT over a nested node filter. +Logical NOT over a nested filter.
windowNodeWindowExpr +
-Restricts evaluation to a time window (inclusive start, exclusive end). +### MetaGraphSort - +One sort key for a graph listing. Set exactly one of `field` or +`metadataKey`. Keys are applied in order, each breaking ties left by the +previous one; graphs missing the sort value sort last. + + + + + + + + + - - + + - - + + - - + + - - + + - - - - +
FieldTypeDescription
atNodeTimeExprfieldMetaGraphField -Restricts evaluation to a single point in time. +Sort on a built-in attribute.
beforeNodeTimeExprmetadataKeyString -Restricts evaluation to times strictly before the given time. +Sort on the value of this metadata key.
afterNodeTimeExprvalueOrder[Value!] -Restricts evaluation to times strictly after the given time. +Explicit ordering for the values of `metadataKey`, lowest first. Values +not listed sort after every listed one, among themselves by their natural +order. Use for keys holding a small vocabulary whose meaningful order +isn't alphabetical.
latestNodeUnaryExprreverseBoolean -Evaluates predicates against the latest available node state. +Reverse this key's direction (default ascending).
snapshotAtNodeTimeExpr +
-Evaluates predicates against a snapshot of the graph at a given time. +### NamespaceFilter - +Narrows a namespace's sub-namespace listing. Sub-namespaces carry no metadata +of their own, so only their path can be matched. + + + + + + + + + - - + + - - + + + +
FieldTypeDescription
snapshotLatestNodeUnaryExprpathContainsString -Evaluates predicates against the most recent snapshot of the graph. +Substring match against the namespace's path.
layersNodeLayersExprcaseSensitiveBoolean -Restricts evaluation to nodes belonging to one or more layers. +Match `pathContains` case-sensitively (defaults to false).
+ +### NamespaceSort + +One sort key for a sub-namespace listing. + + + + + + + + + + - +
FieldTypeDescription
isActivereverse Boolean -Matches nodes that have at least one event in the current view/window. - -When `true`, only active nodes are matched. +Reverse the path ordering (default ascending).
-### NodeLayersExpr - -Restricts node evaluation to one or more layers and applies a nested `NodeFilter`. +### NamespacedItemFilter -Used by `GqlNodeFilter::Layers`. +Narrows a namespace's heterogeneous `items` listing. Each half applies to the +matching kind of item; an item whose kind has no filter is kept. To list only +one kind, query `graphs` or `children` instead. @@ -14461,27 +13070,27 @@ Used by `GqlNodeFilter::Layers`. - - + + - - + +
names[String!]!graphsMetaGraphFilter -Layer names to include. +Applied to the graphs in the collection.
exprNodeFilter!namespacesNamespaceFilter -Filter evaluated within the layer-restricted view. +Applied to the sub-namespaces in the collection.
-### NodeSortBy +### NodeAddition @@ -14493,67 +13102,54 @@ Filter evaluated within the layer-restricted view. - - - - - - - + + - - + + - - + + - - + + - +
reverseBoolean - -Reverse order - -
idBooleannameNodeId! -Unique Id +Node id (string or non-negative integer).
nameBooleannodeTypeString -Node name +Node type.
typeBooleanmetadata[PropertyInput!] -Node type. Untyped nodes sort first (before any named type). +Metadata.
timeSortByTimeupdates[TemporalPropertyInput!] -Time +Updates.
propertylayer String -Property +Layer.
-### NodeTimeExpr - -Restricts node evaluation to a single time bound and applies a nested `NodeFilter`. - -Used by `At`, `Before`, and `After` node filters. +### NodeSortBy @@ -14565,47 +13161,56 @@ Used by `At`, `Before`, and `After` node filters. - - + + - - + + - -
timeTimeInput!reverseBoolean -Reference time for the operation. +Reverse order
exprNodeFilter!idBoolean -Filter evaluated within the restricted time scope. +Unique Id
+ +name +Boolean + -### NodeUnaryExpr +Node name -Applies a unary node-view operation and then evaluates a nested `NodeFilter`. + + + +type +Boolean + -Used by `Latest` and `SnapshotLatest` node filters. +Node type. Untyped nodes sort first (before any named type). - - + + - - - + + + - - - - + + @@ -14741,58 +13346,11 @@ Set the window end to a specified time. - - - - - -
FieldTypeDescriptiontimeSortByTime + +Time + +
exprNodeFilter!propertyString -Filter evaluated after applying the unary operation. +Property
nodeFilterNodeFilter - -Node filter. - -
- -### NodeWindowExpr - -Restricts node evaluation to a time window and applies a nested `NodeFilter`. - -Used by `GqlNodeFilter::Window`. - -The window is inclusive of `start` and exclusive of `end`. - - - - - - - - - - - - - - - - - - - - - - - + + @@ -14928,11 +13486,11 @@ Set the window end to a specified time. - - + + @@ -15145,8 +13703,8 @@ Set the window end to a specified time. Boolean expression over a property value. -`PropCondition` is used inside `PropertyFilterNew.where` to describe -how a property’s value should be matched. +`PropCondition` is the `where` of a namespace metagraph filter: how one +graph-level metadata value, or a graph field, should be matched. It supports: - comparisons (`Eq`, `Gt`, `Le`, …), @@ -15420,21 +13978,7 @@ Applies the nested condition to the **length** of a list-like property.
FieldTypeDescription
startTimeInput! - -Window start time (inclusive). - -
endTimeInput! - -Window end time (exclusive). - -
exprNodeFilter!filterFilterExpr -Filter evaluated within the restricted window. +A filter tree; the entity it tests is written in the tree.
nodeFilterNodeFilterfilterFilterExpr -Node filter. +A filter tree; the entity it tests is written in the tree.
-### PropertyFilterNew - -Filters an entity property or metadata field by name and condition. - -This input is used by both node and edge filters when targeting -a specific property key (or metadata key) and applying a `PropCondition`. - -Fields: -- `name`: The property key to query. -- `where_`: The condition to apply to that property’s value. - -Example (GraphQL): -```graphql -{ Property: { name: "weight", where: { Gt: 0.5 } } } -``` +### PropertyInput @@ -15446,29 +13990,29 @@ Example (GraphQL): - + - - + +
namekey String! -Property (or metadata) key. +Key.
wherePropCondition!valueValue! -Condition applied to the property value. - -Exposed as `where` in GraphQL. +Value.
-### PropertyInput +### Read + +A value read from an entity. @@ -15480,22 +14024,56 @@ Exposed as `where` in GraphQL. - - - + + + - - - + + + + + + + + + + + + + + +
keyString! - -Key. - -entityEntity!
valueValue! +views[ViewOp!]
endpointEndpoint
targetTarget!
-Value. +### Scope - +Where a value is read: the entity, the views to read it through, and for an +edge optionally one of its endpoint nodes. + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
entityEntity!
views[ViewOp!]
endpointEndpoint
@@ -15543,6 +14121,58 @@ Infect this fraction of the nodes, chosen at random. +### Target + +What a read selects on its entity. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
fieldNodeFieldName + +A built-in node field. + +
degreeDegreeDirection + +A node degree in a direction. + +
propertyString + +A property, by name. + +
metadataString + +A metadata entry, by name. + +
+ ### Template @@ -15833,6 +14463,62 @@ Exclusive upper bound of the search window.
+### ViewOp + +One view restriction, applied in list order. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
windowWindow
atTimeInput
afterTimeInput
beforeTimeInput
latestBoolean
snapshotAtTimeInput
snapshotLatestBoolean
layers[String!]
+ ### Window @@ -15954,19 +14640,7 @@ Alignment unit used to align window boundaries. ### DegreeDirection -Filters nodes by computed degree with a directional scope. - -`DegreeFilterNew` lets callers filter on: -- inbound degree (`IN`), -- outbound degree (`OUT`), -- or total degree (`BOTH`). - -The selected degree is compared using the `where` condition. - -Example (GraphQL): -```graphql -{ Degree: { direction: BOTH, where: { Gt: 10 } } } -``` +The direction a node degree counts.
@@ -16018,6 +14692,56 @@ Edge direction to follow during traversal.
+### Endpoint + +Which end of an edge a read looks at. + + + + + + + + + + + + + + + + + + +
ValueDescription
SRC
DST
+ +### Entity + +The kind of thing a filter tests: a node, an edge, or one edge update. + + + + + + + + + + + + + + + + + + + + + + +
ValueDescription
NODE
EDGE
EXPLODED_EDGE
+ ### GraphType @@ -16112,6 +14836,33 @@ Number of edges.
+### NodeFieldName + +A built-in node field. + + + + + + + + + + + + + + + + + + + + + + +
ValueDescription
ID
NAME
NODE_TYPE
+ ### SortByTime diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index d889a5a2d6..de0dbd9af0 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -10,7 +10,49 @@ from __future__ import annotations ############################################################################### from typing import * -from raphtory import GraphView, PersistentGraph, _GraphEncoder, Nodes, PathFromNode, PathFromGraph, MutableNode, Edges, NestedEdges, MutableEdge, SortByTime, NodeSortBy, EdgeSortBy, Properties, PyPropValueList, PropType, Metadata, MetadataView, TemporalProperties, PropertiesView, TemporalProperty, EventTime, OptionalEventTime, History, HistoryTimestamp, HistoryDateTime, HistoryEventId, Intervals, WindowSet, Prop, version, graphql, algorithms, graph_loader, graph_gen, vectors, node_state, filter, iterables, nullmodels, plottingutils +from raphtory import ( + GraphView, + PersistentGraph, + _GraphEncoder, + Nodes, + PathFromNode, + PathFromGraph, + MutableNode, + Edges, + NestedEdges, + MutableEdge, + SortByTime, + NodeSortBy, + EdgeSortBy, + Properties, + PyPropValueList, + PropType, + Metadata, + MetadataView, + TemporalProperties, + PropertiesView, + TemporalProperty, + EventTime, + OptionalEventTime, + History, + HistoryTimestamp, + HistoryDateTime, + HistoryEventId, + Intervals, + WindowSet, + Prop, + version, + graphql, + algorithms, + graph_loader, + graph_gen, + vectors, + node_state, + filter, + iterables, + nullmodels, + plottingutils, +) from raphtory.algorithms import * from raphtory.vectors import * from raphtory.node_state import * @@ -29,8 +71,22 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = ['FilterExpr', 'Expr', 'PropertyExpr', 'Node', 'NodeFilter', 'Edge', 'EdgeFilter', 'EdgeEndpoint', 'ExplodedEdge', 'ExplodedEdgeFilter', 'Graph', 'GraphFilter'] -class FilterExpr(object): +__all__ = [ + "FilterExpr", + "Expr", + "PropertyExpr", + "Node", + "NodeFilter", + "Edge", + "EdgeFilter", + "EdgeEndpoint", + "ExplodedEdge", + "ExplodedEdgeFilter", + "Graph", + "GraphFilter", +] + +class FilterExpr(object): """ A filter as a tree. The same tree runs locally, is sent to a server, and is what `repr` prints, so there is nothing to keep in step. @@ -54,7 +110,7 @@ class FilterExpr(object): def __ror__(self, value): """Return value|self.""" -class Expr(object): +class Expr(object): """ A value expression: a field, degree, property, metadata entry or an aggregate over one. Comparing it to a value or to another expression gives @@ -133,7 +189,9 @@ class Expr(object): filter.Expr: """ - def fuzzy_search(self, other: Prop | filter.Expr, levenshtein_distance: int, prefix_match: bool) -> filter.FilterExpr: + def fuzzy_search( + self, other: Prop | filter.Expr, levenshtein_distance: int, prefix_match: bool + ) -> filter.FilterExpr: """ Performs fuzzy matching against the value's string representation, within a Levenshtein distance and with optional prefix matching. @@ -246,7 +304,7 @@ class Expr(object): filter.Expr: """ -class PropertyExpr(Expr): +class PropertyExpr(Expr): """A property read, which can switch to the property's history with `temporal()`.""" def temporal(self) -> filter.Expr: @@ -258,7 +316,7 @@ class PropertyExpr(Expr): filter.Expr: """ -class Node(object): +class Node(object): """ Entry point for constructing node filter expressions. @@ -476,7 +534,7 @@ class Node(object): filter.NodeFilter: """ -class NodeFilter(object): +class NodeFilter(object): """ A node filter scoped to a view. @@ -678,7 +736,7 @@ class NodeFilter(object): filter.NodeFilter: """ -class Edge(object): +class Edge(object): """ Entry point for constructing edge filter expressions. @@ -868,7 +926,7 @@ class Edge(object): filter.EdgeFilter: """ -class EdgeFilter(object): +class EdgeFilter(object): """ An edge filter scoped to a view. @@ -1044,7 +1102,7 @@ class EdgeFilter(object): filter.EdgeFilter: """ -class EdgeEndpoint(object): +class EdgeEndpoint(object): """ Entry point for filtering an edge endpoint (source or destination). @@ -1104,7 +1162,7 @@ class EdgeEndpoint(object): filter.PropertyExpr: """ -class ExplodedEdge(object): +class ExplodedEdge(object): """ Entry point for constructing exploded-edge filter expressions. @@ -1281,7 +1339,7 @@ class ExplodedEdge(object): filter.ExplodedEdgeFilter: """ -class ExplodedEdgeFilter(object): +class ExplodedEdgeFilter(object): """ An exploded-edge filter scoped to a view. @@ -1448,7 +1506,7 @@ class ExplodedEdgeFilter(object): filter.ExplodedEdgeFilter: """ -class Graph(object): +class Graph(object): """ Entry point for graph-level view filters. @@ -1561,7 +1619,7 @@ class Graph(object): filter.GraphFilter: """ -class GraphFilter(FilterExpr): +class GraphFilter(FilterExpr): """ A graph-level view scope. diff --git a/python/python/raphtory/graph_gen/__init__.pyi b/python/python/raphtory/graph_gen/__init__.pyi index 931f67117f..c4daeb00b3 100644 --- a/python/python/raphtory/graph_gen/__init__.pyi +++ b/python/python/raphtory/graph_gen/__init__.pyi @@ -1,6 +1,7 @@ """ Generate Raphtory graphs from attachment models """ + from __future__ import annotations ############################################################################### @@ -33,8 +34,11 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = ['random_attachment', 'ba_preferential_attachment'] -def random_attachment(g: Graph, nodes_to_add: int, edges_per_step: int, seed: Optional[list[int]] = None) -> None: +__all__ = ["random_attachment", "ba_preferential_attachment"] + +def random_attachment( + g: Graph, nodes_to_add: int, edges_per_step: int, seed: Optional[list[int]] = None +) -> None: """ Generates a graph using the random attachment model @@ -52,7 +56,9 @@ def random_attachment(g: Graph, nodes_to_add: int, edges_per_step: int, seed: Op None: """ -def ba_preferential_attachment(g: Graph, nodes_to_add: int, edges_per_step: int, seed: Optional[list[int]] = None) -> None: +def ba_preferential_attachment( + g: Graph, nodes_to_add: int, edges_per_step: int, seed: Optional[list[int]] = None +) -> None: """ Generates a graph using the preferential attachment model. diff --git a/python/python/raphtory/graph_loader/__init__.pyi b/python/python/raphtory/graph_loader/__init__.pyi index 269bd52748..ed9ab67785 100644 --- a/python/python/raphtory/graph_loader/__init__.pyi +++ b/python/python/raphtory/graph_loader/__init__.pyi @@ -1,6 +1,7 @@ """ Load and save Raphtory graphs from/to file(s) """ + from __future__ import annotations ############################################################################### @@ -33,7 +34,15 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = ['lotr_graph', 'lotr_graph_with_props', 'stable_coin_graph', 'reddit_hyperlink_graph', 'reddit_hyperlink_graph_local', 'karate_club_graph'] +__all__ = [ + "lotr_graph", + "lotr_graph_with_props", + "stable_coin_graph", + "reddit_hyperlink_graph", + "reddit_hyperlink_graph_local", + "karate_club_graph", +] + def lotr_graph() -> Graph: """ Load the Lord of the Rings dataset into a graph. diff --git a/python/python/raphtory/graphql/__init__.pyi b/python/python/raphtory/graphql/__init__.pyi index 5f1031a8cd..80672ff76b 100644 --- a/python/python/raphtory/graphql/__init__.pyi +++ b/python/python/raphtory/graphql/__init__.pyi @@ -29,8 +29,46 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = ['GraphServer', 'RunningGraphServer', 'RaphtoryClient', 'RemoteGraph', 'RemoteEdge', 'RemoteNode', 'RemoteNodes', 'RemotePathFromNode', 'RemotePathFromGraph', 'RemoteEdges', 'RemoteNestedEdges', 'RemoteHistory', 'RemoteHistoryTimestamps', 'RemoteHistoryEventIds', 'RemoteHistoryDateTimes', 'RemoteIntervals', 'RemoteMetadata', 'RemoteProperties', 'RemoteMetadataView', 'RemotePropertiesView', 'RemoteTemporalProperties', 'RemoteTemporalProperty', 'RemoteGraphSchema', 'RemoteNodeSchema', 'RemoteLayerSchema', 'RemoteEdgeSchema', 'RemotePropertySchema', 'RemoteNodeAddition', 'RemoteUpdate', 'RemoteEdgeAddition', 'RemotePermissionError', 'encode_graph', 'decode_graph', 'schema', 'cli', 'has_extension'] -class GraphServer(object): +__all__ = [ + "GraphServer", + "RunningGraphServer", + "RaphtoryClient", + "RemoteGraph", + "RemoteEdge", + "RemoteNode", + "RemoteNodes", + "RemotePathFromNode", + "RemotePathFromGraph", + "RemoteEdges", + "RemoteNestedEdges", + "RemoteHistory", + "RemoteHistoryTimestamps", + "RemoteHistoryEventIds", + "RemoteHistoryDateTimes", + "RemoteIntervals", + "RemoteMetadata", + "RemoteProperties", + "RemoteMetadataView", + "RemotePropertiesView", + "RemoteTemporalProperties", + "RemoteTemporalProperty", + "RemoteGraphSchema", + "RemoteNodeSchema", + "RemoteLayerSchema", + "RemoteEdgeSchema", + "RemotePropertySchema", + "RemoteNodeAddition", + "RemoteUpdate", + "RemoteEdgeAddition", + "RemotePermissionError", + "encode_graph", + "decode_graph", + "schema", + "cli", + "has_extension", +] + +class GraphServer(object): """ A class for defining and running a Raphtory GraphQL server @@ -77,7 +115,12 @@ class GraphServer(object): depends on which extensions the build has. """ - def __new__(cls, work_dir: str | PathLike, config_path: Optional[str | PathLike] = None, config: Optional[dict] = None) -> GraphServer: + def __new__( + cls, + work_dir: str | PathLike, + config_path: Optional[str | PathLike] = None, + config: Optional[dict] = None, + ) -> GraphServer: """Create and return a new object. See help(type) for accurate signature.""" def config_schema(self) -> dict: @@ -101,7 +144,9 @@ class GraphServer(object): None: """ - def start(self, port: Optional[int] = None, timeout_ms: int = 5000) -> RunningGraphServer: + def start( + self, port: Optional[int] = None, timeout_ms: int = 5000 + ) -> RunningGraphServer: """ Start the server and return a handle to it. @@ -116,7 +161,12 @@ class GraphServer(object): RunningGraphServer: The running server """ - def vectorise_all_graphs(self, embeddings: OpenAIEmbeddings, nodes: bool | str = True, edges: bool | str = True) -> None: + def vectorise_all_graphs( + self, + embeddings: OpenAIEmbeddings, + nodes: bool | str = True, + edges: bool | str = True, + ) -> None: """ Vectorise all graphs in the server working directory. @@ -129,7 +179,13 @@ class GraphServer(object): None: """ - def vectorise_graph(self, name: list[str], embeddings: OpenAIEmbeddings, nodes: bool | str = True, edges: bool | str = True) -> None: + def vectorise_graph( + self, + name: list[str], + embeddings: OpenAIEmbeddings, + nodes: bool | str = True, + edges: bool | str = True, + ) -> None: """ Vectorise the graph name in the server working directory. @@ -143,15 +199,11 @@ class GraphServer(object): None: """ -class RunningGraphServer(object): +class RunningGraphServer(object): """A Raphtory server handler that also enables querying the server""" - def __enter__(self): - ... - - def __exit__(self, _exc_type, _exc_val, _exc_tb): - ... - + def __enter__(self): ... + def __exit__(self, _exc_type, _exc_val, _exc_tb): ... def get_client(self) -> RaphtoryClient: """ Get the client for the server. @@ -176,7 +228,7 @@ class RunningGraphServer(object): None: """ -class RaphtoryClient(object): +class RaphtoryClient(object): """ A client for handling GraphQL operations in the context of Raphtory. @@ -232,7 +284,9 @@ class RaphtoryClient(object): None: """ - def new_graph(self, path: str, graph_type: Literal["EVENT", "PERSISTENT"]) -> RemoteGraph: + def new_graph( + self, path: str, graph_type: Literal["EVENT", "PERSISTENT"] + ) -> RemoteGraph: """ Create a new empty Graph on the server at path @@ -245,7 +299,9 @@ class RaphtoryClient(object): """ - def query(self, query: str, variables: Optional[dict[str, Any]] = None) -> dict[str, Any]: + def query( + self, query: str, variables: Optional[dict[str, Any]] = None + ) -> dict[str, Any]: """ Make a GraphQL query against the server. @@ -283,7 +339,9 @@ class RaphtoryClient(object): """ - def send_graph(self, path: str, graph: Graph | PersistentGraph, overwrite: bool = False) -> None: + def send_graph( + self, path: str, graph: Graph | PersistentGraph, overwrite: bool = False + ) -> None: """ Send a graph to the server @@ -325,9 +383,16 @@ class RaphtoryClient(object): RaphtoryClient: a new client using the given token. """ -class RemoteGraph(object): - - def add_edge(self, timestamp: int | str | datetime, src: str | int, dst: str | int, properties: Optional[dict] = None, layer: Optional[str] = None, event_id: Optional[int] = None) -> RemoteEdge: +class RemoteGraph(object): + def add_edge( + self, + timestamp: int | str | datetime, + src: str | int, + dst: str | int, + properties: Optional[dict] = None, + layer: Optional[str] = None, + event_id: Optional[int] = None, + ) -> RemoteEdge: """ Adds a new edge with the given source and destination nodes and properties to the remote graph. @@ -366,7 +431,15 @@ class RemoteGraph(object): None: """ - def add_node(self, timestamp: int | str | datetime, id: str | int, properties: Optional[dict] = None, node_type: Optional[str] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> RemoteNode: + def add_node( + self, + timestamp: int | str | datetime, + id: str | int, + properties: Optional[dict] = None, + node_type: Optional[str] = None, + event_id: Optional[int] = None, + layer: Optional[str] = None, + ) -> RemoteNode: """ Adds a new node with the given id and properties to the remote graph. @@ -394,7 +467,12 @@ class RemoteGraph(object): None: """ - def add_properties(self, timestamp: int | str | datetime, properties: dict, event_id: Optional[int] = None) -> None: + def add_properties( + self, + timestamp: int | str | datetime, + properties: dict, + event_id: Optional[int] = None, + ) -> None: """ Adds temporal properties to the remote graph. @@ -467,7 +545,15 @@ class RemoteGraph(object): int: the number of edge updates. """ - def create_node(self, timestamp: int | str | datetime, id: str | int, properties: Optional[dict] = None, node_type: Optional[str] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> RemoteNode: + def create_node( + self, + timestamp: int | str | datetime, + id: str | int, + properties: Optional[dict] = None, + node_type: Optional[str] = None, + event_id: Optional[int] = None, + layer: Optional[str] = None, + ) -> RemoteNode: """ Create a new node with the given id and properties to the remote graph and fail if the node already exists. @@ -500,7 +586,14 @@ class RemoteGraph(object): RemoteGraph: a new view restricted to the default layer. """ - def delete_edge(self, timestamp: int, src: str | int, dst: str | int, layer: Optional[str] = None, event_id: Optional[int] = None) -> RemoteEdge: + def delete_edge( + self, + timestamp: int, + src: str | int, + dst: str | int, + layer: Optional[str] = None, + event_id: Optional[int] = None, + ) -> RemoteEdge: """ Deletes an edge in the remote graph, given the timestamp, src and dst nodes and layer (optional) @@ -997,7 +1090,9 @@ class RemoteGraph(object): RemoteGraph: a new view restricted to those valid layers. """ - def window(self, start: int | str | datetime, end: int | str | datetime) -> RemoteGraph: + def window( + self, start: int | str | datetime, end: int | str | datetime + ) -> RemoteGraph: """ Restrict the graph to a time window `[start, end)`. @@ -1024,7 +1119,7 @@ class RemoteGraph(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteEdge(object): +class RemoteEdge(object): """ A remote edge reference @@ -1033,7 +1128,9 @@ class RemoteEdge(object): and [RemoteGraph.delete_edge][raphtory.graphql.RemoteGraph.delete_edge]. """ - def add_metadata(self, metadata: dict[str, PropValue], layer: Optional[str] = None) -> None: + def add_metadata( + self, metadata: dict[str, PropValue], layer: Optional[str] = None + ) -> None: """ Add metadata to the edge within the remote graph. This function is used to add metadata to an edge that does not @@ -1047,7 +1144,13 @@ class RemoteEdge(object): None: """ - def add_updates(self, t: int | str | datetime, properties: Optional[dict[str, PropValue]] = None, layer: Optional[str] = None, event_id: Optional[int] = None) -> None: + def add_updates( + self, + t: int | str | datetime, + properties: Optional[dict[str, PropValue]] = None, + layer: Optional[str] = None, + event_id: Optional[int] = None, + ) -> None: """ Add updates to an edge in the remote graph at a specified time. @@ -1106,7 +1209,12 @@ class RemoteEdge(object): RemoteEdge: a new view restricted to the default layer. """ - def delete(self, t: int | str | datetime, layer: Optional[str] = None, event_id: Optional[int] = None) -> None: + def delete( + self, + t: int | str | datetime, + layer: Optional[str] = None, + event_id: Optional[int] = None, + ) -> None: """ Mark the edge as deleted at the specified time. @@ -1466,7 +1574,9 @@ class RemoteEdge(object): there is none. """ - def update_metadata(self, metadata: dict[str, PropValue], layer: Optional[str] = None) -> None: + def update_metadata( + self, metadata: dict[str, PropValue], layer: Optional[str] = None + ) -> None: """ Update metadata of an edge in the remote graph overwriting existing values. This function is used to add properties to an edge that does not @@ -1513,8 +1623,7 @@ class RemoteEdge(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteNode(object): - +class RemoteNode(object): def add_metadata(self, metadata: dict[str, PropValue]) -> None: """ Add metadata to a node in the remote graph. @@ -1528,7 +1637,13 @@ class RemoteNode(object): None: """ - def add_updates(self, t: int | str | datetime, properties: Optional[dict[str, PropValue]] = None, event_id: Optional[int] = None, layer: Optional[str] = None) -> None: + def add_updates( + self, + t: int | str | datetime, + properties: Optional[dict[str, PropValue]] = None, + event_id: Optional[int] = None, + layer: Optional[str] = None, + ) -> None: """ Add updates to a node in the remote graph at a specified time. This function allows for the addition of property updates to a node within the graph. The updates are time-stamped, meaning they are applied at the specified time. @@ -1718,7 +1833,7 @@ class RemoteNode(object): """ @property - def id(self) -> str|int: + def id(self) -> str | int: """ The node's id (as a string, even if the graph uses integer GIDs). Property — attribute access fires one RPC. @@ -2008,7 +2123,7 @@ class RemoteNode(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteNodes(object): +class RemoteNodes(object): """ A handle to a remote collection of nodes. @@ -2471,7 +2586,7 @@ class RemoteNodes(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemotePathFromNode(object): +class RemotePathFromNode(object): """ A handle to a "path from node" collection. @@ -2926,7 +3041,7 @@ class RemotePathFromNode(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemotePathFromGraph(object): +class RemotePathFromGraph(object): """ A handle to a "path from graph" collection. @@ -3388,7 +3503,7 @@ class RemotePathFromGraph(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteEdges(object): +class RemoteEdges(object): """ A handle to a remote collection of edges. @@ -3844,7 +3959,7 @@ class RemoteEdges(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteNestedEdges(object): +class RemoteNestedEdges(object): """ A handle to a nested edges collection. @@ -4297,7 +4412,7 @@ class RemoteNestedEdges(object): Optional[int]: the size of the window, or `None` if the view is unbounded. """ -class RemoteHistory(object): +class RemoteHistory(object): """ A handle to the event history of a remote node or edge. @@ -4399,7 +4514,9 @@ class RemoteHistory(object): OptionalEventTime: the latest event time, or empty. """ - def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[EventTime]: + def page( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[EventTime]: """ A page of events in ascending time order — at most `limit` items, starting `page_index * limit + offset` items in. Both `offset` and @@ -4414,7 +4531,9 @@ class RemoteHistory(object): list[EventTime]: at most `limit` events. """ - def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[EventTime]: + def page_rev( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[EventTime]: """ A page of events in descending time order. Same args as `page()`. Fires one RPC. @@ -4447,7 +4566,7 @@ class RemoteHistory(object): RemoteHistoryTimestamps: the timestamps view of this history. """ -class RemoteHistoryTimestamps(object): +class RemoteHistoryTimestamps(object): """Timestamps view of a `RemoteHistory`. Lists / pages return `list[int]`.""" def __contains__(self, key): @@ -4485,7 +4604,9 @@ class RemoteHistoryTimestamps(object): list[int]: all timestamps in descending time order. """ - def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: + def page( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[int]: """ Fires one RPC. @@ -4498,7 +4619,9 @@ class RemoteHistoryTimestamps(object): list[int]: at most `limit` timestamps, in ascending time order. """ - def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: + def page_rev( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[int]: """ Fires one RPC. @@ -4511,7 +4634,7 @@ class RemoteHistoryTimestamps(object): list[int]: at most `limit` timestamps, in descending time order. """ -class RemoteHistoryEventIds(object): +class RemoteHistoryEventIds(object): """Event-id view of a `RemoteHistory`. Lists / pages return `list[int]`.""" def __contains__(self, key): @@ -4549,7 +4672,9 @@ class RemoteHistoryEventIds(object): list[int]: all event ids in descending time order. """ - def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: + def page( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[int]: """ Fires one RPC. @@ -4562,7 +4687,9 @@ class RemoteHistoryEventIds(object): list[int]: at most `limit` event ids, in ascending time order. """ - def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: + def page_rev( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[int]: """ Fires one RPC. @@ -4575,7 +4702,7 @@ class RemoteHistoryEventIds(object): list[int]: at most `limit` event ids, in descending time order. """ -class RemoteHistoryDateTimes(object): +class RemoteHistoryDateTimes(object): """ Datetime view of a `RemoteHistory`. Lists / pages return `list[datetime]` (UTC), mirroring the local `History.dt`. @@ -4616,7 +4743,9 @@ class RemoteHistoryDateTimes(object): list[datetime]: all datetimes (UTC) in descending time order. """ - def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[datetime]: + def page( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[datetime]: """ Fires one RPC. @@ -4629,7 +4758,9 @@ class RemoteHistoryDateTimes(object): list[datetime]: at most `limit` datetimes (UTC), in ascending time order. """ - def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[datetime]: + def page_rev( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[datetime]: """ Fires one RPC. @@ -4642,7 +4773,7 @@ class RemoteHistoryDateTimes(object): list[datetime]: at most `limit` datetimes (UTC), in descending time order. """ -class RemoteIntervals(object): +class RemoteIntervals(object): """ Intervals view of a `RemoteHistory` — inter-event gaps plus summary stats (`mean`, `median`, `max`, `min`). @@ -4719,7 +4850,9 @@ class RemoteIntervals(object): Optional[int]: the smallest interval, or `None` if fewer than 2 events. """ - def page(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: + def page( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[int]: """ Fires one RPC. @@ -4732,7 +4865,9 @@ class RemoteIntervals(object): list[int]: at most `limit` intervals, in ascending time order. """ - def page_rev(self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None) -> list[int]: + def page_rev( + self, limit: int, offset: Optional[int] = None, page_index: Optional[int] = None + ) -> list[int]: """ Fires one RPC. @@ -4745,7 +4880,7 @@ class RemoteIntervals(object): list[int]: at most `limit` intervals, in descending time order. """ -class RemoteMetadata(object): +class RemoteMetadata(object): """ A handle to the metadata container of a remote graph, node, or edge — the non-temporal properties whose values don't change over the graph's @@ -4818,7 +4953,7 @@ class RemoteMetadata(object): list[PropValue]: the metadata values. """ -class RemoteProperties(object): +class RemoteProperties(object): """ A handle to the full properties container of a remote graph, node, or edge — includes both non-temporal metadata and temporal properties. @@ -4921,7 +5056,7 @@ class RemoteProperties(object): list[PropValue]: the property values. """ -class RemoteMetadataView(object): +class RemoteMetadataView(object): """ A columnar view over the non-temporal metadata of a remote node/edge collection. Every accessor returns one value per member (nested per source @@ -4982,7 +5117,7 @@ class RemoteMetadataView(object): list: one column per key, in key order. """ -class RemotePropertiesView(object): +class RemotePropertiesView(object): """ A columnar view over the properties of a remote node/edge collection (temporal properties yield their most recent value under the current view). @@ -5042,7 +5177,7 @@ class RemotePropertiesView(object): list: one column per key, in key order. """ -class RemoteTemporalProperties(object): +class RemoteTemporalProperties(object): """ A handle to the temporal-only view of a properties container. Each property has a full history over time. @@ -5134,7 +5269,7 @@ class RemoteTemporalProperties(object): list[RemoteTemporalProperty]: the temporal property handles. """ -class RemoteTemporalProperty(object): +class RemoteTemporalProperty(object): """ A handle to a single temporal property — one key with its full history of updates, plus statistical summaries and time-indexed accessors. @@ -5295,7 +5430,7 @@ class RemoteTemporalProperty(object): list[PropValue]: every value the property has taken, in temporal order. """ -class RemoteGraphSchema(object): +class RemoteGraphSchema(object): """ The full schema of a remote graph — the tree of node types, edge layers, and their observed property/metadata fields. @@ -5324,7 +5459,7 @@ class RemoteGraphSchema(object): list[RemoteNodeSchema]: one entry per node type. """ -class RemoteNodeSchema(object): +class RemoteNodeSchema(object): """Schema for nodes of a specific type.""" def __repr__(self): @@ -5357,7 +5492,7 @@ class RemoteNodeSchema(object): str: the node type name. """ -class RemoteLayerSchema(object): +class RemoteLayerSchema(object): """Schema for a single edge layer.""" def __repr__(self): @@ -5381,7 +5516,7 @@ class RemoteLayerSchema(object): str: the layer name. """ -class RemoteEdgeSchema(object): +class RemoteEdgeSchema(object): """Schema for edges between a specific `(src_type, dst_type)` pair.""" def __repr__(self): @@ -5423,7 +5558,7 @@ class RemoteEdgeSchema(object): str: the source node type. """ -class RemotePropertySchema(object): +class RemotePropertySchema(object): """ One property key on a node/edge type, with its observed property type and (for string-valued properties) the set of distinct values seen. @@ -5459,7 +5594,7 @@ class RemotePropertySchema(object): list[str]: the distinct values seen. """ -class RemoteNodeAddition(object): +class RemoteNodeAddition(object): """ Node addition update @@ -5470,10 +5605,16 @@ class RemoteNodeAddition(object): updates (list[RemoteUpdate], optional): the temporal updates """ - def __new__(cls, name: GID, node_type: Optional[str] = None, metadata: Optional[PropInput] = None, updates: Optional[list[RemoteUpdate]] = None) -> RemoteNodeAddition: + def __new__( + cls, + name: GID, + node_type: Optional[str] = None, + metadata: Optional[PropInput] = None, + updates: Optional[list[RemoteUpdate]] = None, + ) -> RemoteNodeAddition: """Create and return a new object. See help(type) for accurate signature.""" -class RemoteUpdate(object): +class RemoteUpdate(object): """ A temporal update @@ -5482,10 +5623,12 @@ class RemoteUpdate(object): properties (PropInput, optional): the properties for the update """ - def __new__(cls, time: TimeInput, properties: Optional[PropInput] = None) -> RemoteUpdate: + def __new__( + cls, time: TimeInput, properties: Optional[PropInput] = None + ) -> RemoteUpdate: """Create and return a new object. See help(type) for accurate signature.""" -class RemoteEdgeAddition(object): +class RemoteEdgeAddition(object): """ An edge update @@ -5497,10 +5640,17 @@ class RemoteEdgeAddition(object): updates (list[RemoteUpdate], optional): the temporal updates for the edge """ - def __new__(cls, src: GID, dst: GID, layer: Optional[str] = None, metadata: Optional[PropInput] = None, updates: Optional[list[RemoteUpdate]] = None) -> RemoteEdgeAddition: + def __new__( + cls, + src: GID, + dst: GID, + layer: Optional[str] = None, + metadata: Optional[PropInput] = None, + updates: Optional[list[RemoteUpdate]] = None, + ) -> RemoteEdgeAddition: """Create and return a new object. See help(type) for accurate signature.""" -class RemotePermissionError(Exception): +class RemotePermissionError(Exception): """Raised when the server denies a request for lack of permission. A denied request is distinct from a missing graph: a forbidden-but-hidden graph is reported as not found, never as this error.""" @property diff --git a/python/python/raphtory/iterables/__init__.pyi b/python/python/raphtory/iterables/__init__.pyi index bb6079cd30..1be45bce63 100644 --- a/python/python/raphtory/iterables/__init__.pyi +++ b/python/python/raphtory/iterables/__init__.pyi @@ -29,9 +29,68 @@ from decimal import Decimal import networkx as nx # type: ignore import pyvis # type: ignore -__all__ = ['NestedUtcDateTimeIterable', 'NestedGIDIterable', 'GIDIterable', 'StringIterable', 'OptionArcStringIterable', 'UsizeIterable', 'OptionI64Iterable', 'NestedOptionArcStringIterable', 'NestedStringIterable', 'NestedOptionI64Iterable', 'NestedI64VecIterable', 'NestedUsizeIterable', 'BoolIterable', 'ArcStringIterable', 'NestedVecUtcDateTimeIterable', 'OptionVecUtcDateTimeIterable', 'GIDGIDIterable', 'NestedGIDGIDIterable', 'NestedBoolIterable', 'U64Iterable', 'OptionUtcDateTimeIterable', 'ArcStringVecIterable', 'NestedArcStringVecIterable', 'NestedEventTimeIterable', 'NestedArcStringIterable', 'NestedOptionEventTimeIterable', 'NestedHistoryIterable', 'EventTimeIterable', 'OptionEventTimeIterable', 'HistoryIterable', 'HistoryTimestampIterable', 'IntervalsIterable', 'HistoryEventIdIterable', 'HistoryDateTimeIterable', 'OptionUsizeIterable', 'ResultOptionUtcDateTimeIterable', 'I64Iterable', 'ResultUtcDateTimeIterable', 'NestedHistoryTimestampIterable', 'NestedIntervalsIterable', 'NestedHistoryEventIdIterable', 'NestedHistoryDateTimeIterable', 'NestedOptionUsizeIterable', 'NestedResultOptionUtcDateTimeIterable', 'NestedI64Iterable', 'NestedResultUtcDateTimeIterable', 'MetadataListList', 'PyNestedPropsIterable', 'PyPropValueListList', 'PyTemporalPropsList', 'PyTemporalPropsListList', 'PyPropHistValueList', 'PyPropHistValueListList', 'PyTemporalPropList', 'PyTemporalPropListList', 'PyPropHistItemsList', 'PyPropHistItemsListList', 'PropIterable'] -class NestedUtcDateTimeIterable(object): - +__all__ = [ + "NestedUtcDateTimeIterable", + "NestedGIDIterable", + "GIDIterable", + "StringIterable", + "OptionArcStringIterable", + "UsizeIterable", + "OptionI64Iterable", + "NestedOptionArcStringIterable", + "NestedStringIterable", + "NestedOptionI64Iterable", + "NestedI64VecIterable", + "NestedUsizeIterable", + "BoolIterable", + "ArcStringIterable", + "NestedVecUtcDateTimeIterable", + "OptionVecUtcDateTimeIterable", + "GIDGIDIterable", + "NestedGIDGIDIterable", + "NestedBoolIterable", + "U64Iterable", + "OptionUtcDateTimeIterable", + "ArcStringVecIterable", + "NestedArcStringVecIterable", + "NestedEventTimeIterable", + "NestedArcStringIterable", + "NestedOptionEventTimeIterable", + "NestedHistoryIterable", + "EventTimeIterable", + "OptionEventTimeIterable", + "HistoryIterable", + "HistoryTimestampIterable", + "IntervalsIterable", + "HistoryEventIdIterable", + "HistoryDateTimeIterable", + "OptionUsizeIterable", + "ResultOptionUtcDateTimeIterable", + "I64Iterable", + "ResultUtcDateTimeIterable", + "NestedHistoryTimestampIterable", + "NestedIntervalsIterable", + "NestedHistoryEventIdIterable", + "NestedHistoryDateTimeIterable", + "NestedOptionUsizeIterable", + "NestedResultOptionUtcDateTimeIterable", + "NestedI64Iterable", + "NestedResultUtcDateTimeIterable", + "MetadataListList", + "PyNestedPropsIterable", + "PyPropValueListList", + "PyTemporalPropsList", + "PyTemporalPropsListList", + "PyPropHistValueList", + "PyPropHistValueListList", + "PyTemporalPropList", + "PyTemporalPropListList", + "PyPropHistItemsList", + "PyPropHistItemsListList", + "PropIterable", +] + +class NestedUtcDateTimeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -67,8 +126,7 @@ class NestedUtcDateTimeIterable(object): list[list]: """ -class NestedGIDIterable(object): - +class NestedGIDIterable(object): def __eq__(self, value): """Return self==value.""" @@ -120,8 +178,7 @@ class NestedGIDIterable(object): Any: """ -class GIDIterable(object): - +class GIDIterable(object): def __eq__(self, value): """Return self==value.""" @@ -173,8 +230,7 @@ class GIDIterable(object): Any: """ -class StringIterable(object): - +class StringIterable(object): def __eq__(self, value): """Return self==value.""" @@ -210,8 +266,7 @@ class StringIterable(object): list: """ -class OptionArcStringIterable(object): - +class OptionArcStringIterable(object): def __eq__(self, value): """Return self==value.""" @@ -247,8 +302,7 @@ class OptionArcStringIterable(object): list: """ -class UsizeIterable(object): - +class UsizeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -316,8 +370,7 @@ class UsizeIterable(object): Any: """ -class OptionI64Iterable(object): - +class OptionI64Iterable(object): def __eq__(self, value): """Return self==value.""" @@ -369,8 +422,7 @@ class OptionI64Iterable(object): Any: """ -class NestedOptionArcStringIterable(object): - +class NestedOptionArcStringIterable(object): def __eq__(self, value): """Return self==value.""" @@ -406,8 +458,7 @@ class NestedOptionArcStringIterable(object): list[list]: """ -class NestedStringIterable(object): - +class NestedStringIterable(object): def __eq__(self, value): """Return self==value.""" @@ -443,8 +494,7 @@ class NestedStringIterable(object): list[list]: """ -class NestedOptionI64Iterable(object): - +class NestedOptionI64Iterable(object): def __eq__(self, value): """Return self==value.""" @@ -496,8 +546,7 @@ class NestedOptionI64Iterable(object): Any: """ -class NestedI64VecIterable(object): - +class NestedI64VecIterable(object): def __eq__(self, value): """Return self==value.""" @@ -533,8 +582,7 @@ class NestedI64VecIterable(object): list[list]: """ -class NestedUsizeIterable(object): - +class NestedUsizeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -602,8 +650,7 @@ class NestedUsizeIterable(object): Any: """ -class BoolIterable(object): - +class BoolIterable(object): def __eq__(self, value): """Return self==value.""" @@ -639,8 +686,7 @@ class BoolIterable(object): list: """ -class ArcStringIterable(object): - +class ArcStringIterable(object): def __iter__(self): """Implement iter(self).""" @@ -658,8 +704,7 @@ class ArcStringIterable(object): list: """ -class NestedVecUtcDateTimeIterable(object): - +class NestedVecUtcDateTimeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -695,8 +740,7 @@ class NestedVecUtcDateTimeIterable(object): list[list]: """ -class OptionVecUtcDateTimeIterable(object): - +class OptionVecUtcDateTimeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -732,8 +776,7 @@ class OptionVecUtcDateTimeIterable(object): list: """ -class GIDGIDIterable(object): - +class GIDGIDIterable(object): def __eq__(self, value): """Return self==value.""" @@ -785,8 +828,7 @@ class GIDGIDIterable(object): Any: """ -class NestedGIDGIDIterable(object): - +class NestedGIDGIDIterable(object): def __eq__(self, value): """Return self==value.""" @@ -838,8 +880,7 @@ class NestedGIDGIDIterable(object): Any: """ -class NestedBoolIterable(object): - +class NestedBoolIterable(object): def __eq__(self, value): """Return self==value.""" @@ -875,8 +916,7 @@ class NestedBoolIterable(object): list[list]: """ -class U64Iterable(object): - +class U64Iterable(object): def __eq__(self, value): """Return self==value.""" @@ -944,8 +984,7 @@ class U64Iterable(object): Any: """ -class OptionUtcDateTimeIterable(object): - +class OptionUtcDateTimeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -981,8 +1020,7 @@ class OptionUtcDateTimeIterable(object): list: """ -class ArcStringVecIterable(object): - +class ArcStringVecIterable(object): def __eq__(self, value): """Return self==value.""" @@ -1018,8 +1056,7 @@ class ArcStringVecIterable(object): list: """ -class NestedArcStringVecIterable(object): - +class NestedArcStringVecIterable(object): def __eq__(self, value): """Return self==value.""" @@ -1055,8 +1092,7 @@ class NestedArcStringVecIterable(object): list[list]: """ -class NestedEventTimeIterable(object): - +class NestedEventTimeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -1138,8 +1174,7 @@ class NestedEventTimeIterable(object): NestedI64Iterable: Nested iterable of millisecond timestamps since the Unix epoch for each EventTime. """ -class NestedArcStringIterable(object): - +class NestedArcStringIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1157,8 +1192,7 @@ class NestedArcStringIterable(object): list[list]: """ -class NestedOptionEventTimeIterable(object): - +class NestedOptionEventTimeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -1240,8 +1274,7 @@ class NestedOptionEventTimeIterable(object): NestedOptionI64Iterable: Nested iterable of millisecond timestamps since the Unix epoch for each EventTime, if available. """ -class NestedHistoryIterable(object): - +class NestedHistoryIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1303,8 +1336,7 @@ class NestedHistoryIterable(object): NestedHistoryTimestampIterable: Iterable of iterables of HistoryTimestamp objects. """ -class EventTimeIterable(object): - +class EventTimeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -1386,8 +1418,7 @@ class EventTimeIterable(object): I64Iterable: Iterable of millisecond timestamps since the Unix epoch for each EventTime. """ -class OptionEventTimeIterable(object): - +class OptionEventTimeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -1469,8 +1500,7 @@ class OptionEventTimeIterable(object): OptionI64Iterable: Iterable of millisecond timestamps since the Unix epoch for each EventTime, if available. """ -class HistoryIterable(object): - +class HistoryIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1532,8 +1562,7 @@ class HistoryIterable(object): HistoryTimestampIterable: Iterable of HistoryTimestamp objects, one for each item. """ -class HistoryTimestampIterable(object): - +class HistoryTimestampIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1551,8 +1580,7 @@ class HistoryTimestampIterable(object): list[NDArray[np.int64]]: NumPy NDArray of timestamps in milliseconds per history. """ -class IntervalsIterable(object): - +class IntervalsIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1570,8 +1598,7 @@ class IntervalsIterable(object): list[NDArray[np.int64]]: NumPy NDArray of intervals per history. """ -class HistoryEventIdIterable(object): - +class HistoryEventIdIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1589,8 +1616,7 @@ class HistoryEventIdIterable(object): list[NDArray[np.uintp]]: NumPy NDArray of event ids per history. """ -class HistoryDateTimeIterable(object): - +class HistoryDateTimeIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1611,8 +1637,7 @@ class HistoryDateTimeIterable(object): TimeError: If a timestamp cannot be converted to a datetime. """ -class OptionUsizeIterable(object): - +class OptionUsizeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -1664,8 +1689,7 @@ class OptionUsizeIterable(object): Any: """ -class ResultOptionUtcDateTimeIterable(object): - +class ResultOptionUtcDateTimeIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1683,8 +1707,7 @@ class ResultOptionUtcDateTimeIterable(object): list[Optional[datetime]]: one entry per element (None where absent). """ -class I64Iterable(object): - +class I64Iterable(object): def __eq__(self, value): """Return self==value.""" @@ -1752,8 +1775,7 @@ class I64Iterable(object): Any: """ -class ResultUtcDateTimeIterable(object): - +class ResultUtcDateTimeIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1771,8 +1793,7 @@ class ResultUtcDateTimeIterable(object): list[datetime]: one datetime per element. """ -class NestedHistoryTimestampIterable(object): - +class NestedHistoryTimestampIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1806,8 +1827,7 @@ class NestedHistoryTimestampIterable(object): list[int]: List of timestamps in milliseconds. """ -class NestedIntervalsIterable(object): - +class NestedIntervalsIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1841,8 +1861,7 @@ class NestedIntervalsIterable(object): list[int]: List of intervals. """ -class NestedHistoryEventIdIterable(object): - +class NestedHistoryEventIdIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1876,8 +1895,7 @@ class NestedHistoryEventIdIterable(object): list[int]: List of timestamps in milliseconds. """ -class NestedHistoryDateTimeIterable(object): - +class NestedHistoryDateTimeIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1909,8 +1927,7 @@ class NestedHistoryDateTimeIterable(object): TimeError: If a timestamp cannot be converted to a datetime. """ -class NestedOptionUsizeIterable(object): - +class NestedOptionUsizeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -1962,8 +1979,7 @@ class NestedOptionUsizeIterable(object): Any: """ -class NestedResultOptionUtcDateTimeIterable(object): - +class NestedResultOptionUtcDateTimeIterable(object): def __iter__(self): """Implement iter(self).""" @@ -1981,8 +1997,7 @@ class NestedResultOptionUtcDateTimeIterable(object): list[list[Optional[datetime]]]: one inner list per outer element (entries are None where absent). """ -class NestedI64Iterable(object): - +class NestedI64Iterable(object): def __eq__(self, value): """Return self==value.""" @@ -2050,8 +2065,7 @@ class NestedI64Iterable(object): Any: """ -class NestedResultUtcDateTimeIterable(object): - +class NestedResultUtcDateTimeIterable(object): def __iter__(self): """Implement iter(self).""" @@ -2069,8 +2083,7 @@ class NestedResultUtcDateTimeIterable(object): list[list[datetime]]: one inner list per outer element. """ -class MetadataListList(object): - +class MetadataListList(object): def __contains__(self, key): """Return bool(key in self).""" @@ -2141,8 +2154,7 @@ class MetadataListList(object): list[PyPropValueListList]: """ -class PyNestedPropsIterable(object): - +class PyNestedPropsIterable(object): def __contains__(self, key): """Return bool(key in self).""" @@ -2223,8 +2235,7 @@ class PyNestedPropsIterable(object): list[list[list[PropValue]]]: """ -class PyPropValueListList(object): - +class PyPropValueListList(object): def __eq__(self, value): """Return self==value.""" @@ -2332,8 +2343,7 @@ class PyPropValueListList(object): list[PropValue]: """ -class PyTemporalPropsList(object): - +class PyTemporalPropsList(object): def __contains__(self, key): """Return bool(key in self).""" @@ -2412,8 +2422,7 @@ class PyTemporalPropsList(object): list[PyTemporalPropList]: """ -class PyTemporalPropsListList(object): - +class PyTemporalPropsListList(object): def __contains__(self, key): """Return bool(key in self).""" @@ -2492,8 +2501,7 @@ class PyTemporalPropsListList(object): list[PyTemporalPropListList]: """ -class PyPropHistValueList(object): - +class PyPropHistValueList(object): def __eq__(self, value): """Return self==value.""" @@ -2593,8 +2601,7 @@ class PyPropHistValueList(object): list[PropValue]: """ -class PyPropHistValueListList(object): - +class PyPropHistValueListList(object): def __eq__(self, value): """Return self==value.""" @@ -2686,8 +2693,7 @@ class PyPropHistValueListList(object): list[list[PropValue]]: """ -class PyTemporalPropList(object): - +class PyTemporalPropList(object): def __eq__(self, value): """Return self==value.""" @@ -2767,8 +2773,7 @@ class PyTemporalPropList(object): PyPropHistValueList: """ -class PyTemporalPropListList(object): - +class PyTemporalPropListList(object): def __eq__(self, value): """Return self==value.""" @@ -2856,8 +2861,7 @@ class PyTemporalPropListList(object): PyPropHistValueListList: """ -class PyPropHistItemsList(object): - +class PyPropHistItemsList(object): def __eq__(self, value): """Return self==value.""" @@ -2893,8 +2897,7 @@ class PyPropHistItemsList(object): list: """ -class PyPropHistItemsListList(object): - +class PyPropHistItemsListList(object): def __eq__(self, value): """Return self==value.""" @@ -2930,8 +2933,7 @@ class PyPropHistItemsListList(object): list[list]: """ -class PropIterable(object): - +class PropIterable(object): def __eq__(self, value): """Return self==value.""" diff --git a/python/python/raphtory/node_state/__init__.pyi b/python/python/raphtory/node_state/__init__.pyi index 87af0aec5c..16bee37fea 100644 --- a/python/python/raphtory/node_state/__init__.pyi +++ b/python/python/raphtory/node_state/__init__.pyi @@ -29,9 +29,59 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = ['NodeGroups', 'DegreeView', 'NodeStateUsize', 'NodeStateOptionUsize', 'NodeStateU64', 'NodeStateOptionI64', 'NodeStateOptionEventTime', 'NodeStateOptionDateTime', 'IdView', 'NodeStateGID', 'EarliestTimeView', 'EarliestTimestampView', 'EarliestEventIdView', 'EarliestDateTimeView', 'LatestTimeView', 'LatestTimestampView', 'LatestEventIdView', 'LatestDateTimeView', 'NameView', 'NodeStateString', 'HistoryView', 'HistoryTimestampView', 'HistoryDateTimeView', 'HistoryEventIdView', 'IntervalsView', 'IntervalsFloatView', 'IntervalsIntegerView', 'EdgeHistoryCountView', 'UsizeIterable', 'NodeTypeView', 'NodeStateOptionStr', 'NodeStateListDateTime', 'NodeStateWeightedSP', 'NodeStateF64', 'NodeStateOptionF64', 'NodeStateNodes', 'NodeStateReachability', 'NodeStateListF64', 'NodeStateMotifs', 'NodeStateHits', 'NodeStateHistory', 'NodeStateHistoryTimestamp', 'NodeStateHistoryDateTime', 'NodeStateHistoryEventId', 'NodeStateIntervals', 'NodeStateSEIR', 'NodeLayout', 'NodeStateF64String', 'OutputNodeState'] -class NodeGroups(object): - +__all__ = [ + "NodeGroups", + "DegreeView", + "NodeStateUsize", + "NodeStateOptionUsize", + "NodeStateU64", + "NodeStateOptionI64", + "NodeStateOptionEventTime", + "NodeStateOptionDateTime", + "IdView", + "NodeStateGID", + "EarliestTimeView", + "EarliestTimestampView", + "EarliestEventIdView", + "EarliestDateTimeView", + "LatestTimeView", + "LatestTimestampView", + "LatestEventIdView", + "LatestDateTimeView", + "NameView", + "NodeStateString", + "HistoryView", + "HistoryTimestampView", + "HistoryDateTimeView", + "HistoryEventIdView", + "IntervalsView", + "IntervalsFloatView", + "IntervalsIntegerView", + "EdgeHistoryCountView", + "UsizeIterable", + "NodeTypeView", + "NodeStateOptionStr", + "NodeStateListDateTime", + "NodeStateWeightedSP", + "NodeStateF64", + "NodeStateOptionF64", + "NodeStateNodes", + "NodeStateReachability", + "NodeStateListF64", + "NodeStateMotifs", + "NodeStateHits", + "NodeStateHistory", + "NodeStateHistoryTimestamp", + "NodeStateHistoryDateTime", + "NodeStateHistoryEventId", + "NodeStateIntervals", + "NodeStateSEIR", + "NodeLayout", + "NodeStateF64String", + "OutputNodeState", +] + +class NodeGroups(object): def __bool__(self): """True if self else False""" @@ -74,7 +124,7 @@ class NodeGroups(object): Iterator[Tuple[Any, GraphView]]: Iterator over subgraphs with corresponding value """ -class DegreeView(object): +class DegreeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -293,8 +343,7 @@ class DegreeView(object): Iterator[int]: Iterator over values """ -class NodeStateUsize(object): - +class NodeStateUsize(object): def __eq__(self, value): """Return self==value.""" @@ -487,8 +536,7 @@ class NodeStateUsize(object): Iterator[int]: Iterator over values """ -class NodeStateOptionUsize(object): - +class NodeStateOptionUsize(object): def __eq__(self, value): """Return self==value.""" @@ -530,7 +578,9 @@ class NodeStateOptionUsize(object): NodeStateOptionUsize: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: + def get( + self, node: NodeInput, default: Optional[Optional[int]] = None + ) -> Optional[Optional[int]]: """ Get value for node @@ -664,8 +714,7 @@ class NodeStateOptionUsize(object): Iterator[Optional[int]]: Iterator over values """ -class NodeStateU64(object): - +class NodeStateU64(object): def __eq__(self, value): """Return self==value.""" @@ -850,8 +899,7 @@ class NodeStateU64(object): Iterator[int]: Iterator over values """ -class NodeStateOptionI64(object): - +class NodeStateOptionI64(object): def __eq__(self, value): """Return self==value.""" @@ -893,7 +941,9 @@ class NodeStateOptionI64(object): NodeStateOptionI64: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: + def get( + self, node: NodeInput, default: Optional[Optional[int]] = None + ) -> Optional[Optional[int]]: """ Get value for node @@ -1027,8 +1077,7 @@ class NodeStateOptionI64(object): Iterator[Optional[int]]: Iterator over values """ -class NodeStateOptionEventTime(object): - +class NodeStateOptionEventTime(object): def __eq__(self, value): """Return self==value.""" @@ -1070,7 +1119,9 @@ class NodeStateOptionEventTime(object): NodeStateOptionEventTime: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[Optional[EventTime]] = None) -> Optional[Optional[EventTime]]: + def get( + self, node: NodeInput, default: Optional[Optional[EventTime]] = None + ) -> Optional[Optional[EventTime]]: """ Get value for node @@ -1204,8 +1255,7 @@ class NodeStateOptionEventTime(object): Iterator[Optional[EventTime]]: Iterator over values """ -class NodeStateOptionDateTime(object): - +class NodeStateOptionDateTime(object): def __eq__(self, value): """Return self==value.""" @@ -1247,7 +1297,9 @@ class NodeStateOptionDateTime(object): NodeStateOptionDateTime: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[Optional[datetime]] = None) -> Optional[Optional[datetime]]: + def get( + self, node: NodeInput, default: Optional[Optional[datetime]] = None + ) -> Optional[Optional[datetime]]: """ Get value for node @@ -1381,7 +1433,7 @@ class NodeStateOptionDateTime(object): Iterator[Optional[datetime]]: Iterator over values """ -class IdView(object): +class IdView(object): """A lazy view over node values""" def __eq__(self, value): @@ -1575,8 +1627,7 @@ class IdView(object): Iterator[GID]: Iterator over values """ -class NodeStateGID(object): - +class NodeStateGID(object): def __eq__(self, value): """Return self==value.""" @@ -1744,7 +1795,7 @@ class NodeStateGID(object): Iterator[GID]: Iterator over values """ -class EarliestTimeView(object): +class EarliestTimeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -1830,7 +1881,9 @@ class EarliestTimeView(object): EarliestEventIdView: A lazy view over the event ids of the earliest times for each node. """ - def get(self, node: NodeInput, default: Optional[Optional[EventTime]] = None) -> Optional[Optional[EventTime]]: + def get( + self, node: NodeInput, default: Optional[Optional[EventTime]] = None + ) -> Optional[Optional[EventTime]]: """ Get value for node @@ -1973,7 +2026,7 @@ class EarliestTimeView(object): Iterator[Optional[EventTime]]: Iterator over values """ -class EarliestTimestampView(object): +class EarliestTimestampView(object): """A lazy view over node values""" def __eq__(self, value): @@ -2041,7 +2094,9 @@ class EarliestTimestampView(object): NodeStateOptionI64: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: + def get( + self, node: NodeInput, default: Optional[Optional[int]] = None + ) -> Optional[Optional[int]]: """ Get value for node @@ -2175,7 +2230,7 @@ class EarliestTimestampView(object): Iterator[Optional[int]]: Iterator over values """ -class EarliestEventIdView(object): +class EarliestEventIdView(object): """A lazy view over node values""" def __eq__(self, value): @@ -2243,7 +2298,9 @@ class EarliestEventIdView(object): NodeStateOptionUsize: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: + def get( + self, node: NodeInput, default: Optional[Optional[int]] = None + ) -> Optional[Optional[int]]: """ Get value for node @@ -2377,7 +2434,7 @@ class EarliestEventIdView(object): Iterator[Optional[int]]: Iterator over values """ -class EarliestDateTimeView(object): +class EarliestDateTimeView(object): """A lazy view over EarliestDateTime values for each node.""" def __eq__(self, value): @@ -2453,7 +2510,9 @@ class EarliestDateTimeView(object): NodeStateOptionDateTime: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[datetime] = None) -> Optional[datetime]: + def get( + self, node: NodeInput, default: Optional[datetime] = None + ) -> Optional[datetime]: """ Get value for node @@ -2618,7 +2677,7 @@ class EarliestDateTimeView(object): Iterator[datetime]: Iterator over values """ -class LatestTimeView(object): +class LatestTimeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -2704,7 +2763,9 @@ class LatestTimeView(object): LatestEventIdView: A lazy view over the event ids of the latest times for each node. """ - def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: + def get( + self, node: NodeInput, default: Optional[Optional[int]] = None + ) -> Optional[Optional[int]]: """ Get value for node @@ -2847,7 +2908,7 @@ class LatestTimeView(object): Iterator[Optional[int]]: Iterator over values """ -class LatestTimestampView(object): +class LatestTimestampView(object): """A lazy view over node values""" def __eq__(self, value): @@ -2915,7 +2976,9 @@ class LatestTimestampView(object): NodeStateOptionI64: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: + def get( + self, node: NodeInput, default: Optional[Optional[int]] = None + ) -> Optional[Optional[int]]: """ Get value for node @@ -3049,7 +3112,7 @@ class LatestTimestampView(object): Iterator[Optional[int]]: Iterator over values """ -class LatestEventIdView(object): +class LatestEventIdView(object): """A lazy view over node values""" def __eq__(self, value): @@ -3117,7 +3180,9 @@ class LatestEventIdView(object): NodeStateOptionUsize: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: + def get( + self, node: NodeInput, default: Optional[Optional[int]] = None + ) -> Optional[Optional[int]]: """ Get value for node @@ -3251,7 +3316,7 @@ class LatestEventIdView(object): Iterator[Optional[int]]: Iterator over values """ -class LatestDateTimeView(object): +class LatestDateTimeView(object): """A lazy view over LatestDateTime values for each node.""" def __eq__(self, value): @@ -3327,7 +3392,9 @@ class LatestDateTimeView(object): NodeStateOptionDateTime: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[datetime] = None) -> Optional[datetime]: + def get( + self, node: NodeInput, default: Optional[datetime] = None + ) -> Optional[datetime]: """ Get value for node @@ -3492,7 +3559,7 @@ class LatestDateTimeView(object): Iterator[datetime]: Iterator over values """ -class NameView(object): +class NameView(object): """A lazy view over node values""" def __eq__(self, value): @@ -3694,8 +3761,7 @@ class NameView(object): Iterator[str]: Iterator over values """ -class NodeStateString(object): - +class NodeStateString(object): def __eq__(self, value): """Return self==value.""" @@ -3871,7 +3937,7 @@ class NodeStateString(object): Iterator[str]: Iterator over values """ -class HistoryView(object): +class HistoryView(object): """A lazy view over History objects for each node.""" def __eq__(self, value): @@ -3962,7 +4028,9 @@ class HistoryView(object): History: a history object containing all time entries """ - def get(self, node: NodeInput, default: Optional[History] = None) -> Optional[History]: + def get( + self, node: NodeInput, default: Optional[History] = None + ) -> Optional[History]: """ Get value for node @@ -4042,7 +4110,7 @@ class HistoryView(object): Iterator[History]: Iterator over histories """ -class HistoryTimestampView(object): +class HistoryTimestampView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4099,7 +4167,9 @@ class HistoryTimestampView(object): NodeStateHistoryTimestamp: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[HistoryTimestamp] = None) -> Optional[HistoryTimestamp]: + def get( + self, node: NodeInput, default: Optional[HistoryTimestamp] = None + ) -> Optional[HistoryTimestamp]: """ Get value for node @@ -4154,7 +4224,7 @@ class HistoryTimestampView(object): Iterator[HistoryTimestamp]: Iterator over values """ -class HistoryDateTimeView(object): +class HistoryDateTimeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4211,7 +4281,9 @@ class HistoryDateTimeView(object): NodeStateHistoryDateTime: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[HistoryDateTime] = None) -> Optional[HistoryDateTime]: + def get( + self, node: NodeInput, default: Optional[HistoryDateTime] = None + ) -> Optional[HistoryDateTime]: """ Get value for node @@ -4266,7 +4338,7 @@ class HistoryDateTimeView(object): Iterator[HistoryDateTime]: Iterator over values """ -class HistoryEventIdView(object): +class HistoryEventIdView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4323,7 +4395,9 @@ class HistoryEventIdView(object): NodeStateHistoryEventId: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[HistoryEventId] = None) -> Optional[HistoryEventId]: + def get( + self, node: NodeInput, default: Optional[HistoryEventId] = None + ) -> Optional[HistoryEventId]: """ Get value for node @@ -4378,7 +4452,7 @@ class HistoryEventIdView(object): Iterator[HistoryEventId]: Iterator over values """ -class IntervalsView(object): +class IntervalsView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4435,7 +4509,9 @@ class IntervalsView(object): NodeStateIntervals: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[Intervals] = None) -> Optional[Intervals]: + def get( + self, node: NodeInput, default: Optional[Intervals] = None + ) -> Optional[Intervals]: """ Get value for node @@ -4522,7 +4598,7 @@ class IntervalsView(object): Iterator[Intervals]: Iterator over values """ -class IntervalsFloatView(object): +class IntervalsFloatView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4590,7 +4666,9 @@ class IntervalsFloatView(object): NodeStateOptionF64: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[Optional[float]] = None) -> Optional[Optional[float]]: + def get( + self, node: NodeInput, default: Optional[Optional[float]] = None + ) -> Optional[Optional[float]]: """ Get value for node @@ -4716,7 +4794,7 @@ class IntervalsFloatView(object): Iterator[Optional[float]]: Iterator over values """ -class IntervalsIntegerView(object): +class IntervalsIntegerView(object): """A lazy view over node values""" def __eq__(self, value): @@ -4784,7 +4862,9 @@ class IntervalsIntegerView(object): NodeStateOptionI64: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[Optional[int]] = None) -> Optional[Optional[int]]: + def get( + self, node: NodeInput, default: Optional[Optional[int]] = None + ) -> Optional[Optional[int]]: """ Get value for node @@ -4910,7 +4990,7 @@ class IntervalsIntegerView(object): Iterator[Optional[int]]: Iterator over values """ -class EdgeHistoryCountView(object): +class EdgeHistoryCountView(object): """A lazy view over node values""" def __eq__(self, value): @@ -5121,8 +5201,7 @@ class EdgeHistoryCountView(object): Iterator[int]: Iterator over values """ -class UsizeIterable(object): - +class UsizeIterable(object): def __eq__(self, value): """Return self==value.""" @@ -5190,7 +5269,7 @@ class UsizeIterable(object): Any: """ -class NodeTypeView(object): +class NodeTypeView(object): """A lazy view over node values""" def __eq__(self, value): @@ -5258,7 +5337,9 @@ class NodeTypeView(object): NodeStateOptionStr: the computed `NodeState` """ - def get(self, node: NodeInput, default: Optional[Optional[str]] = None) -> Optional[Optional[str]]: + def get( + self, node: NodeInput, default: Optional[Optional[str]] = None + ) -> Optional[Optional[str]]: """ Get value for node @@ -5392,8 +5473,7 @@ class NodeTypeView(object): Iterator[Optional[str]]: Iterator over values """ -class NodeStateOptionStr(object): - +class NodeStateOptionStr(object): def __eq__(self, value): """Return self==value.""" @@ -5435,7 +5515,9 @@ class NodeStateOptionStr(object): NodeStateOptionStr: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[Optional[str]] = None) -> Optional[Optional[str]]: + def get( + self, node: NodeInput, default: Optional[Optional[str]] = None + ) -> Optional[Optional[str]]: """ Get value for node @@ -5569,8 +5651,7 @@ class NodeStateOptionStr(object): Iterator[Optional[str]]: Iterator over values """ -class NodeStateListDateTime(object): - +class NodeStateListDateTime(object): def __eq__(self, value): """Return self==value.""" @@ -5612,7 +5693,9 @@ class NodeStateListDateTime(object): NodeStateListDateTime: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[list[datetime]] = None) -> Optional[list[datetime]]: + def get( + self, node: NodeInput, default: Optional[list[datetime]] = None + ) -> Optional[list[datetime]]: """ Get value for node @@ -5738,8 +5821,7 @@ class NodeStateListDateTime(object): Iterator[list[datetime]]: Iterator over values """ -class NodeStateWeightedSP(object): - +class NodeStateWeightedSP(object): def __eq__(self, value): """Return self==value.""" @@ -5770,7 +5852,9 @@ class NodeStateWeightedSP(object): def __repr__(self): """Return repr(self).""" - def get(self, node: NodeInput, default: Optional[Tuple[float, Nodes]] = None) -> Optional[Tuple[float, Nodes]]: + def get( + self, node: NodeInput, default: Optional[Tuple[float, Nodes]] = None + ) -> Optional[Tuple[float, Nodes]]: """ Get value for node @@ -5825,8 +5909,7 @@ class NodeStateWeightedSP(object): Iterator[Tuple[float, Nodes]]: Iterator over values """ -class NodeStateF64(object): - +class NodeStateF64(object): def __eq__(self, value): """Return self==value.""" @@ -6011,8 +6094,7 @@ class NodeStateF64(object): Iterator[float]: Iterator over values """ -class NodeStateOptionF64(object): - +class NodeStateOptionF64(object): def __eq__(self, value): """Return self==value.""" @@ -6054,7 +6136,9 @@ class NodeStateOptionF64(object): NodeStateOptionF64: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[Optional[float]] = None) -> Optional[Optional[float]]: + def get( + self, node: NodeInput, default: Optional[Optional[float]] = None + ) -> Optional[Optional[float]]: """ Get value for node @@ -6180,8 +6264,7 @@ class NodeStateOptionF64(object): Iterator[Optional[float]]: Iterator over values """ -class NodeStateNodes(object): - +class NodeStateNodes(object): def __eq__(self, value): """Return self==value.""" @@ -6267,8 +6350,7 @@ class NodeStateNodes(object): Iterator[Nodes]: Iterator over values """ -class NodeStateReachability(object): - +class NodeStateReachability(object): def __eq__(self, value): """Return self==value.""" @@ -6299,7 +6381,9 @@ class NodeStateReachability(object): def __repr__(self): """Return repr(self).""" - def get(self, node: NodeInput, default: Optional[list[Tuple[int, str]]] = None) -> Optional[list[Tuple[int, str]]]: + def get( + self, node: NodeInput, default: Optional[list[Tuple[int, str]]] = None + ) -> Optional[list[Tuple[int, str]]]: """ Get value for node @@ -6354,8 +6438,7 @@ class NodeStateReachability(object): Iterator[list[Tuple[int, str]]]: Iterator over values """ -class NodeStateListF64(object): - +class NodeStateListF64(object): def __eq__(self, value): """Return self==value.""" @@ -6386,7 +6469,9 @@ class NodeStateListF64(object): def __repr__(self): """Return repr(self).""" - def get(self, node: NodeInput, default: Optional[list[float]] = None) -> Optional[list[float]]: + def get( + self, node: NodeInput, default: Optional[list[float]] = None + ) -> Optional[list[float]]: """ Get value for node @@ -6441,8 +6526,7 @@ class NodeStateListF64(object): Iterator[list[float]]: Iterator over values """ -class NodeStateMotifs(object): - +class NodeStateMotifs(object): def __eq__(self, value): """Return self==value.""" @@ -6484,7 +6568,9 @@ class NodeStateMotifs(object): NodeStateMotifs: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[list[int]] = None) -> Optional[list[int]]: + def get( + self, node: NodeInput, default: Optional[list[int]] = None + ) -> Optional[list[int]]: """ Get value for node @@ -6610,8 +6696,7 @@ class NodeStateMotifs(object): Iterator[list[int]]: Iterator over values """ -class NodeStateHits(object): - +class NodeStateHits(object): def __eq__(self, value): """Return self==value.""" @@ -6653,7 +6738,9 @@ class NodeStateHits(object): NodeStateHits: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[Tuple[float, float]] = None) -> Optional[Tuple[float, float]]: + def get( + self, node: NodeInput, default: Optional[Tuple[float, float]] = None + ) -> Optional[Tuple[float, float]]: """ Get value for node @@ -6779,7 +6866,7 @@ class NodeStateHits(object): Iterator[Tuple[float, float]]: Iterator over values """ -class NodeStateHistory(object): +class NodeStateHistory(object): """A NodeState of History objects for each node.""" def __eq__(self, value): @@ -6854,7 +6941,9 @@ class NodeStateHistory(object): History: A history object containing all time entries. """ - def get(self, node: NodeInput, default: Optional[History] = None) -> Optional[History]: + def get( + self, node: NodeInput, default: Optional[History] = None + ) -> Optional[History]: """ Get History object for the node. @@ -6935,8 +7024,7 @@ class NodeStateHistory(object): Iterator[History]: Iterator over History objects. """ -class NodeStateHistoryTimestamp(object): - +class NodeStateHistoryTimestamp(object): def __eq__(self, value): """Return self==value.""" @@ -6967,7 +7055,9 @@ class NodeStateHistoryTimestamp(object): def __repr__(self): """Return repr(self).""" - def get(self, node: NodeInput, default: Optional[HistoryTimestamp] = None) -> Optional[HistoryTimestamp]: + def get( + self, node: NodeInput, default: Optional[HistoryTimestamp] = None + ) -> Optional[HistoryTimestamp]: """ Get value for node @@ -7022,8 +7112,7 @@ class NodeStateHistoryTimestamp(object): Iterator[HistoryTimestamp]: Iterator over values """ -class NodeStateHistoryDateTime(object): - +class NodeStateHistoryDateTime(object): def __eq__(self, value): """Return self==value.""" @@ -7054,7 +7143,9 @@ class NodeStateHistoryDateTime(object): def __repr__(self): """Return repr(self).""" - def get(self, node: NodeInput, default: Optional[HistoryDateTime] = None) -> Optional[HistoryDateTime]: + def get( + self, node: NodeInput, default: Optional[HistoryDateTime] = None + ) -> Optional[HistoryDateTime]: """ Get value for node @@ -7109,8 +7200,7 @@ class NodeStateHistoryDateTime(object): Iterator[HistoryDateTime]: Iterator over values """ -class NodeStateHistoryEventId(object): - +class NodeStateHistoryEventId(object): def __eq__(self, value): """Return self==value.""" @@ -7141,7 +7231,9 @@ class NodeStateHistoryEventId(object): def __repr__(self): """Return repr(self).""" - def get(self, node: NodeInput, default: Optional[HistoryEventId] = None) -> Optional[HistoryEventId]: + def get( + self, node: NodeInput, default: Optional[HistoryEventId] = None + ) -> Optional[HistoryEventId]: """ Get value for node @@ -7196,8 +7288,7 @@ class NodeStateHistoryEventId(object): Iterator[HistoryEventId]: Iterator over values """ -class NodeStateIntervals(object): - +class NodeStateIntervals(object): def __eq__(self, value): """Return self==value.""" @@ -7228,7 +7319,9 @@ class NodeStateIntervals(object): def __repr__(self): """Return repr(self).""" - def get(self, node: NodeInput, default: Optional[Intervals] = None) -> Optional[Intervals]: + def get( + self, node: NodeInput, default: Optional[Intervals] = None + ) -> Optional[Intervals]: """ Get value for node @@ -7323,8 +7416,7 @@ class NodeStateIntervals(object): Iterator[Intervals]: Iterator over values """ -class NodeStateSEIR(object): - +class NodeStateSEIR(object): def __eq__(self, value): """Return self==value.""" @@ -7366,7 +7458,9 @@ class NodeStateSEIR(object): NodeStateSEIR: The k smallest values as a node state """ - def get(self, node: NodeInput, default: Optional[Infected] = None) -> Optional[Infected]: + def get( + self, node: NodeInput, default: Optional[Infected] = None + ) -> Optional[Infected]: """ Get value for node @@ -7492,8 +7586,7 @@ class NodeStateSEIR(object): Iterator[Infected]: Iterator over values """ -class NodeLayout(object): - +class NodeLayout(object): def __eq__(self, value): """Return self==value.""" @@ -7524,7 +7617,9 @@ class NodeLayout(object): def __repr__(self): """Return repr(self).""" - def get(self, node: NodeInput, default: Optional[list[float]] = None) -> Optional[list[float]]: + def get( + self, node: NodeInput, default: Optional[list[float]] = None + ) -> Optional[list[float]]: """ Get value for node @@ -7579,8 +7674,7 @@ class NodeLayout(object): Iterator[list[float]]: Iterator over values """ -class NodeStateF64String(object): - +class NodeStateF64String(object): def __eq__(self, value): """Return self==value.""" @@ -7611,7 +7705,9 @@ class NodeStateF64String(object): def __repr__(self): """Return repr(self).""" - def get(self, node: NodeInput, default: Optional[Tuple[float, str]] = None) -> Optional[Tuple[float, str]]: + def get( + self, node: NodeInput, default: Optional[Tuple[float, str]] = None + ) -> Optional[Tuple[float, str]]: """ Get value for node @@ -7666,8 +7762,7 @@ class NodeStateF64String(object): Iterator[Tuple[float, str]]: Iterator over values """ -class OutputNodeState(object): - +class OutputNodeState(object): def __eq__(self, value): """Return self==value.""" @@ -7741,7 +7836,13 @@ class OutputNodeState(object): Iterator[Tuple[Node, Dict]]: Iterator over items """ - def merge(self, other: OutputNodeState, index_merge_priority: str = "left", default_column_merge_priority: str = "left", column_merge_priority_map: Optional[dict] = None) -> OutputNodeState: + def merge( + self, + other: OutputNodeState, + index_merge_priority: str = "left", + default_column_merge_priority: str = "left", + column_merge_priority_map: Optional[dict] = None, + ) -> OutputNodeState: """ Merge with another OutputNodeState (produces new OutputNodeState) diff --git a/python/python/raphtory/vectors/__init__.pyi b/python/python/raphtory/vectors/__init__.pyi index 7033452a15..96a8a83300 100644 --- a/python/python/raphtory/vectors/__init__.pyi +++ b/python/python/raphtory/vectors/__init__.pyi @@ -29,11 +29,27 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = ['VectorisedGraph', 'Document', 'Embedding', 'VectorSelection', 'OpenAIEmbeddings', 'VectorCache', 'EmbeddingServer', 'RunningEmbeddingServer', 'embedding_server'] -class VectorisedGraph(object): +__all__ = [ + "VectorisedGraph", + "Document", + "Embedding", + "VectorSelection", + "OpenAIEmbeddings", + "VectorCache", + "EmbeddingServer", + "RunningEmbeddingServer", + "embedding_server", +] + +class VectorisedGraph(object): """VectorisedGraph object that contains embedded documents that correspond to graph entities.""" - def edges_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> VectorSelection: + def edges_by_similarity( + self, + query: str | list, + limit: int, + window: Optional[Tuple[int | str, int | str]] = None, + ) -> VectorSelection: """ Perform a similarity search between each edge's associated document and a specified `query`. Returns a number of edges up to a specified `limit` ranked in ascending order of distance. @@ -54,7 +70,12 @@ class VectorisedGraph(object): VectorSelection: """ - def entities_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> VectorSelection: + def entities_by_similarity( + self, + query: str | list, + limit: int, + window: Optional[Tuple[int | str, int | str]] = None, + ) -> VectorSelection: """ Perform a similarity search between each entity's associated document and a specified `query`. Returns a number of entities up to a specified `limit` ranked in ascending order of distance. @@ -67,7 +88,12 @@ class VectorisedGraph(object): VectorSelection: The vector selection resulting from the search. """ - def nodes_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> VectorSelection: + def nodes_by_similarity( + self, + query: str | list, + limit: int, + window: Optional[Tuple[int | str, int | str]] = None, + ) -> VectorSelection: """ Perform a similarity search between each node's associated document and a specified `query`. Returns a number of nodes up to a specified `limit` ranked in ascending order of distance. @@ -88,7 +114,7 @@ class VectorisedGraph(object): None: """ -class Document(object): +class Document(object): """A document corresponding to a graph entity. Used to generate embeddings.""" def __repr__(self): @@ -121,8 +147,7 @@ class Document(object): Optional[Any]: """ -class Embedding(object): - +class Embedding(object): def __repr__(self): """Return repr(self).""" @@ -134,8 +159,7 @@ class Embedding(object): pyarrow.Array: """ -class VectorSelection(object): - +class VectorSelection(object): def add_edges(self, edges: list) -> None: """ Add all the documents associated with the specified `edges` to the current selection. @@ -181,7 +205,9 @@ class VectorSelection(object): list[Edge]: List of edges in the current selection. """ - def expand(self, hops: int, window: Optional[Tuple[int | str, int | str]] = None) -> None: + def expand( + self, hops: int, window: Optional[Tuple[int | str, int | str]] = None + ) -> None: """ Add all the documents a specified number of `hops` away from the selection. @@ -198,7 +224,12 @@ class VectorSelection(object): None: """ - def expand_edges_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> None: + def expand_edges_by_similarity( + self, + query: str | list, + limit: int, + window: Optional[Tuple[int | str, int | str]] = None, + ) -> None: """ Add to the selection the `limit` adjacent edges closest to `query` @@ -213,7 +244,12 @@ class VectorSelection(object): None: """ - def expand_entities_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> None: + def expand_entities_by_similarity( + self, + query: str | list, + limit: int, + window: Optional[Tuple[int | str, int | str]] = None, + ) -> None: """ Add to the selection the `limit` adjacent entities closest to `query` @@ -235,7 +271,12 @@ class VectorSelection(object): None: """ - def expand_nodes_by_similarity(self, query: str | list, limit: int, window: Optional[Tuple[int | str, int | str]] = None) -> None: + def expand_nodes_by_similarity( + self, + query: str | list, + limit: int, + window: Optional[Tuple[int | str, int | str]] = None, + ) -> None: """ Add to the selection the `limit` adjacent nodes closest to `query` @@ -274,7 +315,7 @@ class VectorSelection(object): list[Node]: List of nodes in the current selection. """ -class OpenAIEmbeddings(object): +class OpenAIEmbeddings(object): """ OpenAI-compatible embedding configuration. Pass an instance of this to `VectorCache(...)` to drive `vectorise(...)`. @@ -288,10 +329,18 @@ class OpenAIEmbeddings(object): dim (int, optional): Embedding dimension override. If None, the model's native dimension is used. Defaults to None. """ - def __new__(cls, model: str = 'text-embedding-3-small', api_base: Optional[str] = None, api_key_env: Optional[str] = None, org_id: Optional[str] = None, project_id: Optional[str] = None, dim: Optional[int] = None) -> OpenAIEmbeddings: + def __new__( + cls, + model: str = "text-embedding-3-small", + api_base: Optional[str] = None, + api_key_env: Optional[str] = None, + org_id: Optional[str] = None, + project_id: Optional[str] = None, + dim: Optional[int] = None, + ) -> OpenAIEmbeddings: """Create and return a new object. See help(type) for accurate signature.""" -class VectorCache(object): +class VectorCache(object): """ Cache wrapping an embedding model. Pass to `Graph.vectorise(model=...)` or other vectorisation entry points. @@ -301,11 +350,12 @@ class VectorCache(object): cache (str, optional): Path to persist the embedding cache on disk. Defaults to None. """ - def __new__(cls, v_cache: OpenAIEmbeddings, cache: Optional[str] = None) -> VectorCache: + def __new__( + cls, v_cache: OpenAIEmbeddings, cache: Optional[str] = None + ) -> VectorCache: """Create and return a new object. See help(type) for accurate signature.""" -class EmbeddingServer(object): - +class EmbeddingServer(object): def run(self, port: int, host: Optional[str] = None) -> None: """ Run the embedding server in the foreground until it's stopped. @@ -330,14 +380,9 @@ class EmbeddingServer(object): RunningEmbeddingServer: handle to stop the server. """ -class RunningEmbeddingServer(object): - - def __enter__(self): - ... - - def __exit__(self, _exc_type, _exc_val, _exc_tb): - ... - +class RunningEmbeddingServer(object): + def __enter__(self): ... + def __exit__(self, _exc_type, _exc_val, _exc_tb): ... def stop(self) -> None: """ Stop the running embedding server. diff --git a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py index 09aebff87e..3d0554a68b 100644 --- a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py @@ -454,7 +454,9 @@ def test_all_property_types(GraphClass): "is not a valid string operand", ), ( - lambda: filter.ExplodedEdge.property("weight").fuzzy_search("blah", 2, False), + lambda: filter.ExplodedEdge.property("weight").fuzzy_search( + "blah", 2, False + ), "string operator requires a Str property", ), # Floats (confidence) @@ -467,7 +469,9 @@ def test_all_property_types(GraphClass): "is not a valid string operand", ), ( - lambda: filter.ExplodedEdge.property("confidence").fuzzy_search("blah", 2, False), + lambda: filter.ExplodedEdge.property("confidence").fuzzy_search( + "blah", 2, False + ), "string operator requires a Str property", ), # Booleans (active) @@ -480,20 +484,28 @@ def test_all_property_types(GraphClass): "string operator requires a Str property", ), ( - lambda: filter.ExplodedEdge.property("active").fuzzy_search("blah", 2, False), + lambda: filter.ExplodedEdge.property("active").fuzzy_search( + "blah", 2, False + ), "string operator requires a Str property", ), # Datetimes (created) ( - lambda: filter.ExplodedEdge.property("created").contains(datetime(2023, 1, 1)), + lambda: filter.ExplodedEdge.property("created").contains( + datetime(2023, 1, 1) + ), "string operator requires a Str property", ), ( - lambda: filter.ExplodedEdge.property("created").not_contains(datetime(2023, 1, 1)), + lambda: filter.ExplodedEdge.property("created").not_contains( + datetime(2023, 1, 1) + ), "string operator requires a Str property", ), ( - lambda: filter.ExplodedEdge.property("created").fuzzy_search("blah", 2, False), + lambda: filter.ExplodedEdge.property("created").fuzzy_search( + "blah", 2, False + ), "string operator requires a Str property", ), # Lists (tags) — odd comparisons diff --git a/python/tests/test_base_install/test_filters/test_node_property_filter.py b/python/tests/test_base_install/test_filters/test_node_property_filter.py index 060a612288..afb64f8416 100644 --- a/python/tests/test_base_install/test_filters/test_node_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_property_filter.py @@ -1189,7 +1189,9 @@ def check(graph): expr = filter.Node.window(10, 20).property("prop5").temporal().sum() >= 0 # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. - with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + with pytest.raises( + Exception, match=r"not valid for list properties|cannot be coerced to List" + ): graph.filter(expr).nodes.id return check @@ -1246,7 +1248,9 @@ def check(graph): expr = filter.Node.after(1).property("prop5").temporal().sum() >= 0 # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. - with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + with pytest.raises( + Exception, match=r"not valid for list properties|cannot be coerced to List" + ): graph.filter(expr).nodes.id expr = filter.Node.after(1).property("prop6").temporal().last().sum() == 12 @@ -1264,7 +1268,9 @@ def check(graph): expr = filter.Node.latest().property("prop5").temporal().sum() >= 0 # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. - with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + with pytest.raises( + Exception, match=r"not valid for list properties|cannot be coerced to List" + ): graph.filter(expr).nodes.id return check @@ -1284,7 +1290,9 @@ def check(graph): expr = filter.Node.snapshot_at(1).property("prop6").temporal().sum() >= 0 # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. - with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + with pytest.raises( + Exception, match=r"not valid for list properties|cannot be coerced to List" + ): graph.filter(expr).nodes.id return check @@ -1302,7 +1310,9 @@ def check(graph): expr = filter.Node.snapshot_latest().property("prop5").temporal().sum() >= 0 # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. - with pytest.raises(Exception, match=r"not valid for list properties|cannot be coerced to List"): + with pytest.raises( + Exception, match=r"not valid for list properties|cannot be coerced to List" + ): graph.filter(expr).nodes.id return check diff --git a/python/tests/test_base_install/test_graphdb/test_graphdb.py b/python/tests/test_base_install/test_graphdb/test_graphdb.py index f5719813c1..6598c01ee6 100644 --- a/python/tests/test_base_install/test_graphdb/test_graphdb.py +++ b/python/tests/test_base_install/test_graphdb/test_graphdb.py @@ -2995,4 +2995,3 @@ def datadir(tmpdir, request): except Exception as e: raise e return tmpdir - diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py index e53d872bb4..d983e7b9e2 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py @@ -50,8 +50,7 @@ def test_sort_key_with_no_or_several_fields_is_rejected(graph): nodes { sorted(sortBys: %s) { list { name } } } } } - """ - % keys, + """ % keys, "exactly one", graph, ) @@ -401,10 +400,7 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): above_upper = _degree(direction, "gt", f"{{ u64: {upper} }}") queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"{{ and: [" - f"{above}," - f"{below_upper}" - "] }", + f"{{ and: [" f"{above}," f"{below_upper}" "] }", _expected_degree_select_names( graph, direction, lambda d: d > threshold and d < upper ), @@ -412,10 +408,7 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"{{ and: [" - f"{above}," - f"{below_upper}" - "] }", + f"{{ and: [" f"{above}," f"{below_upper}" "] }", _expected_degree_names( graph, direction, lambda d: d > threshold and d < upper ), @@ -424,10 +417,7 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"{{ or: [" - f"{below}," - f"{above_upper}" - "] }", + f"{{ or: [" f"{below}," f"{above_upper}" "] }", _expected_degree_select_names( graph, direction, lambda d: d < threshold or d > upper ), @@ -435,10 +425,7 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"{{ or: [" - f"{below}," - f"{above_upper}" - "] }", + f"{{ or: [" f"{below}," f"{above_upper}" "] }", _expected_degree_names( graph, direction, lambda d: d < threshold or d > upper ), @@ -447,12 +434,7 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - f"{{ or: [" - f"{below}," - f"{{ not: " - f"{above_upper}" - f" }}" - "] }", + f"{{ or: [" f"{below}," f"{{ not: " f"{above_upper}" f" }}" "] }", _expected_degree_select_names( graph, direction, lambda d: d < threshold or d <= upper ), @@ -460,12 +442,7 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - f"{{ or: [" - f"{below}," - f"{{ not: " - f"{above_upper}" - f" }}" - "] }", + f"{{ or: [" f"{below}," f"{{ not: " f"{above_upper}" f" }}" "] }", _expected_degree_names( graph, direction, lambda d: d < threshold or d <= upper ), diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 42433e1101..ac6f362b7a 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -7076,3 +7076,4 @@ schema { query: QueryRoot mutation: MutRoot } + From edae027bf809bd0f2dd86fa09c8e9426af1e455b Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:04:22 +0100 Subject: [PATCH 121/155] before(t) excludes the events at t, as the view does Filter windows now pass their EventTime bounds to the graph unchanged instead of truncating them to timestamps, so `before`, `at` and a window bound carrying an event id behave exactly like the graph views. --- .../src/db/graph/views/filter/model/mod.rs | 13 ++-- .../src/db/graph/views/filter/model/tree.rs | 69 ++++++++++++++++++- .../views/filter/model/windowed_filter.rs | 8 +-- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 7bc2a544be..e3ccc6dc47 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -536,9 +536,13 @@ impl InternalViewWrapOps for Arc { } } -/// The window `at(t)` means: the single instant `t`. +/// The window `at(t)` means: every event at the timestamp `t`, whatever its +/// position within that timestamp. pub(crate) fn at_bounds(t: EventTime) -> (EventTime, EventTime) { - (t, EventTime::from(t.t().saturating_add(1))) + ( + EventTime::start(t.t()), + EventTime::start(t.t().saturating_add(1)), + ) } /// The window `after(t)` means: everything strictly after `t`. @@ -549,9 +553,10 @@ pub(crate) fn after_bounds(t: EventTime) -> (EventTime, EventTime) { ) } -/// The window `before(t)` means: everything strictly before `t`. +/// The window `before(t)` means: everything strictly before `t`. Events at +/// the timestamp `t` itself are excluded, matching `GraphViewOps::before`. pub(crate) fn before_bounds(t: EventTime) -> (EventTime, EventTime) { - (EventTime::start(i64::MIN), EventTime::end(t.t())) + (EventTime::start(i64::MIN), EventTime::start(t.t())) } pub trait ViewWrapOps: InternalViewWrapOps + Sized { diff --git a/raphtory/src/db/graph/views/filter/model/tree.rs b/raphtory/src/db/graph/views/filter/model/tree.rs index a2b1df4863..ef2cfa1280 100644 --- a/raphtory/src/db/graph/views/filter/model/tree.rs +++ b/raphtory/src/db/graph/views/filter/model/tree.rs @@ -906,10 +906,11 @@ mod tests { db::{ api::view::Filter, graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::NodeFilter, PropertyExprFactory, ViewWrapOps, + edge_filter::EdgeFilter, node_filter::NodeFilter, windowed_filter::Windowed, + EdgeViewFilterOps, PropertyExprFactory, ViewWrapOps, }, }, - prelude::{AdditionOps, EdgeViewOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, + prelude::{AdditionOps, EdgeViewOps, Graph, GraphViewOps, NodeViewOps, TimeOps, NO_PROPS}, }; use raphtory_api::core::entities::properties::prop::IntoProp; @@ -1138,6 +1139,70 @@ mod tests { assert_eq!(layered.to_string(), "LAYER[works](IS_ACTIVE)"); } + #[test] + fn before_and_at_agree_with_the_graph_views() { + // alice→bob @2 (first event at 2) · carol→dave @2 (second event at 2) · eve→fay @5 + let g = Graph::new(); + g.add_edge(2, "alice", "bob", NO_PROPS, None).unwrap(); + g.add_edge(2, "carol", "dave", NO_PROPS, None).unwrap(); + g.add_edge(5, "eve", "fay", NO_PROPS, None).unwrap(); + fn edge_names<'graph, G: GraphViewOps<'graph>>(g: &G) -> Vec { + let mut ids: Vec = g + .edges() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + ids.sort(); + ids + } + let view = |op: ViewOp| FilterExpr::View(vec![op]).compile().unwrap(); + let typed = |f: Windowed| Arc::new(f.is_active()) as Arc; + let none: [&str; 0] = []; + let at_two = ["alice->bob", "carol->dave"]; + + // `before(t)` excludes every event at `t`, like the graph view does. + assert_eq!(edge_names(&g.before(2)), none); + assert_eq!(edges(&g, view(ViewOp::Before(EventTime::start(2)))), none); + assert_eq!(edges(&g, typed(EdgeFilter.before(2))), none); + assert_eq!(edge_names(&g.before(3)), at_two); + assert_eq!(edges(&g, view(ViewOp::Before(EventTime::start(3)))), at_two); + + // `at(t)` covers the whole timestamp, even when handed a time that sits + // between two events at `t`. + let mid_two = EventTime::start(2).set_event_id(1); + assert_eq!(edge_names(&g.at(mid_two)), at_two); + assert_eq!(edges(&g, view(ViewOp::At(mid_two))), at_two); + assert_eq!(edges(&g, typed(EdgeFilter.at(mid_two))), at_two); + + // A window bound that carries an event id is honoured, as the graph view does. + let from_second_event = EventTime::start(2).set_event_id(1); + assert_eq!( + edge_names(&g.window(from_second_event, EventTime::start(3))), + ["carol->dave"] + ); + assert_eq!( + edges( + &g, + view(ViewOp::Window { + start: from_second_event, + end: EventTime::start(3), + }), + ), + ["carol->dave"] + ); + assert_eq!( + edges(&g, typed(EdgeFilter.window(from_second_event, 3))), + ["carol->dave"] + ); + + // `after(t)` excludes `t` and everything before it. + assert_eq!(edge_names(&g.after(2)), ["eve->fay"]); + assert_eq!( + edges(&g, view(ViewOp::After(EventTime::start(2)))), + ["eve->fay"] + ); + } + #[test] fn a_view_alone_is_the_result() { let g = graph(); 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 bdaa72da4d..b0c584d2be 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -124,7 +124,7 @@ impl CreateFilter for Windowed< Ok(self .inner .filter_graph_view(graph)? - .window(self.start.t(), self.end.t())) + .window(self.start, self.end)) } } @@ -176,7 +176,7 @@ impl CreateView for Windowed { view: G, ) -> Result, GraphError> { let inner = self.inner.create_view(view)?; - Ok(inner.window(self.start.t(), self.end.t())) + Ok(inner.window(self.start, self.end)) } } @@ -189,7 +189,7 @@ impl CreateOp for Windowed { graph: G, ) -> Result> + 'g>, GraphError> { self.inner - .create_node_op(graph.window(self.start.t(), self.end.t())) + .create_node_op(graph.window(self.start, self.end)) } fn create_edge_op<'g, G: GraphView + 'g>( @@ -197,6 +197,6 @@ impl CreateOp for Windowed { graph: G, ) -> Result> + 'g>, GraphError> { self.inner - .create_edge_op(graph.window(self.start.t(), self.end.t())) + .create_edge_op(graph.window(self.start, self.end)) } } From 2326ccb30c390c48abe0db445aa1544f0a796bde Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:10:50 +0100 Subject: [PATCH 122/155] Compare constants as written instead of casting them to the expression's type Comparison validity is decided on PropType (`has_cmp`, `is_comparable_with`) and values are compared by value at run time, so `degree() >= 4.5` keeps the fraction, a string never compares with a number, and a set member of a type the left side can never equal is simply not a member. Sums of F32 values keep their width. --- .../test_filters/test_edge_filter.py | 4 +- .../test_filters/test_exploded_edge_filter.py | 88 ++-- .../test_filters/test_node_filter.py | 455 ++---------------- .../test_filters/test_node_property_filter.py | 21 +- .../test_graph_edges_property_filter.py | 12 +- .../test_graph_nodes_property_filter.py | 12 +- .../test_filters/test_node_filter_gql.py | 89 ++-- .../test_nodes_property_filter.py | 12 +- .../entities/properties/prop/prop_type.rs | 94 +++- raphtory-tests/tests/test_filters.rs | 292 +++++------ .../views/filter/model/edge_expr/filters.rs | 22 +- .../src/db/graph/views/filter/model/mod.rs | 207 ++------ .../views/filter/model/node_expr/dyn_expr.rs | 10 - .../views/filter/model/node_expr/exprs.rs | 4 - .../views/filter/model/node_expr/filters.rs | 36 +- .../graph/views/filter/model/node_expr/mod.rs | 9 - .../graph/views/filter/model/node_expr/ops.rs | 23 +- raphtory/src/python/filter/node_expr.rs | 35 +- 18 files changed, 515 insertions(+), 910 deletions(-) diff --git a/python/tests/test_base_install/test_filters/test_edge_filter.py b/python/tests/test_base_install/test_filters/test_edge_filter.py index 005db206c2..777f7b7813 100644 --- a/python/tests/test_base_install/test_filters/test_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_filter.py @@ -502,7 +502,7 @@ def check(graph): filter_expr = filter.Edge.src().id() == 3 with pytest.raises( Exception, - match=r"Invalid filter: value I64\(3\) of type I64 cannot be coerced to Str", + match=r"Invalid filter: value I64\(3\) of type I64 cannot be compared with Str", ): graph.filter(filter_expr).nodes.id @@ -515,7 +515,7 @@ def check(graph): filter_expr = filter.Edge.src().id() == "3" with pytest.raises( Exception, - match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be coerced to U64', + match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be compared with U64', ): graph.filter(filter_expr).nodes.id diff --git a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py index 3d0554a68b..5d5ffbc31c 100644 --- a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py @@ -447,11 +447,11 @@ def test_all_property_types(GraphClass): # Integers (weight) ( lambda: filter.ExplodedEdge.property("weight").contains(2), - "is not a valid string operand", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("weight").not_contains(3), - "is not a valid string operand", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("weight").fuzzy_search( @@ -462,11 +462,11 @@ def test_all_property_types(GraphClass): # Floats (confidence) ( lambda: filter.ExplodedEdge.property("confidence").contains(0.9), - "is not a valid string operand", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("confidence").not_contains(0.8), - "is not a valid string operand", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("confidence").fuzzy_search( @@ -477,11 +477,11 @@ def test_all_property_types(GraphClass): # Booleans (active) ( lambda: filter.ExplodedEdge.property("active").contains(True), - "string operator requires a Str property", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("active").not_contains(False), - "string operator requires a Str property", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("active").fuzzy_search( @@ -494,13 +494,13 @@ def test_all_property_types(GraphClass): lambda: filter.ExplodedEdge.property("created").contains( datetime(2023, 1, 1) ), - "string operator requires a Str property", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("created").not_contains( datetime(2023, 1, 1) ), - "string operator requires a Str property", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("created").fuzzy_search( @@ -583,14 +583,12 @@ def test_all_property_types(GraphClass): print(e.value) assert message in str(e.value) - # Numeric strings coerce to the property's numeric type: each string form - # matches exactly what its native-typed twin matches. # A string constant never compares against a numeric property, whether or # not it happens to parse as a number. for prop, val in (("weight", 2), ("weight", 3), ("confidence", 2)): for op in ("__eq__", "__ne__", "__lt__", "__gt__", "__le__", "__ge__"): expr = getattr(filter.ExplodedEdge.property(prop), op)(str(val)) - with pytest.raises(Exception, match=r"of type Str cannot be coerced"): + with pytest.raises(Exception, match=r"of type Str cannot be compared with"): g.filter(expr).edges.explode() wrong_types = [ @@ -615,110 +613,110 @@ def test_all_property_types(GraphClass): # # Strings (name) ( lambda: filter.ExplodedEdge.property("name") == 2, - "cannot be coerced to Str", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("name") != 3, - "cannot be coerced to Str", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("name") < 3, - "cannot be coerced to Str", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("name") > 1, - "cannot be coerced to Str", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("name") <= 2, - "cannot be coerced to Str", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("name") >= 3, - "cannot be coerced to Str", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("name").contains(2), - "is not a valid string operand", + "cannot be compared with Str", ), ( lambda: filter.ExplodedEdge.property("name").not_contains(3), - "is not a valid string operand", + "cannot be compared with Str", ), # Booleans (active) ( lambda: filter.ExplodedEdge.property("active") == 2, - "cannot be coerced to Bool", + "cannot be compared with Bool", ), ( lambda: filter.ExplodedEdge.property("active") != 3, - "cannot be coerced to Bool", + "cannot be compared with Bool", ), ( lambda: filter.ExplodedEdge.property("active") < 3, - "cannot be coerced to Bool", + "cannot be compared with Bool", ), ( lambda: filter.ExplodedEdge.property("active") > 1, - "cannot be coerced to Bool", + "cannot be compared with Bool", ), ( lambda: filter.ExplodedEdge.property("active") <= 2, - "cannot be coerced to Bool", + "cannot be compared with Bool", ), ( lambda: filter.ExplodedEdge.property("active") >= 3, - "cannot be coerced to Bool", + "cannot be compared with Bool", ), ( lambda: filter.ExplodedEdge.property("active").contains(2), - "is not a valid string operand", + "cannot be compared with Str", ), # should fail on contains not type ( lambda: filter.ExplodedEdge.property("active").not_contains(3), - "is not a valid string operand", + "cannot be compared with Str", ), # should fail on contains not type # # Datetimes (created) ( lambda: filter.ExplodedEdge.property("created") == 2, - "cannot be coerced to NDTime", + "cannot be compared with NDTime", ), ( lambda: filter.ExplodedEdge.property("created") != 3, - "cannot be coerced to NDTime", + "cannot be compared with NDTime", ), ( lambda: filter.ExplodedEdge.property("created") < 3, - "cannot be coerced to NDTime", + "cannot be compared with NDTime", ), ( lambda: filter.ExplodedEdge.property("created") > 1, - "cannot be coerced to NDTime", + "cannot be compared with NDTime", ), ( lambda: filter.ExplodedEdge.property("created") <= 2, - "cannot be coerced to NDTime", + "cannot be compared with NDTime", ), ( lambda: filter.ExplodedEdge.property("created") >= 3, - "cannot be coerced to NDTime", + "cannot be compared with NDTime", ), ( lambda: filter.ExplodedEdge.property("created").contains(2), - "is not a valid string operand", + "cannot be compared with Str", ), # should fail on contains not type ( lambda: filter.ExplodedEdge.property("created").not_contains(3), - "is not a valid string operand", + "cannot be compared with Str", ), # should fail on contains not type # # Lists (tags) ( lambda: filter.ExplodedEdge.property("tags") == 2, - "cannot be coerced to List", + "cannot be compared with List", ), ( lambda: filter.ExplodedEdge.property("tags") != 3, - "cannot be coerced to List", + "cannot be compared with List", ), ( lambda: filter.ExplodedEdge.property("tags") < 3, @@ -738,20 +736,20 @@ def test_all_property_types(GraphClass): ), ( lambda: filter.ExplodedEdge.property("tags").contains(2), - "is not a valid string operand", + "cannot be compared with Str", ), # should fail on contains not type ( lambda: filter.ExplodedEdge.property("tags").not_contains(3), - "is not a valid string operand", + "cannot be compared with Str", ), # should fail on contains not type # # Dicts (meta) ( lambda: filter.ExplodedEdge.property("meta") == 2, - "cannot be coerced to Map", + "cannot be compared with Map", ), ( lambda: filter.ExplodedEdge.property("meta") != 3, - "cannot be coerced to Map", + "cannot be compared with Map", ), ( lambda: filter.ExplodedEdge.property("meta") < 3, @@ -771,11 +769,11 @@ def test_all_property_types(GraphClass): ), ( lambda: filter.ExplodedEdge.property("meta").contains(2), - "is not a valid string operand", + "cannot be compared with Str", ), # should fail on contains not type ( lambda: filter.ExplodedEdge.property("meta").not_contains(3), - "is not a valid string operand", + "cannot be compared with Str", ), # should fail on contains not type ] @@ -787,7 +785,7 @@ def test_all_property_types(GraphClass): with pytest.raises(Exception) as e: filter.ExplodedEdge.property("name").fuzzy_search(2, 2, False) - assert "is not a valid string operand" in str(e.value) + assert "cannot be compared with Str" in str(e.value) missing_prop = [ (lambda: filter.ExplodedEdge.property("blah") == 2), @@ -814,7 +812,7 @@ def test_all_property_types(GraphClass): # lookup happens assert "Property blah does not exist" in str( e.value - ) or "is not a valid string operand" in str(e.value) + ) or "cannot be compared with Str" in str(e.value) @pytest.mark.parametrize("GraphClass", [Graph, PersistentGraph]) diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index c9212a6039..56064223c4 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -106,6 +106,55 @@ def test_degree_filter_both_direction_comparison(value): ) +@pytest.mark.parametrize("value", range(0, 15)) +def test_degree_filter_with_float_constants(value): + # A float constant is compared as written: a whole float names that number, + # a fraction is never rounded to the degree's type. + graph = degree_graph_with_add_node_and_add_edge(Graph()) + whole = float(value) + half = value + 0.5 + for expr, expected, context in [ + (filter.Node.degree() == whole, lambda d: d == value, "== whole"), + (filter.Node.degree() >= whole, lambda d: d >= value, ">= whole"), + (filter.Node.degree() < half, lambda d: d <= value, "< half"), + (filter.Node.degree() <= half, lambda d: d <= value, "<= half"), + (filter.Node.degree() == half, lambda d: False, "== half"), + (filter.Node.degree() != half, lambda d: True, "!= half"), + (filter.Node.degree() >= half, lambda d: d > value, ">= half"), + (filter.Node.degree() > half, lambda d: d > value, "> half"), + (filter.Node.degree().is_in([half, whole]), lambda d: d == value, "is_in"), + ( + filter.Node.degree().is_not_in([half, whole]), + lambda d: d != value, + "is_not_in", + ), + ]: + assert_filter(graph, expr, "both", expected, context) + + +def test_degree_filter_refuses_string_constants(): + # A string never compares with a degree, even one that spells a number; in + # a set it is simply not a member. + graph = degree_graph_with_add_node_and_add_edge(Graph()) + for op in ("__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__"): + with pytest.raises(TypeError, match="of type Str cannot be compared with U64"): + getattr(filter.Node.degree(), op)("3") + assert_filter( + graph, + filter.Node.degree().is_in(["3", 4]), + "both", + lambda d: d == 4, + "is_in(string, number)", + ) + assert_filter( + graph, + filter.Node.degree().is_in(["3", "4"]), + "both", + lambda d: False, + "is_in(strings)", + ) + + @pytest.mark.parametrize("value", range(0, 15)) def test_degree_filter_in_direction_comparison(value): graph = degree_graph_with_add_node_and_add_edge(Graph()) @@ -399,8 +448,8 @@ def test_degree_filter_with_invalid_expressions(): graph.filter(make_filter()).nodes.id -@pytest.mark.parametrize("value_a, value_b", [("a", "b"), ("foo", "bar")]) -def test_degree_filter_with_invalid_string_values(value_a, value_b): +@pytest.mark.parametrize("value_a", ["a", "foo"]) +def test_degree_filter_with_invalid_string_values(value_a): graph = degree_graph_with_add_node_and_add_edge(Graph()) invalid_filters = [ lambda: filter.Node.degree() < value_a, @@ -421,12 +470,6 @@ def test_degree_filter_with_invalid_string_values(value_a, value_b): lambda: filter.Node.out_degree() != value_a, lambda: filter.Node.out_degree() >= value_a, lambda: filter.Node.out_degree() > value_a, - lambda: filter.Node.degree().is_in([value_a, value_b]), - lambda: filter.Node.degree().is_not_in([value_a, value_b]), - lambda: filter.Node.in_degree().is_in([value_a, value_b]), - lambda: filter.Node.in_degree().is_not_in([value_a, value_b]), - lambda: filter.Node.out_degree().is_in([value_a, value_b]), - lambda: filter.Node.out_degree().is_not_in([value_a, value_b]), ] for make_filter in invalid_filters: @@ -436,398 +479,6 @@ def test_degree_filter_with_invalid_string_values(value_a, value_b): graph.filter(make_filter()).nodes.id -@pytest.mark.parametrize("value", range(0, 15)) -def test_degree_filter_with_string_threshold(value): - graph = degree_graph_with_add_node_and_add_edge(Graph()) - threshold_str = str(value) - parsed_str = int(threshold_str) - - assert_filter( - graph, - filter.Node.degree() < threshold_str, - "both", - lambda d: d < parsed_str, - f"BOTH < string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.degree() <= threshold_str, - "both", - lambda d: d <= parsed_str, - f"BOTH <= string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.degree() == threshold_str, - "both", - lambda d: d == parsed_str, - f"BOTH == string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.degree() != threshold_str, - "both", - lambda d: d != parsed_str, - f"BOTH != string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.degree() >= threshold_str, - "both", - lambda d: d >= parsed_str, - f"BOTH >= string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.degree() > threshold_str, - "both", - lambda d: d > parsed_str, - f"BOTH > string threshold parsed to u64 ({threshold_str})", - ) - - assert_filter( - graph, - filter.Node.in_degree() < threshold_str, - "in", - lambda d: d < parsed_str, - f"IN < string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.in_degree() <= threshold_str, - "in", - lambda d: d <= parsed_str, - f"IN <= string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.in_degree() == threshold_str, - "in", - lambda d: d == parsed_str, - f"IN == string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.in_degree() != threshold_str, - "in", - lambda d: d != parsed_str, - f"IN != string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.in_degree() >= threshold_str, - "in", - lambda d: d >= parsed_str, - f"IN >= string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.in_degree() > threshold_str, - "in", - lambda d: d > parsed_str, - f"IN > string threshold parsed to u64 ({threshold_str})", - ) - - assert_filter( - graph, - filter.Node.out_degree() < threshold_str, - "out", - lambda d: d < parsed_str, - f"OUT < string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.out_degree() <= threshold_str, - "out", - lambda d: d <= parsed_str, - f"OUT <= string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.out_degree() == threshold_str, - "out", - lambda d: d == parsed_str, - f"OUT == string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.out_degree() != threshold_str, - "out", - lambda d: d != parsed_str, - f"OUT != string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.out_degree() >= threshold_str, - "out", - lambda d: d >= parsed_str, - f"OUT >= string threshold parsed to u64 ({threshold_str})", - ) - assert_filter( - graph, - filter.Node.out_degree() > threshold_str, - "out", - lambda d: d > parsed_str, - f"OUT > string threshold parsed to u64 ({threshold_str})", - ) - - -@pytest.mark.parametrize("value", range(0, 15)) -def test_degree_filter_with_string_is_in(value): - graph = degree_graph_with_add_node_and_add_edge(Graph()) - threshold_a_str = str(value) - threshold_b_str = str(value + 1) - set_values = [int(threshold_a_str), int(threshold_b_str)] - - assert_filter( - graph, - filter.Node.degree().is_in([threshold_a_str, threshold_b_str]), - "both", - lambda d: d in set_values, - f"BOTH is_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", - ) - assert_filter( - graph, - filter.Node.in_degree().is_in([threshold_a_str, threshold_b_str]), - "in", - lambda d: d in set_values, - f"IN is_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", - ) - assert_filter( - graph, - filter.Node.out_degree().is_in([threshold_a_str, threshold_b_str]), - "out", - lambda d: d in set_values, - f"OUT is_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", - ) - - -@pytest.mark.parametrize("value", range(0, 15)) -def test_degree_filter_with_string_is_not_in(value): - graph = degree_graph_with_add_node_and_add_edge(Graph()) - threshold_a_str = str(value) - threshold_b_str = str(value + 1) - set_values = [int(threshold_a_str), int(threshold_b_str)] - - assert_filter( - graph, - filter.Node.degree().is_not_in([threshold_a_str, threshold_b_str]), - "both", - lambda d: d not in set_values, - f"BOTH is_not_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", - ) - assert_filter( - graph, - filter.Node.in_degree().is_not_in([threshold_a_str, threshold_b_str]), - "in", - lambda d: d not in set_values, - f"IN is_not_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", - ) - assert_filter( - graph, - filter.Node.out_degree().is_not_in([threshold_a_str, threshold_b_str]), - "out", - lambda d: d not in set_values, - f"OUT is_not_in(string thresholds parsed to u64) ({threshold_a_str}, {threshold_b_str})", - ) - - -@pytest.mark.parametrize("value", range(0, 15)) -def test_degree_filter_with_float_threshold(value): - graph = degree_graph_with_add_node_and_add_edge(Graph()) - threshold_float = value + 0.5 - parsed_float = int(threshold_float) - - assert_filter( - graph, - filter.Node.degree() < threshold_float, - "both", - lambda d: d < parsed_float, - f"BOTH < float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.degree() <= threshold_float, - "both", - lambda d: d <= parsed_float, - f"BOTH <= float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.degree() == threshold_float, - "both", - lambda d: d == parsed_float, - f"BOTH == float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.degree() != threshold_float, - "both", - lambda d: d != parsed_float, - f"BOTH != float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.degree() >= threshold_float, - "both", - lambda d: d >= parsed_float, - f"BOTH >= float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.degree() > threshold_float, - "both", - lambda d: d > parsed_float, - f"BOTH > float threshold cast to u64 ({value})", - ) - - assert_filter( - graph, - filter.Node.in_degree() < threshold_float, - "in", - lambda d: d < parsed_float, - f"IN < float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.in_degree() <= threshold_float, - "in", - lambda d: d <= parsed_float, - f"IN <= float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.in_degree() == threshold_float, - "in", - lambda d: d == parsed_float, - f"IN == float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.in_degree() != threshold_float, - "in", - lambda d: d != parsed_float, - f"IN != float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.in_degree() >= threshold_float, - "in", - lambda d: d >= parsed_float, - f"IN >= float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.in_degree() > threshold_float, - "in", - lambda d: d > parsed_float, - f"IN > float threshold cast to u64 ({value})", - ) - - assert_filter( - graph, - filter.Node.out_degree() < threshold_float, - "out", - lambda d: d < parsed_float, - f"OUT < float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.out_degree() <= threshold_float, - "out", - lambda d: d <= parsed_float, - f"OUT <= float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.out_degree() == threshold_float, - "out", - lambda d: d == parsed_float, - f"OUT == float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.out_degree() != threshold_float, - "out", - lambda d: d != parsed_float, - f"OUT != float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.out_degree() >= threshold_float, - "out", - lambda d: d >= parsed_float, - f"OUT >= float threshold cast to u64 ({value})", - ) - assert_filter( - graph, - filter.Node.out_degree() > threshold_float, - "out", - lambda d: d > parsed_float, - f"OUT > float threshold cast to u64 ({value})", - ) - - -@pytest.mark.parametrize("value", range(0, 15)) -def test_degree_filter_with_float_is_in(value): - graph = degree_graph_with_add_node_and_add_edge(Graph()) - threshold_a = value + 0.25 - threshold_b = value + 1.75 - set_values = [int(threshold_a), int(threshold_b)] - - assert_filter( - graph, - filter.Node.degree().is_in([threshold_a, threshold_b]), - "both", - lambda d: d in set_values, - f"BOTH is_in(float thresholds cast to u64) ({value}, {value + 1})", - ) - assert_filter( - graph, - filter.Node.in_degree().is_in([threshold_a, threshold_b]), - "in", - lambda d: d in set_values, - f"IN is_in(float thresholds cast to u64) ({value}, {value + 1})", - ) - assert_filter( - graph, - filter.Node.out_degree().is_in([threshold_a, threshold_b]), - "out", - lambda d: d in set_values, - f"OUT is_in(float thresholds cast to u64) ({value}, {value + 1})", - ) - - -@pytest.mark.parametrize("value", range(0, 15)) -def test_degree_filter_with_float_is_not_in(value): - graph = degree_graph_with_add_node_and_add_edge(Graph()) - threshold_a = value + 0.25 - threshold_b = value + 1.75 - set_values = [int(threshold_a), int(threshold_b)] - - assert_filter( - graph, - filter.Node.degree().is_not_in([threshold_a, threshold_b]), - "both", - lambda d: d not in set_values, - f"BOTH is_not_in(float thresholds cast to u64) ({value}, {value + 1})", - ) - assert_filter( - graph, - filter.Node.in_degree().is_not_in([threshold_a, threshold_b]), - "in", - lambda d: d not in set_values, - f"IN is_not_in(float thresholds cast to u64) ({value}, {value + 1})", - ) - assert_filter( - graph, - filter.Node.out_degree().is_not_in([threshold_a, threshold_b]), - "out", - lambda d: d not in set_values, - f"OUT is_not_in(float thresholds cast to u64) ({value}, {value + 1})", - ) - - @with_variants(init_graph) def test_filter_nodes_for_node_name_eq(): def check(graph): @@ -1124,7 +775,7 @@ def check(graph): filter_expr = filter.Node.id() == 3 with pytest.raises( Exception, - match=r"Invalid filter: value I64\(3\) of type I64 cannot be coerced to Str", + match=r"Invalid filter: value I64\(3\) of type I64 cannot be compared with Str", ): graph.filter(filter_expr).nodes.id @@ -1137,7 +788,7 @@ def check(graph): filter_expr = filter.Node.id() == "3" with pytest.raises( Exception, - match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be coerced to U64', + match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be compared with U64', ): graph.filter(filter_expr).nodes.id diff --git a/python/tests/test_base_install/test_filters/test_node_property_filter.py b/python/tests/test_base_install/test_filters/test_node_property_filter.py index afb64f8416..335562bd65 100644 --- a/python/tests/test_base_install/test_filters/test_node_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_property_filter.py @@ -793,7 +793,7 @@ def check(graph): filter_expr = filter.Node.property("prop8").any() == value with pytest.raises( Exception, - match=r"of type Str cannot be coerced to I64", + match=r"of type Str cannot be compared with I64", ): graph.filter(filter_expr).nodes.id @@ -1139,7 +1139,7 @@ def check(graph): def test_filter_nodes_for_temporal_property_fails(): def check(graph): filter_expr = filter.Node.property("prop1").temporal() == 60 - msg = r"value I64\(60\) of type I64 cannot be coerced to List" + msg = r"value I64\(60\) of type I64 cannot be compared with List" with pytest.raises( Exception, match=msg, @@ -1147,7 +1147,7 @@ def check(graph): graph.filter(filter_expr).nodes.id filter_expr = filter.Node.property("prop1").temporal() == "pometry" - msg = r"of type Str cannot be coerced to List" + msg = r"of type Str cannot be compared with List" with pytest.raises( Exception, match=msg, @@ -1190,7 +1190,8 @@ def check(graph): # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. with pytest.raises( - Exception, match=r"not valid for list properties|cannot be coerced to List" + Exception, + match=r"not valid for list properties|cannot be compared with List", ): graph.filter(expr).nodes.id @@ -1249,7 +1250,8 @@ def check(graph): # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. with pytest.raises( - Exception, match=r"not valid for list properties|cannot be coerced to List" + Exception, + match=r"not valid for list properties|cannot be compared with List", ): graph.filter(expr).nodes.id @@ -1269,7 +1271,8 @@ def check(graph): # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. with pytest.raises( - Exception, match=r"not valid for list properties|cannot be coerced to List" + Exception, + match=r"not valid for list properties|cannot be compared with List", ): graph.filter(expr).nodes.id @@ -1291,7 +1294,8 @@ def check(graph): # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. with pytest.raises( - Exception, match=r"not valid for list properties|cannot be coerced to List" + Exception, + match=r"not valid for list properties|cannot be compared with List", ): graph.filter(expr).nodes.id @@ -1311,7 +1315,8 @@ def check(graph): # Per-snapshot sums form a list; comparing it to a scalar is a type # error rather than a silent no-match. with pytest.raises( - Exception, match=r"not valid for list properties|cannot be coerced to List" + Exception, + match=r"not valid for list properties|cannot be compared with List", ): graph.filter(expr).nodes.id diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py index 8233b656c0..283f164d4c 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py @@ -55,7 +55,7 @@ def test_graph_edge_property_filter_equal_type_error(graph): } """ expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be coerced to List" + "Invalid filter: value I64(1) of type I64 cannot be compared with List" ) run_graphql_error_test(query, expected_error_message, graph) @@ -103,7 +103,7 @@ def test_graph_edge_property_filter_not_equal_type_error(graph): } """ expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be coerced to Bool" + "Invalid filter: value I64(1) of type I64 cannot be compared with Bool" ) run_graphql_error_test(query, expected_error_message, graph) @@ -150,7 +150,7 @@ def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -201,7 +201,7 @@ def test_graph_edge_property_filter_less_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -247,7 +247,7 @@ def test_graph_edge_property_filter_greater_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -293,7 +293,7 @@ def test_graph_edge_property_filter_less_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index eb3bd1590f..e7802082a3 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -44,7 +44,7 @@ def test_graph_node_property_filter_equal_type_error(graph): } """ expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be coerced to List" + "Invalid filter: value I64(1) of type I64 cannot be compared with List" ) run_graphql_error_test(query, expected_error_message, graph) @@ -88,7 +88,7 @@ def test_graph_node_property_filter_not_equal_type_error(graph): } """ expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be coerced to Bool" + "Invalid filter: value I64(1) of type I64 cannot be compared with Bool" ) run_graphql_error_test(query, expected_error_message, graph) @@ -129,7 +129,7 @@ def test_graph_node_property_filter_greater_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -175,7 +175,7 @@ def test_graph_node_property_filter_less_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -215,7 +215,7 @@ def test_graph_node_property_filter_greater_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -257,7 +257,7 @@ def test_graph_node_property_filter_less_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py index d983e7b9e2..dda51b8577 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py @@ -50,7 +50,8 @@ def test_sort_key_with_no_or_several_fields_is_rejected(graph): nodes { sorted(sortBys: %s) { list { name } } } } } - """ % keys, + """ + % keys, "exactly one", graph, ) @@ -75,7 +76,7 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql2(graph): } """ expected_error_message = ( - "Invalid filter: value U64(1) of type U64 cannot be coerced to Str" + "Invalid filter: value U64(1) of type U64 cannot be compared with Str" ) run_graphql_error_test(query, expected_error_message, graph) @@ -503,59 +504,79 @@ def test_filter_nodes_degree_logic_and_sets_gql(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_filter_nodes_degree_numeric_coercion_gql(graph): - threshold_str = "4" - threshold_float = 4.5 +def test_filter_nodes_degree_float_constants_gql(graph): + # A float constant is compared as written: 4.5 sits between 4 and 5, and a + # fractional set member matches no degree while a whole one still does. queries_and_expected_outputs = [] for direction in ["BOTH", "IN", "OUT"]: queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - _degree(direction, "eq", f'{{ str: "{threshold_str}" }}'), - _expected_degree_select_names( - graph, direction, lambda d: d == int(threshold_str) - ), + _degree(direction, "ge", "{ f64: 4.5 }"), + _expected_degree_select_names(graph, direction, lambda d: d >= 4.5), ) ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - _degree(direction, "eq", f'{{ str: "{threshold_str}" }}'), - _expected_degree_names( - graph, direction, lambda d: d == int(threshold_str) - ), + _degree(direction, "ge", "{ f64: 4.5 }"), + _expected_degree_names(graph, direction, lambda d: d >= 4.5), ) ) - queries_and_expected_outputs.append( _degree_select_nodes_query_expected_pair( - _degree(direction, "ge", f"{{ f64: {threshold_float} }}"), - _expected_degree_select_names( - graph, direction, lambda d: d >= int(threshold_float) - ), + _degree(direction, "eq", "{ f64: 3.0 }"), + _expected_degree_select_names(graph, direction, lambda d: d == 3), ) ) queries_and_expected_outputs.append( _degree_filter_nodes_query_expected_pair( - _degree(direction, "ge", f"{{ f64: {threshold_float} }}"), - _expected_degree_names( - graph, direction, lambda d: d >= int(threshold_float) - ), + _degree(direction, "isIn", "{ list: [{f64: 3.0}, {f64: 4.9}] }"), + _expected_degree_names(graph, direction, lambda d: d == 3), ) ) - queries_and_expected_outputs.append( - _degree_select_nodes_query_expected_pair( - _degree(direction, "isIn", f'{{ list: [{{str: "3"}}, {{f64: 4.9}}] }}'), - _expected_degree_select_names(graph, direction, lambda d: d in [3, 4]), - ) - ) - queries_and_expected_outputs.append( - _degree_filter_nodes_query_expected_pair( - _degree(direction, "isIn", f'{{ list: [{{str: "3"}}, {{f64: 4.9}}] }}'), - _expected_degree_names(graph, direction, lambda d: d in [3, 4]), - ) + run_group_graphql_test(queries_and_expected_outputs, graph, sort_output=True) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_filter_nodes_degree_string_constants_gql(graph): + # A string constant never compares with a degree, even one that spells a + # number; in a set it is simply not a member, so the numbers still count. + for direction in ["BOTH", "IN", "OUT"]: + expr = _degree(direction, "eq", '{ str: "4" }') + query = f""" + query {{ + graph(path: "g") {{ + filterNodes: filter(expr: {expr}) {{ + nodes {{ list {{ name }} }} + }} + }} + }} + """ + run_graphql_error_test( + query, + 'Invalid filter: value Str(ArcStr("4")) of type Str cannot be compared with U64', + graph, ) + queries_and_expected_outputs = [ + _degree_filter_nodes_query_expected_pair( + _degree("BOTH", "isIn", '{ list: [{str: "3"}, {u64: 4}] }'), + _expected_degree_names(graph, "BOTH", lambda d: d == 4), + ), + _degree_filter_nodes_query_expected_pair( + _degree("BOTH", "isNotIn", '{ list: [{str: "3"}, {u64: 4}] }'), + _expected_degree_names(graph, "BOTH", lambda d: d != 4), + ), + _degree_filter_nodes_query_expected_pair( + _degree("OUT", "isIn", '{ list: [{str: "a"}, {str: "b"}] }'), + _expected_degree_names(graph, "OUT", lambda d: False), + ), + _degree_filter_nodes_query_expected_pair( + _degree("BOTH", "isNotIn", '{ list: [{str: "x"}, {str: "y"}] }'), + _expected_degree_names(graph, "BOTH", lambda d: True), + ), + ] run_group_graphql_test(queries_and_expected_outputs, graph, sort_output=True) @@ -564,8 +585,6 @@ def test_filter_nodes_degree_invalid_non_numeric_string_values_gql(graph): invalid_exprs = [ _degree("BOTH", "lt", '{ str: "foo" }'), _degree("IN", "eq", '{ str: "bar" }'), - _degree("OUT", "isIn", '{ list: [{str: "a"}, {str: "b"}] }'), - _degree("BOTH", "isNotIn", '{ list: [{str: "x"}, {str: "y"}] }'), ] for expr in invalid_exprs: diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py index aba4c397d7..d92eaf1843 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py @@ -104,7 +104,7 @@ def test_node_property_filter_equal_type_error(graph): } """ expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be coerced to List" + "Invalid filter: value I64(1) of type I64 cannot be compared with List" ) run_graphql_error_test(query, expected_error_message, graph) @@ -158,7 +158,7 @@ def test_node_property_filter_not_equal_type_error(graph): } """ expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be coerced to Bool" + "Invalid filter: value I64(1) of type I64 cannot be compared with Bool" ) run_graphql_error_test(query, expected_error_message, graph) @@ -209,7 +209,7 @@ def test_node_property_filter_greater_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -261,7 +261,7 @@ def test_node_property_filter_less_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -307,7 +307,7 @@ def test_node_property_filter_greater_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -355,7 +355,7 @@ def test_node_property_filter_less_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be coerced to I64' + expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) diff --git a/raphtory-api/src/core/entities/properties/prop/prop_type.rs b/raphtory-api/src/core/entities/properties/prop/prop_type.rs index 22513cdb6d..e5fba85051 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_type.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_type.rs @@ -181,16 +181,41 @@ impl PropType { matches!(self, PropType::DTime | PropType::NDTime) } + /// An unresolved type: nothing is known about the values yet, so every + /// capability check below passes and the real check happens once the + /// type is resolved. + pub fn is_unknown(&self) -> bool { + matches!(self, PropType::Empty) + } + pub fn has_add(&self) -> bool { - self.is_numeric() || self.is_str() + self.is_unknown() || self.is_numeric() || self.is_str() } pub fn has_divide(&self) -> bool { - self.is_numeric() + self.is_unknown() || self.is_numeric() } + /// Whether values of this type have an ordering (`<`, `<=`, `>`, `>=`). pub fn has_cmp(&self) -> bool { - self.is_bool() || self.is_numeric() || self.is_str() || self.is_date() + self.is_unknown() || self.is_bool() || self.is_numeric() || self.is_str() || self.is_date() + } + + /// Whether a value of this type can ever equal a value of `other`. + /// + /// Numbers compare by value whatever their width, so every numeric type is + /// comparable with every other. Lists are comparable when their elements + /// are; two maps always are, since a declared map type is the union of the + /// shapes its values take. Anything else must be the same type. An + /// unresolved side is comparable with everything. + pub fn is_comparable_with(&self, other: &PropType) -> bool { + match (self, other) { + (PropType::Empty, _) | (_, PropType::Empty) => true, + (l, r) if l.is_numeric() && r.is_numeric() => true, + (PropType::List(l), PropType::List(r)) => l.is_comparable_with(r), + (PropType::Map(_), PropType::Map(_)) => true, + (l, r) => l == r, + } } pub fn homogeneous_map_value_type(&self) -> Option { @@ -536,6 +561,69 @@ mod test { } } + #[test] + fn unknown_type_has_every_capability() { + assert!(PropType::Empty.has_cmp()); + assert!(PropType::Empty.has_add()); + assert!(PropType::Empty.has_divide()); + } + + #[test] + fn ordering_exists_for_scalars_only() { + for pt in [PropType::U8, PropType::F32, PropType::Decimal { scale: 2 }] { + assert!(pt.has_cmp(), "{pt}"); + } + for pt in [ + PropType::Str, + PropType::Bool, + PropType::DTime, + PropType::NDTime, + ] { + assert!(pt.has_cmp(), "{pt}"); + } + for pt in [ + PropType::List(Box::new(PropType::I64)), + PropType::map([("a", PropType::I64)]), + ] { + assert!(!pt.has_cmp(), "{pt}"); + } + } + + #[test] + fn comparable_types() { + let list = |inner| PropType::List(Box::new(inner)); + let comparable = [ + (PropType::U8, PropType::F64), + (PropType::I64, PropType::Decimal { scale: 3 }), + (PropType::Str, PropType::Str), + (PropType::Bool, PropType::Bool), + (PropType::Empty, PropType::Str), + (list(PropType::I64), list(PropType::F32)), + (list(PropType::Empty), list(PropType::Str)), + ( + PropType::map([("a", PropType::I64)]), + PropType::map([("b", PropType::Str)]), + ), + ]; + for (l, r) in comparable { + assert!(l.is_comparable_with(&r), "{l} vs {r}"); + assert!(r.is_comparable_with(&l), "{r} vs {l}"); + } + let incomparable = [ + (PropType::U64, PropType::Str), + (PropType::Bool, PropType::I64), + (PropType::Str, PropType::DTime), + (PropType::DTime, PropType::NDTime), + (PropType::I64, list(PropType::I64)), + (list(PropType::I64), list(PropType::Str)), + (PropType::map([("a", PropType::I64)]), PropType::Str), + ]; + for (l, r) in incomparable { + assert!(!l.is_comparable_with(&r), "{l} vs {r}"); + assert!(!r.is_comparable_with(&l), "{r} vs {l}"); + } + } + #[test] fn test_unify_types_ne() { let l = PropType::List(Box::new(PropType::U8)); diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index d3b961aa78..92a4d878c7 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -2086,163 +2086,105 @@ mod test_node_filter { } proptest! { - #[test] - fn prop_degree_filter_with_string_threshold(threshold in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_str = threshold.to_string(); - let parsed_str = threshold_str.parse::().unwrap(); - - assert_filter(&graph, NodeFilter.degree().lt(threshold_str.clone()), Direction::BOTH, |d| d < parsed_str as usize, "BOTH < string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().le(threshold_str.clone()), Direction::BOTH, |d| d <= parsed_str as usize, "BOTH <= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().eq(threshold_str.clone()), Direction::BOTH, |d| d == parsed_str as usize, "BOTH == string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().ne(threshold_str.clone()), Direction::BOTH, |d| d != parsed_str as usize, "BOTH != string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().ge(threshold_str.clone()), Direction::BOTH, |d| d >= parsed_str as usize, "BOTH >= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.degree().gt(threshold_str.clone()), Direction::BOTH, |d| d > parsed_str as usize, "BOTH > string threshold parsed to u64"); - - assert_filter(&graph, NodeFilter.in_degree().lt(threshold_str.clone()), Direction::IN, |d| d < parsed_str as usize, "IN < string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().le(threshold_str.clone()), Direction::IN, |d| d <= parsed_str as usize, "IN <= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().eq(threshold_str.clone()), Direction::IN, |d| d == parsed_str as usize, "IN == string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().ne(threshold_str.clone()), Direction::IN, |d| d != parsed_str as usize, "IN != string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().ge(threshold_str.clone()), Direction::IN, |d| d >= parsed_str as usize, "IN >= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.in_degree().gt(threshold_str.clone()), Direction::IN, |d| d > parsed_str as usize, "IN > string threshold parsed to u64"); - - assert_filter(&graph, NodeFilter.out_degree().lt(threshold_str.clone()), Direction::OUT, |d| d < parsed_str as usize, "OUT < string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().le(threshold_str.clone()), Direction::OUT, |d| d <= parsed_str as usize, "OUT <= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().eq(threshold_str.clone()), Direction::OUT, |d| d == parsed_str as usize, "OUT == string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().ne(threshold_str.clone()), Direction::OUT, |d| d != parsed_str as usize, "OUT != string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().ge(threshold_str.clone()), Direction::OUT, |d| d >= parsed_str as usize, "OUT >= string threshold parsed to u64"); - assert_filter(&graph, NodeFilter.out_degree().gt(threshold_str), Direction::OUT, |d| d > parsed_str as usize, "OUT > string threshold parsed to u64"); - } - #[test] fn prop_degree_filter_with_float_threshold(threshold in 0u64..15) { let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_float = threshold as f64; - let parsed_float = threshold_float as u64; - - assert_filter(&graph, NodeFilter.degree().lt(threshold_float), Direction::BOTH, |d| d < parsed_float as usize, "BOTH < float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().le(threshold_float), Direction::BOTH, |d| d <= parsed_float as usize, "BOTH <= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().eq(threshold_float), Direction::BOTH, |d| d == parsed_float as usize, "BOTH == float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().ne(threshold_float), Direction::BOTH, |d| d != parsed_float as usize, "BOTH != float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().ge(threshold_float), Direction::BOTH, |d| d >= parsed_float as usize, "BOTH >= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.degree().gt(threshold_float), Direction::BOTH, |d| d > parsed_float as usize, "BOTH > float threshold cast to u64"); - - assert_filter(&graph, NodeFilter.in_degree().lt(threshold_float), Direction::IN, |d| d < parsed_float as usize, "IN < float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().le(threshold_float), Direction::IN, |d| d <= parsed_float as usize, "IN <= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().eq(threshold_float), Direction::IN, |d| d == parsed_float as usize, "IN == float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().ne(threshold_float), Direction::IN, |d| d != parsed_float as usize, "IN != float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().ge(threshold_float), Direction::IN, |d| d >= parsed_float as usize, "IN >= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.in_degree().gt(threshold_float), Direction::IN, |d| d > parsed_float as usize, "IN > float threshold cast to u64"); - - assert_filter(&graph, NodeFilter.out_degree().lt(threshold_float), Direction::OUT, |d| d < parsed_float as usize, "OUT < float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().le(threshold_float), Direction::OUT, |d| d <= parsed_float as usize, "OUT <= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().eq(threshold_float), Direction::OUT, |d| d == parsed_float as usize, "OUT == float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().ne(threshold_float), Direction::OUT, |d| d != parsed_float as usize, "OUT != float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().ge(threshold_float), Direction::OUT, |d| d >= parsed_float as usize, "OUT >= float threshold cast to u64"); - assert_filter(&graph, NodeFilter.out_degree().gt(threshold_float), Direction::OUT, |d| d > parsed_float as usize, "OUT > float threshold cast to u64"); - } - - #[test] - fn prop_degree_filter_with_string_is_in(threshold_a in 0u64..15, threshold_b in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_a_str = threshold_a.to_string(); - let threshold_b_str = threshold_b.to_string(); - let parsed_a = threshold_a_str.parse::().unwrap(); - let parsed_b = threshold_b_str.parse::().unwrap(); - let set = [parsed_a, parsed_b]; - - assert_filter(&graph, NodeFilter.degree().is_in(vec![threshold_a_str.clone().into_prop(), threshold_b_str.clone().into_prop()]), Direction::BOTH, |d| set.contains(&(d as u64)), "BOTH is_in(string thresholds parsed to u64)"); - assert_filter(&graph, NodeFilter.in_degree().is_in(vec![threshold_a_str.clone().into_prop(), threshold_b_str.clone().into_prop()]), Direction::IN, |d| set.contains(&(d as u64)), "IN is_in(string thresholds parsed to u64)"); - assert_filter(&graph, NodeFilter.out_degree().is_in(vec![threshold_a_str.into_prop(), threshold_b_str.into_prop()]), Direction::OUT, |d| set.contains(&(d as u64)), "OUT is_in(string thresholds parsed to u64)"); - } - - #[test] - fn prop_degree_filter_with_string_is_not_in(threshold_a in 0u64..15, threshold_b in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_a_str = threshold_a.to_string(); - let threshold_b_str = threshold_b.to_string(); - let parsed_a = threshold_a_str.parse::().unwrap(); - let parsed_b = threshold_b_str.parse::().unwrap(); - let set = [parsed_a, parsed_b]; - - assert_filter(&graph, NodeFilter.degree().is_not_in(vec![threshold_a_str.clone().into_prop(), threshold_b_str.clone().into_prop()]), Direction::BOTH, |d| !set.contains(&(d as u64)), "BOTH is_not_in(string thresholds parsed to u64)"); - assert_filter(&graph, NodeFilter.in_degree().is_not_in(vec![threshold_a_str.clone().into_prop(), threshold_b_str.clone().into_prop()]), Direction::IN, |d| !set.contains(&(d as u64)), "IN is_not_in(string thresholds parsed to u64)"); - assert_filter(&graph, NodeFilter.out_degree().is_not_in(vec![threshold_a_str.into_prop(), threshold_b_str.into_prop()]), Direction::OUT, |d| !set.contains(&(d as u64)), "OUT is_not_in(string thresholds parsed to u64)"); - } - - #[test] - fn prop_degree_filter_with_float_is_in(threshold_a in 0u64..15, threshold_b in 0u64..15) { - let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_a_float = threshold_a as f64; - let threshold_b_float = threshold_b as f64; - let parsed_a = threshold_a_float as u64; - let parsed_b = threshold_b_float as u64; - let set = [parsed_a, parsed_b]; - - assert_filter(&graph, NodeFilter.degree().is_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::BOTH, |d| set.contains(&(d as u64)), "BOTH is_in(float thresholds cast to u64)"); - assert_filter(&graph, NodeFilter.in_degree().is_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::IN, |d| set.contains(&(d as u64)), "IN is_in(float thresholds cast to u64)"); - assert_filter(&graph, NodeFilter.out_degree().is_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::OUT, |d| set.contains(&(d as u64)), "OUT is_in(float thresholds cast to u64)"); + let whole = threshold as f64; + let t = threshold as usize; + let degrees = [ + (NodeFilter.degree(), Direction::BOTH), + (NodeFilter.in_degree(), Direction::IN), + (NodeFilter.out_degree(), Direction::OUT), + ]; + for (degree, dir) in degrees { + // A whole-number float names that number. + assert_filter(&graph, degree.clone().lt(whole), dir, |d| d < t, "< whole float"); + assert_filter(&graph, degree.clone().le(whole), dir, |d| d <= t, "<= whole float"); + assert_filter(&graph, degree.clone().eq(whole), dir, |d| d == t, "== whole float"); + assert_filter(&graph, degree.clone().ne(whole), dir, |d| d != t, "!= whole float"); + assert_filter(&graph, degree.clone().ge(whole), dir, |d| d >= t, ">= whole float"); + assert_filter(&graph, degree.clone().gt(whole), dir, |d| d > t, "> whole float"); + // A fraction is compared as written; nothing is rounded away. + let half = whole + 0.5; + assert_filter(&graph, degree.clone().lt(half), dir, |d| d <= t, "< half float"); + assert_filter(&graph, degree.clone().le(half), dir, |d| d <= t, "<= half float"); + assert_filter(&graph, degree.clone().eq(half), dir, |_| false, "== half float"); + assert_filter(&graph, degree.clone().ne(half), dir, |_| true, "!= half float"); + assert_filter(&graph, degree.clone().ge(half), dir, |d| d > t, ">= half float"); + assert_filter(&graph, degree.clone().gt(half), dir, |d| d > t, "> half float"); + } } #[test] - fn prop_degree_filter_with_float_is_not_in(threshold_a in 0u64..15, threshold_b in 0u64..15) { + fn prop_degree_filter_with_float_is_in(a in 0u64..15, b in 0u64..15) { let graph = degree_graph_with_add_node_and_add_edge(); - let threshold_a_float = threshold_a as f64; - let threshold_b_float = threshold_b as f64; - let parsed_a = threshold_a_float as u64; - let parsed_b = threshold_b_float as u64; - let set = [parsed_a, parsed_b]; - - assert_filter(&graph, NodeFilter.degree().is_not_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::BOTH, |d| !set.contains(&(d as u64)), "BOTH is_not_in(float thresholds cast to u64)"); - assert_filter(&graph, NodeFilter.in_degree().is_not_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::IN, |d| !set.contains(&(d as u64)), "IN is_not_in(float thresholds cast to u64)"); - assert_filter(&graph, NodeFilter.out_degree().is_not_in(vec![threshold_a_float.into_prop(), threshold_b_float.into_prop()]), Direction::OUT, |d| !set.contains(&(d as u64)), "OUT is_not_in(float thresholds cast to u64)"); + let whole = [a as f64, b as f64]; + let set = [a as usize, b as usize]; + assert_filter(&graph, NodeFilter.degree().is_in(vec![whole[0].into_prop(), whole[1].into_prop()]), Direction::BOTH, |d| set.contains(&d), "is_in(whole floats)"); + assert_filter(&graph, NodeFilter.degree().is_not_in(vec![whole[0].into_prop(), whole[1].into_prop()]), Direction::BOTH, |d| !set.contains(&d), "is_not_in(whole floats)"); + // A fractional member matches no degree, so only the whole member counts. + let mixed = vec![(a as f64 + 0.5).into_prop(), (b as f64).into_prop()]; + assert_filter(&graph, NodeFilter.degree().is_in(mixed.clone()), Direction::BOTH, |d| d == b as usize, "is_in(half, whole)"); + assert_filter(&graph, NodeFilter.degree().is_not_in(mixed), Direction::BOTH, |d| d != b as usize, "is_not_in(half, whole)"); } + } - #[test] - fn prop_degree_filter_invalid_non_numeric_string_values(value_a in "[a-zA-Z]{1,8}", value_b in "[a-zA-Z]{1,8}") { - let graph = degree_graph_with_add_node_and_add_edge(); - - let invalid_filters = vec![ - NodeFilter.degree().lt(value_a.clone()), - NodeFilter.degree().le(value_a.clone()), - NodeFilter.degree().eq(value_a.clone()), - NodeFilter.degree().ne(value_a.clone()), - NodeFilter.degree().ge(value_a.clone()), - NodeFilter.degree().gt(value_a.clone()), - NodeFilter.in_degree().lt(value_a.clone()), - NodeFilter.in_degree().le(value_a.clone()), - NodeFilter.in_degree().eq(value_a.clone()), - NodeFilter.in_degree().ne(value_a.clone()), - NodeFilter.in_degree().ge(value_a.clone()), - NodeFilter.in_degree().gt(value_a.clone()), - NodeFilter.out_degree().lt(value_a.clone()), - NodeFilter.out_degree().le(value_a.clone()), - NodeFilter.out_degree().eq(value_a.clone()), - NodeFilter.out_degree().ne(value_a.clone()), - NodeFilter.out_degree().ge(value_a.clone()), - NodeFilter.out_degree().gt(value_a.clone()), - ]; - for filter in invalid_filters { - assert!( - matches!(graph.filter(filter), Err(GraphError::InvalidFilter(_))), - "expected InvalidFilter for non-numeric string values" - ); - } - - let set_invalid_filters = vec![ - NodeFilter.degree().is_in(vec![value_a.clone(), value_b.clone()]), - NodeFilter.degree().is_not_in(vec![value_a.clone(), value_b.clone()]), - NodeFilter.in_degree().is_in(vec![value_a.clone(), value_b.clone()]), - NodeFilter.in_degree().is_not_in(vec![value_a.clone(), value_b.clone()]), - NodeFilter.out_degree().is_in(vec![value_a.clone(), value_b.clone()]), - NodeFilter.out_degree().is_not_in(vec![value_a.clone(), value_b.clone()]), + #[test] + fn degree_never_compares_with_a_string() { + let graph = degree_graph_with_add_node_and_add_edge(); + let degrees = [ + (NodeFilter.degree(), Direction::BOTH), + (NodeFilter.in_degree(), Direction::IN), + (NodeFilter.out_degree(), Direction::OUT), + ]; + for (degree, dir) in degrees { + // A string constant is refused even when it spells a number. + let refused = [ + degree.clone().eq("3"), + degree.clone().ne("3"), + degree.clone().lt("3"), + degree.clone().le("3"), + degree.clone().gt("3"), + degree.clone().ge("3"), ]; - for filter in set_invalid_filters { + for filter in refused { assert!( matches!(graph.filter(filter), Err(GraphError::InvalidFilter(_))), - "expected InvalidFilter for is_in/is_not_in on numeric degree" + "a string constant must not compare with a degree" ); } + // In a set, a string member can never be the degree, so it is simply + // not there; the numeric members still count. + let strings_only = vec!["3".into_prop(), "4".into_prop()]; + assert_filter( + &graph, + degree.clone().is_in(strings_only.clone()), + dir, + |_| false, + "is_in(strings)", + ); + assert_filter( + &graph, + degree.clone().is_not_in(strings_only), + dir, + |_| true, + "is_not_in(strings)", + ); + let mixed = vec!["3".into_prop(), 4u64.into_prop()]; + assert_filter( + &graph, + degree.clone().is_in(mixed.clone()), + dir, + |d| d == 4, + "is_in(string, number)", + ); + assert_filter( + &graph, + degree.is_not_in(mixed), + dir, + |d| d != 4, + "is_not_in(string, number)", + ); } } @@ -3052,6 +2994,70 @@ mod test_node_property_filter { ); } + #[test] + fn test_filter_nodes_for_property_against_a_fraction() { + // p40 is a U64 property. Latest values: node "1" holds 15, node "2" + // holds 20; first values: 5 and 10. A fractional constant is compared + // as written, not rounded to the property's type. + let filter = NodeFilter.property("p40").ge(15.5); + let expected_results = vec!["2"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + let filter = NodeFilter.property("p40").lt(15.5); + let expected_results = vec!["1"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + let filter = NodeFilter.property("p40").temporal().first().lt(5.5); + let expected_results = vec!["1"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + // Set members compare by value across numeric widths; a member of a + // type the property can never equal is simply absent. + let filter = NodeFilter.property("p40").is_in(vec![ + Prop::Str("20".into()), + Prop::F64(15.0), + Prop::U64(20), + ]); + let expected_results = vec!["1", "2"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + let filter = NodeFilter + .property("p40") + .is_in(vec![Prop::Str("20".into()), Prop::F64(15.5)]); + let expected_results: Vec<&str> = vec![]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + } + #[test] fn test_filter_nodes_for_property_gt() { let filter = NodeFilter.property("p2").gt(2u64); diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs index f9bc339ff6..94d1a6de52 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -18,7 +18,7 @@ use crate::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, model::{ - coerce_set_values, + comparable_set_values, edge_filter::EdgeFilter, elem_prop_type, filter_operator::ElemQual, @@ -27,8 +27,8 @@ use crate::{ ops::{AllEdgeOp, AnyEdgeOp}, CreateOp, }, - resolved_prop_type, validate_binary_op, validate_const_castable, - validate_string_op, validate_types_compatible, CreateFilter, ExplodedEdgeFilter, + resolved_prop_type, validate_binary_op, validate_const_comparable, + validate_string_op, validate_types_comparable, CreateFilter, ExplodedEdgeFilter, }, }, }, @@ -105,8 +105,8 @@ where let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; match right.const_value() { - Some(c) => validate_const_castable(&lhs_pt, c.as_ref())?, - None => validate_types_compatible(&lhs_pt, &rhs_pt)?, + Some(c) => validate_const_comparable(&lhs_pt, c.as_ref())?, + None => validate_types_comparable(&lhs_pt, &rhs_pt)?, } let op: Arc + 'graph> = if quals.is_empty() { Arc::new(BinaryCmpEdgeOp { @@ -171,8 +171,8 @@ where let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; match right.const_value() { - Some(c) => validate_const_castable(&lhs_pt, c.as_ref())?, - None => validate_types_compatible(&lhs_pt, &rhs_pt)?, + Some(c) => validate_const_comparable(&lhs_pt, c.as_ref())?, + None => validate_types_comparable(&lhs_pt, &rhs_pt)?, } let op: Arc + 'graph> = if quals.is_empty() { Arc::new(BinaryCmpEdgeOp { @@ -335,7 +335,7 @@ where let right = self.right.create_edge_op(filtered.clone())?; validate_string_op(&elem_prop_type(&left.prop_type(), quals.len())?)?; match right.const_value() { - Some(c) => validate_const_castable(&PropType::Str, c.as_ref())?, + Some(c) => validate_const_comparable(&PropType::Str, c.as_ref())?, None => {} } let op: Arc + 'graph> = if quals.is_empty() { @@ -397,7 +397,7 @@ where let right = self.right.create_edge_op(filtered.clone())?; validate_string_op(&elem_prop_type(&left.prop_type(), quals.len())?)?; match right.const_value() { - Some(c) => validate_const_castable(&PropType::Str, c.as_ref())?, + Some(c) => validate_const_comparable(&PropType::Str, c.as_ref())?, None => {} } let op: Arc + 'graph> = if quals.is_empty() { @@ -458,7 +458,7 @@ impl CreateFilter for PropValueSetExpr { let expr_pt = self.expr.prop_type(); let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; - let values = coerce_set_values(&lhs_pt, self.values)?; + let values = comparable_set_values(&lhs_pt, self.values); let op: Arc + 'graph> = if quals.is_empty() { Arc::new(PropValueSetEdgeOp { inner, @@ -513,7 +513,7 @@ impl CreateFilter for PropValueSetExpr { let expr_pt = self.expr.prop_type(); let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; - let values = coerce_set_values(&lhs_pt, self.values)?; + let values = comparable_set_values(&lhs_pt, self.values); let op: Arc + 'graph> = if quals.is_empty() { Arc::new(PropValueSetEdgeOp { inner, diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index e3ccc6dc47..9741df7ffb 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -56,10 +56,7 @@ use crate::{ prelude::LayerOps, }; use raphtory_api::core::{ - entities::{ - properties::prop::{unify_types, Prop}, - Layer, - }, + entities::{properties::prop::Prop, Layer}, storage::timeindex::{AsTime, EventTime}, utils::time::IntoTime, }; @@ -955,25 +952,25 @@ impl PredicateLhs for Latest {} impl PredicateLhs for SnapshotAt {} impl PredicateLhs for SnapshotLatest {} -/// Reject ordering operators on boolean properties. -//. TODO: Also check if both the types are comparable. +/// Reject ordering operators on a type that has no ordering. +/// +/// An unresolved type (`PropType::Empty`) passes; the check runs again once +/// the type is known. pub fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), GraphError> { - if matches!( + let ordering = matches!( op, BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge - ) { - if matches!(prop_type, PropType::Map(_)) { - return Err(GraphError::InvalidFilter(format!( - "operator {:?} is not valid for map properties", - op - ))); - } - if matches!(prop_type, PropType::List(_)) { - return Err(GraphError::InvalidFilter(format!( - "operator {:?} is not valid for list properties", - op - ))); - } + ); + if ordering && !prop_type.has_cmp() { + let kind = match prop_type { + PropType::List(_) => "list".to_string(), + PropType::Map(_) => "map".to_string(), + other => other.to_string(), + }; + return Err(GraphError::InvalidFilter(format!( + "operator {:?} is not valid for {} properties", + op, kind + ))); } Ok(()) } @@ -982,7 +979,7 @@ pub fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), Gra /// /// Only fires when the type is known (`!= PropType::Empty`). pub fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { - if *prop_type != PropType::Empty && *prop_type != PropType::Str { + if !(prop_type.is_unknown() || prop_type.is_str()) { return Err(GraphError::InvalidFilter(format!( "string operator requires a Str property, but the property type is {}", prop_type @@ -1005,121 +1002,41 @@ pub fn resolved_prop_type(expr_pt: PropType, op_pt: PropType) -> PropType { } } -/// Reject a constant RHS value whose type cannot be coerced to the LHS type. +/// Reject a constant compared against an expression whose type it can never +/// equal. /// -/// Cast a constant to the type an expression wants to compare it as, erroring -/// when it does not convert. Used where the expression defines the comparison's -/// type rather than adopting the constant's — see [`CreateOp::const_cast_type`]. -pub fn cast_const_to(target: &PropType, value: Option<&Prop>) -> Result, GraphError> { - value.map(|v| cast_prop_to(target, v)).transpose() -} - -/// [`cast_const_to`] for a value that is known to be present. -pub fn cast_prop_to(target: &PropType, value: &Prop) -> Result { - if value.dtype() == *target { - return Ok(value.clone()); - } - value.clone().try_cast(target.clone()).map_err(|v| { - GraphError::InvalidFilter(format!( - "value {:?} of type {} cannot be compared as {}", - v, - v.dtype(), - target - )) - }) -} - -/// Only fires when both sides are known and the RHS is a literal/const. Defers -/// to runtime when the LHS type is unknown (`PropType::Empty`) or the RHS isn't -/// a const value. -pub fn validate_const_castable( +/// Constants are never converted: a numeric constant compares by value with +/// any numeric expression (`degree() > 2.5` keeps the `.5`), a string constant +/// never compares with a number, and so on. A missing constant (`None`) and an +/// unresolved expression type both pass. +pub fn validate_const_comparable( lhs_pt: &PropType, - rhs_const: Option<&Prop>, + value: Option<&Prop>, ) -> Result<(), GraphError> { - if *lhs_pt == PropType::Empty { - return Ok(()); - } - if let Some(rhs) = rhs_const { - // Map values carry partial schemas against a union-schema declared - // type and compare structurally at runtime; a non-map constant can - // never match a map property. - if matches!(lhs_pt, PropType::Map(_)) { - return if matches!(rhs, Prop::Map(_)) { - Ok(()) - } else { - Err(GraphError::InvalidFilter(format!( - "value {:?} of type {} cannot be coerced to {}", - rhs, - rhs.dtype(), - lhs_pt - ))) - }; - } - // A numeric constant may target any numeric property, since comparison - // widens across the numeric variants. Everything else must unify with - // the property's own type — a constant that merely *casts* into it - // (a numeric string, a bool read as an int) is a cross-type comparison - // the caller almost certainly did not mean. - let rhs_pt = rhs.dtype(); - let compatible = (lhs_pt.is_numeric() && rhs_pt.is_numeric()) - || unify_types(lhs_pt, &rhs_pt, &mut false).is_ok(); - if !compatible { - return Err(GraphError::InvalidFilter(format!( - "value {:?} of type {} cannot be coerced to {}", - rhs, rhs_pt, lhs_pt - ))); + match value { + Some(v) if !lhs_pt.is_comparable_with(&v.dtype()) => { + Err(GraphError::InvalidFilter(format!( + "value {:?} of type {} cannot be compared with {}", + v, + v.dtype(), + lhs_pt + ))) } + _ => Ok(()), } - Ok(()) } -/// A representative value for a given `PropType` — used to check type-level -/// compatibility via the value-based `Prop::try_cast` matrix. Returns `None` -/// for composite types (List, Map) where no canonical scalar default exists. -fn representative_prop(pt: &PropType) -> Option { - Some(match pt { - PropType::Str => Prop::Str("".into()), - PropType::U8 => Prop::U8(0), - PropType::U16 => Prop::U16(0), - PropType::U32 => Prop::U32(0), - PropType::U64 => Prop::U64(0), - PropType::I32 => Prop::I32(0), - PropType::I64 => Prop::I64(0), - PropType::F32 => Prop::F32(0.0), - PropType::F64 => Prop::F64(0.0), - PropType::Bool => Prop::Bool(false), - PropType::Empty - | PropType::List(_) - | PropType::Map(_) - | PropType::NDTime - | PropType::DTime - | PropType::Decimal { .. } => return None, - }) -} - -/// Reject a binary comparison where LHS and RHS types are known but incompatible. -/// -/// Complements `validate_const_castable` (which only checks const RHS) by also -/// catching mismatches when the RHS is another expression with a declared -/// `prop_type`. Uses the same `Prop::try_cast` matrix for coercion checks via -/// a representative value, so the numeric family (U/I/F) is considered -/// compatible while cross-domain (Bool vs U64, Str vs I64) is rejected. -/// -/// Both sides being `Empty` defers to runtime (no-op). -pub fn validate_types_compatible(lhs_pt: &PropType, rhs_pt: &PropType) -> Result<(), GraphError> { - if *lhs_pt == PropType::Empty || *rhs_pt == PropType::Empty || lhs_pt == rhs_pt { - return Ok(()); - } - let castable = representative_prop(rhs_pt) - .and_then(|v| v.try_cast(lhs_pt.clone()).ok()) - .is_some(); - if !castable { - return Err(GraphError::InvalidFilter(format!( +/// Reject a comparison between two expressions whose types can never be +/// equal. Either side being unresolved defers to runtime. +pub fn validate_types_comparable(lhs_pt: &PropType, rhs_pt: &PropType) -> Result<(), GraphError> { + if lhs_pt.is_comparable_with(rhs_pt) { + Ok(()) + } else { + Err(GraphError::InvalidFilter(format!( "type mismatch: lhs is {}, rhs is {}", lhs_pt, rhs_pt - ))); + ))) } - Ok(()) } /// Reject aggregators called on a declared scalar expression. @@ -1158,37 +1075,19 @@ pub fn require_aggregable(pt: &PropType, op: &str) -> Result<(), GraphError> { } } -/// Narrow an `is_in`/`is_not_in` set to the members that could match the LHS. +/// Narrow an `is_in`/`is_not_in` set to the members that could equal the LHS. /// /// Set membership asks whether a value is present, so a member of a type the -/// LHS can never equal simply is not present — it is dropped rather than -/// rejected, leaving `is_in` answering "no" where a comparison would refuse the -/// question. Members that share the LHS type, or are numeric alongside a -/// numeric LHS, are kept and cast so the runtime comparison sees one type. -/// An unknown LHS type (`PropType::Empty`) defers the whole decision to -/// runtime. -pub fn coerce_set_values(lhs_pt: &PropType, values: Vec) -> Result, GraphError> { - if *lhs_pt == PropType::Empty { - return Ok(values); - } - // Map values carry partial schemas and compare structurally; the declared - // map type is the union schema, so per-value coercion would reject - // legitimate members. - if matches!(lhs_pt, PropType::Map(_)) { - return Ok(values); - } - Ok(values +/// LHS can never equal simply is not present: it is dropped rather than +/// rejected, leaving `is_in` answering "no" where a comparison would refuse +/// the question. The members that remain are kept exactly as written; the +/// runtime comparison handles mixed numeric widths by value. An unresolved +/// LHS type keeps every member. +pub fn comparable_set_values(lhs_pt: &PropType, values: Vec) -> Vec { + values .into_iter() - .filter_map(|v| { - if v.dtype() == *lhs_pt { - return Some(v); - } - if lhs_pt.is_numeric() && v.dtype().is_numeric() { - return v.clone().try_cast(lhs_pt.clone()).ok().or(Some(v)); - } - None - }) - .collect()) + .filter(|v| lhs_pt.is_comparable_with(&v.dtype())) + .collect() } pub trait CombinedFilter: CreateFilter + Clone + Send + Sync + 'static {} diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 9be87b7d23..76afb382c6 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -76,8 +76,6 @@ impl DynTemporal for EdgeEndpointWrapper> { } pub trait DynCreateOp: DynEntityExpr { - fn dyn_const_cast_type(&self) -> Option; - fn dyn_selects_node_id(&self) -> bool; fn dyn_create_node_op<'g>( @@ -102,10 +100,6 @@ pub trait DynCreateOp: DynEntityExpr { } impl DynCreateOp for E { - fn dyn_const_cast_type(&self) -> Option { - self.const_cast_type() - } - fn dyn_selects_node_id(&self) -> bool { self.selects_node_id() } @@ -158,10 +152,6 @@ impl EntityExpr for Arc { impl PredicateLhs for Arc {} impl CreateOp for Arc { - fn const_cast_type(&self) -> Option { - self.as_ref().dyn_const_cast_type() - } - fn selects_node_id(&self) -> bool { self.as_ref().dyn_selects_node_id() } 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 6b67dfd964..f2d23a518c 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 @@ -479,10 +479,6 @@ impl EntityExpr for DegreeExpr impl PredicateLhs for DegreeExpr {} impl CreateOp for DegreeExpr { - fn const_cast_type(&self) -> Option { - Some(PropType::U64) - } - fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, 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 cbb614a35c..dcb852c8df 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 @@ -51,12 +51,12 @@ use super::{ use crate::{ db::{ api::{ - state::ops::{Const, NodeOp}, + state::ops::NodeOp, view::{internal::GraphView, BoxableGraphView}, }, graph::views::filter::{ model::{ - cast_const_to, cast_prop_to, coerce_set_values, + comparable_set_values, edge_expr::{ ops::{ ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, @@ -66,8 +66,8 @@ use crate::{ }, elem_prop_type, filter_operator::{BinaryOp, ElemQual, SetOp, StringOp, UnaryOp}, - resolved_prop_type, validate_binary_op, validate_const_castable, - validate_string_op, validate_types_compatible, ComposableFilter, CreateFilter, + resolved_prop_type, validate_binary_op, validate_const_comparable, + validate_string_op, validate_types_comparable, ComposableFilter, CreateFilter, EntityMarker, ExplodedEdgeFilter, }, node_filtered_graph::NodeFilteredGraph, @@ -229,18 +229,9 @@ where let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len())?; let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); validate_binary_op(&self.op, &lhs_pt)?; - let mut right = right; match right.const_value() { - Some(c) => match self.left.const_cast_type() { - // The expression fixes the comparison's type, so the constant is - // converted into it rather than compared across types. - Some(target) => { - let casted = cast_const_to(&target, c.as_ref())?; - right = Arc::new(Const(casted)); - } - None => validate_const_castable(&lhs_pt, c.as_ref())?, - }, - None => validate_types_compatible(&lhs_pt, &rhs_pt)?, + Some(c) => validate_const_comparable(&lhs_pt, c.as_ref())?, + None => validate_types_comparable(&lhs_pt, &rhs_pt)?, } if quals.is_empty() { let cmp: Arc + 'graph> = Arc::new(BinaryCmpNodeOp { @@ -639,7 +630,7 @@ impl CreateFilter for StringExpr { quals.len(), )?)?; match right.const_value() { - Some(c) => validate_const_castable(&PropType::Str, c.as_ref())?, + Some(c) => validate_const_comparable(&PropType::Str, c.as_ref())?, None => {} } if quals.is_empty() { @@ -821,18 +812,7 @@ impl CreateFilter for PropValueSetExpr { let id_type = filtered.id_type(); let (inner, quals) = self.expr.create_qualified_node_op(filtered)?; let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; - // An expression that fixes the comparison's type fixes it for set - // members too, so they convert into it or the filter is refused — - // unlike the general case, where a member of an unrelated type is - // simply absent from the set. - let values = match self.expr.const_cast_type() { - Some(target) => self - .values - .into_iter() - .map(|v| cast_prop_to(&target, &v)) - .collect::, _>>()?, - None => coerce_set_values(&lhs_pt, self.values)?, - }; + let values = comparable_set_values(&lhs_pt, self.values); if quals.is_empty() { let gids: Option> = (self.op == SetOp::IsIn && self.expr.selects_node_id()) .then(|| { 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 index 0a98e2797a..be8900197f 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -48,15 +48,6 @@ pub use ops::*; /// ``` /// pub trait CreateOp: EntityExpr + Clone + Send + Sync + 'static { - /// The type a constant compared against this expression is cast to before - /// validation. Degree is a count, so a constant that converts to one — the - /// string `"5"` from a wire query, say — is compared as that count rather - /// than across types. `None` (the default) leaves the constant alone and - /// requires it to match the expression's own type. - fn const_cast_type(&self) -> Option { - None - } - /// Whether this expression selects the node id field. Comparisons against /// constants use it to narrow the evaluation domain to the named nodes /// instead of scanning every node. 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 index 78ac69c3be..7c4d2493ac 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -245,16 +245,15 @@ fn agg_out_type(pt: PropType, scalar: Option) -> PropType { agg_out_type_with(pt, &|elem| scalar.clone().unwrap_or(elem)) } -/// The type a sum produces from its element type. Summing widens: the +/// The type a sum produces from its element type. Integer sums widen: the /// evaluator below accumulates every unsigned width into a `U64`, every signed -/// width into an `I64`, and either into a `Decimal` when that overflows, so a -/// narrow element type would understate what the sum can hold. Keep the arms -/// in step with the evaluator's. +/// width into an `I64`, and either into a `Decimal` when that overflows. +/// Floats keep their width, as `Prop::add` does. Keep the arms in step with +/// the evaluator's. fn sum_out_type(pt: PropType) -> PropType { agg_out_type_with(pt, &|elem| match elem { PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => PropType::U64, PropType::I32 | PropType::I64 => PropType::I64, - PropType::F32 | PropType::F64 => PropType::F64, other => other, }) } @@ -277,9 +276,12 @@ mod sum_out_type_tests { for elem in [PropType::I32, PropType::I64] { assert_eq!(sum_out_type(list(elem)), PropType::I64); } - for elem in [PropType::F32, PropType::F64] { - assert_eq!(sum_out_type(list(elem)), PropType::F64); - } + } + + #[test] + fn float_elements_keep_their_width() { + assert_eq!(sum_out_type(list(PropType::F32)), PropType::F32); + assert_eq!(sum_out_type(list(PropType::F64)), PropType::F64); } #[test] @@ -364,9 +366,8 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |pt| sum_out_type(pt), |vals| { Prop::I64(s64) }) } - PropType::F32 | PropType::F64 => { - scan_f64_sum_count(vals).map(|(sum, _)| Prop::F64(sum)) - } + PropType::F32 => scan_f64_sum_count(vals).map(|(sum, _)| Prop::F32(sum as f32)), + PropType::F64 => scan_f64_sum_count(vals).map(|(sum, _)| Prop::F64(sum)), _ => None, } }) diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 0ac14eda07..d99a86b207 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -5,6 +5,7 @@ use crate::{ Agg, CmpOp, Entity, Expr, Field, FilterExpr, OpaqueFilter, Qual, Scope, StrOp, Structural, Target, ViewOp, }, + validate_const_comparable, }, python::{ filter::filter_expr::PyFilterExpr, graph::node_state::PyOutputNodeState, @@ -73,23 +74,8 @@ fn static_type(lhs: &Expr) -> PyResult { } fn check_value(lhs: &Expr, v: &Prop) -> PyResult<()> { - let pt = static_type(lhs)?; - if pt != PropType::Empty && v.dtype() != pt && v.clone().try_cast(pt.clone()).is_err() { - return Err(PyTypeError::new_err(format!( - "value {v:?} of type {} is not comparable with an expression of type {pt}", - v.dtype() - ))); - } - Ok(()) -} - -/// Every member of a set is checked the way a single constant is. -fn checked_values(lhs: &Expr, values: FromIterable) -> PyResult> { - let values: Vec = values.into(); - for v in &values { - check_value(lhs, v)?; - } - Ok(values) + validate_const_comparable(&static_type(lhs)?, Some(v)) + .map_err(|e| PyTypeError::new_err(e.to_string())) } /// Presence tests only mean something on an expression that can be missing. @@ -102,15 +88,10 @@ fn check_nullable(lhs: &Expr, op: &str) -> PyResult<()> { Ok(()) } -/// String operators require a string-castable operand whatever the lhs type. +/// String operators require a string operand whatever the lhs type. fn check_str_value(v: &Prop) -> PyResult<()> { - if v.dtype() != PropType::Str && v.clone().try_cast(PropType::Str).is_err() { - return Err(PyTypeError::new_err(format!( - "value {v:?} of type {} is not a valid string operand", - v.dtype() - ))); - } - Ok(()) + validate_const_comparable(&PropType::Str, Some(v)) + .map_err(|e| PyTypeError::new_err(e.to_string())) } /// The right-hand side of a comparison, with a constant checked against the lhs. @@ -274,7 +255,7 @@ impl PyExpr { fn is_in(&self, values: FromIterable) -> PyResult { Ok(PyFilterExpr(FilterExpr::In { expr: self.0.clone(), - values: checked_values(&self.0, values)?, + values: values.into(), negated: false, })) } @@ -288,7 +269,7 @@ impl PyExpr { fn is_not_in(&self, values: FromIterable) -> PyResult { Ok(PyFilterExpr(FilterExpr::In { expr: self.0.clone(), - values: checked_values(&self.0, values)?, + values: values.into(), negated: true, })) } From e3b74f4cd741248dcd6cde3f90a975f3ed588c5a Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:17:16 +0100 Subject: [PATCH 123/155] Add the entity-split filter expression tree One expression type per entity surface, mirroring NodeFilter, EdgeFilter and ExplodedEdgeFilter, with the shared combinators generic over the entity's leaves. A comparison's result type is decided when the filter is built against a graph: Bool when the sides are comparable, List for an element-wise comparison that any() or all() collapse. The existing tree stays in place until its users move over. --- .../graph/views/filter/model/expr/compile.rs | 1393 +++++++++++++++++ .../graph/views/filter/model/expr/display.rs | 194 +++ .../db/graph/views/filter/model/expr/mod.rs | 235 +++ .../db/graph/views/filter/model/expr/tests.rs | 431 +++++ .../src/db/graph/views/filter/model/mod.rs | 1 + 5 files changed, 2254 insertions(+) create mode 100644 raphtory/src/db/graph/views/filter/model/expr/compile.rs create mode 100644 raphtory/src/db/graph/views/filter/model/expr/display.rs create mode 100644 raphtory/src/db/graph/views/filter/model/expr/mod.rs create mode 100644 raphtory/src/db/graph/views/filter/model/expr/tests.rs diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs new file mode 100644 index 0000000000..5fa3f6b518 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -0,0 +1,1393 @@ +//! From expression data to a filter the engine can apply. +//! +//! Reads and views replay onto the erased factories exactly as the typed API +//! would build them. Comparisons, tests and combinators become value +//! expressions ([`CmpExpr`], [`QualExpr`], …) whose result type is decided +//! when they are built against a graph, because only then are property types +//! known. A [`Predicate`] turns a `Bool`-typed value expression into a filter +//! on the entity the expression belongs to. + +use super::{ + Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, FilterExpr, NodeExpr, NodeLeaf, StrOp, + ViewOp, +}; +use crate::{ + db::{ + api::{ + state::NodeOp, + view::internal::{DynGraphArc, GraphView, NodeList}, + }, + graph::views::filter::{ + edge_expr_filtered_graph::EdgeExprFilteredGraph, + exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, + model::{ + and_filter::AndFilter, + comparable_set_values, + dyn_factory::{DynEdgeFilterFactory, DynNodeFilterFactory}, + edge_expr::EdgeOp, + edge_filter::{EdgeEndpointWrapper, EdgeFilter, Endpoint}, + exploded_edge_filter::ExplodedEdgeFilter, + filter_operator::{BinaryOp, Comparable, StringComparable, StringOp, UnaryOp}, + graph_filter::GraphFilter, + is_active_edge_filter::IsActiveEdge, + is_active_node_filter::IsActiveNode, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + node_expr::{ + ops::{ + broadcast_binary, broadcast_unary, gid_for_id_lookup, AllEdgeOp, AllNodeOp, + AnyEdgeOp, AnyNodeOp, IdDomainNodeOp, + }, + CreateOp, DynCreateOp, EntityExpr, Scoped, + }, + node_filter::NodeFilter, + not_filter::NotFilter, + or_filter::OrFilter, + resolved_prop_type, validate_binary_op, validate_const_comparable, + validate_string_op, validate_types_comparable, DynCreateFilter, DynView, + EntityMarker, ViewWrapOps, + }, + node_filtered_graph::NodeFilteredGraph, + CreateFilter, + }, + }, + errors::GraphError, + prelude::{EntityAggOps, Layer}, +}; +use raphtory_api::core::{ + entities::{ + edges::edge_ref::EdgeRef, + properties::prop::{Prop, PropType}, + GID, VID, + }, + Direction, +}; +use raphtory_storage::graph::graph::GraphStorage; +use std::{fmt::Debug, sync::Arc}; + +fn invalid(msg: impl Into) -> GraphError { + GraphError::InvalidFilter(msg.into()) +} + +// ── leaves ─────────────────────────────────────────────────────────────────── + +/// What an entity can read. Implemented by the leaf enum of each entity. +pub trait Leaf: Clone + Debug + PartialEq + Send + Sync + 'static { + /// The entity every expression over this leaf type belongs to. + const ENTITY: EntityMarker; + + /// The erased value this read produces. + fn compile(&self) -> Result, GraphError>; + + /// Whether the read is scoped by a view. + fn has_view(&self) -> bool; +} + +fn node_factory(views: &[ViewOp]) -> Arc { + let mut f: Arc = Arc::new(NodeFilter); + for op in views { + f = match op { + ViewOp::Window { start, end } => f.window(*start, *end), + ViewOp::At(t) => f.at(*t), + ViewOp::After(t) => f.after(*t), + ViewOp::Before(t) => f.before(*t), + ViewOp::Latest => Arc::new(f.latest()), + ViewOp::SnapshotAt(t) => Arc::new(f.snapshot_at(*t)), + ViewOp::SnapshotLatest => Arc::new(f.snapshot_latest()), + ViewOp::Layers(names) => Arc::new(f.layer(names.clone())), + }; + } + f +} + +fn edge_factory(exploded: bool, views: &[ViewOp]) -> Arc { + let mut f: Arc = if exploded { + Arc::new(ExplodedEdgeFilter) + } else { + Arc::new(EdgeFilter) + }; + for op in views { + f = match op { + ViewOp::Window { start, end } => f.dyn_window(*start, *end), + ViewOp::At(t) => f.dyn_at(*t), + ViewOp::After(t) => f.dyn_after(*t), + ViewOp::Before(t) => f.dyn_before(*t), + ViewOp::Latest => f.dyn_latest(), + ViewOp::SnapshotAt(t) => f.dyn_snapshot_at(*t), + ViewOp::SnapshotLatest => f.dyn_snapshot_latest(), + ViewOp::Layers(names) => f.dyn_layer(names.clone()), + }; + } + f +} + +impl Leaf for NodeLeaf { + const ENTITY: EntityMarker = EntityMarker::Node; + + fn compile(&self) -> Result, GraphError> { + Ok(match self { + NodeLeaf::Field { views, field } => { + let f = node_factory(views); + match field { + Field::Id => f.dyn_id(), + Field::Name => f.dyn_name(), + Field::NodeType => f.dyn_node_type(), + } + } + NodeLeaf::Degree { views, direction } => { + let f = node_factory(views); + match direction { + Direction::BOTH => f.dyn_degree(), + Direction::IN => f.dyn_in_degree(), + Direction::OUT => f.dyn_out_degree(), + } + } + NodeLeaf::Property { + views, + name, + temporal, + } => { + let prop = node_factory(views).dyn_property(name.clone()); + if *temporal { + prop.temporal() + } else { + prop + } + } + NodeLeaf::Metadata { views, name } => node_factory(views).dyn_metadata(name.clone()), + NodeLeaf::IsActive { views } => Arc::new(Scoped { + view: node_factory(views), + inner: IsActiveNode, + }), + }) + } + + fn has_view(&self) -> bool { + match self { + NodeLeaf::Field { views, .. } + | NodeLeaf::Degree { views, .. } + | NodeLeaf::Property { views, .. } + | NodeLeaf::Metadata { views, .. } + | NodeLeaf::IsActive { views } => !views.is_empty(), + } + } +} + +impl Leaf for EdgeLeaf { + const ENTITY: EntityMarker = EntityMarker::Edge; + + fn compile(&self) -> Result, GraphError> { + let f = |views| edge_factory(false, views); + Ok(match self { + EdgeLeaf::Property { + views, + name, + temporal, + } => { + let prop = f(views).dyn_property(name.clone()); + if *temporal { + prop.temporal() + } else { + prop + } + } + EdgeLeaf::Metadata { views, name } => f(views).dyn_metadata(name.clone()), + EdgeLeaf::IsActive { views } => Arc::new(Scoped { + view: f(views), + inner: IsActiveEdge, + }), + EdgeLeaf::IsValid { views } => Arc::new(Scoped { + view: f(views), + inner: IsValidEdge, + }), + EdgeLeaf::IsDeleted { views } => Arc::new(Scoped { + view: f(views), + inner: IsDeletedEdge, + }), + EdgeLeaf::IsSelfLoop { views } => Arc::new(Scoped { + view: f(views), + inner: IsSelfLoopEdge, + }), + // An endpoint read is a node expression the edge evaluates on the + // node at that end; its views scope that node read. + EdgeLeaf::Src(inner) => Arc::new(EdgeEndpointWrapper::new( + inner.compile_value()?, + Endpoint::Src, + )), + EdgeLeaf::Dst(inner) => Arc::new(EdgeEndpointWrapper::new( + inner.compile_value()?, + Endpoint::Dst, + )), + }) + } + + fn has_view(&self) -> bool { + match self { + EdgeLeaf::Property { views, .. } + | EdgeLeaf::Metadata { views, .. } + | EdgeLeaf::IsActive { views } + | EdgeLeaf::IsValid { views } + | EdgeLeaf::IsDeleted { views } + | EdgeLeaf::IsSelfLoop { views } => !views.is_empty(), + EdgeLeaf::Src(inner) | EdgeLeaf::Dst(inner) => inner.has_view(), + } + } +} + +impl Leaf for ExplodedEdgeLeaf { + const ENTITY: EntityMarker = EntityMarker::ExplodedEdge; + + fn compile(&self) -> Result, GraphError> { + let f = |views| edge_factory(true, views); + Ok(match self { + ExplodedEdgeLeaf::Property { + views, + name, + temporal, + } => { + let prop = f(views).dyn_property(name.clone()); + if *temporal { + prop.temporal() + } else { + prop + } + } + ExplodedEdgeLeaf::Metadata { views, name } => f(views).dyn_metadata(name.clone()), + ExplodedEdgeLeaf::IsActive { views } => Arc::new(Scoped { + view: f(views), + inner: IsActiveEdge, + }), + ExplodedEdgeLeaf::IsValid { views } => Arc::new(Scoped { + view: f(views), + inner: IsValidEdge, + }), + ExplodedEdgeLeaf::IsDeleted { views } => Arc::new(Scoped { + view: f(views), + inner: IsDeletedEdge, + }), + ExplodedEdgeLeaf::IsSelfLoop { views } => Arc::new(Scoped { + view: f(views), + inner: IsSelfLoopEdge, + }), + }) + } + + fn has_view(&self) -> bool { + match self { + ExplodedEdgeLeaf::Property { views, .. } + | ExplodedEdgeLeaf::Metadata { views, .. } + | ExplodedEdgeLeaf::IsActive { views } + | ExplodedEdgeLeaf::IsValid { views } + | ExplodedEdgeLeaf::IsDeleted { views } + | ExplodedEdgeLeaf::IsSelfLoop { views } => !views.is_empty(), + } + } +} + +// ── values ─────────────────────────────────────────────────────────────────── + +impl Expr { + /// The erased, compilable value this expression stands for. + pub fn compile_value(&self) -> Result, GraphError> { + let entity = L::ENTITY; + Ok(match self { + Expr::Const(value) => Arc::new(value.clone()), + Expr::Read(leaf) => leaf.compile()?, + Expr::Agg(agg, inner) => { + let op = inner.compile_value()?; + match agg { + Agg::Sum => Arc::new(op.sum()), + Agg::Avg => Arc::new(op.avg()), + Agg::Min => Arc::new(op.min()), + Agg::Max => Arc::new(op.max()), + Agg::First => Arc::new(op.first()), + Agg::Last => Arc::new(op.last()), + Agg::Len => Arc::new(op.len()), + } + } + Expr::Cmp(op, lhs, rhs) => Arc::new(CmpExpr { + op: binary_op(*op), + lhs: lhs.compile_value()?, + rhs: rhs.compile_value()?, + entity, + }), + Expr::Str(op, lhs, rhs) => Arc::new(StrExpr { + op: string_op(op), + lhs: lhs.compile_value()?, + rhs: rhs.compile_value()?, + entity, + }), + Expr::In { + expr, + values, + negated, + } => Arc::new(SetExpr { + inner: expr.compile_value()?, + values: values.clone(), + negated: *negated, + entity, + }), + Expr::IsSome(inner) => Arc::new(PresenceExpr { + inner: inner.compile_value()?, + op: UnaryOp::IsSome, + entity, + }), + Expr::IsNone(inner) => Arc::new(PresenceExpr { + inner: inner.compile_value()?, + op: UnaryOp::IsNone, + entity, + }), + Expr::Any(inner) => Arc::new(QualExpr { + inner: inner.compile_value()?, + all: false, + entity, + }), + Expr::All(inner) => Arc::new(QualExpr { + inner: inner.compile_value()?, + all: true, + entity, + }), + Expr::And(items) => Arc::new(BoolCombineExpr { + items: items + .iter() + .map(Self::compile_value) + .collect::>()?, + all: true, + entity, + }), + Expr::Or(items) => Arc::new(BoolCombineExpr { + items: items + .iter() + .map(Self::compile_value) + .collect::>()?, + all: false, + entity, + }), + Expr::Not(inner) => Arc::new(BoolNotExpr { + inner: inner.compile_value()?, + entity, + }), + }) + } +} + +fn binary_op(op: CmpOp) -> BinaryOp { + match op { + CmpOp::Eq => BinaryOp::Eq, + CmpOp::Ne => BinaryOp::Ne, + CmpOp::Lt => BinaryOp::Lt, + CmpOp::Le => BinaryOp::Le, + CmpOp::Gt => BinaryOp::Gt, + CmpOp::Ge => BinaryOp::Ge, + } +} + +fn string_op(op: &StrOp) -> StringOp { + match op { + StrOp::StartsWith => StringOp::StartsWith, + StrOp::EndsWith => StringOp::EndsWith, + StrOp::Contains => StringOp::Contains, + StrOp::NotContains => StringOp::NotContains, + StrOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => StringOp::FuzzySearch { + levenshtein_distance: *levenshtein_distance, + prefix_match: *prefix_match, + }, + } +} + +// ── result types ───────────────────────────────────────────────────────────── + +/// How a two-sided test evaluates: on the whole values, or once per element +/// of a list-valued side. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Shape { + Whole, + Elementwise, +} + +fn list(inner: PropType) -> PropType { + PropType::List(Box::new(inner)) +} + +/// The result type of comparing `lhs` with `rhs`: `Bool` when the two are +/// comparable as they stand, `List` (nested to match) when a list-valued +/// side is compared element by element against the other. +fn comparison_shape( + op: &BinaryOp, + lhs: &PropType, + rhs: &PropType, + rhs_const: Option<&Prop>, +) -> Result<(PropType, Shape), GraphError> { + if lhs.is_comparable_with(rhs) { + validate_binary_op(op, lhs)?; + return Ok((PropType::Bool, Shape::Whole)); + } + if let PropType::List(inner) = lhs { + if let Ok((out, _)) = comparison_shape(op, inner, rhs, rhs_const) { + return Ok((list(out), Shape::Elementwise)); + } + } + if let PropType::List(inner) = rhs { + if let Ok((out, _)) = comparison_shape(op, lhs, inner, None) { + return Ok((list(out), Shape::Elementwise)); + } + } + let mismatch = match rhs_const { + Some(value) => validate_const_comparable(lhs, Some(value)), + None => validate_types_comparable(lhs, rhs), + }; + Err(mismatch + .err() + .unwrap_or_else(|| invalid(format!("type mismatch: lhs is {lhs}, rhs is {rhs}")))) +} + +/// The result type of a string test: the left side must be a string, or a +/// list whose elements are, and the right side a string. +fn string_shape( + lhs: &PropType, + rhs: &PropType, + rhs_const: Option<&Prop>, +) -> Result<(PropType, Shape), GraphError> { + if lhs.is_unknown() || lhs.is_str() { + match rhs_const { + Some(value) => validate_const_comparable(&PropType::Str, Some(value))?, + None => validate_types_comparable(&PropType::Str, rhs)?, + } + return Ok((PropType::Bool, Shape::Whole)); + } + if let PropType::List(inner) = lhs { + if let Ok((out, _)) = string_shape(inner, rhs, rhs_const) { + return Ok((list(out), Shape::Elementwise)); + } + } + Err(validate_string_op(lhs).err().unwrap_or_else(|| { + invalid(format!( + "string operator requires a Str property, but the property type is {lhs}" + )) + })) +} + +/// The result type of a membership test, and the members that can match. A +/// list-valued side whose whole value no member can equal is tested element +/// by element instead. +fn set_shape(lhs: &PropType, values: &[Prop]) -> (PropType, Shape, Vec) { + let whole = comparable_set_values(lhs, values.to_vec()); + if let PropType::List(inner) = lhs { + if whole.is_empty() && !values.is_empty() { + let (out, _, members) = set_shape(inner, values); + return (list(out), Shape::Elementwise, members); + } + } + (PropType::Bool, Shape::Whole, whole) +} + +/// The type `any()`/`all()` produce over `inner`: one list level fewer, and +/// only over an element-wise yes/no result. +fn qualified_type(inner: &PropType) -> Result { + match inner { + PropType::List(elem) if matches!(**elem, PropType::Bool | PropType::List(_)) => { + Ok((**elem).clone()) + } + other => Err(invalid(format!( + "any()/all() collapse an element-wise comparison (a list of yes/no answers), \ + but this expression has type {other}" + ))), + } +} + +fn require_bool(pt: &PropType, what: &str) -> Result<(), GraphError> { + if *pt == PropType::Bool { + Ok(()) + } else { + Err(invalid(format!( + "{what} needs a yes/no answer, but this expression has type {pt}" + ))) + } +} + +fn truthy(v: &Option) -> bool { + matches!(v, Some(Prop::Bool(true))) +} + +// ── runtime ops ────────────────────────────────────────────────────────────── + +macro_rules! value_ops { + ($op_trait:ident, $id:ty, $binary:ident, $unary:ident, $nary:ident $(, $domain:item)?) => { + struct $binary<'g, K> { + left: Arc> + 'g>, + right: Arc> + 'g>, + kernel: K, + out: PropType, + } + + impl<'g, K> $op_trait for $binary<'g, K> + where + K: Fn(Option, Option) -> Option + Send + Sync, + { + type Output = Option; + $($domain)? + fn prop_type(&self) -> PropType { + self.out.clone() + } + fn apply(&self, storage: &GraphStorage, id: $id) -> Option { + (self.kernel)(self.left.apply(storage, id), self.right.apply(storage, id)) + } + } + + struct $unary<'g, K> { + inner: Arc> + 'g>, + kernel: K, + out: PropType, + } + + impl<'g, K> $op_trait for $unary<'g, K> + where + K: Fn(Option) -> Option + Send + Sync, + { + type Output = Option; + $($domain)? + fn prop_type(&self) -> PropType { + self.out.clone() + } + fn apply(&self, storage: &GraphStorage, id: $id) -> Option { + (self.kernel)(self.inner.apply(storage, id)) + } + } + + /// `and` / `or` over yes/no values, short-circuiting. + struct $nary<'g> { + items: Vec> + 'g>>, + all: bool, + } + + impl<'g> $op_trait for $nary<'g> { + type Output = Option; + $($domain)? + fn prop_type(&self) -> PropType { + PropType::Bool + } + fn apply(&self, storage: &GraphStorage, id: $id) -> Option { + let hit = if self.all { + self.items.iter().all(|item| truthy(&item.apply(storage, id))) + } else { + self.items.iter().any(|item| truthy(&item.apply(storage, id))) + }; + Some(Prop::Bool(hit)) + } + } + }; +} + +value_ops!( + NodeOp, + VID, + BinaryValueNodeOp, + UnaryValueNodeOp, + NaryBoolNodeOp, + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All + } +); +value_ops!( + EdgeOp, + EdgeRef, + BinaryValueEdgeOp, + UnaryValueEdgeOp, + NaryBoolEdgeOp +); + +/// Adapts a yes/no edge value to the plain boolean the filtered graphs consume. +struct TruthyEdgeOp<'g> { + inner: Arc> + 'g>, +} + +impl<'g> EdgeOp for TruthyEdgeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + truthy(&self.inner.apply(storage, edge)) + } +} + +fn cmp_kernel( + op: BinaryOp, + shape: Shape, +) -> impl Fn(Option, Option) -> Option + Clone { + move |l, r| match shape { + Shape::Whole => Some(Prop::Bool(Option::::binary_cmp(&op, &l, &r))), + Shape::Elementwise => broadcast_binary(l, r, &|l, r| { + Some(Prop::Bool(Prop::binary_cmp(&op, &l?, &r?))) + }), + } +} + +fn str_kernel( + op: StringOp, + shape: Shape, +) -> impl Fn(Option, Option) -> Option + Clone { + move |l, r| match shape { + Shape::Whole => Some(Prop::Bool(Option::::string_cmp(&op, &l, &r))), + Shape::Elementwise => broadcast_binary(l, r, &|l, r| { + Some(Prop::Bool(Option::::string_cmp(&op, &l, &r))) + }), + } +} + +fn set_kernel( + values: Vec, + negated: bool, + shape: Shape, +) -> impl Fn(Option) -> Option + Clone { + let values = Arc::new(values); + move |v| { + let member = |v: Option| { + let v = v?; + let present = values.iter().any(|x| x.equals(&v)); + Some(Prop::Bool(present != negated)) + }; + match shape { + Shape::Whole => member(v), + Shape::Elementwise => broadcast_unary(v, member), + } + } +} + +// ── value expressions ──────────────────────────────────────────────────────── + +macro_rules! entity_expr { + ($name:ident) => { + impl EntityExpr for $name { + type Marker = EntityMarker; + + fn entity(&self) -> EntityMarker { + self.entity + } + + fn prop_type(&self) -> PropType { + PropType::Empty + } + + fn nullable(&self) -> bool { + false + } + } + }; +} + +/// A comparison of two values, whole or element-wise as their types decide. +#[derive(Clone)] +struct CmpExpr { + op: BinaryOp, + lhs: Arc, + rhs: Arc, + entity: EntityMarker, +} +entity_expr!(CmpExpr); + +impl CreateOp for CmpExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.lhs.create_node_op(graph.clone())?; + let right = self.rhs.create_node_op(graph)?; + let lhs_pt = resolved_prop_type(self.lhs.prop_type(), left.prop_type()); + let rhs_pt = resolved_prop_type(self.rhs.prop_type(), right.prop_type()); + let rhs_const = right.const_value().flatten(); + let (out, shape) = comparison_shape(&self.op, &lhs_pt, &rhs_pt, rhs_const.as_ref())?; + Ok(Arc::new(BinaryValueNodeOp { + left, + right, + kernel: cmp_kernel(self.op, shape), + out, + })) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.lhs.create_edge_op(graph.clone())?; + let right = self.rhs.create_edge_op(graph)?; + let lhs_pt = resolved_prop_type(self.lhs.prop_type(), left.prop_type()); + let rhs_pt = resolved_prop_type(self.rhs.prop_type(), right.prop_type()); + let rhs_const = right.const_value().flatten(); + let (out, shape) = comparison_shape(&self.op, &lhs_pt, &rhs_pt, rhs_const.as_ref())?; + Ok(Arc::new(BinaryValueEdgeOp { + left, + right, + kernel: cmp_kernel(self.op, shape), + out, + })) + } +} + +/// A string test of a string-valued (or list-of-strings-valued) side. +#[derive(Clone)] +struct StrExpr { + op: StringOp, + lhs: Arc, + rhs: Arc, + entity: EntityMarker, +} +entity_expr!(StrExpr); + +impl CreateOp for StrExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.lhs.create_node_op(graph.clone())?; + let right = self.rhs.create_node_op(graph)?; + let lhs_pt = resolved_prop_type(self.lhs.prop_type(), left.prop_type()); + let rhs_pt = resolved_prop_type(self.rhs.prop_type(), right.prop_type()); + let rhs_const = right.const_value().flatten(); + let (out, shape) = string_shape(&lhs_pt, &rhs_pt, rhs_const.as_ref())?; + Ok(Arc::new(BinaryValueNodeOp { + left, + right, + kernel: str_kernel(self.op.clone(), shape), + out, + })) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let left = self.lhs.create_edge_op(graph.clone())?; + let right = self.rhs.create_edge_op(graph)?; + let lhs_pt = resolved_prop_type(self.lhs.prop_type(), left.prop_type()); + let rhs_pt = resolved_prop_type(self.rhs.prop_type(), right.prop_type()); + let rhs_const = right.const_value().flatten(); + let (out, shape) = string_shape(&lhs_pt, &rhs_pt, rhs_const.as_ref())?; + Ok(Arc::new(BinaryValueEdgeOp { + left, + right, + kernel: str_kernel(self.op.clone(), shape), + out, + })) + } +} + +/// Membership of a value in a fixed set. +#[derive(Clone)] +struct SetExpr { + inner: Arc, + values: Vec, + negated: bool, + entity: EntityMarker, +} +entity_expr!(SetExpr); + +impl CreateOp for SetExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.inner.create_node_op(graph)?; + let pt = resolved_prop_type(self.inner.prop_type(), inner.prop_type()); + let (out, shape, members) = set_shape(&pt, &self.values); + Ok(Arc::new(UnaryValueNodeOp { + inner, + kernel: set_kernel(members, self.negated, shape), + out, + })) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.inner.create_edge_op(graph)?; + let pt = resolved_prop_type(self.inner.prop_type(), inner.prop_type()); + let (out, shape, members) = set_shape(&pt, &self.values); + Ok(Arc::new(UnaryValueEdgeOp { + inner, + kernel: set_kernel(members, self.negated, shape), + out, + })) + } +} + +/// Whether a value is present (`is_some`) or missing (`is_none`). +#[derive(Clone)] +struct PresenceExpr { + inner: Arc, + op: UnaryOp, + entity: EntityMarker, +} +entity_expr!(PresenceExpr); + +impl PresenceExpr { + fn check(&self) -> Result<(), GraphError> { + if self.inner.nullable() { + Ok(()) + } else { + let name = match self.op { + UnaryOp::IsSome => "is_some", + UnaryOp::IsNone => "is_none", + }; + Err(invalid(format!( + "{name}() is not valid on an expression that always has a value" + ))) + } + } + + fn kernel(&self) -> impl Fn(Option) -> Option + Clone { + let op = self.op; + move |v| { + Some(Prop::Bool(match op { + UnaryOp::IsSome => v.is_some(), + UnaryOp::IsNone => v.is_none(), + })) + } + } +} + +impl CreateOp for PresenceExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.check()?; + Ok(Arc::new(UnaryValueNodeOp { + inner: self.inner.create_node_op(graph)?, + kernel: self.kernel(), + out: PropType::Bool, + })) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + self.check()?; + Ok(Arc::new(UnaryValueEdgeOp { + inner: self.inner.create_edge_op(graph)?, + kernel: self.kernel(), + out: PropType::Bool, + })) + } +} + +/// `any()` / `all()` over an element-wise yes/no result. +#[derive(Clone)] +struct QualExpr { + inner: Arc, + all: bool, + entity: EntityMarker, +} +entity_expr!(QualExpr); + +impl CreateOp for QualExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.inner.create_node_op(graph)?; + qualified_type(&resolved_prop_type( + self.inner.prop_type(), + inner.prop_type(), + ))?; + Ok(if self.all { + Arc::new(AllNodeOp { inner }) + } else { + Arc::new(AnyNodeOp { inner }) + }) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.inner.create_edge_op(graph)?; + qualified_type(&resolved_prop_type( + self.inner.prop_type(), + inner.prop_type(), + ))?; + Ok(if self.all { + Arc::new(AllEdgeOp { inner }) + } else { + Arc::new(AnyEdgeOp { inner }) + }) + } +} + +/// `and` / `or` of yes/no values. +#[derive(Clone)] +struct BoolCombineExpr { + items: Vec>, + all: bool, + entity: EntityMarker, +} +entity_expr!(BoolCombineExpr); + +impl BoolCombineExpr { + fn name(&self) -> &'static str { + if self.all { + "and" + } else { + "or" + } + } +} + +impl CreateOp for BoolCombineExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + if self.items.is_empty() { + return Err(invalid(format!( + "`{}` needs at least one operand", + self.name() + ))); + } + let mut items = Vec::with_capacity(self.items.len()); + for item in &self.items { + let op = item.create_node_op(graph.clone())?; + require_bool( + &resolved_prop_type(item.prop_type(), op.prop_type()), + self.name(), + )?; + items.push(op); + } + Ok(Arc::new(NaryBoolNodeOp { + items, + all: self.all, + })) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + if self.items.is_empty() { + return Err(invalid(format!( + "`{}` needs at least one operand", + self.name() + ))); + } + let mut items = Vec::with_capacity(self.items.len()); + for item in &self.items { + let op = item.create_edge_op(graph.clone())?; + require_bool( + &resolved_prop_type(item.prop_type(), op.prop_type()), + self.name(), + )?; + items.push(op); + } + Ok(Arc::new(NaryBoolEdgeOp { + items, + all: self.all, + })) + } +} + +/// `not` of a yes/no value. +#[derive(Clone)] +struct BoolNotExpr { + inner: Arc, + entity: EntityMarker, +} +entity_expr!(BoolNotExpr); + +fn not_kernel(v: Option) -> Option { + Some(Prop::Bool(!truthy(&v))) +} + +impl CreateOp for BoolNotExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.inner.create_node_op(graph)?; + require_bool( + &resolved_prop_type(self.inner.prop_type(), inner.prop_type()), + "not", + )?; + Ok(Arc::new(UnaryValueNodeOp { + inner, + kernel: not_kernel, + out: PropType::Bool, + })) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let inner = self.inner.create_edge_op(graph)?; + require_bool( + &resolved_prop_type(self.inner.prop_type(), inner.prop_type()), + "not", + )?; + Ok(Arc::new(UnaryValueEdgeOp { + inner, + kernel: not_kernel, + out: PropType::Bool, + })) + } +} + +// ── predicates ─────────────────────────────────────────────────────────────── + +/// A yes/no value expression applied as a filter on its entity. +#[derive(Clone)] +struct Predicate { + entity: EntityMarker, + inner: Arc, + /// Node ids the predicate names outright (`id == v`, `id in [..]`), so the + /// node filter can start from those nodes instead of scanning every one. + ids: Option>, +} + +impl Predicate { + fn new(expr: &Expr, ids: Option>) -> Result { + Ok(Predicate { + entity: L::ENTITY, + inner: expr.compile_value()?, + ids, + }) + } + + fn node_filter<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result + 'graph>, GraphError> { + let id_type = graph.id_type(); + let op = self.inner.create_node_op(graph)?; + require_bool( + &resolved_prop_type(self.inner.prop_type(), op.prop_type()), + "a filter", + )?; + let filter: Arc + 'graph> = Arc::new(op.map(|v| truthy(&v))); + let gids: Option> = self + .ids + .as_ref() + .and_then(|ids| ids.iter().map(|v| gid_for_id_lookup(id_type, v)).collect()); + Ok(match gids { + Some(gids) => Arc::new(IdDomainNodeOp { + gids: Arc::from(gids), + inner: filter, + }), + None => filter, + }) + } + + fn edge_filter<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result + 'graph>, GraphError> { + let op = self.inner.create_edge_op(graph)?; + require_bool( + &resolved_prop_type(self.inner.prop_type(), op.prop_type()), + "a filter", + )?; + Ok(Arc::new(TruthyEdgeOp { inner: op })) + } +} + +impl CreateFilter for Predicate { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = DynGraphArc<'graph> + where + Self: 'graph; + + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc + 'graph>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + Ok(match self.entity { + EntityMarker::Node => { + Arc::new(NodeFilteredGraph::new(graph, self.node_filter(filtered)?)) + } + EntityMarker::Edge => Arc::new(EdgeExprFilteredGraph::new( + graph, + self.edge_filter(filtered)?, + )), + EntityMarker::ExplodedEdge => Arc::new(ExplodedEdgeExprFilteredGraph::new( + graph, + self.edge_filter(filtered)?, + )), + EntityMarker::Const => return Err(invalid("a constant is not a filter")), + }) + } + + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + _graph: G, + filtered: F, + ) -> Result, GraphError> { + if !matches!(self.entity, EntityMarker::Node) { + return Err(GraphError::NotNodeFilter); + } + self.node_filter(filtered) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + Ok(graph) + } +} + +/// The node ids a node predicate names outright, if it is `id == v` or +/// `id in [..]` on the bare id field. +fn named_ids(expr: &NodeExpr) -> Option> { + fn is_bare_id(e: &NodeExpr) -> bool { + matches!( + e, + Expr::Read(NodeLeaf::Field { views, field: Field::Id }) if views.is_empty() + ) + } + match expr { + Expr::Cmp(CmpOp::Eq, lhs, rhs) => match (&**lhs, &**rhs) { + (id, Expr::Const(v)) | (Expr::Const(v), id) if is_bare_id(id) => Some(vec![v.clone()]), + _ => None, + }, + Expr::In { + expr, + values, + negated: false, + } if is_bare_id(expr) => Some(values.clone()), + _ => None, + } +} + +// ── filters ────────────────────────────────────────────────────────────────── + +impl FilterExpr { + /// The erased, applicable form of this filter. + /// + /// A view (`View`) applies first: the graph is seen through it and the other + /// legs run inside it, reads included, the way `graph.window(..).filter(expr)` + /// does. A view therefore stands alone or is a leg of the top-level `and` + /// (nested `and`s count as top level); under `or` or `not` it has no meaning the + /// engine can give it and is refused. + pub fn compile(&self) -> Result, GraphError> { + let (views, predicates, saw_view) = self.split_top_views(); + if saw_view && views.is_empty() { + return Err(invalid("a view filter needs at least one view")); + } + if views.is_empty() { + return self.compile_nested(); + } + let inner: Arc = if predicates.is_empty() { + Arc::new(GraphFilter) + } else { + combine( + predicates.iter().map(|p| p.compile_nested()), + "and", + |left, right| Arc::new(AndFilter { left, right }), + )? + }; + Ok(Arc::new(Viewed { views, inner })) + } + + /// The view ops at the top of the filter, in order, and the predicates beside + /// them. `and` nests flatten; anything else is a predicate. The flag says whether + /// a `View` node was seen at all, so an empty one can be told from none. + fn split_top_views(&self) -> (Vec, Vec<&FilterExpr>, bool) { + fn walk<'a>( + filter: &'a FilterExpr, + views: &mut Vec, + predicates: &mut Vec<&'a FilterExpr>, + saw_view: &mut bool, + ) { + match filter { + FilterExpr::View(ops) => { + *saw_view = true; + views.extend(ops.iter().cloned()); + } + FilterExpr::And(items) => { + for item in items { + walk(item, views, predicates, saw_view); + } + } + other => predicates.push(other), + } + } + let (mut views, mut predicates, mut saw_view) = (Vec::new(), Vec::new(), false); + walk(self, &mut views, &mut predicates, &mut saw_view); + (views, predicates, saw_view) + } + + /// A filter below the top level: every node but a view. + fn compile_nested(&self) -> Result, GraphError> { + Ok(match self { + FilterExpr::Node(expr) => Arc::new(Predicate::new(expr, named_ids(expr))?), + FilterExpr::Edge(expr) => Arc::new(Predicate::new(expr, None)?), + FilterExpr::ExplodedEdge(expr) => Arc::new(Predicate::new(expr, None)?), + FilterExpr::View(_) => { + return Err(invalid( + "a view applies to the whole filter: use it alone or as a leg of the \ + top-level `and`, not under `or` or `not`", + )) + } + FilterExpr::And(items) => combine( + items.iter().map(Self::compile_nested), + "and", + |left, right| Arc::new(AndFilter { left, right }), + )?, + FilterExpr::Or(items) => combine( + items.iter().map(Self::compile_nested), + "or", + |left, right| Arc::new(OrFilter { left, right }), + )?, + FilterExpr::Not(inner) => Arc::new(NotFilter(inner.compile_nested()?)), + FilterExpr::Opaque(filter) => filter.0.clone(), + }) + } +} + +/// The graph-level view a chain of view ops describes, applied in order. +fn compile_view(views: &[ViewOp]) -> DynView { + let mut v: DynView = Arc::new(GraphFilter); + for op in views { + v = match op { + ViewOp::Window { start, end } => v.window(*start, *end), + ViewOp::At(t) => v.at(*t), + ViewOp::After(t) => v.after(*t), + ViewOp::Before(t) => v.before(*t), + ViewOp::Latest => Arc::new(v.latest()), + ViewOp::SnapshotAt(t) => Arc::new(v.snapshot_at(*t)), + ViewOp::SnapshotLatest => Arc::new(v.snapshot_latest()), + ViewOp::Layers(names) => Arc::new(v.layer(Layer::from(names.clone()))), + }; + } + v +} + +/// A filter applied inside a view: the graph is seen through `views` first and +/// `inner` runs on that graph, reads included, so `and: [view, pred]` is +/// `graph.view(..).filter(pred)`. +#[derive(Clone)] +struct Viewed { + views: Vec, + inner: Arc, +} + +impl Viewed { + fn view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + compile_view(&self.views).dyn_filter_graph_view(Arc::new(graph)) + } +} + +impl CreateFilter for Viewed { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = DynGraphArc<'graph> + where + Self: 'graph; + + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = Arc + 'graph> + where + Self: 'graph; + + type FilteredGraph<'graph, G> + = DynGraphArc<'graph> + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + let viewed = self.view(graph)?; + self.inner.create_dyn_filter(viewed, Arc::new(filtered)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + let viewed = self.view(graph)?; + self.inner + .create_dyn_node_filter(viewed, Arc::new(filtered)) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + let viewed = self.view(graph)?; + self.inner.dyn_filter_graph_view(viewed) + } +} + +/// Fold compiled operands pairwise, left to right. An empty list has no +/// meaning either way (`and` of nothing is not "everything", `or` of nothing +/// is not "nothing" the caller asked for), so it is refused. +fn combine( + mut compiled: impl Iterator, GraphError>>, + name: &str, + join: impl Fn(Arc, Arc) -> Arc, +) -> Result, GraphError> { + let first = compiled + .next() + .ok_or_else(|| invalid(format!("`{name}` needs at least one operand")))??; + compiled.try_fold(first, |acc, next| Ok(join(acc, next?))) +} + +/// A tree is a filter in its own right: applying it compiles it first. +impl CreateFilter for FilterExpr { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = DynGraphArc<'graph> + where + Self: 'graph; + + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc + 'graph>; + + type FilteredGraph<'graph, G> + = DynGraphArc<'graph> + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + self.compile()?.create_filter(graph, filtered) + } + + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + self.compile()?.create_node_filter(graph, filtered) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + self.compile()?.filter_graph_view(graph) + } +} diff --git a/raphtory/src/db/graph/views/filter/model/expr/display.rs b/raphtory/src/db/graph/views/filter/model/expr/display.rs new file mode 100644 index 0000000000..3d57b210f6 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/expr/display.rs @@ -0,0 +1,194 @@ +//! A readable rendering of an expression, for logs, errors and tests. + +use super::{ + Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, FilterExpr, NodeLeaf, StrOp, ViewOp, +}; +use raphtory_api::core::Direction; +use std::fmt::{self, Display}; + +fn views(f: &mut fmt::Formatter<'_>, views: &[ViewOp], inner: &dyn Display) -> fmt::Result { + if views.is_empty() { + return write!(f, "{inner}"); + } + let chain = views + .iter() + .map(ToString::to_string) + .collect::>() + .join(" . "); + write!(f, "{chain}({inner})") +} + +fn property(f: &mut fmt::Formatter<'_>, v: &[ViewOp], name: &str, temporal: bool) -> fmt::Result { + if temporal { + views(f, v, &format!("TEMPORAL({name})")) + } else { + views(f, v, &name) + } +} + +impl Display for NodeLeaf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NodeLeaf::Field { views: v, field } => { + let name = match field { + Field::Id => "id", + Field::Name => "name", + Field::NodeType => "node_type", + }; + views(f, v, &name) + } + NodeLeaf::Degree { + views: v, + direction, + } => { + let name = match direction { + Direction::BOTH => "degree", + Direction::IN => "in_degree", + Direction::OUT => "out_degree", + }; + views(f, v, &name) + } + NodeLeaf::Property { + views: v, + name, + temporal, + } => property(f, v, name, *temporal), + NodeLeaf::Metadata { views: v, name } => views(f, v, &format!("METADATA({name})")), + NodeLeaf::IsActive { views: v } => views(f, v, &"IS_ACTIVE"), + } + } +} + +impl Display for EdgeLeaf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EdgeLeaf::Property { + views: v, + name, + temporal, + } => property(f, v, name, *temporal), + EdgeLeaf::Metadata { views: v, name } => views(f, v, &format!("METADATA({name})")), + EdgeLeaf::IsActive { views: v } => views(f, v, &"IS_ACTIVE"), + EdgeLeaf::IsValid { views: v } => views(f, v, &"IS_VALID"), + EdgeLeaf::IsDeleted { views: v } => views(f, v, &"IS_DELETED"), + EdgeLeaf::IsSelfLoop { views: v } => views(f, v, &"IS_SELF_LOOP"), + EdgeLeaf::Src(inner) => write!(f, "SRC({inner})"), + EdgeLeaf::Dst(inner) => write!(f, "DST({inner})"), + } + } +} + +impl Display for ExplodedEdgeLeaf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ExplodedEdgeLeaf::Property { + views: v, + name, + temporal, + } => property(f, v, name, *temporal), + ExplodedEdgeLeaf::Metadata { views: v, name } => { + views(f, v, &format!("METADATA({name})")) + } + ExplodedEdgeLeaf::IsActive { views: v } => views(f, v, &"IS_ACTIVE"), + ExplodedEdgeLeaf::IsValid { views: v } => views(f, v, &"IS_VALID"), + ExplodedEdgeLeaf::IsDeleted { views: v } => views(f, v, &"IS_DELETED"), + ExplodedEdgeLeaf::IsSelfLoop { views: v } => views(f, v, &"IS_SELF_LOOP"), + } + } +} + +impl Display for Expr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Expr::Const(v) => write!(f, "{v}"), + Expr::Read(leaf) => write!(f, "{leaf}"), + Expr::Agg(agg, e) => { + let name = match agg { + Agg::Sum => "SUM", + Agg::Avg => "AVG", + Agg::Min => "MIN", + Agg::Max => "MAX", + Agg::First => "FIRST", + Agg::Last => "LAST", + Agg::Len => "LEN", + }; + write!(f, "{name}({e})") + } + Expr::Cmp(op, l, r) => { + let sym = match op { + CmpOp::Eq => "==", + CmpOp::Ne => "!=", + CmpOp::Lt => "<", + CmpOp::Le => "<=", + CmpOp::Gt => ">", + CmpOp::Ge => ">=", + }; + write!(f, "{l} {sym} {r}") + } + Expr::Str(op, l, r) => { + let name = match op { + StrOp::StartsWith => "STARTS_WITH".to_string(), + StrOp::EndsWith => "ENDS_WITH".to_string(), + StrOp::Contains => "CONTAINS".to_string(), + StrOp::NotContains => "NOT_CONTAINS".to_string(), + StrOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => format!("FUZZY_SEARCH[{levenshtein_distance}, {prefix_match}]"), + }; + write!(f, "{l} {name} {r}") + } + Expr::In { + expr, + values, + negated, + } => { + let items = values + .iter() + .map(ToString::to_string) + .collect::>() + .join(", "); + let name = if *negated { "NOT IN" } else { "IN" }; + write!(f, "{expr} {name} [{items}]") + } + Expr::IsSome(e) => write!(f, "IS_SOME({e})"), + Expr::IsNone(e) => write!(f, "IS_NONE({e})"), + Expr::Any(e) => write!(f, "ANY({e})"), + Expr::All(e) => write!(f, "ALL({e})"), + Expr::And(items) => joined(f, items, " AND "), + Expr::Or(items) => joined(f, items, " OR "), + Expr::Not(e) => write!(f, "NOT({e})"), + } + } +} + +fn joined(f: &mut fmt::Formatter<'_>, items: &[T], sep: &str) -> fmt::Result { + let parts = items + .iter() + .map(|i| format!("({i})")) + .collect::>() + .join(sep); + write!(f, "{parts}") +} + +impl Display for FilterExpr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FilterExpr::Node(e) => write!(f, "NODE({e})"), + FilterExpr::Edge(e) => write!(f, "EDGE({e})"), + FilterExpr::ExplodedEdge(e) => write!(f, "EXPLODED_EDGE({e})"), + FilterExpr::View(ops) => { + let chain = ops + .iter() + .map(ToString::to_string) + .collect::>() + .join(" . "); + write!(f, "VIEW({chain})") + } + FilterExpr::And(items) => joined(f, items, " AND "), + FilterExpr::Or(items) => joined(f, items, " OR "), + FilterExpr::Not(e) => write!(f, "NOT({e})"), + FilterExpr::Opaque(_) => write!(f, "OPAQUE"), + } + } +} diff --git a/raphtory/src/db/graph/views/filter/model/expr/mod.rs b/raphtory/src/db/graph/views/filter/model/expr/mod.rs new file mode 100644 index 0000000000..76f0a8fe3d --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/expr/mod.rs @@ -0,0 +1,235 @@ +//! A filter as data, one expression type per entity. +//! +//! The typed expression API (`NodeFilter.property("score").gt(4)`) is what +//! rust callers write and what the engine compiles. Everything that has to +//! *carry* a filter — a python object, a GraphQL request, a stored permission +//! grant — needs the same filter as plain data. These enums are that data. +//! +//! [`Expr`] holds what every entity can do: constants, aggregates, +//! comparisons, string and set tests, presence tests, `any`/`all` over an +//! element-wise result, and `and`/`or`/`not`. What an entity can *read* is +//! its leaf type: [`NodeLeaf`] mirrors `NodeFilter`, [`EdgeLeaf`] mirrors +//! `EdgeFilter` (with `src`/`dst` holding a node expression), and +//! [`ExplodedEdgeLeaf`] mirrors `ExplodedEdgeFilter`. A combination the API +//! does not have cannot be written down, so it need not be rejected. +//! +//! Every expression has a result type. A comparison of two comparable values +//! is a `Bool`; a comparison of a list-valued side against a value its +//! elements are comparable with is a `List`, one answer per element, +//! which `Any` or `All` turn into a `Bool`. A [`FilterExpr`] accepts an +//! expression only when it is a `Bool`. Property types are known only once a +//! graph is at hand, so that check runs when the filter is built against a +//! graph, in [`compile`]. + +mod compile; +mod display; +#[cfg(test)] +mod tests; + +pub use super::tree::{Agg, CmpOp, Field, OpaqueFilter, StrOp, ViewOp, OPAQUE_FILTER_ERROR}; +pub use compile::Leaf; + +use raphtory_api::core::{entities::properties::prop::Prop, Direction}; +use serde::{Deserialize, Serialize}; + +/// What every entity can do with a value, whatever the entity reads. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Expr { + Const(Prop), + Agg(Agg, Box>), + Cmp(CmpOp, Box>, Box>), + Str(StrOp, Box>, Box>), + In { + expr: Box>, + values: Vec, + negated: bool, + }, + IsSome(Box>), + IsNone(Box>), + /// Holds when the element-wise result inside holds for any element. + Any(Box>), + /// Holds when the element-wise result inside holds for every element. + All(Box>), + And(Vec>), + Or(Vec>), + Not(Box>), + /// A read the entity offers; see [`NodeLeaf`], [`EdgeLeaf`], [`ExplodedEdgeLeaf`]. + /// Serialised as the leaf itself, so the read's name is the key. + #[serde(untagged)] + Read(L), +} + +/// What a node offers: the surface of `NodeFilter`. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum NodeLeaf { + Field { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + field: Field, + }, + Degree { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + direction: Direction, + }, + Property { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + name: String, + /// The property's history as a list instead of its latest value. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + temporal: bool, + }, + Metadata { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + name: String, + }, + IsActive { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + }, +} + +/// What an edge offers: the surface of `EdgeFilter`, plus a look at either +/// endpoint node. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum EdgeLeaf { + Property { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + name: String, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + temporal: bool, + }, + Metadata { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + name: String, + }, + IsActive { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + }, + IsValid { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + }, + IsDeleted { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + }, + IsSelfLoop { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + }, + /// A node expression evaluated on the edge's source node. + Src(Box), + /// A node expression evaluated on the edge's destination node. + Dst(Box), +} + +/// What an exploded edge offers: the surface of `ExplodedEdgeFilter`. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ExplodedEdgeLeaf { + Property { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + name: String, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + temporal: bool, + }, + Metadata { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + name: String, + }, + IsActive { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + }, + IsValid { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + }, + IsDeleted { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + }, + IsSelfLoop { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + views: Vec, + }, +} + +pub type NodeExpr = Expr; +pub type EdgeExpr = Expr; +pub type ExplodedEdgeExpr = Expr; + +/// The filter itself: a yes/no on one kind of entity, a view, or a +/// combination of filters. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FilterExpr { + Node(NodeExpr), + Edge(EdgeExpr), + ExplodedEdge(ExplodedEdgeExpr), + /// A graph-level view with no predicate: the result *is* the view. + View(Vec), + And(Vec), + Or(Vec), + Not(Box), + /// A filter over in-process state (a node-state column) that has no wire + /// form: it runs where it was built and cannot be sent anywhere. + Opaque(OpaqueFilter), +} + +impl Expr { + /// Whether a view appears in any read of this expression. + pub fn has_view(&self) -> bool + where + L: Leaf, + { + match self { + Expr::Const(_) => false, + Expr::Read(leaf) => leaf.has_view(), + Expr::Agg(_, e) + | Expr::IsSome(e) + | Expr::IsNone(e) + | Expr::Any(e) + | Expr::All(e) + | Expr::Not(e) => e.has_view(), + Expr::In { expr, .. } => expr.has_view(), + Expr::Cmp(_, l, r) | Expr::Str(_, l, r) => l.has_view() || r.has_view(), + Expr::And(items) | Expr::Or(items) => items.iter().any(Self::has_view), + } + } +} + +impl FilterExpr { + /// Whether a graph-level view appears anywhere in this filter. + pub fn has_view(&self) -> bool { + match self { + FilterExpr::View(_) => true, + FilterExpr::And(items) | FilterExpr::Or(items) => items.iter().any(Self::has_view), + FilterExpr::Not(inner) => inner.has_view(), + _ => false, + } + } + + /// Whether any part of this filter tests edges. An edge test says nothing + /// about which nodes belong in a node collection, so a node-collection + /// subscript refuses such a filter. + pub fn tests_edges(&self) -> bool { + match self { + FilterExpr::Edge(_) | FilterExpr::ExplodedEdge(_) => true, + FilterExpr::Node(_) | FilterExpr::View(_) | FilterExpr::Opaque(_) => false, + FilterExpr::And(items) | FilterExpr::Or(items) => items.iter().any(Self::tests_edges), + FilterExpr::Not(inner) => inner.tests_edges(), + } + } +} diff --git a/raphtory/src/db/graph/views/filter/model/expr/tests.rs b/raphtory/src/db/graph/views/filter/model/expr/tests.rs new file mode 100644 index 0000000000..c084ab48a2 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/expr/tests.rs @@ -0,0 +1,431 @@ +use super::*; +use crate::{ + db::api::view::Filter, + prelude::{AdditionOps, EdgeViewOps, Graph, GraphViewOps, NodeViewOps}, +}; +use raphtory_api::core::{ + entities::properties::prop::IntoProp, storage::timeindex::EventTime, Direction, +}; + +/// alice.score 3@0 7@2 9@6 · bob.score 5@1 2@7 · carol none · dave.score 1@2 1@3 +/// eve.scores [1,2]@0 [5,5]@1 +/// alice→bob [knows] w=1@1 w=2@4 · bob→carol [works] w=1@2 · carol→dave [knows] w=3@6 +fn graph() -> Graph { + let g = Graph::new(); + for (t, name, score) in [ + (0, "alice", 3.0), + (2, "alice", 7.0), + (6, "alice", 9.0), + (1, "bob", 5.0), + (7, "bob", 2.0), + (2, "dave", 1.0), + (3, "dave", 1.0), + ] { + g.add_node(t, name, [("score", score.into_prop())], None, None) + .unwrap(); + } + g.add_node(0, "carol", [("tag", "x".into_prop())], None, None) + .unwrap(); + for (t, scores) in [(0, vec![1i64, 2]), (1, vec![5, 5])] { + let list = Prop::List(scores.into_iter().map(Prop::I64).collect::>().into()); + g.add_node(t, "eve", [("scores", list)], None, None) + .unwrap(); + } + for (t, src, dst, layer, w) in [ + (1, "alice", "bob", "knows", 1i64), + (4, "alice", "bob", "knows", 2), + (2, "bob", "carol", "works", 1), + (6, "carol", "dave", "knows", 3), + ] { + g.add_edge(t, src, dst, [("w", w.into_prop())], Some(layer)) + .unwrap(); + } + g +} + +fn nodes(g: &Graph, filter: &FilterExpr) -> Vec { + let mut names: Vec = g + .filter(filter.clone()) + .unwrap() + .nodes() + .iter() + .map(|n| n.name()) + .collect(); + names.sort(); + names +} + +fn edges(g: &Graph, filter: &FilterExpr) -> Vec { + let mut ids: Vec = g + .filter(filter.clone()) + .unwrap() + .edges() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + ids.sort(); + ids +} + +fn error(g: &Graph, filter: &FilterExpr) -> String { + match g.filter(filter.clone()) { + Ok(_) => panic!("expected {filter} to be refused"), + Err(e) => e.to_string(), + } +} + +fn c(v: impl Into) -> NodeExpr { + Expr::Const(v.into()) +} + +fn prop(name: &str) -> NodeExpr { + Expr::Read(NodeLeaf::Property { + views: vec![], + name: name.into(), + temporal: false, + }) +} + +fn history(name: &str) -> NodeExpr { + Expr::Read(NodeLeaf::Property { + views: vec![], + name: name.into(), + temporal: true, + }) +} + +fn field(field: Field) -> NodeExpr { + Expr::Read(NodeLeaf::Field { + views: vec![], + field, + }) +} + +fn degree(direction: Direction) -> NodeExpr { + Expr::Read(NodeLeaf::Degree { + views: vec![], + direction, + }) +} + +fn cmp(op: CmpOp, l: Expr, r: Expr) -> Expr { + Expr::Cmp(op, Box::new(l), Box::new(r)) +} + +fn node(e: NodeExpr) -> FilterExpr { + FilterExpr::Node(e) +} + +fn edge_prop(name: &str) -> EdgeExpr { + Expr::Read(EdgeLeaf::Property { + views: vec![], + name: name.into(), + temporal: false, + }) +} + +fn src(e: NodeExpr) -> EdgeExpr { + Expr::Read(EdgeLeaf::Src(Box::new(e))) +} + +fn dst(e: NodeExpr) -> EdgeExpr { + Expr::Read(EdgeLeaf::Dst(Box::new(e))) +} + +fn window(start: i64, end: i64) -> ViewOp { + ViewOp::Window { + start: EventTime::start(start), + end: EventTime::start(end), + } +} + +#[test] +fn a_constant_comparison_reads_the_latest_value() { + let g = graph(); + let f = node(cmp(CmpOp::Gt, prop("score"), c(4.0))); + assert_eq!(nodes(&g, &f), ["alice"]); + assert_eq!(f.to_string(), "NODE(score > 4)"); + // The constant may stand on either side. + let f = node(cmp(CmpOp::Lt, c(4.0), prop("score"))); + assert_eq!(nodes(&g, &f), ["alice"]); +} + +#[test] +fn views_scope_the_read_not_the_result() { + let g = graph(); + let windowed = Expr::Read(NodeLeaf::Property { + views: vec![window(0, 5)], + name: "score".into(), + temporal: false, + }); + // inside [0,5): alice's latest score is 7, bob's is 5 + let f = node(cmp(CmpOp::Gt, windowed, c(4.0))); + assert_eq!(nodes(&g, &f), ["alice", "bob"]); + assert_eq!(f.to_string(), "NODE(WINDOW[0..5](score) > 4)"); +} + +#[test] +fn both_sides_may_be_expressions() { + let g = graph(); + let f = node(cmp( + CmpOp::Gt, + degree(Direction::BOTH), + degree(Direction::IN), + )); + assert_eq!(nodes(&g, &f), ["alice", "bob", "carol"]); + // A degree compares by value with a fractional constant. + let f = node(cmp(CmpOp::Ge, degree(Direction::BOTH), c(1.5))); + assert_eq!(nodes(&g, &f), ["bob", "carol"]); +} + +#[test] +fn string_and_set_tests() { + let g = graph(); + let starts = node(Expr::Str( + StrOp::StartsWith, + Box::new(field(Field::Name)), + Box::new(c("a")), + )); + assert_eq!(nodes(&g, &starts), ["alice"]); + let members = node(Expr::In { + expr: Box::new(field(Field::Name)), + values: vec!["alice".into(), "dave".into(), 7i64.into()], + negated: false, + }); + assert_eq!(nodes(&g, &members), ["alice", "dave"]); + let others = node(Expr::In { + expr: Box::new(field(Field::Name)), + values: vec!["alice".into(), "dave".into()], + negated: true, + }); + assert_eq!(nodes(&g, &others), ["bob", "carol", "eve"]); + // Naming ids outright narrows the scan to those nodes and still answers. + let by_id = node(cmp(CmpOp::Eq, field(Field::Id), c("bob"))); + assert_eq!(nodes(&g, &by_id), ["bob"]); + let by_ids = node(Expr::In { + expr: Box::new(field(Field::Id)), + values: vec!["bob".into(), "eve".into()], + negated: false, + }); + assert_eq!(nodes(&g, &by_ids), ["bob", "eve"]); +} + +#[test] +fn presence_and_combinators() { + let g = graph(); + let has_score = node(Expr::IsSome(Box::new(prop("score")))); + assert_eq!(nodes(&g, &has_score), ["alice", "bob", "dave"]); + let no_score = node(Expr::IsNone(Box::new(prop("score")))); + assert_eq!(nodes(&g, &no_score), ["carol", "eve"]); + // Inside one entity's expression. + let both = node(Expr::And(vec![ + cmp(CmpOp::Gt, prop("score"), c(1.5)), + cmp(CmpOp::Lt, prop("score"), c(8.0)), + ])); + assert_eq!(nodes(&g, &both), ["bob"]); + let either = node(Expr::Or(vec![ + cmp(CmpOp::Gt, prop("score"), c(8.0)), + Expr::IsNone(Box::new(prop("score"))), + ])); + assert_eq!(nodes(&g, &either), ["alice", "carol", "eve"]); + let not = node(Expr::Not(Box::new(cmp(CmpOp::Gt, prop("score"), c(1.5))))); + assert_eq!(nodes(&g, ¬), ["carol", "dave", "eve"]); + // And across filters. + let f = FilterExpr::And(vec![ + node(cmp(CmpOp::Gt, prop("score"), c(1.5))), + FilterExpr::Not(Box::new(node(cmp(CmpOp::Gt, prop("score"), c(8.0))))), + ]); + assert_eq!(nodes(&g, &f), ["bob"]); +} + +#[test] +fn qualifiers_follow_the_comparison() { + let g = graph(); + // alice 3,7,9 · bob 5,2 · dave 1,1 + let any_high = node(Expr::Any(Box::new(cmp( + CmpOp::Gt, + history("score"), + c(8.0), + )))); + assert_eq!(nodes(&g, &any_high), ["alice"]); + let all_over_two = node(Expr::All(Box::new(cmp( + CmpOp::Gt, + history("score"), + c(2.5), + )))); + assert_eq!(nodes(&g, &all_over_two), ["alice"]); + let any_member = node(Expr::Any(Box::new(Expr::In { + expr: Box::new(history("score")), + values: vec![1.0.into(), 5.0.into()], + negated: false, + }))); + assert_eq!(nodes(&g, &any_member), ["bob", "dave"]); + // An element-wise result is a list of answers, not a filter, until it is + // collapsed. + let bare = node(cmp(CmpOp::Gt, history("score"), c(8.0))); + assert!( + error(&g, &bare).contains("List"), + "{}", + error(&g, &bare) + ); + // any()/all() over a plain yes/no has nothing to collapse. + let scalar = node(Expr::Any(Box::new(cmp(CmpOp::Gt, prop("score"), c(8.0))))); + assert!(error(&g, &scalar).contains("any()/all()")); + assert_eq!(any_high.to_string(), "NODE(ANY(TEMPORAL(score) > 8))"); +} + +#[test] +fn aggregates_over_history_and_over_lists_of_lists() { + let g = graph(); + let sum = Expr::Agg(Agg::Sum, Box::new(history("score"))); + let f = node(cmp(CmpOp::Ge, sum, c(10.0))); + assert_eq!(nodes(&g, &f), ["alice"]); + let len = Expr::Agg(Agg::Len, Box::new(history("score"))); + let f = node(cmp(CmpOp::Eq, len, c(2u64))); + assert_eq!(nodes(&g, &f), ["bob", "dave"]); + // eve.scores is a list per update: the history is a list of lists, the + // sum is one number per update, and the comparison is one answer per + // update that any()/all() collapse. + let sums = Expr::Agg(Agg::Sum, Box::new(history("scores"))); + let any_big = node(Expr::Any(Box::new(cmp(CmpOp::Ge, sums.clone(), c(5i64))))); + assert_eq!(nodes(&g, &any_big), ["eve"]); + let all_big = node(Expr::All(Box::new(cmp(CmpOp::Ge, sums, c(5i64))))); + assert_eq!(nodes(&g, &all_big), Vec::::new()); +} + +#[test] +fn edges_endpoints_and_structure() { + let g = graph(); + let heavy = FilterExpr::Edge(cmp(CmpOp::Gt, edge_prop("w"), Expr::Const(1i64.into()))); + assert_eq!(edges(&g, &heavy), ["alice->bob", "carol->dave"]); + let from_alice = FilterExpr::Edge(cmp( + CmpOp::Eq, + src(field(Field::Name)), + Expr::Const("alice".into()), + )); + assert_eq!(edges(&g, &from_alice), ["alice->bob"]); + // Two endpoint values compare at the edge level. + let downhill = FilterExpr::Edge(cmp(CmpOp::Gt, src(prop("score")), dst(prop("score")))); + assert_eq!(edges(&g, &downhill), ["alice->bob"]); + // A node predicate through an endpoint is an edge predicate. + let into_scored = FilterExpr::Edge(dst(Expr::IsSome(Box::new(prop("score"))))); + assert_eq!(edges(&g, &into_scored), ["alice->bob", "carol->dave"]); + let works = FilterExpr::Edge(Expr::Read(EdgeLeaf::IsActive { + views: vec![ViewOp::Layers(vec!["works".into()])], + })); + assert_eq!(edges(&g, &works), ["bob->carol"]); + assert_eq!(works.to_string(), "EDGE(LAYER[works](IS_ACTIVE))"); + let exploded = FilterExpr::ExplodedEdge(cmp( + CmpOp::Eq, + Expr::Read(ExplodedEdgeLeaf::Property { + views: vec![], + name: "w".into(), + temporal: false, + }), + Expr::Const(1i64.into()), + )); + assert_eq!(edges(&g, &exploded), ["alice->bob", "bob->carol"]); + assert!(heavy.tests_edges() && !node(c(true)).tests_edges()); +} + +#[test] +fn type_clashes_are_refused_when_the_filter_is_built() { + let g = graph(); + let cases: Vec<(FilterExpr, &str)> = vec![ + ( + node(cmp(CmpOp::Gt, prop("score"), c("x"))), + "cannot be compared with F64", + ), + (node(prop("score")), "needs a yes/no answer"), + ( + node(Expr::IsSome(Box::new(degree(Direction::BOTH)))), + "always has a value", + ), + ( + node(Expr::Str( + StrOp::Contains, + Box::new(prop("score")), + Box::new(c("x")), + )), + "string operator requires a Str property", + ), + ( + node(Expr::Str( + StrOp::Contains, + Box::new(field(Field::Name)), + Box::new(c(3i64)), + )), + "cannot be compared with Str", + ), + (node(cmp(CmpOp::Gt, prop("scores"), c(1i64))), "List"), + ( + node(Expr::And(vec![prop("score"), c(true)])), + "and needs a yes/no answer", + ), + (node(Expr::And(vec![])), "needs at least one operand"), + ]; + for (filter, expected) in cases { + let msg = error(&g, &filter); + assert!(msg.contains(expected), "{filter}: {msg}"); + } +} + +#[test] +fn a_view_leg_restricts_the_whole_filter() { + let g = graph(); + let score_gt_4 = node(cmp(CmpOp::Gt, prop("score"), c(4.0))); + let win = FilterExpr::View(vec![window(0, 7)]); + // The view applies first and the predicate runs inside it. + let f = FilterExpr::And(vec![win.clone(), score_gt_4.clone()]); + assert_eq!(nodes(&g, &f), ["alice", "bob"]); + // A view alone is the result. + let f = FilterExpr::View(vec![window(0, 5), ViewOp::Latest]); + assert_eq!(edges(&g, &f), ["alice->bob"]); + assert_eq!(f.to_string(), "VIEW(WINDOW[0..5] . LATEST)"); + assert!(FilterExpr::View(vec![]).compile().is_err()); + // Under `or` or `not` a view has no meaning the engine can give it. + assert!(FilterExpr::Or(vec![win.clone(), score_gt_4.clone()]) + .compile() + .is_err()); + assert!(FilterExpr::Not(Box::new(win.clone())).compile().is_err()); + assert!(f.has_view() && !score_gt_4.has_view()); +} + +#[test] +fn trees_round_trip_through_json_with_the_read_as_the_key() { + let f = FilterExpr::And(vec![ + node(Expr::Any(Box::new(cmp( + CmpOp::Gt, + Expr::Read(NodeLeaf::Property { + views: vec![window(0, 5)], + name: "score".into(), + temporal: true, + }), + c(4.0), + )))), + FilterExpr::Edge(cmp( + CmpOp::Eq, + src(field(Field::Name)), + Expr::Const("alice".into()), + )), + ]); + let json = serde_json::to_string(&f).unwrap(); + assert!( + json.contains(r#""property":{"views":[{"window":"#), + "{json}" + ); + assert!( + json.contains(r#""src":{"field":{"field":"name"}}"#), + "{json}" + ); + assert!(!json.contains("read"), "{json}"); + let back: FilterExpr = serde_json::from_str(&json).unwrap(); + assert_eq!(back, f); +} + +#[test] +fn an_opaque_filter_refuses_to_serialise() { + let f = FilterExpr::Opaque(OpaqueFilter(node(c(true)).compile().unwrap())); + let err = serde_json::to_string(&f).unwrap_err().to_string(); + assert!(err.contains(OPAQUE_FILTER_ERROR)); +} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 9741df7ffb..02376368db 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -67,6 +67,7 @@ pub mod dyn_factory; pub mod edge_expr; pub mod edge_filter; pub mod exploded_edge_filter; +pub mod expr; pub mod filter; pub mod filter_operator; pub mod filter_value; From 189d39df7dfa6a75f1a574eaf45278f6a9f6f542 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:03:47 +0100 Subject: [PATCH 124/155] Compile the typed filter API through the expression tree Typed predicates convert to the entity-split tree and compile there, so rust, python and GraphQL filters go through one compiler and one set of checks. The qualifier inversion that let `.eq(x).any()` compile, and the comparison, string, presence and membership ops only that path used, are removed. The previous tree compiles through an adapter until its users move over. --- .../test_filters/test_exploded_edge_filter.py | 26 +- .../test_filters/test_node_filter.py | 7 +- .../test_filters/test_node_property_filter.py | 25 +- .../test_graph_edges_property_filter.py | 4 +- .../test_graph_nodes_property_filter.py | 4 +- .../test_nodes_property_filter.py | 4 +- raphtory-tests/tests/edge_expr_filter.rs | 4 +- raphtory-tests/tests/test_filters.rs | 6 +- raphtory/src/db/api/state/ops/mod.rs | 27 +- .../views/filter/model/edge_expr/filters.rs | 550 ---------- .../graph/views/filter/model/edge_expr/mod.rs | 1 - .../graph/views/filter/model/edge_expr/ops.rs | 221 +---- .../graph/views/filter/model/edge_filter.rs | 165 +-- .../filter/model/exploded_edge_filter.rs | 121 +-- .../graph/views/filter/model/expr/compile.rs | 178 +++- .../graph/views/filter/model/expr/convert.rs | 586 +++++++++++ .../db/graph/views/filter/model/expr/mod.rs | 2 + .../views/filter/model/filter_operator.rs | 9 - .../src/db/graph/views/filter/model/mod.rs | 37 +- .../views/filter/model/node_expr/dyn_expr.rs | 43 +- .../views/filter/model/node_expr/exprs.rs | 99 +- .../views/filter/model/node_expr/filters.rs | 939 +----------------- .../graph/views/filter/model/node_expr/mod.rs | 22 +- .../graph/views/filter/model/node_expr/ops.rs | 300 +----- .../src/db/graph/views/filter/model/tree.rs | 645 ++++++------ 25 files changed, 1170 insertions(+), 2855 deletions(-) delete mode 100644 raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs create mode 100644 raphtory/src/db/graph/views/filter/model/expr/convert.rs diff --git a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py index 5d5ffbc31c..e39e1e2960 100644 --- a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py @@ -511,15 +511,15 @@ def test_all_property_types(GraphClass): # Lists (tags) — odd comparisons ( lambda: filter.ExplodedEdge.property("tags").contains("team_a"), - "string operator requires a Str property", + "one answer per element", ), ( lambda: filter.ExplodedEdge.property("tags").not_contains("team_z"), - "string operator requires a Str property", + "one answer per element", ), ( lambda: filter.ExplodedEdge.property("tags").fuzzy_search("blah", 2, False), - "string operator requires a Str property", + "one answer per element", ), ( lambda: filter.ExplodedEdge.property("tags") < ["x"], @@ -720,19 +720,19 @@ def test_all_property_types(GraphClass): ), ( lambda: filter.ExplodedEdge.property("tags") < 3, - "not valid for list properties", + "cannot be compared with List", ), ( lambda: filter.ExplodedEdge.property("tags") > 1, - "not valid for list properties", + "cannot be compared with List", ), ( lambda: filter.ExplodedEdge.property("tags") <= 2, - "not valid for list properties", + "cannot be compared with List", ), ( lambda: filter.ExplodedEdge.property("tags") >= 3, - "not valid for list properties", + "cannot be compared with List", ), ( lambda: filter.ExplodedEdge.property("tags").contains(2), @@ -753,19 +753,19 @@ def test_all_property_types(GraphClass): ), ( lambda: filter.ExplodedEdge.property("meta") < 3, - "not valid for map properties", + "cannot be compared with Map", ), ( lambda: filter.ExplodedEdge.property("meta") > 1, - "not valid for map properties", + "cannot be compared with Map", ), ( lambda: filter.ExplodedEdge.property("meta") <= 2, - "not valid for map properties", + "cannot be compared with Map", ), ( lambda: filter.ExplodedEdge.property("meta") >= 3, - "not valid for map properties", + "cannot be compared with Map", ), ( lambda: filter.ExplodedEdge.property("meta").contains(2), @@ -860,8 +860,8 @@ def test_temporal_constant(GraphClass): (filter.ExplodedEdge.property("weight").temporal().any() >= 3, 2), (filter.ExplodedEdge.property("weight").temporal().any().is_in([1, 2]), 4), (filter.ExplodedEdge.property("weight").temporal().any().is_not_in([3]), 4), - (filter.ExplodedEdge.property("weight").temporal().any().is_some(), 6), - (filter.ExplodedEdge.property("weight").temporal().any().is_none(), 0), + (filter.ExplodedEdge.property("weight").temporal().is_some(), 6), + (filter.ExplodedEdge.property("weight").temporal().is_none(), 0), (filter.ExplodedEdge.property("weight").temporal().last() == 2, 2), (filter.ExplodedEdge.property("weight").temporal().last() != 3, 4), (filter.ExplodedEdge.property("weight").temporal().last() < 3, 4), diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index 56064223c4..0dd9f86c21 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -947,11 +947,8 @@ def test_filter_nodes_for_node_name_all_is_invalid(): def check(graph): # The expression builds (the python surface is one Expr type); applying # it rejects the qualifier on a scalar field. - filter_expr = filter.Node.name().all() == True - with pytest.raises( - Exception, - match=r"any\(\)/all\(\) require list or temporal values, found Str", - ): + with pytest.raises(Exception, match=r"cannot be compared with Str"): + filter_expr = filter.Node.name().all() == True graph.filter(filter_expr).nodes.id return check diff --git a/python/tests/test_base_install/test_filters/test_node_property_filter.py b/python/tests/test_base_install/test_filters/test_node_property_filter.py index 335562bd65..71c8a43d7c 100644 --- a/python/tests/test_base_install/test_filters/test_node_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_property_filter.py @@ -793,7 +793,7 @@ def check(graph): filter_expr = filter.Node.property("prop8").any() == value with pytest.raises( Exception, - match=r"of type Str cannot be compared with I64", + match=r"of type Str cannot be compared with List", ): graph.filter(filter_expr).nodes.id @@ -803,9 +803,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_with_with_qualifier_alongside_illegal_operators(): def check(graph): - # Elementwise presence with a qualifier collapse: list elements are - # always present, so this matches every node carrying the property. + # Presence is a whole-value question; a qualifier has nothing to + # collapse on a single yes/no. filter_expr = filter.Node.property("prop8").any().is_some() + with pytest.raises(Exception, match=r"any\(\)/all\(\) collapse"): + graph.filter(filter_expr).nodes.id + filter_expr = filter.Node.property("prop8").is_some() assert sorted(graph.filter(filter_expr).nodes.id) == ["a", "d"] return check @@ -817,14 +820,14 @@ def check(graph): filter_expr = filter.Node.property("prop8").all().len() > 0 with pytest.raises( Exception, - match=r"len\(\) is not valid on a scalar expression", + match=r"any\(\)/all\(\) collapse", ): graph.filter(filter_expr).nodes.id filter_expr = filter.Node.property("prop8").sum().any() > 0 with pytest.raises( Exception, - match=r"any\(\)/all\(\) require list or temporal values", + match=r"any\(\)/all\(\) collapse", ): graph.filter(filter_expr).nodes.id @@ -1139,7 +1142,7 @@ def check(graph): def test_filter_nodes_for_temporal_property_fails(): def check(graph): filter_expr = filter.Node.property("prop1").temporal() == 60 - msg = r"value I64\(60\) of type I64 cannot be compared with List" + msg = r"one answer per element" with pytest.raises( Exception, match=msg, @@ -1191,7 +1194,7 @@ def check(graph): # error rather than a silent no-match. with pytest.raises( Exception, - match=r"not valid for list properties|cannot be compared with List", + match=r"not valid for list properties|one answer per element", ): graph.filter(expr).nodes.id @@ -1251,7 +1254,7 @@ def check(graph): # error rather than a silent no-match. with pytest.raises( Exception, - match=r"not valid for list properties|cannot be compared with List", + match=r"not valid for list properties|one answer per element", ): graph.filter(expr).nodes.id @@ -1272,7 +1275,7 @@ def check(graph): # error rather than a silent no-match. with pytest.raises( Exception, - match=r"not valid for list properties|cannot be compared with List", + match=r"not valid for list properties|one answer per element", ): graph.filter(expr).nodes.id @@ -1295,7 +1298,7 @@ def check(graph): # error rather than a silent no-match. with pytest.raises( Exception, - match=r"not valid for list properties|cannot be compared with List", + match=r"not valid for list properties|one answer per element", ): graph.filter(expr).nodes.id @@ -1316,7 +1319,7 @@ def check(graph): # error rather than a silent no-match. with pytest.raises( Exception, - match=r"not valid for list properties|cannot be compared with List", + match=r"not valid for list properties|one answer per element", ): graph.filter(expr).nodes.id diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py index 283f164d4c..8b78b364fd 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py @@ -54,9 +54,7 @@ def test_graph_edge_property_filter_equal_type_error(graph): } } """ - expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be compared with List" - ) + expected_error_message = "Invalid filter: a filter needs a yes/no answer, but this comparison gives one answer per element (List); add any() or all() to say which elements must match" run_graphql_error_test(query, expected_error_message, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index e7802082a3..081178ed3f 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -43,9 +43,7 @@ def test_graph_node_property_filter_equal_type_error(graph): } } """ - expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be compared with List" - ) + expected_error_message = "Invalid filter: a filter needs a yes/no answer, but this comparison gives one answer per element (List); add any() or all() to say which elements must match" run_graphql_error_test(query, expected_error_message, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py index d92eaf1843..634a7e6cec 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py @@ -103,9 +103,7 @@ def test_node_property_filter_equal_type_error(graph): } } """ - expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be compared with List" - ) + expected_error_message = "Invalid filter: a filter needs a yes/no answer, but this comparison gives one answer per element (List); add any() or all() to say which elements must match" run_graphql_error_test(query, expected_error_message, graph) diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs index 61a0c011b2..1bb0a70f26 100644 --- a/raphtory-tests/tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -477,8 +477,8 @@ fn test_edge_quantified_any_is_not_in() { let filter = EdgeFilter .property("tag") .temporal() - .any() - .is_not_in([Prop::str("metal"), Prop::str("blues")]); + .is_not_in([Prop::str("metal"), Prop::str("blues")]) + .any(); let result = g.filter(filter).unwrap(); // A->B has "rock" and "rock-n-roll" not in set → passes // C->D has "jazz" not in set → passes diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 92a4d878c7..26d1299a09 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -7883,8 +7883,8 @@ mod test_node_property_filter_agg { fn test_node_property_all() { let filter = NodeFilter .property("p_bools_all") - .all() - .eq(Prop::Bool(true)); + .eq(Prop::Bool(true)) + .all(); let expected = vec!["n10", "n4"]; apply_assertion(filter, &expected); } @@ -8041,8 +8041,8 @@ mod test_node_property_filter_agg { let filter = NodeFilter .property("p_bools_all") .temporal() - .all() .eq(true) + .all() .all(); let expected = vec!["n4", "n10"]; apply_assertion(filter, &expected); diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 8cba0bac14..4ba24058c3 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -3,12 +3,9 @@ pub mod history; pub mod node; pub mod properties; -use crate::db::{ - api::{ - state::ops::filter::{AndOp, NotOp, OrOp}, - view::internal::NodeList, - }, - graph::views::filter::model::{node_expr::BinaryCmpNodeOp, BinaryOp, Comparable}, +use crate::db::api::{ + state::ops::filter::{AndOp, NotOp, OrOp}, + view::internal::NodeList, }; pub use history::*; pub use node::*; @@ -18,7 +15,6 @@ 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; @@ -49,23 +45,6 @@ pub trait NodeOp: Send + Sync { { Map { op: self, map } } - - /// Override if binary comparison can be optimized - fn bin_cmp( - &self, - op: BinaryOp, - rhs: Arc>, - ) -> Arc> - where - Self: Clone + 'static, - Self::Output: Comparable, - { - Arc::new(BinaryCmpNodeOp { - left: Arc::new(self.clone()), - right: rhs, - op, - }) - } } // impl EntityExpr for T { diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs deleted file mode 100644 index 94d1a6de52..0000000000 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs +++ /dev/null @@ -1,550 +0,0 @@ -//! Filter types for edge expressions — bridge from EdgeExpr to a filtered graph. -//! -//! Parallel to `node_expr/filters.rs` but for edges: `create_filter` produces -//! an `EdgeExprFilteredGraph` instead of a `NodeFilteredGraph`. - -use super::{ - ops::{ - BinaryCmpEdgeOp, ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, - ListAwareUnaryEdgeOp, PropValueSetEdgeOp, StringEdgeOp, UnaryEdgeOp, - }, - EdgeOp, -}; -pub(crate) use crate::db::graph::views::filter::model::{BinaryCmpExpr, StringExpr, UnaryExpr}; -use crate::{ - db::{ - api::{state::ops::NotANodeFilter, view::internal::GraphView}, - graph::views::filter::{ - edge_expr_filtered_graph::EdgeExprFilteredGraph, - exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, - model::{ - comparable_set_values, - edge_filter::EdgeFilter, - elem_prop_type, - filter_operator::ElemQual, - node_expr::{ - filters::PropValueSetExpr, - ops::{AllEdgeOp, AnyEdgeOp}, - CreateOp, - }, - resolved_prop_type, validate_binary_op, validate_const_comparable, - validate_string_op, validate_types_comparable, CreateFilter, ExplodedEdgeFilter, - }, - }, - }, - errors::GraphError, -}; -use raphtory_api::core::entities::{ - edges::edge_ref::EdgeRef, - properties::prop::{Prop, PropType}, -}; -use raphtory_storage::graph::graph::GraphStorage; -use std::sync::Arc; -// ───────────────────────────────────────────────────────────────────────────── -// BinaryCmpExpr -// ───────────────────────────────────────────────────────────────────────────── - -/// Adapts an elementwise boolean edge op to the plain boolean output the -/// filtered-graph wrappers consume. -#[derive(Clone)] -struct TruthyEdgeOp<'g> { - inner: Arc> + 'g>, -} - -impl<'g> EdgeOp for TruthyEdgeOp<'g> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - matches!(self.inner.apply(storage, edge), Some(Prop::Bool(true))) - } -} - -/// Collapse elementwise boolean results per the collected qualifiers, -/// innermost list level first, and adapt to a boolean edge filter. -pub(crate) fn qualify_edge_filter<'g>( - elemwise: Arc> + 'g>, - quals: &[ElemQual], -) -> Arc + 'g> { - let mut op = elemwise; - // Qualifiers are collected in call order (outermost list level first); - // wrapping starts at the innermost level, so iterate in reverse. - for q in quals.iter().rev() { - op = match q { - ElemQual::Any => Arc::new(AnyEdgeOp { inner: op }), - ElemQual::All => Arc::new(AllEdgeOp { inner: op }), - }; - } - Arc::new(TruthyEdgeOp { inner: op }) -} - -impl CreateFilter for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - EdgeExprFilteredGraph + 'graph>>; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let expr_pt = self.left.prop_type(); - let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; - let right = self.right.create_edge_op(filtered.clone())?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len())?; - let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); - validate_binary_op(&self.op, &lhs_pt)?; - match right.const_value() { - Some(c) => validate_const_comparable(&lhs_pt, c.as_ref())?, - None => validate_types_comparable(&lhs_pt, &rhs_pt)?, - } - let op: Arc + 'graph> = if quals.is_empty() { - Arc::new(BinaryCmpEdgeOp { - left, - right, - op: self.op, - }) - } else { - qualify_edge_filter( - Arc::new(ListAwareCmpEdgeOp { - left, - right, - op: self.op, - }), - &quals, - ) - }; - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -impl CreateFilter for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - ExplodedEdgeExprFilteredGraph + 'graph>>; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let expr_pt = self.left.prop_type(); - let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; - let right = self.right.create_edge_op(filtered.clone())?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len())?; - let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); - validate_binary_op(&self.op, &lhs_pt)?; - match right.const_value() { - Some(c) => validate_const_comparable(&lhs_pt, c.as_ref())?, - None => validate_types_comparable(&lhs_pt, &rhs_pt)?, - } - let op: Arc + 'graph> = if quals.is_empty() { - Arc::new(BinaryCmpEdgeOp { - left, - right, - op: self.op, - }) - } else { - qualify_edge_filter( - Arc::new(ListAwareCmpEdgeOp { - left, - right, - op: self.op, - }), - &quals, - ) - }; - Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// UnaryExpr -// ───────────────────────────────────────────────────────────────────────────── - -impl CreateFilter for UnaryExpr -where - E: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; - let op: Arc + 'graph> = if quals.is_empty() { - Arc::new(UnaryEdgeOp { inner, op: self.op }) - } else { - qualify_edge_filter( - Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op }), - &quals, - ) - }; - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -impl CreateFilter for UnaryExpr -where - E: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - ExplodedEdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; - let op: Arc + 'graph> = if quals.is_empty() { - Arc::new(UnaryEdgeOp { inner, op: self.op }) - } else { - qualify_edge_filter( - Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op }), - &quals, - ) - }; - Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// StringExpr — string expression filter for edges -// ───────────────────────────────────────────────────────────────────────────── - -impl CreateFilter for StringExpr -where - L: CreateOp, - R: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; - let right = self.right.create_edge_op(filtered.clone())?; - validate_string_op(&elem_prop_type(&left.prop_type(), quals.len())?)?; - match right.const_value() { - Some(c) => validate_const_comparable(&PropType::Str, c.as_ref())?, - None => {} - } - let op: Arc + 'graph> = if quals.is_empty() { - Arc::new(StringEdgeOp { - left, - right, - op: self.op, - }) - } else { - qualify_edge_filter( - Arc::new(ListAwareStringEdgeOp { - left, - right, - op: self.op, - }), - &quals, - ) - }; - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -impl CreateFilter for StringExpr -where - L: CreateOp, - R: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - ExplodedEdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let (left, quals) = self.left.create_qualified_edge_op(filtered.clone())?; - let right = self.right.create_edge_op(filtered.clone())?; - validate_string_op(&elem_prop_type(&left.prop_type(), quals.len())?)?; - match right.const_value() { - Some(c) => validate_const_comparable(&PropType::Str, c.as_ref())?, - None => {} - } - let op: Arc + 'graph> = if quals.is_empty() { - Arc::new(StringEdgeOp { - left, - right, - op: self.op, - }) - } else { - qualify_edge_filter( - Arc::new(ListAwareStringEdgeOp { - left, - right, - op: self.op, - }), - &quals, - ) - }; - Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// PropValueSetExpr — is_in / is_not_in for edge-side exprs -// ───────────────────────────────────────────────────────────────────────────── - -impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let expr_pt = self.expr.prop_type(); - let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; - let values = comparable_set_values(&lhs_pt, self.values); - let op: Arc + 'graph> = if quals.is_empty() { - Arc::new(PropValueSetEdgeOp { - inner, - values, - op: self.op, - }) - } else { - qualify_edge_filter( - Arc::new(ListAwareSetEdgeOp { - inner, - values, - op: self.op, - }), - &quals, - ) - }; - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - ExplodedEdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let expr_pt = self.expr.prop_type(); - let (inner, quals) = self.expr.create_qualified_edge_op(filtered.clone())?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; - let values = comparable_set_values(&lhs_pt, self.values); - let op: Arc + 'graph> = if quals.is_empty() { - Arc::new(PropValueSetEdgeOp { - inner, - values, - op: self.op, - }) - } else { - qualify_edge_filter( - Arc::new(ListAwareSetEdgeOp { - inner, - values, - op: self.op, - }), - &quals, - ) - }; - Ok(ExplodedEdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index b501187f53..30e3210bf9 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -22,7 +22,6 @@ use raphtory_api::core::entities::{edges::edge_ref::EdgeRef, properties::prop::PropType}; use raphtory_storage::graph::graph::GraphStorage; -pub mod filters; pub mod ops; pub use super::{Metadata, Property}; diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index 25d952147d..fd1be29728 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -20,20 +20,10 @@ use raphtory_api::core::entities::{ use raphtory_storage::graph::graph::GraphStorage; use super::EdgeOp; -use crate::db::{ - api::state::ops::NodeOp, - graph::views::filter::model::{ - edge_filter::Endpoint, - filter_operator::{BinaryOp, Comparable}, - node_expr::ops::{broadcast_binary, broadcast_unary}, - }, -}; +use crate::db::{api::state::ops::NodeOp, graph::views::filter::model::edge_filter::Endpoint}; use raphtory_api::core::entities::properties::prop::PropArray; use std::sync::Arc; -use crate::db::graph::views::filter::model::{ - filter_operator::UnaryOp, SetOp, StringComparable, StringOp, -}; // ───────────────────────────────────────────────────────────────────────────── // Arc — blanket impl so Arc-boxed ops satisfy EdgeOp // ───────────────────────────────────────────────────────────────────────────── @@ -148,215 +138,6 @@ impl EdgeOp for TemporalEdgePropOp { } } -// ───────────────────────────────────────────────────────────────────────────── -// BinaryCmpEdgeOp<'g> — compares two EdgeOp outputs, returns bool -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct BinaryCmpEdgeOp<'g, L> { - pub(crate) left: Arc + 'g>, - pub(crate) right: Arc + 'g>, - pub(crate) op: BinaryOp, -} - -impl<'g, L: Comparable + Clone + Send + Sync + 'static> EdgeOp for BinaryCmpEdgeOp<'g, L> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - let l = self.left.apply(storage, edge); - let r = self.right.apply(storage, edge); - L::binary_cmp(&self.op, &l, &r) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// UnaryEdgeOp<'g, I> — is_some / is_none on Option-valued expressions -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct UnaryEdgeOp<'g, I: Clone + Send + Sync + 'static> { - pub(crate) inner: Arc> + 'g>, - pub(crate) op: UnaryOp, -} - -impl<'g, I: Clone + Send + Sync + 'static> EdgeOp for UnaryEdgeOp<'g, I> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - let v = self.inner.apply(storage, edge); - match self.op { - UnaryOp::IsSome => v.is_some(), - UnaryOp::IsNone => v.is_none(), - } - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// StringEdgeOp<'g, T> — applies a StringOp to two EdgeOp values -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct StringEdgeOp<'g, T: StringComparable> { - pub(crate) left: Arc + 'g>, - pub(crate) right: Arc + 'g>, - pub(crate) op: StringOp, -} - -impl<'g, T: StringComparable> EdgeOp for StringEdgeOp<'g, T> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - T::string_cmp( - &self.op, - &self.left.apply(storage, edge), - &self.right.apply(storage, edge), - ) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// PropValueSetEdgeOp<'g> — is_in / is_not_in for Option (linear scan) -// ───────────────────────────────────────────────────────────────────────────── - -/// Checks whether an `Option` value is in (or not in) a fixed `Vec`. -/// Uses linear scan because `Prop` may contain floats that don't implement `Hash`. -#[derive(Clone)] -pub(crate) struct PropValueSetEdgeOp<'g> { - pub(crate) inner: Arc> + 'g>, - pub(crate) values: Vec, - pub(crate) op: SetOp, -} - -impl<'g> EdgeOp for PropValueSetEdgeOp<'g> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - match self.inner.apply(storage, edge) { - None => false, - Some(v) => match self.op { - SetOp::IsIn => self - .values - .iter() - .any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), - SetOp::IsNotIn => self - .values - .iter() - .all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), - }, - } - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// ListAwareCmpEdgeOp<'g> — element-wise comparison via broadcast_binary -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct ListAwareCmpEdgeOp<'g> { - pub(crate) left: Arc> + 'g>, - pub(crate) right: Arc> + 'g>, - pub(crate) op: BinaryOp, -} - -impl<'g> EdgeOp for ListAwareCmpEdgeOp<'g> { - type Output = Option; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let lv = self.left.apply(storage, edge); - let rhs = self.right.apply(storage, edge); - let op = &self.op; - broadcast_binary(lv, rhs, &|lv, rhs| { - Some(Prop::Bool(Prop::binary_cmp(op, &lv?, &rhs?))) - }) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// ListAwareStringEdgeOp<'g> — element-wise string comparison via broadcast_binary -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct ListAwareStringEdgeOp<'g> { - pub(crate) left: Arc> + 'g>, - pub(crate) right: Arc> + 'g>, - pub(crate) op: StringOp, -} - -impl<'g> EdgeOp for ListAwareStringEdgeOp<'g> { - type Output = Option; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let lv = self.left.apply(storage, edge); - let rhs = self.right.apply(storage, edge); - let op = &self.op; - broadcast_binary(lv, rhs, &|lv, rhs| { - Some(Prop::Bool(Option::::string_cmp(op, &lv, &rhs))) - }) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// ListAwareSetEdgeOp<'g> — element-wise set membership via broadcast_unary -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct ListAwareSetEdgeOp<'g> { - pub(crate) inner: Arc> + 'g>, - pub(crate) values: Vec, - pub(crate) op: SetOp, -} - -impl<'g> EdgeOp for ListAwareSetEdgeOp<'g> { - type Output = Option; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let vals = self.inner.apply(storage, edge); - let values = &self.values; - let op = &self.op; - broadcast_unary(vals, |v| { - let v = v?; - Some(Prop::Bool(match op { - SetOp::IsIn => values - .iter() - .any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), - SetOp::IsNotIn => values - .iter() - .all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), - })) - }) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// ListAwareUnaryEdgeOp — element-wise is_some / is_none via broadcast_unary -// -// Unlike `UnaryEdgeOp` (which returns `bool` for use in `CreateFilter`), this -// op returns `Option` so it can plug into the expression chain via -// `CreateOp`. The closure intentionally does NOT `?`-propagate the inner -// `None` — the whole purpose of `is_some`/`is_none` is to test that case. -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct ListAwareUnaryEdgeOp<'g> { - pub(crate) inner: Arc> + 'g>, - pub(crate) op: UnaryOp, -} - -impl<'g> EdgeOp for ListAwareUnaryEdgeOp<'g> { - type Output = Option; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let vals = self.inner.apply(storage, edge); - let op = &self.op; - broadcast_unary(vals, |v| { - Some(Prop::Bool(match op { - UnaryOp::IsSome => v.is_some(), - UnaryOp::IsNone => v.is_none(), - })) - }) - } -} - // ───────────────────────────────────────────────────────────────────────────── // EdgeEndpointNodeOp — applies a node op to the src or dst VID of an edge // 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 6adfaeefb9..4ff625960a 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -1,37 +1,25 @@ use crate::{ db::{ - api::{ - state::ops::{NodeOp, NotANodeFilter}, - view::internal::GraphView, - }, - graph::views::filter::{ - edge_expr_filtered_graph::EdgeExprFilteredGraph, - edge_node_filtered_graph::EdgeNodeFilteredGraph, - model::{ - edge_expr::{ops::EdgeEndpointNodeOp, EdgeOp}, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - latest_filter::Latest, - layered_filter::Layered, - node_expr::{CreateOp, EntityExpr, PredicateLhs}, - node_filter::NodeFilter, - snapshot_filter::{SnapshotAt, SnapshotLatest}, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, DynFilter, EdgeViewFilterOps, EntityMarker, - InternalViewWrapOps, Wrap, - }, - CreateFilter, + api::view::internal::GraphView, + graph::views::filter::model::{ + edge_expr::{ops::EdgeEndpointNodeOp, EdgeOp}, + is_active_edge_filter::IsActiveEdge, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + latest_filter::Latest, + layered_filter::Layered, + node_expr::{CreateOp, EntityExpr, PredicateLhs}, + node_filter::NodeFilter, + snapshot_filter::{SnapshotAt, SnapshotLatest}, + windowed_filter::Windowed, + CombinedFilter, ComposableFilter, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, + Wrap, }, }, errors::GraphError, }; -use raphtory_api::core::{ - entities::{edges::edge_ref::EdgeRef, properties::prop::Prop}, - storage::timeindex::EventTime, -}; -use raphtory_storage::graph::graph::GraphStorage; +use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; use serde::{Deserialize, Serialize}; use std::{fmt, fmt::Display, sync::Arc}; @@ -183,127 +171,6 @@ impl Wrap for EdgeEndpointWrapper { impl ComposableFilter for EdgeEndpointWrapper where T: Clone + Send + Sync {} -impl CreateFilter for EdgeEndpointWrapper { - type EntityFiltered<'graph, G, F> - = EdgeNodeFilteredGraph> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type NodeFilter<'graph, G, F> - = NotANodeFilter - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type FilteredGraph<'graph, G> - = T::FilteredGraph<'graph, G> - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let filter = self.inner.create_node_filter(graph.clone(), filtered)?; - Ok(EdgeNodeFilteredGraph::new(graph, self.endpoint, filter)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - self.inner.filter_graph_view(graph) - } -} - -// ── expr layer: a full node filter evaluated on an edge endpoint ── - -/// Evaluates an erased node filter against the src or dst node of each edge. -/// -/// Carries whatever the nested filter is (combinators, views, property -/// conditions) by compiling it to a boolean node op and applying that to the -/// endpoint's VID at evaluation time. -#[derive(Clone)] -pub struct EdgeEndpointNodeFilter { - pub endpoint: Endpoint, - pub inner: DynFilter, -} - -#[derive(Clone)] -struct EndpointNodeBoolOp<'g> { - endpoint: Endpoint, - node_op: Arc + 'g>, -} - -impl<'g> EdgeOp for EndpointNodeBoolOp<'g> { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - let vid = match self.endpoint { - Endpoint::Src => edge.src(), - Endpoint::Dst => edge.dst(), - }; - self.node_op.apply(storage, vid) - } -} - -impl CreateFilter for EdgeEndpointNodeFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - EdgeExprFilteredGraph + 'graph>>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let node_op = self - .inner - .create_dyn_node_filter(Arc::new(graph.clone()), Arc::new(filtered))?; - let op: Arc + 'graph> = Arc::new(EndpointNodeBoolOp { - endpoint: self.endpoint, - node_op, - }); - Ok(EdgeExprFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -impl ComposableFilter for EdgeEndpointNodeFilter {} - // ── expr layer: endpoint expressions bridge node ops into edge ops ── impl PredicateLhs for EdgeEndpointWrapper {} 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 3ab096c1fe..816e6e473d 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 @@ -1,21 +1,9 @@ -use crate::{ - db::{ - api::{state::ops::NotANodeFilter, view::internal::GraphView}, - graph::views::filter::{ - exploded_edge_node_filtered_graph::ExplodedEdgeNodeFilteredGraph, - model::{ - edge_filter::Endpoint, is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, node_filter::NodeFilter, windowed_filter::Windowed, - CombinedFilter, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, Wrap, - }, - CreateFilter, - }, - }, - errors::GraphError, +use crate::db::graph::views::filter::model::{ + is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, windowed_filter::Windowed, + CombinedFilter, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, Wrap, }; use raphtory_api::core::storage::timeindex::EventTime; -use std::{fmt, fmt::Display}; #[derive(Clone, Debug, Copy, Default, PartialEq, Eq)] pub struct ExplodedEdgeFilter; @@ -26,18 +14,6 @@ impl From for EntityMarker { } } -impl ExplodedEdgeFilter { - #[inline] - pub fn src() -> ExplodedEdgeEndpointWrapper { - ExplodedEdgeEndpointWrapper::new(NodeFilter, Endpoint::Src) - } - - #[inline] - pub fn dst() -> ExplodedEdgeEndpointWrapper { - ExplodedEdgeEndpointWrapper::new(NodeFilter, Endpoint::Dst) - } -} - impl Wrap for ExplodedEdgeFilter { type Wrapped = T; @@ -73,92 +49,3 @@ impl EdgeViewFilterOps for ExplodedEdgeFilter { IsSelfLoopEdge } } - -#[derive(Debug, Clone)] -pub struct ExplodedEdgeEndpointWrapper { - pub(crate) inner: T, - endpoint: Endpoint, -} - -impl Display for ExplodedEdgeEndpointWrapper { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner.fmt(f) - } -} - -impl ExplodedEdgeEndpointWrapper { - #[inline] - pub fn new(inner: T, endpoint: Endpoint) -> Self { - Self { inner, endpoint } - } - - #[inline] - pub fn map(self, f: impl FnOnce(T) -> U) -> ExplodedEdgeEndpointWrapper { - ExplodedEdgeEndpointWrapper { - inner: f(self.inner), - endpoint: self.endpoint, - } - } -} - -impl Wrap for ExplodedEdgeEndpointWrapper { - type Wrapped = ExplodedEdgeEndpointWrapper; - - fn wrap(&self, inner: T) -> Self::Wrapped { - ExplodedEdgeEndpointWrapper { - inner, - endpoint: self.endpoint, - } - } -} - -impl CreateFilter for ExplodedEdgeEndpointWrapper { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = ExplodedEdgeNodeFilteredGraph> - where - Self: 'graph, - G: GraphView + 'graph; - - type NodeFilter<'graph, G, F> - = NotANodeFilter - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - type FilteredGraph<'graph, G> - = T::FilteredGraph<'graph, G> - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> - where - T: 'graph, - { - let filter = self.inner.create_node_filter(graph.clone(), filtered)?; - Ok(ExplodedEdgeNodeFilteredGraph::new( - graph, - self.endpoint, - filter, - )) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Err(GraphError::NotNodeFilter) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - self.inner.filter_graph_view(graph) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index 5fa3f6b518..02de61e78d 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -82,6 +82,49 @@ pub trait Leaf: Clone + Debug + PartialEq + Send + Sync + 'static { /// Whether the read is scoped by a view. fn has_view(&self) -> bool; + + /// The latest value of a property, or its history when `temporal`, seen + /// through `views`. + fn property(views: Vec, name: String, temporal: bool) -> Self; + + /// A metadata entry seen through `views`. + fn metadata(views: Vec, name: String) -> Self; + + /// Whether the entity is active inside `views`. + fn is_active(views: Vec) -> Self; + + /// A view applied around the read: it scopes the read. + fn push_view(&mut self, op: ViewOp); + + /// The filter a yes/no expression over this leaf type is. + fn filter(expr: Expr) -> FilterExpr; +} + +impl Expr { + /// Scope every read in this expression by one more view, applied after + /// the views the reads already carry. + pub fn push_view(&mut self, op: ViewOp) { + match self { + Expr::Const(_) => {} + Expr::Read(leaf) => leaf.push_view(op), + Expr::Agg(_, e) + | Expr::IsSome(e) + | Expr::IsNone(e) + | Expr::Any(e) + | Expr::All(e) + | Expr::Not(e) => e.push_view(op), + Expr::In { expr, .. } => expr.push_view(op), + Expr::Cmp(_, l, r) | Expr::Str(_, l, r) => { + l.push_view(op.clone()); + r.push_view(op); + } + Expr::And(items) | Expr::Or(items) => { + for item in items { + item.push_view(op.clone()); + } + } + } + } } fn node_factory(views: &[ViewOp]) -> Arc { @@ -164,12 +207,52 @@ impl Leaf for NodeLeaf { } fn has_view(&self) -> bool { + !self.views().is_empty() + } + + fn property(views: Vec, name: String, temporal: bool) -> Self { + NodeLeaf::Property { + views, + name, + temporal, + } + } + + fn metadata(views: Vec, name: String) -> Self { + NodeLeaf::Metadata { views, name } + } + + fn is_active(views: Vec) -> Self { + NodeLeaf::IsActive { views } + } + + fn push_view(&mut self, op: ViewOp) { + self.views_mut().push(op); + } + + fn filter(expr: Expr) -> FilterExpr { + FilterExpr::Node(expr) + } +} + +impl NodeLeaf { + fn views(&self) -> &[ViewOp] { match self { NodeLeaf::Field { views, .. } | NodeLeaf::Degree { views, .. } | NodeLeaf::Property { views, .. } | NodeLeaf::Metadata { views, .. } - | NodeLeaf::IsActive { views } => !views.is_empty(), + | NodeLeaf::IsActive { views } => views, + } + } + + fn views_mut(&mut self) -> &mut Vec { + match self { + NodeLeaf::Field { views, .. } + | NodeLeaf::Degree { views, .. } + | NodeLeaf::Property { views, .. } + | NodeLeaf::Metadata { views, .. } + | NodeLeaf::IsActive { views } => views, } } } @@ -233,6 +316,39 @@ impl Leaf for EdgeLeaf { EdgeLeaf::Src(inner) | EdgeLeaf::Dst(inner) => inner.has_view(), } } + + fn property(views: Vec, name: String, temporal: bool) -> Self { + EdgeLeaf::Property { + views, + name, + temporal, + } + } + + fn metadata(views: Vec, name: String) -> Self { + EdgeLeaf::Metadata { views, name } + } + + fn is_active(views: Vec) -> Self { + EdgeLeaf::IsActive { views } + } + + /// A view around an endpoint read scopes the node read at that end. + fn push_view(&mut self, op: ViewOp) { + match self { + EdgeLeaf::Property { views, .. } + | EdgeLeaf::Metadata { views, .. } + | EdgeLeaf::IsActive { views } + | EdgeLeaf::IsValid { views } + | EdgeLeaf::IsDeleted { views } + | EdgeLeaf::IsSelfLoop { views } => views.push(op), + EdgeLeaf::Src(inner) | EdgeLeaf::Dst(inner) => inner.push_view(op), + } + } + + fn filter(expr: Expr) -> FilterExpr { + FilterExpr::Edge(expr) + } } impl Leaf for ExplodedEdgeLeaf { @@ -274,13 +390,54 @@ impl Leaf for ExplodedEdgeLeaf { } fn has_view(&self) -> bool { + !self.views().is_empty() + } + + fn property(views: Vec, name: String, temporal: bool) -> Self { + ExplodedEdgeLeaf::Property { + views, + name, + temporal, + } + } + + fn metadata(views: Vec, name: String) -> Self { + ExplodedEdgeLeaf::Metadata { views, name } + } + + fn is_active(views: Vec) -> Self { + ExplodedEdgeLeaf::IsActive { views } + } + + fn push_view(&mut self, op: ViewOp) { + self.views_mut().push(op); + } + + fn filter(expr: Expr) -> FilterExpr { + FilterExpr::ExplodedEdge(expr) + } +} + +impl ExplodedEdgeLeaf { + fn views(&self) -> &[ViewOp] { + match self { + ExplodedEdgeLeaf::Property { views, .. } + | ExplodedEdgeLeaf::Metadata { views, .. } + | ExplodedEdgeLeaf::IsActive { views } + | ExplodedEdgeLeaf::IsValid { views } + | ExplodedEdgeLeaf::IsDeleted { views } + | ExplodedEdgeLeaf::IsSelfLoop { views } => views, + } + } + + fn views_mut(&mut self) -> &mut Vec { match self { ExplodedEdgeLeaf::Property { views, .. } | ExplodedEdgeLeaf::Metadata { views, .. } | ExplodedEdgeLeaf::IsActive { views } | ExplodedEdgeLeaf::IsValid { views } | ExplodedEdgeLeaf::IsDeleted { views } - | ExplodedEdgeLeaf::IsSelfLoop { views } => !views.is_empty(), + | ExplodedEdgeLeaf::IsSelfLoop { views } => views, } } } @@ -500,12 +657,17 @@ fn qualified_type(inner: &PropType) -> Result { } fn require_bool(pt: &PropType, what: &str) -> Result<(), GraphError> { - if *pt == PropType::Bool { - Ok(()) - } else { - Err(invalid(format!( - "{what} needs a yes/no answer, but this expression has type {pt}" - ))) + match pt { + PropType::Bool => Ok(()), + PropType::List(inner) if matches!(**inner, PropType::Bool | PropType::List(_)) => { + Err(invalid(format!( + "{what} needs a yes/no answer, but this comparison gives one answer per \ + element ({pt}); add any() or all() to say which elements must match" + ))) + } + other => Err(invalid(format!( + "{what} needs a yes/no answer, but this expression has type {other}" + ))), } } diff --git a/raphtory/src/db/graph/views/filter/model/expr/convert.rs b/raphtory/src/db/graph/views/filter/model/expr/convert.rs new file mode 100644 index 0000000000..d287a265c3 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/expr/convert.rs @@ -0,0 +1,586 @@ +//! From the typed expression API to the tree. +//! +//! The typed builders (`NodeFilter.property("score").gt(4)`) stay the way rust +//! callers write a filter; here they turn into the same data a python object +//! or a GraphQL request carries, and compile through the one compiler in +//! [`super::compile`]. A factory chain names its entity and its views +//! ([`FactoryLeaf`]); every typed expression converts to an [`Expr`] +//! ([`ToExpr`]); a typed predicate converts to a [`FilterExpr`] +//! ([`ToFilterExpr`]) and gets its `CreateFilter` from that. + +use super::{ + Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, FilterExpr, Leaf, NodeLeaf, StrOp, ViewOp, +}; +use crate::{ + db::{ + api::{ + state::{ + ops::node::{Id, Name, Type}, + NodeOp, + }, + view::internal::{DynGraphArc, GraphView}, + }, + graph::views::filter::{ + model::{ + edge_filter::{EdgeEndpointWrapper, EdgeFilter, Endpoint}, + exploded_edge_filter::ExplodedEdgeFilter, + filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, + 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::{ + AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, ConstExpr, DegreeExpr, EntityExpr, + FirstExpr, LastExpr, LenExpr, Marker, MaxExpr, MinExpr, PropValueSetExpr, + StringExpr, SumExpr, TemporalPropExpr, UnaryExpr, + }, + node_filter::NodeFilter, + snapshot_filter::{SnapshotAt, SnapshotLatest}, + windowed_filter::Windowed, + MetadataExpr, PropertyExpr, + }, + CreateFilter, + }, + }, + errors::GraphError, + prelude::Layer, +}; +use raphtory_api::core::entities::{ + properties::prop::{IntoProp, Prop}, + GID, +}; +use std::sync::Arc; + +// ── factories ──────────────────────────────────────────────────────────────── + +/// A factory chain: the entity it reads from and the views on the way, +/// outermost wrapper last. +pub trait FactoryLeaf: Clone { + type Leaf: Leaf; + + fn views(&self) -> Vec; +} + +impl FactoryLeaf for NodeFilter { + type Leaf = NodeLeaf; + + fn views(&self) -> Vec { + Vec::new() + } +} + +impl FactoryLeaf for EdgeFilter { + type Leaf = EdgeLeaf; + + fn views(&self) -> Vec { + Vec::new() + } +} + +impl FactoryLeaf for ExplodedEdgeFilter { + type Leaf = ExplodedEdgeLeaf; + + fn views(&self) -> Vec { + Vec::new() + } +} + +impl FactoryLeaf for Windowed { + type Leaf = T::Leaf; + + fn views(&self) -> Vec { + let mut views = self.inner.views(); + views.push(ViewOp::Window { + start: self.start, + end: self.end, + }); + views + } +} + +impl FactoryLeaf for Layered { + type Leaf = T::Leaf; + + fn views(&self) -> Vec { + let mut views = self.inner.views(); + views.extend(layer_view(&self.layer)); + views + } +} + +impl FactoryLeaf for Latest { + type Leaf = T::Leaf; + + fn views(&self) -> Vec { + let mut views = self.inner.views(); + views.push(ViewOp::Latest); + views + } +} + +impl FactoryLeaf for SnapshotAt { + type Leaf = T::Leaf; + + fn views(&self) -> Vec { + let mut views = self.inner.views(); + views.push(ViewOp::SnapshotAt(self.time)); + views + } +} + +impl FactoryLeaf for SnapshotLatest { + type Leaf = T::Leaf; + + fn views(&self) -> Vec { + let mut views = self.inner.views(); + views.push(ViewOp::SnapshotLatest); + views + } +} + +/// The view op a layer restriction is; `Layer::All` restricts nothing. +fn layer_view(layer: &Layer) -> Option { + let names = match layer { + Layer::All => return None, + Layer::None => Vec::new(), + Layer::Default => vec!["_default".to_string()], + Layer::One(name) => vec![name.to_string()], + Layer::Multiple(names) => names.iter().map(ToString::to_string).collect(), + }; + Some(ViewOp::Layers(names)) +} + +/// The leaf type a predicate's entity marker stands for. +pub trait MarkerLeaf: Marker { + type Leaf: Leaf; +} + +impl MarkerLeaf for NodeFilter { + type Leaf = NodeLeaf; +} + +impl MarkerLeaf for EdgeFilter { + type Leaf = EdgeLeaf; +} + +impl MarkerLeaf for ExplodedEdgeFilter { + type Leaf = ExplodedEdgeLeaf; +} + +// ── values ─────────────────────────────────────────────────────────────────── + +/// A typed expression as tree data over the leaves of one entity. +pub trait ToExpr { + fn to_expr(&self) -> Expr; +} + +macro_rules! const_to_expr { + ($($t:ty),* $(,)?) => {$( + impl ToExpr for $t { + fn to_expr(&self) -> Expr { + Expr::Const(self.clone().into_prop()) + } + } + )*}; +} + +const_to_expr!( + Prop, + String, + &'static str, + bool, + u8, + u16, + u32, + u64, + i32, + i64, + f32, + f64 +); + +impl ToExpr for usize { + fn to_expr(&self) -> Expr { + Expr::Const(Prop::U64(*self as u64)) + } +} + +impl ToExpr for GID { + fn to_expr(&self) -> Expr { + Expr::Const(match self { + GID::U64(id) => Prop::U64(*id), + GID::Str(name) => Prop::str(name.clone()), + }) + } +} + +impl + Clone> ToExpr for ConstExpr { + fn to_expr(&self) -> Expr { + Expr::Const(self.0.clone().into()) + } +} + +impl ToExpr for PropertyExpr { + fn to_expr(&self) -> Expr { + Expr::Read(E::Leaf::property( + self.view_expr.views(), + self.name.clone(), + false, + )) + } +} + +impl ToExpr for TemporalPropExpr { + fn to_expr(&self) -> Expr { + Expr::Read(E::Leaf::property( + self.view_expr.views(), + self.name.clone(), + true, + )) + } +} + +impl ToExpr for MetadataExpr { + fn to_expr(&self) -> Expr { + Expr::Read(E::Leaf::metadata(self.view_expr.views(), self.name.clone())) + } +} + +impl> ToExpr for DegreeExpr { + fn to_expr(&self) -> Expr { + Expr::Read(NodeLeaf::Degree { + views: self.view_expr.views(), + direction: self.dir, + }) + } +} + +macro_rules! field_to_expr { + ($($t:ident => $field:ident),* $(,)?) => {$( + impl ToExpr for $t { + fn to_expr(&self) -> Expr { + Expr::Read(NodeLeaf::Field { + views: Vec::new(), + field: Field::$field, + }) + } + } + )*}; +} + +field_to_expr!(Id => Id, Name => Name, Type => NodeType); + +impl ToExpr for IsActiveNode { + fn to_expr(&self) -> Expr { + Expr::Read(NodeLeaf::is_active(Vec::new())) + } +} + +macro_rules! edge_structural_to_expr { + ($($t:ident => $variant:ident),* $(,)?) => {$( + impl ToExpr for $t { + fn to_expr(&self) -> Expr { + Expr::Read(EdgeLeaf::$variant { views: Vec::new() }) + } + } + + impl ToExpr for $t { + fn to_expr(&self) -> Expr { + Expr::Read(ExplodedEdgeLeaf::$variant { views: Vec::new() }) + } + } + )*}; +} + +edge_structural_to_expr!( + IsActiveEdge => IsActive, + IsValidEdge => IsValid, + IsDeletedEdge => IsDeleted, + IsSelfLoopEdge => IsSelfLoop, +); + +// A view wrapped around an expression scopes every read inside it. + +impl> ToExpr for Windowed { + fn to_expr(&self) -> Expr { + let mut expr = self.inner.to_expr(); + expr.push_view(ViewOp::Window { + start: self.start, + end: self.end, + }); + expr + } +} + +impl> ToExpr for Layered { + fn to_expr(&self) -> Expr { + let mut expr = self.inner.to_expr(); + if let Some(op) = layer_view(&self.layer) { + expr.push_view(op); + } + expr + } +} + +impl> ToExpr for Latest { + fn to_expr(&self) -> Expr { + let mut expr = self.inner.to_expr(); + expr.push_view(ViewOp::Latest); + expr + } +} + +impl> ToExpr for SnapshotAt { + fn to_expr(&self) -> Expr { + let mut expr = self.inner.to_expr(); + expr.push_view(ViewOp::SnapshotAt(self.time)); + expr + } +} + +impl> ToExpr for SnapshotLatest { + fn to_expr(&self) -> Expr { + let mut expr = self.inner.to_expr(); + expr.push_view(ViewOp::SnapshotLatest); + expr + } +} + +macro_rules! agg_to_expr { + ($($t:ident => $agg:ident),* $(,)?) => {$( + impl> ToExpr for $t { + fn to_expr(&self) -> Expr { + Expr::Agg(Agg::$agg, Box::new(self.0.to_expr())) + } + } + )*}; +} + +agg_to_expr!( + SumExpr => Sum, + AvgExpr => Avg, + MinExpr => Min, + MaxExpr => Max, + FirstExpr => First, + LastExpr => Last, + LenExpr => Len, +); + +impl> ToExpr for AnyExpr { + fn to_expr(&self) -> Expr { + Expr::Any(Box::new(self.0.to_expr())) + } +} + +impl> ToExpr for AllExpr { + fn to_expr(&self) -> Expr { + Expr::All(Box::new(self.0.to_expr())) + } +} + +impl, Rhs: ToExpr, M> ToExpr for BinaryCmpExpr { + fn to_expr(&self) -> Expr { + let op = match self.op { + BinaryOp::Eq => CmpOp::Eq, + BinaryOp::Ne => CmpOp::Ne, + BinaryOp::Lt => CmpOp::Lt, + BinaryOp::Le => CmpOp::Le, + BinaryOp::Gt => CmpOp::Gt, + BinaryOp::Ge => CmpOp::Ge, + }; + Expr::Cmp( + op, + Box::new(self.left.to_expr()), + Box::new(self.right.to_expr()), + ) + } +} + +impl, Rhs: ToExpr, M> ToExpr for StringExpr { + fn to_expr(&self) -> Expr { + let op = match &self.op { + StringOp::StartsWith => StrOp::StartsWith, + StringOp::EndsWith => StrOp::EndsWith, + StringOp::Contains => StrOp::Contains, + StringOp::NotContains => StrOp::NotContains, + StringOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => StrOp::FuzzySearch { + levenshtein_distance: *levenshtein_distance, + prefix_match: *prefix_match, + }, + }; + Expr::Str( + op, + Box::new(self.left.to_expr()), + Box::new(self.right.to_expr()), + ) + } +} + +impl, M> ToExpr for UnaryExpr { + fn to_expr(&self) -> Expr { + let inner = Box::new(self.expr.to_expr()); + match self.op { + UnaryOp::IsSome => Expr::IsSome(inner), + UnaryOp::IsNone => Expr::IsNone(inner), + } + } +} + +impl, M> ToExpr for PropValueSetExpr { + fn to_expr(&self) -> Expr { + Expr::In { + expr: Box::new(self.expr.to_expr()), + values: self.values.clone(), + negated: matches!(self.op, SetOp::IsNotIn), + } + } +} + +/// A node expression read at one end of the edge. +impl> ToExpr for EdgeEndpointWrapper { + fn to_expr(&self) -> Expr { + let inner = Box::new(self.inner.to_expr()); + Expr::Read(match self.endpoint() { + Endpoint::Src => EdgeLeaf::Src(inner), + Endpoint::Dst => EdgeLeaf::Dst(inner), + }) + } +} + +// ── filters ────────────────────────────────────────────────────────────────── + +/// A typed predicate as a filter tree. +pub trait ToFilterExpr { + fn to_filter_expr(&self) -> FilterExpr; +} + +impl ToFilterExpr for BinaryCmpExpr +where + Self: ToExpr, +{ + fn to_filter_expr(&self) -> FilterExpr { + M::Leaf::filter(self.to_expr()) + } +} + +impl ToFilterExpr for StringExpr +where + Self: ToExpr, +{ + fn to_filter_expr(&self) -> FilterExpr { + M::Leaf::filter(self.to_expr()) + } +} + +impl ToFilterExpr for UnaryExpr +where + Self: ToExpr, +{ + fn to_filter_expr(&self) -> FilterExpr { + M::Leaf::filter(self.to_expr()) + } +} + +impl ToFilterExpr for PropValueSetExpr +where + Self: ToExpr, +{ + fn to_filter_expr(&self) -> FilterExpr { + M::Leaf::filter(self.to_expr()) + } +} + +impl ToFilterExpr for AnyExpr +where + E::Marker: MarkerLeaf, + Self: ToExpr<::Leaf>, +{ + fn to_filter_expr(&self) -> FilterExpr { + ::Leaf::filter(self.to_expr()) + } +} + +impl ToFilterExpr for AllExpr +where + E::Marker: MarkerLeaf, + Self: ToExpr<::Leaf>, +{ + fn to_filter_expr(&self) -> FilterExpr { + ::Leaf::filter(self.to_expr()) + } +} + +impl ToFilterExpr for EdgeEndpointWrapper +where + Self: ToExpr, +{ + fn to_filter_expr(&self) -> FilterExpr { + EdgeLeaf::filter(self.to_expr()) + } +} + +/// A typed predicate is applied by converting it to its tree and compiling +/// that: one compiler, one set of checks, whatever built the filter. +macro_rules! compile_through_tree { + ($(impl<$($g:ident),*> for $ty:ty;)+) => {$( + impl<$($g),*> CreateFilter for $ty + where + Self: ToFilterExpr + Clone + Send + Sync + 'static, + { + type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + = DynGraphArc<'graph> + where + Self: 'graph; + + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = + Arc + 'graph>; + + type FilteredGraph<'graph, G> + = DynGraphArc<'graph> + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + self.to_filter_expr().compile()?.create_filter(graph, filtered) + } + + fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + self, + graph: G, + filtered: F, + ) -> Result, GraphError> { + self.to_filter_expr() + .compile()? + .create_node_filter(graph, filtered) + } + + fn filter_graph_view<'graph, G: GraphView + 'graph>( + &self, + graph: G, + ) -> Result, GraphError> { + self.to_filter_expr().compile()?.filter_graph_view(graph) + } + } + )+}; +} + +compile_through_tree! { + impl for BinaryCmpExpr; + impl for StringExpr; + impl for UnaryExpr; + impl for PropValueSetExpr; + impl for AnyExpr; + impl for AllExpr; + impl for EdgeEndpointWrapper; +} diff --git a/raphtory/src/db/graph/views/filter/model/expr/mod.rs b/raphtory/src/db/graph/views/filter/model/expr/mod.rs index 76f0a8fe3d..3cdd1e0db8 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/mod.rs @@ -22,12 +22,14 @@ //! graph, in [`compile`]. mod compile; +pub mod convert; mod display; #[cfg(test)] mod tests; pub use super::tree::{Agg, CmpOp, Field, OpaqueFilter, StrOp, ViewOp, OPAQUE_FILTER_ERROR}; pub use compile::Leaf; +pub use convert::{FactoryLeaf, MarkerLeaf, ToExpr, ToFilterExpr}; use raphtory_api::core::{entities::properties::prop::Prop, Direction}; use serde::{Deserialize, Serialize}; 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 c65ded9408..60b982fd34 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -348,15 +348,6 @@ pub enum StringOp { }, } -/// Element qualifiers collected from a leading `any()`/`all()` chain: the -/// comparison is applied per element and the qualifiers collapse the results, -/// innermost list level first. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ElemQual { - Any, - All, -} - /// Unary presence operators used by `UnaryExpr`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnaryOp { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 02376368db..7f12b6223d 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -6,7 +6,7 @@ pub use crate::{ filter::{ model::{ edge_filter::{EdgeEndpointWrapper, EdgeFilter}, - exploded_edge_filter::{ExplodedEdgeEndpointWrapper, ExplodedEdgeFilter}, + exploded_edge_filter::ExplodedEdgeFilter, filter_operator::{ BinaryOp, Comparable, FilterOperator, SetOp, StringComparable, StringOp, UnaryOp, @@ -312,8 +312,8 @@ impl EntityExpr for PropertyExpr { #[derive(Clone)] pub struct MetadataExpr { - view_expr: E, - name: String, + pub(crate) view_expr: E, + pub(crate) name: String, } impl EntityExpr for MetadataExpr { @@ -802,8 +802,8 @@ pub trait EdgeViewFilterOps: ViewWrapOps { /// Comparison, string, set, and presence operators on any [`CreateOp`]. /// -/// `.any()` / `.all()` are qualifiers on a list-valued expression: the comparison that follows -/// is applied to each element and the results are reduced, so `.any().gt(10i64)` holds when any +/// A comparison against a list-valued expression gives one answer per element; `.any()` / +/// `.all()` written after it collapse those answers, so `.gt(10i64).any()` holds when any /// element is greater than ten. /// /// ```rust,ignore @@ -1042,30 +1042,9 @@ pub fn validate_types_comparable(lhs_pt: &PropType, rhs_pt: &PropType) -> Result /// Reject aggregators called on a declared scalar expression. /// -/// Lists and unresolved (`PropType::Empty`) types pass through — unresolved -/// is the case where a property name hasn't been looked up yet at expression- -/// build time, so we defer to filter-build / runtime to catch scalar/list -/// mismatches there. Anything declaring a scalar type up front (e.g. -/// `IsActiveNode` → `Bool`, `DegreeExpr` → `U64`) is rejected. -/// The element type a leading `any()`/`all()` chain compares against: one -/// list level is stripped per qualifier. Unknown types stay unknown; a -/// qualifier over a known scalar is an error. -pub fn elem_prop_type(pt: &PropType, levels: usize) -> Result { - let mut pt = pt.clone(); - for _ in 0..levels { - pt = match pt { - PropType::List(inner) => *inner, - PropType::Empty => PropType::Empty, - other => { - return Err(GraphError::InvalidFilter(format!( - "any()/all() require list or temporal values, found {other}" - ))) - } - }; - } - Ok(pt) -} - +/// Lists and unresolved (`PropType::Empty`) types pass through; anything +/// declaring a scalar type up front (`IsActiveNode` → `Bool`, `DegreeExpr` → +/// `U64`) is rejected. pub fn require_aggregable(pt: &PropType, op: &str) -> Result<(), GraphError> { match pt { PropType::List(_) | PropType::Empty => Ok(()), diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 76afb382c6..3b54ddce49 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -11,10 +11,9 @@ use crate::{ graph::views::filter::model::{ edge_expr::EdgeOp, edge_filter::EdgeEndpointWrapper, - filter_operator::ElemQual, node_expr::{ AvgExpr, CreateOp, EntityAggOps, EntityExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, - MinExpr, PredicateLhs, SumExpr, + MinExpr, SumExpr, }, CreateView, EntityMarker, PropertyExpr, }, @@ -87,16 +86,6 @@ pub trait DynCreateOp: DynEntityExpr { &self, graph: Arc, ) -> Result> + 'g>, GraphError>; - - fn dyn_create_qualified_node_op<'g>( - &self, - graph: Arc, - ) -> Result<(Arc> + 'g>, Vec), GraphError>; - - fn dyn_create_qualified_edge_op<'g>( - &self, - graph: Arc, - ) -> Result<(Arc> + 'g>, Vec), GraphError>; } impl DynCreateOp for E { @@ -117,20 +106,6 @@ impl DynCreateOp for E { ) -> Result> + 'g>, GraphError> { self.create_edge_op(graph) } - - fn dyn_create_qualified_node_op<'g>( - &self, - graph: Arc, - ) -> Result<(Arc> + 'g>, Vec), GraphError> { - self.create_qualified_node_op(graph) - } - - fn dyn_create_qualified_edge_op<'g>( - &self, - graph: Arc, - ) -> Result<(Arc> + 'g>, Vec), GraphError> { - self.create_qualified_edge_op(graph) - } } impl EntityExpr for Arc { @@ -149,8 +124,6 @@ impl EntityExpr for Arc { } } -impl PredicateLhs for Arc {} - impl CreateOp for Arc { fn selects_node_id(&self) -> bool { self.as_ref().dyn_selects_node_id() @@ -169,20 +142,6 @@ impl CreateOp for Arc { ) -> Result> + 'g>, GraphError> { self.deref().dyn_create_edge_op(Arc::new(graph)) } - - fn create_qualified_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result<(Arc> + 'g>, Vec), GraphError> { - self.deref().dyn_create_qualified_node_op(Arc::new(graph)) - } - - fn create_qualified_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result<(Arc> + 'g>, Vec), GraphError> { - self.deref().dyn_create_qualified_edge_op(Arc::new(graph)) - } } impl EntityAggOps for Arc { 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 f2d23a518c..1e89278dc0 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 @@ -66,8 +66,8 @@ use super::{ AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, SumNodeOp, TemporalNodePropOp, WithPropType, }, - AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, CreateOp, EntityExpr, FirstEdgeOp, - LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, PredicateLhs, SumEdgeOp, + AvgEdgeOp, CreateOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, + PredicateLhs, SumEdgeOp, }; use crate::{ db::{ @@ -77,10 +77,9 @@ use crate::{ }, graph::views::filter::model::{ edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, - elem_prop_type, - filter_operator::{Comparable, ElemQual}, + filter_operator::Comparable, node_filter::NodeFilter, - require_aggregable, resolved_prop_type, CreateView, EntityMarker, + require_aggregable, resolved_prop_type, ComposableFilter, CreateView, EntityMarker, }, }, errors::GraphError, @@ -609,66 +608,37 @@ pub trait EntityAggOps: EntityExpr + Sized { } macro_rules! impl_agg_expr { - ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident, $name:literal, $qual:expr) => { - impl_agg_expr!(@common $expr, $node_op_ty, $edge_op_ty); - - impl CreateOp for $expr { - impl_agg_expr!(@create $node_op_ty, $edge_op_ty, $name); - - fn create_qualified_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result<(Arc> + 'g>, Vec), GraphError> - { - let (inner, mut quals) = self.0.create_qualified_node_op(graph)?; - quals.push($qual); - Ok((inner, quals)) - } - - fn create_qualified_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result<(Arc> + 'g>, Vec), GraphError> - { - let (inner, mut quals) = self.0.create_qualified_edge_op(graph)?; - quals.push($qual); - Ok((inner, quals)) - } - } - }; ($expr:ident, $node_op_ty:ident, $edge_op_ty:ident, $name:literal) => { - impl_agg_expr!(@common $expr, $node_op_ty, $edge_op_ty); + impl_agg_expr!(@common $expr); impl CreateOp for $expr { - impl_agg_expr!(@create $node_op_ty, $edge_op_ty, $name); - - // Leading qualifiers float through aggregates: the aggregate - // applies per element (aggregate_list_values recurses into - // nested lists) and the qualifiers collapse afterwards. - fn create_qualified_node_op<'g, G: GraphView + 'g>( + fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, - ) -> Result<(Arc> + 'g>, Vec), GraphError> - { - let (inner, quals) = self.0.create_qualified_node_op(graph)?; + ) -> Result> + 'g>, GraphError> { + let inner = self.0.create_node_op(graph)?; let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&elem_prop_type(&pt, quals.len())?, $name)?; - Ok((Arc::new($node_op_ty { inner }), quals)) + require_aggregable(&pt, $name)?; + Ok(Arc::new($node_op_ty { inner })) } - fn create_qualified_edge_op<'g, G: GraphView + 'g>( + fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, - ) -> Result<(Arc> + 'g>, Vec), GraphError> - { - let (inner, quals) = self.0.create_qualified_edge_op(graph)?; + ) -> Result> + 'g>, GraphError> { + let inner = self.0.create_edge_op(graph)?; let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&elem_prop_type(&pt, quals.len())?, $name)?; - Ok((Arc::new($edge_op_ty { inner }), quals)) + require_aggregable(&pt, $name)?; + Ok(Arc::new($edge_op_ty { inner })) } } }; - (@common $expr:ident, $node_op_ty:ident, $edge_op_ty:ident) => { + ($expr:ident) => { + impl_agg_expr!(@common $expr); + + impl ComposableFilter for $expr {} + }; + (@common $expr:ident) => { #[derive(Clone)] pub struct $expr(pub E); @@ -704,28 +674,6 @@ macro_rules! impl_agg_expr { LenExpr(self) } } - - }; - (@create $node_op_ty:ident, $edge_op_ty:ident, $name:literal) => { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.0.create_node_op(graph)?; - let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&pt, $name)?; - Ok(Arc::new($node_op_ty { inner })) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.0.create_edge_op(graph)?; - let pt = resolved_prop_type(self.0.prop_type(), inner.prop_type()); - require_aggregable(&pt, $name)?; - Ok(Arc::new($edge_op_ty { inner })) - } }; } @@ -736,5 +684,6 @@ impl_agg_expr!(MaxExpr, MaxNodeOp, MaxEdgeOp, "max()"); impl_agg_expr!(FirstExpr, FirstNodeOp, FirstEdgeOp, "first()"); impl_agg_expr!(LastExpr, LastNodeOp, LastEdgeOp, "last()"); impl_agg_expr!(LenExpr, LenNodeOp, LenEdgeOp, "len()"); -impl_agg_expr!(AnyExpr, AnyNodeOp, AnyEdgeOp, "any()", ElemQual::Any); -impl_agg_expr!(AllExpr, AllNodeOp, AllEdgeOp, "all()", ElemQual::All); +// `any()` / `all()` after a comparison: they collapse an element-wise result. +impl_agg_expr!(AnyExpr); +impl_agg_expr!(AllExpr); 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 dcb852c8df..022e65bede 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 @@ -1,106 +1,22 @@ -//! 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) -//! ──► BinaryCmpExpr { left: Property("age"), op: Gt, right: 30i64 } -//! -//! Phase 2 — Compile (bind to graph, resolve names): -//! BinaryCmpExpr::create_node_filter(graph)? -//! ──► Arc> -//! = BinaryCmpNodeOp { left: NodePropOp(id=3), right: Const(Some(I64(30))), op: Gt } -//! -//! Phase 3 — Runtime (per-node, O(1)): -//! filter.apply(storage, vid) → age_value = NodePropOp.apply(...) -//! Prop::binary_cmp(Gt, age_value, Some(I64(30))) → true/false -//! ``` -//! -//! # Temporal quantification -//! -//! Filter types also implement `NodeExpr` (producing list-aware ops), enabling chaining -//! before `.any()`/`.all()`: -//! -//! ```rust,ignore -//! // "pass if any temporal value of 'score' > 10" -//! NodeFilter.property("score").temporal().gt(10i64).any() -//! ──► BinaryCmpExpr>, Prop> -//! create_node_filter(graph)? -//! ──► BinaryCmpNodeOp { left: AnyNodeOp { inner: ListAwareCmpNodeOp { TemporalNodePropOp, -//! Const(I64(10)), Gt } }, -//! right: Const(Bool(true)), op: Eq } -//! -//! // "pass if sum of 'score' > 100" -//! NodeFilter.property("score").temporal().sum().gt(100i64) -//! ──► BinaryCmpExpr, i64> -//! ``` - -use super::{ - ops::{ - gid_for_id_lookup, AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, IdDomainNodeOp, - ListAwareCmpNodeOp, ListAwareSetNodeOp, ListAwareStringNodeOp, ListAwareUnaryNodeOp, - PropValueSetNodeOp, StringNodeOp, UnaryNodeOp, - }, - CreateOp, EntityExpr, Marker, PredicateLhs, -}; -use crate::{ - db::{ - api::{ - state::ops::NodeOp, - view::{internal::GraphView, BoxableGraphView}, - }, - graph::views::filter::{ - model::{ - comparable_set_values, - edge_expr::{ - ops::{ - ListAwareCmpEdgeOp, ListAwareSetEdgeOp, ListAwareStringEdgeOp, - ListAwareUnaryEdgeOp, - }, - EdgeOp, - }, - elem_prop_type, - filter_operator::{BinaryOp, ElemQual, SetOp, StringOp, UnaryOp}, - resolved_prop_type, validate_binary_op, validate_const_comparable, - validate_string_op, validate_types_comparable, ComposableFilter, CreateFilter, - EntityMarker, ExplodedEdgeFilter, - }, - node_filtered_graph::NodeFilteredGraph, - }, - }, - errors::GraphError, - prelude::{EdgeFilter, NodeFilter}, -}; -use raphtory_api::core::entities::{ - properties::prop::{Prop, PropType}, - GID, +//! The predicate forms of the typed expression API: a comparison, a string +//! test, a presence test and a set membership test. They are builders: each +//! converts to the filter expression tree (see `model::expr::convert`) and +//! compiles through it, so what a rust caller writes and what a python object +//! or GraphQL request carries run through the same compiler. + +use super::{EntityExpr, Marker, PredicateLhs}; +use crate::db::graph::views::filter::model::{ + filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, + ComposableFilter, }; -use std::sync::Arc; -// ───────────────────────────────────────────────────────────────────────────── -// BinaryCmpExpr — binary expression filter -// ───────────────────────────────────────────────────────────────────────────── +use raphtory_api::core::entities::properties::prop::Prop; -/// A node filter that compares two [`CreateOp`] values using a [`BinaryOp`]. -/// -/// Both sides produce `Option` at runtime. Created by [`EntityExprFilterOps`] methods -/// (`.gt`, `.lt`, `.eq`, `.ne`, `.ge`, `.le`). -/// -/// As a **terminal filter** (`CreateFilter`): compiles to `BinaryCmpNodeOp` → bool. -/// As a **mid-chain expression** (`NodeExpr`): compiles to `ListAwareCmpNodeOp` → `Option`. +/// Two values compared with a [`BinaryOp`]; either side may be a constant. /// /// ```rust,ignore /// NodeFilter.degree().gt(2usize) -/// → BinaryCmpExpr, usize> -/// → BinaryCmpNodeOp { left: Degree(..).map(Prop::U64), right: Const(Some(U64(2))), op: Gt } -/// /// NodeFilter.property("age").eq(30i64) -/// → BinaryCmpExpr -/// → BinaryCmpNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(I64(30))), op: Eq } +/// NodeFilter.out_degree().gt(NodeFilter.in_degree()) /// ``` #[derive(Clone)] pub struct BinaryCmpExpr { @@ -110,24 +26,6 @@ pub struct BinaryCmpExpr { pub entity: Entity, } -/// Collapse elementwise boolean results per the collected qualifiers, -/// innermost list level first, and adapt to a boolean node filter. -fn qualify_node_filter<'g>( - elemwise: Arc> + 'g>, - quals: &[ElemQual], -) -> Arc + 'g> { - let mut op = elemwise; - // Qualifiers are collected in call order (outermost list level first); - // wrapping starts at the innermost level, so iterate in reverse. - for q in quals.iter().rev() { - op = match q { - ElemQual::Any => Arc::new(AnyNodeOp { inner: op }), - ElemQual::All => Arc::new(AllNodeOp { inner: op }), - }; - } - Arc::new(op.map(|v| matches!(v, Some(Prop::Bool(true))))) -} - impl BinaryCmpExpr { pub fn new(left: L, op: BinaryOp, right: R, entity: E) -> Self { Self { @@ -137,15 +35,6 @@ impl BinaryCmpExpr { entity, } } - - fn with_entity(self, entity: T) -> BinaryCmpExpr { - BinaryCmpExpr { - left: self.left, - op: self.op, - right: self.right, - entity, - } - } } impl ComposableFilter for BinaryCmpExpr {} @@ -154,197 +43,17 @@ impl PredicateLhs for BinaryCmpExpr EntityExpr for BinaryCmpExpr { type Marker = E; + fn entity(&self) -> Self::Marker { self.entity } - fn prop_type(&self) -> PropType { - // TODO: depending on the types of left and right, we should figure out the type to return here - PropType::Empty - } -} -impl CreateOp for BinaryCmpExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; - Ok(Arc::new(ListAwareCmpNodeOp { - left, - right, - op: self.op, - })) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(ListAwareCmpEdgeOp { - left, - right, - op: self.op, - })) + fn nullable(&self) -> bool { + false } } -impl CreateFilter for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph>; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone(), filtered)?; - Ok(NodeFilteredGraph::new(graph, filter)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - let expr_pt = self.left.prop_type(); - let id_type = filtered.id_type(); - let (left, quals) = self.left.create_qualified_node_op(filtered.clone())?; - let right = self.right.create_node_op(filtered)?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, left.prop_type()), quals.len())?; - let rhs_pt = resolved_prop_type(self.right.prop_type(), right.prop_type()); - validate_binary_op(&self.op, &lhs_pt)?; - match right.const_value() { - Some(c) => validate_const_comparable(&lhs_pt, c.as_ref())?, - None => validate_types_comparable(&lhs_pt, &rhs_pt)?, - } - if quals.is_empty() { - let cmp: Arc + 'graph> = Arc::new(BinaryCmpNodeOp { - left, - right: right.clone(), - op: self.op, - }); - if self.op == BinaryOp::Eq && self.left.selects_node_id() { - if let Some(Some(value)) = right.const_value() { - if let Some(gid) = gid_for_id_lookup(id_type, &value) { - return Ok(Arc::new(IdDomainNodeOp { - gids: Arc::from([gid]), - inner: cmp, - })); - } - } - } - Ok(cmp) - } else { - let elemwise = Arc::new(ListAwareCmpNodeOp { - left, - right, - op: self.op, - }); - Ok(qualify_node_filter(elemwise, &quals)) - } - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -impl CreateFilter for BinaryCmpExpr -where - L: CreateOp, - R: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(match self.entity { - EntityMarker::Node => Arc::new( - self.with_entity(NodeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::Edge => Arc::new( - self.with_entity(EdgeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::ExplodedEdge => Arc::new( - self.with_entity(ExplodedEdgeFilter) - .create_filter(graph, filtered)?, - ), - - EntityMarker::Const => Err(GraphError::NotSupported)?, - }) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - match self.entity { - EntityMarker::Node => Ok(self - .with_entity(NodeFilter) - .create_node_filter(graph, filtered)?), - EntityMarker::Edge => Err(GraphError::NotNodeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - EntityMarker::Const => Err(GraphError::NotSupported)?, - } - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// UnaryExpr — 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::() -/// → UnaryExpr -/// → UnaryNodeOp { inner: NodePropOp(prop_id=N), op: IsSome } -/// ``` +/// A presence test: `is_some()` / `is_none()`. #[derive(Clone)] pub struct UnaryExpr { pub expr: E, @@ -352,179 +61,23 @@ pub struct UnaryExpr { pub entity: Entity, } -impl UnaryExpr { - fn with_entity(self, entity: T) -> UnaryExpr { - UnaryExpr { - expr: self.expr, - op: self.op, - entity, - } - } -} - impl ComposableFilter for UnaryExpr {} impl PredicateLhs for UnaryExpr {} impl EntityExpr for UnaryExpr { type Marker = M; + fn entity(&self) -> Self::Marker { self.entity } -} - -impl CreateOp for UnaryExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_node_op(graph)?; - Ok(Arc::new(ListAwareUnaryNodeOp { inner, op: self.op })) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_edge_op(graph)?; - Ok(Arc::new(ListAwareUnaryEdgeOp { inner, op: self.op })) - } -} - -impl CreateFilter for UnaryExpr -where - E: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph>; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone(), filtered)?; - Ok(NodeFilteredGraph::new(graph, filter)) - } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - if !self.expr.nullable() { - return Err(GraphError::InvalidFilter(format!( - "operator {:?} is not valid for non-nullable expression", - self.op - ))); - } - let (inner, quals) = self.expr.create_qualified_node_op(filtered)?; - if quals.is_empty() { - Ok(Arc::new(UnaryNodeOp { inner, op: self.op })) - } else { - let elemwise = Arc::new(ListAwareUnaryNodeOp { inner, op: self.op }); - Ok(qualify_node_filter(elemwise, &quals)) - } - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) + fn nullable(&self) -> bool { + false } } -impl CreateFilter for UnaryExpr -where - E: CreateOp, -{ - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(match self.entity { - EntityMarker::Node => Arc::new( - self.with_entity(NodeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::Edge => Arc::new( - self.with_entity(EdgeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::ExplodedEdge => Arc::new( - self.with_entity(ExplodedEdgeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::Const => Err(GraphError::NotSupported)?, - }) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - match self.entity { - EntityMarker::Node => Ok(self - .with_entity(NodeFilter) - .create_node_filter(graph, filtered)?), - EntityMarker::Edge => Err(GraphError::NotNodeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - EntityMarker::Const => Err(GraphError::NotSupported)?, - } - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// StringExpr — string expression filter -// ───────────────────────────────────────────────────────────────────────────── - -/// A node filter that applies a [`StringOp`] to two [`CreateOp`] values. -/// -/// Both sides must produce the same string-comparable type (`L::Output: StringComparable`). -/// Created by the string methods on [`EntityExprFilterOps`] (`.starts_with`, `.ends_with`, -/// `.contains`, `.not_contains`, `.fuzzy_search`). -/// Compiles to a `StringNodeOp` wrapped in `Arc>`. -/// -/// ```rust,ignore -/// NodeFilter.name().starts_with("Al") -/// → StringExpr -/// → StringNodeOp { left: Name.map(...), right: Const(Some(Str("Al"))), op: StartsWith } -/// -/// NodeFilter.property("tag").contains(Prop::Str("foo".into())) -/// → StringExpr -/// → StringNodeOp { left: NodePropOp(prop_id=N), right: Const(Some(Str("foo"))), op: Contains } -/// ``` +/// A string test with a [`StringOp`]: `starts_with`, `contains`, `fuzzy_search`, … #[derive(Clone)] pub struct StringExpr { pub left: L, @@ -542,15 +95,6 @@ impl StringExpr { entity, } } - - fn with_entity(self, entity: T) -> StringExpr { - StringExpr { - left: self.left, - op: self.op, - right: self.right, - entity, - } - } } impl ComposableFilter for StringExpr {} @@ -559,170 +103,17 @@ impl PredicateLhs for StringExpr EntityExpr for StringExpr { type Marker = M; + fn entity(&self) -> Self::Marker { self.entity } - fn prop_type(&self) -> PropType { - PropType::Empty - } -} - -impl CreateOp for StringExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_node_op(graph.clone())?; - let right = self.right.create_node_op(graph)?; - Ok(Arc::new(ListAwareStringNodeOp { - left, - right, - op: self.op, - })) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let left = self.left.create_edge_op(graph.clone())?; - let right = self.right.create_edge_op(graph)?; - Ok(Arc::new(ListAwareStringEdgeOp { - left, - right, - op: self.op, - })) - } -} - -impl CreateFilter for StringExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph>; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone(), filtered)?; - Ok(NodeFilteredGraph::new(graph, filter)) - } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - let expr_pt = self.left.prop_type(); - let (left, quals) = self.left.create_qualified_node_op(filtered.clone())?; - let right = self.right.create_node_op(filtered)?; - validate_string_op(&elem_prop_type( - &resolved_prop_type(expr_pt, left.prop_type()), - quals.len(), - )?)?; - match right.const_value() { - Some(c) => validate_const_comparable(&PropType::Str, c.as_ref())?, - None => {} - } - if quals.is_empty() { - Ok(Arc::new(StringNodeOp { - left, - right, - op: self.op, - })) - } else { - let elemwise = Arc::new(ListAwareStringNodeOp { - left, - right, - op: self.op, - }); - Ok(qualify_node_filter(elemwise, &quals)) - } - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) + fn nullable(&self) -> bool { + false } } -impl CreateFilter for StringExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(match self.entity { - EntityMarker::Node => Arc::new( - self.with_entity(NodeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::Edge => Arc::new( - self.with_entity(EdgeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::ExplodedEdge => Arc::new( - self.with_entity(ExplodedEdgeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::Const => Err(GraphError::NotSupported)?, - }) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - match self.entity { - EntityMarker::Node => Ok(self - .with_entity(NodeFilter) - .create_node_filter(graph, filtered)?), - EntityMarker::Edge => Err(GraphError::NotNodeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - EntityMarker::Const => Err(GraphError::NotSupported)?, - } - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// PropValueSetExpr — is_in / is_not_in for aggregated Option values -// ───────────────────────────────────────────────────────────────────────────── - -/// A filter that checks whether a scalar property value is in (or not in) a fixed set. -/// -/// Uses linear scan because `Prop` may contain floats that don't implement `Hash`. -/// Works for both nodes (`Entity = NodeFilter`) and edges (`Entity = EdgeFilter`). +/// A membership test against a fixed set of values: `is_in` / `is_not_in`. #[derive(Clone)] pub struct PropValueSetExpr { pub(crate) expr: E, @@ -731,292 +122,18 @@ pub struct PropValueSetExpr { pub(crate) entity: Entity, } -impl PropValueSetExpr { - fn with_entity(self, entity: T) -> PropValueSetExpr { - PropValueSetExpr { - expr: self.expr, - values: self.values, - op: self.op, - entity, - } - } -} - impl ComposableFilter for PropValueSetExpr {} impl PredicateLhs for PropValueSetExpr {} impl EntityExpr for PropValueSetExpr { type Marker = M; + fn entity(&self) -> Self::Marker { self.entity } - fn prop_type(&self) -> PropType { - PropType::Empty - } -} - -impl CreateOp for PropValueSetExpr { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_node_op(graph)?; - Ok(Arc::new(ListAwareSetNodeOp { - inner, - values: self.values.clone(), - op: self.op, - })) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - let inner = self.expr.create_edge_op(graph)?; - Ok(Arc::new(ListAwareSetEdgeOp { - inner, - values: self.values.clone(), - op: self.op, - })) - } -} - -impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - NodeFilteredGraph>; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let filter = self.create_node_filter(graph.clone(), filtered)?; - Ok(NodeFilteredGraph::new(graph, filter)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - let expr_pt = self.expr.prop_type(); - let id_type = filtered.id_type(); - let (inner, quals) = self.expr.create_qualified_node_op(filtered)?; - let lhs_pt = elem_prop_type(&resolved_prop_type(expr_pt, inner.prop_type()), quals.len())?; - let values = comparable_set_values(&lhs_pt, self.values); - if quals.is_empty() { - let gids: Option> = (self.op == SetOp::IsIn && self.expr.selects_node_id()) - .then(|| { - values - .iter() - .map(|v| gid_for_id_lookup(id_type, v)) - .collect() - }) - .flatten(); - let set_op: Arc + 'graph> = Arc::new(PropValueSetNodeOp { - inner, - values, - op: self.op, - }); - match gids { - Some(gids) => Ok(Arc::new(IdDomainNodeOp { - gids: gids.into(), - inner: set_op, - })), - None => Ok(set_op), - } - } else { - let elemwise = Arc::new(ListAwareSetNodeOp { - inner, - values, - op: self.op, - }); - Ok(qualify_node_filter(elemwise, &quals)) - } - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - -impl CreateFilter for PropValueSetExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(match self.entity { - EntityMarker::Node => Arc::new( - self.with_entity(NodeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::Edge => Arc::new( - self.with_entity(EdgeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::ExplodedEdge => Arc::new( - self.with_entity(ExplodedEdgeFilter) - .create_filter(graph, filtered)?, - ), - EntityMarker::Const => Err(GraphError::NotSupported)?, - }) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - match self.entity { - EntityMarker::Node => Ok(self - .with_entity(NodeFilter) - .create_node_filter(graph, filtered)?), - EntityMarker::Edge => Err(GraphError::NotNodeFilter), - EntityMarker::ExplodedEdge => Err(GraphError::NotNodeFilter), - EntityMarker::Const => Err(GraphError::NotSupported)?, - } - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) + fn nullable(&self) -> bool { + false } } - -use crate::db::graph::views::filter::{ - edge_expr_filtered_graph::EdgeExprFilteredGraph, - exploded_edge_expr_filtered_graph::ExplodedEdgeExprFilteredGraph, - model::{ - edge_expr::filters::qualify_edge_filter, - node_expr::exprs::{AllExpr, AnyExpr}, - }, -}; - -/// A bare leading qualifier used directly as a filter keeps its historical -/// meaning: each element compares equal to `true` and the qualifier chain -/// collapses the results. One impl serves every entity, dispatching on the -/// runtime marker like the other expression filters with erased entities. -macro_rules! impl_qualifier_filter { - ($($ty:ident),+ $(,)?) => {$( - impl CreateFilter for $ty - where - E::Marker: Into, - { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - match self.0.entity().into() { - EntityMarker::Node => { - let filter = self.create_node_filter(graph.clone(), filtered)?; - Ok(Arc::new(NodeFilteredGraph::new(graph, filter))) - } - EntityMarker::Edge => { - let (left, mut quals) = self.create_qualified_edge_op(filtered.clone())?; - quals.reverse(); - elem_prop_type(&left.prop_type(), quals.len())?; - let right = Prop::Bool(true).create_edge_op(filtered)?; - let elemwise = Arc::new(ListAwareCmpEdgeOp { - left, - right, - op: BinaryOp::Eq, - }); - Ok(Arc::new(EdgeExprFilteredGraph::new( - graph, - qualify_edge_filter(elemwise, &quals), - ))) - } - EntityMarker::ExplodedEdge => { - let (left, mut quals) = self.create_qualified_edge_op(filtered.clone())?; - quals.reverse(); - elem_prop_type(&left.prop_type(), quals.len())?; - let right = Prop::Bool(true).create_edge_op(filtered)?; - let elemwise = Arc::new(ListAwareCmpEdgeOp { - left, - right, - op: BinaryOp::Eq, - }); - Ok(Arc::new(ExplodedEdgeExprFilteredGraph::new( - graph, - qualify_edge_filter(elemwise, &quals), - ))) - } - EntityMarker::Const => Err(GraphError::NotSupported), - } - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - if !matches!(self.0.entity().into(), EntityMarker::Node) { - return Err(GraphError::NotNodeFilter); - } - let (left, mut quals) = self.create_qualified_node_op(filtered.clone())?; - // Trailing qualifiers collect innermost level first; the - // collapse helper wraps innermost first after reversing, so - // reverse here to cancel it. - quals.reverse(); - elem_prop_type(&left.prop_type(), quals.len())?; - let right = Prop::Bool(true).create_node_op(filtered)?; - let elemwise = Arc::new(ListAwareCmpNodeOp { - left, - right, - op: BinaryOp::Eq, - }); - Ok(qualify_node_filter(elemwise, &quals)) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } - } - - impl ComposableFilter for $ty where E::Marker: Into {} - - )+}; -} - -impl_qualifier_filter!(AnyExpr, AllExpr); 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 index be8900197f..90b99c3c85 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -17,9 +17,7 @@ pub mod ops; mod tests; pub use super::{Metadata, Property}; -use crate::db::graph::views::filter::model::{ - edge_expr::EdgeOp, filter_operator::ElemQual, EntityMarker, -}; +use crate::db::graph::views::filter::model::{edge_expr::EdgeOp, EntityMarker}; pub use dyn_expr::*; pub use exprs::*; pub use filters::*; @@ -71,24 +69,6 @@ pub trait CreateOp: EntityExpr + Clone + Send + Sync + 'static { ) -> Result> + 'g>, GraphError> { Err(GraphError::NotEdgeFilter) } - - /// Compile the expression for use as the lhs of a comparison, separating - /// any leading `any()`/`all()` qualifiers from the value expression they - /// qualify. The default has no qualifiers; `AnyExpr`/`AllExpr` strip - /// themselves and record their collapse mode instead of aggregating. - fn create_qualified_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result<(Arc> + 'g>, Vec), GraphError> { - Ok((self.create_node_op(graph)?, Vec::new())) - } - - fn create_qualified_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result<(Arc> + 'g>, Vec), GraphError> { - Ok((self.create_edge_op(graph)?, Vec::new())) - } } pub trait Marker: Into + Copy + Send + Sync + 'static {} 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 index 7c4d2493ac..b7d8800d78 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -14,8 +14,8 @@ //! ──► NodePropOp { graph, prop_id: 3 } ← NodeOp: apply() reads column 3 in O(1) //! //! NodeFilter.property("age").gt(30i64) ← BinaryCmpExpr (pure data) -//! .create_node_filter(graph)? -//! ──► BinaryCmpNodeOp { left: NodePropOp, right: Const(Some(I64(30))), op: Gt } +//! converts to the expression tree (`model::expr`) and compiles there: +//! ──► a comparison op over NodePropOp and Const(Some(I64(30))) //! apply: Prop::binary_cmp(Gt, age_value, Some(I64(30))) //! //! NodeFilter.property("score").temporal().sum() ← SumExpr (pure data) @@ -26,14 +26,13 @@ //! //! # Quantified evaluation //! -//! Filter types (`BinaryCmpExpr`, `StringExpr`, `PropValueSetExpr`) also -//! implement `NodeExpr`, producing list-aware ops for mid-chain use before `.any()`/`.all()`: +//! A comparison against a list-valued side gives one answer per element; +//! `.any()` / `.all()` written after it collapse that list: //! //! ```text //! temporal values = [8, 12, 5], rhs = 10 -//! .gt(10i64) as NodeExpr → ListAwareCmpNodeOp → Prop::List([false, true, false]) -//! .any() → AnyNodeOp reduces boolean list → Prop::Bool(true) -//! Eq Bool(true) → true (at least one matched) +//! .gt(10i64) → Prop::List([false, true, false]) (element-wise, in `model::expr`) +//! .any() → AnyNodeOp reduces the list → Prop::Bool(true) //! ``` use super::EdgeOp; @@ -48,11 +47,8 @@ use crate::{ NodeViewOps, }, }, - graph::views::filter::model::{ - filter_operator::{BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp}, - property_filter::evaluate::{ - aggregate_list_values, scan_f64_sum_count, scan_i64_sum, scan_u64_sum, - }, + graph::views::filter::model::property_filter::evaluate::{ + aggregate_list_values, scan_f64_sum_count, scan_i64_sum, scan_u64_sum, }, }, prelude::GraphViewOps, @@ -175,7 +171,7 @@ impl NodeOp for WithPropType { /// /// Collects all recorded values within the current view window into a `Some(Prop::List([...]))`. /// That list is then consumed by aggregator ops (`SumNodeOp`, `LenNodeOp`, …) or -/// by `ListAwareCmpNodeOp` for element-wise comparisons before `.any()`/`.all()` reduction. +/// compared element-wise, one answer per element, for `.any()`/`.all()` to collapse. #[derive(Clone)] pub(crate) struct TemporalNodePropOp { pub(crate) graph: G, @@ -476,70 +472,6 @@ impl_agg_entity_op!( } ); -// ───────────────────────────────────────────────────────────────────────────── -// ListAwareCmpNodeOp / ListAwareStringNodeOp / ListAwareSetNodeOp -// -// These ops implement NodeExpr for BinaryCmpExpr, StringExpr, and -// PropValueSetExpr respectively, enabling mid-chain use before .any()/.all(). -// -// Each uses broadcasting so that comparisons applied to a `Prop::List(...)` -// fan out element-wise; scalar inputs are passed through to the op directly. -// temporal().gt(5).any() -// temporal().contains("rock").all() -// temporal().is_in([...]).any() -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct ListAwareCmpNodeOp<'g> { - pub(crate) left: Arc> + 'g>, - pub(crate) right: Arc> + 'g>, - pub(crate) op: BinaryOp, -} - -impl<'g> NodeOp for ListAwareCmpNodeOp<'g> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - type Output = Option; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - let lv = self.left.apply(storage, node); - let rhs = self.right.apply(storage, node); - let op = &self.op; - broadcast_binary(lv, rhs, &|lv, rhs| { - Some(Prop::Bool(Prop::binary_cmp(op, &lv?, &rhs?))) - }) - } -} - -#[derive(Clone)] -pub(crate) struct ListAwareStringNodeOp<'g> { - pub(crate) left: Arc> + 'g>, - pub(crate) right: Arc> + 'g>, - pub(crate) op: StringOp, -} - -impl<'g> NodeOp for ListAwareStringNodeOp<'g> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - type Output = Option; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - let lv = self.left.apply(storage, node); - let rhs = self.right.apply(storage, node); - let op = &self.op; - broadcast_binary(lv, rhs, &|lv, rhs| { - Some(Prop::Bool(Option::::string_cmp(op, &lv, &rhs))) - }) - } -} - -// [1,2,3] == [1,2,3] -// [4,5,6] > [1,2,3] - pub fn broadcast_unary(v: Option, op: impl Fn(Option) -> Option) -> Option { match v { Some(Prop::List(v)) => Some(Prop::List(v.iter_all().map(|l| op(l)).flatten().collect())), @@ -585,155 +517,6 @@ pub fn broadcast_binary( } } -#[derive(Clone)] -pub(crate) struct ListAwareSetNodeOp<'g> { - pub(crate) inner: Arc> + 'g>, - pub(crate) values: Vec, - pub(crate) op: SetOp, -} - -impl<'g> NodeOp for ListAwareSetNodeOp<'g> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - type Output = Option; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - let vals = self.inner.apply(storage, node); - let values = &self.values; - let op = &self.op; - broadcast_unary(vals, |v| { - let v = v?; - Some(Prop::Bool(match op { - SetOp::IsIn => values - .iter() - .any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), - SetOp::IsNotIn => values - .iter() - .all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), - })) - }) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// ListAwareUnaryNodeOp — element-wise is_some / is_none via broadcast_unary -// -// Unlike `UnaryNodeOp` (which returns `bool` for use in `CreateFilter`), this -// op returns `Option` so it can plug into the expression chain via -// `CreateOp`. The closure intentionally does NOT `?`-propagate the inner -// `None` — the whole purpose of `is_some`/`is_none` is to test that case. -// ───────────────────────────────────────────────────────────────────────────── - -#[derive(Clone)] -pub(crate) struct ListAwareUnaryNodeOp<'g> { - pub(crate) inner: Arc> + 'g>, - pub(crate) op: UnaryOp, -} - -impl<'g> NodeOp for ListAwareUnaryNodeOp<'g> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - type Output = Option; - - fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - let vals = self.inner.apply(storage, node); - let op = &self.op; - broadcast_unary(vals, |v| { - Some(Prop::Bool(match op { - UnaryOp::IsSome => v.is_some(), - UnaryOp::IsNone => v.is_none(), - })) - }) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// PropValueSetNodeOp<'g> — is_in / is_not_in for Option (linear scan) -// ───────────────────────────────────────────────────────────────────────────── - -/// Checks whether an `Option` value is in (or not in) a fixed `Vec`. -/// Uses linear scan because `Prop` may contain floats (`F32`, `F64`) which don't -/// implement `Hash`. -pub struct PropValueSetNodeOp<'g> { - pub(crate) inner: Arc> + 'g>, - pub(crate) values: Vec, - pub(crate) op: SetOp, -} - -impl<'g> Clone for PropValueSetNodeOp<'g> { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - values: self.values.clone(), - op: self.op, - } - } -} - -impl<'g> NodeOp for PropValueSetNodeOp<'g> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - type Output = bool; - - fn apply(&self, storage: &GraphStorage, node: VID) -> bool { - match self.inner.apply(storage, node) { - None => false, - Some(v) => match self.op { - SetOp::IsIn => self - .values - .iter() - .any(|x| Prop::binary_cmp(&BinaryOp::Eq, x, &v)), - SetOp::IsNotIn => self - .values - .iter() - .all(|x| Prop::binary_cmp(&BinaryOp::Ne, x, &v)), - }, - } - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// BinaryCmpNodeOp<'g, T> — compares two NodeOp using BinaryOp -// ───────────────────────────────────────────────────────────────────────────── - -/// Internal op produced by [`BinaryCmpExpr::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: Const(Some(I64(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> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - 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 - } -} - // ───────────────────────────────────────────────────────────────────────────── // IdDomainNodeOp<'g> — id comparisons resolve their domain directly // ───────────────────────────────────────────────────────────────────────────── @@ -804,68 +587,3 @@ impl<'g> NodeOp for IdDomainNodeOp<'g> { self.inner.prop_type() } } - -// ───────────────────────────────────────────────────────────────────────────── -// StringNodeOp<'g, T> — applies a StringOp to two NodeOp -// ───────────────────────────────────────────────────────────────────────────── - -/// Internal op produced by [`StringExpr::create_node_filter`]. -/// -/// e.g. `NodeFilter.name().starts_with("Al")` compiles to: -/// `StringNodeOp { left: Name.map(...), right: Const(Some(Str("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> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - 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 [`UnaryExpr::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> { - fn domain(&self, _storage: &GraphStorage) -> NodeList { - NodeList::All - } - - 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 - } -} diff --git a/raphtory/src/db/graph/views/filter/model/tree.rs b/raphtory/src/db/graph/views/filter/model/tree.rs index ef2cfa1280..bfc8a2fa01 100644 --- a/raphtory/src/db/graph/views/filter/model/tree.rs +++ b/raphtory/src/db/graph/views/filter/model/tree.rs @@ -20,23 +20,20 @@ use crate::{ }, graph::views::filter::{ model::{ - and_filter::AndFilter, - dyn_factory::{DynEdgeFilterFactory, DynNodeFilterFactory}, - edge_filter::{EdgeEndpointNodeFilter, EdgeEndpointWrapper, EdgeFilter, Endpoint}, - exploded_edge_filter::ExplodedEdgeFilter, - graph_filter::GraphFilter, + edge_filter::Endpoint, + expr::{ + EdgeLeaf, ExplodedEdgeLeaf, Expr as SplitExpr, FilterExpr as SplitFilter, Leaf, + NodeLeaf, + }, layered_filter::layer_label, - node_expr::{DynCreateOp, DynTemporal}, - node_filter::NodeFilter, - not_filter::NotFilter, - or_filter::OrFilter, - DynCreateFilter, DynView, ViewWrapOps, + node_expr::DynCreateOp, + DynCreateFilter, }, CreateFilter, }, }, errors::GraphError, - prelude::{EntityAggOps, EntityExprFilterOps, Layer}, + prelude::Layer, }; use raphtory_api::core::{ entities::properties::prop::Prop, @@ -296,406 +293,321 @@ impl FilterExpr { } } -// ── compiling ──────────────────────────────────────────────────────────────── +// ── compiling: through the entity-split tree ──────────────────────────────── +// +// This tree predates the per-entity one in `model::expr`; python and GraphQL +// still speak it. It compiles by converting to that tree, so there is one +// compiler and one set of checks. A qualifier written on a value here +// (`x.any() == v`) becomes the qualifier on the comparison there +// (`(x == v).any()`). -/// A partly compiled value. Property reads keep the ability to switch to -/// their history until an aggregate or qualifier is applied. -enum Compiled { - Op(Arc), - Property(Arc), +fn invalid(msg: impl Into) -> GraphError { + GraphError::InvalidFilter(msg.into()) } -impl Compiled { - fn op(self) -> Arc { - match self { - Compiled::Op(op) => op, - Compiled::Property(prop) => prop, - } - } - - /// An endpoint read is a node read the edge evaluates on the node at that - /// end. The wrapping happens here, at the read, so that qualifiers and - /// aggregates applied above it see an edge expression and are compiled the - /// way an edge filter compiles them. - fn through(self, endpoint: Endpoint) -> Self { - match self { - Compiled::Op(op) => Compiled::Op(Arc::new(EdgeEndpointWrapper::new(op, endpoint))), - Compiled::Property(prop) => { - Compiled::Property(Arc::new(EdgeEndpointWrapper::new(prop, endpoint))) - } - } - } - - fn map_op(self, f: impl FnOnce(Arc) -> Arc) -> Self { - Compiled::Op(f(self.op())) - } +/// A leaf of the per-entity tree built from this tree's read. +trait FromOldRead: Leaf { + fn from_read(scope: &Scope, target: &Target) -> Result; + fn structural(scope: &Scope, pred: Structural) -> Result; + /// Switch a property read to its history. + fn temporal(self) -> Result; } -fn invalid(msg: impl Into) -> GraphError { - GraphError::InvalidFilter(msg.into()) +fn not_a_property() -> GraphError { + invalid("temporal() applies to a property") } -fn node_factory(views: &[ViewOp]) -> Arc { - let mut f: Arc = Arc::new(NodeFilter); - for op in views { - f = match op { - ViewOp::Window { start, end } => f.window(*start, *end), - ViewOp::At(t) => f.at(*t), - ViewOp::After(t) => f.after(*t), - ViewOp::Before(t) => f.before(*t), - ViewOp::Latest => Arc::new(f.latest()), - ViewOp::SnapshotAt(t) => Arc::new(f.snapshot_at(*t)), - ViewOp::SnapshotLatest => Arc::new(f.snapshot_latest()), - ViewOp::Layers(names) => Arc::new(f.layer(names.clone())), - }; +impl FromOldRead for NodeLeaf { + fn from_read(scope: &Scope, target: &Target) -> Result { + if scope.endpoint.is_some() { + return Err(invalid("a node expression has no src()/dst() endpoint")); + } + let views = scope.views.clone(); + Ok(match target { + Target::Field(field) => NodeLeaf::Field { + views, + field: *field, + }, + Target::Degree(direction) => NodeLeaf::Degree { + views, + direction: *direction, + }, + Target::Property(name) => NodeLeaf::property(views, name.clone(), false), + Target::Metadata(name) => NodeLeaf::metadata(views, name.clone()), + }) } - f -} -fn edge_factory(entity: Entity, views: &[ViewOp]) -> Arc { - let mut f: Arc = match entity { - Entity::ExplodedEdge => Arc::new(ExplodedEdgeFilter), - Entity::Edge | Entity::Node => Arc::new(EdgeFilter), - }; - for op in views { - f = match op { - ViewOp::Window { start, end } => f.dyn_window(*start, *end), - ViewOp::At(t) => f.dyn_at(*t), - ViewOp::After(t) => f.dyn_after(*t), - ViewOp::Before(t) => f.dyn_before(*t), - ViewOp::Latest => f.dyn_latest(), - ViewOp::SnapshotAt(t) => f.dyn_snapshot_at(*t), - ViewOp::SnapshotLatest => f.dyn_snapshot_latest(), - ViewOp::Layers(names) => f.dyn_layer(names.clone()), - }; + fn structural(scope: &Scope, pred: Structural) -> Result { + if scope.endpoint.is_some() { + return Err(invalid("a node has no src()/dst() endpoint")); + } + match pred { + Structural::IsActive => Ok(NodeLeaf::is_active(scope.views.clone())), + other => Err(invalid(format!("{other} is an edge predicate"))), + } } - f -} -fn read_node(f: &Arc, target: &Target) -> Compiled { - match target { - Target::Field(Field::Id) => Compiled::Op(f.dyn_id()), - Target::Field(Field::Name) => Compiled::Op(f.dyn_name()), - Target::Field(Field::NodeType) => Compiled::Op(f.dyn_node_type()), - Target::Degree(Direction::BOTH) => Compiled::Op(f.dyn_degree()), - Target::Degree(Direction::IN) => Compiled::Op(f.dyn_in_degree()), - Target::Degree(Direction::OUT) => Compiled::Op(f.dyn_out_degree()), - Target::Property(name) => Compiled::Property(f.dyn_property(name.clone())), - Target::Metadata(name) => Compiled::Op(f.dyn_metadata(name.clone())), + fn temporal(self) -> Result { + match self { + NodeLeaf::Property { + views, + name, + temporal: false, + } => Ok(NodeLeaf::Property { + views, + name, + temporal: true, + }), + _ => Err(not_a_property()), + } } } -impl Expr { - /// The erased, compilable form of this value. - pub fn compile(&self) -> Result, GraphError> { - Ok(self.compile_inner()?.op()) +impl FromOldRead for EdgeLeaf { + fn from_read(scope: &Scope, target: &Target) -> Result { + // An endpoint read is a node read, scoped by the same views, that the + // edge evaluates on the node at that end. + if let Some(endpoint) = scope.endpoint { + let node_scope = Scope { + entity: Entity::Node, + views: scope.views.clone(), + endpoint: None, + }; + let inner = Box::new(SplitExpr::Read(NodeLeaf::from_read(&node_scope, target)?)); + return Ok(match endpoint { + Endpoint::Src => EdgeLeaf::Src(inner), + Endpoint::Dst => EdgeLeaf::Dst(inner), + }); + } + let views = scope.views.clone(); + match target { + Target::Property(name) => Ok(EdgeLeaf::property(views, name.clone(), false)), + Target::Metadata(name) => Ok(EdgeLeaf::metadata(views, name.clone())), + Target::Field(_) | Target::Degree(_) => Err(invalid( + "an edge has no fields or degree; read them through src() or dst()", + )), + } } - fn compile_inner(&self) -> Result { - match self { - Expr::Const(value) => Ok(Compiled::Op(Arc::new(value.clone()))), - Expr::Read { scope, target } => match (scope.entity, scope.endpoint) { - (Entity::Node, Some(_)) => { - Err(invalid("a node expression has no src()/dst() endpoint")) - } - (Entity::Node, None) => Ok(read_node(&node_factory(&scope.views), target)), - // An endpoint read is a node read, scoped by the same views, - // that the edge evaluates on the node at that end. It is an - // edge's read: an edge update has no endpoint wrapper of its own. - (Entity::ExplodedEdge, Some(_)) => { - Err(invalid("an exploded edge has no src()/dst() endpoint")) - } - (Entity::Edge, Some(endpoint)) => { - Ok(read_node(&node_factory(&scope.views), target).through(endpoint)) - } - (entity, None) => { - let f = edge_factory(entity, &scope.views); - Ok(match target { - Target::Property(name) => Compiled::Property(f.dyn_property(name.clone())), - Target::Metadata(name) => Compiled::Op(f.dyn_metadata(name.clone())), - Target::Field(_) | Target::Degree(_) => { - return Err(invalid( - "an edge has no fields or degree; read them through src() or dst()", - )) - } - }) - } - }, - Expr::Temporal(inner) => match inner.compile_inner()? { - Compiled::Property(prop) => Ok(Compiled::Op(prop.temporal())), - Compiled::Op(_) => Err(invalid("temporal() applies to a property")), - }, - Expr::Agg(agg, inner) => Ok(inner.compile_inner()?.map_op(|op| match agg { - Agg::Sum => Arc::new(op.sum()), - Agg::Avg => Arc::new(op.avg()), - Agg::Min => Arc::new(op.min()), - Agg::Max => Arc::new(op.max()), - Agg::First => Arc::new(op.first()), - Agg::Last => Arc::new(op.last()), - Agg::Len => Arc::new(op.len()), - })), - Expr::Qual(qual, inner) => Ok(inner.compile_inner()?.map_op(|op| match qual { - Qual::Any => Arc::new(op.any()), - Qual::All => Arc::new(op.all()), - })), + fn structural(scope: &Scope, pred: Structural) -> Result { + if let Some(endpoint) = scope.endpoint { + if pred != Structural::IsActive { + return Err(invalid(format!("{pred} is an edge predicate"))); + } + let inner = Box::new(SplitExpr::Read(NodeLeaf::is_active(scope.views.clone()))); + return Ok(match endpoint { + Endpoint::Src => EdgeLeaf::Src(inner), + Endpoint::Dst => EdgeLeaf::Dst(inner), + }); } + let views = scope.views.clone(); + Ok(match pred { + Structural::IsActive => EdgeLeaf::IsActive { views }, + Structural::IsValid => EdgeLeaf::IsValid { views }, + Structural::IsDeleted => EdgeLeaf::IsDeleted { views }, + Structural::IsSelfLoop => EdgeLeaf::IsSelfLoop { views }, + }) } -} -/// Compile a comparison whose right-hand side is either a constant or an -/// expression; both go through the same typed method, a constant being the -/// expression of its own value. -macro_rules! binary { - ($lhs:expr, $rhs:expr, $method:ident $(, $arg:expr)*) => {{ - let lhs = $lhs.compile()?; - let rhs: Arc = $rhs.compile()?; - let filter: Arc = Arc::new(lhs.$method(rhs $(, $arg)*)); - filter - }}; + fn temporal(self) -> Result { + match self { + EdgeLeaf::Property { + views, + name, + temporal: false, + } => Ok(EdgeLeaf::Property { + views, + name, + temporal: true, + }), + EdgeLeaf::Src(inner) => Ok(EdgeLeaf::Src(Box::new(temporal_read(*inner)?))), + EdgeLeaf::Dst(inner) => Ok(EdgeLeaf::Dst(Box::new(temporal_read(*inner)?))), + _ => Err(not_a_property()), + } + } } -impl FilterExpr { - /// The erased, applicable form of this filter. - /// - /// A view (`View`) applies first: the graph is seen through it and the other - /// legs run inside it, reads included, the way `graph.window(..).filter(expr)` - /// does. A view therefore stands alone or is a leg of the top-level `and` - /// (nested `and`s count as top level); under `or` or `not` it has no meaning the - /// engine can give it and is refused. - pub fn compile(&self) -> Result, GraphError> { - let (views, predicates, saw_view) = self.split_top_views(); - if saw_view && views.is_empty() { - return Err(invalid("a view filter needs at least one view")); +impl FromOldRead for ExplodedEdgeLeaf { + fn from_read(scope: &Scope, target: &Target) -> Result { + if scope.endpoint.is_some() { + return Err(invalid("an exploded edge has no src()/dst() endpoint")); } - if views.is_empty() { - return self.compile_nested(); + let views = scope.views.clone(); + match target { + Target::Property(name) => Ok(ExplodedEdgeLeaf::property(views, name.clone(), false)), + Target::Metadata(name) => Ok(ExplodedEdgeLeaf::metadata(views, name.clone())), + Target::Field(_) | Target::Degree(_) => Err(invalid( + "an edge has no fields or degree; read them through src() or dst()", + )), } - let inner: Arc = if predicates.is_empty() { - Arc::new(GraphFilter) - } else { - combine( - predicates.iter().map(|p| p.compile_nested()), - "and", - |left, right| Arc::new(AndFilter { left, right }), - )? - }; - Ok(Arc::new(Viewed { views, inner })) } - /// The view ops at the top of the filter, in order, and the predicates beside - /// them. `and` nests flatten; anything else is a predicate. The flag says whether - /// a `View` node was seen at all, so an empty one can be told from none. - fn split_top_views(&self) -> (Vec, Vec<&FilterExpr>, bool) { - fn walk<'a>( - filter: &'a FilterExpr, - views: &mut Vec, - predicates: &mut Vec<&'a FilterExpr>, - saw_view: &mut bool, - ) { - match filter { - FilterExpr::View(ops) => { - *saw_view = true; - views.extend(ops.iter().cloned()); - } - FilterExpr::And(items) => { - for item in items { - walk(item, views, predicates, saw_view); - } - } - other => predicates.push(other), - } + fn structural(scope: &Scope, pred: Structural) -> Result { + if scope.endpoint.is_some() { + return Err(invalid("an exploded edge has no src()/dst() endpoint")); } - let (mut views, mut predicates, mut saw_view) = (Vec::new(), Vec::new(), false); - walk(self, &mut views, &mut predicates, &mut saw_view); - (views, predicates, saw_view) + let views = scope.views.clone(); + Ok(match pred { + Structural::IsActive => ExplodedEdgeLeaf::IsActive { views }, + Structural::IsValid => ExplodedEdgeLeaf::IsValid { views }, + Structural::IsDeleted => ExplodedEdgeLeaf::IsDeleted { views }, + Structural::IsSelfLoop => ExplodedEdgeLeaf::IsSelfLoop { views }, + }) } - /// A filter below the top level: every node but a view. - fn compile_nested(&self) -> Result, GraphError> { - Ok(match self { - FilterExpr::Cmp { op, lhs, rhs } => match op { - CmpOp::Eq => binary!(lhs, rhs, eq), - CmpOp::Ne => binary!(lhs, rhs, ne), - CmpOp::Lt => binary!(lhs, rhs, lt), - CmpOp::Le => binary!(lhs, rhs, le), - CmpOp::Gt => binary!(lhs, rhs, gt), - CmpOp::Ge => binary!(lhs, rhs, ge), - }, - FilterExpr::Str { op, lhs, rhs } => match op { - StrOp::StartsWith => binary!(lhs, rhs, starts_with), - StrOp::EndsWith => binary!(lhs, rhs, ends_with), - StrOp::Contains => binary!(lhs, rhs, contains), - StrOp::NotContains => binary!(lhs, rhs, not_contains), - StrOp::FuzzySearch { - levenshtein_distance, - prefix_match, - } => binary!(lhs, rhs, fuzzy_search, *levenshtein_distance, *prefix_match), - }, - FilterExpr::IsSome(expr) => Arc::new(expr.compile()?.is_some()), - FilterExpr::IsNone(expr) => Arc::new(expr.compile()?.is_none()), - FilterExpr::In { - expr, - values, - negated, - } => { - let lhs = expr.compile()?; - if *negated { - Arc::new(lhs.is_not_in(values.clone())) - } else { - Arc::new(lhs.is_in(values.clone())) - } - } - FilterExpr::Structural { scope, pred } => { - // Through an endpoint, the predicate is a node predicate - // evaluated on the node at that end of the edge. - if let Some(endpoint) = scope.endpoint { - if scope.entity == Entity::Node { - return Err(invalid("a node has no src()/dst() endpoint")); - } - if scope.entity == Entity::ExplodedEdge { - return Err(invalid("an exploded edge has no src()/dst() endpoint")); - } - if *pred != Structural::IsActive { - return Err(invalid(format!("{pred} is an edge predicate"))); - } - return Ok(Arc::new(EdgeEndpointNodeFilter { - endpoint, - inner: node_factory(&scope.views).dyn_is_active(), - })); - } - match (scope.entity, pred) { - (Entity::Node, Structural::IsActive) => { - node_factory(&scope.views).dyn_is_active() - } - (Entity::Node, other) => { - return Err(invalid(format!("{other} is an edge predicate"))) - } - (entity, pred) => { - let f = edge_factory(entity, &scope.views); - match pred { - Structural::IsActive => f.dyn_is_active(), - Structural::IsValid => f.dyn_is_valid(), - Structural::IsDeleted => f.dyn_is_deleted(), - Structural::IsSelfLoop => f.dyn_is_self_loop(), - } - } - } - } - FilterExpr::View(_) => { - return Err(invalid( - "a view applies to the whole filter: use it alone or as a leg of the \ - top-level `and`, not under `or` or `not`", - )) - } - FilterExpr::And(items) => combine( - items.iter().map(Self::compile_nested), - "and", - |left, right| Arc::new(AndFilter { left, right }), - )?, - FilterExpr::Or(items) => combine( - items.iter().map(Self::compile_nested), - "or", - |left, right| Arc::new(OrFilter { left, right }), - )?, - FilterExpr::Not(inner) => Arc::new(NotFilter(inner.compile_nested()?)), - FilterExpr::Opaque(filter) => filter.0.clone(), - }) + fn temporal(self) -> Result { + match self { + ExplodedEdgeLeaf::Property { + views, + name, + temporal: false, + } => Ok(ExplodedEdgeLeaf::Property { + views, + name, + temporal: true, + }), + _ => Err(not_a_property()), + } } } -/// The graph-level view a chain of view ops describes, applied in order. -fn compile_view(views: &[ViewOp]) -> DynView { - let mut v: DynView = Arc::new(GraphFilter); - for op in views { - v = match op { - ViewOp::Window { start, end } => v.window(*start, *end), - ViewOp::At(t) => v.at(*t), - ViewOp::After(t) => v.after(*t), - ViewOp::Before(t) => v.before(*t), - ViewOp::Latest => Arc::new(v.latest()), - ViewOp::SnapshotAt(t) => Arc::new(v.snapshot_at(*t)), - ViewOp::SnapshotLatest => Arc::new(v.snapshot_latest()), - ViewOp::Layers(names) => Arc::new(v.layer(Layer::from(names.clone()))), - }; +/// `temporal()` on a read: the read must be a property's latest value. +fn temporal_read(expr: SplitExpr) -> Result, GraphError> { + match expr { + SplitExpr::Read(leaf) => Ok(SplitExpr::Read(leaf.temporal()?)), + _ => Err(not_a_property()), } - v } -/// A filter applied inside a view: the graph is seen through `views` first and -/// `inner` runs on that graph, reads included, so `and: [view, pred]` is -/// `graph.view(..).filter(pred)`. -#[derive(Clone)] -struct Viewed { - views: Vec, - inner: Arc, +/// Convert a value, collecting the qualifiers written on it (innermost first) +/// so the predicate around it can take them. +fn convert_value( + expr: &Expr, + quals: &mut Vec, +) -> Result, GraphError> { + Ok(match expr { + Expr::Const(value) => SplitExpr::Const(value.clone()), + Expr::Read { scope, target } => SplitExpr::Read(L::from_read(scope, target)?), + Expr::Temporal(inner) => temporal_read(convert_value(inner, quals)?)?, + Expr::Agg(agg, inner) => SplitExpr::Agg(*agg, Box::new(convert_value(inner, quals)?)), + Expr::Qual(qual, inner) => { + let value = convert_value(inner, quals)?; + quals.push(*qual); + value + } + }) } -impl Viewed { - fn view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - compile_view(&self.views).dyn_filter_graph_view(Arc::new(graph)) +/// Wrap a predicate in the qualifiers its value carried. The innermost +/// qualifier written collapses the outermost list level, so it goes on last. +fn qualify(mut pred: SplitExpr, quals: Vec) -> SplitExpr { + for qual in quals.iter().rev() { + pred = match qual { + Qual::Any => SplitExpr::Any(Box::new(pred)), + Qual::All => SplitExpr::All(Box::new(pred)), + }; } + pred } -impl CreateFilter for Viewed { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = DynGraphArc<'graph> - where - Self: 'graph; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = Arc + 'graph> - where - Self: 'graph; - - type FilteredGraph<'graph, G> - = DynGraphArc<'graph> - where - Self: 'graph, - G: GraphView + 'graph; +fn convert_predicate(filter: &FilterExpr) -> Result, GraphError> { + let mut quals = Vec::new(); + let pred = match filter { + FilterExpr::Cmp { op, lhs, rhs } => SplitExpr::Cmp( + *op, + Box::new(convert_value(lhs, &mut quals)?), + Box::new(convert_value(rhs, &mut quals)?), + ), + FilterExpr::Str { op, lhs, rhs } => SplitExpr::Str( + op.clone(), + Box::new(convert_value(lhs, &mut quals)?), + Box::new(convert_value(rhs, &mut quals)?), + ), + FilterExpr::IsSome(e) => SplitExpr::IsSome(Box::new(convert_value(e, &mut quals)?)), + FilterExpr::IsNone(e) => SplitExpr::IsNone(Box::new(convert_value(e, &mut quals)?)), + FilterExpr::In { + expr, + values, + negated, + } => SplitExpr::In { + expr: Box::new(convert_value(expr, &mut quals)?), + values: values.clone(), + negated: *negated, + }, + FilterExpr::Structural { scope, pred } => SplitExpr::Read(L::structural(scope, *pred)?), + FilterExpr::View(_) + | FilterExpr::And(_) + | FilterExpr::Or(_) + | FilterExpr::Not(_) + | FilterExpr::Opaque(_) => unreachable!("handled by FilterExpr::to_split"), + }; + Ok(qualify(pred, quals)) +} - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let viewed = self.view(graph)?; - self.inner.create_dyn_filter(viewed, Arc::new(filtered)) +/// The entity a value reads from, if it reads at all. +fn value_entity(expr: &Expr) -> Option { + match expr { + Expr::Const(_) => None, + Expr::Read { scope, .. } => Some(scope.entity), + Expr::Temporal(e) | Expr::Agg(_, e) | Expr::Qual(_, e) => value_entity(e), } +} - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let viewed = self.view(graph)?; - self.inner - .create_dyn_node_filter(viewed, Arc::new(filtered)) +fn predicate_entity(filter: &FilterExpr) -> Result { + let entity = match filter { + FilterExpr::Cmp { lhs, rhs, .. } | FilterExpr::Str { lhs, rhs, .. } => { + value_entity(lhs).or_else(|| value_entity(rhs)) + } + FilterExpr::IsSome(e) | FilterExpr::IsNone(e) | FilterExpr::In { expr: e, .. } => { + value_entity(e) + } + FilterExpr::Structural { scope, .. } => Some(scope.entity), + _ => None, + }; + entity.ok_or_else(|| invalid("a comparison needs an entity value on at least one side")) +} + +impl FilterExpr { + /// This filter as the per-entity tree. + pub fn to_split(&self) -> Result { + Ok(match self { + FilterExpr::View(ops) => SplitFilter::View(ops.clone()), + FilterExpr::And(items) => { + SplitFilter::And(items.iter().map(Self::to_split).collect::>()?) + } + FilterExpr::Or(items) => { + SplitFilter::Or(items.iter().map(Self::to_split).collect::>()?) + } + FilterExpr::Not(inner) => SplitFilter::Not(Box::new(inner.to_split()?)), + FilterExpr::Opaque(filter) => SplitFilter::Opaque(filter.clone()), + predicate => match predicate_entity(predicate)? { + Entity::Node => SplitFilter::Node(convert_predicate(predicate)?), + Entity::Edge => SplitFilter::Edge(convert_predicate(predicate)?), + Entity::ExplodedEdge => SplitFilter::ExplodedEdge(convert_predicate(predicate)?), + }, + }) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - let viewed = self.view(graph)?; - self.inner.dyn_filter_graph_view(viewed) + /// The erased, applicable form of this filter. + pub fn compile(&self) -> Result, GraphError> { + self.to_split()?.compile() } } -/// Fold compiled operands pairwise, left to right. An empty list has no -/// meaning either way (`and` of nothing is not "everything", `or` of nothing -/// is not "nothing" the caller asked for), so it is refused. -fn combine( - mut compiled: impl Iterator, GraphError>>, - name: &str, - join: impl Fn(Arc, Arc) -> Arc, -) -> Result, GraphError> { - let first = compiled - .next() - .ok_or_else(|| invalid(format!("`{name}` needs at least one operand")))??; - compiled.try_fold(first, |acc, next| Ok(join(acc, next?))) +impl Expr { + /// The erased, compilable form of this value. A qualifier written on the + /// value has no meaning without the comparison that follows it and is + /// left out here; the value it qualifies is what compiles. + pub fn compile(&self) -> Result, GraphError> { + let mut quals = Vec::new(); + match value_entity(self).unwrap_or(Entity::Node) { + Entity::Node => convert_value::(self, &mut quals)?.compile_value(), + Entity::Edge => convert_value::(self, &mut quals)?.compile_value(), + Entity::ExplodedEdge => { + convert_value::(self, &mut quals)?.compile_value() + } + } + } } /// A tree is a filter in its own right: applying it compiles it first. @@ -910,7 +822,10 @@ mod tests { EdgeViewFilterOps, PropertyExprFactory, ViewWrapOps, }, }, - prelude::{AdditionOps, EdgeViewOps, Graph, GraphViewOps, NodeViewOps, TimeOps, NO_PROPS}, + prelude::{ + AdditionOps, EdgeViewOps, EntityAggOps, EntityExprFilterOps, Graph, GraphViewOps, + NodeViewOps, TimeOps, NO_PROPS, + }, }; use raphtory_api::core::entities::properties::prop::IntoProp; From 10562f78aea943363fc92382faea52069ccbbde0 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:41:47 +0100 Subject: [PATCH 125/155] Key the GraphQL filter grammar by entity FilterExpr is a choice of node, edge or explodedEdge expression, a view, or a combination; each entity's expression names what it reads as a plain field (`property: "p"`, `field: NAME`, `isActive: true`) and takes views through a `viewed` wrapper. The input types mirror the expression tree one to one and convert to and from it; the previous read/scope grammar is removed. --- .../test_graphql/misc/test_snapshot.py | 6 +- .../test_graphql/test_apply_views.py | 54 +- .../test_graphql/test_component_filter.py | 34 +- .../test_graphql/test_filter_expr_grammar.py | 98 +- .../test_filters/test_edge_filter_gql.py | 110 +- .../test_graph_edges_property_filter.py | 912 +++++++++---- .../test_graph_nodes_property_filter.py | 483 +++++-- .../test_filters/test_neighbours_filter.py | 338 +++-- .../test_filters/test_node_filter_gql.py | 165 ++- .../test_nodes_property_filter.py | 1187 ++++++++++------ .../test_graphql/test_gql_history.py | 108 +- .../test_gql_mutation_time_input.py | 42 +- .../test_graphql/test_gql_node_id.py | 48 +- .../test_gql_same_timestamp_writes.py | 72 +- .../test_graphql/test_metadata_dispatch.py | 6 +- .../test_graphql/test_namespace.py | 6 +- .../test_graphql/test_nodes.py | 18 +- raphtory-graphql/schema.graphql | 441 ++++-- .../src/client/graphql_transport.rs | 34 +- raphtory-graphql/src/lib.rs | 58 +- .../tests/components/out_component.rs | 223 ++- .../src/model/graph/filter_expr_input.rs | 1199 +++++++++-------- 22 files changed, 3673 insertions(+), 1969 deletions(-) diff --git a/python/tests/test_base_install/test_graphql/misc/test_snapshot.py b/python/tests/test_base_install/test_graphql/misc/test_snapshot.py index 1bb4dc8ba6..d16f4c980d 100644 --- a/python/tests/test_base_install/test_graphql/misc/test_snapshot.py +++ b/python/tests/test_base_install/test_graphql/misc/test_snapshot.py @@ -9,7 +9,8 @@ def test_snapshot(): client = server.get_client() def query(graph: str, window: str): - return client.query(f"""{{ + return client.query( + f"""{{ graph(path: "{graph}") {{ window: {window} {{ edges {{ @@ -24,7 +25,8 @@ def query(graph: str, window: str): }} }} }} - }}""") + }}""" + ) client.new_graph("event", "EVENT") g = client.remote_graph("event") diff --git a/python/tests/test_base_install/test_graphql/test_apply_views.py b/python/tests/test_base_install/test_graphql/test_apply_views.py index f244ca1aae..2764ede8a8 100644 --- a/python/tests/test_base_install/test_graphql/test_apply_views.py +++ b/python/tests/test_base_install/test_graphql/test_apply_views.py @@ -2073,11 +2073,19 @@ def test_apply_view_node_filter(): applyViews(views: [ { filter: { - eq: { - lhs: { read: { entity: NODE, target: { property: "where" } } } - rhs: { const: { str: "Berlin" } } - } - } + node: { + eq: { + lhs: { + property: "where" + } + rhs: { + const: { + str: "Berlin" + } + } + } + } + } } ]) { nodes { @@ -2102,11 +2110,19 @@ def test_apply_view_edge_filter(): applyViews(views: [ { filter: { - eq: { - lhs: { read: { entity: EDGE, target: { property: "where" } } } - rhs: { const: { str: "fishbowl" } } - } - } + edge: { + eq: { + lhs: { + property: "where" + } + rhs: { + const: { + str: "fishbowl" + } + } + } + } + } } ]) { edges { @@ -2264,11 +2280,19 @@ def test_apply_view_a_lot_of_views(): { window: { start: 1735689600000, end: 1735862400000 } }, { layers: ["follows"] }, { filter: { - eq: { - lhs: { read: { entity: NODE, target: { property: "where" } } } - rhs: { const: { str: "Berlin" } } - } - } } + node: { + eq: { + lhs: { + property: "where" + } + rhs: { + const: { + str: "Berlin" + } + } + } + } + } } ]) { list { name diff --git a/python/tests/test_base_install/test_graphql/test_component_filter.py b/python/tests/test_base_install/test_graphql/test_component_filter.py index f881803b5e..db647bbae4 100644 --- a/python/tests/test_base_install/test_graphql/test_component_filter.py +++ b/python/tests/test_base_install/test_graphql/test_component_filter.py @@ -2,7 +2,7 @@ while returning full-graph nodes, so their other-layer neighbours remain queryable. All tests are read-only over the same graph, so one module-scoped server serves them all. -Filter selects use `{ isValid: { entity: EDGE } }` as a pass-all edge expression. +Filter selects use `{ edge: { isValid: true } }` as a pass-all edge expression. """ import pytest @@ -38,14 +38,14 @@ def test_out_component_scoped_by_edge_layer(client): client, "a", "outComponent", - '{isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}', + '{ edge: { viewed: { views: [{ layers: ["owns"] }], expr: { isValid: true } } } }', ) == ["b", "c"] # scope to `has` -> only a's own satellite (owns edges are not followed) assert _names( client, "a", "outComponent", - '{isValid: {entity: EDGE, views: [{layers: ["has"]}]}}', + '{ edge: { viewed: { views: [{ layers: ["has"] }], expr: { isValid: true } } } }', ) == ["x"] @@ -55,7 +55,7 @@ def test_out_component_scoped_by_node_filter(client): client, "a", "outComponent", - '{isIn: {expr: {read: {entity: NODE, target: {field: NAME}}}, values: {list: [{str: "b"}, {str: "c"}]}}}', + '{ node: { isIn: { expr: { field: NAME }, values: { list: [{ str: "b" }, { str: "c" }] } } } }', ) == ["b", "c"] @@ -75,28 +75,28 @@ def test_component_filter_and_or_combinators(client): client, "a", "outComponent", - '{or: [{eq: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "b"}}}}, {eq: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "c"}}}}]}', + '{ or: [{ node: { eq: { lhs: { field: NAME }, rhs: { const: { str: "b" } } } } }, { node: { eq: { lhs: { field: NAME }, rhs: { const: { str: "c" } } } } }] }', ) == ["b", "c"] # node AND: step through nodes that are neither x nor y assert _names( client, "a", "outComponent", - '{and: [{ne: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "x"}}}}, {ne: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "y"}}}}]}', + '{ and: [{ node: { ne: { lhs: { field: NAME }, rhs: { const: { str: "x" } } } } }, { node: { ne: { lhs: { field: NAME }, rhs: { const: { str: "y" } } } } }] }', ) == ["b", "c"] # edge OR: follow owns OR has edges -> everything downstream assert _names( client, "a", "outComponent", - '{or: [{isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}, {isValid: {entity: EDGE, views: [{layers: ["has"]}]}}]}', + '{ or: [{ edge: { viewed: { views: [{ layers: ["owns"] }], expr: { isValid: true } } } }, { edge: { viewed: { views: [{ layers: ["has"] }], expr: { isValid: true } } } }] }', ) == ["b", "c", "x", "y"] # edge AND: owns AND valid assert _names( client, "a", "outComponent", - '{and: [{isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}, {isValid: {entity: EDGE}}]}', + '{ and: [{ edge: { viewed: { views: [{ layers: ["owns"] }], expr: { isValid: true } } } }, { edge: { isValid: true } }] }', ) == ["b", "c"] @@ -107,14 +107,14 @@ def test_component_top_level_and_or_across_kinds(client): client, "a", "outComponent", - '{and: [{view: [{layers: ["owns"]}]}, {ne: {lhs: {read: {entity: NODE, target: {field: NAME}}}, rhs: {const: {str: "c"}}}}]}', + '{ and: [{ view: [{ layers: ["owns"] }] }, { node: { ne: { lhs: { field: NAME }, rhs: { const: { str: "c" } } } } }] }', ) == ["b"] # edge(has layer) OR edge(owns layer) -> everything downstream assert _names( client, "a", "outComponent", - '{or: [{isValid: {entity: EDGE, views: [{layers: ["has"]}]}}, {isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}]}', + '{ or: [{ edge: { viewed: { views: [{ layers: ["has"] }], expr: { isValid: true } } } }, { edge: { viewed: { views: [{ layers: ["owns"] }], expr: { isValid: true } } } }] }', ) == ["b", "c", "x", "y"] @@ -125,14 +125,14 @@ def test_in_component_scoped_by_filters(client): client, "c", "inComponent", - '{isIn: {expr: {read: {entity: NODE, target: {field: NAME}}}, values: {list: [{str: "a"}, {str: "b"}]}}}', + '{ node: { isIn: { expr: { field: NAME }, values: { list: [{ str: "a" }, { str: "b" }] } } } }', ) == ["a", "b"] # edge filter assert _names( client, "c", "inComponent", - '{isValid: {entity: EDGE, views: [{layers: ["owns"]}]}}', + '{ edge: { viewed: { views: [{ layers: ["owns"] }], expr: { isValid: true } } } }', ) == ["a", "b"] # graph (layer) filter assert _names(client, "c", "inComponent", '{view: [{layers: ["owns"]}]}') == [ @@ -147,8 +147,8 @@ def test_component_respects_an_external_graph_filter(client): # A graph-level filter (here removing `x`) applied before the walk must be honoured — the # returned nodes are over that already-filtered graph. q = ( - '{ graph(path: "g") { filterNodes: filter(expr: { ne: { lhs: { read: { entity: NODE, target: { field: NAME } } }, ' - 'rhs: { const: { str: "x" } } } }) ' + '{ graph(path: "g") { filterNodes: filter(expr: { node: { ne: { lhs: { field: NAME }, ' + 'rhs: { const: { str: "x" } } } } }) ' '{ node(name: "a") { outComponent { list { name } } } } } }' ) got = client.query(q)["graph"]["filterNodes"]["node"]["outComponent"]["list"] @@ -159,9 +159,9 @@ def test_component_external_graph_filter_composed_with_select(client): # External graph filter (remove `c`) AND a component `select` (owns layer) compose: the # owns walk from `a` would reach b, c — but c is filtered out, leaving only b. q = ( - '{ graph(path: "g") { filterNodes: filter(expr: { ne: { lhs: { read: { entity: NODE, target: { field: NAME } } }, ' - 'rhs: { const: { str: "c" } } } }) ' - '{ node(name: "a") { outComponent(select: { isValid: { entity: EDGE, views: [{ layers: ["owns"] }] } }) ' + '{ graph(path: "g") { filterNodes: filter(expr: { node: { ne: { lhs: { field: NAME }, ' + 'rhs: { const: { str: "c" } } } } }) ' + '{ node(name: "a") { outComponent(select: { edge: { viewed: { views: [{ layers: ["owns"] }], expr: { isValid: true } } } }) ' "{ list { name } } } } } }" ) got = client.query(q)["graph"]["filterNodes"]["node"]["outComponent"]["list"] diff --git a/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py b/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py index 3b8da8f013..b0abe01729 100644 --- a/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py +++ b/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py @@ -1,10 +1,10 @@ """The filter tree as a GraphQL input. -`FilterExpr` is the same tree the local engine compiles: one grammar for -nodes, edges and views, with an expression on *both* sides of a comparison. -These tests send trees as JSON variables and check the answers against the -same graph read locally, so the wire grammar is pinned by results, not by -shape. +`FilterExpr` is the same tree the local engine compiles: the entity is the key +(`node`, `edge`, `explodedEdge`), what it reads is a plain field, and an +expression stands on *both* sides of a comparison. These tests send trees as +JSON variables and check the answers against the same graph read locally, so +the wire grammar is pinned by results, not by shape. """ import pytest @@ -50,14 +50,22 @@ def build(): """ -def read(t): - return {"read": {"entity": "NODE", "target": t}} +def node(expr): + return {"node": expr} + + +def edge(expr): + return {"edge": expr} def const(v): return {"const": v} +def viewed(views, expr): + return {"viewed": {"views": views, "expr": expr}} + + def node_names(client, tree): out = client.query(NODES, {"f": tree}) return sorted(n["name"] for n in out["graph"]["filter"]["nodes"]["list"]) @@ -76,7 +84,7 @@ def test_both_sides_of_a_comparison_are_expressions(): constant-only grammar could not say. The tree can, and the server answers what the local engine answers.""" g = build() - tree = {"gt": {"lhs": read({"degree": "BOTH"}), "rhs": read({"degree": "IN"})}} + tree = node({"gt": {"lhs": {"degree": "BOTH"}, "rhs": {"degree": "IN"}}}) with graphql_client(g) as client: assert node_names(client, tree) == ["alice", "bob", "carol"] local = sorted(n.name for n in g.filter(f.Node.degree() > f.Node.in_degree()).nodes) @@ -86,15 +94,9 @@ def test_both_sides_of_a_comparison_are_expressions(): def test_views_belong_to_the_read(): """Inside [0, 5) alice's latest score is 7 and bob's is 5.""" g = build() - windowed = { - "read": { - "entity": "NODE", - "views": [{"window": {"start": 0, "end": 5}}], - "target": {"property": "score"}, - } - } - tree = {"gt": {"lhs": windowed, "rhs": const({"f64": 4.0})}} - plain = {"gt": {"lhs": read({"property": "score"}), "rhs": const({"f64": 4.0})}} + windowed = viewed([{"window": {"start": 0, "end": 5}}], {"property": "score"}) + tree = node({"gt": {"lhs": windowed, "rhs": const({"f64": 4.0})}}) + plain = node({"gt": {"lhs": {"property": "score"}, "rhs": const({"f64": 4.0})}}) with graphql_client(g) as client: assert node_names(client, tree) == ["alice", "bob"] assert node_names(client, plain) == ["alice"] @@ -102,10 +104,11 @@ def test_views_belong_to_the_read(): def test_temporal_aggregates_and_qualifiers(): g = build() - history = {"temporal": read({"property": "score"})} - total = {"gt": {"lhs": {"sum": history}, "rhs": const({"f64": 10.0})}} - any_high = {"gt": {"lhs": {"any": history}, "rhs": const({"f64": 4.0})}} - two_updates = {"eq": {"lhs": {"len": history}, "rhs": const({"u64": 2})}} + history = {"temporalProperty": "score"} + total = node({"gt": {"lhs": {"sum": history}, "rhs": const({"f64": 10.0})}}) + # The qualifier follows the comparison: one answer per update, any must hold. + any_high = node({"any": {"gt": {"lhs": history, "rhs": const({"f64": 4.0})}}}) + two_updates = node({"eq": {"lhs": {"len": history}, "rhs": const({"u64": 2})}}) with graphql_client(g) as client: assert node_names(client, total) == ["alice"] assert node_names(client, any_high) == ["alice", "bob"] @@ -117,15 +120,10 @@ def test_edge_reads_through_an_endpoint_keep_the_edge_views(): alice's latest score is 7, so asking for the later 9 matches nothing.""" g = build() src_score = { - "read": { - "entity": "EDGE", - "endpoint": "SRC", - "views": [{"window": {"start": 0, "end": 5}}], - "target": {"property": "score"}, - } + "src": viewed([{"window": {"start": 0, "end": 5}}], {"property": "score"}) } - late = {"eq": {"lhs": src_score, "rhs": const({"f64": 9.0})}} - early = {"eq": {"lhs": src_score, "rhs": const({"f64": 7.0})}} + late = edge({"eq": {"lhs": src_score, "rhs": const({"f64": 9.0})}}) + early = edge({"eq": {"lhs": src_score, "rhs": const({"f64": 7.0})}}) with graphql_client(g) as client: assert edge_pairs(client, late) == [] assert edge_pairs(client, early) == [("alice", "bob")] @@ -137,7 +135,7 @@ def test_a_view_leg_restricts_the_whole_filter(): outside [0, 2), so he is gone before the predicate runs.""" g = build() window = {"view": [{"window": {"start": 0, "end": 2}}]} - has_score = {"isSome": read({"property": "score"})} + has_score = node({"isSome": {"property": "score"}}) with graphql_client(g) as client: assert node_names(client, {"and": [window, has_score]}) == ["alice", "bob"] assert node_names(client, has_score) == ["alice", "bob", "dave"] @@ -148,7 +146,7 @@ def test_a_view_leg_restricts_the_whole_filter(): def test_structural_predicates_and_views(): g = build() - works = {"isActive": {"entity": "EDGE", "views": [{"layers": ["works"]}]}} + works = edge(viewed([{"layers": ["works"]}], {"isActive": True})) window_then_latest = { "view": [{"window": {"start": 0, "end": 5}}, {"latest": True}] } @@ -159,25 +157,29 @@ def test_structural_predicates_and_views(): def test_combinators_presence_and_membership(): g = build() - tree = { - "and": [ - {"isSome": read({"property": "score"})}, - { - "not": { - "startsWith": { - "lhs": read({"field": "NAME"}), - "rhs": const({"str": "a"}), + tree = node( + { + "and": [ + {"isSome": {"property": "score"}}, + { + "not": { + "startsWith": { + "lhs": {"field": "NAME"}, + "rhs": const({"str": "a"}), + } } - } - }, - ] - } - members = { - "isIn": { - "expr": read({"field": "NAME"}), - "values": {"list": [{"str": "alice"}, {"str": "dave"}]}, + }, + ] } - } + ) + members = node( + { + "isIn": { + "expr": {"field": "NAME"}, + "values": {"list": [{"str": "alice"}, {"str": "dave"}]}, + } + } + ) with graphql_client(g) as client: assert node_names(client, tree) == ["bob", "dave"] assert node_names(client, members) == ["alice", "dave"] diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py index 2acab1f716..20ee81de42 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py @@ -13,11 +13,21 @@ def test_filter_edges_with_str_ids_for_node_id_eq_gql(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: SRC } } - rhs: { const: { str: "3" } } - } - }) { + edge: { + eq: { + lhs: { + src: { + field: ID + } + } + rhs: { + const: { + str: "3" + } + } + } + } + }) { edges { list { src { name } @@ -53,11 +63,21 @@ def test_filter_edges_with_num_ids_for_node_id_eq_gql(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: SRC } } - rhs: { const: { u64: 1 } } - } - }) { + edge: { + eq: { + lhs: { + src: { + field: ID + } + } + rhs: { + const: { + u64: 1 + } + } + } + } + }) { edges { list { src { name } @@ -85,17 +105,35 @@ def test_edges_chained_selection_with_edge_filter(graph): graph(path: "g") { edges { select(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: DST } } - rhs: { const: { u64: 2 } } - } - }) { + edge: { + eq: { + lhs: { + dst: { + field: ID + } + } + rhs: { + const: { + u64: 2 + } + } + } + } + }) { select(expr: { - gt: { - lhs: { read: { entity: EDGE, target: { property: "p2" } } } - rhs: { const: { i64: 2 } } - } - }) { + edge: { + gt: { + lhs: { + property: "p2" + } + rhs: { + const: { + i64: 2 + } + } + } + } + }) { list { src { name } dst { name } } } } @@ -126,8 +164,20 @@ def test_edges_filter_window_is_active(graph): graph(path: "g") { edges { select(expr: { - isActive: { entity: EDGE, views: [{ window: { start: 1, end: 4 } }] } - }) { + edge: { + viewed: { + views: [{ + window: { + start: 1 + end: 4 + } + }] + expr: { + isActive: true + } + } + } + }) { list { src { name @@ -177,8 +227,20 @@ def test_edges_filter_window_is_deleted(graph, expected_edges): graph(path: "g") { edges { select(expr: { - isDeleted: { entity: EDGE, views: [{ window: { start: 1, end: 5 } }] } - }) { + edge: { + viewed: { + views: [{ + window: { + start: 1 + end: 5 + } + }] + expr: { + isDeleted: true + } + } + } + }) { list { src { name diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py index 8b78b364fd..7988181c98 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py @@ -18,11 +18,25 @@ def test_graph_edge_property_filter_equal(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { property: "eprop5" } } } - rhs: { const: { list: [{ i64: 1 }, { i64: 2 }, { i64: 3 }] } } - } - }) { + edge: { + eq: { + lhs: { + property: "eprop5" + } + rhs: { + const: { + list: [{ + i64: 1 + }, { + i64: 2 + }, { + i64: 3 + }] + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -44,11 +58,19 @@ def test_graph_edge_property_filter_equal_type_error(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { property: "eprop5" } } } - rhs: { const: { i64: 1 } } - } - }) { + edge: { + eq: { + lhs: { + property: "eprop5" + } + rhs: { + const: { + i64: 1 + } + } + } + } + }) { nodes { list { name } } } } @@ -64,11 +86,19 @@ def test_graph_edge_property_filter_not_equal(graph): query { graph(path: "g") { filterEdges: filter(expr: { - ne: { - lhs: { read: { entity: EDGE, target: { property: "eprop4" } } } - rhs: { const: { bool: true } } - } - }) { + edge: { + ne: { + lhs: { + property: "eprop4" + } + rhs: { + const: { + bool: true + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -90,11 +120,19 @@ def test_graph_edge_property_filter_not_equal_type_error(graph): query { graph(path: "g") { filterEdges: filter(expr: { - ne: { - lhs: { read: { entity: EDGE, target: { property: "eprop4" } } } - rhs: { const: { i64: 1 } } - } - }) { + edge: { + ne: { + lhs: { + property: "eprop4" + } + rhs: { + const: { + i64: 1 + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -112,11 +150,19 @@ def test_graph_edge_property_filter_greater_than_or_equal(graph): query { graph(path: "g") { filterEdges: filter(expr: { - ge: { - lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } - rhs: { const: { i64: 60 } } - } - }) { + edge: { + ge: { + lhs: { + property: "eprop1" + } + rhs: { + const: { + i64: 60 + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -138,11 +184,19 @@ def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): query { graph(path: "g") { filterEdges: filter(expr: { - ge: { - lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } - rhs: { const: { str: "shivam" } } - } - }) { + edge: { + ge: { + lhs: { + property: "eprop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -158,11 +212,19 @@ def test_graph_edge_property_filter_less_than_or_equal(graph): query { graph(path: "g") { filterEdges: filter(expr: { - le: { - lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } - rhs: { const: { i64: 30 } } - } - }) { + edge: { + le: { + lhs: { + property: "eprop1" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -189,11 +251,19 @@ def test_graph_edge_property_filter_less_than_or_equal_type_error(graph): query { graph(path: "g") { filterEdges: filter(expr: { - le: { - lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } - rhs: { const: { str: "shivam" } } - } - }) { + edge: { + le: { + lhs: { + property: "eprop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -209,11 +279,19 @@ def test_graph_edge_property_filter_greater_than(graph): query { graph(path: "g") { filterEdges: filter(expr: { - gt: { - lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } - rhs: { const: { i64: 30 } } - } - }) { + edge: { + gt: { + lhs: { + property: "eprop1" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -235,11 +313,19 @@ def test_graph_edge_property_filter_greater_than_type_error(graph): query { graph(path: "g") { filterEdges: filter(expr: { - gt: { - lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } - rhs: { const: { str: "shivam" } } - } - }) { + edge: { + gt: { + lhs: { + property: "eprop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -255,11 +341,19 @@ def test_graph_edge_property_filter_less_than(graph): query { graph(path: "g") { filterEdges: filter(expr: { - lt: { - lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } - rhs: { const: { i64: 30 } } - } - }) { + edge: { + lt: { + lhs: { + property: "eprop1" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -281,11 +375,19 @@ def test_graph_edge_property_filter_less_than_type_error(graph): query { graph(path: "g") { filterEdges: filter(expr: { - lt: { - lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } - rhs: { const: { str: "shivam" } } - } - }) { + edge: { + lt: { + lhs: { + property: "eprop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -300,7 +402,7 @@ def test_graph_edge_property_filter_is_none(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { isNone: { read: { entity: EDGE, target: { property: "eprop5" } } } }) { + filterEdges: filter(expr: { edge: { isNone: { property: "eprop5" } } }) { edges { list { src { name } dst { name } } } } } @@ -315,7 +417,7 @@ def test_graph_edge_property_filter_is_some(graph): query = """ query { graph(path: "g") { - filterEdges: filter(expr: { isSome: { read: { entity: EDGE, target: { property: "eprop5" } } } }) { + filterEdges: filter(expr: { edge: { isSome: { property: "eprop5" } } }) { edges { list { src { name } dst { name } } } } } @@ -343,11 +445,23 @@ def test_graph_edge_property_filter_is_in(graph): query { graph(path: "g") { filterEdges: filter(expr: { - isIn: { - expr: { read: { entity: EDGE, target: { property: "eprop1" } } } - values: { list: [{ i64: 10 }, { i64: 20 }, { i64: 30 }] } - } - }) { + edge: { + isIn: { + expr: { + property: "eprop1" + } + values: { + list: [{ + i64: 10 + }, { + i64: 20 + }, { + i64: 30 + }] + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -374,11 +488,17 @@ def test_graph_edge_property_filter_is_empty_list(graph): query { graph(path: "g") { filterEdges: filter(expr: { - isIn: { - expr: { read: { entity: EDGE, target: { property: "eprop1" } } } - values: { list: [] } - } - }) { + edge: { + isIn: { + expr: { + property: "eprop1" + } + values: { + list: [] + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -394,11 +514,17 @@ def test_graph_edge_property_filter_is_in_type_error(graph): query { graph(path: "g") { filterEdges: filter(expr: { - isIn: { - expr: { read: { entity: EDGE, target: { property: "eprop1" } } } - values: { str: "shivam" } - } - }) { + edge: { + isIn: { + expr: { + property: "eprop1" + } + values: { + str: "shivam" + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -416,11 +542,23 @@ def test_graph_edge_property_filter_is_not_in(graph): query { graph(path: "g") { filterEdges: filter(expr: { - isNotIn: { - expr: { read: { entity: EDGE, target: { property: "eprop1" } } } - values: { list: [{ i64: 10 }, { i64: 20 }, { i64: 30 }] } - } - }) { + edge: { + isNotIn: { + expr: { + property: "eprop1" + } + values: { + list: [{ + i64: 10 + }, { + i64: 20 + }, { + i64: 30 + }] + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -442,11 +580,17 @@ def test_graph_edge_property_filter_is_not_in_empty_list(graph): query { graph(path: "g") { filterEdges: filter(expr: { - isNotIn: { - expr: { read: { entity: EDGE, target: { property: "eprop1" } } } - values: { list: [] } - } - }) { + edge: { + isNotIn: { + expr: { + property: "eprop1" + } + values: { + list: [] + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -474,11 +618,17 @@ def test_graph_edge_property_filter_is_not_in_type_error(graph): query { graph(path: "g") { filterEdges: filter(expr: { - isNotIn: { - expr: { read: { entity: EDGE, target: { property: "eprop1" } } } - values: { str: "shivam" } - } - }) { + edge: { + isNotIn: { + expr: { + property: "eprop1" + } + values: { + str: "shivam" + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -496,13 +646,25 @@ def test_graph_edge_not_property_filter(graph): query { graph(path: "g") { filterEdges: filter(expr: { - not: { - eq: { - lhs: { read: { entity: EDGE, target: { property: "eprop5" } } } - rhs: { const: { list: [{ i64: 1 }, { i64: 2 }] } } - } - } - }) { + not: { + edge: { + eq: { + lhs: { + property: "eprop5" + } + rhs: { + const: { + list: [{ + i64: 1 + }, { + i64: 2 + }] + } + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -530,11 +692,19 @@ def test_edges_property_filter_starts_with(graph): query { graph(path: "g") { filterEdges: filter(expr: { - startsWith: { - lhs: { read: { entity: EDGE, target: { property: "eprop3" } } } - rhs: { const: { str: "xyz" } } - } - }) { + edge: { + startsWith: { + lhs: { + property: "eprop3" + } + rhs: { + const: { + str: "xyz" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -562,11 +732,19 @@ def test_edges_property_filter_ends_with(graph): query { graph(path: "g") { filterEdges: filter(expr: { - endsWith: { - lhs: { read: { entity: EDGE, target: { property: "eprop3" } } } - rhs: { const: { str: "123" } } - } - }) { + edge: { + endsWith: { + lhs: { + property: "eprop3" + } + rhs: { + const: { + str: "123" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -598,11 +776,19 @@ def test_edges_selection(graph): query { graph(path: "g") { edges(select: { - gt: { - lhs: { read: { entity: EDGE, target: { property: "p2" } } } - rhs: { const: { i64: 3 } } - } - }) { + edge: { + gt: { + lhs: { + property: "p2" + } + rhs: { + const: { + i64: 3 + } + } + } + } + }) { list { src { name } dst { name } } } } @@ -630,17 +816,33 @@ def test_edges_selection_edges_filter_paired(graph): query { graph(path: "g") { edges(select: { - gt: { - lhs: { read: { entity: EDGE, target: { property: "p2" } } } - rhs: { const: { i64: 3 } } - } - }) { + edge: { + gt: { + lhs: { + property: "p2" + } + rhs: { + const: { + i64: 3 + } + } + } + } + }) { filter(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { property: "p3" } } } - rhs: { const: { i64: 5 } } - } - }) { + edge: { + eq: { + lhs: { + property: "p3" + } + rhs: { + const: { + i64: 5 + } + } + } + } + }) { list { src { name } dst { name } } } } @@ -670,23 +872,49 @@ def test_edges_chained_selection_edges_filter_paired(graph): query { graph(path: "g") { edges(select: { - gt: { - lhs: { read: { entity: EDGE, target: { property: "p2" } } } - rhs: { const: { i64: 3 } } - } - }) { + edge: { + gt: { + lhs: { + property: "p2" + } + rhs: { + const: { + i64: 3 + } + } + } + } + }) { select(expr: { - lt: { - lhs: { read: { entity: EDGE, target: { property: "p2" } } } - rhs: { const: { i64: 5 } } - } - }) { + edge: { + lt: { + lhs: { + property: "p2" + } + rhs: { + const: { + i64: 5 + } + } + } + } + }) { filter(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: DST } } - rhs: { const: { u64: 2 } } - } - }) { + edge: { + eq: { + lhs: { + dst: { + field: ID + } + } + rhs: { + const: { + u64: 2 + } + } + } + } + }) { list { src { name } dst { name } } } } @@ -713,23 +941,49 @@ def test_edges_chained_selection_edges_filter_paired_ver2(graph): graph(path: "g") { edges { select(expr: { - gt: { - lhs: { read: { entity: EDGE, target: { property: "p2" } } } - rhs: { const: { i64: 3 } } - } - }) { + edge: { + gt: { + lhs: { + property: "p2" + } + rhs: { + const: { + i64: 3 + } + } + } + } + }) { select(expr: { - lt: { - lhs: { read: { entity: EDGE, target: { property: "p2" } } } - rhs: { const: { i64: 5 } } - } - }) { + edge: { + lt: { + lhs: { + property: "p2" + } + rhs: { + const: { + i64: 5 + } + } + } + } + }) { filter(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { field: ID }, endpoint: DST } } - rhs: { const: { u64: 2 } } - } - }) { + edge: { + eq: { + lhs: { + dst: { + field: ID + } + } + rhs: { + const: { + u64: 2 + } + } + } + } + }) { list { src { name } dst { name } } } } @@ -760,17 +1014,28 @@ def test_edge_temporal_property_filter_empty_layers(graph): query { graph(path: "g") { filterEdges: filter(expr: { - lt: { - lhs: { - avg: { - temporal: { - read: { entity: EDGE, target: { property: "p2" }, views: [{ layers: [] }] } - } - } - } - rhs: { const: { f64: 1.0 } } - } - }) { + edge: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + layers: [] + }] + expr: { + temporalProperty: "p2" + } + } + } + } + rhs: { + const: { + f64: 1.0 + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -787,21 +1052,28 @@ def test_edges_temporal_property_last_with_single_layer(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { - last: { - temporal: { - read: { - entity: EDGE - target: { property: "p10" } - views: [{ layers: ["air_nomads"] }] - } - } - } - } - rhs: { const: { str: "Paper_ship" } } - } - }) { + edge: { + eq: { + lhs: { + last: { + viewed: { + views: [{ + layers: ["air_nomads"] + }] + expr: { + temporalProperty: "p10" + } + } + } + } + rhs: { + const: { + str: "Paper_ship" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -826,21 +1098,28 @@ def test_edges_temporal_property_last_with_multiple_layers(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { - last: { - temporal: { - read: { - entity: EDGE - target: { property: "p10" } - views: [{ layers: ["fire_nation", "air_nomads"] }] - } - } - } - } - rhs: { const: { str: "Paper_airplane" } } - } - }) { + edge: { + eq: { + lhs: { + last: { + viewed: { + views: [{ + layers: ["fire_nation", "air_nomads"] + }] + expr: { + temporalProperty: "p10" + } + } + } + } + rhs: { + const: { + str: "Paper_airplane" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -864,21 +1143,28 @@ def test_edges_temporal_property_last_with_default_layer(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { - last: { - temporal: { - read: { - entity: EDGE - target: { property: "p10" } - views: [{ layers: ["_default"] }] - } - } - } - } - rhs: { const: { str: "Paper_airplane" } } - } - }) { + edge: { + eq: { + lhs: { + last: { + viewed: { + views: [{ + layers: ["_default"] + }] + expr: { + temporalProperty: "p10" + } + } + } + } + rhs: { + const: { + str: "Paper_airplane" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -902,17 +1188,28 @@ def test_edges_at_temporal_last(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { - last: { - temporal: { - read: { entity: EDGE, target: { property: "p10" }, views: [{ at: 1 }] } - } - } - } - rhs: { const: { str: "Paper_airplane" } } - } - }) { + edge: { + eq: { + lhs: { + last: { + viewed: { + views: [{ + at: 1 + }] + expr: { + temporalProperty: "p10" + } + } + } + } + rhs: { + const: { + str: "Paper_airplane" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -935,17 +1232,28 @@ def test_edges_before_temporal_last(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { - last: { - temporal: { - read: { entity: EDGE, target: { property: "p10" }, views: [{ before: 2 }] } - } - } - } - rhs: { const: { str: "Paper_airplane" } } - } - }) { + edge: { + eq: { + lhs: { + last: { + viewed: { + views: [{ + before: 2 + }] + expr: { + temporalProperty: "p10" + } + } + } + } + rhs: { + const: { + str: "Paper_airplane" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -968,17 +1276,28 @@ def test_edges_after_temporal_last(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { - last: { - temporal: { - read: { entity: EDGE, target: { property: "p10" }, views: [{ after: 2 }] } - } - } - } - rhs: { const: { str: "Paper_ship" } } - } - }) { + edge: { + eq: { + lhs: { + last: { + viewed: { + views: [{ + after: 2 + }] + expr: { + temporalProperty: "p10" + } + } + } + } + rhs: { + const: { + str: "Paper_ship" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -1001,17 +1320,28 @@ def test_edges_latest_temporal_last(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { - last: { - temporal: { - read: { entity: EDGE, target: { property: "p10" }, views: [{ latest: true }] } - } - } - } - rhs: { const: { str: "Paper_ship" } } - } - }) { + edge: { + eq: { + lhs: { + last: { + viewed: { + views: [{ + latest: true + }] + expr: { + temporalProperty: "p10" + } + } + } + } + rhs: { + const: { + str: "Paper_ship" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -1033,21 +1363,28 @@ def test_edges_snapshot_at_temporal_last(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { - last: { - temporal: { - read: { - entity: EDGE - target: { property: "p10" } - views: [{ snapshotAt: 2 }] - } - } - } - } - rhs: { const: { str: "Paper_ship" } } - } - }) { + edge: { + eq: { + lhs: { + last: { + viewed: { + views: [{ + snapshotAt: 2 + }] + expr: { + temporalProperty: "p10" + } + } + } + } + rhs: { + const: { + str: "Paper_ship" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } @@ -1069,21 +1406,28 @@ def test_edges_snapshot_latest_temporal_last(graph): query { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { - last: { - temporal: { - read: { - entity: EDGE - target: { property: "p10" } - views: [{ snapshotLatest: true }] - } - } - } - } - rhs: { const: { str: "Paper_ship" } } - } - }) { + edge: { + eq: { + lhs: { + last: { + viewed: { + views: [{ + snapshotLatest: true + }] + expr: { + temporalProperty: "p10" + } + } + } + } + rhs: { + const: { + str: "Paper_ship" + } + } + } + } + }) { edges { list { src { name } dst { name } } } } } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index 081178ed3f..9b0c08829d 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -13,11 +13,25 @@ def test_graph_node_property_filter_equal(graph): query { graph(path: "g") { filterNodes: filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "prop5" } } } - rhs: { const: { list: [{ i64: 1 }, { i64: 2 }, { i64: 3 }] } } - } - }) { + node: { + eq: { + lhs: { + property: "prop5" + } + rhs: { + const: { + list: [{ + i64: 1 + }, { + i64: 2 + }, { + i64: 3 + }] + } + } + } + } + }) { nodes { list { name } } } } @@ -33,11 +47,19 @@ def test_graph_node_property_filter_equal_type_error(graph): query { graph(path: "g") { filterNodes: filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "prop5" } } } - rhs: { const: { i64: 1 } } - } - }) { + node: { + eq: { + lhs: { + property: "prop5" + } + rhs: { + const: { + i64: 1 + } + } + } + } + }) { nodes { list { name } } } } @@ -53,11 +75,19 @@ def test_graph_node_property_filter_not_equal(graph): query { graph(path: "g") { filterNodes: filter(expr: { - ne: { - lhs: { read: { entity: NODE, target: { property: "prop4" } } } - rhs: { const: { bool: true } } - } - }) { + node: { + ne: { + lhs: { + property: "prop4" + } + rhs: { + const: { + bool: true + } + } + } + } + }) { nodes { list { name } } } } @@ -75,11 +105,19 @@ def test_graph_node_property_filter_not_equal_type_error(graph): query { graph(path: "g") { filterNodes: filter(expr: { - ne: { - lhs: { read: { entity: NODE, target: { property: "prop4" } } } - rhs: { const: { i64: 1 } } - } - }) { + node: { + ne: { + lhs: { + property: "prop4" + } + rhs: { + const: { + i64: 1 + } + } + } + } + }) { nodes { list { name } } } } @@ -97,11 +135,19 @@ def test_graph_node_property_filter_greater_than_or_equal(graph): query { graph(path: "g") { filterNodes: filter(expr: { - ge: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 60 } } - } - }) { + node: { + ge: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 60 + } + } + } + } + }) { nodes { list { name } } } } @@ -117,11 +163,19 @@ def test_graph_node_property_filter_greater_than_or_equal_type_error(graph): query { graph(path: "g") { filterNodes: filter(expr: { - ge: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { str: "shivam" } } - } - }) { + node: { + ge: { + lhs: { + property: "prop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + }) { nodes { list { name } } } } @@ -137,11 +191,19 @@ def test_graph_node_property_filter_less_than_or_equal(graph): query { graph(path: "g") { filterNodes: filter(expr: { - le: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + le: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { nodes { list { name } } } } @@ -163,11 +225,19 @@ def test_graph_node_property_filter_less_than_or_equal_type_error(graph): query { graph(path: "g") { filterNodes: filter(expr: { - le: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { str: "shivam" } } - } - }) { + node: { + le: { + lhs: { + property: "prop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + }) { nodes { list { name } } } } @@ -183,11 +253,19 @@ def test_graph_node_property_filter_greater_than(graph): query { graph(path: "g") { filterNodes: filter(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { nodes { list { name } } } } @@ -203,11 +281,19 @@ def test_graph_node_property_filter_greater_than_type_error(graph): query { graph(path: "g") { filterNodes: filter(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { str: "shivam" } } - } - }) { + node: { + gt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + }) { nodes { list { name } } } } @@ -223,11 +309,19 @@ def test_graph_node_property_filter_less_than(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + lt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { nodes { list { name } } } } @@ -245,11 +339,19 @@ def test_graph_node_property_filter_less_than_type_error(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { str: "shivam" } } - } - }) { + node: { + lt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + }) { nodes { list { name } } } } @@ -264,7 +366,7 @@ def test_graph_node_property_filter_is_none(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { isNone: { read: { entity: NODE, target: { property: "prop5" } } } }) { + filterNodes: filter(expr: { node: { isNone: { property: "prop5" } } }) { nodes { list { name } } } } @@ -281,7 +383,7 @@ def test_graph_node_property_filter_is_some(graph): query = """ query { graph(path: "g") { - filterNodes: filter(expr: { isSome: { read: { entity: NODE, target: { property: "prop5" } } } }) { + filterNodes: filter(expr: { node: { isSome: { property: "prop5" } } }) { nodes { list { name } } } } @@ -299,11 +401,25 @@ def test_graph_node_property_filter_is_in(graph): query { graph(path: "g") { filterNodes: filter(expr: { - isIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [{ i64: 10 }, { i64: 30 }, { i64: 50 }, { i64: 70 }] } - } - }) { + node: { + isIn: { + expr: { + property: "prop1" + } + values: { + list: [{ + i64: 10 + }, { + i64: 30 + }, { + i64: 50 + }, { + i64: 70 + }] + } + } + } + }) { nodes { list { name } } } } @@ -322,11 +438,17 @@ def test_node_property_filter_is_in_empty_list(graph): graph(path: "g") { nodes { select(expr: { - isIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [] } - } - } + node: { + isIn: { + expr: { + property: "prop1" + } + values: { + list: [] + } + } + } + } ) { list { name } } @@ -345,11 +467,17 @@ def test_graph_node_property_filter_is_in_no_value(graph): query { graph(path: "g") { filterNodes: filter(expr: { - isIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [] } - } - }) { + node: { + isIn: { + expr: { + property: "prop1" + } + values: { + list: [] + } + } + } + }) { nodes { list { name } } } } @@ -365,11 +493,17 @@ def test_graph_node_property_filter_is_in_type_error(graph): query { graph(path: "g") { filterNodes: filter(expr: { - isIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { str: "shivam" } - } - }) { + node: { + isIn: { + expr: { + property: "prop1" + } + values: { + str: "shivam" + } + } + } + }) { nodes { list { name } } } } @@ -387,11 +521,25 @@ def test_graph_node_property_filter_is_not_in_any(graph): query { graph(path: "g") { filterNodes: filter(expr: { - isNotIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [{ i64: 10 }, { i64: 30 }, { i64: 50 }, { i64: 70 }] } - } - }) { + node: { + isNotIn: { + expr: { + property: "prop1" + } + values: { + list: [{ + i64: 10 + }, { + i64: 30 + }, { + i64: 50 + }, { + i64: 70 + }] + } + } + } + }) { nodes { list { name } } } } @@ -410,11 +558,17 @@ def test_node_property_filter_not_is_not_in_empty_list(graph): graph(path: "g") { nodes { filter(expr: { - isNotIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [] } - } - }) { + node: { + isNotIn: { + expr: { + property: "prop1" + } + values: { + list: [] + } + } + } + }) { list { name } } } @@ -439,11 +593,17 @@ def test_graph_node_property_filter_is_not_in_type_error(graph): query { graph(path: "g") { filterNodes: filter(expr: { - isNotIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { str: "shivam" } - } - }) { + node: { + isNotIn: { + expr: { + property: "prop1" + } + values: { + str: "shivam" + } + } + } + }) { nodes { list { name } } } } @@ -461,13 +621,25 @@ def test_graph_node_not_property_filter(graph): query { graph(path: "g") { filterNodes: filter(expr: { - not: { - eq: { - lhs: { read: { entity: NODE, target: { property: "prop5" } } } - rhs: { const: { list: [{ i64: 1 }, { i64: 2 }] } } - } - } - }) { + not: { + node: { + eq: { + lhs: { + property: "prop5" + } + rhs: { + const: { + list: [{ + i64: 1 + }, { + i64: 2 + }] + } + } + } + } + } + }) { nodes { list { name } } } } @@ -492,21 +664,36 @@ def test_graph_node_type_and_property_filter(graph): graph(path: "g") { nodes { select(expr: { - and: [ - { - isIn: { - expr: { read: { entity: NODE, target: { field: NODE_TYPE } } } - values: { list: [{ str: "fire_nation" }, { str: "water_tribe" }] } - } - }, - { - gt: { - lhs: { read: { entity: NODE, target: { property: "prop2" } } } - rhs: { const: { f64: 1 } } - } - } - ] - }) { + and: [{ + node: { + isIn: { + expr: { + field: NODE_TYPE + } + values: { + list: [{ + str: "fire_nation" + }, { + str: "water_tribe" + }] + } + } + } + }, { + node: { + gt: { + lhs: { + property: "prop2" + } + rhs: { + const: { + f64: 1 + } + } + } + } + }] + }) { count list { name } } @@ -533,11 +720,19 @@ def test_graph_nodes_property_filter_starts_with(graph): query { graph(path: "g") { filterNodes: filter(expr: { - startsWith: { - lhs: { read: { entity: NODE, target: { property: "prop3" } } } - rhs: { const: { str: "abc" } } - } - }) { + node: { + startsWith: { + lhs: { + property: "prop3" + } + rhs: { + const: { + str: "abc" + } + } + } + } + }) { nodes { list { name } } } } @@ -561,11 +756,19 @@ def test_graph_nodes_property_filter_ends_with(graph): query { graph(path: "g") { filterNodes: filter(expr: { - endsWith: { - lhs: { read: { entity: NODE, target: { property: "prop3" } } } - rhs: { const: { str: "123" } } - } - }) { + node: { + endsWith: { + lhs: { + property: "prop3" + } + rhs: { + const: { + str: "123" + } + } + } + } + }) { nodes { list { name } } } } @@ -581,13 +784,21 @@ def test_graph_nodes_property_filter_starts_with_temporal_any(graph): query { graph(path: "g") { filterNodes: filter(expr: { - startsWith: { - lhs: { - any: { temporal: { read: { entity: NODE, target: { property: "prop3" } } } } - } - rhs: { const: { str: "abc1" } } - } - }) { + node: { + any: { + startsWith: { + lhs: { + temporalProperty: "prop3" + } + rhs: { + const: { + str: "abc1" + } + } + } + } + } + }) { nodes { list { name } } } } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py index 47f4b40adf..bd5d7d8bb3 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py @@ -14,21 +14,34 @@ def test_out_neighbours_found(graph): graph(path: "g") { node(name: "a") { filter(expr: { - and: [ - { - eq: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "d" } } - } - }, - { - gt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 10 } } - } - } - ] - }) { + and: [{ + node: { + eq: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "d" + } + } + } + } + }, { + node: { + gt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 10 + } + } + } + } + }] + }) { outNeighbours { list { name } } @@ -50,27 +63,48 @@ def test_out_neighbours_found_select(graph): graph(path: "g") { node(name: "a") { filter(expr: { - and: [ - { - eq: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "d" } } - } - }, - { - gt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 10 } } - } - } - ] - }) { + and: [{ + node: { + eq: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "d" + } + } + } + } + }, { + node: { + gt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 10 + } + } + } + } + }] + }) { outNeighbours(select: { - eq: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "d" } } - } - }) { + node: { + eq: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "d" + } + } + } + } + }) { list { name } } } @@ -91,11 +125,19 @@ def test_out_neighbours_not_found(graph): graph(path: "g") { node(name: "a") { filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "e" } } - } - }) { + node: { + eq: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "e" + } + } + } + } + }) { outNeighbours { list { name } } @@ -115,11 +157,19 @@ def test_in_neighbours_found(graph): graph(path: "g") { node(name: "d") { filter(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 10 } } - } - }) { + node: { + gt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 10 + } + } + } + } + }) { inNeighbours { list { name } } @@ -145,17 +195,33 @@ def test_in_neighbours_found_select(graph): graph(path: "g") { node(name: "d") { filter(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 10 } } - } - }) { + node: { + gt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 10 + } + } + } + } + }) { inNeighbours(select: { - eq: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "c" } } - } - }) { + node: { + eq: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "c" + } + } + } + } + }) { list { name } } } @@ -176,11 +242,19 @@ def test_in_neighbours_not_found(graph): graph(path: "g") { node(name: "d") { filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "e" } } - } - }) { + node: { + eq: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "e" + } + } + } + } + }) { inNeighbours { list { name } } @@ -200,11 +274,19 @@ def test_neighbours_found(graph): graph(path: "g") { node(name: "d") { filter(expr: { - ne: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "a" } } - } - }) { + node: { + ne: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "a" + } + } + } + } + }) { neighbours { list { name } } @@ -228,17 +310,33 @@ def test_neighbours_found_select(graph): graph(path: "g") { node(name: "d") { filter(expr: { - ne: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "a" } } - } - }) { + node: { + ne: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "a" + } + } + } + } + }) { neighbours(select: { - eq: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "b" } } - } - }) { + node: { + eq: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "b" + } + } + } + } + }) { list { name } } } @@ -259,11 +357,19 @@ def test_neighbours_not_found(graph): graph(path: "g") { node(name: "d") { filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { field: NAME } } } - rhs: { const: { str: "e" } } - } - }) { + node: { + eq: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "e" + } + } + } + } + }) { neighbours { list { name } } @@ -286,19 +392,35 @@ def test_neighbours_selection(graph): query { graph(path: "g") { nodes(select: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p100" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "p100" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { list { neighbours { select(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p2" } } } - rhs: { const: { i64: 3 } } - } - }) { + node: { + gt: { + lhs: { + property: "p2" + } + rhs: { + const: { + i64: 3 + } + } + } + } + }) { list { name } @@ -328,20 +450,36 @@ def test_neighbours_neighbours_filtering(graph): query { graph(path: "g") { nodes(select: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p100" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "p100" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { list { name neighbours { filter(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p2" } } } - rhs: { const: { i64: 3 } } - } - }) { + node: { + gt: { + lhs: { + property: "p2" + } + rhs: { + const: { + i64: 3 + } + } + } + } + }) { list { name neighbours { diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py index dda51b8577..fd8051ef1a 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py @@ -22,11 +22,19 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql(graph): query { graph(path: "g") { filterNodes: filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { field: ID } } } - rhs: { const: { str: "1" } } - } - }) { + node: { + eq: { + lhs: { + field: ID + } + rhs: { + const: { + str: "1" + } + } + } + } + }) { nodes { list { name } } @@ -63,11 +71,19 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql2(graph): query { graph(path: "g") { filterNodes: filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { field: ID } } } - rhs: { const: { u64: 1 } } - } - }) { + node: { + eq: { + lhs: { + field: ID + } + rhs: { + const: { + u64: 1 + } + } + } + } + }) { nodes { list { name } } @@ -91,11 +107,19 @@ def test_filter_nodes_with_num_ids_for_node_id_eq_gql(graph): query { graph(path: "g") { filterNodes: filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { field: ID } } } - rhs: { const: { u64: 1 } } - } - }) { + node: { + eq: { + lhs: { + field: ID + } + rhs: { + const: { + u64: 1 + } + } + } + } + }) { nodes { list { name } } @@ -114,23 +138,47 @@ def test_nodes_chained_selection_with_node_filter(graph): graph(path: "g") { nodes { select(expr: { - eq: { - lhs: { read: { entity: NODE, target: { field: NODE_TYPE } } } - rhs: { const: { str: "fire_nation" } } - } - }) { + node: { + eq: { + lhs: { + field: NODE_TYPE + } + rhs: { + const: { + str: "fire_nation" + } + } + } + } + }) { select(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "p9" } } } - rhs: { const: { i64: 5 } } - } - }) { + node: { + eq: { + lhs: { + property: "p9" + } + rhs: { + const: { + i64: 5 + } + } + } + } + }) { filter(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p100" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "p100" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { list { name } @@ -156,8 +204,20 @@ def test_nodes_filter_windowed_is_active(graph): graph(path: "g") { nodes { select(expr: { - isActive: { entity: NODE, views: [{ window: { start: 1, end: 4 } }] } - }) { + node: { + viewed: { + views: [{ + window: { + start: 1 + end: 4 + } + }] + expr: { + isActive: true + } + } + } + }) { list { name } @@ -186,10 +246,22 @@ def test_nodes_filter_windowed_is_not_active(graph): graph(path: "g") { nodes { select(expr: { - not: { - isActive: { entity: NODE, views: [{ window: { start: 1, end: 4 } }] } - } - }) { + not: { + node: { + viewed: { + views: [{ + window: { + start: 1 + end: 4 + } + }] + expr: { + isActive: true + } + } + } + } + }) { list { name } @@ -237,17 +309,22 @@ def _expected_degree_select_names(graph, direction, predicate): def _degree(direction, op, value=None, over=None): - """A degree predicate in the tree grammar: `degree(direction) value`, the degree - optionally wrapped in an aggregate or qualifier (`over`) so invalid chains can be spelled. + """A degree predicate in the tree grammar: `degree(direction) value`. `over` wraps + the degree in an aggregate, or the comparison in a qualifier, so invalid chains can be + spelled. """ - lhs = f"{{ read: {{ entity: NODE, target: {{ degree: {direction} }} }} }}" - if over: + lhs = f"{{ degree: {direction} }}" + if over in ("sum", "avg", "min", "max", "first", "last", "len"): lhs = f"{{ {over}: {lhs} }}" if op in ("isSome", "isNone"): - return f"{{ {op}: {lhs} }}" - if op in ("isIn", "isNotIn"): - return f"{{ {op}: {{ expr: {lhs}, values: {value} }} }}" - return f"{{ {op}: {{ lhs: {lhs}, rhs: {{ const: {value} }} }} }}" + pred = f"{{ {op}: {lhs} }}" + elif op in ("isIn", "isNotIn"): + pred = f"{{ {op}: {{ expr: {lhs}, values: {value} }} }}" + else: + pred = f"{{ {op}: {{ lhs: {lhs}, rhs: {{ const: {value} }} }} }}" + if over in ("any", "all"): + pred = f"{{ {over}: {pred} }}" + return f"{{ node: {pred} }}" def _degree_filter_nodes_query_expected_pair(expr, expected_names): diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py index 634a7e6cec..a2369ec552 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py @@ -24,11 +24,25 @@ def test_node_property_filter_equal2(graph): graph(path: "g") { nodes { filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "prop5" } } } - rhs: { const: { list: [{ i64: 1 }, { i64: 2 }, { i64: 3 }] } } - } - }) { + node: { + eq: { + lhs: { + property: "prop5" + } + rhs: { + const: { + list: [{ + i64: 1 + }, { + i64: 2 + }, { + i64: 3 + }] + } + } + } + } + }) { list { name neighbours { @@ -66,11 +80,25 @@ def test_node_property_filter_equal3(graph): graph(path: "g") { nodes { select(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "prop5" } } } - rhs: { const: { list: [{ i64: 1 }, { i64: 2 }, { i64: 3 }] } } - } - } + node: { + eq: { + lhs: { + property: "prop5" + } + rhs: { + const: { + list: [{ + i64: 1 + }, { + i64: 2 + }, { + i64: 3 + }] + } + } + } + } + } ) { list { name } } @@ -89,11 +117,19 @@ def test_node_property_filter_equal_type_error(graph): graph(path: "g") { nodes { select(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "prop5" } } } - rhs: { const: { i64: 1 } } - } - } + node: { + eq: { + lhs: { + property: "prop5" + } + rhs: { + const: { + i64: 1 + } + } + } + } + } ) { list { name @@ -114,11 +150,19 @@ def test_node_property_filter_not_equal(graph): graph(path: "g") { nodes { select(expr: { - ne: { - lhs: { read: { entity: NODE, target: { property: "prop4" } } } - rhs: { const: { bool: true } } - } - } + node: { + ne: { + lhs: { + property: "prop4" + } + rhs: { + const: { + bool: true + } + } + } + } + } ) { list { name @@ -141,11 +185,19 @@ def test_node_property_filter_not_equal_type_error(graph): graph(path: "g") { nodes { select(expr: { - ne: { - lhs: { read: { entity: NODE, target: { property: "prop4" } } } - rhs: { const: { i64: 1 } } - } - } + node: { + ne: { + lhs: { + property: "prop4" + } + rhs: { + const: { + i64: 1 + } + } + } + } + } ) { list { name @@ -168,11 +220,19 @@ def test_node_property_filter_greater_than_or_equal(graph): graph(path: "g") { nodes { select(expr: { - ge: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 60 } } - } - } + node: { + ge: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 60 + } + } + } + } + } ) { list { name @@ -193,11 +253,19 @@ def test_node_property_filter_greater_than_or_equal_type_error(graph): graph(path: "g") { nodes { select(expr: { - ge: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { str: "shivam" } } - } - } + node: { + ge: { + lhs: { + property: "prop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + } ) { list { name @@ -218,11 +286,19 @@ def test_node_property_filter_less_than_or_equal(graph): graph(path: "g") { nodes { select(expr: { - le: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 30 } } - } - } + node: { + le: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 30 + } + } + } + } + } ) { list { name @@ -247,11 +323,19 @@ def test_node_property_filter_less_than_or_equal_type_error(graph): graph(path: "g") { nodes { select(expr: { - le: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { str: "shivam" } } - } - } + node: { + le: { + lhs: { + property: "prop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + } ) { list { name } } @@ -270,11 +354,19 @@ def test_node_property_filter_greater_than(graph): graph(path: "g") { nodes { select(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 30 } } - } - } + node: { + gt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 30 + } + } + } + } + } ) { list { name } } @@ -293,11 +385,19 @@ def test_node_property_filter_greater_than_type_error(graph): graph(path: "g") { nodes { select(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { str: "shivam" } } - } - } + node: { + gt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + } ) { list { name } } @@ -316,11 +416,19 @@ def test_node_property_filter_less_than(graph): graph(path: "g") { nodes { select(expr: { - lt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { i64: 30 } } - } - } + node: { + lt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + i64: 30 + } + } + } + } + } ) { list { name } } @@ -341,11 +449,19 @@ def test_node_property_filter_less_than_type_error(graph): graph(path: "g") { nodes { select(expr: { - lt: { - lhs: { read: { entity: NODE, target: { property: "prop1" } } } - rhs: { const: { str: "shivam" } } - } - } + node: { + lt: { + lhs: { + property: "prop1" + } + rhs: { + const: { + str: "shivam" + } + } + } + } + } ) { list { name } } @@ -363,7 +479,7 @@ def test_node_property_filter_is_none(graph): query { graph(path: "g") { nodes { - select(expr: { isNone: { read: { entity: NODE, target: { property: "prop5" } } } } + select(expr: { node: { isNone: { property: "prop5" } } } ) { list { name } } @@ -383,7 +499,7 @@ def test_node_property_filter_is_some(graph): query { graph(path: "g") { nodes { - select(expr: { isSome: { read: { entity: NODE, target: { property: "prop5" } } } } + select(expr: { node: { isSome: { property: "prop5" } } } ) { list { name } } @@ -404,11 +520,25 @@ def test_node_property_filter_is_in(graph): graph(path: "g") { nodes { select(expr: { - isIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [{ i64: 10 }, { i64: 30 }, { i64: 50 }, { i64: 70 }] } - } - } + node: { + isIn: { + expr: { + property: "prop1" + } + values: { + list: [{ + i64: 10 + }, { + i64: 30 + }, { + i64: 50 + }, { + i64: 70 + }] + } + } + } + } ) { list { name } } @@ -429,11 +559,17 @@ def test_node_property_filter_is_in_empty_list(graph): graph(path: "g") { nodes { select(expr: { - isIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [] } - } - } + node: { + isIn: { + expr: { + property: "prop1" + } + values: { + list: [] + } + } + } + } ) { list { name } } @@ -453,11 +589,19 @@ def test_node_property_filter_is_in_no_value(graph): graph(path: "g") { nodes { select(expr: { - isIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [{ i64: 100 }] } - } - } + node: { + isIn: { + expr: { + property: "prop1" + } + values: { + list: [{ + i64: 100 + }] + } + } + } + } ) { list { name } } @@ -476,11 +620,17 @@ def test_node_property_filter_is_in_type_error(graph): graph(path: "g") { nodes { select(expr: { - isIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { str: "shivam" } - } - } + node: { + isIn: { + expr: { + property: "prop1" + } + values: { + str: "shivam" + } + } + } + } ) { list { name } } @@ -501,11 +651,25 @@ def test_node_property_filter_is_not_in(graph): graph(path: "g") { nodes { select(expr: { - isNotIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [{ i64: 10 }, { i64: 30 }, { i64: 50 }, { i64: 70 }] } - } - } + node: { + isNotIn: { + expr: { + property: "prop1" + } + values: { + list: [{ + i64: 10 + }, { + i64: 30 + }, { + i64: 50 + }, { + i64: 70 + }] + } + } + } + } ) { list { name } } @@ -526,11 +690,17 @@ def test_node_property_filter_is_not_in_empty_list(graph): graph(path: "g") { nodes { select(expr: { - isNotIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { list: [] } - } - } + node: { + isNotIn: { + expr: { + property: "prop1" + } + values: { + list: [] + } + } + } + } ) { list { name } } @@ -557,11 +727,17 @@ def test_node_property_filter_is_not_in_type_error(graph): graph(path: "g") { nodes { select(expr: { - isNotIn: { - expr: { read: { entity: NODE, target: { property: "prop1" } } } - values: { str: "shivam" } - } - } + node: { + isNotIn: { + expr: { + property: "prop1" + } + values: { + str: "shivam" + } + } + } + } ) { list { name } } @@ -581,11 +757,19 @@ def test_node_property_filter_contains_wrong_value_type_error(graph): query { graph(path: "g") { filterNodes: filter(expr: { - contains: { - lhs: { read: { entity: NODE, target: { property: "p10" } } } - rhs: { const: { u64: 2 } } - } - }) { + node: { + contains: { + lhs: { + property: "p10" + } + rhs: { + const: { + u64: 2 + } + } + } + } + }) { nodes { list { name } } @@ -604,11 +788,19 @@ def test_nodes_property_filter_starts_with(graph): graph(path: "g") { nodes { select(expr: { - startsWith: { - lhs: { read: { entity: NODE, target: { property: "prop3" } } } - rhs: { const: { str: "abc" } } - } - }) { + node: { + startsWith: { + lhs: { + property: "prop3" + } + rhs: { + const: { + str: "abc" + } + } + } + } + }) { list { name } } } @@ -634,11 +826,19 @@ def test_nodes_property_filter_ends_with(graph): graph(path: "g") { nodes { select(expr: { - endsWith: { - lhs: { read: { entity: NODE, target: { property: "prop3" } } } - rhs: { const: { str: "333" } } - } - }) { + node: { + endsWith: { + lhs: { + property: "prop3" + } + rhs: { + const: { + str: "333" + } + } + } + } + }) { list { name } } } @@ -656,13 +856,21 @@ def test_nodes_property_filter_temporal_first_starts_with(graph): graph(path: "g") { nodes { select(expr: { - startsWith: { - lhs: { - first: { temporal: { read: { entity: NODE, target: { property: "prop3" } } } } - } - rhs: { const: { str: "abc" } } - } - }) { + node: { + startsWith: { + lhs: { + first: { + temporalProperty: "prop3" + } + } + rhs: { + const: { + str: "abc" + } + } + } + } + }) { list { name } } } @@ -688,13 +896,21 @@ def test_nodes_property_filter_temporal_all_starts_with(graph): graph(path: "g") { nodes { select(expr: { - startsWith: { - lhs: { - any: { temporal: { read: { entity: NODE, target: { property: "prop3" } } } } - } - rhs: { const: { str: "abc1" } } - } - }) { + node: { + any: { + startsWith: { + lhs: { + temporalProperty: "prop3" + } + rhs: { + const: { + str: "abc1" + } + } + } + } + } + }) { list { name } } } @@ -712,11 +928,21 @@ def test_nodes_property_filter_list_agg(graph): query { graph(path: "g") { filterNodes: filter(expr: { - eq: { - lhs: { sum: { read: { entity: NODE, target: { property: "prop5" } } } } - rhs: { const: { i64: 6 } } - } - }) { + node: { + eq: { + lhs: { + sum: { + property: "prop5" + } + } + rhs: { + const: { + i64: 6 + } + } + } + } + }) { nodes { list { name } } } } @@ -732,11 +958,21 @@ def test_nodes_property_filter_list_qualifier(graph): query { graph(path: "g") { filterNodes: filter(expr: { - eq: { - lhs: { any: { read: { entity: NODE, target: { property: "prop5" } } } } - rhs: { const: { i64: 6 } } - } - }) { + node: { + any: { + eq: { + lhs: { + property: "prop5" + } + rhs: { + const: { + i64: 6 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -756,13 +992,21 @@ def test_nodes_temporal_property_filter_agg(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { temporal: { read: { entity: NODE, target: { property: "p2" } } } } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + lt: { + lhs: { + avg: { + temporalProperty: "p2" + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + }) { nodes { list { name } } } } @@ -785,15 +1029,23 @@ def test_nodes_temporal_property_filter_any_avg(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { temporal: { read: { entity: NODE, target: { property: "prop5" } } } } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + temporalProperty: "prop5" + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -815,11 +1067,19 @@ def test_nodes_neighbours_selection_with_prop_filter(graph): query { graph(path: "g") { nodes(select: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p100" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "p100" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { list { name neighbours { @@ -859,11 +1119,19 @@ def test_nodes_selection(graph): query { graph(path: "g") { nodes(select: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p100" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "p100" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { list { name } @@ -882,17 +1150,33 @@ def test_nodes_selection_nodes_filter_paired(graph): query { graph(path: "g") { nodes(select: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p100" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "p100" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "p9" } } } - rhs: { const: { i64: 5 } } - } - }) { + node: { + eq: { + lhs: { + property: "p9" + } + rhs: { + const: { + i64: 5 + } + } + } + } + }) { list { name } @@ -914,17 +1198,33 @@ def test_nodes_selection_nodes_filter_paired2(graph): query { graph(path: "g") { nodes(select: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p100" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "p100" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "p9" } } } - rhs: { const: { i64: 5 } } - } - }) { + node: { + eq: { + lhs: { + property: "p9" + } + rhs: { + const: { + i64: 5 + } + } + } + } + }) { list { neighbours { list { @@ -958,23 +1258,47 @@ def test_nodes_chained_selection_node_filter_paired(graph): query { graph(path: "g") { nodes(select: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p100" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "p100" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { select(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "p9" } } } - rhs: { const: { i64: 5 } } - } - }) { + node: { + eq: { + lhs: { + property: "p9" + } + rhs: { + const: { + i64: 5 + } + } + } + } + }) { filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { field: NODE_TYPE } } } - rhs: { const: { str: "fire_nation" } } - } - }) { + node: { + eq: { + lhs: { + field: NODE_TYPE + } + rhs: { + const: { + str: "fire_nation" + } + } + } + } + }) { list { name } @@ -997,23 +1321,47 @@ def test_nodes_chained_selection_node_filter_paired_ver2(graph): graph(path: "g") { nodes { select(expr: { - gt: { - lhs: { read: { entity: NODE, target: { property: "p100" } } } - rhs: { const: { i64: 30 } } - } - }) { + node: { + gt: { + lhs: { + property: "p100" + } + rhs: { + const: { + i64: 30 + } + } + } + } + }) { select(expr: { - eq: { - lhs: { read: { entity: NODE, target: { property: "p9" } } } - rhs: { const: { i64: 5 } } - } - }) { + node: { + eq: { + lhs: { + property: "p9" + } + rhs: { + const: { + i64: 5 + } + } + } + } + }) { filter(expr: { - eq: { - lhs: { read: { entity: NODE, target: { field: NODE_TYPE } } } - rhs: { const: { str: "fire_nation" } } - } - }) { + node: { + eq: { + lhs: { + field: NODE_TYPE + } + rhs: { + const: { + str: "fire_nation" + } + } + } + } + }) { list { name } @@ -1042,23 +1390,33 @@ def test_nodes_temporal_property_filter_any_avg_with_window(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { - entity: NODE - target: { property: "prop5" } - views: [{ window: { start: 1, end: 3 } }] - } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + window: { + start: 1 + end: 3 + } + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -1077,23 +1435,30 @@ def test_node_property_layer_filter_not_supported(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { - entity: NODE - target: { property: "prop5" } - views: [{ layers: ["air_nomads"] }] - } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + layers: ["air_nomads"] + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -1114,19 +1479,30 @@ def test_nodes_at_temporal_property(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { entity: NODE, target: { property: "prop5" }, views: [{ at: 2 }] } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + at: 2 + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -1142,19 +1518,30 @@ def test_nodes_before_temporal_property(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { entity: NODE, target: { property: "prop5" }, views: [{ before: 3 }] } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + before: 3 + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -1172,19 +1559,30 @@ def test_nodes_after_temporal_property(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { entity: NODE, target: { property: "prop5" }, views: [{ after: 2 }] } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + after: 2 + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -1202,23 +1600,30 @@ def test_nodes_latest_temporal_property(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { - entity: NODE - target: { property: "prop5" } - views: [{ latest: true }] - } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + latest: true + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -1234,23 +1639,30 @@ def test_nodes_snapshot_at_temporal_property(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { - entity: NODE - target: { property: "prop5" } - views: [{ snapshotAt: 2 }] - } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + snapshotAt: 2 + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -1268,23 +1680,30 @@ def test_nodes_snapshot_latest_temporal_property(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { - entity: NODE - target: { property: "prop5" } - views: [{ snapshotLatest: true }] - } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + snapshotLatest: true + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -1305,23 +1724,32 @@ def test_nodes_layer_then_latest(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { - entity: NODE - target: { property: "prop5" } - views: [{ layers: ["_default"] }, { latest: true }] - } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + layers: ["_default"] + }, { + latest: true + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } @@ -1337,23 +1765,32 @@ def test_nodes_latest_then_layer(graph): query { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { - avg: { - any: { - temporal: { - read: { - entity: NODE - target: { property: "prop5" } - views: [{ latest: true }, { layers: ["_default"] }] - } - } - } - } - } - rhs: { const: { f64: 10.0 } } - } - }) { + node: { + any: { + lt: { + lhs: { + avg: { + viewed: { + views: [{ + latest: true + }, { + layers: ["_default"] + }] + expr: { + temporalProperty: "prop5" + } + } + } + } + rhs: { + const: { + f64: 10.0 + } + } + } + } + } + }) { nodes { list { name } } } } diff --git a/python/tests/test_base_install/test_graphql/test_gql_history.py b/python/tests/test_base_install/test_graphql/test_gql_history.py index dfe633890b..2dccbb3916 100644 --- a/python/tests/test_base_install/test_graphql/test_gql_history.py +++ b/python/tests/test_base_install/test_graphql/test_gql_history.py @@ -244,11 +244,19 @@ def test_history(): { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { property: "weight" } } } - rhs: { const: { f64: 0.9 } } - } - }) { + edge: { + eq: { + lhs: { + property: "weight" + } + rhs: { + const: { + f64: 0.9 + } + } + } + } + }) { edge(src: "Dumbledore", dst: "Harry") { history { timestamps { @@ -273,11 +281,19 @@ def test_history(): { graph(path: "g") { filterEdges: filter(expr: { - eq: { - lhs: { read: { entity: EDGE, target: { property: "weight" } } } - rhs: { const: { f64: 0.7 } } - } - }) { + edge: { + eq: { + lhs: { + property: "weight" + } + rhs: { + const: { + f64: 0.7 + } + } + } + } + }) { edge(src: "Dumbledore", dst: "Harry") { history { timestamps { @@ -296,11 +312,19 @@ def test_history(): { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { read: { entity: NODE, target: { property: "Age" } } } - rhs: { const: { i64: 51 } } - } - }) { + node: { + lt: { + lhs: { + property: "Age" + } + rhs: { + const: { + i64: 51 + } + } + } + } + }) { node(name: "Dumbledore") { history { timestamps { @@ -318,11 +342,19 @@ def test_history(): { graph(path: "g") { filterNodes: filter(expr: { - ge: { - lhs: { read: { entity: NODE, target: { property: "Age" } } } - rhs: { const: { i64: 51 } } - } - }) { + node: { + ge: { + lhs: { + property: "Age" + } + rhs: { + const: { + i64: 51 + } + } + } + } + }) { node(name: "Dumbledore") { history { timestamps { @@ -346,11 +378,19 @@ def test_history(): { graph(path: "g") { filterNodes: filter(expr: { - lt: { - lhs: { read: { entity: NODE, target: { property: "Age" } } } - rhs: { const: { i64: 21 } } - } - }) { + node: { + lt: { + lhs: { + property: "Age" + } + rhs: { + const: { + i64: 21 + } + } + } + } + }) { node(name: "Harry") { history { timestamps { @@ -368,11 +408,19 @@ def test_history(): { graph(path: "g") { filterNodes: filter(expr: { - ge: { - lhs: { read: { entity: NODE, target: { property: "Age" } } } - rhs: { const: { i64: 21 } } - } - }) { + node: { + ge: { + lhs: { + property: "Age" + } + rhs: { + const: { + i64: 21 + } + } + } + } + }) { node(name: "Harry") { history { timestamps { diff --git a/python/tests/test_base_install/test_graphql/test_gql_mutation_time_input.py b/python/tests/test_base_install/test_graphql/test_gql_mutation_time_input.py index 7b15e6ba9c..26ac49b404 100644 --- a/python/tests/test_base_install/test_graphql/test_gql_mutation_time_input.py +++ b/python/tests/test_base_install/test_graphql/test_gql_mutation_time_input.py @@ -29,7 +29,8 @@ def test_add_node_accepts_int_string_and_object_time(): client.new_graph("g", "EVENT") # Three forms, three different nodes. - client.query(""" + client.query( + """ { updateGraph(path: "g") { int_form: addNode(time: 100, name: "intNode") { success } @@ -37,7 +38,8 @@ def test_add_node_accepts_int_string_and_object_time(): obj_form: addNode(time: {timestamp: 300, eventId: 0}, name: "objNode") { success } } } - """) + """ + ) # Verify each landed at the expected timestamp. result = _query( @@ -65,7 +67,8 @@ def test_add_edge_and_delete_edge_accept_time_input_shapes(): client = server.get_client() client.new_graph("g", "PERSISTENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { int_add: addEdge(time: 10, src: "a", dst: "b") { success } @@ -73,7 +76,8 @@ def test_add_edge_and_delete_edge_accept_time_input_shapes(): obj_del: deleteEdge(time: {timestamp: 30, eventId: 0}, src: "a", dst: "b") { success } } } - """) + """ + ) result = _query( server, @@ -105,7 +109,8 @@ def test_add_properties_accepts_time_input_shapes(): client = server.get_client() client.new_graph("g", "EVENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { p1: addProperties(t: 100, properties: [{key: "score", value: {i64: 1}}]) @@ -113,7 +118,8 @@ def test_add_properties_accepts_time_input_shapes(): p3: addProperties(t: {timestamp: 300, eventId: 0}, properties: [{key: "score", value: {i64: 3}}]) } } - """) + """ + ) result = _query( server, @@ -148,7 +154,8 @@ def test_temporal_property_input_accepts_time_input_in_batch(): client = server.get_client() client.new_graph("g", "EVENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { addNodes(nodes: [ @@ -163,7 +170,8 @@ def test_temporal_property_input_accepts_time_input_in_batch(): ]) } } - """) + """ + ) result = _query( server, @@ -195,7 +203,8 @@ def test_add_edges_batch_accepts_time_input_shapes(): client = server.get_client() client.new_graph("g", "EVENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { addEdges(edges: [ @@ -210,7 +219,8 @@ def test_add_edges_batch_accepts_time_input_shapes(): ]) } } - """) + """ + ) result = _query( server, @@ -242,16 +252,19 @@ def test_mutable_node_and_edge_add_updates_accept_time_input(): client.new_graph("g", "PERSISTENT") # Seed the node and edge so we can look them up below. - client.query(""" + client.query( + """ { updateGraph(path: "g") { addNode(time: 0, name: "n") { success } addEdge(time: 0, src: "a", dst: "b") { success } } } - """) + """ + ) - client.query(""" + client.query( + """ { updateGraph(path: "g") { node(name: "n") { @@ -266,7 +279,8 @@ def test_mutable_node_and_edge_add_updates_accept_time_input(): } } } - """) + """ + ) result = _query( server, diff --git a/python/tests/test_base_install/test_graphql/test_gql_node_id.py b/python/tests/test_base_install/test_graphql/test_gql_node_id.py index b509deec56..00ce0ced72 100644 --- a/python/tests/test_base_install/test_graphql/test_gql_node_id.py +++ b/python/tests/test_base_install/test_graphql/test_gql_node_id.py @@ -27,7 +27,8 @@ def test_addnode_and_node_lookup_with_integer_ids(): client = server.get_client() client.new_graph("g", "EVENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { a: addNode(time: 1, name: 1) { success } @@ -35,7 +36,8 @@ def test_addnode_and_node_lookup_with_integer_ids(): c: addNode(time: 3, name: 42) { success } } } - """) + """ + ) result = _query( server, @@ -66,14 +68,16 @@ def test_addedge_and_edge_lookup_with_integer_endpoints(): client = server.get_client() client.new_graph("g", "EVENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { e1: addEdge(time: 10, src: 1, dst: 2) { success } e2: addEdge(time: 20, src: 2, dst: 3) { success } } } - """) + """ + ) result = _query( server, @@ -106,7 +110,8 @@ def test_view_transforms_with_integer_node_ids(): client.new_graph("g", "EVENT") # Build a small integer-id graph: 1 → 2, 1 → 3, 4 → 2 (so 1 and 4 share neighbour 2). - client.query(""" + client.query( + """ { updateGraph(path: "g") { e1: addEdge(time: 1, src: 1, dst: 2) { success } @@ -114,7 +119,8 @@ def test_view_transforms_with_integer_node_ids(): e3: addEdge(time: 3, src: 4, dst: 2) { success } } } - """) + """ + ) result = _query( server, @@ -144,7 +150,8 @@ def test_batch_addnodes_addedges_with_integer_ids(): client = server.get_client() client.new_graph("g", "EVENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { addNodes(nodes: [ @@ -157,7 +164,8 @@ def test_batch_addnodes_addedges_with_integer_ids(): ]) } } - """) + """ + ) result = _query( server, @@ -187,7 +195,8 @@ def test_view_transforms_with_string_node_ids(): client.new_graph("g", "EVENT") # alice → bob, alice → carol, dave → bob (alice and dave share bob). - client.query(""" + client.query( + """ { updateGraph(path: "g") { e1: addEdge(time: 1, src: "alice", dst: "bob") { success } @@ -195,7 +204,8 @@ def test_view_transforms_with_string_node_ids(): e3: addEdge(time: 3, src: "dave", dst: "bob") { success } } } - """) + """ + ) result = _query( server, @@ -223,7 +233,8 @@ def test_batch_addnodes_addedges_with_string_ids(): client = server.get_client() client.new_graph("g", "EVENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { addNodes(nodes: [ @@ -236,7 +247,8 @@ def test_batch_addnodes_addedges_with_string_ids(): ]) } } - """) + """ + ) result = _query( server, @@ -266,14 +278,16 @@ def test_string_ids_remain_unchanged_for_existing_clients(): client = server.get_client() client.new_graph("g", "EVENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { addNode(time: 1, name: "alice") { success } addEdge(time: 2, src: "alice", dst: "bob") { success } } } - """) + """ + ) result = _query( server, @@ -298,13 +312,15 @@ def test_negative_integer_rejected(): client.new_graph("g", "EVENT") try: - client.query(""" + client.query( + """ { updateGraph(path: "g") { addNode(time: 1, name: -1) { success } } } - """) + """ + ) raise AssertionError( "Expected schema-level rejection for negative integer NodeId" ) diff --git a/python/tests/test_base_install/test_graphql/test_gql_same_timestamp_writes.py b/python/tests/test_base_install/test_graphql/test_gql_same_timestamp_writes.py index 6bd923fa35..b49419eac4 100644 --- a/python/tests/test_base_install/test_graphql/test_gql_same_timestamp_writes.py +++ b/python/tests/test_base_install/test_graphql/test_gql_same_timestamp_writes.py @@ -43,7 +43,8 @@ def test_add_properties_same_timestamp_appends(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start() as server: _new_event_graph(server) - server.get_client().query(""" + server.get_client().query( + """ { updateGraph(path: "g") { a: addProperties(t: 100, properties: [{key: "x", value: {i64: 1}}]) @@ -51,7 +52,8 @@ def test_add_properties_same_timestamp_appends(): c: addProperties(t: 100, properties: [{key: "x", value: {i64: 3}}]) } } - """) + """ + ) result = _query( server, """ @@ -76,14 +78,16 @@ def test_add_node_same_timestamp_appends(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start() as server: _new_event_graph(server) - server.get_client().query(""" + server.get_client().query( + """ { updateGraph(path: "g") { a: addNode(time: 100, name: "n", properties: [{key: "v", value: {i64: 1}}]) { success } b: addNode(time: 100, name: "n", properties: [{key: "v", value: {i64: 2}}]) { success } } } - """) + """ + ) result = _query( server, """ @@ -112,14 +116,16 @@ def test_add_edge_same_timestamp_appends(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start() as server: _new_event_graph(server) - server.get_client().query(""" + server.get_client().query( + """ { updateGraph(path: "g") { a: addEdge(time: 100, src: "a", dst: "b", properties: [{key: "w", value: {i64: 1}}]) { success } b: addEdge(time: 100, src: "a", dst: "b", properties: [{key: "w", value: {i64: 2}}]) { success } } } - """) + """ + ) result = _query( server, """ @@ -148,14 +154,16 @@ def test_create_node_then_add_node_same_timestamp_appends(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start() as server: _new_event_graph(server) - server.get_client().query(""" + server.get_client().query( + """ { updateGraph(path: "g") { a: createNode(time: 100, name: "n", properties: [{key: "v", value: {i64: 1}}]) { success } b: addNode(time: 100, name: "n", properties: [{key: "v", value: {i64: 2}}]) { success } } } - """) + """ + ) result = _query( server, """ @@ -185,10 +193,13 @@ def test_mutable_node_add_updates_same_timestamp_appends(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start() as server: _new_event_graph(server) - server.get_client().query(""" + server.get_client().query( + """ { updateGraph(path: "g") { addNode(time: 0, name: "n") { success } } } - """) - server.get_client().query(""" + """ + ) + server.get_client().query( + """ { updateGraph(path: "g") { node(name: "n") { @@ -197,7 +208,8 @@ def test_mutable_node_add_updates_same_timestamp_appends(): } } } - """) + """ + ) result = _query( server, """ @@ -225,10 +237,13 @@ def test_mutable_edge_add_updates_same_timestamp_appends(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start() as server: _new_event_graph(server) - server.get_client().query(""" + server.get_client().query( + """ { updateGraph(path: "g") { addEdge(time: 0, src: "a", dst: "b") { success } } } - """) - server.get_client().query(""" + """ + ) + server.get_client().query( + """ { updateGraph(path: "g") { edge(src: "a", dst: "b") { @@ -237,7 +252,8 @@ def test_mutable_edge_add_updates_same_timestamp_appends(): } } } - """) + """ + ) result = _query( server, """ @@ -270,7 +286,8 @@ def test_add_nodes_batch_same_timestamp_appends(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start() as server: _new_event_graph(server) - server.get_client().query(""" + server.get_client().query( + """ { updateGraph(path: "g") { addNodes(nodes: [ @@ -282,7 +299,8 @@ def test_add_nodes_batch_same_timestamp_appends(): ]) } } - """) + """ + ) result = _query( server, """ @@ -309,7 +327,8 @@ def test_add_edges_batch_same_timestamp_appends(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start() as server: _new_event_graph(server) - server.get_client().query(""" + server.get_client().query( + """ { updateGraph(path: "g") { addEdges(edges: [ @@ -321,7 +340,8 @@ def test_add_edges_batch_same_timestamp_appends(): ]) } } - """) + """ + ) result = _query( server, """ @@ -354,7 +374,8 @@ def test_delete_edge_same_timestamp_appends(): with GraphServer(work_dir).start() as server: client = server.get_client() client.new_graph("g", "PERSISTENT") - client.query(""" + client.query( + """ { updateGraph(path: "g") { a: addEdge(time: 1, src: "a", dst: "b") { success } @@ -362,7 +383,8 @@ def test_delete_edge_same_timestamp_appends(): d2: deleteEdge(time: 100, src: "a", dst: "b") { success } } } - """) + """ + ) result = _query( server, """ @@ -386,14 +408,16 @@ def test_object_time_input_distinct_event_ids_append(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start() as server: _new_event_graph(server) - server.get_client().query(""" + server.get_client().query( + """ { updateGraph(path: "g") { a: addProperties(t: {timestamp: 100, eventId: 0}, properties: [{key: "x", value: {i64: 1}}]) b: addProperties(t: {timestamp: 100, eventId: 1}, properties: [{key: "x", value: {i64: 2}}]) } } - """) + """ + ) result = _query( server, """ diff --git a/python/tests/test_base_install/test_graphql/test_metadata_dispatch.py b/python/tests/test_base_install/test_graphql/test_metadata_dispatch.py index 0802ae4c31..a9ad949f91 100644 --- a/python/tests/test_base_install/test_graphql/test_metadata_dispatch.py +++ b/python/tests/test_base_install/test_graphql/test_metadata_dispatch.py @@ -67,7 +67,8 @@ def _read_is_diskgraph(graph_dir): def _list_metadata_by_path(client): """Query metadata for every graph in the namespace via the standard listing field. Returns ``{path: {key: value}}``.""" - result = client.query("""{ + result = client.query( + """{ root { graphs { list { @@ -76,7 +77,8 @@ def _list_metadata_by_path(client): } } } - }""") + }""" + ) return { entry["path"]: {item["key"]: item["value"] for item in entry["metadata"]} for entry in result["root"]["graphs"]["list"] diff --git a/python/tests/test_base_install/test_graphql/test_namespace.py b/python/tests/test_base_install/test_graphql/test_namespace.py index cbeafda342..67fd9c2c0a 100644 --- a/python/tests/test_base_install/test_graphql/test_namespace.py +++ b/python/tests/test_base_install/test_graphql/test_namespace.py @@ -478,7 +478,8 @@ def test_namespace_listing_does_not_load_each_graph(): for i in range(n_graphs): client.send_graph(f"bulk_{i}", g, overwrite=True) - result = client.query("""{ + result = client.query( + """{ root { graphs { list { @@ -489,7 +490,8 @@ def test_namespace_listing_does_not_load_each_graph(): } } } - }""") + }""" + ) graphs = result["root"]["graphs"]["list"] assert len(graphs) == n_graphs diff --git a/python/tests/test_base_install/test_graphql/test_nodes.py b/python/tests/test_base_install/test_graphql/test_nodes.py index bc2081bfda..4cd61b1b3b 100644 --- a/python/tests/test_base_install/test_graphql/test_nodes.py +++ b/python/tests/test_base_install/test_graphql/test_nodes.py @@ -13,11 +13,19 @@ def test_graph_node_sort_by_nothing(graph): query = """{ graph(path: "g") { nodes(select: { - eq: { - lhs: { read: { entity: NODE, target: { field: ID } } } - rhs: { const: { u64: 1 } } - } - }) { + node: { + eq: { + lhs: { + field: ID + } + rhs: { + const: { + u64: 1 + } + } + } + } + }) { list { name degree diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index ac6f362b7a..ed4f855af6 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -522,14 +522,6 @@ enum AlignmentUnit { YEAR } -""" -Two expressions to compare. -""" -input Cmp { - lhs: Expr! - rhs: Expr! -} - """ Collection of items """ @@ -1060,6 +1052,114 @@ input EdgeAddition { updates: [TemporalPropertyInput!] } +""" +Two expressions to compare. +""" +input EdgeCmp { + lhs: EdgeExpr! + rhs: EdgeExpr! +} + +input EdgeExpr @oneOf { + """ + A literal. + """ + const: Value + """ + The latest value of a property. + """ + property: String + """ + The history of a property, as a list. + """ + temporalProperty: String + """ + A metadata entry. + """ + metadata: String + """ + Whether the entity is active; written `isActive: true`. + """ + isActive: Boolean + """ + Whether the edge is valid; written `isValid: true`. + """ + isValid: Boolean + """ + Whether the edge is deleted; written `isDeleted: true`. + """ + isDeleted: Boolean + """ + Whether the edge is a self loop; written `isSelfLoop: true`. + """ + isSelfLoop: Boolean + """ + A node expression evaluated on the edge's source node. + """ + src: NodeExpr + """ + A node expression evaluated on the edge's destination node. + """ + dst: NodeExpr + """ + Views applied to every read inside. + """ + viewed: EdgeViewed + sum: EdgeExpr + avg: EdgeExpr + min: EdgeExpr + max: EdgeExpr + first: EdgeExpr + last: EdgeExpr + len: EdgeExpr + eq: EdgeCmp + ne: EdgeCmp + lt: EdgeCmp + le: EdgeCmp + gt: EdgeCmp + ge: EdgeCmp + startsWith: EdgeCmp + endsWith: EdgeCmp + contains: EdgeCmp + notContains: EdgeCmp + fuzzySearch: EdgeFuzzyCmp + isIn: EdgeMembership + isNotIn: EdgeMembership + isSome: EdgeExpr + isNone: EdgeExpr + """ + Holds when the element-wise result inside holds for any element. + """ + any: EdgeExpr + """ + Holds when the element-wise result inside holds for every element. + """ + all: EdgeExpr + and: [EdgeExpr!] + or: [EdgeExpr!] + not: EdgeExpr +} + +""" +A fuzzy string match: `lhs` is within `levenshteinDistance` edits of +`rhs`, optionally matching by prefix. +""" +input EdgeFuzzyCmp { + lhs: EdgeExpr! + rhs: EdgeExpr! + levenshteinDistance: Int! + prefixMatch: Boolean! +} + +""" +A membership test. `values` is a list; a policy may also leave a single +placeholder here (`{"var": …}`) that resolves to the list per caller. +""" +input EdgeMembership { + expr: EdgeExpr! + values: Value! +} + """ Describes edges between a specific pair of node types — the property and metadata keys seen on such edges, along with their observed value types. @@ -1174,6 +1274,14 @@ input EdgeViewCollection @oneOf { filter: FilterExpr } +""" +Views applied to every read inside `expr`, in list order. +""" +input EdgeViewed { + views: [ViewOp!]! + expr: EdgeExpr! +} + """ A lazy sequence of per-window views of a single edge, produced by `edge.rolling` / `edge.expanding`. Each entry is the edge as it exists in @@ -1637,23 +1745,6 @@ input EmbeddingModel @oneOf { openAI: OpenAIConfig } -""" -Which end of an edge a read looks at. -""" -enum Endpoint { - SRC - DST -} - -""" -The kind of thing a filter tests: a node, an edge, or one edge update. -""" -enum Entity { - NODE - EDGE - EXPLODED_EDGE -} - """ Raphtory’s EventTime. Represents a unique timepoint in the graph’s history as (timestamp, event_id). @@ -1691,61 +1782,120 @@ type EventTime { } """ -A value: what stands on either side of a comparison. +Two expressions to compare. """ -input Expr @oneOf { +input ExplodedEdgeCmp { + lhs: ExplodedEdgeExpr! + rhs: ExplodedEdgeExpr! +} + +input ExplodedEdgeExpr @oneOf { """ A literal. """ const: Value """ - A field, degree, property or metadata read from an entity. + The latest value of a property. + """ + property: String + """ + The history of a property, as a list. + """ + temporalProperty: String + """ + A metadata entry. + """ + metadata: String + """ + Whether the entity is active; written `isActive: true`. + """ + isActive: Boolean + """ + Whether the edge update is valid; written `isValid: true`. + """ + isValid: Boolean + """ + Whether the edge update is deleted; written `isDeleted: true`. + """ + isDeleted: Boolean """ - read: Read + Whether the edge update is a self loop; written `isSelfLoop: true`. """ - The full history of a property instead of its latest value. + isSelfLoop: Boolean """ - temporal: Expr - sum: Expr - avg: Expr - min: Expr - max: Expr - first: Expr - last: Expr - len: Expr + Views applied to every read inside. """ - The predicate holds if it holds for any element. + viewed: ExplodedEdgeViewed + sum: ExplodedEdgeExpr + avg: ExplodedEdgeExpr + min: ExplodedEdgeExpr + max: ExplodedEdgeExpr + first: ExplodedEdgeExpr + last: ExplodedEdgeExpr + len: ExplodedEdgeExpr + eq: ExplodedEdgeCmp + ne: ExplodedEdgeCmp + lt: ExplodedEdgeCmp + le: ExplodedEdgeCmp + gt: ExplodedEdgeCmp + ge: ExplodedEdgeCmp + startsWith: ExplodedEdgeCmp + endsWith: ExplodedEdgeCmp + contains: ExplodedEdgeCmp + notContains: ExplodedEdgeCmp + fuzzySearch: ExplodedEdgeFuzzyCmp + isIn: ExplodedEdgeMembership + isNotIn: ExplodedEdgeMembership + isSome: ExplodedEdgeExpr + isNone: ExplodedEdgeExpr """ - any: Expr + Holds when the element-wise result inside holds for any element. """ - The predicate holds if it holds for every element. + any: ExplodedEdgeExpr """ - all: Expr + Holds when the element-wise result inside holds for every element. + """ + all: ExplodedEdgeExpr + and: [ExplodedEdgeExpr!] + or: [ExplodedEdgeExpr!] + not: ExplodedEdgeExpr +} + +""" +A fuzzy string match: `lhs` is within `levenshteinDistance` edits of +`rhs`, optionally matching by prefix. +""" +input ExplodedEdgeFuzzyCmp { + lhs: ExplodedEdgeExpr! + rhs: ExplodedEdgeExpr! + levenshteinDistance: Int! + prefixMatch: Boolean! +} + +""" +A membership test. `values` is a list; a policy may also leave a single +placeholder here (`{"var": …}`) that resolves to the list per caller. +""" +input ExplodedEdgeMembership { + expr: ExplodedEdgeExpr! + values: Value! } """ -The filter itself: a yes/no over an entity. +Views applied to every read inside `expr`, in list order. +""" +input ExplodedEdgeViewed { + views: [ViewOp!]! + expr: ExplodedEdgeExpr! +} + +""" +The filter itself: a yes/no over one kind of entity, a view, or a combination. """ input FilterExpr @oneOf { - eq: Cmp - ne: Cmp - lt: Cmp - le: Cmp - gt: Cmp - ge: Cmp - startsWith: Cmp - endsWith: Cmp - contains: Cmp - notContains: Cmp - fuzzySearch: FuzzyCmp - isSome: Expr - isNone: Expr - isIn: Membership - isNotIn: Membership - isActive: Scope - isValid: Scope - isDeleted: Scope - isSelfLoop: Scope + node: NodeExpr + edge: EdgeExpr + explodedEdge: ExplodedEdgeExpr """ A graph-level view with no predicate: the result is the view. """ @@ -1755,17 +1905,6 @@ input FilterExpr @oneOf { not: FilterExpr } -""" -A fuzzy string match: `lhs` is within `levenshteinDistance` edits of -`rhs`, optionally matching by prefix. -""" -input FuzzyCmp { - lhs: Expr! - rhs: Expr! - levenshteinDistance: Int! - prefixMatch: Boolean! -} - """ Fuzzy string match: passes when the candidate is within `levenshteinDistance` edits of `value` (optionally also matching by prefix). Mirrors the local @@ -2849,15 +2988,6 @@ type Matching { ): Boolean! } -""" -A membership test. `values` is a list; a policy may also leave a single -placeholder here (`{"var": …}`) that resolves to the list per caller. -""" -input Membership { - expr: Expr! - values: Value! -} - """ Lightweight summary of a stored graph — its name, path, counts, and filesystem timestamps — served without deserializing the full graph. @@ -4424,6 +4554,82 @@ input NodeAddition { layer: String } +""" +Two expressions to compare. +""" +input NodeCmp { + lhs: NodeExpr! + rhs: NodeExpr! +} + +input NodeExpr @oneOf { + """ + A literal. + """ + const: Value + """ + The latest value of a property. + """ + property: String + """ + The history of a property, as a list. + """ + temporalProperty: String + """ + A metadata entry. + """ + metadata: String + """ + Whether the entity is active; written `isActive: true`. + """ + isActive: Boolean + """ + A built-in node field. + """ + field: NodeFieldName + """ + The node's degree in a direction. + """ + degree: DegreeDirection + """ + Views applied to every read inside. + """ + viewed: NodeViewed + sum: NodeExpr + avg: NodeExpr + min: NodeExpr + max: NodeExpr + first: NodeExpr + last: NodeExpr + len: NodeExpr + eq: NodeCmp + ne: NodeCmp + lt: NodeCmp + le: NodeCmp + gt: NodeCmp + ge: NodeCmp + startsWith: NodeCmp + endsWith: NodeCmp + contains: NodeCmp + notContains: NodeCmp + fuzzySearch: NodeFuzzyCmp + isIn: NodeMembership + isNotIn: NodeMembership + isSome: NodeExpr + isNone: NodeExpr + """ + Holds when the element-wise result inside holds for any element. + """ + any: NodeExpr + """ + Holds when the element-wise result inside holds for every element. + """ + all: NodeExpr + and: [NodeExpr!] + or: [NodeExpr!] + not: NodeExpr +} + """ A built-in node field. """ @@ -4433,6 +4639,17 @@ enum NodeFieldName { NODE_TYPE } +""" +A fuzzy string match: `lhs` is within `levenshteinDistance` edits of +`rhs`, optionally matching by prefix. +""" +input NodeFuzzyCmp { + lhs: NodeExpr! + rhs: NodeExpr! + levenshteinDistance: Int! + prefixMatch: Boolean! +} + """ Identifier for a node — either a string (`"alice"`) or a non-negative integer (`42`). Use whichever form matches how the graph was indexed @@ -4440,6 +4657,15 @@ when nodes were added. """ scalar NodeId +""" +A membership test. `values` is a list; a policy may also leave a single +placeholder here (`{"var": …}`) that resolves to the list per caller. +""" +input NodeMembership { + expr: NodeExpr! + values: Value! +} + """ Describes nodes of a specific type in a graph — its property keys and observed value types (and, for string-valued properties, the set of @@ -4831,6 +5057,14 @@ input NodeViewCollection @oneOf { filter: FilterExpr } +""" +Views applied to every read inside `expr`, in list order. +""" +input NodeViewed { + views: [ViewOp!]! + expr: NodeExpr! +} + """ A lazy sequence of per-window views of a single node, produced by `node.rolling` / `node.expanding`. Each entry is the node as it exists in @@ -6476,26 +6710,6 @@ type QueryRoot { ): VectorisedGraph } -""" -A value read from an entity. -""" -input Read { - entity: Entity! - views: [ViewOp!] - endpoint: Endpoint - target: Target! -} - -""" -Where a value is read: the entity, the views to read it through, and for an -edge optionally one of its endpoint nodes. -""" -input Scope { - entity: Entity! - views: [ViewOp!] - endpoint: Endpoint -} - """ How the initially infected nodes are chosen. """ @@ -6537,28 +6751,6 @@ enum SpecialFloat { NEG_INFINITY } -""" -What a read selects on its entity. -""" -input Target @oneOf { - """ - A built-in node field. - """ - field: NodeFieldName - """ - A node degree in a direction. - """ - degree: DegreeDirection - """ - A property, by name. - """ - property: String - """ - A metadata entry, by name. - """ - metadata: String -} - input Template @oneOf { """ The default template. @@ -7076,4 +7268,3 @@ schema { query: QueryRoot mutation: MutRoot } - diff --git a/raphtory-graphql/src/client/graphql_transport.rs b/raphtory-graphql/src/client/graphql_transport.rs index c813287e48..3dc0f0ccb4 100644 --- a/raphtory-graphql/src/client/graphql_transport.rs +++ b/raphtory-graphql/src/client/graphql_transport.rs @@ -4296,13 +4296,14 @@ mod tests { use crate::{ data::GqlGraphType, model::graph::{ - filter_expr_input::{GqlCmp, GqlEntity, GqlExpr, GqlRead, GqlTarget}, + filter_expr_input::{GqlNodeCmp, GqlNodeExpr}, + filtering::Wrapped, property::Value as GqlValue, }, server::GraphServer, }; use raphtory::{ - db::graph::views::filter::model::tree::FilterExpr, + db::graph::views::filter::model::expr::FilterExpr, prelude::{Args, NO_PROPS}, }; use raphtory_api::core::storage::timeindex::AsTime; @@ -4313,16 +4314,11 @@ mod tests { // ============ Unit tests for the read pipeline ============ /// A node-property comparison in the tree grammar: `property(name) value`. - fn node_prop(name: &str, op: fn(GqlCmp) -> GqlFilter, value: GqlValue) -> GqlFilter { - op(GqlCmp { - lhs: GqlExpr::Read(GqlRead { - entity: GqlEntity::Node, - views: None, - endpoint: None, - target: GqlTarget::Property(name.into()), - }), - rhs: GqlExpr::Const(value), - }) + fn node_prop(name: &str, op: fn(GqlNodeCmp) -> GqlNodeExpr, value: GqlValue) -> GqlFilter { + GqlFilter::Node(op(GqlNodeCmp { + lhs: Wrapped::from(GqlNodeExpr::Property(name.into())), + rhs: Wrapped::from(GqlNodeExpr::Const(value)), + })) } /// The tree a client hands to `filter`, as python does. @@ -4693,7 +4689,7 @@ mod tests { // break out of), not rendered into the query text. let filter = node_prop( "score".into(), - GqlFilter::Eq, + GqlNodeExpr::Eq, GqlValue::Str("O\"Brien".into()), ); let mut vars = VarCollector::default(); @@ -4745,7 +4741,7 @@ mod tests { #[test] fn property_key_rides_json_variable_intact() { // A quote-bearing property KEY is carried as JSON data too. - let filter = node_prop("wei\"rd".into(), GqlFilter::Eq, GqlValue::Str("v".into())); + let filter = node_prop("wei\"rd".into(), GqlNodeExpr::Eq, GqlValue::Str("v".into())); let mut vars = VarCollector::default(); vars.add_filter(&filter).unwrap(); let json = serde_json::to_string(&vars.vars["f0"]).unwrap(); @@ -4761,7 +4757,7 @@ mod tests { // with each field arg referencing its own variable — the payloads must // not collide or swap. let prop_filter = - |name: &str| node_prop(name.into(), GqlFilter::Eq, GqlValue::Str("x".into())); + |name: &str| node_prop(name.into(), GqlNodeExpr::Eq, GqlValue::Str("x".into())); let expr = ReadExpr::Ids { input: Arc::new(ReadExpr::Filtered { input: Arc::new(ReadExpr::Filtered { @@ -4815,7 +4811,7 @@ mod tests { GqlValue::F64(f64::INFINITY), GqlValue::F32(f32::NEG_INFINITY), ] { - let filter = node_prop("x".into(), GqlFilter::Eq, bad); + let filter = node_prop("x".into(), GqlNodeExpr::Eq, bad); let mut vars = VarCollector::default(); assert!(matches!( vars.add_filter(&filter), @@ -4824,7 +4820,7 @@ mod tests { } // A finite float serializes fine. - let filter = node_prop("x".into(), GqlFilter::Eq, GqlValue::F64(1.5)); + let filter = node_prop("x".into(), GqlNodeExpr::Eq, GqlValue::F64(1.5)); let mut vars = VarCollector::default(); assert!(vars.add_filter(&filter).is_ok()); } @@ -4994,7 +4990,7 @@ mod tests { rg.add_edge(2i64, "b", "c", NO_PROPS, None).await.unwrap(); rg.add_edge(3i64, "c", "a", NO_PROPS, None).await.unwrap(); - let score_gt_15 = node_prop("score".into(), GqlFilter::Gt, GqlValue::I64(15)); + let score_gt_15 = node_prop("score".into(), GqlNodeExpr::Gt, GqlValue::I64(15)); // Membership: filter keeps every node addressable — including `a`, // which fails the filter itself. @@ -5107,7 +5103,7 @@ mod tests { .nodes() .filter(tree(node_prop( "score".into(), - GqlFilter::Gt, + GqlNodeExpr::Gt, GqlValue::I64(15), ))) .unwrap(); diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs index dac669a927..0d8d86e62e 100644 --- a/raphtory-graphql/src/lib.rs +++ b/raphtory-graphql/src/lib.rs @@ -559,22 +559,19 @@ mod graphql_test { graph(path: "g") { filterNodes: filter( expr: { - gt: { - lhs: { - read: { - entity: NODE - target: { - degree: BOTH - } - } - } - rhs: { - const: { - u64: 0 - } - } - } - } + node: { + gt: { + lhs: { + degree: BOTH + } + rhs: { + const: { + u64: 0 + } + } + } + } + } ) { nodes { list { @@ -584,22 +581,19 @@ mod graphql_test { } nodes { select(expr: { - gt: { - lhs: { - read: { - entity: NODE - target: { - degree: BOTH - } - } - } - rhs: { - const: { - u64: 0 - } - } - } - }) { + node: { + gt: { + lhs: { + degree: BOTH + } + rhs: { + const: { + u64: 0 + } + } + } + } + }) { list { name } diff --git a/raphtory-graphql/src/model/algorithms/tests/components/out_component.rs b/raphtory-graphql/src/model/algorithms/tests/components/out_component.rs index 096ae37ff8..56906797bc 100644 --- a/raphtory-graphql/src/model/algorithms/tests/components/out_component.rs +++ b/raphtory-graphql/src/model/algorithms/tests/components/out_component.rs @@ -70,22 +70,19 @@ async fn test_algorithm_out_component_filtered() { graph(path: "g") { algorithm { outComponent(node: "a", filter: { - ne: { - lhs: { - read: { - entity: NODE - target: { - field: NAME - } - } - } - rhs: { - const: { - str: "c" - } - } - } - }) { + node: { + ne: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "c" + } + } + } + } + }) { nodes { list { id } } } } @@ -117,43 +114,34 @@ async fn test_algorithm_out_component_node_filter_composed() { graph(path: "g") { algorithm { outComponent(node: "a", filter: { - and: [ - { - ne: { - lhs: { - read: { - entity: NODE - target: { - field: NAME - } - } - } - rhs: { - const: { - str: "b" - } - } - } - }, - { - ne: { - lhs: { - read: { - entity: NODE - target: { - field: NAME - } - } - } - rhs: { - const: { - str: "c" - } - } - } - } - ] - }) { + and: [{ + node: { + ne: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "b" + } + } + } + } + }, { + node: { + ne: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "c" + } + } + } + } + }] + }) { nodes { list { id } } } } @@ -184,45 +172,38 @@ async fn test_algorithm_out_component_edge_filter_composed() { graph(path: "g") { algorithm { outComponent(node: "a", filter: { - and: [ - { - ne: { - lhs: { - read: { - entity: EDGE - target: { - field: NAME - } - endpoint: DST - } - } - rhs: { - const: { - str: "b" - } - } - } - }, - { - ne: { - lhs: { - read: { - entity: EDGE - target: { - field: NAME - } - endpoint: DST - } - } - rhs: { - const: { - str: "c" - } - } - } - } - ] - }) { + and: [{ + edge: { + ne: { + lhs: { + dst: { + field: NAME + } + } + rhs: { + const: { + str: "b" + } + } + } + } + }, { + edge: { + ne: { + lhs: { + dst: { + field: NAME + } + } + rhs: { + const: { + str: "c" + } + } + } + } + }] + }) { nodes { list { id } } } } @@ -304,22 +285,19 @@ async fn test_algorithm_out_component_filter_equivalence() { graph(path: "g") { algorithm { outComponent(node: "a", filter: { - ne: { - lhs: { - read: { - entity: NODE - target: { - field: NAME - } - } - } - rhs: { - const: { - str: "c" - } - } - } - }) { + node: { + ne: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "c" + } + } + } + } + }) { rows { node { id } entries { @@ -337,22 +315,19 @@ async fn test_algorithm_out_component_filter_equivalence() { { graph(path: "g") { filter(expr: { - ne: { - lhs: { - read: { - entity: NODE - target: { - field: NAME - } - } - } - rhs: { - const: { - str: "c" - } - } - } - }) { + node: { + ne: { + lhs: { + field: NAME + } + rhs: { + const: { + str: "c" + } + } + } + } + }) { algorithm { outComponent(node: "a") { rows { diff --git a/raphtory-graphql/src/model/graph/filter_expr_input.rs b/raphtory-graphql/src/model/graph/filter_expr_input.rs index e012b3d27b..a19606f4fc 100644 --- a/raphtory-graphql/src/model/graph/filter_expr_input.rs +++ b/raphtory-graphql/src/model/graph/filter_expr_input.rs @@ -1,14 +1,18 @@ -//! The GraphQL form of the filter tree. +//! The GraphQL form of the filter expression tree. //! -//! One input type per node of [`tree::FilterExpr`] / [`tree::Expr`], named the -//! same way, so a filter written in python, rust or a GraphQL document is the -//! same tree spelled in three syntaxes. Both sides of a comparison are -//! expressions; a constant is the `const` expression. +//! One input type per entity, mirroring [`expr::NodeExpr`], [`expr::EdgeExpr`] +//! and [`expr::ExplodedEdgeExpr`] field for field, so a filter written in +//! python, rust or a GraphQL document is the same tree spelled three ways. The +//! entity is the key; what the entity reads is a plain field of it. //! //! ```graphql -//! filter(expr: { gt: { lhs: { read: { entity: NODE, target: { degree: BOTH } } }, -//! rhs: { read: { entity: NODE, target: { degree: IN } } } } }) +//! filter(expr: { node: { gt: { lhs: { degree: BOTH }, rhs: { degree: IN } } } }) +//! filter(expr: { edge: { eq: { lhs: { src: { field: NAME } }, rhs: { const: { str: "alice" } } } } }) +//! filter(expr: { node: { any: { gt: { lhs: { temporalProperty: "score" }, rhs: { const: { f64: 4 } } } } } }) //! ``` +//! +//! Views scope reads: `{ node: { viewed: { views: [...], expr: { property: "score" } } } }` +//! applies the views to every read inside `expr`. use crate::model::graph::{ filtering::{Window, Wrapped}, @@ -24,12 +28,11 @@ use raphtory::{ }, graph::views::filter::{ model::{ - edge_filter::Endpoint, - tree::{ - self, Agg, CmpOp, Entity, Field, FilterExpr, Qual, Scope, StrOp, Structural, - Target, ViewOp, OPAQUE_FILTER_ERROR, + expr::{ + self, Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, Leaf, NodeLeaf, + StrOp, ViewOp, OPAQUE_FILTER_ERROR, }, - DynFilter, + tree, DynFilter, }, CreateFilter, }, @@ -45,25 +48,6 @@ use raphtory_api::core::{ use serde::{Deserialize, Serialize}; use std::{ops::Deref, sync::Arc}; -/// The kind of thing a filter tests: a node, an edge, or one edge update. -#[derive(Enum, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -#[graphql(name = "Entity")] -pub enum GqlEntity { - Node, - Edge, - ExplodedEdge, -} - -/// Which end of an edge a read looks at. -#[derive(Enum, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -#[graphql(name = "Endpoint")] -pub enum GqlEndpoint { - Src, - Dst, -} - /// A built-in node field. #[derive(Enum, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] @@ -108,131 +92,515 @@ impl From for Direction { } } -/// What a read selects on its entity. -#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[graphql(name = "Target")] -pub enum GqlTarget { - /// A built-in node field. - Field(GqlNodeField), - /// A node degree in a direction. - Degree(DegreeDirection), - /// A property, by name. - Property(String), - /// A metadata entry, by name. - Metadata(String), +fn degree_direction(d: Direction) -> DegreeDirection { + match d { + Direction::IN => DegreeDirection::In, + Direction::OUT => DegreeDirection::Out, + Direction::BOTH => DegreeDirection::Both, + } } -/// Where a value is read: the entity, the views to read it through, and for an -/// edge optionally one of its endpoint nodes. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -#[graphql(name = "Scope")] -pub struct GqlScope { - pub entity: GqlEntity, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub views: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint: Option, +impl From for Field { + fn from(f: GqlNodeField) -> Self { + match f { + GqlNodeField::Id => Field::Id, + GqlNodeField::Name => Field::Name, + GqlNodeField::NodeType => Field::NodeType, + } + } } -/// A value read from an entity. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -#[graphql(name = "Read")] -pub struct GqlRead { - pub entity: GqlEntity, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub views: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint: Option, - pub target: GqlTarget, +impl From for GqlNodeField { + fn from(f: Field) -> Self { + match f { + Field::Id => GqlNodeField::Id, + Field::Name => GqlNodeField::Name, + Field::NodeType => GqlNodeField::NodeType, + } + } } -/// A value: what stands on either side of a comparison. -#[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[graphql(name = "Expr")] -pub enum GqlExpr { - /// A literal. - Const(Value), - /// A field, degree, property or metadata read from an entity. - Read(GqlRead), - /// The full history of a property instead of its latest value. - Temporal(Wrapped), - Sum(Wrapped), - Avg(Wrapped), - Min(Wrapped), - Max(Wrapped), - First(Wrapped), - Last(Wrapped), - Len(Wrapped), - /// The predicate holds if it holds for any element. - Any(Wrapped), - /// The predicate holds if it holds for every element. - All(Wrapped), +// ── the per-entity expression inputs ───────────────────────────────────────── + +/// Stamps out the input types of one entity: the expression itself, its two-sided +/// tests, its membership test and its view wrapper. The variants every entity has +/// are written once here; the entity's own reads are passed in. +macro_rules! entity_expr_input { + ( + $expr:ident = $expr_name:literal, + $cmp:ident = $cmp_name:literal, + $fuzzy:ident = $fuzzy_name:literal, + $membership:ident = $membership_name:literal, + $viewed:ident = $viewed_name:literal, + leaf = $leaf:ident, + own reads { $( $(#[$own_meta:meta])* $own:ident($own_ty:ty) => $own_conv:expr ),* $(,)? } + ) => { + /// Two expressions to compare. + #[derive(InputObject, Clone, Debug, Serialize, Deserialize)] + #[serde(deny_unknown_fields)] + #[serde(rename_all = "camelCase")] + #[graphql(name = $cmp_name)] + pub struct $cmp { + pub lhs: Wrapped<$expr>, + pub rhs: Wrapped<$expr>, + } + + /// A fuzzy string match: `lhs` is within `levenshteinDistance` edits of + /// `rhs`, optionally matching by prefix. + #[derive(InputObject, Clone, Debug, Serialize, Deserialize)] + #[serde(deny_unknown_fields)] + #[serde(rename_all = "camelCase")] + #[graphql(name = $fuzzy_name)] + pub struct $fuzzy { + pub lhs: Wrapped<$expr>, + pub rhs: Wrapped<$expr>, + pub levenshtein_distance: usize, + pub prefix_match: bool, + } + + /// A membership test. `values` is a list; a policy may also leave a single + /// placeholder here (`{"var": …}`) that resolves to the list per caller. + #[derive(InputObject, Clone, Debug, Serialize, Deserialize)] + #[serde(deny_unknown_fields)] + #[serde(rename_all = "camelCase")] + #[graphql(name = $membership_name)] + pub struct $membership { + pub expr: Wrapped<$expr>, + pub values: Value, + } + + /// Views applied to every read inside `expr`, in list order. + #[derive(InputObject, Clone, Debug, Serialize, Deserialize)] + #[serde(deny_unknown_fields)] + #[serde(rename_all = "camelCase")] + #[graphql(name = $viewed_name)] + pub struct $viewed { + pub views: Vec, + pub expr: Wrapped<$expr>, + } + + #[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + #[graphql(name = $expr_name)] + pub enum $expr { + /// A literal. + Const(Value), + /// The latest value of a property. + Property(String), + /// The history of a property, as a list. + TemporalProperty(String), + /// A metadata entry. + Metadata(String), + /// Whether the entity is active; written `isActive: true`. + IsActive(bool), + $( $(#[$own_meta])* $own($own_ty), )* + /// Views applied to every read inside. + Viewed(Wrapped<$viewed>), + Sum(Wrapped<$expr>), + Avg(Wrapped<$expr>), + Min(Wrapped<$expr>), + Max(Wrapped<$expr>), + First(Wrapped<$expr>), + Last(Wrapped<$expr>), + Len(Wrapped<$expr>), + Eq($cmp), + Ne($cmp), + Lt($cmp), + Le($cmp), + Gt($cmp), + Ge($cmp), + StartsWith($cmp), + EndsWith($cmp), + Contains($cmp), + NotContains($cmp), + FuzzySearch($fuzzy), + IsIn($membership), + IsNotIn($membership), + IsSome(Wrapped<$expr>), + IsNone(Wrapped<$expr>), + /// Holds when the element-wise result inside holds for any element. + Any(Wrapped<$expr>), + /// Holds when the element-wise result inside holds for every element. + All(Wrapped<$expr>), + And(Vec<$expr>), + Or(Vec<$expr>), + Not(Wrapped<$expr>), + } + + impl TryFrom<$expr> for Expr<$leaf> { + type Error = GraphError; + + fn try_from(e: $expr) -> Result { + let inner = |w: Wrapped<$expr>| -> Result>, GraphError> { + Ok(Box::new(Expr::try_from(w.deref().clone())?)) + }; + let cmp = |op: CmpOp, c: $cmp| -> Result, GraphError> { + Ok(Expr::Cmp( + op, + Box::new(Expr::try_from(c.lhs.deref().clone())?), + Box::new(Expr::try_from(c.rhs.deref().clone())?), + )) + }; + let str_op = |op: StrOp, c: $cmp| -> Result, GraphError> { + Ok(Expr::Str( + op, + Box::new(Expr::try_from(c.lhs.deref().clone())?), + Box::new(Expr::try_from(c.rhs.deref().clone())?), + )) + }; + let members = |m: $membership, negated: bool| -> Result, GraphError> { + Ok(Expr::In { + expr: Box::new(Expr::try_from(m.expr.deref().clone())?), + values: member_values(m.values, negated)?, + negated, + }) + }; + Ok(match e { + $expr::Const(v) => Expr::Const(prop(v)?), + $expr::Property(name) => Expr::Read($leaf::property(Vec::new(), name, false)), + $expr::TemporalProperty(name) => { + Expr::Read($leaf::property(Vec::new(), name, true)) + } + $expr::Metadata(name) => Expr::Read($leaf::metadata(Vec::new(), name)), + $expr::IsActive(applied) => { + applied_test(applied, "isActive")?; + Expr::Read($leaf::is_active(Vec::new())) + } + $( $expr::$own(v) => { + let convert: &dyn Fn($own_ty) -> Result, GraphError> = + &$own_conv; + convert(v)? + } )* + $expr::Viewed(v) => { + let v = v.deref().clone(); + let mut expr = Expr::try_from(v.expr.deref().clone())?; + for op in view_ops(Some(v.views))? { + expr.push_view(op); + } + expr + } + $expr::Sum(e) => Expr::Agg(Agg::Sum, inner(e)?), + $expr::Avg(e) => Expr::Agg(Agg::Avg, inner(e)?), + $expr::Min(e) => Expr::Agg(Agg::Min, inner(e)?), + $expr::Max(e) => Expr::Agg(Agg::Max, inner(e)?), + $expr::First(e) => Expr::Agg(Agg::First, inner(e)?), + $expr::Last(e) => Expr::Agg(Agg::Last, inner(e)?), + $expr::Len(e) => Expr::Agg(Agg::Len, inner(e)?), + $expr::Eq(c) => cmp(CmpOp::Eq, c)?, + $expr::Ne(c) => cmp(CmpOp::Ne, c)?, + $expr::Lt(c) => cmp(CmpOp::Lt, c)?, + $expr::Le(c) => cmp(CmpOp::Le, c)?, + $expr::Gt(c) => cmp(CmpOp::Gt, c)?, + $expr::Ge(c) => cmp(CmpOp::Ge, c)?, + $expr::StartsWith(c) => str_op(StrOp::StartsWith, c)?, + $expr::EndsWith(c) => str_op(StrOp::EndsWith, c)?, + $expr::Contains(c) => str_op(StrOp::Contains, c)?, + $expr::NotContains(c) => str_op(StrOp::NotContains, c)?, + $expr::FuzzySearch(f) => Expr::Str( + StrOp::FuzzySearch { + levenshtein_distance: f.levenshtein_distance, + prefix_match: f.prefix_match, + }, + Box::new(Expr::try_from(f.lhs.deref().clone())?), + Box::new(Expr::try_from(f.rhs.deref().clone())?), + ), + $expr::IsIn(m) => members(m, false)?, + $expr::IsNotIn(m) => members(m, true)?, + $expr::IsSome(e) => Expr::IsSome(inner(e)?), + $expr::IsNone(e) => Expr::IsNone(inner(e)?), + $expr::Any(e) => Expr::Any(inner(e)?), + $expr::All(e) => Expr::All(inner(e)?), + $expr::And(items) => Expr::And( + items + .into_iter() + .map(Expr::try_from) + .collect::>()?, + ), + $expr::Or(items) => Expr::Or( + items + .into_iter() + .map(Expr::try_from) + .collect::>()?, + ), + $expr::Not(e) => Expr::Not(inner(e)?), + }) + } + } + + impl TryFrom<&Expr<$leaf>> for $expr { + type Error = GraphError; + + fn try_from(e: &Expr<$leaf>) -> Result { + let inner = |e: &Expr<$leaf>| -> Result, GraphError> { + Ok(Wrapped::from($expr::try_from(e)?)) + }; + let cmp = |l: &Expr<$leaf>, r: &Expr<$leaf>| -> Result<$cmp, GraphError> { + Ok($cmp { + lhs: Wrapped::from($expr::try_from(l)?), + rhs: Wrapped::from($expr::try_from(r)?), + }) + }; + Ok(match e { + Expr::Const(p) => $expr::Const(value(p)?), + Expr::Read(leaf) => { + let (views, read) = $expr::leaf_read(leaf)?; + if views.is_empty() { + read + } else { + $expr::Viewed(Wrapped::from($viewed { + views: views.iter().map(GqlViewOp::from).collect(), + expr: Wrapped::from(read), + })) + } + } + Expr::Agg(Agg::Sum, e) => $expr::Sum(inner(e)?), + Expr::Agg(Agg::Avg, e) => $expr::Avg(inner(e)?), + Expr::Agg(Agg::Min, e) => $expr::Min(inner(e)?), + Expr::Agg(Agg::Max, e) => $expr::Max(inner(e)?), + Expr::Agg(Agg::First, e) => $expr::First(inner(e)?), + Expr::Agg(Agg::Last, e) => $expr::Last(inner(e)?), + Expr::Agg(Agg::Len, e) => $expr::Len(inner(e)?), + Expr::Cmp(CmpOp::Eq, l, r) => $expr::Eq(cmp(l, r)?), + Expr::Cmp(CmpOp::Ne, l, r) => $expr::Ne(cmp(l, r)?), + Expr::Cmp(CmpOp::Lt, l, r) => $expr::Lt(cmp(l, r)?), + Expr::Cmp(CmpOp::Le, l, r) => $expr::Le(cmp(l, r)?), + Expr::Cmp(CmpOp::Gt, l, r) => $expr::Gt(cmp(l, r)?), + Expr::Cmp(CmpOp::Ge, l, r) => $expr::Ge(cmp(l, r)?), + Expr::Str(StrOp::StartsWith, l, r) => $expr::StartsWith(cmp(l, r)?), + Expr::Str(StrOp::EndsWith, l, r) => $expr::EndsWith(cmp(l, r)?), + Expr::Str(StrOp::Contains, l, r) => $expr::Contains(cmp(l, r)?), + Expr::Str(StrOp::NotContains, l, r) => $expr::NotContains(cmp(l, r)?), + Expr::Str( + StrOp::FuzzySearch { + levenshtein_distance, + prefix_match, + }, + l, + r, + ) => $expr::FuzzySearch($fuzzy { + lhs: Wrapped::from($expr::try_from(l.deref())?), + rhs: Wrapped::from($expr::try_from(r.deref())?), + levenshtein_distance: *levenshtein_distance, + prefix_match: *prefix_match, + }), + Expr::In { + expr, + values, + negated, + } => { + let m = $membership { + expr: Wrapped::from($expr::try_from(expr.deref())?), + values: Value::List( + values.iter().map(value).collect::, _>>()?, + ), + }; + if *negated { + $expr::IsNotIn(m) + } else { + $expr::IsIn(m) + } + } + Expr::IsSome(e) => $expr::IsSome(inner(e)?), + Expr::IsNone(e) => $expr::IsNone(inner(e)?), + Expr::Any(e) => $expr::Any(inner(e)?), + Expr::All(e) => $expr::All(inner(e)?), + Expr::And(items) => $expr::And( + items + .iter() + .map($expr::try_from) + .collect::>()?, + ), + Expr::Or(items) => $expr::Or( + items + .iter() + .map($expr::try_from) + .collect::>()?, + ), + Expr::Not(e) => $expr::Not(inner(e)?), + }) + } + } + }; } -/// Two expressions to compare. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -#[graphql(name = "Cmp")] -pub struct GqlCmp { - pub lhs: GqlExpr, - pub rhs: GqlExpr, +entity_expr_input! { + GqlNodeExpr = "NodeExpr", + GqlNodeCmp = "NodeCmp", + GqlNodeFuzzyCmp = "NodeFuzzyCmp", + GqlNodeMembership = "NodeMembership", + GqlNodeViewed = "NodeViewed", + leaf = NodeLeaf, + own reads { + /// A built-in node field. + Field(GqlNodeField) => |f: GqlNodeField| { + Ok(Expr::Read(NodeLeaf::Field { + views: Vec::new(), + field: f.into(), + })) + }, + /// The node's degree in a direction. + Degree(DegreeDirection) => |d: DegreeDirection| { + Ok(Expr::Read(NodeLeaf::Degree { + views: Vec::new(), + direction: d.into(), + })) + }, + } } -/// A fuzzy string match: `lhs` is within `levenshteinDistance` edits of -/// `rhs`, optionally matching by prefix. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -#[graphql(name = "FuzzyCmp")] -pub struct GqlFuzzyCmp { - pub lhs: GqlExpr, - pub rhs: GqlExpr, - pub levenshtein_distance: usize, - pub prefix_match: bool, +entity_expr_input! { + GqlEdgeExpr = "EdgeExpr", + GqlEdgeCmp = "EdgeCmp", + GqlEdgeFuzzyCmp = "EdgeFuzzyCmp", + GqlEdgeMembership = "EdgeMembership", + GqlEdgeViewed = "EdgeViewed", + leaf = EdgeLeaf, + own reads { + /// Whether the edge is valid; written `isValid: true`. + IsValid(bool) => |applied: bool| { + applied_test(applied, "isValid")?; + Ok(Expr::Read(EdgeLeaf::IsValid { views: Vec::new() })) + }, + /// Whether the edge is deleted; written `isDeleted: true`. + IsDeleted(bool) => |applied: bool| { + applied_test(applied, "isDeleted")?; + Ok(Expr::Read(EdgeLeaf::IsDeleted { views: Vec::new() })) + }, + /// Whether the edge is a self loop; written `isSelfLoop: true`. + IsSelfLoop(bool) => |applied: bool| { + applied_test(applied, "isSelfLoop")?; + Ok(Expr::Read(EdgeLeaf::IsSelfLoop { views: Vec::new() })) + }, + /// A node expression evaluated on the edge's source node. + Src(Wrapped) => |e: Wrapped| { + Ok(Expr::Read(EdgeLeaf::Src(Box::new(Expr::try_from(e.deref().clone())?)))) + }, + /// A node expression evaluated on the edge's destination node. + Dst(Wrapped) => |e: Wrapped| { + Ok(Expr::Read(EdgeLeaf::Dst(Box::new(Expr::try_from(e.deref().clone())?)))) + }, + } } -/// A membership test. `values` is a list; a policy may also leave a single -/// placeholder here (`{"var": …}`) that resolves to the list per caller. -#[derive(InputObject, Clone, Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -#[graphql(name = "Membership")] -pub struct GqlMembership { - pub expr: GqlExpr, - pub values: Value, +entity_expr_input! { + GqlExplodedEdgeExpr = "ExplodedEdgeExpr", + GqlExplodedEdgeCmp = "ExplodedEdgeCmp", + GqlExplodedEdgeFuzzyCmp = "ExplodedEdgeFuzzyCmp", + GqlExplodedEdgeMembership = "ExplodedEdgeMembership", + GqlExplodedEdgeViewed = "ExplodedEdgeViewed", + leaf = ExplodedEdgeLeaf, + own reads { + /// Whether the edge update is valid; written `isValid: true`. + IsValid(bool) => |applied: bool| { + applied_test(applied, "isValid")?; + Ok(Expr::Read(ExplodedEdgeLeaf::IsValid { views: Vec::new() })) + }, + /// Whether the edge update is deleted; written `isDeleted: true`. + IsDeleted(bool) => |applied: bool| { + applied_test(applied, "isDeleted")?; + Ok(Expr::Read(ExplodedEdgeLeaf::IsDeleted { views: Vec::new() })) + }, + /// Whether the edge update is a self loop; written `isSelfLoop: true`. + IsSelfLoop(bool) => |applied: bool| { + applied_test(applied, "isSelfLoop")?; + Ok(Expr::Read(ExplodedEdgeLeaf::IsSelfLoop { views: Vec::new() })) + }, + } +} + +// ── the entity-specific reads, tree → GraphQL ──────────────────────────────── + +impl GqlNodeExpr { + /// A leaf as the read it is, and the views it carries. + fn leaf_read(leaf: &NodeLeaf) -> Result<(&[ViewOp], GqlNodeExpr), GraphError> { + Ok(match leaf { + NodeLeaf::Field { views, field } => (views, GqlNodeExpr::Field((*field).into())), + NodeLeaf::Degree { views, direction } => { + (views, GqlNodeExpr::Degree(degree_direction(*direction))) + } + NodeLeaf::Property { + views, + name, + temporal: false, + } => (views, GqlNodeExpr::Property(name.clone())), + NodeLeaf::Property { + views, + name, + temporal: true, + } => (views, GqlNodeExpr::TemporalProperty(name.clone())), + NodeLeaf::Metadata { views, name } => (views, GqlNodeExpr::Metadata(name.clone())), + NodeLeaf::IsActive { views } => (views, GqlNodeExpr::IsActive(true)), + }) + } +} + +impl GqlEdgeExpr { + fn leaf_read(leaf: &EdgeLeaf) -> Result<(&[ViewOp], GqlEdgeExpr), GraphError> { + Ok(match leaf { + EdgeLeaf::Property { + views, + name, + temporal: false, + } => (views, GqlEdgeExpr::Property(name.clone())), + EdgeLeaf::Property { + views, + name, + temporal: true, + } => (views, GqlEdgeExpr::TemporalProperty(name.clone())), + EdgeLeaf::Metadata { views, name } => (views, GqlEdgeExpr::Metadata(name.clone())), + EdgeLeaf::IsActive { views } => (views, GqlEdgeExpr::IsActive(true)), + EdgeLeaf::IsValid { views } => (views, GqlEdgeExpr::IsValid(true)), + EdgeLeaf::IsDeleted { views } => (views, GqlEdgeExpr::IsDeleted(true)), + EdgeLeaf::IsSelfLoop { views } => (views, GqlEdgeExpr::IsSelfLoop(true)), + // The endpoint's own reads carry their views; there are none here. + EdgeLeaf::Src(inner) => ( + &[], + GqlEdgeExpr::Src(Wrapped::from(GqlNodeExpr::try_from(inner.deref())?)), + ), + EdgeLeaf::Dst(inner) => ( + &[], + GqlEdgeExpr::Dst(Wrapped::from(GqlNodeExpr::try_from(inner.deref())?)), + ), + }) + } } -/// The filter itself: a yes/no over an entity. +impl GqlExplodedEdgeExpr { + fn leaf_read(leaf: &ExplodedEdgeLeaf) -> Result<(&[ViewOp], GqlExplodedEdgeExpr), GraphError> { + Ok(match leaf { + ExplodedEdgeLeaf::Property { + views, + name, + temporal: false, + } => (views, GqlExplodedEdgeExpr::Property(name.clone())), + ExplodedEdgeLeaf::Property { + views, + name, + temporal: true, + } => (views, GqlExplodedEdgeExpr::TemporalProperty(name.clone())), + ExplodedEdgeLeaf::Metadata { views, name } => { + (views, GqlExplodedEdgeExpr::Metadata(name.clone())) + } + ExplodedEdgeLeaf::IsActive { views } => (views, GqlExplodedEdgeExpr::IsActive(true)), + ExplodedEdgeLeaf::IsValid { views } => (views, GqlExplodedEdgeExpr::IsValid(true)), + ExplodedEdgeLeaf::IsDeleted { views } => (views, GqlExplodedEdgeExpr::IsDeleted(true)), + ExplodedEdgeLeaf::IsSelfLoop { views } => { + (views, GqlExplodedEdgeExpr::IsSelfLoop(true)) + } + }) + } +} + +// ── the filter ─────────────────────────────────────────────────────────────── + +/// The filter itself: a yes/no over one kind of entity, a view, or a combination. #[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[graphql(name = "FilterExpr")] pub enum GqlFilter { - Eq(GqlCmp), - Ne(GqlCmp), - Lt(GqlCmp), - Le(GqlCmp), - Gt(GqlCmp), - Ge(GqlCmp), - StartsWith(GqlCmp), - EndsWith(GqlCmp), - Contains(GqlCmp), - NotContains(GqlCmp), - FuzzySearch(GqlFuzzyCmp), - IsSome(Wrapped), - IsNone(Wrapped), - IsIn(GqlMembership), - IsNotIn(GqlMembership), - IsActive(GqlScope), - IsValid(GqlScope), - IsDeleted(GqlScope), - IsSelfLoop(GqlScope), + Node(GqlNodeExpr), + Edge(GqlEdgeExpr), + ExplodedEdge(GqlExplodedEdgeExpr), /// A graph-level view with no predicate: the result is the view. View(Vec), And(Vec), @@ -240,38 +608,19 @@ pub enum GqlFilter { Not(Wrapped), } -// ── GraphQL → tree ─────────────────────────────────────────────────────────── - fn invalid(msg: impl Into) -> GraphError { GraphError::InvalidGqlFilter(msg.into()) } -impl From for Entity { - fn from(e: GqlEntity) -> Self { - match e { - GqlEntity::Node => Entity::Node, - GqlEntity::Edge => Entity::Edge, - GqlEntity::ExplodedEdge => Entity::ExplodedEdge, - } - } -} - -impl From for Endpoint { - fn from(e: GqlEndpoint) -> Self { - match e { - GqlEndpoint::Src => Endpoint::Src, - GqlEndpoint::Dst => Endpoint::Dst, - } - } -} - -impl From for Field { - fn from(f: GqlNodeField) -> Self { - match f { - GqlNodeField::Id => Field::Id, - GqlNodeField::Name => Field::Name, - GqlNodeField::NodeType => Field::NodeType, - } +/// A test with no payload is written `name: true`; `false` is refused rather +/// than ignored, since a test that is not applied has no place in a filter. +fn applied_test(applied: bool, name: &str) -> Result<(), GraphError> { + if applied { + Ok(()) + } else { + Err(invalid(format!( + "{name}: false is not a test; write {name}: true" + ))) } } @@ -309,33 +658,24 @@ fn view_ops(views: Option>) -> Result, GraphError> { .collect() } -fn scope( - entity: GqlEntity, - views: Option>, - endpoint: Option, -) -> Result { - Ok(Scope { - entity: entity.into(), - views: view_ops(views)?, - endpoint: endpoint.map(Endpoint::from), - }) -} - -impl TryFrom for Scope { - type Error = GraphError; - - fn try_from(s: GqlScope) -> Result { - scope(s.entity, s.views, s.endpoint) - } +fn time(t: EventTime) -> GqlTimeInput { + GqlTimeInput(InputTime::Indexed(t.0, t.1)) } -impl From for Target { - fn from(t: GqlTarget) -> Self { - match t { - GqlTarget::Field(f) => Target::Field(f.into()), - GqlTarget::Degree(d) => Target::Degree(d.into()), - GqlTarget::Property(name) => Target::Property(name), - GqlTarget::Metadata(name) => Target::Metadata(name), +impl From<&ViewOp> for GqlViewOp { + fn from(op: &ViewOp) -> Self { + match op { + ViewOp::Window { start, end } => GqlViewOp::Window(Window { + start: time(*start), + end: time(*end), + }), + ViewOp::At(t) => GqlViewOp::At(time(*t)), + ViewOp::After(t) => GqlViewOp::After(time(*t)), + ViewOp::Before(t) => GqlViewOp::Before(time(*t)), + ViewOp::Latest => GqlViewOp::Latest(true), + ViewOp::SnapshotAt(t) => GqlViewOp::SnapshotAt(time(*t)), + ViewOp::SnapshotLatest => GqlViewOp::SnapshotLatest(true), + ViewOp::Layers(names) => GqlViewOp::Layers(names.clone()), } } } @@ -344,103 +684,29 @@ fn prop(value: Value) -> Result { Prop::try_from(value).map_err(|e| invalid(format!("invalid constant: {e}"))) } -fn inner(expr: &Wrapped) -> Result, GraphError> { - Ok(Box::new(tree::Expr::try_from(expr.deref().clone())?)) -} - -impl TryFrom for tree::Expr { - type Error = GraphError; - - fn try_from(expr: GqlExpr) -> Result { - use tree::Expr as E; - Ok(match expr { - GqlExpr::Const(value) => E::Const(prop(value)?), - GqlExpr::Read(read) => E::Read { - scope: scope(read.entity, read.views, read.endpoint)?, - target: read.target.into(), - }, - GqlExpr::Temporal(e) => E::Temporal(inner(&e)?), - GqlExpr::Sum(e) => E::Agg(Agg::Sum, inner(&e)?), - GqlExpr::Avg(e) => E::Agg(Agg::Avg, inner(&e)?), - GqlExpr::Min(e) => E::Agg(Agg::Min, inner(&e)?), - GqlExpr::Max(e) => E::Agg(Agg::Max, inner(&e)?), - GqlExpr::First(e) => E::Agg(Agg::First, inner(&e)?), - GqlExpr::Last(e) => E::Agg(Agg::Last, inner(&e)?), - GqlExpr::Len(e) => E::Agg(Agg::Len, inner(&e)?), - GqlExpr::Any(e) => E::Qual(Qual::Any, inner(&e)?), - GqlExpr::All(e) => E::Qual(Qual::All, inner(&e)?), - }) - } -} - -fn cmp(op: CmpOp, c: GqlCmp) -> Result { - Ok(tree::FilterExpr::Cmp { - op, - lhs: c.lhs.try_into()?, - rhs: c.rhs.try_into()?, - }) -} - -fn str_op(op: StrOp, c: GqlCmp) -> Result { - Ok(tree::FilterExpr::Str { - op, - lhs: c.lhs.try_into()?, - rhs: c.rhs.try_into()?, - }) +fn value(p: &Prop) -> Result { + Value::try_from(p).map_err(|e| invalid(format!("constant has no wire form: {e}"))) } -fn membership(m: GqlMembership, negated: bool) -> Result { +/// The members of a set: a list of constants. Anything else, a placeholder a +/// policy failed to resolve included, is refused. +fn member_values(values: Value, negated: bool) -> Result, GraphError> { let op = if negated { "isNotIn" } else { "isIn" }; - let values = match m.values { - Value::List(items) => items.into_iter().map(prop).collect::, _>>()?, - other => return Err(invalid(format!("{op} requires a list value, got {other}"))), - }; - Ok(tree::FilterExpr::In { - expr: m.expr.try_into()?, - values, - negated, - }) -} - -fn structural(s: GqlScope, pred: Structural) -> Result { - Ok(tree::FilterExpr::Structural { - scope: s.try_into()?, - pred, - }) + match values { + Value::List(items) => items.into_iter().map(prop).collect(), + other => Err(invalid(format!("{op} requires a list value, got {other}"))), + } } -impl TryFrom for tree::FilterExpr { +impl TryFrom for expr::FilterExpr { type Error = GraphError; fn try_from(filter: GqlFilter) -> Result { - use tree::FilterExpr as F; + use expr::FilterExpr as F; Ok(match filter { - GqlFilter::Eq(c) => cmp(CmpOp::Eq, c)?, - GqlFilter::Ne(c) => cmp(CmpOp::Ne, c)?, - GqlFilter::Lt(c) => cmp(CmpOp::Lt, c)?, - GqlFilter::Le(c) => cmp(CmpOp::Le, c)?, - GqlFilter::Gt(c) => cmp(CmpOp::Gt, c)?, - GqlFilter::Ge(c) => cmp(CmpOp::Ge, c)?, - GqlFilter::StartsWith(c) => str_op(StrOp::StartsWith, c)?, - GqlFilter::EndsWith(c) => str_op(StrOp::EndsWith, c)?, - GqlFilter::Contains(c) => str_op(StrOp::Contains, c)?, - GqlFilter::NotContains(c) => str_op(StrOp::NotContains, c)?, - GqlFilter::FuzzySearch(f) => F::Str { - op: StrOp::FuzzySearch { - levenshtein_distance: f.levenshtein_distance, - prefix_match: f.prefix_match, - }, - lhs: f.lhs.try_into()?, - rhs: f.rhs.try_into()?, - }, - GqlFilter::IsSome(e) => F::IsSome(e.deref().clone().try_into()?), - GqlFilter::IsNone(e) => F::IsNone(e.deref().clone().try_into()?), - GqlFilter::IsIn(m) => membership(m, false)?, - GqlFilter::IsNotIn(m) => membership(m, true)?, - GqlFilter::IsActive(s) => structural(s, Structural::IsActive)?, - GqlFilter::IsValid(s) => structural(s, Structural::IsValid)?, - GqlFilter::IsDeleted(s) => structural(s, Structural::IsDeleted)?, - GqlFilter::IsSelfLoop(s) => structural(s, Structural::IsSelfLoop)?, + GqlFilter::Node(e) => F::Node(e.try_into()?), + GqlFilter::Edge(e) => F::Edge(e.try_into()?), + GqlFilter::ExplodedEdge(e) => F::ExplodedEdge(e.try_into()?), GqlFilter::View(ops) => F::View(view_ops(Some(ops))?), GqlFilter::And(items) => F::And( items @@ -459,195 +725,17 @@ impl TryFrom for tree::FilterExpr { } } -// ── tree → GraphQL ─────────────────────────────────────────────────────────── -// // Clients build trees and send them; this is the spelling they send. - -impl From for GqlEntity { - fn from(e: Entity) -> Self { - match e { - Entity::Node => GqlEntity::Node, - Entity::Edge => GqlEntity::Edge, - Entity::ExplodedEdge => GqlEntity::ExplodedEdge, - } - } -} - -impl From for GqlEndpoint { - fn from(e: Endpoint) -> Self { - match e { - Endpoint::Src => GqlEndpoint::Src, - Endpoint::Dst => GqlEndpoint::Dst, - } - } -} - -impl From for GqlNodeField { - fn from(f: Field) -> Self { - match f { - Field::Id => GqlNodeField::Id, - Field::Name => GqlNodeField::Name, - Field::NodeType => GqlNodeField::NodeType, - } - } -} - -fn time(t: EventTime) -> GqlTimeInput { - GqlTimeInput(InputTime::Indexed(t.0, t.1)) -} - -impl From<&ViewOp> for GqlViewOp { - fn from(op: &ViewOp) -> Self { - match op { - ViewOp::Window { start, end } => GqlViewOp::Window(Window { - start: time(*start), - end: time(*end), - }), - ViewOp::At(t) => GqlViewOp::At(time(*t)), - ViewOp::After(t) => GqlViewOp::After(time(*t)), - ViewOp::Before(t) => GqlViewOp::Before(time(*t)), - ViewOp::Latest => GqlViewOp::Latest(true), - ViewOp::SnapshotAt(t) => GqlViewOp::SnapshotAt(time(*t)), - ViewOp::SnapshotLatest => GqlViewOp::SnapshotLatest(true), - ViewOp::Layers(names) => GqlViewOp::Layers(names.clone()), - } - } -} - -fn views(v: &[ViewOp]) -> Option> { - (!v.is_empty()).then(|| v.iter().map(GqlViewOp::from).collect()) -} - -impl From<&Scope> for GqlScope { - fn from(s: &Scope) -> Self { - GqlScope { - entity: s.entity.into(), - views: views(&s.views), - endpoint: s.endpoint.map(GqlEndpoint::from), - } - } -} - -fn direction(d: Direction) -> DegreeDirection { - match d { - Direction::IN => DegreeDirection::In, - Direction::OUT => DegreeDirection::Out, - Direction::BOTH => DegreeDirection::Both, - } -} - -impl From<&Target> for GqlTarget { - fn from(t: &Target) -> Self { - match t { - Target::Field(f) => GqlTarget::Field((*f).into()), - Target::Degree(d) => GqlTarget::Degree(direction(*d)), - Target::Property(name) => GqlTarget::Property(name.clone()), - Target::Metadata(name) => GqlTarget::Metadata(name.clone()), - } - } -} - -fn value(p: &Prop) -> Result { - Value::try_from(p).map_err(|e| invalid(format!("constant has no wire form: {e}"))) -} - -fn wrapped(e: &tree::Expr) -> Result, GraphError> { - Ok(Wrapped::from(GqlExpr::try_from(e)?)) -} - -impl TryFrom<&tree::Expr> for GqlExpr { - type Error = GraphError; - - fn try_from(expr: &tree::Expr) -> Result { - use tree::Expr as E; - Ok(match expr { - E::Const(p) => GqlExpr::Const(value(p)?), - E::Read { scope, target } => GqlExpr::Read(GqlRead { - entity: scope.entity.into(), - views: views(&scope.views), - endpoint: scope.endpoint.map(GqlEndpoint::from), - target: target.into(), - }), - E::Temporal(e) => GqlExpr::Temporal(wrapped(e)?), - E::Agg(Agg::Sum, e) => GqlExpr::Sum(wrapped(e)?), - E::Agg(Agg::Avg, e) => GqlExpr::Avg(wrapped(e)?), - E::Agg(Agg::Min, e) => GqlExpr::Min(wrapped(e)?), - E::Agg(Agg::Max, e) => GqlExpr::Max(wrapped(e)?), - E::Agg(Agg::First, e) => GqlExpr::First(wrapped(e)?), - E::Agg(Agg::Last, e) => GqlExpr::Last(wrapped(e)?), - E::Agg(Agg::Len, e) => GqlExpr::Len(wrapped(e)?), - E::Qual(Qual::Any, e) => GqlExpr::Any(wrapped(e)?), - E::Qual(Qual::All, e) => GqlExpr::All(wrapped(e)?), - }) - } -} - -fn gql_cmp(lhs: &tree::Expr, rhs: &tree::Expr) -> Result { - Ok(GqlCmp { - lhs: lhs.try_into()?, - rhs: rhs.try_into()?, - }) -} - -impl TryFrom<&tree::FilterExpr> for GqlFilter { +impl TryFrom<&expr::FilterExpr> for GqlFilter { type Error = GraphError; - fn try_from(filter: &tree::FilterExpr) -> Result { - use tree::FilterExpr as F; + fn try_from(filter: &expr::FilterExpr) -> Result { + use expr::FilterExpr as F; Ok(match filter { F::Opaque(_) => return Err(invalid(OPAQUE_FILTER_ERROR)), - F::Cmp { op, lhs, rhs } => { - let c = gql_cmp(lhs, rhs)?; - match op { - CmpOp::Eq => GqlFilter::Eq(c), - CmpOp::Ne => GqlFilter::Ne(c), - CmpOp::Lt => GqlFilter::Lt(c), - CmpOp::Le => GqlFilter::Le(c), - CmpOp::Gt => GqlFilter::Gt(c), - CmpOp::Ge => GqlFilter::Ge(c), - } - } - F::Str { op, lhs, rhs } => match op { - StrOp::StartsWith => GqlFilter::StartsWith(gql_cmp(lhs, rhs)?), - StrOp::EndsWith => GqlFilter::EndsWith(gql_cmp(lhs, rhs)?), - StrOp::Contains => GqlFilter::Contains(gql_cmp(lhs, rhs)?), - StrOp::NotContains => GqlFilter::NotContains(gql_cmp(lhs, rhs)?), - StrOp::FuzzySearch { - levenshtein_distance, - prefix_match, - } => GqlFilter::FuzzySearch(GqlFuzzyCmp { - lhs: lhs.try_into()?, - rhs: rhs.try_into()?, - levenshtein_distance: *levenshtein_distance, - prefix_match: *prefix_match, - }), - }, - F::IsSome(e) => GqlFilter::IsSome(wrapped(e)?), - F::IsNone(e) => GqlFilter::IsNone(wrapped(e)?), - F::In { - expr, - values, - negated, - } => { - let m = GqlMembership { - expr: expr.try_into()?, - values: Value::List(values.iter().map(value).collect::, _>>()?), - }; - if *negated { - GqlFilter::IsNotIn(m) - } else { - GqlFilter::IsIn(m) - } - } - F::Structural { scope, pred } => { - let s = GqlScope::from(scope); - match pred { - Structural::IsActive => GqlFilter::IsActive(s), - Structural::IsValid => GqlFilter::IsValid(s), - Structural::IsDeleted => GqlFilter::IsDeleted(s), - Structural::IsSelfLoop => GqlFilter::IsSelfLoop(s), - } - } + F::Node(e) => GqlFilter::Node(e.try_into()?), + F::Edge(e) => GqlFilter::Edge(e.try_into()?), + F::ExplodedEdge(e) => GqlFilter::ExplodedEdge(e.try_into()?), F::View(ops) => GqlFilter::View(ops.iter().map(GqlViewOp::from).collect()), F::And(items) => GqlFilter::And( items @@ -666,11 +754,20 @@ impl TryFrom<&tree::FilterExpr> for GqlFilter { } } -impl TryFrom for GqlFilter { +impl TryFrom for GqlFilter { type Error = GraphError; - fn try_from(tree: FilterExpr) -> Result { - GqlFilter::try_from(&tree) + fn try_from(filter: expr::FilterExpr) -> Result { + GqlFilter::try_from(&filter) + } +} + +/// The tree python objects still hold, until they move to the per-entity one. +impl TryFrom for GqlFilter { + type Error = GraphError; + + fn try_from(filter: tree::FilterExpr) -> Result { + GqlFilter::try_from(&filter.to_split()?) } } @@ -679,7 +776,7 @@ impl TryFrom for DynFilter { type Error = GraphError; fn try_from(value: GqlFilter) -> Result { - FilterExpr::try_from(value)?.compile() + expr::FilterExpr::try_from(value)?.compile() } } @@ -705,7 +802,7 @@ impl CreateFilter for GqlFilter { graph: G, filtered: F, ) -> Result, GraphError> { - FilterExpr::try_from(self)?.create_filter(graph, filtered) + expr::FilterExpr::try_from(self)?.create_filter(graph, filtered) } fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( @@ -713,104 +810,113 @@ impl CreateFilter for GqlFilter { graph: G, filtered: F, ) -> Result, GraphError> { - FilterExpr::try_from(self)?.create_node_filter(graph, filtered) + expr::FilterExpr::try_from(self)?.create_node_filter(graph, filtered) } fn filter_graph_view<'graph, G: GraphView + 'graph>( &self, graph: G, ) -> Result, GraphError> { - FilterExpr::try_from(self.clone())?.filter_graph_view(graph) + expr::FilterExpr::try_from(self.clone())?.filter_graph_view(graph) } } #[cfg(test)] mod tests { use super::*; + use expr::FilterExpr as F; - fn read(target: Target) -> tree::Expr { - tree::Expr::Read { - scope: Scope::new(Entity::Node), - target, - } + fn degree(direction: Direction) -> Expr { + Expr::Read(NodeLeaf::Degree { + views: Vec::new(), + direction, + }) } #[test] fn a_tree_survives_the_trip_through_the_wire_type_and_json() { - let tree = tree::FilterExpr::And(vec![ - tree::FilterExpr::Cmp { - op: CmpOp::Gt, - lhs: tree::Expr::Agg( + let tree = F::And(vec![ + F::Node(Expr::Any(Box::new(Expr::Cmp( + CmpOp::Gt, + Box::new(Expr::Agg( Agg::Sum, - Box::new(tree::Expr::Temporal(Box::new(read(Target::Property( - "score".into(), - ))))), - ), - rhs: tree::Expr::Const(Prop::F64(10.0)), - }, - tree::FilterExpr::Cmp { - op: CmpOp::Gt, - lhs: read(Target::Degree(Direction::BOTH)), - rhs: read(Target::Degree(Direction::IN)), - }, - tree::FilterExpr::Structural { - scope: Scope::new(Entity::Edge) - .with_view(ViewOp::Window { - start: EventTime::from(0), - end: EventTime::from(5), - }) - .with_view(ViewOp::Layers(vec!["knows".into()])), - pred: Structural::IsActive, - }, - tree::FilterExpr::Not(Box::new(tree::FilterExpr::In { - expr: tree::Expr::Read { - scope: Scope::new(Entity::Edge).through(Endpoint::Src), - target: Target::Field(Field::Name), - }, + Box::new(Expr::Read(NodeLeaf::Property { + views: vec![ViewOp::Window { + start: EventTime::from(0), + end: EventTime::from(5), + }], + name: "score".into(), + temporal: true, + })), + )), + Box::new(Expr::Const(Prop::F64(10.0))), + )))), + F::Node(Expr::Cmp( + CmpOp::Gt, + Box::new(degree(Direction::BOTH)), + Box::new(degree(Direction::IN)), + )), + F::Edge(Expr::Read(EdgeLeaf::IsActive { + views: vec![ViewOp::Layers(vec!["works".into()])], + })), + F::Not(Box::new(F::Edge(Expr::In { + expr: Box::new(Expr::Read(EdgeLeaf::Src(Box::new(Expr::Read( + NodeLeaf::Field { + views: Vec::new(), + field: Field::Name, + }, + ))))), values: vec![Prop::str("alice"), Prop::str("bob")], negated: false, - })), - tree::FilterExpr::View(vec![ViewOp::Latest]), + }))), + F::ExplodedEdge(Expr::Str( + StrOp::FuzzySearch { + levenshtein_distance: 2, + prefix_match: false, + }, + Box::new(Expr::Read(ExplodedEdgeLeaf::Property { + views: Vec::new(), + name: "tag".into(), + temporal: false, + })), + Box::new(Expr::Const(Prop::str("rock"))), + )), + F::View(vec![ViewOp::Latest]), ]); - let wire = GqlFilter::try_from(&tree).unwrap(); let json = serde_json::to_string(&wire).unwrap(); let wire_back: GqlFilter = serde_json::from_str(&json).unwrap(); - let tree_back = tree::FilterExpr::try_from(wire_back).unwrap(); + let tree_back = F::try_from(wire_back).unwrap(); assert_eq!(tree_back, tree); } #[test] - fn the_json_spelling_is_the_documented_one() { - let tree = tree::FilterExpr::Cmp { - op: CmpOp::Gt, - lhs: read(Target::Degree(Direction::BOTH)), - rhs: read(Target::Degree(Direction::IN)), - }; + fn the_json_spelling_keys_on_the_entity_and_the_read() { + let tree = F::Edge(Expr::Cmp( + CmpOp::Eq, + Box::new(Expr::Read(EdgeLeaf::Src(Box::new(Expr::Read( + NodeLeaf::Property { + views: vec![ViewOp::Latest], + name: "score".into(), + temporal: false, + }, + ))))), + Box::new(Expr::Const(Prop::I64(1))), + )); let wire = GqlFilter::try_from(&tree).unwrap(); assert_eq!( serde_json::to_value(&wire).unwrap(), serde_json::json!({ - "gt": { - "lhs": { "read": { "entity": "NODE", "target": { "degree": "BOTH" } } }, - "rhs": { "read": { "entity": "NODE", "target": { "degree": "IN" } } } - } + "edge": { "eq": { + "lhs": { "src": { "viewed": { "views": [{ "latest": true }], "expr": { "property": "score" } } } }, + "rhs": { "const": { "i64": 1 } } + } } }) ); } #[test] - fn an_opaque_filter_has_no_wire_form() { - let compiled = tree::FilterExpr::View(vec![ViewOp::Latest]) - .compile() - .unwrap(); - let opaque = tree::FilterExpr::Opaque(tree::OpaqueFilter(compiled)); - let err = GqlFilter::try_from(&opaque).unwrap_err(); - assert!(err.to_string().contains(OPAQUE_FILTER_ERROR), "{err}"); - } - - #[test] - fn a_view_op_that_is_not_applied_is_refused() { + fn a_test_that_is_not_applied_is_refused() { for (op, name) in [ (GqlViewOp::Latest(false), "latest"), (GqlViewOp::SnapshotLatest(false), "snapshotLatest"), @@ -818,9 +924,40 @@ mod tests { let err = ViewOp::try_from(op).unwrap_err(); assert!(err.to_string().contains(name), "{err}"); } + let err = Expr::::try_from(GqlNodeExpr::IsActive(false)).unwrap_err(); + assert!(err.to_string().contains("isActive"), "{err}"); + let err = Expr::::try_from(GqlEdgeExpr::IsValid(false)).unwrap_err(); + assert!(err.to_string().contains("isValid"), "{err}"); + } + + #[test] + fn an_opaque_filter_has_no_wire_form() { + let compiled = F::View(vec![ViewOp::Latest]).compile().unwrap(); + let opaque = F::Opaque(expr::OpaqueFilter(compiled)); + let err = GqlFilter::try_from(&opaque).unwrap_err(); + assert!(err.to_string().contains(OPAQUE_FILTER_ERROR), "{err}"); + } + + #[test] + fn the_python_tree_still_crosses_the_wire() { + let old = tree::FilterExpr::Cmp { + op: CmpOp::Eq, + lhs: tree::Expr::Qual( + tree::Qual::Any, + Box::new(tree::Expr::Temporal(Box::new(tree::Expr::Read { + scope: tree::Scope::new(tree::Entity::Node), + target: tree::Target::Property("p".into()), + }))), + ), + rhs: tree::Expr::Const(Prop::I64(1)), + }; + let wire = GqlFilter::try_from(old).unwrap(); assert_eq!( - ViewOp::try_from(GqlViewOp::Latest(true)).unwrap(), - ViewOp::Latest + serde_json::to_value(&wire).unwrap(), + serde_json::json!({ "node": { "any": { "eq": { + "lhs": { "temporalProperty": "p" }, + "rhs": { "const": { "i64": 1 } } + } } } }) ); } } From 4d0e58dedf85338cd02f06b7f046cac11afc5f09 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:35:19 +0100 Subject: [PATCH 126/155] Hold the expression tree in the python filter objects Comparisons, string tests, membership, presence and the structural tests build an Expr on the entity the chain started from; any() and all() follow the comparison, and eq/ne/lt/le/gt/ge exist as methods so a qualifier can follow without brackets. An Expr is accepted wherever a filter is, and the previous tree, which nothing builds any more, is removed. --- docs/user-guide/views/6_filtering.md | 6 +- python/python/raphtory/filter/__init__.pyi | 252 ++-- .../test_edge_property_filter_semantics.py | 4 +- .../test_node_property_filter_semantics.py | 4 +- .../test_edge_endpoint_filters.py | 6 +- .../test_filters/test_edge_property_filter.py | 72 +- .../test_filters/test_exploded_edge_filter.py | 16 +- .../test_filters/test_node_filter.py | 16 +- .../test_filters/test_node_property_filter.py | 96 +- .../parity/test_parity_filters.py | 8 +- python/tests/test_base_install/test_repr.py | 10 +- .../src/client/graphql_transport.rs | 21 +- .../src/model/graph/filter_expr_input.rs | 34 +- raphtory-graphql/src/python/client/mod.rs | 2 +- .../graph/views/filter/model/expr/display.rs | 20 +- .../db/graph/views/filter/model/expr/mod.rs | 100 +- .../db/graph/views/filter/model/expr/tests.rs | 84 +- .../src/db/graph/views/filter/model/mod.rs | 1 - .../src/db/graph/views/filter/model/tree.rs | 1237 ----------------- raphtory/src/python/filter/edge_expr.rs | 149 +- .../src/python/filter/exploded_edge_expr.rs | 95 +- raphtory/src/python/filter/filter_expr.rs | 70 +- raphtory/src/python/filter/graph_filter.rs | 2 +- raphtory/src/python/filter/node_expr.rs | 633 ++++++--- .../types/macros/trait_impl/filter_ops.rs | 2 +- 25 files changed, 1133 insertions(+), 1807 deletions(-) delete mode 100644 raphtory/src/db/graph/views/filter/model/tree.rs diff --git a/docs/user-guide/views/6_filtering.md b/docs/user-guide/views/6_filtering.md index 135542a582..ea66f3d1c2 100644 --- a/docs/user-guide/views/6_filtering.md +++ b/docs/user-guide/views/6_filtering.md @@ -122,8 +122,8 @@ assert [n.name for n in g.filter(early_high).nodes] == ["bob"] `.temporal()` switches a property read from its latest value to its whole history. An aggregate then turns the history back into one value: `.sum()`, `.avg()`, `.min()`, `.max()`, `.first()`, -`.last()`, `.len()`. `.any()` and `.all()` ask whether the comparison holds for any, or every, -value in the history. +`.last()`, `.len()`. Comparing the history itself gives one answer per value; `.any()` and +`.all()`, written after the comparison, ask whether any, or every, answer holds. /// tab | :fontawesome-brands-python: Python @@ -131,7 +131,7 @@ value in the history. total = filter.Node.property("score").temporal().sum() > 8 assert [n.name for n in g.filter(total).nodes] == ["alice"] -ever_low = filter.Node.property("score").temporal().any() < 4 +ever_low = (filter.Node.property("score").temporal() < 4).any() assert [n.name for n in g.filter(ever_low).nodes] == ["alice"] ``` /// diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index de0dbd9af0..af186f6fe5 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -90,6 +90,9 @@ class FilterExpr(object): """ A filter as a tree. The same tree runs locally, is sent to a server, and is what `repr` prints, so there is nothing to keep in step. + + Anywhere a filter is expected, a yes/no [`Expr`] is accepted too: it is the + filter on its own entity. """ def __and__(self, value): @@ -112,11 +115,14 @@ class FilterExpr(object): class Expr(object): """ - A value expression: a field, degree, property, metadata entry or an - aggregate over one. Comparing it to a value or to another expression gives - a [`FilterExpr`]. + A value expression: a field, degree, property, metadata entry, an aggregate + over one, or a yes/no built from them. Comparing it to a value or to another + expression gives a yes/no [`Expr`], which is a filter on its entity. """ + def __and__(self, value): + """Return self&value.""" + def __eq__(self, value): """Return self==value.""" @@ -126,6 +132,9 @@ class Expr(object): def __gt__(self, value): """Return self>value.""" + def __invert__(self): + """~self""" + def __le__(self, value): """Return self<=value.""" @@ -135,9 +144,23 @@ class Expr(object): def __ne__(self, value): """Return self!=value.""" + def __or__(self, value): + """Return self|value.""" + + def __rand__(self, value): + """Return value&self.""" + + def __repr__(self): + """Return repr(self).""" + + def __ror__(self, value): + """Return value|self.""" + def all(self) -> filter.Expr: """ - Requires that **all** elements match when the value is list-like (a temporal history or a list property). + Requires that **all** elements match. Follows a comparison against a + list-like value (a temporal history or a list property): + `(filter.Node.property("p").temporal() > 4).all()`. Returns: filter.Expr: @@ -145,7 +168,9 @@ class Expr(object): def any(self) -> filter.Expr: """ - Requires that **any** element matches when the value is list-like (a temporal history or a list property). + Requires that **any** element matches. Follows a comparison against a + list-like value (a temporal history or a list property): + `(filter.Node.property("p").temporal() > 4).any()`. Returns: filter.Expr: @@ -159,26 +184,38 @@ class Expr(object): filter.Expr: """ - def contains(self, other: Prop | filter.Expr) -> filter.FilterExpr: + def contains(self, other: str | filter.Expr) -> filter.Expr: """ - Checks whether the value's string representation contains the given value. + Checks whether the string value contains the given substring. Arguments: - other (Prop | filter.Expr): Substring that must appear within the value. + other (str | filter.Expr): The substring, or an expression giving it. Returns: - filter.FilterExpr: + filter.Expr: """ - def ends_with(self, other: Prop | filter.Expr) -> filter.FilterExpr: + def ends_with(self, other: str | filter.Expr) -> filter.Expr: """ - Checks whether the value's string representation ends with the given value. + Checks whether the string value ends with the given suffix. Arguments: - other (Prop | filter.Expr): Suffix to check for. + other (str | filter.Expr): The suffix, or an expression giving it. Returns: - filter.FilterExpr: + filter.Expr: + """ + + def eq(self, other: Prop | filter.Expr) -> filter.Expr: + """ + `self == other`, as a method, so a qualifier can follow without brackets: + `filter.Node.property("p").temporal().eq(3).any()`. + + Arguments: + other (Prop | filter.Expr): The value or expression to compare with. + + Returns: + filter.Expr: """ def first(self) -> filter.Expr: @@ -190,21 +227,43 @@ class Expr(object): """ def fuzzy_search( - self, other: Prop | filter.Expr, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: + self, other: str | filter.Expr, levenshtein_distance: int, prefix_match: bool + ) -> filter.Expr: """ - Performs fuzzy matching against the value's string representation, within a Levenshtein distance and with optional prefix matching. + Checks whether the string value is within a Levenshtein distance of the given text. Arguments: - other (Prop | filter.Expr): String to approximately match against. - levenshtein_distance (int): Maximum allowed Levenshtein distance. - prefix_match (bool): Whether to require a matching prefix. + other (str | filter.Expr): The text to match, or an expression giving it. + levenshtein_distance (int): Maximum edit distance for a match. + prefix_match (bool): Whether a prefix match within the distance also passes. Returns: - filter.FilterExpr: + filter.Expr: + """ + + def ge(self, other: Prop | filter.Expr) -> filter.Expr: + """ + `self >= other`, as a method. + + Arguments: + other (Prop | filter.Expr): The value or expression to compare with. + + Returns: + filter.Expr: + """ + + def gt(self, other: Prop | filter.Expr) -> filter.Expr: """ + `self > other`, as a method. - def is_in(self, values: list[Prop]) -> filter.FilterExpr: + Arguments: + other (Prop | filter.Expr): The value or expression to compare with. + + Returns: + filter.Expr: + """ + + def is_in(self, values: list[Prop]) -> filter.Expr: """ Checks whether the value is contained within the given values. @@ -212,18 +271,18 @@ class Expr(object): values (list[Prop]): Values to match against. Returns: - filter.FilterExpr: + filter.Expr: """ - def is_none(self) -> filter.FilterExpr: + def is_none(self) -> filter.Expr: """ - Checks whether the value is `None` / missing. + Checks whether the value is missing. Returns: - filter.FilterExpr: + filter.Expr: """ - def is_not_in(self, values: list[Prop]) -> filter.FilterExpr: + def is_not_in(self, values: list[Prop]) -> filter.Expr: """ Checks whether the value is **not** contained within the given values. @@ -231,15 +290,15 @@ class Expr(object): values (list[Prop]): Values to exclude. Returns: - filter.FilterExpr: + filter.Expr: """ - def is_some(self) -> filter.FilterExpr: + def is_some(self) -> filter.Expr: """ - Checks whether the value is present (not `None`). + Checks whether the value is present. Returns: - filter.FilterExpr: + filter.Expr: """ def last(self) -> filter.Expr: @@ -250,6 +309,17 @@ class Expr(object): filter.Expr: """ + def le(self, other: Prop | filter.Expr) -> filter.Expr: + """ + `self <= other`, as a method. + + Arguments: + other (Prop | filter.Expr): The value or expression to compare with. + + Returns: + filter.Expr: + """ + def len(self) -> filter.Expr: """ Selects the number of elements when the value is list-like. @@ -258,6 +328,17 @@ class Expr(object): filter.Expr: """ + def lt(self, other: Prop | filter.Expr) -> filter.Expr: + """ + `self < other`, as a method. + + Arguments: + other (Prop | filter.Expr): The value or expression to compare with. + + Returns: + filter.Expr: + """ + def max(self) -> filter.Expr: """ Selects the maximum element when the value is list-like. @@ -274,26 +355,37 @@ class Expr(object): filter.Expr: """ - def not_contains(self, other: Prop | filter.Expr) -> filter.FilterExpr: + def ne(self, other: Prop | filter.Expr) -> filter.Expr: """ - Checks whether the value's string representation **does not** contain the given value. + `self != other`, as a method. Arguments: - other (Prop | filter.Expr): Substring that must not appear within the value. + other (Prop | filter.Expr): The value or expression to compare with. Returns: - filter.FilterExpr: + filter.Expr: """ - def starts_with(self, other: Prop | filter.Expr) -> filter.FilterExpr: + def not_contains(self, other: str | filter.Expr) -> filter.Expr: """ - Checks whether the value's string representation starts with the given value. + Checks whether the string value does **not** contain the given substring. Arguments: - other (Prop | filter.Expr): Prefix to check for. + other (str | filter.Expr): The substring, or an expression giving it. Returns: - filter.FilterExpr: + filter.Expr: + """ + + def starts_with(self, other: str | filter.Expr) -> filter.Expr: + """ + Checks whether the string value starts with the given prefix. + + Arguments: + other (str | filter.Expr): The prefix, or an expression giving it. + + Returns: + filter.Expr: """ def sum(self) -> filter.Expr: @@ -310,7 +402,8 @@ class PropertyExpr(Expr): def temporal(self) -> filter.Expr: """ Switches from the property's latest value to its full temporal history, - unlocking the aggregate chain (`sum`, `avg`, `min`, `max`, `any`, ...). + unlocking the aggregate chain (`sum`, `avg`, `min`, `max`, ...) and the + element-wise comparisons `any()` / `all()` collapse. Returns: filter.Expr: @@ -402,12 +495,12 @@ class Node(object): """ @staticmethod - def is_active() -> filter.FilterExpr: + def is_active() -> filter.Expr: """ Matches nodes that have at least one event in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ @staticmethod @@ -543,9 +636,6 @@ class NodeFilter(object): view, and its own view methods narrow it further. """ - def __new__(cls) -> NodeFilter: - """Create and return a new object. See help(type) for accurate signature.""" - def after(self, time: int) -> filter.NodeFilter: """ Restricts node evaluation to times strictly after the given time. @@ -615,12 +705,12 @@ class NodeFilter(object): filter.Expr: """ - def is_active(self) -> filter.FilterExpr: + def is_active(self) -> filter.Expr: """ Matches nodes that have at least one event in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ def latest(self) -> filter.NodeFilter: @@ -791,39 +881,39 @@ class Edge(object): """ @staticmethod - def is_active() -> filter.FilterExpr: + def is_active() -> filter.Expr: """ Matches edges that have at least one event in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ @staticmethod - def is_deleted() -> filter.FilterExpr: + def is_deleted() -> filter.Expr: """ Matches edges that have been deleted. Returns: - filter.FilterExpr: + filter.Expr: """ @staticmethod - def is_self_loop() -> filter.FilterExpr: + def is_self_loop() -> filter.Expr: """ Matches edges that are self-loops (source == destination). Returns: - filter.FilterExpr: + filter.Expr: """ @staticmethod - def is_valid() -> filter.FilterExpr: + def is_valid() -> filter.Expr: """ Matches edges that are structurally valid in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ @staticmethod @@ -935,9 +1025,6 @@ class EdgeFilter(object): evaluate within that view, and its own view methods narrow it further. """ - def __new__(cls) -> EdgeFilter: - """Create and return a new object. See help(type) for accurate signature.""" - def after(self, time: int) -> filter.EdgeFilter: """ Restricts edge evaluation to times strictly after the given time. @@ -979,36 +1066,36 @@ class EdgeFilter(object): filter.EdgeEndpoint: """ - def is_active(self) -> filter.FilterExpr: + def is_active(self) -> filter.Expr: """ Matches edges that have at least one event in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ - def is_deleted(self) -> filter.FilterExpr: + def is_deleted(self) -> filter.Expr: """ Matches edges that have been deleted. Returns: - filter.FilterExpr: + filter.Expr: """ - def is_self_loop(self) -> filter.FilterExpr: + def is_self_loop(self) -> filter.Expr: """ Matches edges that are self-loops (source == destination). Returns: - filter.FilterExpr: + filter.Expr: """ - def is_valid(self) -> filter.FilterExpr: + def is_valid(self) -> filter.Expr: """ Matches edges that are structurally valid in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ def latest(self) -> filter.EdgeFilter: @@ -1207,39 +1294,39 @@ class ExplodedEdge(object): """ @staticmethod - def is_active() -> filter.FilterExpr: + def is_active() -> filter.Expr: """ Matches exploded edges that have at least one event in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ @staticmethod - def is_deleted() -> filter.FilterExpr: + def is_deleted() -> filter.Expr: """ Matches exploded edges that have been deleted. Returns: - filter.FilterExpr: + filter.Expr: """ @staticmethod - def is_self_loop() -> filter.FilterExpr: + def is_self_loop() -> filter.Expr: """ Matches exploded edges that are self-loops (source == destination). Returns: - filter.FilterExpr: + filter.Expr: """ @staticmethod - def is_valid() -> filter.FilterExpr: + def is_valid() -> filter.Expr: """ Matches exploded edges that are structurally valid in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ @staticmethod @@ -1349,9 +1436,6 @@ class ExplodedEdgeFilter(object): within that view, and its own view methods narrow it further. """ - def __new__(cls) -> ExplodedEdgeFilter: - """Create and return a new object. See help(type) for accurate signature.""" - def after(self, time: int) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to times strictly after the given time. @@ -1385,36 +1469,36 @@ class ExplodedEdgeFilter(object): filter.ExplodedEdgeFilter: """ - def is_active(self) -> filter.FilterExpr: + def is_active(self) -> filter.Expr: """ Matches exploded edges that have at least one event in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ - def is_deleted(self) -> filter.FilterExpr: + def is_deleted(self) -> filter.Expr: """ Matches exploded edges that have been deleted. Returns: - filter.FilterExpr: + filter.Expr: """ - def is_self_loop(self) -> filter.FilterExpr: + def is_self_loop(self) -> filter.Expr: """ Matches exploded edges that are self-loops (source == destination). Returns: - filter.FilterExpr: + filter.Expr: """ - def is_valid(self) -> filter.FilterExpr: + def is_valid(self) -> filter.Expr: """ Matches exploded edges that are structurally valid in the current view. Returns: - filter.FilterExpr: + filter.Expr: """ def latest(self) -> filter.ExplodedEdgeFilter: diff --git a/python/tests/test_base_install/test_filters/semantics/test_edge_property_filter_semantics.py b/python/tests/test_base_install/test_filters/semantics/test_edge_property_filter_semantics.py index 1e7e52832a..3d7e9d7fe6 100644 --- a/python/tests/test_base_install/test_filters/semantics/test_edge_property_filter_semantics.py +++ b/python/tests/test_base_install/test_filters/semantics/test_edge_property_filter_semantics.py @@ -47,7 +47,7 @@ def check(graph): @with_variants(init_edges_graph, variants=["graph"]) def test_temporal_any_semantics(): def check(graph): - filter_expr = filter.Edge.property("p1").temporal().any() == 1 + filter_expr = (filter.Edge.property("p1").temporal() == 1).any() result_ids = sorted(graph.filter(filter_expr).edges.id) expected_ids = sorted( [ @@ -72,7 +72,7 @@ def check(graph): ) def test_temporal_any_semantics_for_event_ids(): def check(graph): - filter_expr = filter.Edge.property("p1").temporal().any() == 1 + filter_expr = (filter.Edge.property("p1").temporal() == 1).any() result_ids = sorted(graph.filter(filter_expr).edges.id) expected_ids = sorted( [ diff --git a/python/tests/test_base_install/test_filters/semantics/test_node_property_filter_semantics.py b/python/tests/test_base_install/test_filters/semantics/test_node_property_filter_semantics.py index 67ce669836..c51475f597 100644 --- a/python/tests/test_base_install/test_filters/semantics/test_node_property_filter_semantics.py +++ b/python/tests/test_base_install/test_filters/semantics/test_node_property_filter_semantics.py @@ -32,7 +32,7 @@ def check(graph): @with_variants(init_nodes_graph) def test_temporal_any_semantics(): def check(graph): - filter_expr = filter.Node.property("p1").temporal().any() == 1 + filter_expr = (filter.Node.property("p1").temporal() == 1).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted(["N1", "N2", "N3", "N4", "N5", "N6", "N7", "N8"]) assert result_ids == expected_ids @@ -45,7 +45,7 @@ def check(graph): ) def test_temporal_any_semantics_for_event_ids(): def check(graph): - filter_expr = filter.Node.property("p1").temporal().any() == 1 + filter_expr = (filter.Node.property("p1").temporal() == 1).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = sorted( ["N1", "N16", "N17", "N2", "N3", "N4", "N5", "N6", "N7", "N8"] diff --git a/python/tests/test_base_install/test_filters/test_edge_endpoint_filters.py b/python/tests/test_base_install/test_filters/test_edge_endpoint_filters.py index ae953e1d30..593a05e323 100644 --- a/python/tests/test_base_install/test_filters/test_edge_endpoint_filters.py +++ b/python/tests/test_base_install/test_filters/test_edge_endpoint_filters.py @@ -31,7 +31,7 @@ def check(graph): def test_edges_dst_property_any_contains(): def check(graph): # dst node "3" has p20 == "Gold_boat"; dst node "4" has p20 updated from Gold_boat to Gold_ship so it appears with .temporal().any() - expr = filter.Edge.dst().property("p20").temporal().any().contains("boat") + expr = (filter.Edge.dst().property("p20").temporal().contains("boat")).any() result = sorted(graph.filter(expr).edges.id) expected = sorted([("2", "3"), ("3", "4")]) assert result == expected @@ -67,7 +67,7 @@ def check(graph): def test_edges_src_property_any_equals(): def check(graph): # src node "d" doesn't exist as src; src of edges are a,b,c; node "a" has prop8 = [2,3,3] - expr = filter.Edge.src().property("prop8").temporal().any().any() == 3 + expr = (filter.Edge.src().property("prop8").temporal() == 3).any().any() result = sorted(graph.filter(expr).edges.id) expected = sorted([("a", "d")]) assert result == expected @@ -103,7 +103,7 @@ def check(graph): def test_edges_src_property_and_edge_property(): def check(graph): expr = (filter.Edge.src().property("p2") == 2) & ( - filter.Edge.property("p20").temporal().any().contains("ship") + (filter.Edge.property("p20").temporal().contains("ship")).any() ) result = sorted(graph.filter(expr).edges.id) expected = sorted([("2", "3")]) diff --git a/python/tests/test_base_install/test_filters/test_edge_property_filter.py b/python/tests/test_base_install/test_filters/test_edge_property_filter.py index c443014fad..ea5bbfe67a 100644 --- a/python/tests/test_base_install/test_filters/test_edge_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_property_filter.py @@ -213,7 +213,9 @@ def check(graph): expected_ids = [("1", "2"), ("2", "1"), ("2", "3")] assert result_ids == expected_ids - filter_expr = filter.Edge.property("p10").temporal().any().starts_with("Paper") + filter_expr = ( + filter.Edge.property("p10").temporal().starts_with("Paper") + ).any() result_ids = sorted(graph.filter(filter_expr).edges.id) expected_ids = [("1", "2"), ("2", "1"), ("2", "3")] assert result_ids == expected_ids @@ -240,7 +242,7 @@ def check(graph): ] assert result_ids == expected_ids - filter_expr = filter.Edge.property("p20").temporal().all().starts_with("Gold") + filter_expr = (filter.Edge.property("p20").temporal().starts_with("Gold")).all() result_ids = sorted(graph.filter(filter_expr).edges.id) expected_ids = [ ("1", "2"), @@ -268,7 +270,7 @@ def check(graph): expected_ids = [("2", "3")] assert result_ids == expected_ids - filter_expr = filter.Edge.property("p10").temporal().any().ends_with("lane") + filter_expr = (filter.Edge.property("p10").temporal().ends_with("lane")).any() result_ids = sorted(graph.filter(filter_expr).edges.id) expected_ids = [("1", "2"), ("2", "1")] assert result_ids == expected_ids @@ -288,7 +290,7 @@ def check(graph): expected_ids = [("2", "3"), ("David Gilmour", "John Mayer")] assert result_ids == expected_ids - filter_expr = filter.Edge.property("p20").temporal().all().ends_with("ship") + filter_expr = (filter.Edge.property("p20").temporal().ends_with("ship")).all() result_ids = sorted(graph.filter(filter_expr).edges.id) expected_ids = [("1", "2"), ("John Mayer", "Jimmy Page")] assert result_ids == expected_ids @@ -311,7 +313,7 @@ def check(graph): expected_ids = [("1", "2"), ("2", "1"), ("2", "3")] assert result_ids == expected_ids - filter_expr = filter.Edge.property("p10").temporal().any().contains("Paper") + filter_expr = (filter.Edge.property("p10").temporal().contains("Paper")).any() result_ids = sorted(graph.filter(filter_expr).edges.id) expected_ids = [("1", "2"), ("2", "1"), ("2", "3")] assert result_ids == expected_ids @@ -344,7 +346,9 @@ def check(graph): expected_ids = [("1", "2"), ("2", "1")] assert result_ids == expected_ids - filter_expr = filter.Edge.property("p10").temporal().any().not_contains("ship") + filter_expr = ( + filter.Edge.property("p10").temporal().not_contains("ship") + ).any() result_ids = sorted(graph.filter(filter_expr).edges.id) expected_ids = [("1", "2"), ("2", "1")] assert result_ids == expected_ids @@ -817,7 +821,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_all_sum_i64s(): def check(graph): - expr = filter.Edge.property("p_i64s").temporal().all().sum() == Prop.i64(-3) + expr = (filter.Edge.property("p_i64s").temporal().sum() == Prop.i64(-3)).all() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("a", "b")} @@ -827,7 +831,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_all_avg_f32s(): def check(graph): - expr = filter.Edge.property("p_f32s").temporal().all().avg() == Prop.f64(2.0) + expr = (filter.Edge.property("p_f32s").temporal().avg() == Prop.f64(2.0)).all() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("a", "b")} @@ -837,7 +841,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_all_min_u64s(): def check(graph): - expr = filter.Edge.property("p_u64s").temporal().all().min() == Prop.u64(1) + expr = (filter.Edge.property("p_u64s").temporal().min() == Prop.u64(1)).all() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("a", "b"), ("d", "a")} @@ -847,9 +851,9 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_all_max_u32s(): def check(graph): - expr = filter.Edge.property("p_u32s").temporal().all().max() == Prop.u32( - 3_000_000 - ) + expr = ( + filter.Edge.property("p_u32s").temporal().max() == Prop.u32(3_000_000) + ).all() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("b", "c")} @@ -859,7 +863,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_all_len_u16s(): def check(graph): - expr = filter.Edge.property("p_u16s").temporal().all().len() == Prop.u64(2) + expr = (filter.Edge.property("p_u16s").temporal().len() == Prop.u64(2)).all() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("a", "b")} @@ -921,7 +925,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_any_sum_u64s(): def check(graph): - expr = filter.Edge.property("p_u64s").temporal().any().sum() == Prop.u64(3) + expr = (filter.Edge.property("p_u64s").temporal().sum() == Prop.u64(3)).any() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("a", "b")} @@ -931,7 +935,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_any_avg_i32s(): def check(graph): - expr = filter.Edge.property("p_i32s").temporal().any().avg() == Prop.f64(0.0) + expr = (filter.Edge.property("p_i32s").temporal().avg() == Prop.f64(0.0)).any() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("d", "a")} @@ -941,7 +945,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_any_min_f32s(): def check(graph): - expr = filter.Edge.property("p_f32s").temporal().any().min() == Prop.f32(-1.5) + expr = (filter.Edge.property("p_f32s").temporal().min() == Prop.f32(-1.5)).any() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("d", "a")} @@ -951,7 +955,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_any_max_u8s(): def check(graph): - expr = filter.Edge.property("p_u8s").temporal().any().max() == Prop.u8(U8_MAX) + expr = (filter.Edge.property("p_u8s").temporal().max() == Prop.u8(U8_MAX)).any() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("d", "a")} @@ -961,7 +965,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_temporal_any_len_f64s(): def check(graph): - expr = filter.Edge.property("p_f64s").temporal().any().len() == Prop.u64(3) + expr = (filter.Edge.property("p_f64s").temporal().len() == Prop.u64(3)).any() pairs = _pairs(graph.filter(expr).edges) assert pairs == {("d", "a")} @@ -1060,7 +1064,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_any(): def check(graph): - expr = filter.Edge.property("p_u8s").any() == Prop.u8(2) + expr = (filter.Edge.property("p_u8s") == Prop.u8(2)).any() assert _pairs(graph.filter(expr).edges) == {("a", "b")} return check @@ -1069,7 +1073,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_property_all(): def check(graph): - expr = filter.Edge.property("p_bools").all() == Prop.bool(True) + expr = (filter.Edge.property("p_bools") == Prop.bool(True)).all() assert _pairs(graph.filter(expr).edges) == {("b", "c")} return check @@ -1078,7 +1082,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_temporal_property_first_any(): def check(graph): - expr = filter.Edge.property("p_u64s").temporal().first().any() == Prop.u64(2) + expr = (filter.Edge.property("p_u64s").temporal().first() == Prop.u64(2)).any() assert _pairs(graph.filter(expr).edges) == {("a", "b")} return check @@ -1087,9 +1091,9 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_temporal_property_first_all(): def check(graph): - expr = filter.Edge.property("p_bools").temporal().first().all() == Prop.bool( - True - ) + expr = ( + filter.Edge.property("p_bools").temporal().first() == Prop.bool(True) + ).all() assert _pairs(graph.filter(expr).edges) == {("b", "c")} return check @@ -1098,7 +1102,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_temporal_property_last_any(): def check(graph): - expr = filter.Edge.property("p_i64s").temporal().last().any() == Prop.i64(-2) + expr = (filter.Edge.property("p_i64s").temporal().last() == Prop.i64(-2)).any() assert _pairs(graph.filter(expr).edges) == {("a", "b")} return check @@ -1107,7 +1111,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_temporal_property_last_all(): def check(graph): - expr = filter.Edge.property("p_f32s").temporal().last().all() == Prop.f32(3.0) + expr = (filter.Edge.property("p_f32s").temporal().last() == Prop.f32(3.0)).all() assert _pairs(graph.filter(expr).edges) == {("b", "c")} return check @@ -1116,8 +1120,8 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_temporal_property_all_any(): def check(graph): - expr = filter.Edge.property("p_bools").temporal().all().any() == Prop.bool( - False + expr = ( + (filter.Edge.property("p_bools").temporal() == Prop.bool(False)).any().all() ) assert _pairs(graph.filter(expr).edges) == {("a", "b"), ("d", "a")} @@ -1127,8 +1131,8 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_temporal_property_all_all(): def check(graph): - expr = filter.Edge.property("p_bools").temporal().all().all() == Prop.bool( - False + expr = ( + (filter.Edge.property("p_bools").temporal() == Prop.bool(False)).all().all() ) assert _pairs(graph.filter(expr).edges) == {("d", "a")} @@ -1138,7 +1142,7 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_temporal_property_any_any(): def check(graph): - expr = filter.Edge.property("p_strs").temporal().any().any() == Prop.str("x") + expr = (filter.Edge.property("p_strs").temporal() == Prop.str("x")).any().any() assert _pairs(graph.filter(expr).edges) == {("b", "c")} return check @@ -1147,8 +1151,10 @@ def check(graph): @with_variants(create_test_graph2) def test_edge_temporal_property_any_all(): def check(graph): - expr = filter.Edge.property("p_strs").temporal().any().all() == Prop.str( - "longword" + expr = ( + (filter.Edge.property("p_strs").temporal() == Prop.str("longword")) + .all() + .any() ) assert _pairs(graph.filter(expr).edges) == {("d", "a")} diff --git a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py index e39e1e2960..fd469e4ded 100644 --- a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py @@ -852,14 +852,14 @@ def test_temporal_constant(GraphClass): # Temporal shoudl act exactly the same as non-temporal test_cases = [ - (filter.ExplodedEdge.property("weight").temporal().any() == 2, 2), - (filter.ExplodedEdge.property("weight").temporal().any() != 3, 4), - (filter.ExplodedEdge.property("weight").temporal().any() < 3, 4), - (filter.ExplodedEdge.property("weight").temporal().any() > 1, 4), - (filter.ExplodedEdge.property("weight").temporal().any() <= 2, 4), - (filter.ExplodedEdge.property("weight").temporal().any() >= 3, 2), - (filter.ExplodedEdge.property("weight").temporal().any().is_in([1, 2]), 4), - (filter.ExplodedEdge.property("weight").temporal().any().is_not_in([3]), 4), + ((filter.ExplodedEdge.property("weight").temporal() == 2).any(), 2), + ((filter.ExplodedEdge.property("weight").temporal() != 3).any(), 4), + ((filter.ExplodedEdge.property("weight").temporal() < 3).any(), 4), + ((filter.ExplodedEdge.property("weight").temporal() > 1).any(), 4), + ((filter.ExplodedEdge.property("weight").temporal() <= 2).any(), 4), + ((filter.ExplodedEdge.property("weight").temporal() >= 3).any(), 2), + ((filter.ExplodedEdge.property("weight").temporal().is_in([1, 2])).any(), 4), + ((filter.ExplodedEdge.property("weight").temporal().is_not_in([3])).any(), 4), (filter.ExplodedEdge.property("weight").temporal().is_some(), 6), (filter.ExplodedEdge.property("weight").temporal().is_none(), 0), (filter.ExplodedEdge.property("weight").temporal().last() == 2, 2), diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index 0dd9f86c21..230c0ab386 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -412,8 +412,8 @@ def test_degree_filter_with_invalid_expressions(): lambda: filter.Node.out_degree().contains("1"), lambda: filter.Node.out_degree().not_contains("1"), lambda: filter.Node.out_degree().fuzzy_search("1", 1, False), - lambda: filter.Node.degree().any() == 1, - lambda: filter.Node.degree().all() == 1, + lambda: (filter.Node.degree() == 1).any(), + lambda: (filter.Node.degree() == 1).all(), lambda: filter.Node.degree().len() > 0, lambda: filter.Node.degree().sum() == 1, lambda: filter.Node.degree().avg() == 1, @@ -421,8 +421,8 @@ def test_degree_filter_with_invalid_expressions(): lambda: filter.Node.degree().max() == 1, lambda: filter.Node.degree().first() == 1, lambda: filter.Node.degree().last() == 1, - lambda: filter.Node.in_degree().any() == 1, - lambda: filter.Node.in_degree().all() == 1, + lambda: (filter.Node.in_degree() == 1).any(), + lambda: (filter.Node.in_degree() == 1).all(), lambda: filter.Node.in_degree().len() > 0, lambda: filter.Node.in_degree().sum() == 1, lambda: filter.Node.in_degree().avg() == 1, @@ -430,8 +430,8 @@ def test_degree_filter_with_invalid_expressions(): lambda: filter.Node.in_degree().max() == 1, lambda: filter.Node.in_degree().first() == 1, lambda: filter.Node.in_degree().last() == 1, - lambda: filter.Node.out_degree().any() == 1, - lambda: filter.Node.out_degree().all() == 1, + lambda: (filter.Node.out_degree() == 1).any(), + lambda: (filter.Node.out_degree() == 1).all(), lambda: filter.Node.out_degree().len() > 0, lambda: filter.Node.out_degree().sum() == 1, lambda: filter.Node.out_degree().avg() == 1, @@ -554,7 +554,7 @@ def test_node_type_comparison_to_a_non_string_type_is_a_python_error(): with pytest.raises(TypeError): filter.Node.node_type() != 5 # A correctly typed comparison still builds an expression. - assert isinstance(filter.Node.node_type() == "person", filter.FilterExpr) + assert isinstance(filter.Node.node_type() == "person", filter.Expr) @with_variants(init_graph) @@ -948,7 +948,7 @@ def check(graph): # The expression builds (the python surface is one Expr type); applying # it rejects the qualifier on a scalar field. with pytest.raises(Exception, match=r"cannot be compared with Str"): - filter_expr = filter.Node.name().all() == True + filter_expr = (filter.Node.name() == True).all() graph.filter(filter_expr).nodes.id return check diff --git a/python/tests/test_base_install/test_filters/test_node_property_filter.py b/python/tests/test_base_install/test_filters/test_node_property_filter.py index 71c8a43d7c..800efb2ac8 100644 --- a/python/tests/test_base_install/test_filters/test_node_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_property_filter.py @@ -145,12 +145,12 @@ def check(graph): expected_ids = ["1", "2", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.property("p10").temporal().any().starts_with("Pap") + filter_expr = (filter.Node.property("p10").temporal().starts_with("Pap")).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "2", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.property("p10").temporal().any().starts_with("Cap") + filter_expr = (filter.Node.property("p10").temporal().starts_with("Cap")).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids @@ -172,7 +172,7 @@ def check(graph): expected_ids = ["2"] assert result_ids == expected_ids - filter_expr = filter.Node.property("p20").temporal().all().starts_with("Gold") + filter_expr = (filter.Node.property("p20").temporal().starts_with("Gold")).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3", "4"] assert result_ids == expected_ids @@ -193,7 +193,7 @@ def check(graph): expected_ids = [] assert result_ids == expected_ids - filter_expr = filter.Node.property("p10").temporal().any().ends_with("lane") + filter_expr = (filter.Node.property("p10").temporal().ends_with("lane")).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids @@ -208,7 +208,7 @@ def check(graph): expected_ids = ["3", "4"] assert result_ids == expected_ids - filter_expr = filter.Node.property("p20").temporal().all().ends_with("ship") + filter_expr = (filter.Node.property("p20").temporal().ends_with("ship")).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "2"] assert result_ids == expected_ids @@ -231,7 +231,7 @@ def check(graph): expected_ids = ["1", "2", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.property("p10").temporal().any().contains("Paper") + filter_expr = (filter.Node.property("p10").temporal().contains("Paper")).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "2", "3"] assert result_ids == expected_ids @@ -264,7 +264,9 @@ def check(graph): expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.property("p10").temporal().any().not_contains("ship") + filter_expr = ( + filter.Node.property("p10").temporal().not_contains("ship") + ).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids @@ -418,12 +420,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_any_property_sum(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().any().sum() == 12 + filter_expr = (filter.Node.property("prop6").temporal().sum() == 12).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop6").temporal().any().sum() == 6 + filter_expr = (filter.Node.property("prop6").temporal().sum() == 6).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -434,12 +436,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_any_property_avg(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().any().avg() == 2.0 + filter_expr = (filter.Node.property("prop6").temporal().avg() == 2.0).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop6").temporal().any().avg() == 4.0 + filter_expr = (filter.Node.property("prop6").temporal().avg() == 4.0).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -450,12 +452,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_any_property_min(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().any().min() == 1 + filter_expr = (filter.Node.property("prop6").temporal().min() == 1).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop6").temporal().any().min() == 3 + filter_expr = (filter.Node.property("prop6").temporal().min() == 3).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -466,12 +468,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_any_property_max(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().any().max() == 3 + filter_expr = (filter.Node.property("prop6").temporal().max() == 3).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop6").temporal().any().max() == 5 + filter_expr = (filter.Node.property("prop6").temporal().max() == 5).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -482,9 +484,9 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_any_property_len(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().any().len() == Prop.u64( - 3 - ) + filter_expr = ( + filter.Node.property("prop6").temporal().len() == Prop.u64(3) + ).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -495,12 +497,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_all_property_sum(): def check(graph): - filter_expr = filter.Node.property("prop5").temporal().all().sum() == 6 + filter_expr = (filter.Node.property("prop5").temporal().sum() == 6).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop6").temporal().all().sum() == 6 + filter_expr = (filter.Node.property("prop6").temporal().sum() == 6).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids @@ -511,12 +513,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_all_property_avg(): def check(graph): - filter_expr = filter.Node.property("prop5").temporal().all().avg() == 2.0 + filter_expr = (filter.Node.property("prop5").temporal().avg() == 2.0).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop6").temporal().all().avg() == 2.0 + filter_expr = (filter.Node.property("prop6").temporal().avg() == 2.0).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids @@ -527,12 +529,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_all_property_min(): def check(graph): - filter_expr = filter.Node.property("prop5").temporal().all().min() == 1 + filter_expr = (filter.Node.property("prop5").temporal().min() == 1).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop6").temporal().all().min() == 1 + filter_expr = (filter.Node.property("prop6").temporal().min() == 1).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids @@ -543,12 +545,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_all_property_max(): def check(graph): - filter_expr = filter.Node.property("prop5").temporal().all().max() == 3 + filter_expr = (filter.Node.property("prop5").temporal().max() == 3).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop6").temporal().all().max() == 3 + filter_expr = (filter.Node.property("prop6").temporal().max() == 3).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = [] assert result_ids == expected_ids @@ -559,9 +561,9 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_all_property_len(): def check(graph): - filter_expr = filter.Node.property("prop5").temporal().all().len() == Prop.u64( - 3 - ) + filter_expr = ( + filter.Node.property("prop5").temporal().len() == Prop.u64(3) + ).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a", "c"] assert result_ids == expected_ids @@ -662,7 +664,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_metadata_any(): def check(graph): - filter_expr = filter.Node.metadata("prop2").any() == -2 + filter_expr = (filter.Node.metadata("prop2") == -2).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a", "b"] assert result_ids == expected_ids @@ -673,7 +675,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_metadata_all(): def check(graph): - filter_expr = filter.Node.metadata("prop4").all() > 10 + filter_expr = (filter.Node.metadata("prop4") > 10).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["b"] assert result_ids == expected_ids @@ -684,7 +686,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_property_any(): def check(graph): - filter_expr = filter.Node.property("prop8").any().is_in([3]) + filter_expr = (filter.Node.property("prop8").is_in([3])).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a", "d"] assert result_ids == expected_ids @@ -695,7 +697,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_property_all(): def check(graph): - filter_expr = filter.Node.property("prop8").all() == 3 + filter_expr = (filter.Node.property("prop8") == 3).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["d"] assert result_ids == expected_ids @@ -706,7 +708,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporary_property_first_any(): def check(graph): - filter_expr = filter.Node.property("prop8").temporal().first().any() == 3 + filter_expr = (filter.Node.property("prop8").temporal().first() == 3).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["d"] assert result_ids == expected_ids @@ -717,7 +719,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporary_property_first_all(): def check(graph): - filter_expr = filter.Node.property("prop8").temporal().first().all() == 2 + filter_expr = (filter.Node.property("prop8").temporal().first() == 2).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -728,7 +730,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporary_property_last_any(): def check(graph): - filter_expr = filter.Node.property("prop8").temporal().last().any() == 3 + filter_expr = (filter.Node.property("prop8").temporal().last() == 3).any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a", "d"] assert result_ids == expected_ids @@ -739,12 +741,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporary_property_last_all(): def check(graph): - filter_expr = filter.Node.property("prop8").temporal().last().all() > 1 + filter_expr = (filter.Node.property("prop8").temporal().last() > 1).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a", "d"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop8").temporal().last().all() > 2 + filter_expr = (filter.Node.property("prop8").temporal().last() > 2).all() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["d"] assert result_ids == expected_ids @@ -755,12 +757,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporary_property_any_any(): def check(graph): - filter_expr = filter.Node.property("prop8").temporal().any().any() == 3 + filter_expr = (filter.Node.property("prop8").temporal() == 3).any().any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a", "d"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop9").temporal().any().any() == 3 + filter_expr = (filter.Node.property("prop9").temporal() == 3).any().any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -771,12 +773,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporary_property_any_all(): def check(graph): - filter_expr = filter.Node.property("prop8").temporal().any().all() == 2 + filter_expr = (filter.Node.property("prop8").temporal() == 2).all().any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop8").temporal().any().all() > 2 + filter_expr = (filter.Node.property("prop8").temporal() > 2).all().any() result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["d"] assert result_ids == expected_ids @@ -790,7 +792,7 @@ def check(graph): # A string constant never compares against a numeric property, # whether or not it happens to parse as a number. for value in ["3", "pometry"]: - filter_expr = filter.Node.property("prop8").any() == value + filter_expr = (filter.Node.property("prop8") == value).any() with pytest.raises( Exception, match=r"of type Str cannot be compared with List", @@ -805,7 +807,7 @@ def test_filter_nodes_with_with_qualifier_alongside_illegal_operators(): def check(graph): # Presence is a whole-value question; a qualifier has nothing to # collapse on a single yes/no. - filter_expr = filter.Node.property("prop8").any().is_some() + filter_expr = (filter.Node.property("prop8").is_some()).any() with pytest.raises(Exception, match=r"any\(\)/all\(\) collapse"): graph.filter(filter_expr).nodes.id filter_expr = filter.Node.property("prop8").is_some() @@ -817,14 +819,14 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_with_with_qualifier_alongside_illegal_agg_operators(): def check(graph): - filter_expr = filter.Node.property("prop8").all().len() > 0 + filter_expr = (filter.Node.property("prop8").len() > 0).all() with pytest.raises( Exception, match=r"any\(\)/all\(\) collapse", ): graph.filter(filter_expr).nodes.id - filter_expr = filter.Node.property("prop8").sum().any() > 0 + filter_expr = (filter.Node.property("prop8").sum() > 0).any() with pytest.raises( Exception, match=r"any\(\)/all\(\) collapse", @@ -1046,7 +1048,7 @@ def check(graph): @with_variants(init_graph) def test_prop_not_found_error(): def check(graph): - filter_expr = filter.Node.property("p").any().is_some() + filter_expr = (filter.Node.property("p").is_some()).any() with pytest.raises( Exception, match=r"Property p does not exist", diff --git a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index cfa7f29512..f1e0ec7177 100644 --- a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py +++ b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py @@ -296,8 +296,8 @@ def _probe_path_from_graph(h): "node.metadata.is_none": lambda: f.Node.metadata("region").is_none(), "node.metadata.contains": lambda: f.Node.metadata("region").contains("a"), "node.prop.level_eq": lambda: f.Node.property("level") == "bronze", - "node.temporal.any": lambda: f.Node.property("score").temporal().any() > 50, - "node.temporal.all": lambda: f.Node.property("score").temporal().all() > 5, + "node.temporal.any": lambda: (f.Node.property("score").temporal() > 50).any(), + "node.temporal.all": lambda: (f.Node.property("score").temporal() > 5).all(), "node.temporal.first": (lambda: f.Node.property("score").temporal().first() > 15), "node.temporal.last": (lambda: f.Node.property("score").temporal().last() > 15), "node.temporal.min": lambda: f.Node.property("score").temporal().min() > 5, @@ -373,7 +373,7 @@ def _probe_path_from_graph(h): "edge.metadata.is_some": lambda: f.Edge.metadata("kind").is_some(), "edge.metadata.is_none": lambda: f.Edge.metadata("kind").is_none(), "edge.prop.note_eq": lambda: f.Edge.property("note") == "zz", - "edge.temporal.any": (lambda: f.Edge.property("weight").temporal().any() > 3.0), + "edge.temporal.any": (lambda: (f.Edge.property("weight").temporal() > 3.0).any()), "edge.temporal.avg": (lambda: f.Edge.property("weight").temporal().avg() > 3.0), "edge.temporal.first": (lambda: f.Edge.property("weight").temporal().first() > 2.0), "edge.temporal.last": (lambda: f.Edge.property("weight").temporal().last() > 2.0), @@ -791,7 +791,7 @@ def test_property_sources_are_distinct(filter_pair): aggregated = lambda g: _names(g.filter(f.Node.property("level") == "bronze")) crossed = lambda g: _names(g.filter(f.Node.property("level") == "gold")) temporal = lambda g: _names( - g.filter(f.Node.property("level").temporal().any() == "bronze") + g.filter((f.Node.property("level").temporal() == "bronze").any()) ) for read in (metadata, aggregated, crossed, temporal): diff --git a/python/tests/test_base_install/test_repr.py b/python/tests/test_base_install/test_repr.py index 7bff9a43b0..f9de20051d 100644 --- a/python/tests/test_base_install/test_repr.py +++ b/python/tests/test_base_install/test_repr.py @@ -66,12 +66,12 @@ def test_persistent_graph(self): class FilterExprReprTest(TestCase): - """`repr` shows the filter tree, which is both what runs locally and what a - server receives.""" + """`repr` shows the expression tree, which is both what runs locally and + what a server receives.""" def test_repr_shows_the_recorded_wire_form(self): expr = filter.Node.window(0, 5).property("score") > 4 - self.assertEqual(repr(expr), "FilterExpr(WINDOW[0..5](score) > 4)") + self.assertEqual(repr(expr), "Expr(WINDOW[0..5](score) > 4)") def test_repr_shows_temporal_ops_and_combinators(self): expr = (filter.Node.property("score").temporal().sum() > 10) & ~( @@ -79,9 +79,9 @@ def test_repr_shows_temporal_ops_and_combinators(self): ) self.assertEqual( repr(expr), - "FilterExpr((sum(temporal(score)) > 10 AND NOT(name == carol)))", + "Expr((SUM(TEMPORAL(score)) > 10) AND (NOT(name == carol)))", ) def test_repr_shows_expressions_on_both_sides(self): expr = filter.Node.degree() > filter.Node.in_degree() - self.assertEqual(repr(expr), "FilterExpr(degree > in_degree)") + self.assertEqual(repr(expr), "Expr(degree > in_degree)") diff --git a/raphtory-graphql/src/client/graphql_transport.rs b/raphtory-graphql/src/client/graphql_transport.rs index 3dc0f0ccb4..03eff0e8e1 100644 --- a/raphtory-graphql/src/client/graphql_transport.rs +++ b/raphtory-graphql/src/client/graphql_transport.rs @@ -4956,9 +4956,7 @@ mod tests { use raphtory::{ db::{ api::storage::storage::Config, - graph::views::filter::model::tree::{ - CmpOp, Entity, Expr, FilterExpr, Scope, Target, - }, + graph::views::filter::model::expr::{CmpOp, Expr, FilterExpr, NodeLeaf}, }, prelude::Prop, }; @@ -5053,14 +5051,15 @@ mod tests { // select() narrows membership only — handles see the unfiltered graph. // Passed as a tree to pin that tree-typed callers satisfy the widened // `TryInto` bound. - let score_gt_15_tree = FilterExpr::Cmp { - op: CmpOp::Gt, - lhs: Expr::Read { - scope: Scope::new(Entity::Node), - target: Target::Property("score".into()), - }, - rhs: Expr::Const(Prop::I64(15)), - }; + let score_gt_15_tree = FilterExpr::Node(Expr::Cmp( + CmpOp::Gt, + Box::new(Expr::Read(NodeLeaf::Property { + views: Vec::new(), + name: "score".into(), + temporal: false, + })), + Box::new(Expr::Const(Prop::I64(15))), + )); let selected = rg.nodes().select(score_gt_15_tree).unwrap(); let mut selected_ids = selected.id().await.unwrap(); selected_ids.sort(); diff --git a/raphtory-graphql/src/model/graph/filter_expr_input.rs b/raphtory-graphql/src/model/graph/filter_expr_input.rs index a19606f4fc..8a86b222c8 100644 --- a/raphtory-graphql/src/model/graph/filter_expr_input.rs +++ b/raphtory-graphql/src/model/graph/filter_expr_input.rs @@ -32,7 +32,7 @@ use raphtory::{ self, Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, Leaf, NodeLeaf, StrOp, ViewOp, OPAQUE_FILTER_ERROR, }, - tree, DynFilter, + DynFilter, }, CreateFilter, }, @@ -762,15 +762,6 @@ impl TryFrom for GqlFilter { } } -/// The tree python objects still hold, until they move to the per-entity one. -impl TryFrom for GqlFilter { - type Error = GraphError; - - fn try_from(filter: tree::FilterExpr) -> Result { - GqlFilter::try_from(&filter.to_split()?) - } -} - /// The compiled filter, for callers that apply one filter to several handles. impl TryFrom for DynFilter { type Error = GraphError; @@ -937,27 +928,4 @@ mod tests { let err = GqlFilter::try_from(&opaque).unwrap_err(); assert!(err.to_string().contains(OPAQUE_FILTER_ERROR), "{err}"); } - - #[test] - fn the_python_tree_still_crosses_the_wire() { - let old = tree::FilterExpr::Cmp { - op: CmpOp::Eq, - lhs: tree::Expr::Qual( - tree::Qual::Any, - Box::new(tree::Expr::Temporal(Box::new(tree::Expr::Read { - scope: tree::Scope::new(tree::Entity::Node), - target: tree::Target::Property("p".into()), - }))), - ), - rhs: tree::Expr::Const(Prop::I64(1)), - }; - let wire = GqlFilter::try_from(old).unwrap(); - assert_eq!( - serde_json::to_value(&wire).unwrap(), - serde_json::json!({ "node": { "any": { "eq": { - "lhs": { "temporalProperty": "p" }, - "rhs": { "const": { "i64": 1 } } - } } } }) - ); - } } diff --git a/raphtory-graphql/src/python/client/mod.rs b/raphtory-graphql/src/python/client/mod.rs index 47505d76b8..2f326d845c 100644 --- a/raphtory-graphql/src/python/client/mod.rs +++ b/raphtory-graphql/src/python/client/mod.rs @@ -7,7 +7,7 @@ use crate::{ }; use pyo3::{exceptions::PyValueError, prelude::*, pyclass, pymethods}; use raphtory::{ - db::graph::views::filter::model::tree::FilterExpr, errors::GraphError, + db::graph::views::filter::model::expr::FilterExpr, errors::GraphError, python::filter::filter_expr::PyFilterExpr, }; use raphtory_api::{ diff --git a/raphtory/src/db/graph/views/filter/model/expr/display.rs b/raphtory/src/db/graph/views/filter/model/expr/display.rs index 3d57b210f6..c3c8be9d15 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/display.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/display.rs @@ -3,9 +3,27 @@ use super::{ Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, FilterExpr, NodeLeaf, StrOp, ViewOp, }; -use raphtory_api::core::Direction; +use crate::{db::graph::views::filter::model::layered_filter::layer_label, prelude::Layer}; +use raphtory_api::core::{storage::timeindex::AsTime, Direction}; use std::fmt::{self, Display}; +impl Display for ViewOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ViewOp::Window { start, end } => write!(f, "WINDOW[{}..{}]", start.t(), end.t()), + ViewOp::At(t) => write!(f, "AT[{}]", t.t()), + ViewOp::After(t) => write!(f, "AFTER[{}]", t.t()), + ViewOp::Before(t) => write!(f, "BEFORE[{}]", t.t()), + ViewOp::Latest => write!(f, "LATEST"), + ViewOp::SnapshotAt(t) => write!(f, "SNAPSHOT_AT[{}]", t.t()), + ViewOp::SnapshotLatest => write!(f, "SNAPSHOT_LATEST"), + ViewOp::Layers(names) => { + write!(f, "LAYER[{}]", layer_label(&Layer::from(names.clone()))) + } + } + } +} + fn views(f: &mut fmt::Formatter<'_>, views: &[ViewOp], inner: &dyn Display) -> fmt::Result { if views.is_empty() { return write!(f, "{inner}"); diff --git a/raphtory/src/db/graph/views/filter/model/expr/mod.rs b/raphtory/src/db/graph/views/filter/model/expr/mod.rs index 3cdd1e0db8..2e152ec17e 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/mod.rs @@ -27,12 +27,108 @@ mod display; #[cfg(test)] mod tests; -pub use super::tree::{Agg, CmpOp, Field, OpaqueFilter, StrOp, ViewOp, OPAQUE_FILTER_ERROR}; pub use compile::Leaf; pub use convert::{FactoryLeaf, MarkerLeaf, ToExpr, ToFilterExpr}; -use raphtory_api::core::{entities::properties::prop::Prop, Direction}; +use super::DynCreateFilter; +use raphtory_api::core::{ + entities::properties::prop::Prop, storage::timeindex::EventTime, Direction, +}; use serde::{Deserialize, Serialize}; +use std::{fmt, sync::Arc}; + +/// One view restriction, in the order it was applied. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ViewOp { + Window { start: EventTime, end: EventTime }, + At(EventTime), + After(EventTime), + Before(EventTime), + Latest, + SnapshotAt(EventTime), + SnapshotLatest, + Layers(Vec), +} + +/// A built-in node field. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Field { + Id, + Name, + NodeType, +} + +/// A reduction over a list-valued expression. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Agg { + Sum, + Avg, + Min, + Max, + First, + Last, + Len, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CmpOp { + Eq, + Ne, + Lt, + Le, + Gt, + Ge, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum StrOp { + StartsWith, + EndsWith, + Contains, + NotContains, + FuzzySearch { + levenshtein_distance: usize, + prefix_match: bool, + }, +} + +/// An already compiled filter carried inside a tree. It exists for filters +/// built from data that lives only in this process, so it runs but does not +/// serialise: asking for its wire form is an error, not a guess. +#[derive(Clone)] +pub struct OpaqueFilter(pub Arc); + +pub const OPAQUE_FILTER_ERROR: &str = + "this filter has no server-side form; it was built from in-process state"; + +impl fmt::Debug for OpaqueFilter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("OpaqueFilter") + } +} + +impl PartialEq for OpaqueFilter { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Serialize for OpaqueFilter { + fn serialize(&self, _: S) -> Result { + Err(serde::ser::Error::custom(OPAQUE_FILTER_ERROR)) + } +} + +impl<'de> Deserialize<'de> for OpaqueFilter { + fn deserialize>(_: D) -> Result { + Err(serde::de::Error::custom(OPAQUE_FILTER_ERROR)) + } +} /// What every entity can do with a value, whatever the entity reads. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] diff --git a/raphtory/src/db/graph/views/filter/model/expr/tests.rs b/raphtory/src/db/graph/views/filter/model/expr/tests.rs index c084ab48a2..d07bb0cca7 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/tests.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/tests.rs @@ -1,8 +1,15 @@ use super::*; use crate::{ - db::api::view::Filter, - prelude::{AdditionOps, EdgeViewOps, Graph, GraphViewOps, NodeViewOps}, + db::{ + api::view::Filter, + graph::views::filter::model::{ + edge_filter::EdgeFilter, windowed_filter::Windowed, DynCreateFilter, EdgeViewFilterOps, + ViewWrapOps, + }, + }, + prelude::{AdditionOps, EdgeViewOps, Graph, GraphViewOps, NodeViewOps, TimeOps, NO_PROPS}, }; +use std::sync::Arc; use raphtory_api::core::{ entities::properties::prop::IntoProp, storage::timeindex::EventTime, Direction, }; @@ -429,3 +436,76 @@ fn an_opaque_filter_refuses_to_serialise() { let err = serde_json::to_string(&f).unwrap_err().to_string(); assert!(err.contains(OPAQUE_FILTER_ERROR)); } + +#[test] +fn before_and_at_agree_with_the_graph_views() { + // alice→bob @2 (first event at 2) · carol→dave @2 (second event at 2) · eve→fay @5 + let g = Graph::new(); + g.add_edge(2, "alice", "bob", NO_PROPS, None).unwrap(); + g.add_edge(2, "carol", "dave", NO_PROPS, None).unwrap(); + g.add_edge(5, "eve", "fay", NO_PROPS, None).unwrap(); + fn edge_names<'graph, G: GraphViewOps<'graph>>(g: &G) -> Vec { + let mut ids: Vec = g + .edges() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + ids.sort(); + ids + } + let view = |op: ViewOp| FilterExpr::View(vec![op]); + let typed = |f: Windowed| Arc::new(f.is_active()) as Arc; + fn applied(g: &Graph, filter: Arc) -> Vec { + let mut ids: Vec = g + .filter(filter) + .unwrap() + .edges() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + ids.sort(); + ids + } + let none: [&str; 0] = []; + let at_two = ["alice->bob", "carol->dave"]; + + // `before(t)` excludes every event at `t`, like the graph view does. + assert_eq!(edge_names(&g.before(2)), none); + assert_eq!(edges(&g, &view(ViewOp::Before(EventTime::start(2)))), none); + assert_eq!(applied(&g, typed(EdgeFilter.before(2))), none); + assert_eq!(edge_names(&g.before(3)), at_two); + assert_eq!(edges(&g, &view(ViewOp::Before(EventTime::start(3)))), at_two); + + // `at(t)` covers the whole timestamp, even when handed a time that sits + // between two events at `t`. + let mid_two = EventTime::start(2).set_event_id(1); + assert_eq!(edge_names(&g.at(mid_two)), at_two); + assert_eq!(edges(&g, &view(ViewOp::At(mid_two))), at_two); + assert_eq!(applied(&g, typed(EdgeFilter.at(mid_two))), at_two); + + // A window bound that carries an event id is honoured, as the graph view does. + let from_second_event = EventTime::start(2).set_event_id(1); + assert_eq!( + edge_names(&g.window(from_second_event, EventTime::start(3))), + ["carol->dave"] + ); + assert_eq!( + edges(&g, &view(ViewOp::Window { + start: from_second_event, + end: EventTime::start(3), + }), + ), + ["carol->dave"] + ); + assert_eq!( + applied(&g, typed(EdgeFilter.window(from_second_event, 3))), + ["carol->dave"] + ); + + // `after(t)` excludes `t` and everything before it. + assert_eq!(edge_names(&g.after(2)), ["eve->fay"]); + assert_eq!( + edges(&g, &view(ViewOp::After(EventTime::start(2)))), + ["eve->fay"] + ); +} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 7f12b6223d..c1910dfaf4 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -86,7 +86,6 @@ pub mod not_filter; pub mod or_filter; pub mod property_filter; pub mod snapshot_filter; -pub mod tree; pub mod windowed_filter; #[derive(Debug, Copy, Clone)] diff --git a/raphtory/src/db/graph/views/filter/model/tree.rs b/raphtory/src/db/graph/views/filter/model/tree.rs deleted file mode 100644 index bfc8a2fa01..0000000000 --- a/raphtory/src/db/graph/views/filter/model/tree.rs +++ /dev/null @@ -1,1237 +0,0 @@ -//! A filter as data. -//! -//! The typed expression API (`NodeFilter.property("score").gt(4)`) is what -//! rust callers write and what the engine compiles. Everything that has to -//! *carry* a filter — a python object, a GraphQL request, a stored permission -//! grant — needs the same filter as plain data instead. [`Expr`] and -//! [`FilterExpr`] are that data: one node per operation of the typed API, with -//! both sides of a comparison allowed to be expressions. -//! -//! A tree is built wherever the filter is written and compiled wherever it -//! runs, by [`FilterExpr::compile`], which replays the tree onto the typed -//! factories. Because the tree mirrors the API one to one, "what runs" and -//! "what is sent" cannot mean different things. - -use crate::{ - db::{ - api::{ - state::NodeOp, - view::internal::{DynGraphArc, GraphView}, - }, - graph::views::filter::{ - model::{ - edge_filter::Endpoint, - expr::{ - EdgeLeaf, ExplodedEdgeLeaf, Expr as SplitExpr, FilterExpr as SplitFilter, Leaf, - NodeLeaf, - }, - layered_filter::layer_label, - node_expr::DynCreateOp, - DynCreateFilter, - }, - CreateFilter, - }, - }, - errors::GraphError, - prelude::Layer, -}; -use raphtory_api::core::{ - entities::properties::prop::Prop, - storage::timeindex::{AsTime, EventTime}, - Direction, -}; -use serde::{Deserialize, Serialize}; -use std::{ - fmt::{self, Display}, - sync::Arc, -}; - -/// Which kind of entity an expression reads from. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Entity { - Node, - Edge, - ExplodedEdge, -} - -/// One view restriction, in the order it was applied. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ViewOp { - Window { start: EventTime, end: EventTime }, - At(EventTime), - After(EventTime), - Before(EventTime), - Latest, - SnapshotAt(EventTime), - SnapshotLatest, - Layers(Vec), -} - -/// A built-in node field. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Field { - Id, - Name, - NodeType, -} - -/// A reduction over a list-valued expression. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Agg { - Sum, - Avg, - Min, - Max, - First, - Last, - Len, -} - -/// How the elements of a list-valued expression must satisfy a predicate. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Qual { - Any, - All, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CmpOp { - Eq, - Ne, - Lt, - Le, - Gt, - Ge, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum StrOp { - StartsWith, - EndsWith, - Contains, - NotContains, - FuzzySearch { - levenshtein_distance: usize, - prefix_match: bool, - }, -} - -/// A predicate on the state of an entity rather than on a value it holds. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Structural { - IsActive, - IsValid, - IsDeleted, - IsSelfLoop, -} - -/// Where a value is read: which entity, through which views, and for an edge -/// optionally through one of its endpoint nodes. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Scope { - pub entity: Entity, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub views: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint: Option, -} - -impl Scope { - pub fn new(entity: Entity) -> Self { - Scope { - entity, - views: Vec::new(), - endpoint: None, - } - } - - pub fn with_view(mut self, op: ViewOp) -> Self { - self.views.push(op); - self - } - - pub fn through(mut self, endpoint: Endpoint) -> Self { - self.endpoint = Some(endpoint); - self - } -} - -/// What a leaf reads from its scope. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Target { - Field(Field), - Degree(Direction), - Property(String), - Metadata(String), -} - -/// A value: what stands on either side of a comparison. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Expr { - Const(Prop), - Read { scope: Scope, target: Target }, - Temporal(Box), - Agg(Agg, Box), - Qual(Qual, Box), -} - -/// A yes/no: the filter itself. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum FilterExpr { - Cmp { - op: CmpOp, - lhs: Expr, - rhs: Expr, - }, - Str { - op: StrOp, - lhs: Expr, - rhs: Expr, - }, - IsSome(Expr), - IsNone(Expr), - In { - expr: Expr, - values: Vec, - negated: bool, - }, - Structural { - scope: Scope, - pred: Structural, - }, - /// A graph-level view with no predicate: the result *is* the view. - View(Vec), - And(Vec), - Or(Vec), - Not(Box), - /// A filter over in-process state (a node-state column) that has no wire - /// form: it runs where it was built and cannot be sent anywhere. - Opaque(OpaqueFilter), -} - -/// An already compiled filter carried inside a tree. It exists for filters -/// built from data that lives only in this process, so it runs but does not -/// serialise: asking for its wire form is an error, not a guess. -#[derive(Clone)] -pub struct OpaqueFilter(pub Arc); - -pub const OPAQUE_FILTER_ERROR: &str = - "this filter has no server-side form; it was built from in-process state"; - -impl fmt::Debug for OpaqueFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("OpaqueFilter") - } -} - -impl PartialEq for OpaqueFilter { - fn eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) - } -} - -impl Serialize for OpaqueFilter { - fn serialize(&self, _: S) -> Result { - Err(serde::ser::Error::custom(OPAQUE_FILTER_ERROR)) - } -} - -impl<'de> Deserialize<'de> for OpaqueFilter { - fn deserialize>(_: D) -> Result { - Err(serde::de::Error::custom(OPAQUE_FILTER_ERROR)) - } -} - -impl FilterExpr { - /// Whether a view appears anywhere in this filter. - pub fn has_view(&self) -> bool { - match self { - FilterExpr::View(_) => true, - FilterExpr::And(items) | FilterExpr::Or(items) => items.iter().any(Self::has_view), - FilterExpr::Not(inner) => inner.has_view(), - _ => false, - } - } - - /// Whether any part of this filter tests edges. An edge test says nothing - /// about which nodes belong in a node collection, so a node-collection - /// subscript refuses such a filter. - pub fn tests_edges(&self) -> bool { - let edge_scope = |scope: &Scope| scope.entity != Entity::Node; - fn expr_tests_edges(expr: &Expr, edge_scope: &dyn Fn(&Scope) -> bool) -> bool { - match expr { - Expr::Const(_) => false, - Expr::Read { scope, .. } => edge_scope(scope), - Expr::Temporal(e) | Expr::Agg(_, e) | Expr::Qual(_, e) => { - expr_tests_edges(e, edge_scope) - } - } - } - match self { - FilterExpr::Cmp { lhs, rhs, .. } | FilterExpr::Str { lhs, rhs, .. } => { - expr_tests_edges(lhs, &edge_scope) || expr_tests_edges(rhs, &edge_scope) - } - FilterExpr::IsSome(e) | FilterExpr::IsNone(e) | FilterExpr::In { expr: e, .. } => { - expr_tests_edges(e, &edge_scope) - } - FilterExpr::Structural { scope, .. } => edge_scope(scope), - FilterExpr::View(_) | FilterExpr::Opaque(_) => false, - FilterExpr::And(items) | FilterExpr::Or(items) => items.iter().any(Self::tests_edges), - FilterExpr::Not(inner) => inner.tests_edges(), - } - } -} - -// ── compiling: through the entity-split tree ──────────────────────────────── -// -// This tree predates the per-entity one in `model::expr`; python and GraphQL -// still speak it. It compiles by converting to that tree, so there is one -// compiler and one set of checks. A qualifier written on a value here -// (`x.any() == v`) becomes the qualifier on the comparison there -// (`(x == v).any()`). - -fn invalid(msg: impl Into) -> GraphError { - GraphError::InvalidFilter(msg.into()) -} - -/// A leaf of the per-entity tree built from this tree's read. -trait FromOldRead: Leaf { - fn from_read(scope: &Scope, target: &Target) -> Result; - fn structural(scope: &Scope, pred: Structural) -> Result; - /// Switch a property read to its history. - fn temporal(self) -> Result; -} - -fn not_a_property() -> GraphError { - invalid("temporal() applies to a property") -} - -impl FromOldRead for NodeLeaf { - fn from_read(scope: &Scope, target: &Target) -> Result { - if scope.endpoint.is_some() { - return Err(invalid("a node expression has no src()/dst() endpoint")); - } - let views = scope.views.clone(); - Ok(match target { - Target::Field(field) => NodeLeaf::Field { - views, - field: *field, - }, - Target::Degree(direction) => NodeLeaf::Degree { - views, - direction: *direction, - }, - Target::Property(name) => NodeLeaf::property(views, name.clone(), false), - Target::Metadata(name) => NodeLeaf::metadata(views, name.clone()), - }) - } - - fn structural(scope: &Scope, pred: Structural) -> Result { - if scope.endpoint.is_some() { - return Err(invalid("a node has no src()/dst() endpoint")); - } - match pred { - Structural::IsActive => Ok(NodeLeaf::is_active(scope.views.clone())), - other => Err(invalid(format!("{other} is an edge predicate"))), - } - } - - fn temporal(self) -> Result { - match self { - NodeLeaf::Property { - views, - name, - temporal: false, - } => Ok(NodeLeaf::Property { - views, - name, - temporal: true, - }), - _ => Err(not_a_property()), - } - } -} - -impl FromOldRead for EdgeLeaf { - fn from_read(scope: &Scope, target: &Target) -> Result { - // An endpoint read is a node read, scoped by the same views, that the - // edge evaluates on the node at that end. - if let Some(endpoint) = scope.endpoint { - let node_scope = Scope { - entity: Entity::Node, - views: scope.views.clone(), - endpoint: None, - }; - let inner = Box::new(SplitExpr::Read(NodeLeaf::from_read(&node_scope, target)?)); - return Ok(match endpoint { - Endpoint::Src => EdgeLeaf::Src(inner), - Endpoint::Dst => EdgeLeaf::Dst(inner), - }); - } - let views = scope.views.clone(); - match target { - Target::Property(name) => Ok(EdgeLeaf::property(views, name.clone(), false)), - Target::Metadata(name) => Ok(EdgeLeaf::metadata(views, name.clone())), - Target::Field(_) | Target::Degree(_) => Err(invalid( - "an edge has no fields or degree; read them through src() or dst()", - )), - } - } - - fn structural(scope: &Scope, pred: Structural) -> Result { - if let Some(endpoint) = scope.endpoint { - if pred != Structural::IsActive { - return Err(invalid(format!("{pred} is an edge predicate"))); - } - let inner = Box::new(SplitExpr::Read(NodeLeaf::is_active(scope.views.clone()))); - return Ok(match endpoint { - Endpoint::Src => EdgeLeaf::Src(inner), - Endpoint::Dst => EdgeLeaf::Dst(inner), - }); - } - let views = scope.views.clone(); - Ok(match pred { - Structural::IsActive => EdgeLeaf::IsActive { views }, - Structural::IsValid => EdgeLeaf::IsValid { views }, - Structural::IsDeleted => EdgeLeaf::IsDeleted { views }, - Structural::IsSelfLoop => EdgeLeaf::IsSelfLoop { views }, - }) - } - - fn temporal(self) -> Result { - match self { - EdgeLeaf::Property { - views, - name, - temporal: false, - } => Ok(EdgeLeaf::Property { - views, - name, - temporal: true, - }), - EdgeLeaf::Src(inner) => Ok(EdgeLeaf::Src(Box::new(temporal_read(*inner)?))), - EdgeLeaf::Dst(inner) => Ok(EdgeLeaf::Dst(Box::new(temporal_read(*inner)?))), - _ => Err(not_a_property()), - } - } -} - -impl FromOldRead for ExplodedEdgeLeaf { - fn from_read(scope: &Scope, target: &Target) -> Result { - if scope.endpoint.is_some() { - return Err(invalid("an exploded edge has no src()/dst() endpoint")); - } - let views = scope.views.clone(); - match target { - Target::Property(name) => Ok(ExplodedEdgeLeaf::property(views, name.clone(), false)), - Target::Metadata(name) => Ok(ExplodedEdgeLeaf::metadata(views, name.clone())), - Target::Field(_) | Target::Degree(_) => Err(invalid( - "an edge has no fields or degree; read them through src() or dst()", - )), - } - } - - fn structural(scope: &Scope, pred: Structural) -> Result { - if scope.endpoint.is_some() { - return Err(invalid("an exploded edge has no src()/dst() endpoint")); - } - let views = scope.views.clone(); - Ok(match pred { - Structural::IsActive => ExplodedEdgeLeaf::IsActive { views }, - Structural::IsValid => ExplodedEdgeLeaf::IsValid { views }, - Structural::IsDeleted => ExplodedEdgeLeaf::IsDeleted { views }, - Structural::IsSelfLoop => ExplodedEdgeLeaf::IsSelfLoop { views }, - }) - } - - fn temporal(self) -> Result { - match self { - ExplodedEdgeLeaf::Property { - views, - name, - temporal: false, - } => Ok(ExplodedEdgeLeaf::Property { - views, - name, - temporal: true, - }), - _ => Err(not_a_property()), - } - } -} - -/// `temporal()` on a read: the read must be a property's latest value. -fn temporal_read(expr: SplitExpr) -> Result, GraphError> { - match expr { - SplitExpr::Read(leaf) => Ok(SplitExpr::Read(leaf.temporal()?)), - _ => Err(not_a_property()), - } -} - -/// Convert a value, collecting the qualifiers written on it (innermost first) -/// so the predicate around it can take them. -fn convert_value( - expr: &Expr, - quals: &mut Vec, -) -> Result, GraphError> { - Ok(match expr { - Expr::Const(value) => SplitExpr::Const(value.clone()), - Expr::Read { scope, target } => SplitExpr::Read(L::from_read(scope, target)?), - Expr::Temporal(inner) => temporal_read(convert_value(inner, quals)?)?, - Expr::Agg(agg, inner) => SplitExpr::Agg(*agg, Box::new(convert_value(inner, quals)?)), - Expr::Qual(qual, inner) => { - let value = convert_value(inner, quals)?; - quals.push(*qual); - value - } - }) -} - -/// Wrap a predicate in the qualifiers its value carried. The innermost -/// qualifier written collapses the outermost list level, so it goes on last. -fn qualify(mut pred: SplitExpr, quals: Vec) -> SplitExpr { - for qual in quals.iter().rev() { - pred = match qual { - Qual::Any => SplitExpr::Any(Box::new(pred)), - Qual::All => SplitExpr::All(Box::new(pred)), - }; - } - pred -} - -fn convert_predicate(filter: &FilterExpr) -> Result, GraphError> { - let mut quals = Vec::new(); - let pred = match filter { - FilterExpr::Cmp { op, lhs, rhs } => SplitExpr::Cmp( - *op, - Box::new(convert_value(lhs, &mut quals)?), - Box::new(convert_value(rhs, &mut quals)?), - ), - FilterExpr::Str { op, lhs, rhs } => SplitExpr::Str( - op.clone(), - Box::new(convert_value(lhs, &mut quals)?), - Box::new(convert_value(rhs, &mut quals)?), - ), - FilterExpr::IsSome(e) => SplitExpr::IsSome(Box::new(convert_value(e, &mut quals)?)), - FilterExpr::IsNone(e) => SplitExpr::IsNone(Box::new(convert_value(e, &mut quals)?)), - FilterExpr::In { - expr, - values, - negated, - } => SplitExpr::In { - expr: Box::new(convert_value(expr, &mut quals)?), - values: values.clone(), - negated: *negated, - }, - FilterExpr::Structural { scope, pred } => SplitExpr::Read(L::structural(scope, *pred)?), - FilterExpr::View(_) - | FilterExpr::And(_) - | FilterExpr::Or(_) - | FilterExpr::Not(_) - | FilterExpr::Opaque(_) => unreachable!("handled by FilterExpr::to_split"), - }; - Ok(qualify(pred, quals)) -} - -/// The entity a value reads from, if it reads at all. -fn value_entity(expr: &Expr) -> Option { - match expr { - Expr::Const(_) => None, - Expr::Read { scope, .. } => Some(scope.entity), - Expr::Temporal(e) | Expr::Agg(_, e) | Expr::Qual(_, e) => value_entity(e), - } -} - -fn predicate_entity(filter: &FilterExpr) -> Result { - let entity = match filter { - FilterExpr::Cmp { lhs, rhs, .. } | FilterExpr::Str { lhs, rhs, .. } => { - value_entity(lhs).or_else(|| value_entity(rhs)) - } - FilterExpr::IsSome(e) | FilterExpr::IsNone(e) | FilterExpr::In { expr: e, .. } => { - value_entity(e) - } - FilterExpr::Structural { scope, .. } => Some(scope.entity), - _ => None, - }; - entity.ok_or_else(|| invalid("a comparison needs an entity value on at least one side")) -} - -impl FilterExpr { - /// This filter as the per-entity tree. - pub fn to_split(&self) -> Result { - Ok(match self { - FilterExpr::View(ops) => SplitFilter::View(ops.clone()), - FilterExpr::And(items) => { - SplitFilter::And(items.iter().map(Self::to_split).collect::>()?) - } - FilterExpr::Or(items) => { - SplitFilter::Or(items.iter().map(Self::to_split).collect::>()?) - } - FilterExpr::Not(inner) => SplitFilter::Not(Box::new(inner.to_split()?)), - FilterExpr::Opaque(filter) => SplitFilter::Opaque(filter.clone()), - predicate => match predicate_entity(predicate)? { - Entity::Node => SplitFilter::Node(convert_predicate(predicate)?), - Entity::Edge => SplitFilter::Edge(convert_predicate(predicate)?), - Entity::ExplodedEdge => SplitFilter::ExplodedEdge(convert_predicate(predicate)?), - }, - }) - } - - /// The erased, applicable form of this filter. - pub fn compile(&self) -> Result, GraphError> { - self.to_split()?.compile() - } -} - -impl Expr { - /// The erased, compilable form of this value. A qualifier written on the - /// value has no meaning without the comparison that follows it and is - /// left out here; the value it qualifies is what compiles. - pub fn compile(&self) -> Result, GraphError> { - let mut quals = Vec::new(); - match value_entity(self).unwrap_or(Entity::Node) { - Entity::Node => convert_value::(self, &mut quals)?.compile_value(), - Entity::Edge => convert_value::(self, &mut quals)?.compile_value(), - Entity::ExplodedEdge => { - convert_value::(self, &mut quals)?.compile_value() - } - } - } -} - -/// A tree is a filter in its own right: applying it compiles it first. -impl CreateFilter for FilterExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = DynGraphArc<'graph> - where - Self: 'graph; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; - - type FilteredGraph<'graph, G> - = DynGraphArc<'graph> - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - self.compile()?.create_filter(graph, filtered) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - self.compile()?.create_node_filter(graph, filtered) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - self.compile()?.filter_graph_view(graph) - } -} - -// ── printing ───────────────────────────────────────────────────────────────── - -impl Display for ViewOp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ViewOp::Window { start, end } => write!(f, "WINDOW[{}..{}]", start.t(), end.t()), - ViewOp::At(t) => write!(f, "AT[{}]", t.t()), - ViewOp::After(t) => write!(f, "AFTER[{}]", t.t()), - ViewOp::Before(t) => write!(f, "BEFORE[{}]", t.t()), - ViewOp::Latest => write!(f, "LATEST"), - ViewOp::SnapshotAt(t) => write!(f, "SNAPSHOT_AT[{}]", t.t()), - ViewOp::SnapshotLatest => write!(f, "SNAPSHOT_LATEST"), - ViewOp::Layers(names) => { - write!(f, "LAYER[{}]", layer_label(&Layer::from(names.clone()))) - } - } - } -} - -impl Display for Target { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Target::Field(Field::Id) => write!(f, "id"), - Target::Field(Field::Name) => write!(f, "name"), - Target::Field(Field::NodeType) => write!(f, "node_type"), - Target::Degree(Direction::BOTH) => write!(f, "degree"), - Target::Degree(Direction::IN) => write!(f, "in_degree"), - Target::Degree(Direction::OUT) => write!(f, "out_degree"), - Target::Property(name) => write!(f, "{name}"), - Target::Metadata(name) => write!(f, "metadata({name})"), - } - } -} - -/// Wraps `inner` in the scope's views (innermost first) and endpoint. -fn scoped(f: &mut fmt::Formatter<'_>, scope: &Scope, inner: &str) -> fmt::Result { - let mut text = inner.to_string(); - for view in &scope.views { - text = format!("{view}({text})"); - } - match scope.endpoint { - Some(Endpoint::Src) => write!(f, "SRC({text})"), - Some(Endpoint::Dst) => write!(f, "DST({text})"), - None => write!(f, "{text}"), - } -} - -impl Display for Expr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Expr::Const(value) => write!(f, "{value}"), - Expr::Read { scope, target } => scoped(f, scope, &target.to_string()), - Expr::Temporal(inner) => write!(f, "temporal({inner})"), - Expr::Agg(agg, inner) => write!(f, "{agg}({inner})"), - Expr::Qual(qual, inner) => write!(f, "{qual}({inner})"), - } - } -} - -impl Display for Agg { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Agg::Sum => "sum", - Agg::Avg => "avg", - Agg::Min => "min", - Agg::Max => "max", - Agg::First => "first", - Agg::Last => "last", - Agg::Len => "len", - }) - } -} - -impl Display for Qual { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Qual::Any => "any", - Qual::All => "all", - }) - } -} - -impl Display for CmpOp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - CmpOp::Eq => "==", - CmpOp::Ne => "!=", - CmpOp::Lt => "<", - CmpOp::Le => "<=", - CmpOp::Gt => ">", - CmpOp::Ge => ">=", - }) - } -} - -impl Display for StrOp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - StrOp::StartsWith => write!(f, "STARTS_WITH"), - StrOp::EndsWith => write!(f, "ENDS_WITH"), - StrOp::Contains => write!(f, "CONTAINS"), - StrOp::NotContains => write!(f, "NOT_CONTAINS"), - StrOp::FuzzySearch { - levenshtein_distance, - prefix_match, - } => write!(f, "FUZZY_SEARCH[{levenshtein_distance}, {prefix_match}]"), - } - } -} - -impl Display for Structural { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Structural::IsActive => "IS_ACTIVE", - Structural::IsValid => "IS_VALID", - Structural::IsDeleted => "IS_DELETED", - Structural::IsSelfLoop => "IS_SELF_LOOP", - }) - } -} - -impl Display for FilterExpr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let list = |items: &[FilterExpr], sep: &str| { - items - .iter() - .map(|i| i.to_string()) - .collect::>() - .join(sep) - }; - match self { - FilterExpr::Cmp { op, lhs, rhs } => write!(f, "{lhs} {op} {rhs}"), - FilterExpr::Str { op, lhs, rhs } => write!(f, "{lhs} {op} {rhs}"), - FilterExpr::IsSome(expr) => write!(f, "{expr} IS_SOME"), - FilterExpr::IsNone(expr) => write!(f, "{expr} IS_NONE"), - FilterExpr::In { - expr, - values, - negated, - } => { - let values = values - .iter() - .map(|v| v.to_string()) - .collect::>() - .join(", "); - let op = if *negated { "NOT_IN" } else { "IN" }; - write!(f, "{expr} {op} [{values}]") - } - FilterExpr::Structural { scope, pred } => scoped(f, scope, &pred.to_string()), - FilterExpr::View(views) => { - let views = views.iter().map(|v| v.to_string()).collect::>(); - write!(f, "VIEW({})", views.join(" . ")) - } - FilterExpr::And(items) => write!(f, "({})", list(items, " AND ")), - FilterExpr::Or(items) => write!(f, "({})", list(items, " OR ")), - FilterExpr::Not(inner) => write!(f, "NOT({inner})"), - FilterExpr::Opaque(_) => write!(f, ""), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - db::{ - api::view::Filter, - graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::NodeFilter, windowed_filter::Windowed, - EdgeViewFilterOps, PropertyExprFactory, ViewWrapOps, - }, - }, - prelude::{ - AdditionOps, EdgeViewOps, EntityAggOps, EntityExprFilterOps, Graph, GraphViewOps, - NodeViewOps, TimeOps, NO_PROPS, - }, - }; - use raphtory_api::core::entities::properties::prop::IntoProp; - - /// alice.score 3@0 7@2 9@6 · bob.score 5@1 2@7 · carol none · dave.score 1@2 1@3 - /// alice→bob [knows] @1 @4 · bob→carol [works] @2 · carol→dave [knows] @6 - fn graph() -> Graph { - let g = Graph::new(); - for (t, name, score) in [ - (0, "alice", 3.0), - (2, "alice", 7.0), - (6, "alice", 9.0), - (1, "bob", 5.0), - (7, "bob", 2.0), - (2, "dave", 1.0), - (3, "dave", 1.0), - ] { - g.add_node(t, name, [("score", score.into_prop())], None, None) - .unwrap(); - } - g.add_node(0, "carol", NO_PROPS, None, None).unwrap(); - g.add_edge(1, "alice", "bob", NO_PROPS, Some("knows")) - .unwrap(); - g.add_edge(4, "alice", "bob", NO_PROPS, Some("knows")) - .unwrap(); - g.add_edge(2, "bob", "carol", NO_PROPS, Some("works")) - .unwrap(); - g.add_edge(6, "carol", "dave", NO_PROPS, Some("knows")) - .unwrap(); - g - } - - fn nodes(g: &Graph, filter: Arc) -> Vec { - let mut names: Vec = g - .filter(filter) - .unwrap() - .nodes() - .iter() - .map(|n| n.name()) - .collect(); - names.sort(); - names - } - - fn edges(g: &Graph, filter: Arc) -> Vec { - let mut ids: Vec = g - .filter(filter) - .unwrap() - .edges() - .iter() - .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) - .collect(); - ids.sort(); - ids - } - - fn node(target: Target) -> Expr { - Expr::Read { - scope: Scope::new(Entity::Node), - target, - } - } - - fn score() -> Expr { - node(Target::Property("score".into())) - } - - fn cmp(op: CmpOp, lhs: Expr, rhs: Expr) -> FilterExpr { - FilterExpr::Cmp { op, lhs, rhs } - } - - fn c(v: impl Into) -> Expr { - Expr::Const(v.into()) - } - - #[test] - fn a_constant_comparison_matches_the_typed_api() { - let g = graph(); - let tree = cmp(CmpOp::Gt, score(), c(4.0)); - assert_eq!(nodes(&g, tree.compile().unwrap()), vec!["alice"]); - let typed: Arc = Arc::new(NodeFilter.property("score").gt(4.0)); - assert_eq!(nodes(&g, typed), vec!["alice"]); - assert_eq!(tree.to_string(), "score > 4"); - } - - #[test] - fn views_scope_the_read_not_the_result() { - let g = graph(); - let tree = cmp( - CmpOp::Gt, - Expr::Read { - scope: Scope::new(Entity::Node).with_view(ViewOp::Window { - start: EventTime::from(0), - end: EventTime::from(5), - }), - target: Target::Property("score".into()), - }, - c(4.0), - ); - // inside [0,5): alice's latest score is 7, bob's is 5 - assert_eq!(nodes(&g, tree.compile().unwrap()), vec!["alice", "bob"]); - let typed: Arc = - Arc::new(NodeFilter.window(0, 5).property("score").gt(4.0)); - assert_eq!(nodes(&g, typed), vec!["alice", "bob"]); - assert_eq!(tree.to_string(), "WINDOW[0..5](score) > 4"); - } - - #[test] - fn both_sides_may_be_expressions() { - let g = graph(); - let tree = cmp( - CmpOp::Gt, - node(Target::Degree(Direction::BOTH)), - node(Target::Degree(Direction::IN)), - ); - assert_eq!( - nodes(&g, tree.compile().unwrap()), - vec!["alice", "bob", "carol"] - ); - assert_eq!(tree.to_string(), "degree > in_degree"); - } - - #[test] - fn temporal_aggregates_and_qualifiers() { - let g = graph(); - let sum = cmp( - CmpOp::Gt, - Expr::Agg(Agg::Sum, Box::new(Expr::Temporal(Box::new(score())))), - c(10.0), - ); - assert_eq!(nodes(&g, sum.compile().unwrap()), vec!["alice"]); - assert_eq!(sum.to_string(), "sum(temporal(score)) > 10"); - - let any = cmp( - CmpOp::Gt, - Expr::Qual(Qual::Any, Box::new(Expr::Temporal(Box::new(score())))), - c(4.0), - ); - assert_eq!(nodes(&g, any.compile().unwrap()), vec!["alice", "bob"]); - - let all = cmp( - CmpOp::Gt, - Expr::Qual(Qual::All, Box::new(Expr::Temporal(Box::new(score())))), - c(4.0), - ); - assert!(nodes(&g, all.compile().unwrap()).is_empty()); - - let len = cmp( - CmpOp::Eq, - Expr::Agg(Agg::Len, Box::new(Expr::Temporal(Box::new(score())))), - c(2u64), - ); - assert_eq!(nodes(&g, len.compile().unwrap()), vec!["bob", "dave"]); - } - - #[test] - fn temporal_needs_a_property() { - let tree = Expr::Temporal(Box::new(node(Target::Degree(Direction::BOTH)))); - assert!(tree.compile().is_err()); - } - - #[test] - fn combinators_and_presence() { - let g = graph(); - let tree = FilterExpr::And(vec![ - FilterExpr::IsSome(score()), - FilterExpr::Not(Box::new(FilterExpr::Str { - op: StrOp::StartsWith, - lhs: node(Target::Field(Field::Name)), - rhs: c("a"), - })), - ]); - assert_eq!(nodes(&g, tree.compile().unwrap()), vec!["bob", "dave"]); - assert_eq!( - tree.to_string(), - "(score IS_SOME AND NOT(name STARTS_WITH a))" - ); - - let none = FilterExpr::IsNone(score()); - assert_eq!(nodes(&g, none.compile().unwrap()), vec!["carol"]); - - let set = FilterExpr::In { - expr: node(Target::Field(Field::Name)), - values: vec!["alice".into(), "dave".into()], - negated: false, - }; - assert_eq!(nodes(&g, set.compile().unwrap()), vec!["alice", "dave"]); - assert!(FilterExpr::And(vec![]).compile().is_err()); - assert!(FilterExpr::Or(vec![]).compile().is_err()); - } - - #[test] - fn edge_endpoints_and_structure() { - let g = graph(); - let src_name = FilterExpr::Cmp { - op: CmpOp::Eq, - lhs: Expr::Read { - scope: Scope::new(Entity::Edge).through(Endpoint::Src), - target: Target::Field(Field::Name), - }, - rhs: c("alice"), - }; - assert_eq!(edges(&g, src_name.compile().unwrap()), vec!["alice->bob"]); - let typed: Arc = Arc::new(EdgeFilter::src().name().eq("alice")); - assert_eq!(edges(&g, typed), vec!["alice->bob"]); - assert_eq!(src_name.to_string(), "SRC(name) == alice"); - - let endpoint_score = FilterExpr::Cmp { - op: CmpOp::Gt, - lhs: Expr::Read { - scope: Scope::new(Entity::Edge).through(Endpoint::Dst), - target: Target::Property("score".into()), - }, - rhs: Expr::Read { - scope: Scope::new(Entity::Edge).through(Endpoint::Src), - target: Target::Property("score".into()), - }, - }; - // dst score > src score: alice(9)->bob(2) no; bob(2)->carol(none) no; carol(none)->dave no - assert!(edges(&g, endpoint_score.compile().unwrap()).is_empty()); - - let layered = FilterExpr::Structural { - scope: Scope::new(Entity::Edge).with_view(ViewOp::Layers(vec!["works".into()])), - pred: Structural::IsActive, - }; - assert_eq!(edges(&g, layered.compile().unwrap()), vec!["bob->carol"]); - assert_eq!(layered.to_string(), "LAYER[works](IS_ACTIVE)"); - } - - #[test] - fn before_and_at_agree_with_the_graph_views() { - // alice→bob @2 (first event at 2) · carol→dave @2 (second event at 2) · eve→fay @5 - let g = Graph::new(); - g.add_edge(2, "alice", "bob", NO_PROPS, None).unwrap(); - g.add_edge(2, "carol", "dave", NO_PROPS, None).unwrap(); - g.add_edge(5, "eve", "fay", NO_PROPS, None).unwrap(); - fn edge_names<'graph, G: GraphViewOps<'graph>>(g: &G) -> Vec { - let mut ids: Vec = g - .edges() - .iter() - .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) - .collect(); - ids.sort(); - ids - } - let view = |op: ViewOp| FilterExpr::View(vec![op]).compile().unwrap(); - let typed = |f: Windowed| Arc::new(f.is_active()) as Arc; - let none: [&str; 0] = []; - let at_two = ["alice->bob", "carol->dave"]; - - // `before(t)` excludes every event at `t`, like the graph view does. - assert_eq!(edge_names(&g.before(2)), none); - assert_eq!(edges(&g, view(ViewOp::Before(EventTime::start(2)))), none); - assert_eq!(edges(&g, typed(EdgeFilter.before(2))), none); - assert_eq!(edge_names(&g.before(3)), at_two); - assert_eq!(edges(&g, view(ViewOp::Before(EventTime::start(3)))), at_two); - - // `at(t)` covers the whole timestamp, even when handed a time that sits - // between two events at `t`. - let mid_two = EventTime::start(2).set_event_id(1); - assert_eq!(edge_names(&g.at(mid_two)), at_two); - assert_eq!(edges(&g, view(ViewOp::At(mid_two))), at_two); - assert_eq!(edges(&g, typed(EdgeFilter.at(mid_two))), at_two); - - // A window bound that carries an event id is honoured, as the graph view does. - let from_second_event = EventTime::start(2).set_event_id(1); - assert_eq!( - edge_names(&g.window(from_second_event, EventTime::start(3))), - ["carol->dave"] - ); - assert_eq!( - edges( - &g, - view(ViewOp::Window { - start: from_second_event, - end: EventTime::start(3), - }), - ), - ["carol->dave"] - ); - assert_eq!( - edges(&g, typed(EdgeFilter.window(from_second_event, 3))), - ["carol->dave"] - ); - - // `after(t)` excludes `t` and everything before it. - assert_eq!(edge_names(&g.after(2)), ["eve->fay"]); - assert_eq!( - edges(&g, view(ViewOp::After(EventTime::start(2)))), - ["eve->fay"] - ); - } - - #[test] - fn a_view_alone_is_the_result() { - let g = graph(); - let tree = FilterExpr::View(vec![ - ViewOp::Window { - start: EventTime::from(0), - end: EventTime::from(5), - }, - ViewOp::Latest, - ]); - assert_eq!(tree.to_string(), "VIEW(WINDOW[0..5] . LATEST)"); - assert_eq!(edges(&g, tree.compile().unwrap()), vec!["alice->bob"]); - assert!(FilterExpr::View(vec![]).compile().is_err()); - } - - #[test] - fn trees_round_trip_through_json() { - let tree = FilterExpr::And(vec![ - cmp( - CmpOp::Gt, - Expr::Agg(Agg::Sum, Box::new(Expr::Temporal(Box::new(score())))), - c(10.0), - ), - FilterExpr::Structural { - scope: Scope::new(Entity::Edge).with_view(ViewOp::Layers(vec!["works".into()])), - pred: Structural::IsActive, - }, - FilterExpr::View(vec![ViewOp::Latest]), - ]); - let json = serde_json::to_string(&tree).unwrap(); - let back: FilterExpr = serde_json::from_str(&json).unwrap(); - assert_eq!(back, tree); - } - - #[test] - fn an_opaque_filter_refuses_to_serialise() { - let compiled = FilterExpr::View(vec![ViewOp::Latest]).compile().unwrap(); - let opaque = FilterExpr::Opaque(OpaqueFilter(compiled)); - let err = serde_json::to_string(&opaque).unwrap_err(); - assert!(err.to_string().contains(OPAQUE_FILTER_ERROR), "{err}"); - assert!(!opaque.tests_edges()); - } - - #[test] - fn an_exploded_edge_has_no_endpoint() { - let scope = Scope::new(Entity::ExplodedEdge).through(Endpoint::Src); - let read = FilterExpr::Cmp { - op: CmpOp::Eq, - lhs: Expr::Read { - scope: scope.clone(), - target: Target::Field(Field::Name), - }, - rhs: Expr::Const(Prop::str("alice")), - }; - assert!(read.compile().is_err()); - let active = FilterExpr::Structural { - scope, - pred: Structural::IsActive, - }; - assert!(active.compile().is_err()); - } - - #[test] - fn a_view_leg_restricts_the_whole_filter() { - // alice's only update (t=6) and bob's (t=1) are inside [0, 7); dave's (t=8) is not. - let g = Graph::new(); - g.add_node(6, "alice", [("score", Prop::F64(9.0))], None, None) - .unwrap(); - g.add_node(1, "bob", [("score", Prop::F64(5.0))], None, None) - .unwrap(); - g.add_node(8, "dave", [("score", Prop::F64(10.0))], None, None) - .unwrap(); - g.add_node(0, "carol", NO_PROPS, None, None).unwrap(); - let score_gt_4 = FilterExpr::Cmp { - op: CmpOp::Gt, - lhs: Expr::Read { - scope: Scope::new(Entity::Node), - target: Target::Property("score".into()), - }, - rhs: Expr::Const(Prop::F64(4.0)), - }; - let window = FilterExpr::View(vec![ViewOp::Window { - start: EventTime::start(0), - end: EventTime::end(7), - }]); - // The view applies first and the predicate runs inside it. - let tree = FilterExpr::And(vec![window.clone(), score_gt_4.clone()]); - assert_eq!(nodes(&g, tree.compile().unwrap()), ["alice", "bob"]); - // A predicate every node passes still leaves the view's members only. - let named = FilterExpr::IsSome(Expr::Read { - scope: Scope::new(Entity::Node), - target: Target::Field(Field::Name), - }); - let all_in_window = FilterExpr::And(vec![window.clone(), named]); - assert_eq!( - nodes(&g, all_in_window.compile().unwrap()), - ["alice", "bob", "carol"] - ); - // Nested `and`s flatten, so the view still reaches the top. - let nested = FilterExpr::And(vec![ - FilterExpr::And(vec![window.clone()]), - score_gt_4.clone(), - ]); - assert_eq!(nodes(&g, nested.compile().unwrap()), ["alice", "bob"]); - // Under `or` or `not` a view has no meaning the engine can give it. - assert!(FilterExpr::Or(vec![window.clone(), score_gt_4.clone()]) - .compile() - .is_err()); - assert!(FilterExpr::Not(Box::new(window.clone())).compile().is_err()); - assert!(FilterExpr::Or(vec![ - FilterExpr::And(vec![window, score_gt_4.clone()]), - score_gt_4 - ]) - .compile() - .is_err()); - } -} diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 4ae60b387f..4d058220f7 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -1,13 +1,10 @@ use crate::{ db::graph::views::filter::model::{ edge_filter::Endpoint, - tree::{Entity, Expr, Field, FilterExpr, Scope, Structural, Target, ViewOp}, + expr::{EdgeLeaf, Expr, Field, Leaf, NodeExpr, NodeLeaf, ViewOp}, }, python::{ - filter::{ - filter_expr::PyFilterExpr, - node_expr::{PyExpr, PyPropertyExpr}, - }, + filter::node_expr::{PyExpr, PyPropertyExpr, Typed}, types::iterable::FromIterable, }, }; @@ -25,15 +22,41 @@ use raphtory_api::core::storage::timeindex::EventTime; /// Edge.dst().name().starts_with("user:") /// Edge.src().property("country") == "UK" #[pyclass(frozen, name = "EdgeEndpoint", module = "raphtory.filter")] -pub struct PyEdgeEndpoint(pub(crate) Scope); +pub struct PyEdgeEndpoint { + views: Vec, + endpoint: Endpoint, +} impl PyEdgeEndpoint { - fn read(&self, target: Target) -> Expr { - Expr::Read { - scope: self.0.clone(), - target, + /// A node expression evaluated on the node at this end of the edge. The + /// edge's views scope that node read. + fn through(&self, inner: NodeExpr) -> EdgeLeaf { + match self.endpoint { + Endpoint::Src => EdgeLeaf::Src(Box::new(inner)), + Endpoint::Dst => EdgeLeaf::Dst(Box::new(inner)), } } + + fn read(&self, leaf: NodeLeaf) -> PyExpr { + PyExpr(Typed::Edge(Expr::Read(self.through(Expr::Read(leaf))))) + } + + fn field(&self, field: Field) -> PyExpr { + self.read(NodeLeaf::Field { + views: self.views.clone(), + field, + }) + } + + fn property_read(&self, name: String) -> PyPropertyExpr { + PyPropertyExpr::new(|temporal| { + Typed::Edge(Expr::Read(self.through(Expr::Read(NodeLeaf::property( + self.views.clone(), + name.clone(), + temporal, + ))))) + }) + } } #[pymethods] @@ -43,7 +66,7 @@ impl PyEdgeEndpoint { /// Returns: /// filter.Expr: fn id(&self) -> PyExpr { - PyExpr(self.read(Target::Field(Field::Id))) + self.field(Field::Id) } /// Selects the endpoint node name field for filtering. @@ -51,7 +74,7 @@ impl PyEdgeEndpoint { /// Returns: /// filter.Expr: fn name(&self) -> PyExpr { - PyExpr(self.read(Target::Field(Field::Name))) + self.field(Field::Name) } /// Selects the endpoint node type field for filtering. @@ -59,7 +82,7 @@ impl PyEdgeEndpoint { /// Returns: /// filter.Expr: fn node_type(&self) -> PyExpr { - PyExpr(self.read(Target::Field(Field::NodeType))) + self.field(Field::NodeType) } /// Filters an endpoint node property by name. @@ -70,7 +93,7 @@ impl PyEdgeEndpoint { /// Returns: /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { - PyPropertyExpr(self.read(Target::Property(name))) + self.property_read(name) } /// Filters an endpoint node metadata field by name. @@ -81,24 +104,33 @@ impl PyEdgeEndpoint { /// Returns: /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { - PyExpr(self.read(Target::Metadata(name))) + self.read(NodeLeaf::metadata(self.views.clone(), name)) } } impl PyEdgeFilter { pub(crate) fn root() -> Self { - PyEdgeFilter(Scope::new(Entity::Edge)) + PyEdgeFilter(Vec::new()) } fn with_view(&self, view: ViewOp) -> Self { - PyEdgeFilter(self.0.clone().with_view(view)) + let mut views = self.0.clone(); + views.push(view); + PyEdgeFilter(views) } - fn read(&self, target: Target) -> Expr { - Expr::Read { - scope: self.0.clone(), - target, - } + fn read(&self, leaf: EdgeLeaf) -> PyExpr { + PyExpr(Typed::Edge(Expr::Read(leaf))) + } + + fn property_read(&self, name: String) -> PyPropertyExpr { + PyPropertyExpr::new(|temporal| { + Typed::Edge(Expr::Read(EdgeLeaf::property( + self.0.clone(), + name.clone(), + temporal, + ))) + }) } } @@ -108,21 +140,19 @@ impl PyEdgeFilter { /// `Edge.layer(...)`, ...); its endpoint, property and structural predicates /// evaluate within that view, and its own view methods narrow it further. #[pyclass(frozen, name = "EdgeFilter", module = "raphtory.filter")] -pub struct PyEdgeFilter(pub(crate) Scope); +pub struct PyEdgeFilter(pub(crate) Vec); #[pymethods] impl PyEdgeFilter { - #[new] - fn new() -> PyEdgeFilter { - Self::root() - } - /// Selects the edge **source endpoint** for filtering. /// /// Returns: /// filter.EdgeEndpoint: fn src(&self) -> PyEdgeEndpoint { - PyEdgeEndpoint(self.0.clone().through(Endpoint::Src)) + PyEdgeEndpoint { + views: self.0.clone(), + endpoint: Endpoint::Src, + } } /// Selects the edge **destination endpoint** for filtering. @@ -130,7 +160,10 @@ impl PyEdgeFilter { /// Returns: /// filter.EdgeEndpoint: fn dst(&self) -> PyEdgeEndpoint { - PyEdgeEndpoint(self.0.clone().through(Endpoint::Dst)) + PyEdgeEndpoint { + views: self.0.clone(), + endpoint: Endpoint::Dst, + } } /// Filters an edge property by name. @@ -141,7 +174,7 @@ impl PyEdgeFilter { /// Returns: /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { - PyPropertyExpr(self.read(Target::Property(name))) + self.property_read(name) } /// Filters an edge metadata field by name. @@ -152,7 +185,7 @@ impl PyEdgeFilter { /// Returns: /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { - PyExpr(self.read(Target::Metadata(name))) + self.read(EdgeLeaf::metadata(self.0.clone(), name)) } /// Restricts edge evaluation to the given time window. @@ -252,44 +285,40 @@ impl PyEdgeFilter { /// Matches edges that have at least one event in the current view. /// /// Returns: - /// filter.FilterExpr: - fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(FilterExpr::Structural { - scope: self.0.clone(), - pred: Structural::IsActive, + /// filter.Expr: + fn is_active(&self) -> PyExpr { + self.read(EdgeLeaf::IsActive { + views: self.0.clone(), }) } /// Matches edges that are structurally valid in the current view. /// /// Returns: - /// filter.FilterExpr: - fn is_valid(&self) -> PyFilterExpr { - PyFilterExpr(FilterExpr::Structural { - scope: self.0.clone(), - pred: Structural::IsValid, + /// filter.Expr: + fn is_valid(&self) -> PyExpr { + self.read(EdgeLeaf::IsValid { + views: self.0.clone(), }) } /// Matches edges that have been deleted. /// /// Returns: - /// filter.FilterExpr: - fn is_deleted(&self) -> PyFilterExpr { - PyFilterExpr(FilterExpr::Structural { - scope: self.0.clone(), - pred: Structural::IsDeleted, + /// filter.Expr: + fn is_deleted(&self) -> PyExpr { + self.read(EdgeLeaf::IsDeleted { + views: self.0.clone(), }) } /// Matches edges that are self-loops (source == destination). /// /// Returns: - /// filter.FilterExpr: - fn is_self_loop(&self) -> PyFilterExpr { - PyFilterExpr(FilterExpr::Structural { - scope: self.0.clone(), - pred: Structural::IsSelfLoop, + /// filter.Expr: + fn is_self_loop(&self) -> PyExpr { + self.read(EdgeLeaf::IsSelfLoop { + views: self.0.clone(), }) } } @@ -452,36 +481,36 @@ impl PyEdge { /// Matches edges that have at least one event in the current view. /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: #[staticmethod] - fn is_active() -> PyFilterExpr { + fn is_active() -> PyExpr { PyEdgeFilter::root().is_active() } /// Matches edges that are structurally valid in the current view. /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: #[staticmethod] - fn is_valid() -> PyFilterExpr { + fn is_valid() -> PyExpr { PyEdgeFilter::root().is_valid() } /// Matches edges that have been deleted. /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: #[staticmethod] - fn is_deleted() -> PyFilterExpr { + fn is_deleted() -> PyExpr { PyEdgeFilter::root().is_deleted() } /// Matches edges that are self-loops (source == destination). /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: #[staticmethod] - fn is_self_loop() -> PyFilterExpr { + fn is_self_loop() -> PyExpr { PyEdgeFilter::root().is_self_loop() } } diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index 8c82eaa746..46c29092c8 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -1,12 +1,7 @@ use crate::{ - db::graph::views::filter::model::tree::{ - Entity, Expr, FilterExpr, Scope, Structural, Target, ViewOp, - }, + db::graph::views::filter::model::expr::{ExplodedEdgeLeaf, Expr, Leaf, ViewOp}, python::{ - filter::{ - filter_expr::PyFilterExpr, - node_expr::{PyExpr, PyPropertyExpr}, - }, + filter::node_expr::{PyExpr, PyPropertyExpr, Typed}, types::iterable::FromIterable, }, }; @@ -20,32 +15,36 @@ use raphtory_api::core::storage::timeindex::EventTime; /// methods on [`ExplodedEdge`]; its property and structural predicates evaluate /// within that view, and its own view methods narrow it further. #[pyclass(frozen, name = "ExplodedEdgeFilter", module = "raphtory.filter")] -pub struct PyExplodedEdgeFilter(pub(crate) Scope); +pub struct PyExplodedEdgeFilter(pub(crate) Vec); impl PyExplodedEdgeFilter { pub(crate) fn root() -> Self { - PyExplodedEdgeFilter(Scope::new(Entity::ExplodedEdge)) + PyExplodedEdgeFilter(Vec::new()) } fn with_view(&self, view: ViewOp) -> Self { - PyExplodedEdgeFilter(self.0.clone().with_view(view)) + let mut views = self.0.clone(); + views.push(view); + PyExplodedEdgeFilter(views) + } + + fn read(&self, leaf: ExplodedEdgeLeaf) -> PyExpr { + PyExpr(Typed::ExplodedEdge(Expr::Read(leaf))) } - fn read(&self, target: Target) -> Expr { - Expr::Read { - scope: self.0.clone(), - target, - } + fn property_read(&self, name: String) -> PyPropertyExpr { + PyPropertyExpr::new(|temporal| { + Typed::ExplodedEdge(Expr::Read(ExplodedEdgeLeaf::property( + self.0.clone(), + name.clone(), + temporal, + ))) + }) } } #[pymethods] impl PyExplodedEdgeFilter { - #[new] - fn new() -> PyExplodedEdgeFilter { - Self::root() - } - /// Filters an exploded edge property by name. /// /// The property may be static or temporal depending on the query context. @@ -56,7 +55,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { - PyPropertyExpr(self.read(Target::Property(name))) + self.property_read(name) } /// Filters an exploded edge metadata field by name. @@ -69,7 +68,7 @@ impl PyExplodedEdgeFilter { /// Returns: /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { - PyExpr(self.read(Target::Metadata(name))) + self.read(ExplodedEdgeLeaf::metadata(self.0.clone(), name)) } /// Restricts exploded edge evaluation to the given time window. @@ -171,44 +170,40 @@ impl PyExplodedEdgeFilter { /// Matches exploded edges that have at least one event in the current view. /// /// Returns: - /// filter.FilterExpr: - fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(FilterExpr::Structural { - scope: self.0.clone(), - pred: Structural::IsActive, + /// filter.Expr: + fn is_active(&self) -> PyExpr { + self.read(ExplodedEdgeLeaf::IsActive { + views: self.0.clone(), }) } /// Matches exploded edges that are structurally valid in the current view. /// /// Returns: - /// filter.FilterExpr: - fn is_valid(&self) -> PyFilterExpr { - PyFilterExpr(FilterExpr::Structural { - scope: self.0.clone(), - pred: Structural::IsValid, + /// filter.Expr: + fn is_valid(&self) -> PyExpr { + self.read(ExplodedEdgeLeaf::IsValid { + views: self.0.clone(), }) } /// Matches exploded edges that have been deleted. /// /// Returns: - /// filter.FilterExpr: - fn is_deleted(&self) -> PyFilterExpr { - PyFilterExpr(FilterExpr::Structural { - scope: self.0.clone(), - pred: Structural::IsDeleted, + /// filter.Expr: + fn is_deleted(&self) -> PyExpr { + self.read(ExplodedEdgeLeaf::IsDeleted { + views: self.0.clone(), }) } /// Matches exploded edges that are self-loops (source == destination). /// /// Returns: - /// filter.FilterExpr: - fn is_self_loop(&self) -> PyFilterExpr { - PyFilterExpr(FilterExpr::Structural { - scope: self.0.clone(), - pred: Structural::IsSelfLoop, + /// filter.Expr: + fn is_self_loop(&self) -> PyExpr { + self.read(ExplodedEdgeLeaf::IsSelfLoop { + views: self.0.clone(), }) } } @@ -358,36 +353,36 @@ impl PyExplodedEdge { /// Matches exploded edges that have at least one event in the current view. /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: #[staticmethod] - fn is_active() -> PyFilterExpr { + fn is_active() -> PyExpr { PyExplodedEdgeFilter::root().is_active() } /// Matches exploded edges that are structurally valid in the current view. /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: #[staticmethod] - fn is_valid() -> PyFilterExpr { + fn is_valid() -> PyExpr { PyExplodedEdgeFilter::root().is_valid() } /// Matches exploded edges that have been deleted. /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: #[staticmethod] - fn is_deleted() -> PyFilterExpr { + fn is_deleted() -> PyExpr { PyExplodedEdgeFilter::root().is_deleted() } /// Matches exploded edges that are self-loops (source == destination). /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: #[staticmethod] - fn is_self_loop() -> PyFilterExpr { + fn is_self_loop() -> PyExpr { PyExplodedEdgeFilter::root().is_self_loop() } } diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 29d1e0eab8..e94c7a2cbe 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -4,21 +4,25 @@ use crate::{ state::NodeOp, view::internal::{DynGraphArc, GraphView}, }, - graph::views::filter::{model::tree::FilterExpr, CreateFilter}, + graph::views::filter::{model::expr::FilterExpr, CreateFilter}, }, errors::GraphError, + python::filter::node_expr::PyExpr, }; -use pyo3::{exceptions::PyTypeError, prelude::*}; +use pyo3::{exceptions::PyTypeError, prelude::*, Borrowed}; use std::sync::Arc; /// A filter as a tree. The same tree runs locally, is sent to a server, and is /// what `repr` prints, so there is nothing to keep in step. +/// +/// Anywhere a filter is expected, a yes/no [`Expr`] is accepted too: it is the +/// filter on its own entity. #[pyclass( frozen, name = "FilterExpr", module = "raphtory.filter", subclass, - from_py_object + skip_from_py_object )] #[derive(Clone)] pub struct PyFilterExpr(pub FilterExpr); @@ -29,19 +33,61 @@ impl PyFilterExpr { } } +impl<'py> FromPyObject<'_, 'py> for PyFilterExpr { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { + if let Ok(filter) = ob.cast::() { + return Ok(filter.get().clone()); + } + if let Ok(expr) = ob.cast::() { + return Ok(PyFilterExpr(expr.get().0.clone().into_filter())); + } + Err(PyTypeError::new_err(format!( + "expected a filter (filter.Expr or filter.FilterExpr), got {}", + ob.get_type().name()? + ))) + } +} + +/// Either side of `&`, `|`: a yes/no expression or a filter. +#[derive(FromPyObject)] +pub(crate) enum ExprOrFilter { + Expr(PyExpr), + Filter(PyFilterExpr), +} + +impl ExprOrFilter { + pub(crate) fn into_filter(self) -> FilterExpr { + match self { + ExprOrFilter::Expr(e) => e.0.into_filter(), + ExprOrFilter::Filter(f) => f.0, + } + } +} + #[pymethods] impl PyFilterExpr { - pub fn __and__(&self, other: &Self) -> Self { - PyFilterExpr(FilterExpr::And(vec![self.0.clone(), other.0.clone()])) + pub fn __and__(&self, other: ExprOrFilter) -> Self { + PyFilterExpr(FilterExpr::And(vec![self.0.clone(), other.into_filter()])) + } + + pub fn __rand__(&self, other: ExprOrFilter) -> Self { + PyFilterExpr(FilterExpr::And(vec![other.into_filter(), self.0.clone()])) + } + + pub fn __or__(&self, other: ExprOrFilter) -> PyResult { + let other = other.into_filter(); + no_view(&self.0)?; + no_view(&other)?; + Ok(PyFilterExpr(FilterExpr::Or(vec![self.0.clone(), other]))) } - pub fn __or__(&self, other: &Self) -> PyResult { + pub fn __ror__(&self, other: ExprOrFilter) -> PyResult { + let other = other.into_filter(); no_view(&self.0)?; - no_view(&other.0)?; - Ok(PyFilterExpr(FilterExpr::Or(vec![ - self.0.clone(), - other.0.clone(), - ]))) + no_view(&other)?; + Ok(PyFilterExpr(FilterExpr::Or(vec![other, self.0.clone()]))) } fn __invert__(&self) -> PyResult { @@ -58,7 +104,7 @@ impl PyFilterExpr { /// A view applies to the whole filter, so it can be `&`-ed with predicates or applied /// alone, but has no meaning under `|` or `~`. Refused where it is written, as the /// engine would refuse it when applied. -fn no_view(filter: &FilterExpr) -> PyResult<()> { +pub(crate) fn no_view(filter: &FilterExpr) -> PyResult<()> { if filter.has_view() { return Err(PyTypeError::new_err( "a view (filter.Graph...) applies to the whole filter: combine it with `&` or apply it alone, not with `|` or `~`", diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index 5706f65c8d..dea8bce08b 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -1,5 +1,5 @@ use crate::{ - db::graph::views::filter::model::tree::{FilterExpr, ViewOp}, + db::graph::views::filter::model::expr::{FilterExpr, ViewOp}, python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index d99a86b207..50a3b8fb26 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,20 +1,24 @@ use crate::{ db::graph::views::filter::model::{ - node_state_filter::NodeStateBoolColOp, - tree::{ - Agg, CmpOp, Entity, Expr, Field, FilterExpr, OpaqueFilter, Qual, Scope, StrOp, - Structural, Target, ViewOp, + expr::{ + Agg, CmpOp, EdgeExpr, ExplodedEdgeExpr, Expr, Field, FilterExpr, Leaf, NodeExpr, + NodeLeaf, OpaqueFilter, StrOp, ViewOp, }, + node_expr::DynCreateOp, + node_state_filter::NodeStateBoolColOp, validate_const_comparable, }, + errors::GraphError, python::{ - filter::filter_expr::PyFilterExpr, graph::node_state::PyOutputNodeState, + filter::filter_expr::{no_view, ExprOrFilter, PyFilterExpr}, + graph::node_state::PyOutputNodeState, types::iterable::FromIterable, }, }; use pyo3::{ exceptions::{PyTypeError, PyValueError}, - pyclass, pymethods, Bound, FromPyObject, IntoPyObject, PyErr, PyResult, Python, + prelude::*, + IntoPyObjectExt, }; use raphtory_api::core::{ entities::properties::prop::{Prop, PropType}, @@ -23,9 +27,103 @@ use raphtory_api::core::{ }; use std::sync::Arc; -/// A value expression: a field, degree, property, metadata entry or an -/// aggregate over one. Comparing it to a value or to another expression gives -/// a [`FilterExpr`]. +/// An expression over one kind of entity. Which kind is fixed by where the +/// chain started (`filter.Node`, `filter.Edge`, `filter.ExplodedEdge`), so +/// the tree it holds is the entity's own. +#[derive(Clone)] +pub(crate) enum Typed { + Node(NodeExpr), + Edge(EdgeExpr), + ExplodedEdge(ExplodedEdgeExpr), +} + +/// The same construction on the expression, whatever its entity. +macro_rules! map_typed { + ($typed:expr, |$e:ident| $body:expr) => { + match $typed { + Typed::Node($e) => Typed::Node($body), + Typed::Edge($e) => Typed::Edge($body), + Typed::ExplodedEdge($e) => Typed::ExplodedEdge($body), + } + }; +} + +/// A construction over two expressions of the same entity; a mix is refused. +macro_rules! zip_typed { + ($a:expr, $b:expr, |$l:ident, $r:ident| $body:expr) => { + match ($a, $b) { + (Typed::Node($l), Typed::Node($r)) => Ok(Typed::Node($body)), + (Typed::Edge($l), Typed::Edge($r)) => Ok(Typed::Edge($body)), + (Typed::ExplodedEdge($l), Typed::ExplodedEdge($r)) => Ok(Typed::ExplodedEdge($body)), + (a, b) => Err(mixed(&a, &b)), + } + }; +} + +fn mixed(a: &Typed, b: &Typed) -> PyErr { + PyTypeError::new_err(format!( + "cannot combine {} expression with {} expression", + a.entity(), + b.entity() + )) +} + +impl Typed { + fn entity(&self) -> &'static str { + match self { + Typed::Node(_) => "a node", + Typed::Edge(_) => "an edge", + Typed::ExplodedEdge(_) => "an exploded edge", + } + } + + /// The compiled value, for its statically known type and nullability. + fn compile_value(&self) -> Result, GraphError> { + match self { + Typed::Node(e) => e.compile_value(), + Typed::Edge(e) => e.compile_value(), + Typed::ExplodedEdge(e) => e.compile_value(), + } + } + + /// A constant standing on the other side of this expression. + fn constant(&self, value: Prop) -> Typed { + match self { + Typed::Node(_) => Typed::Node(Expr::Const(value)), + Typed::Edge(_) => Typed::Edge(Expr::Const(value)), + Typed::ExplodedEdge(_) => Typed::ExplodedEdge(Expr::Const(value)), + } + } + + /// The filter this yes/no expression is, on its entity. + pub(crate) fn into_filter(self) -> FilterExpr { + match self { + Typed::Node(e) => NodeLeaf::filter(e), + Typed::Edge(e) => as Into>::into(e), + Typed::ExplodedEdge(e) => FilterExpr::ExplodedEdge(e), + } + } +} + +impl From for FilterExpr { + fn from(e: EdgeExpr) -> Self { + FilterExpr::Edge(e) + } +} + +impl std::fmt::Display for Typed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Typed::Node(e) => e.fmt(f), + Typed::Edge(e) => e.fmt(f), + Typed::ExplodedEdge(e) => e.fmt(f), + } + } +} + +/// A value expression: a field, degree, property, metadata entry, an aggregate +/// over one, or a yes/no built from them. Comparing it to a value or to another +/// expression gives a yes/no [`Expr`], which is a filter on its entity. #[pyclass( frozen, subclass, @@ -34,7 +132,7 @@ use std::sync::Arc; from_py_object )] #[derive(Clone)] -pub struct PyExpr(pub(crate) Expr); +pub struct PyExpr(pub(crate) Typed); /// A property read, which can switch to the property's history with `temporal()`. #[pyclass( @@ -45,7 +143,20 @@ pub struct PyExpr(pub(crate) Expr); from_py_object )] #[derive(Clone)] -pub struct PyPropertyExpr(pub(crate) Expr); +pub struct PyPropertyExpr { + latest: Typed, + history: Typed, +} + +impl PyPropertyExpr { + /// Both readings of the property, built by `read(temporal)`. + pub(crate) fn new(read: impl Fn(bool) -> Typed) -> Self { + PyPropertyExpr { + latest: read(false), + history: read(true), + } + } +} impl<'py> IntoPyObject<'py> for PyPropertyExpr { type Target = PyPropertyExpr; @@ -53,7 +164,7 @@ impl<'py> IntoPyObject<'py> for PyPropertyExpr { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyExpr(self.0.clone()); + let parent = PyExpr(self.latest.clone()); Bound::new(py, (self, parent)) } } @@ -68,19 +179,20 @@ enum ExprOrValue { /// Values are checked against the expression's statically known type at the /// comparison itself, so a mistyped literal fails where it is written instead -/// of at some later `filter()` call. Unknown types defer to filter time. -fn static_type(lhs: &Expr) -> PyResult { - Ok(lhs.compile()?.dyn_prop_type()) +/// of at some later `filter()` call. Unknown types defer to filter time. The +/// check is the engine's own; python only asks it early. +fn static_type(lhs: &Typed) -> PyResult { + Ok(lhs.compile_value()?.dyn_prop_type()) } -fn check_value(lhs: &Expr, v: &Prop) -> PyResult<()> { +fn check_value(lhs: &Typed, v: &Prop) -> PyResult<()> { validate_const_comparable(&static_type(lhs)?, Some(v)) .map_err(|e| PyTypeError::new_err(e.to_string())) } /// Presence tests only mean something on an expression that can be missing. -fn check_nullable(lhs: &Expr, op: &str) -> PyResult<()> { - if !lhs.compile()?.dyn_nullable() { +fn check_nullable(lhs: &Typed, op: &str) -> PyResult<()> { + if !lhs.compile_value()?.dyn_nullable() { return Err(PyTypeError::new_err(format!( "{op}() is not valid on an expression that always has a value" ))); @@ -94,155 +206,248 @@ fn check_str_value(v: &Prop) -> PyResult<()> { .map_err(|e| PyTypeError::new_err(e.to_string())) } -/// The right-hand side of a comparison, with a constant checked against the lhs. -fn compared(lhs: &Expr, other: ExprOrValue) -> PyResult { - Ok(match other { - ExprOrValue::Expr(e) => e.0, - ExprOrValue::Value(v) => { - check_value(lhs, &v)?; - Expr::Const(v) - } - }) -} +impl PyExpr { + fn compare(&self, op: CmpOp, other: ExprOrValue) -> PyResult { + let rhs = match other { + ExprOrValue::Expr(e) => e.0, + ExprOrValue::Value(v) => { + check_value(&self.0, &v)?; + self.0.constant(v) + } + }; + Ok(PyExpr(zip_typed!(self.0.clone(), rhs, |l, r| Expr::Cmp( + op, + Box::new(l), + Box::new(r) + ))?)) + } + + fn string_op(&self, op: StrOp, other: ExprOrValue) -> PyResult { + let rhs = match other { + ExprOrValue::Expr(e) => e.0, + ExprOrValue::Value(v) => { + check_str_value(&v)?; + self.0.constant(v) + } + }; + Ok(PyExpr(zip_typed!(self.0.clone(), rhs, |l, r| Expr::Str( + op.clone(), + Box::new(l), + Box::new(r) + ))?)) + } + + fn membership(&self, values: FromIterable, negated: bool) -> PyExpr { + let values: Vec = values.into(); + PyExpr(map_typed!(self.0.clone(), |e| Expr::In { + expr: Box::new(e), + values: values.clone(), + negated, + })) + } + + fn presence(&self, none: bool, name: &str) -> PyResult { + check_nullable(&self.0, name)?; + Ok(PyExpr(map_typed!(self.0.clone(), |e| if none { + Expr::IsNone(Box::new(e)) + } else { + Expr::IsSome(Box::new(e)) + }))) + } + + fn agg(&self, agg: Agg) -> PyExpr { + PyExpr(map_typed!(self.0.clone(), |e| Expr::Agg(agg, Box::new(e)))) + } -/// The right-hand side of a string operator. -fn string_operand(lhs: &Expr, other: ExprOrValue, typed: bool) -> PyResult { - Ok(match other { - ExprOrValue::Expr(e) => e.0, - ExprOrValue::Value(v) => { - check_str_value(&v)?; - if typed { - check_value(lhs, &v)?; + /// `&` / `|` with another expression of the same entity stays an + /// expression; with a filter, or across entities, it is a filter. + fn combine<'py>( + &self, + py: Python<'py>, + other: ExprOrFilter, + all: bool, + ) -> PyResult> { + if let ExprOrFilter::Expr(other) = &other { + let joined = zip_typed!(self.0.clone(), other.0.clone(), |l, r| if all { + Expr::And(vec![l, r]) + } else { + Expr::Or(vec![l, r]) + }); + if let Ok(joined) = joined { + return PyExpr(joined).into_bound_py_any(py); } - Expr::Const(v) } - }) + let other = other.into_filter(); + let mine = self.0.clone().into_filter(); + let filter = if all { + FilterExpr::And(vec![mine, other]) + } else { + no_view(&other)?; + FilterExpr::Or(vec![mine, other]) + }; + PyFilterExpr(filter).into_bound_py_any(py) + } } #[pymethods] impl PyExpr { - fn __eq__(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Cmp { - op: CmpOp::Eq, - lhs: self.0.clone(), - rhs: compared(&self.0, other)?, - })) + fn __eq__(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Eq, other) } - fn __ne__(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Cmp { - op: CmpOp::Ne, - lhs: self.0.clone(), - rhs: compared(&self.0, other)?, - })) + + fn __ne__(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Ne, other) } - fn __lt__(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Cmp { - op: CmpOp::Lt, - lhs: self.0.clone(), - rhs: compared(&self.0, other)?, - })) + + fn __lt__(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Lt, other) } - fn __le__(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Cmp { - op: CmpOp::Le, - lhs: self.0.clone(), - rhs: compared(&self.0, other)?, - })) + + fn __le__(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Le, other) } - fn __gt__(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Cmp { - op: CmpOp::Gt, - lhs: self.0.clone(), - rhs: compared(&self.0, other)?, - })) + + fn __gt__(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Gt, other) } - fn __ge__(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Cmp { - op: CmpOp::Ge, - lhs: self.0.clone(), - rhs: compared(&self.0, other)?, - })) + + fn __ge__(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Ge, other) } - /// Checks whether the value's string representation starts with the given value. + /// `self == other`, as a method, so a qualifier can follow without brackets: + /// `filter.Node.property("p").temporal().eq(3).any()`. /// /// Arguments: - /// other (Prop | filter.Expr): Prefix to check for. + /// other (Prop | filter.Expr): The value or expression to compare with. /// /// Returns: - /// filter.FilterExpr: - fn starts_with(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Str { - op: StrOp::StartsWith, - lhs: self.0.clone(), - rhs: string_operand(&self.0, other, true)?, - })) + /// filter.Expr: + fn eq(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Eq, other) } - /// Checks whether the value's string representation ends with the given value. + + /// `self != other`, as a method. /// /// Arguments: - /// other (Prop | filter.Expr): Suffix to check for. + /// other (Prop | filter.Expr): The value or expression to compare with. /// /// Returns: - /// filter.FilterExpr: - fn ends_with(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Str { - op: StrOp::EndsWith, - lhs: self.0.clone(), - rhs: string_operand(&self.0, other, true)?, - })) + /// filter.Expr: + fn ne(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Ne, other) } - /// Checks whether the value's string representation contains the given value. + + /// `self < other`, as a method. /// /// Arguments: - /// other (Prop | filter.Expr): Substring that must appear within the value. + /// other (Prop | filter.Expr): The value or expression to compare with. /// /// Returns: - /// filter.FilterExpr: - fn contains(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Str { - op: StrOp::Contains, - lhs: self.0.clone(), - rhs: string_operand(&self.0, other, true)?, - })) + /// filter.Expr: + fn lt(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Lt, other) } - /// Checks whether the value's string representation **does not** contain the given value. + + /// `self <= other`, as a method. /// /// Arguments: - /// other (Prop | filter.Expr): Substring that must not appear within the value. + /// other (Prop | filter.Expr): The value or expression to compare with. /// /// Returns: - /// filter.FilterExpr: - fn not_contains(&self, other: ExprOrValue) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Str { - op: StrOp::NotContains, - lhs: self.0.clone(), - rhs: string_operand(&self.0, other, true)?, - })) + /// filter.Expr: + fn le(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Le, other) } - /// Performs fuzzy matching against the value's string representation, within a Levenshtein distance and with optional prefix matching. + + /// `self > other`, as a method. /// /// Arguments: - /// other (Prop | filter.Expr): String to approximately match against. - /// levenshtein_distance (int): Maximum allowed Levenshtein distance. - /// prefix_match (bool): Whether to require a matching prefix. + /// other (Prop | filter.Expr): The value or expression to compare with. /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: + fn gt(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Gt, other) + } + + /// `self >= other`, as a method. + /// + /// Arguments: + /// other (Prop | filter.Expr): The value or expression to compare with. + /// + /// Returns: + /// filter.Expr: + fn ge(&self, other: ExprOrValue) -> PyResult { + self.compare(CmpOp::Ge, other) + } + + /// Checks whether the string value starts with the given prefix. + /// + /// Arguments: + /// other (str | filter.Expr): The prefix, or an expression giving it. + /// + /// Returns: + /// filter.Expr: + fn starts_with(&self, other: ExprOrValue) -> PyResult { + self.string_op(StrOp::StartsWith, other) + } + + /// Checks whether the string value ends with the given suffix. + /// + /// Arguments: + /// other (str | filter.Expr): The suffix, or an expression giving it. + /// + /// Returns: + /// filter.Expr: + fn ends_with(&self, other: ExprOrValue) -> PyResult { + self.string_op(StrOp::EndsWith, other) + } + + /// Checks whether the string value contains the given substring. + /// + /// Arguments: + /// other (str | filter.Expr): The substring, or an expression giving it. + /// + /// Returns: + /// filter.Expr: + fn contains(&self, other: ExprOrValue) -> PyResult { + self.string_op(StrOp::Contains, other) + } + + /// Checks whether the string value does **not** contain the given substring. + /// + /// Arguments: + /// other (str | filter.Expr): The substring, or an expression giving it. + /// + /// Returns: + /// filter.Expr: + fn not_contains(&self, other: ExprOrValue) -> PyResult { + self.string_op(StrOp::NotContains, other) + } + + /// Checks whether the string value is within a Levenshtein distance of the given text. + /// + /// Arguments: + /// other (str | filter.Expr): The text to match, or an expression giving it. + /// levenshtein_distance (int): Maximum edit distance for a match. + /// prefix_match (bool): Whether a prefix match within the distance also passes. + /// + /// Returns: + /// filter.Expr: fn fuzzy_search( &self, other: ExprOrValue, levenshtein_distance: usize, prefix_match: bool, - ) -> PyResult { - Ok(PyFilterExpr(FilterExpr::Str { - op: StrOp::FuzzySearch { + ) -> PyResult { + self.string_op( + StrOp::FuzzySearch { levenshtein_distance, prefix_match, }, - lhs: self.0.clone(), - rhs: string_operand(&self.0, other, false)?, - })) + other, + ) } /// Checks whether the value is contained within the given values. @@ -251,121 +456,142 @@ impl PyExpr { /// values (list[Prop]): Values to match against. /// /// Returns: - /// filter.FilterExpr: - fn is_in(&self, values: FromIterable) -> PyResult { - Ok(PyFilterExpr(FilterExpr::In { - expr: self.0.clone(), - values: values.into(), - negated: false, - })) + /// filter.Expr: + fn is_in(&self, values: FromIterable) -> PyExpr { + self.membership(values, false) } + /// Checks whether the value is **not** contained within the given values. /// /// Arguments: /// values (list[Prop]): Values to exclude. /// /// Returns: - /// filter.FilterExpr: - fn is_not_in(&self, values: FromIterable) -> PyResult { - Ok(PyFilterExpr(FilterExpr::In { - expr: self.0.clone(), - values: values.into(), - negated: true, - })) + /// filter.Expr: + fn is_not_in(&self, values: FromIterable) -> PyExpr { + self.membership(values, true) } - /// Checks whether the value is present (not `None`). + /// Checks whether the value is present. /// /// Returns: - /// filter.FilterExpr: - fn is_some(&self) -> PyResult { - check_nullable(&self.0, "is_some")?; - Ok(PyFilterExpr(FilterExpr::IsSome(self.0.clone()))) + /// filter.Expr: + fn is_some(&self) -> PyResult { + self.presence(false, "is_some") } - /// Checks whether the value is `None` / missing. + + /// Checks whether the value is missing. /// /// Returns: - /// filter.FilterExpr: - fn is_none(&self) -> PyResult { - check_nullable(&self.0, "is_none")?; - Ok(PyFilterExpr(FilterExpr::IsNone(self.0.clone()))) + /// filter.Expr: + fn is_none(&self) -> PyResult { + self.presence(true, "is_none") } - /// Requires that **any** element matches when the value is list-like (a temporal history or a list property). + /// Requires that **any** element matches. Follows a comparison against a + /// list-like value (a temporal history or a list property): + /// `(filter.Node.property("p").temporal() > 4).any()`. /// /// Returns: /// filter.Expr: - fn any(&self) -> Self { - PyExpr(Expr::Qual(Qual::Any, Box::new(self.0.clone()))) + fn any(&self) -> PyExpr { + PyExpr(map_typed!(self.0.clone(), |e| Expr::Any(Box::new(e)))) } - /// Requires that **all** elements match when the value is list-like (a temporal history or a list property). + + /// Requires that **all** elements match. Follows a comparison against a + /// list-like value (a temporal history or a list property): + /// `(filter.Node.property("p").temporal() > 4).all()`. /// /// Returns: /// filter.Expr: - fn all(&self) -> Self { - PyExpr(Expr::Qual(Qual::All, Box::new(self.0.clone()))) + fn all(&self) -> PyExpr { + PyExpr(map_typed!(self.0.clone(), |e| Expr::All(Box::new(e)))) } /// Sums the elements when the value is numeric and list-like. /// /// Returns: /// filter.Expr: - fn sum(&self) -> Self { - PyExpr(Expr::Agg(Agg::Sum, Box::new(self.0.clone()))) + fn sum(&self) -> PyExpr { + self.agg(Agg::Sum) } + /// Averages the elements when the value is numeric and list-like. /// /// Returns: /// filter.Expr: - fn avg(&self) -> Self { - PyExpr(Expr::Agg(Agg::Avg, Box::new(self.0.clone()))) + fn avg(&self) -> PyExpr { + self.agg(Agg::Avg) } + /// Selects the minimum element when the value is list-like. /// /// Returns: /// filter.Expr: - fn min(&self) -> Self { - PyExpr(Expr::Agg(Agg::Min, Box::new(self.0.clone()))) + fn min(&self) -> PyExpr { + self.agg(Agg::Min) } + /// Selects the maximum element when the value is list-like. /// /// Returns: /// filter.Expr: - fn max(&self) -> Self { - PyExpr(Expr::Agg(Agg::Max, Box::new(self.0.clone()))) + fn max(&self) -> PyExpr { + self.agg(Agg::Max) } + /// Selects the first element when the value is list-like. /// /// Returns: /// filter.Expr: - fn first(&self) -> Self { - PyExpr(Expr::Agg(Agg::First, Box::new(self.0.clone()))) + fn first(&self) -> PyExpr { + self.agg(Agg::First) } + /// Selects the last element when the value is list-like. /// /// Returns: /// filter.Expr: - fn last(&self) -> Self { - PyExpr(Expr::Agg(Agg::Last, Box::new(self.0.clone()))) + fn last(&self) -> PyExpr { + self.agg(Agg::Last) } + /// Selects the number of elements when the value is list-like. /// /// Returns: /// filter.Expr: - fn len(&self) -> Self { - PyExpr(Expr::Agg(Agg::Len, Box::new(self.0.clone()))) + fn len(&self) -> PyExpr { + self.agg(Agg::Len) + } + + fn __and__<'py>(&self, py: Python<'py>, other: ExprOrFilter) -> PyResult> { + self.combine(py, other, true) + } + + fn __or__<'py>(&self, py: Python<'py>, other: ExprOrFilter) -> PyResult> { + self.combine(py, other, false) + } + + fn __invert__(&self) -> PyExpr { + PyExpr(map_typed!(self.0.clone(), |e| Expr::Not(Box::new(e)))) + } + + /// Shows the expression tree: what runs locally and what a server receives. + fn __repr__(&self) -> String { + format!("Expr({})", self.0) } } #[pymethods] impl PyPropertyExpr { /// Switches from the property's latest value to its full temporal history, - /// unlocking the aggregate chain (`sum`, `avg`, `min`, `max`, `any`, ...). + /// unlocking the aggregate chain (`sum`, `avg`, `min`, `max`, ...) and the + /// element-wise comparisons `any()` / `all()` collapse. /// /// Returns: /// filter.Expr: fn temporal(&self) -> PyExpr { - PyExpr(Expr::Temporal(Box::new(self.0.clone()))) + PyExpr(self.history.clone()) } } @@ -375,38 +601,56 @@ impl PyPropertyExpr { /// `Node.latest()`, ...); its field and property methods evaluate within that /// view, and its own view methods narrow it further. #[pyclass(frozen, name = "NodeFilter", module = "raphtory.filter")] -pub struct PyNodeFilter(pub(crate) Scope); +pub struct PyNodeFilter(pub(crate) Vec); impl PyNodeFilter { pub(crate) fn root() -> Self { - PyNodeFilter(Scope::new(Entity::Node)) + PyNodeFilter(Vec::new()) } fn with_view(&self, view: ViewOp) -> Self { - PyNodeFilter(self.0.clone().with_view(view)) + let mut views = self.0.clone(); + views.push(view); + PyNodeFilter(views) } - fn read(&self, target: Target) -> Expr { - Expr::Read { - scope: self.0.clone(), - target, - } + fn read(&self, leaf: NodeLeaf) -> PyExpr { + PyExpr(Typed::Node(Expr::Read(leaf))) + } + + fn field(&self, field: Field) -> PyExpr { + self.read(NodeLeaf::Field { + views: self.0.clone(), + field, + }) + } + + fn degree_read(&self, direction: Direction) -> PyExpr { + self.read(NodeLeaf::Degree { + views: self.0.clone(), + direction, + }) + } + + fn property_read(&self, name: String) -> PyPropertyExpr { + PyPropertyExpr::new(|temporal| { + Typed::Node(Expr::Read(NodeLeaf::property( + self.0.clone(), + name.clone(), + temporal, + ))) + }) } } #[pymethods] impl PyNodeFilter { - #[new] - fn new() -> PyNodeFilter { - Self::root() - } - /// Selects the node ID field for filtering. /// /// Returns: /// filter.Expr: fn id(&self) -> PyExpr { - PyExpr(self.read(Target::Field(Field::Id))) + self.field(Field::Id) } /// Selects the node name field for filtering. @@ -414,7 +658,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn name(&self) -> PyExpr { - PyExpr(self.read(Target::Field(Field::Name))) + self.field(Field::Name) } /// Selects the node type field for filtering. @@ -422,7 +666,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn node_type(&self) -> PyExpr { - PyExpr(self.read(Target::Field(Field::NodeType))) + self.field(Field::NodeType) } /// Selects incoming node degree for filtering. @@ -430,7 +674,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn in_degree(&self) -> PyExpr { - PyExpr(self.read(Target::Degree(Direction::IN))) + self.degree_read(Direction::IN) } /// Selects total node degree for filtering. @@ -438,7 +682,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn degree(&self) -> PyExpr { - PyExpr(self.read(Target::Degree(Direction::BOTH))) + self.degree_read(Direction::BOTH) } /// Selects outgoing node degree for filtering. @@ -446,7 +690,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn out_degree(&self) -> PyExpr { - PyExpr(self.read(Target::Degree(Direction::OUT))) + self.degree_read(Direction::OUT) } /// Filters a node property by name. @@ -459,7 +703,7 @@ impl PyNodeFilter { /// Returns: /// filter.PropertyExpr: fn property(&self, name: String) -> PyPropertyExpr { - PyPropertyExpr(self.read(Target::Property(name))) + self.property_read(name) } /// Filters a node metadata field by name. @@ -472,7 +716,7 @@ impl PyNodeFilter { /// Returns: /// filter.Expr: fn metadata(&self, name: String) -> PyExpr { - PyExpr(self.read(Target::Metadata(name))) + self.read(NodeLeaf::metadata(self.0.clone(), name)) } /// Restricts node evaluation to the given time window. @@ -574,12 +818,9 @@ impl PyNodeFilter { /// Matches nodes that have at least one event in the current view. /// /// Returns: - /// filter.FilterExpr: - fn is_active(&self) -> PyFilterExpr { - PyFilterExpr(FilterExpr::Structural { - scope: self.0.clone(), - pred: Structural::IsActive, - }) + /// filter.Expr: + fn is_active(&self) -> PyExpr { + self.read(NodeLeaf::is_active(self.0.clone())) } /// Build a node filter from a boolean column of an existing node-state result. @@ -797,9 +1038,9 @@ impl PyNode { /// Matches nodes that have at least one event in the current view. /// /// Returns: - /// filter.FilterExpr: + /// filter.Expr: #[staticmethod] - fn is_active() -> PyFilterExpr { + fn is_active() -> PyExpr { PyNodeFilter::root().is_active() } diff --git a/raphtory/src/python/types/macros/trait_impl/filter_ops.rs b/raphtory/src/python/types/macros/trait_impl/filter_ops.rs index 7e4d767462..81b3c7209c 100644 --- a/raphtory/src/python/types/macros/trait_impl/filter_ops.rs +++ b/raphtory/src/python/types/macros/trait_impl/filter_ops.rs @@ -16,7 +16,7 @@ macro_rules! impl_filter_ops { /// Return a filtered view that only includes nodes and edges that satisfy the filter /// /// Arguments: - /// filter (filter.FilterExpr): The filter to apply to the nodes and edges. + /// filter (filter.FilterExpr | filter.Expr): The filter to apply; a yes/no expression is the filter on its entity. /// /// Returns: #[doc=concat!(" ", $name, ": The filtered view")] From 82706ec1066a9164978dbbd3fb7d8c8e79cb0756 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:21:37 +0100 Subject: [PATCH 127/155] remove the field-based Filter type and its operator paths `Filter`, `FieldFilterValue` and the generic `FilterValue` had no remaining callers: field predicates compile through the expression tree now. Drop them together with `FilterOperator::apply` / `apply_id` and the private helpers only those two used, and the prelude re-export. --- raphtory-tests/tests/test_filters.rs | 31 --- .../db/graph/views/filter/model/expr/tests.rs | 11 +- .../src/db/graph/views/filter/model/filter.rs | 246 ------------------ .../views/filter/model/filter_operator.rs | 125 +-------- .../graph/views/filter/model/filter_value.rs | 50 ---- .../src/db/graph/views/filter/model/mod.rs | 2 - raphtory/src/lib.rs | 3 +- 7 files changed, 12 insertions(+), 456 deletions(-) delete mode 100644 raphtory/src/db/graph/views/filter/model/filter.rs delete mode 100644 raphtory/src/db/graph/views/filter/model/filter_value.rs diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 26d1299a09..fd1376a8d0 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -5,7 +5,6 @@ use raphtory::{db::api::view::StaticGraphViewOps, prelude::*}; mod test_composite_filters { use raphtory::{ db::graph::views::filter::model::{ - filter::Filter, node_filter::NodeFilter, property_filter::{PropertyFilter, PropertyFilterValue, PropertyRef}, FilterOperator, @@ -43,36 +42,6 @@ mod test_composite_filters { )) } - #[test] - fn test_fuzzy_search() { - let filter = Filter::fuzzy_search("name", "pomet", 2, false); - assert!(filter.matches(Some("pometry"))); - - let filter = Filter::fuzzy_search("name", "shivam_kapoor", 2, false); - assert!(filter.matches(Some("shivam_kapoor2"))); - - let filter = Filter::fuzzy_search("name", "shivam kapoor", 2, false); - assert!(filter.matches(Some("shivam_kapoor2"))); - - let filter = Filter::fuzzy_search("name", "shivam kapoor", 2, false); - assert!(filter.matches(Some("shivam_kapoor2"))); - - let filter = Filter::fuzzy_search("name", "shivam kapoor", 2, false); - assert!(!filter.matches(Some("shivam1_kapoor2"))); - - let filter = Filter::fuzzy_search("name", "khivam sapoor", 2, false); - assert!(!filter.matches(Some("shivam1_kapoor2"))); - } - - #[test] - fn test_fuzzy_search_prefix_match() { - let filter = Filter::fuzzy_search("name", "pome", 2, false); - assert!(!filter.matches(Some("pometry"))); - - let filter = Filter::fuzzy_search("name", "pome", 2, true); - assert!(filter.matches(Some("pometry"))); - } - #[test] fn test_fuzzy_search_property() { let filter = prop_filter( diff --git a/raphtory/src/db/graph/views/filter/model/expr/tests.rs b/raphtory/src/db/graph/views/filter/model/expr/tests.rs index d07bb0cca7..2f213234fe 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/tests.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/tests.rs @@ -9,10 +9,10 @@ use crate::{ }, prelude::{AdditionOps, EdgeViewOps, Graph, GraphViewOps, NodeViewOps, TimeOps, NO_PROPS}, }; -use std::sync::Arc; use raphtory_api::core::{ entities::properties::prop::IntoProp, storage::timeindex::EventTime, Direction, }; +use std::sync::Arc; /// alice.score 3@0 7@2 9@6 · bob.score 5@1 2@7 · carol none · dave.score 1@2 1@3 /// eve.scores [1,2]@0 [5,5]@1 @@ -474,7 +474,10 @@ fn before_and_at_agree_with_the_graph_views() { assert_eq!(edges(&g, &view(ViewOp::Before(EventTime::start(2)))), none); assert_eq!(applied(&g, typed(EdgeFilter.before(2))), none); assert_eq!(edge_names(&g.before(3)), at_two); - assert_eq!(edges(&g, &view(ViewOp::Before(EventTime::start(3)))), at_two); + assert_eq!( + edges(&g, &view(ViewOp::Before(EventTime::start(3)))), + at_two + ); // `at(t)` covers the whole timestamp, even when handed a time that sits // between two events at `t`. @@ -490,7 +493,9 @@ fn before_and_at_agree_with_the_graph_views() { ["carol->dave"] ); assert_eq!( - edges(&g, &view(ViewOp::Window { + edges( + &g, + &view(ViewOp::Window { start: from_second_event, end: EventTime::start(3), }), diff --git a/raphtory/src/db/graph/views/filter/model/filter.rs b/raphtory/src/db/graph/views/filter/model/filter.rs deleted file mode 100644 index 01d13af43f..0000000000 --- a/raphtory/src/db/graph/views/filter/model/filter.rs +++ /dev/null @@ -1,246 +0,0 @@ -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 FieldFilterValue { - Single(String), - Set(Arc>), - ID(GID), - IDSet(Arc>), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Filter { - pub field_name: String, - pub field_value: FieldFilterValue, - pub operator: FilterOperator, -} - -impl Display for Filter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.field_value { - FieldFilterValue::Single(value) => { - write!(f, "{} {} {}", self.field_name, self.operator, value) - } - FieldFilterValue::Set(values) => { - let mut sorted: Vec<&String> = values.iter().collect(); - sorted.sort(); - let values_str = sorted - .iter() - .map(|s| s.as_str()) - .collect::>() - .join(", "); - write!(f, "{} {} [{}]", self.field_name, self.operator, values_str) - } - FieldFilterValue::ID(id) => { - write!(f, "{} {} {}", self.field_name, self.operator, id) - } - FieldFilterValue::IDSet(values) => { - let mut sorted: Vec<&GID> = values.iter().collect(); - sorted.sort(); - let values_str = sorted - .iter() - .map(ToString::to_string) - .collect::>() - .join(", "); - write!(f, "{} {} [{}]", self.field_name, self.operator, values_str) - } - } - } -} - -impl Filter { - pub fn eq(field_name: impl Into, field_value: impl Into) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::Single(field_value.into()), - operator: FilterOperator::Eq, - } - } - - pub fn ne(field_name: impl Into, field_value: impl Into) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::Single(field_value.into()), - operator: FilterOperator::Ne, - } - } - - pub fn is_in( - field_name: impl Into, - field_values: impl IntoIterator>, - ) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::Set(Arc::new( - field_values.into_iter().map(|s| s.into()).collect(), - )), - operator: FilterOperator::IsIn, - } - } - - /// Is not in - /// - /// Arguments: - /// field_name (str) - /// field_values (list[str]): - pub fn is_not_in( - field_name: impl Into, - field_values: impl IntoIterator>, - ) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::Set(Arc::new( - field_values.into_iter().map(|s| s.into()).collect(), - )), - operator: FilterOperator::IsNotIn, - } - } - - pub fn starts_with(field_name: impl Into, field_value: impl Into) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::Single(field_value.into()), - operator: FilterOperator::StartsWith, - } - } - - pub fn ends_with(field_name: impl Into, field_value: impl Into) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::Single(field_value.into()), - operator: FilterOperator::EndsWith, - } - } - - pub fn contains(field_name: impl Into, field_value: impl Into) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::Single(field_value.into()), - operator: FilterOperator::Contains, - } - } - - pub fn not_contains(field_name: impl Into, field_value: impl Into) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::Single(field_value.into()), - operator: FilterOperator::NotContains, - } - } - - /// Returns a filter expression that checks if the specified properties approximately match the specified string. - /// - /// Uses a specified Levenshtein distance and optional prefix matching. - /// - /// Arguments: - /// levenshtein_distance (int): - /// prefix_match (bool): - /// - /// Returns: - /// PropValue (str): - pub fn fuzzy_search( - field_name: impl Into, - field_value: impl Into, - levenshtein_distance: usize, - prefix_match: bool, - ) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::Single(field_value.into()), - operator: FilterOperator::FuzzySearch { - levenshtein_distance, - prefix_match, - }, - } - } - - pub fn eq_id(field_name: impl Into, field_value: impl Into) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::ID(field_value.into()), - operator: FilterOperator::Eq, - } - } - - pub fn ne_id(field_name: impl Into, field_value: impl Into) -> Self { - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::ID(field_value.into()), - operator: FilterOperator::Ne, - } - } - - pub fn is_in_id(field_name: impl Into, field_values: I) -> Self - where - I: IntoIterator, - V: Into, - { - let set: HashSet = field_values.into_iter().map(|x| x.into()).collect(); - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::IDSet(Arc::new(set)), - operator: FilterOperator::IsIn, - } - } - - pub fn is_not_in_id(field_name: impl Into, field_values: I) -> Self - where - I: IntoIterator, - V: Into, - { - let set: HashSet = field_values.into_iter().map(|x| x.into()).collect(); - Self { - field_name: field_name.into(), - field_value: FieldFilterValue::IDSet(Arc::new(set)), - operator: FilterOperator::IsNotIn, - } - } - - pub fn lt>(field_name: impl Into, field_value: V) -> Self { - Filter { - field_name: field_name.into(), - field_value: FieldFilterValue::ID(field_value.into()), - operator: FilterOperator::Lt, - } - } - - pub fn le>(field_name: impl Into, field_value: V) -> Self { - Filter { - field_name: field_name.into(), - field_value: FieldFilterValue::ID(field_value.into()), - operator: FilterOperator::Le, - } - } - - pub fn gt>(field_name: impl Into, field_value: V) -> Self { - Filter { - field_name: field_name.into(), - field_value: FieldFilterValue::ID(field_value.into()), - operator: FilterOperator::Gt, - } - } - - pub fn ge>(field_name: impl Into, field_value: V) -> Self { - Filter { - field_name: field_name.into(), - field_value: FieldFilterValue::ID(field_value.into()), - operator: FilterOperator::Ge, - } - } - - pub fn matches(&self, node_value: Option<&str>) -> bool { - self.operator.apply(&self.field_value, node_value) - } - - pub fn id_matches(&self, node_value: GidRef<'_>) -> bool { - self.operator.apply_id(&self.field_value, node_value) - } -} - -// The GraphQL model spells a field's filter value `FilterValue`; the generic -// `filter_value::FilterValue` owns that name here, so the field form carries -// the longer one and this alias keeps the model's spelling valid. -pub type FilterValue = FieldFilterValue; 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 60b982fd34..83be244230 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -1,11 +1,9 @@ -use crate::db::graph::views::filter::model::{ - filter::FilterValue, property_filter::PropertyFilterValue, -}; +use crate::db::graph::views::filter::model::property_filter::PropertyFilterValue; use raphtory_api::core::{ - entities::{properties::prop::Prop, GidRef, GID}, + entities::{properties::prop::Prop, GID}, storage::arc_str::ArcStr, }; -use std::{collections::HashSet, fmt, fmt::Display, ops::Deref}; +use std::{fmt, fmt::Display, ops::Deref}; use strsim::levenshtein; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -77,21 +75,6 @@ impl FilterOperator { ) } - fn operation(&self) -> impl Fn(&T, &T) -> bool - where - T: ?Sized + PartialEq + PartialOrd, - { - match self { - FilterOperator::Eq => T::eq, - FilterOperator::Ne => T::ne, - FilterOperator::Lt => T::lt, - FilterOperator::Le => T::le, - FilterOperator::Gt => T::gt, - FilterOperator::Ge => T::ge, - _ => panic!("Operation not supported for this operator"), - } - } - /// Fuzzy search /// /// Arguments: @@ -114,17 +97,6 @@ impl FilterOperator { } } - fn collection_operation(&self) -> impl Fn(&HashSet, &T) -> bool - where - T: Eq + std::hash::Hash, - { - match self { - FilterOperator::IsIn => |set: &HashSet, value: &T| set.contains(value), - FilterOperator::IsNotIn => |set: &HashSet, value: &T| !set.contains(value), - _ => panic!("Collection operation not supported for this operator"), - } - } - pub fn apply_to_property(&self, left: &PropertyFilterValue, right: Option<&Prop>) -> bool { use std::cmp::Ordering::*; use FilterOperator::*; @@ -221,97 +193,6 @@ impl FilterOperator { }, } } - - pub fn apply(&self, left: &FilterValue, right: Option<&str>) -> bool { - match left { - FilterValue::Single(l) => match self { - FilterOperator::Eq | FilterOperator::Ne => match right { - Some(r) => self.operation()(r, l), - None => matches!(self, FilterOperator::Ne), - }, - FilterOperator::StartsWith => right.is_some_and(|r| r.starts_with(l)), - FilterOperator::EndsWith => right.is_some_and(|r| r.ends_with(l)), - FilterOperator::Contains => right.is_some_and(|r| r.contains(l)), - FilterOperator::NotContains => right.is_some_and(|r| !r.contains(l)), - FilterOperator::FuzzySearch { - levenshtein_distance, - prefix_match, - } => right.is_some_and(|r| { - let fuzzy_fn = self.fuzzy_search(*levenshtein_distance, *prefix_match); - fuzzy_fn(l, r) - }), - _ => unreachable!(), - }, - - FilterValue::Set(l) => match self { - FilterOperator::IsIn | FilterOperator::IsNotIn => match right { - Some(r) => self.collection_operation()(l, &r.to_string()), - None => matches!(self, FilterOperator::IsNotIn), - }, - _ => unreachable!(), - }, - - FilterValue::ID(_) | FilterValue::IDSet(_) => unreachable!(), - } - } - - pub fn apply_id(&self, left: &FilterValue, right: GidRef<'_>) -> bool { - match left { - FilterValue::ID(GID::U64(l)) => match right { - GidRef::U64(r) => match self { - FilterOperator::Eq - | FilterOperator::Ne - | FilterOperator::Lt - | FilterOperator::Le - | FilterOperator::Gt - | FilterOperator::Ge => self.operation()(&r, l), - _ => false, - }, - GidRef::Str(_) => false, - }, - - FilterValue::ID(GID::Str(ls)) | FilterValue::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), - FilterOperator::EndsWith => rs.ends_with(ls), - FilterOperator::Contains => rs.contains(ls), - FilterOperator::NotContains => !rs.contains(ls), - FilterOperator::FuzzySearch { - levenshtein_distance, - prefix_match, - } => { - let f = self.fuzzy_search(*levenshtein_distance, *prefix_match); - f(ls, rs) - } - _ => false, - }, - GidRef::U64(_) => false, - }, - - FilterValue::IDSet(set) => match right { - GidRef::U64(r) => match self { - FilterOperator::IsIn => set.contains(&GID::U64(r)), - FilterOperator::IsNotIn => !set.contains(&GID::U64(r)), - _ => false, - }, - GidRef::Str(s) => match self { - FilterOperator::IsIn => set.contains(&GID::Str(s.to_string())), - FilterOperator::IsNotIn => !set.contains(&GID::Str(s.to_string())), - _ => false, - }, - }, - - FilterValue::Set(set) => match right { - GidRef::U64(_) => false, - GidRef::Str(s) => match self { - FilterOperator::IsIn => set.contains(s), - FilterOperator::IsNotIn => !set.contains(s), - _ => false, - }, - }, - } - } } // ── expr-layer operator kinds (consumed by node_expr/edge_expr) ── diff --git a/raphtory/src/db/graph/views/filter/model/filter_value.rs b/raphtory/src/db/graph/views/filter/model/filter_value.rs deleted file mode 100644 index ef778604c1..0000000000 --- a/raphtory/src/db/graph/views/filter/model/filter_value.rs +++ /dev/null @@ -1,50 +0,0 @@ -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 c1910dfaf4..72c2f1fba8 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -68,9 +68,7 @@ pub mod edge_expr; pub mod edge_filter; pub mod exploded_edge_filter; pub mod expr; -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/lib.rs b/raphtory/src/lib.rs index 8c3477b829..a9b76d0c88 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -144,8 +144,7 @@ pub mod prelude { views::{ deletion_graph::PersistentGraph, filter::model::{ - filter::Filter, node_filter::NodeFilter, property_filter::PropertyFilter, - EdgeFilter, + node_filter::NodeFilter, property_filter::PropertyFilter, EdgeFilter, }, property_redacted_graph::PropertyRedaction, PropertyRedactedGraph, From 0f30b9e42a378e5f94ac5d9c8c374e03afb46ee8 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:28:55 +0100 Subject: [PATCH 128/155] drop the CreateOp impls on the view wrappers Windowed, Latest, Layered, SnapshotAt and SnapshotLatest only scope a read or a validity predicate through CreateView now; nothing wraps a compiled op in them any more. --- .../graph/views/filter/model/latest_filter.rs | 29 +++-------- .../views/filter/model/layered_filter.rs | 34 +++---------- .../views/filter/model/snapshot_filter.rs | 50 +++---------------- .../views/filter/model/windowed_filter.rs | 30 ++--------- 4 files changed, 24 insertions(+), 119 deletions(-) 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 a8232f46b6..32cb7f7e65 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -1,13 +1,12 @@ use crate::{ db::{ - api::{state::NodeOp, view::internal::GraphView}, + api::view::internal::GraphView, graph::views::{ filter::{ model::{ - edge_expr::EdgeOp, is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, - node_expr::CreateOp, windowed_filter::Windowed, CombinedFilter, + is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, + is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, windowed_filter::Windowed, CombinedFilter, ComposableFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, NodeViewFilterOps, Wrap, }, @@ -19,8 +18,8 @@ use crate::{ errors::GraphError, prelude::TimeOps, }; -use raphtory_api::core::{entities::properties::prop::Prop, storage::timeindex::EventTime}; -use std::{fmt, fmt::Display, sync::Arc}; +use raphtory_api::core::storage::timeindex::EventTime; +use std::{fmt, fmt::Display}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Latest { @@ -152,19 +151,3 @@ impl CreateView for Latest { // ── expr layer: the latest view scopes any inner expression (per-expression view) ── // Nesting order of chained views is pinned by the view-semantics tests. - -impl CreateOp for Latest { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner.create_node_op(graph.latest()) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner.create_edge_op(graph.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 49d4c07c61..dd13b93dd5 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -1,14 +1,13 @@ use crate::{ db::{ - api::{state::NodeOp, view::internal::GraphView}, + api::view::internal::GraphView, graph::views::{ filter::{ model::{ - edge_expr::EdgeOp, is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, - node_expr::CreateOp, CombinedFilter, ComposableFilter, EdgeViewFilterOps, - InternalViewWrapOps, NodeViewFilterOps, Wrap, + is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, + is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, CombinedFilter, ComposableFilter, + EdgeViewFilterOps, InternalViewWrapOps, NodeViewFilterOps, Wrap, }, CreateFilter, }, @@ -18,11 +17,8 @@ use crate::{ errors::GraphError, prelude::LayerOps, }; -use raphtory_api::core::{ - entities::{properties::prop::Prop, Layer}, - storage::timeindex::EventTime, -}; -use std::{fmt, fmt::Display, sync::Arc}; +use raphtory_api::core::{entities::Layer, storage::timeindex::EventTime}; +use std::{fmt, fmt::Display}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Layered { @@ -172,19 +168,3 @@ impl EdgeViewFilterOps for Layered { // ── expr layer: the layer view scopes any inner expression (per-expression view) ── // Nesting order of chained views is pinned by the view-semantics tests. - -impl CreateOp for Layered { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner.create_node_op(graph.layers(self.layer.clone())?) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner.create_edge_op(graph.layers(self.layer.clone())?) - } -} 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 4e02597279..f7f80a4e23 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -1,16 +1,12 @@ use crate::{ db::{ - api::{ - state::NodeOp, - view::{internal::GraphView, time::TimeOps}, - }, + api::view::{internal::GraphView, time::TimeOps}, graph::views::{ filter::{ model::{ - edge_expr::EdgeOp, is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, - node_expr::CreateOp, windowed_filter::Windowed, CombinedFilter, + is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, + is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, windowed_filter::Windowed, CombinedFilter, ComposableFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, NodeViewFilterOps, Wrap, }, @@ -21,10 +17,8 @@ use crate::{ }, errors::GraphError, }; -use raphtory_api::core::{ - entities::properties::prop::Prop, storage::timeindex::EventTime, utils::time::IntoTime, -}; -use std::{fmt, fmt::Display, sync::Arc}; +use raphtory_api::core::{storage::timeindex::EventTime, utils::time::IntoTime}; +use std::{fmt, fmt::Display}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SnapshotAt { @@ -289,37 +283,5 @@ impl CreateView for SnapshotLatest { // ── expr layer: the snapshot-at view scopes any inner expression (per-expression view) ── // Nesting order of chained views is pinned by the view-semantics tests. -impl CreateOp for SnapshotAt { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner.create_node_op(graph.snapshot_at(self.time)) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner.create_edge_op(graph.snapshot_at(self.time)) - } -} - // ── expr layer: the snapshot-latest view scopes any inner expression (per-expression view) ── // Nesting order of chained views is pinned by the view-semantics tests. - -impl CreateOp for SnapshotLatest { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner.create_node_op(graph.snapshot_latest()) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner.create_edge_op(graph.snapshot_latest()) - } -} 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 b0c584d2be..63ea6a73a2 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -1,13 +1,12 @@ use crate::{ db::{ - api::{state::NodeOp, view::internal::GraphView}, + api::view::internal::GraphView, graph::views::{ filter::{ model::{ - edge_expr::EdgeOp, is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, - node_expr::CreateOp, CombinedFilter, ComposableFilter, CreateView, + is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, + is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, CombinedFilter, ComposableFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, NodeViewFilterOps, Wrap, }, CreateFilter, @@ -19,11 +18,10 @@ use crate::{ prelude::TimeOps, }; use raphtory_api::core::{ - entities::properties::prop::Prop, storage::timeindex::{AsTime, EventTime}, utils::time::IntoTime, }; -use std::{fmt, fmt::Display, sync::Arc}; +use std::{fmt, fmt::Display}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Windowed { @@ -182,21 +180,3 @@ impl CreateView for Windowed { // ── expr layer: the windowed view scopes any inner expression (per-expression view) ── // Nesting order of chained views is pinned by the view-semantics tests. - -impl CreateOp for Windowed { - fn create_node_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner - .create_node_op(graph.window(self.start, self.end)) - } - - fn create_edge_op<'g, G: GraphView + 'g>( - &self, - graph: G, - ) -> Result> + 'g>, GraphError> { - self.inner - .create_edge_op(graph.window(self.start, self.end)) - } -} From 82bee0dcb22dd5547f2885be8a665466b3a5078f Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:48:08 +0100 Subject: [PATCH 129/155] remove the unused Property and Metadata leaf structs Reads carry their name inside PropertyExpr and MetadataExpr; these two bare-name structs had no callers. --- .../graph/views/filter/model/edge_expr/mod.rs | 2 -- .../src/db/graph/views/filter/model/mod.rs | 32 ------------------- .../graph/views/filter/model/node_expr/mod.rs | 1 - 3 files changed, 35 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index 30e3210bf9..1a22b8ad42 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -24,8 +24,6 @@ use raphtory_storage::graph::graph::GraphStorage; pub mod ops; -pub use super::{Metadata, Property}; - // ───────────────────────────────────────────────────────────────────────────── // EdgeOp — compiled evaluator: EdgeRef → typed value // ───────────────────────────────────────────────────────────────────────────── diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 72c2f1fba8..99a7de1588 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -261,38 +261,6 @@ pub enum EntityMarker { Const, } -// ───────────────────────────────────────────────────────────────────────────── -// Shared property name expressions -// -// These structs carry only a property name. They implement both NodeExpr and -// EdgeExpr in their respective modules (node_expr/exprs.rs, edge_expr/exprs.rs), -// reading from node_meta() or edge_meta() depending on the context. -// ───────────────────────────────────────────────────────────────────────────── - -/// Latest temporal property value — implements both `NodeExpr` and `EdgeExpr`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Property { - pub name: String, -} - -impl Property { - pub fn new(name: impl Into) -> Self { - Self { name: name.into() } - } -} - -/// Static metadata field — implements both `NodeExpr` and `EdgeExpr`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Metadata { - pub name: String, -} - -impl Metadata { - pub fn new(name: impl Into) -> Self { - Self { name: name.into() } - } -} - #[derive(Clone)] pub struct PropertyExpr { pub(crate) view_expr: 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 index 90b99c3c85..62e125bf22 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -16,7 +16,6 @@ pub mod ops; #[cfg(test)] mod tests; -pub use super::{Metadata, Property}; use crate::db::graph::views::filter::model::{edge_expr::EdgeOp, EntityMarker}; pub use dyn_expr::*; pub use exprs::*; From bbe047c9195961111ae88356a77b8cfe18e1f2f8 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:54:57 +0100 Subject: [PATCH 130/155] give CreateFilter one graph and three entry points A filter is applied through create_graph_filter, create_node_filter or create_edge_filter, each compiled against the graph it is handed. The view a filter used to build beside itself is gone: views scope reads, and a view leg is applied to the graph before the predicate runs. Unit predicates on a factory are reads scoped to its views (Scoped), so the view wrappers only ever wrap view chains. Every filter answers per edge: an edge predicate directly, anything else through EdgeExistsOp on its own filtered graph, which the edge collections use to select. Edge ops can tell layers and exploded instances apart when they need to. Removes Unfiltered, the erased factories' unit-predicate methods, the wrapper Wrap impls and the four view graphs left without callers. --- .../test_edges_collection_filter.py | 25 +-- .../src/model/graph/filter_expr_input.rs | 38 ++-- .../algorithms/components/in_components.rs | 4 +- .../algorithms/components/out_components.rs | 4 +- raphtory/src/db/api/view/filter_ops.rs | 17 +- raphtory/src/db/graph/edges.rs | 16 +- raphtory/src/db/graph/path.rs | 21 +- .../views/filter/edge_expr_filtered_graph.rs | 77 ++++++- .../graph/views/filter/edge_filtered_graph.rs | 72 ------- raphtory/src/db/graph/views/filter/mod.rs | 127 +++++------ .../db/graph/views/filter/model/and_filter.rs | 70 +++--- .../graph/views/filter/model/dyn_factory.rs | 65 +----- .../graph/views/filter/model/edge_expr/mod.rs | 17 ++ .../graph/views/filter/model/edge_expr/ops.rs | 154 ++++++++++++- .../graph/views/filter/model/edge_filter.rs | 35 +-- .../filter/model/exploded_edge_filter.rs | 32 +-- .../graph/views/filter/model/expr/compile.rs | 162 ++++++++------ .../graph/views/filter/model/expr/convert.rs | 102 ++++++--- .../graph/views/filter/model/graph_filter.rs | 53 +++-- .../filter/model/is_active_edge_filter.rs | 58 +---- .../filter/model/is_active_node_filter.rs | 58 +---- .../views/filter/model/is_deleted_filter.rs | 58 +---- .../views/filter/model/is_self_loop_filter.rs | 57 +---- .../views/filter/model/is_valid_filter.rs | 58 +---- .../graph/views/filter/model/latest_filter.rs | 99 +++------ .../views/filter/model/layered_filter.rs | 104 +++------ .../src/db/graph/views/filter/model/mod.rs | 204 ++++++++---------- .../graph/views/filter/model/node_expr/mod.rs | 4 +- .../views/filter/model/node_expr/tests.rs | 42 ++-- .../views/filter/model/node_filter/mod.rs | 23 +- .../db/graph/views/filter/model/not_filter.rs | 54 +++-- .../db/graph/views/filter/model/or_filter.rs | 67 +++--- .../views/filter/model/snapshot_filter.rs | 199 +++++------------ .../views/filter/model/windowed_filter.rs | 103 +++------ .../src/db/graph/views/is_active_graph.rs | 80 ------- .../src/db/graph/views/is_deleted_graph.rs | 89 -------- .../src/db/graph/views/is_self_loop_graph.rs | 75 ------- raphtory/src/db/graph/views/mod.rs | 3 - raphtory/src/python/filter/filter_expr.rs | 38 ++-- 39 files changed, 922 insertions(+), 1642 deletions(-) delete mode 100644 raphtory/src/db/graph/views/filter/edge_filtered_graph.rs delete mode 100644 raphtory/src/db/graph/views/is_active_graph.rs delete mode 100644 raphtory/src/db/graph/views/is_deleted_graph.rs delete mode 100644 raphtory/src/db/graph/views/is_self_loop_graph.rs diff --git a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py index 9cd28cc84c..c58e9b179b 100644 --- a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py +++ b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py @@ -287,8 +287,7 @@ def check(graph): def test_nested_edge_collection_matches_the_graph_filter(): def check(graph): atoms = _atoms() - # Node-kind filters fail open on the nested path — pinned in the broken-classes test. - working = {n: e for n, e in atoms.items() if n not in NODE_KIND} + working = dict(atoms) working["edge_prop & layer"] = atoms["edge_prop"] & atoms["layer"] working["edge_prop | src"] = atoms["edge_prop"] | atoms["src"] working["~edge_prop"] = ~atoms["edge_prop"] @@ -306,6 +305,7 @@ def _subset(atoms): """A representative slice of the working shapes: one atom per family plus one composite each.""" return { "edge_prop": atoms["edge_prop"], + "node_prop": atoms["node_prop"], "window": atoms["window"], "layer": atoms["layer"], "is_deleted": atoms["is_deleted"], @@ -322,6 +322,7 @@ def check(graph): every = _ids(graph.edges) want_sets = { "edge_prop": single["edge_prop"], + "node_prop": single["node_prop"], "window": single["window"], "layer": single["layer"], "is_deleted": single["is_deleted"], @@ -329,8 +330,6 @@ def check(graph): "edge_prop | dst": single["edge_prop"] | single["dst"], "~edge_prop": every - single["edge_prop"], } - # Node-kind filters fail open here too when the anchor node fails the predicate — pinned - # in the broken-classes test. exprs = _subset(atoms) for name in ("a", "b"): node = graph.node(name) @@ -338,7 +337,13 @@ def check(graph): for label, expr in exprs.items(): got = _ids(node.edges[expr]) assert got == incident & want_sets[label], f"node {name}: {label}" - reference = _ids(graph.filter(expr).node(name).edges) + # A node filter the anchor itself fails leaves it with no edges. + filtered_node = graph.filter(expr).node(name) + reference = ( + _ids(filtered_node.edges) + if filtered_node is not None + else frozenset() + ) assert got == reference, f"node {name}: {label} vs graph filter" return check @@ -351,6 +356,7 @@ def check(graph): every = _ids(graph.edges) want_sets = { "edge_prop": single["edge_prop"], + "node_prop": single["node_prop"], "window": single["window"], "layer": single["layer"], "is_deleted": single["is_deleted"], @@ -461,15 +467,6 @@ def check(graph): and _ids(graph.filter(expr).edges) == want ): fixed.append(label) - nested = sorted( - e.id for es in graph.nodes.edges[atoms["node_prop"]] for e in es - ) - nested_ref = sorted( - e.id for es in graph.filter(atoms["node_prop"]).nodes.edges for e in es - ) - per_node = _ids(graph.node("a").edges[atoms["node_prop"]]) - if nested == nested_ref and per_node == frozenset(): - fixed.append("per-node/nested edges with a node filter") assert ( not fixed ), f"now FIXED: {fixed} — move the class into the working set by deleting its rule" diff --git a/raphtory-graphql/src/model/graph/filter_expr_input.rs b/raphtory-graphql/src/model/graph/filter_expr_input.rs index 8a86b222c8..63332ea64c 100644 --- a/raphtory-graphql/src/model/graph/filter_expr_input.rs +++ b/raphtory-graphql/src/model/graph/filter_expr_input.rs @@ -34,7 +34,7 @@ use raphtory::{ }, DynFilter, }, - CreateFilter, + CreateFilter, DynEdgeFilter, }, }, errors::GraphError, @@ -772,43 +772,43 @@ impl TryFrom for DynFilter { } impl CreateFilter for GqlFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type FilteredGraph<'graph, G> = DynGraphArc<'graph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type NodeFilter<'graph, G> = Arc + 'graph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = DynGraphArc<'graph> + type EdgeFilter<'graph, G> + = DynEdgeFilter<'graph> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - expr::FilterExpr::try_from(self)?.create_filter(graph, filtered) + ) -> Result, GraphError> { + expr::FilterExpr::try_from(self)?.create_graph_filter(graph) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - expr::FilterExpr::try_from(self)?.create_node_filter(graph, filtered) + ) -> Result, GraphError> { + expr::FilterExpr::try_from(self)?.create_node_filter(graph) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - expr::FilterExpr::try_from(self.clone())?.filter_graph_view(graph) + ) -> Result, GraphError> { + expr::FilterExpr::try_from(self)?.create_edge_filter(graph) } } diff --git a/raphtory/src/algorithms/components/in_components.rs b/raphtory/src/algorithms/components/in_components.rs index dc37124a6b..c038145180 100644 --- a/raphtory/src/algorithms/components/in_components.rs +++ b/raphtory/src/algorithms/components/in_components.rs @@ -92,7 +92,7 @@ pub fn in_components_filtered( where G: StaticGraphViewOps, F: CreateFilter + 'static, - F::EntityFiltered<'static, G, F::FilteredGraph<'static, G>>: StaticGraphViewOps, + F::FilteredGraph<'static, G>: StaticGraphViewOps, { let filtered = g.filter(filter)?; let ctx: Context<_, _> = (&filtered).into(); @@ -193,7 +193,7 @@ pub fn in_component_filtered<'graph, G, F>( where G: GraphViewOps<'graph>, F: CreateFilter + 'graph, - F::EntityFiltered<'graph, G, F::FilteredGraph<'graph, G>>: GraphViewOps<'graph>, + F::FilteredGraph<'graph, G>: GraphViewOps<'graph>, { let mut in_components = IndexMap::with_hasher(ahash::RandomState::new()); let mut to_check_stack = VecDeque::new(); diff --git a/raphtory/src/algorithms/components/out_components.rs b/raphtory/src/algorithms/components/out_components.rs index 868a570371..dffbd9e6d7 100644 --- a/raphtory/src/algorithms/components/out_components.rs +++ b/raphtory/src/algorithms/components/out_components.rs @@ -89,7 +89,7 @@ pub fn out_components_filtered( where G: StaticGraphViewOps, F: CreateFilter + 'static, - F::EntityFiltered<'static, G, F::FilteredGraph<'static, G>>: StaticGraphViewOps, + F::FilteredGraph<'static, G>: StaticGraphViewOps, { let filtered = g.filter(filter)?; let ctx: Context<_, _> = (&filtered).into(); @@ -190,7 +190,7 @@ pub fn out_component_filtered<'graph, G, F>( where G: GraphViewOps<'graph>, F: CreateFilter + 'graph, - F::EntityFiltered<'graph, G, F::FilteredGraph<'graph, G>>: GraphViewOps<'graph>, + F::FilteredGraph<'graph, G>: GraphViewOps<'graph>, { let mut out_components = IndexMap::with_hasher(ahash::RandomState::new()); let mut to_check_stack = VecDeque::new(); diff --git a/raphtory/src/db/api/view/filter_ops.rs b/raphtory/src/db/api/view/filter_ops.rs index 6b5e46e9a4..c1fa98d56b 100644 --- a/raphtory/src/db/api/view/filter_ops.rs +++ b/raphtory/src/db/api/view/filter_ops.rs @@ -10,14 +10,8 @@ pub trait Filter<'graph>: InternalFilter<'graph> { fn filter( &self, filter: F, - ) -> Result< - Self::Filtered< - F::EntityFiltered<'graph, Self::Graph, F::FilteredGraph<'graph, Self::Graph>>, - >, - GraphError, - > { - let fg = filter.filter_graph_view(self.base_graph().clone())?; - Ok(self.apply_filter(filter.create_filter(self.base_graph().clone(), fg)?)) + ) -> Result>, GraphError> { + Ok(self.apply_filter(filter.create_graph_filter(self.base_graph().clone())?)) } } @@ -31,13 +25,10 @@ pub trait Select<'graph>: 'graph { impl<'graph, T: InternalNodeSelect<'graph> + 'graph> Select<'graph> for T { type IterFiltered = - >::IterFiltered< - Filter::NodeFilter<'graph, T::IterGraph, Filter::FilteredGraph<'graph, T::IterGraph>>, - >; + >::IterFiltered>; fn select(&self, filter: F) -> Result, GraphError> { - let fg = filter.filter_graph_view(self.iter_graph().clone())?; - Ok(self.apply_iter_filter(filter.create_node_filter(self.iter_graph().clone(), fg)?)) + Ok(self.apply_iter_filter(filter.create_node_filter(self.iter_graph().clone())?)) } } diff --git a/raphtory/src/db/graph/edges.rs b/raphtory/src/db/graph/edges.rs index 54850cbb6c..ae2c19fd20 100644 --- a/raphtory/src/db/graph/edges.rs +++ b/raphtory/src/db/graph/edges.rs @@ -13,7 +13,7 @@ use crate::{ graph::{ edge::EdgeView, path::{PathFromGraph, PathFromNode}, - views::filter::CreateFilter, + views::filter::{edge_expr_filtered_graph::EdgeExprFilteredGraph, CreateFilter}, }, }, errors::GraphError, @@ -238,14 +238,11 @@ impl<'graph, G: GraphView + 'graph> Select<'graph> for Edges<'graph, G> { &self, filter: F, ) -> Result, GraphError> { - // Chain onto the current select rather than AND a fresh filter with the base graph: - // AndFilteredGraph inherits time semantics from its base, so a time view (window/before/ - // after/snapshot) on the right operand is silently dropped and the collection fails open. - let filtered_graph = filter.filter_graph_view(self.select.clone())?; - let filtered_graph = filter.create_filter(self.select.clone(), filtered_graph)?; + // Chain onto the current select so every earlier selection keeps its say. + let filter = filter.create_edge_filter(self.select.clone())?; Ok(Edges { base_graph: self.base_graph.clone(), - select: Arc::new(filtered_graph), + select: Arc::new(EdgeExprFilteredGraph::new(self.select.clone(), filter)), edges: self.edges.clone(), }) } @@ -433,12 +430,11 @@ impl<'graph, G: GraphView + 'graph> Select<'graph> for NestedEdges<'graph, G> { &self, filter: F, ) -> Result, GraphError> { - let filtered_graph = filter.filter_graph_view(self.select.clone())?; - let filtered_graph = filter.create_filter(self.select.clone(), filtered_graph)?; + let filter = filter.create_edge_filter(self.select.clone())?; Ok(NestedEdges { graph: self.graph.clone(), nodes: self.nodes.clone(), - select: Arc::new(filtered_graph), + select: Arc::new(EdgeExprFilteredGraph::new(self.select.clone(), filter)), edges: self.edges.clone(), }) } diff --git a/raphtory/src/db/graph/path.rs b/raphtory/src/db/graph/path.rs index fb2e9c65ba..789b81ebb4 100644 --- a/raphtory/src/db/graph/path.rs +++ b/raphtory/src/db/graph/path.rs @@ -15,7 +15,7 @@ use crate::{ create_node_type_filter, edges::{Edges, NestedEdges}, node::NodeView, - views::filter::{and_filtered_graph::AndFilteredGraph, CreateFilter}, + views::filter::CreateFilter, }, }, errors::GraphError, @@ -301,14 +301,8 @@ where &self, filter: F, ) -> Result, GraphError> { - let filter_graph = filter.filter_graph_view(self.base_graph.clone())?; - let filter = filter.create_node_filter(self.base_graph.clone(), filter_graph.clone())?; - - let select = Arc::new(AndFilteredGraph::new( - self.base_graph.clone(), - self.select.clone(), - filter_graph, - )); + let filter = filter.create_node_filter(self.base_graph.clone())?; + let select = self.select.clone(); let op = self.op.clone(); let op = Arc::new(move |graph: DynGraphArc<'graph>, node| { @@ -552,13 +546,8 @@ where filter: F, ) -> Result, GraphError> { let op = self.op.clone(); - let filter_graph = filter.filter_graph_view(self.base_graph.clone())?; - let select = Arc::new(AndFilteredGraph::new( - self.base_graph.clone(), - self.select.clone(), - filter_graph.clone(), - )); - let filter_op = filter.create_node_filter(self.base_graph.clone(), filter_graph)?; + let select = self.select.clone(); + let filter_op = filter.create_node_filter(self.base_graph.clone())?; Ok(PathFromNode { base_graph: self.base_graph.clone(), select, diff --git a/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs index e24b758a88..6cc383ad08 100644 --- a/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs @@ -6,19 +6,27 @@ use crate::{ InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, }, view::internal::{ - Immutable, InheritEdgeHistoryFilter, InheritEdgeLayerFilterOps, - InheritExplodedEdgeFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, - InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, - InheritTimeSemantics, InternalEdgeFilterOps, Static, + Immutable, InheritEdgeHistoryFilter, InheritLayerOps, InheritListOps, + InheritMaterialize, InheritNodeFilterOps, InheritNodeHistoryFilter, + InheritStorageOps, InheritTimeSemantics, InternalEdgeFilterOps, + InternalEdgeLayerFilterOps, InternalExplodedEdgeFilterOps, Static, }, }, graph::views::filter::model::edge_expr::EdgeOp, }, prelude::GraphViewOps, }; -use raphtory_api::{core::entities::edges::edge_ref::EdgeRef, inherit::Base}; +use either::Either; +use raphtory_api::{ + core::{ + entities::{edges::edge_ref::EdgeRef, LayerId, ELID}, + storage::timeindex::EventTime, + }, + inherit::Base, +}; use raphtory_storage::{ - core_ops::InheritCoreGraphOps, graph::edges::edge_storage_ops::EdgeStorageOps, + core_ops::{CoreGraphOps, InheritCoreGraphOps}, + graph::edges::edge_storage_ops::EdgeStorageOps, }; use storage::EdgeEntryRef; @@ -97,13 +105,64 @@ impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritE for EdgeExprFilteredGraph { } -impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritExplodedEdgeFilterOps - for EdgeExprFilteredGraph +/// An op that tells exploded instances apart (`filters_exploded`) is asked about each +/// one; a plain op decides per edge and the instances of a kept edge all pass. +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> + InternalExplodedEdgeFilterOps for EdgeExprFilteredGraph { + fn internal_exploded_edge_filtered(&self) -> bool { + self.filter.filters_exploded() || self.graph.internal_exploded_edge_filtered() + } + + fn internal_exploded_filter_edge_list_trusted(&self) -> bool { + !self.filter.filters_exploded() && self.graph.internal_exploded_filter_edge_list_trusted() + } + + fn internal_filter_exploded_edge(&self, eid: ELID, t: EventTime, layer_ids: &LayerIds) -> bool { + if !self.graph.internal_filter_exploded_edge(eid, t, layer_ids) { + return false; + } + // Deletions carry no properties, so they always pass through: filtering + // them out would silently extend the previous addition's interval on a + // persistent graph. + if !self.filter.filters_exploded() || eid.is_deletion() { + return true; + } + let edge_ref: EdgeRef = self.core_edge(Either::Left(eid.eid())).out_ref(); + self.filter.apply_exploded( + self.graph.core_graph(), + edge_ref.at_layer(eid.layer()).at(t), + ) + } + + fn node_filter_includes_exploded_edge_filter(&self) -> bool { + !self.filter.filters_exploded() && self.graph.node_filter_includes_exploded_edge_filter() + } } -impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgeLayerFilterOps +impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InternalEdgeLayerFilterOps for EdgeExprFilteredGraph { + fn internal_edge_layer_filtered(&self) -> bool { + self.filter.filters_exploded() || self.graph.internal_edge_layer_filtered() + } + + fn internal_layer_filter_edge_list_trusted(&self) -> bool { + !self.filter.filters_exploded() && self.graph.internal_layer_filter_edge_list_trusted() + } + + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: LayerId) -> bool { + if !self.graph.internal_filter_edge_layer(edge, layer) { + return false; + } + !self.filter.filters_exploded() + || self + .filter + .apply_exploded(self.graph.core_graph(), edge.out_ref().at_layer(layer)) + } + + fn node_filter_includes_edge_layer_filter(&self) -> bool { + !self.filter.filters_exploded() && self.graph.node_filter_includes_edge_layer_filter() + } } impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InternalEdgeFilterOps diff --git a/raphtory/src/db/graph/views/filter/edge_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_filtered_graph.rs deleted file mode 100644 index 1ea6a923ac..0000000000 --- a/raphtory/src/db/graph/views/filter/edge_filtered_graph.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::db::api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - state::ops::GraphView, - view::internal::{ - FilterOps, Immutable, InheritEdgeHistoryFilter, InheritEdgeLayerFilterOps, - InheritExplodedEdgeFilterOps, InheritListOps, InheritMaterialize, InheritNodeFilterOps, - InheritNodeHistoryFilter, InheritStorageOps, InheritTimeSemantics, InternalEdgeFilterOps, - Static, - }, -}; -use raphtory_api::{core::entities::LayerIds, inherit::Base}; -use raphtory_storage::{core_ops::InheritCoreGraphOps, layer_ops::InheritLayerOps}; -use storage::EdgeEntryRef; - -#[derive(Debug, Clone)] -pub struct EdgeFilteredGraph { - base: G, - filter: F, -} - -impl EdgeFilteredGraph { - pub fn new(base: G, filter: F) -> Self { - Self { base, filter } - } -} - -impl Base for EdgeFilteredGraph { - type Base = G; - - fn base(&self) -> &Self::Base { - &self.base - } -} - -impl Static for EdgeFilteredGraph {} -impl Immutable for EdgeFilteredGraph {} - -impl InheritCoreGraphOps for EdgeFilteredGraph {} -impl InheritStorageOps for EdgeFilteredGraph {} -impl InheritLayerOps for EdgeFilteredGraph {} -impl InheritListOps for EdgeFilteredGraph {} -impl InheritMaterialize for EdgeFilteredGraph {} -impl InheritNodeFilterOps for EdgeFilteredGraph {} -impl InheritPropertiesOps for EdgeFilteredGraph {} -impl InheritNodePropertySchemaOps for EdgeFilteredGraph {} -impl InheritEdgePropertySchemaOps for EdgeFilteredGraph {} -impl InheritTimeSemantics for EdgeFilteredGraph {} -impl InheritNodeHistoryFilter for EdgeFilteredGraph {} -impl InheritEdgeHistoryFilter for EdgeFilteredGraph {} - -impl InheritEdgeLayerFilterOps for EdgeFilteredGraph {} - -impl InheritExplodedEdgeFilterOps for EdgeFilteredGraph {} - -impl InternalEdgeFilterOps for EdgeFilteredGraph { - #[inline] - fn internal_edge_filtered(&self) -> bool { - self.filter.filtered() - } - - #[inline] - fn internal_edge_list_trusted(&self) -> bool { - false - } - - #[inline] - fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { - self.base.internal_filter_edge(edge, layer_ids) && self.filter.filter_edge(edge) - } -} diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 83cf3f116e..97ccc72362 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -4,15 +4,17 @@ use crate::{ state::ops::{filter::NodeExistsOp, NodeFilterOp}, view::internal::GraphView, }, - graph::views::filter::node_filtered_graph::NodeFilteredGraph, + graph::views::filter::{ + model::edge_expr::{ops::EdgeExistsOp, EdgeOp}, + node_filtered_graph::NodeFilteredGraph, + }, }, errors::GraphError, - prelude::GraphViewOps, }; +use std::sync::Arc; pub mod and_filtered_graph; pub mod edge_expr_filtered_graph; -pub mod edge_filtered_graph; pub mod edge_node_filtered_graph; mod exploded_edge_expr_filtered_graph; pub mod exploded_edge_filtered_graph; @@ -25,129 +27,134 @@ pub mod or_filtered_graph; pub struct Exists; impl CreateFilter for Exists { - type EntityFiltered<'graph, G, F> - = F + type FilteredGraph<'graph, G> + = G where Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - type NodeFilter<'graph, G, F> - = NodeExistsOp + G: GraphView + 'graph; + + type NodeFilter<'graph, G> + = NodeExistsOp where Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G + G: GraphView + 'graph; + + type EdgeFilter<'graph, G> + = EdgeExistsOp where Self: 'graph, - G: GraphViewOps<'graph>; + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(filtered) + graph: G, + ) -> Result, GraphError> { + Ok(graph) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(NodeExistsOp::new(filtered)) + graph: G, + ) -> Result, GraphError> { + Ok(NodeExistsOp::new(graph)) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - Ok(graph) + ) -> Result, GraphError> { + Ok(EdgeExistsOp::new(graph)) } } +/// How a filter is applied. Each entry point compiles the filter against the graph +/// it is handed and answers for one kind of entity: the graph with the failing +/// entities hidden, a yes/no operation per node, or a yes/no operation per edge. +/// Every filter has a per-edge answer, since a filtered graph always decides which +/// edges it keeps; only a node predicate has a per-node one, and an edge predicate +/// refuses that entry point. pub trait CreateFilter: Sized { - type EntityFiltered<'graph, G, F>: GraphView + 'graph + type FilteredGraph<'graph, G>: GraphView + 'graph where Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; + G: GraphView + 'graph; - type NodeFilter<'graph, G, F>: NodeFilterOp + 'graph + type NodeFilter<'graph, G>: NodeFilterOp + 'graph where Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; + G: GraphView + 'graph; - type FilteredGraph<'graph, G>: GraphView + 'graph + type EdgeFilter<'graph, G>: EdgeOp + Clone + 'graph where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError>; + ) -> Result, GraphError>; - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError>; + ) -> Result, GraphError>; - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError>; + ) -> Result, GraphError>; } +/// An erased per-edge predicate, the edge filter of every erased or entity-agnostic +/// filter. +pub type DynEdgeFilter<'graph> = Arc + 'graph>; + impl CreateFilter for T { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type FilteredGraph<'graph, G> = NodeFilteredGraph where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type NodeFilter<'graph, G> = Self where - Self: 'graph; - type FilteredGraph<'graph, G> - = G + Self: 'graph, + G: GraphView + 'graph; + + type EdgeFilter<'graph, G> + = EdgeExistsOp> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - _filtered: F, - ) -> Result, GraphError> + ) -> Result, GraphError> where Self: 'graph, { Ok(NodeFilteredGraph::new(graph, self)) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, _graph: G, - _filtered: F, - ) -> Result, GraphError> + ) -> Result, GraphError> where Self: 'graph, { Ok(self) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> + ) -> Result, GraphError> where Self: 'graph, { - Ok(graph) + Ok(EdgeExistsOp::new(NodeFilteredGraph::new(graph, self))) } } 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 a685fd1ced..827e6e3ac9 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -5,11 +5,12 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - and_filtered_graph::AndFilteredGraph, model::ComposableFilter, CreateFilter, + and_filtered_graph::AndFilteredGraph, + model::{edge_expr::ops::AndEdgeOp, ComposableFilter}, + CreateFilter, }, }, errors::GraphError, - prelude::GraphViewOps, }; use std::{fmt, fmt::Display}; @@ -28,63 +29,48 @@ impl Display for AndFilter { impl ComposableFilter for AndFilter {} impl CreateFilter for AndFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = AndFilteredGraph< - G, - L::EntityFiltered<'graph, G, L::FilteredGraph<'graph, F>>, - R::EntityFiltered<'graph, G, R::FilteredGraph<'graph, F>>, - > + type FilteredGraph<'graph, G> + = AndFilteredGraph, R::FilteredGraph<'graph, G>> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = AndOp< - L::NodeFilter<'graph, G, L::FilteredGraph<'graph, F>>, - R::NodeFilter<'graph, G, R::FilteredGraph<'graph, F>>, - > + type NodeFilter<'graph, G> + = AndOp, R::NodeFilter<'graph, G>> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G + type EdgeFilter<'graph, G> + = AndEdgeOp, R::EdgeFilter<'graph, G>> where Self: 'graph, - G: GraphViewOps<'graph>; + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - let l = self.left.filter_graph_view(filtered.clone())?; - let r = self.right.filter_graph_view(filtered)?; - let left = self.left.create_filter(graph.clone(), l)?; - let right = self.right.create_filter(graph.clone(), r)?; + ) -> Result, GraphError> { + let left = self.left.create_graph_filter(graph.clone())?; + let right = self.right.create_graph_filter(graph.clone())?; Ok(AndFilteredGraph::new(graph, left, right)) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> - where - Self: 'graph, - { - let l = self.left.filter_graph_view(filtered.clone())?; - let r = self.right.filter_graph_view(filtered)?; - let left = self.left.create_node_filter(graph.clone(), l)?; - let right = self.right.create_node_filter(graph, r)?; + ) -> Result, GraphError> { + 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, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> - where - Self: 'graph, - { - Ok(graph) + ) -> Result, GraphError> { + let left = self.left.create_edge_filter(graph.clone())?; + let right = self.right.create_edge_filter(graph)?; + Ok(AndEdgeOp { left, right }) } } diff --git a/raphtory/src/db/graph/views/filter/model/dyn_factory.rs b/raphtory/src/db/graph/views/filter/model/dyn_factory.rs index 9325ac52ef..a247d96cfa 100644 --- a/raphtory/src/db/graph/views/filter/model/dyn_factory.rs +++ b/raphtory/src/db/graph/views/filter/model/dyn_factory.rs @@ -10,16 +10,10 @@ use crate::db::graph::views::filter::model::{ after_bounds, at_bounds, before_bounds, - is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, node_expr::{DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr}, windowed_filter::Windowed, - CombinedFilter, CreateView, DynCreateFilter, DynCreateView, DynPropertyExprFactory, - EdgeFilterFactory, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, NodeFilterFactory, - NodeViewFilterOps, PropertyExprFactory, ViewWrapOps, + CreateView, DynCreateView, DynPropertyExprFactory, EdgeFilterFactory, EntityMarker, + InternalViewWrapOps, NodeFilterFactory, PropertyExprFactory, ViewWrapOps, }; use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; @@ -33,7 +27,6 @@ pub trait DynNodeFilterFactory: fn dyn_degree(&self) -> Arc; fn dyn_in_degree(&self) -> Arc; fn dyn_out_degree(&self) -> Arc; - fn dyn_is_active(&self) -> Arc; fn dyn_metadata(&self, name: String) -> Arc; fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc; @@ -57,7 +50,7 @@ impl InternalViewWrapOps for Arc { impl DynNodeFilterFactory for T where - T: NodeFilterFactory + NodeViewFilterOps + Send + Sync + 'static, + T: NodeFilterFactory + Send + Sync + 'static, { fn dyn_id(&self) -> Arc { Arc::new(self.id()) @@ -79,10 +72,6 @@ where Arc::new(self.out_degree()) } - fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) - } - fn dyn_metadata(&self, name: String) -> Arc { Arc::new(PropertyExprFactory::metadata(self, name)) } @@ -100,23 +89,10 @@ impl NodeFilterFactory for Arc { type NodeWindow = Self::Window; } -impl NodeViewFilterOps for Arc { - type Output = Arc; - - fn is_active(&self) -> Self::Output { - self.as_ref().dyn_is_active() - } -} - pub trait DynEdgeFilterFactory: DynEntityExpr + DynCreateView + Send + Sync + 'static { fn dyn_property(&self, name: String) -> Arc; fn dyn_metadata(&self, name: String) -> Arc; - fn dyn_is_active(&self) -> Arc; - fn dyn_is_valid(&self) -> Arc; - fn dyn_is_deleted(&self) -> Arc; - fn dyn_is_self_loop(&self) -> Arc; - fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc; fn dyn_at(&self, time: EventTime) -> Arc; fn dyn_after(&self, time: EventTime) -> Arc; @@ -129,26 +105,6 @@ pub trait DynEdgeFilterFactory: DynEntityExpr + DynCreateView + Send + Sync + 's impl EdgeFilterFactory for Arc {} -impl EdgeViewFilterOps for Arc { - type Output = Arc; - - fn is_active(&self) -> Self::Output { - self.as_ref().dyn_is_active() - } - - fn is_valid(&self) -> Self::Output { - self.as_ref().dyn_is_valid() - } - - fn is_deleted(&self) -> Self::Output { - self.as_ref().dyn_is_deleted() - } - - fn is_self_loop(&self) -> Self::Output { - self.as_ref().dyn_is_self_loop() - } -} - impl InternalViewWrapOps for Arc { type Window = Arc; @@ -159,7 +115,7 @@ impl InternalViewWrapOps for Arc { impl DynEdgeFilterFactory for T where - T: EdgeFilterFactory + EdgeViewFilterOps + ViewWrapOps + CreateView + EntityExpr + Clone, + T: EdgeFilterFactory + ViewWrapOps + CreateView + EntityExpr + Clone, T: Send + Sync + 'static, ::Marker: Into, { @@ -170,19 +126,6 @@ where Arc::new(PropertyExprFactory::metadata(self, name)) } - fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) - } - fn dyn_is_valid(&self) -> Arc { - Arc::new(self.is_valid()) - } - fn dyn_is_deleted(&self) -> Arc { - Arc::new(self.is_deleted()) - } - fn dyn_is_self_loop(&self) -> Arc { - Arc::new(self.is_self_loop()) - } - // The window wrapper is constructed over the erased factory directly: // routing through ViewWrapOps::window would dispatch straight back into // this method through the erased build_window. diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index 1a22b8ad42..17cab31dd0 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -44,4 +44,21 @@ pub trait EdgeOp: Send + Sync { fn const_value(&self) -> Option { None } + + /// Whether the answer depends on which layer or exploded instance of the edge is + /// asked about. A plain op answers for the edge as a whole, and a filtered graph + /// consults it once per edge. + fn filters_exploded(&self) -> bool { + false + } + + /// The answer for one layer, or one exploded instance, of an edge: `edge` carries + /// the layer and, for an instance, the time. An op that answers per edge sees the + /// edge as a whole. + fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> Self::Output { + self.apply( + storage, + EdgeRef::new(edge.pid(), edge.src(), edge.dst(), edge.dir()), + ) + } } diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index fd1be29728..a3a6616ebc 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -7,17 +7,19 @@ use crate::{ api::{ properties::internal::{InternalMetadataOps, InternalTemporalPropertyViewOps}, state::ops::Const, - view::internal::GraphView, + view::internal::{FilterOps, GraphView}, }, graph::edge::EdgeView, }, prelude::EdgeViewOps, }; +use either::Either; use raphtory_api::core::entities::{ edges::edge_ref::EdgeRef, properties::prop::{Prop, PropType}, + ELID, }; -use raphtory_storage::graph::graph::GraphStorage; +use raphtory_storage::{core_ops::CoreGraphOps, graph::graph::GraphStorage}; use super::EdgeOp; use crate::db::{api::state::ops::NodeOp, graph::views::filter::model::edge_filter::Endpoint}; @@ -42,6 +44,14 @@ impl<'a, V: Clone + Send + Sync> EdgeOp for Arc + 'a> { fn const_value(&self) -> Option { self.as_ref().const_value() } + + fn filters_exploded(&self) -> bool { + self.as_ref().filters_exploded() + } + + fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> V { + self.as_ref().apply_exploded(storage, edge) + } } // ───────────────────────────────────────────────────────────────────────────── @@ -246,3 +256,143 @@ impl EdgeOp for IsSelfLoopEdgePropOp { PropType::Bool } } + +// ───────────────────────────────────────────────────────────────────────────── +// Edge predicates: existence in a view and the boolean combinators +// ───────────────────────────────────────────────────────────────────────────── + +/// Whether the edge exists in `graph`; the edge side of `NodeExistsOp`. +#[derive(Debug, Clone)] +pub struct EdgeExistsOp { + graph: G, +} + +impl EdgeExistsOp { + pub(crate) fn new(graph: G) -> Self { + Self { graph } + } +} + +impl EdgeOp for EdgeExistsOp { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + self.graph + .filter_edge(storage.core_edge(Either::Right(edge)).as_ref()) + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn filters_exploded(&self) -> bool { + true + } + + fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + match (edge.layer(), edge.time()) { + (Some(layer), Some(t)) => self + .graph + .filter_exploded_edge(ELID::new(edge.pid(), layer), t), + (Some(layer), None) => self + .graph + .filter_edge_layer(storage.core_edge(Either::Right(edge)).as_ref(), layer), + (None, _) => self.apply(storage, edge), + } + } +} + +#[derive(Debug, Clone)] +pub struct AndEdgeOp { + pub(crate) left: L, + pub(crate) right: R, +} + +impl, R: EdgeOp> EdgeOp for AndEdgeOp { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + self.left.apply(storage, edge) && self.right.apply(storage, edge) + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn const_value(&self) -> Option { + match (self.left.const_value(), self.right.const_value()) { + (Some(false), _) | (_, Some(false)) => Some(false), + (Some(true), Some(true)) => Some(true), + _ => None, + } + } + + fn filters_exploded(&self) -> bool { + self.left.filters_exploded() || self.right.filters_exploded() + } + + fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + self.left.apply_exploded(storage, edge) && self.right.apply_exploded(storage, edge) + } +} + +#[derive(Debug, Clone)] +pub struct OrEdgeOp { + pub(crate) left: L, + pub(crate) right: R, +} + +impl, R: EdgeOp> EdgeOp for OrEdgeOp { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + self.left.apply(storage, edge) || self.right.apply(storage, edge) + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn const_value(&self) -> Option { + match (self.left.const_value(), self.right.const_value()) { + (Some(true), _) | (_, Some(true)) => Some(true), + (Some(false), Some(false)) => Some(false), + _ => None, + } + } + + fn filters_exploded(&self) -> bool { + self.left.filters_exploded() || self.right.filters_exploded() + } + + fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + self.left.apply_exploded(storage, edge) || self.right.apply_exploded(storage, edge) + } +} + +#[derive(Debug, Clone)] +pub struct NotEdgeOp(pub(crate) T); + +impl> EdgeOp for NotEdgeOp { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + !self.0.apply(storage, edge) + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn const_value(&self) -> Option { + self.0.const_value().map(|v| !v) + } + + fn filters_exploded(&self) -> bool { + self.0.filters_exploded() + } + + fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + !self.0.apply_exploded(storage, edge) + } +} 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 4ff625960a..2774ae00a7 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -3,18 +3,13 @@ use crate::{ api::view::internal::GraphView, graph::views::filter::model::{ edge_expr::{ops::EdgeEndpointNodeOp, EdgeOp}, - is_active_edge_filter::IsActiveEdge, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, latest_filter::Latest, layered_filter::Layered, node_expr::{CreateOp, EntityExpr, PredicateLhs}, node_filter::NodeFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - CombinedFilter, ComposableFilter, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, - Wrap, + ComposableFilter, EntityMarker, InternalViewWrapOps, Wrap, }, }, errors::GraphError, @@ -45,14 +40,6 @@ impl EdgeFilter { } } -impl Wrap for EdgeFilter { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - impl InternalViewWrapOps for EdgeFilter { type Window = Windowed; @@ -61,26 +48,6 @@ impl InternalViewWrapOps for EdgeFilter { } } -impl EdgeViewFilterOps for EdgeFilter { - type Output = T; - - fn is_active(&self) -> Self::Output { - IsActiveEdge - } - - fn is_valid(&self) -> Self::Output { - IsValidEdge - } - - fn is_deleted(&self) -> Self::Output { - IsDeletedEdge - } - - fn is_self_loop(&self) -> Self::Output { - IsSelfLoopEdge - } -} - #[derive(Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Endpoint { 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 816e6e473d..826c55b32b 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 @@ -1,7 +1,5 @@ use crate::db::graph::views::filter::model::{ - is_active_edge_filter::IsActiveEdge, is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, is_valid_filter::IsValidEdge, windowed_filter::Windowed, - CombinedFilter, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, Wrap, + windowed_filter::Windowed, EntityMarker, InternalViewWrapOps, }; use raphtory_api::core::storage::timeindex::EventTime; @@ -14,14 +12,6 @@ impl From for EntityMarker { } } -impl Wrap for ExplodedEdgeFilter { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - impl InternalViewWrapOps for ExplodedEdgeFilter { type Window = Windowed; @@ -29,23 +19,3 @@ impl InternalViewWrapOps for ExplodedEdgeFilter { Windowed::from_times(start, end, self) } } - -impl EdgeViewFilterOps for ExplodedEdgeFilter { - type Output = T; - - fn is_active(&self) -> Self::Output { - IsActiveEdge - } - - fn is_valid(&self) -> Self::Output { - IsValidEdge - } - - fn is_deleted(&self) -> Self::Output { - IsDeletedEdge - } - - fn is_self_loop(&self) -> Self::Output { - IsSelfLoopEdge - } -} diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index 02de61e78d..69b503e3d6 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -14,7 +14,10 @@ use super::{ use crate::{ db::{ api::{ - state::NodeOp, + state::{ + ops::{filter::NodeExistsOp, NodeFilterOp}, + NodeOp, + }, view::internal::{DynGraphArc, GraphView, NodeList}, }, graph::views::filter::{ @@ -24,7 +27,10 @@ use crate::{ and_filter::AndFilter, comparable_set_values, dyn_factory::{DynEdgeFilterFactory, DynNodeFilterFactory}, - edge_expr::EdgeOp, + edge_expr::{ + ops::{AndEdgeOp, EdgeExistsOp}, + EdgeOp, + }, edge_filter::{EdgeEndpointWrapper, EdgeFilter, Endpoint}, exploded_edge_filter::ExplodedEdgeFilter, filter_operator::{BinaryOp, Comparable, StringComparable, StringOp, UnaryOp}, @@ -49,7 +55,7 @@ use crate::{ EntityMarker, ViewWrapOps, }, node_filtered_graph::NodeFilteredGraph, - CreateFilter, + CreateFilter, DynEdgeFilter, }, }, errors::GraphError, @@ -1256,57 +1262,68 @@ impl Predicate { } impl CreateFilter for Predicate { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type FilteredGraph<'graph, G> = DynGraphArc<'graph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; + type NodeFilter<'graph, G> + = Arc + 'graph> + where + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G + type EdgeFilter<'graph, G> + = DynEdgeFilter<'graph> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { + ) -> Result, GraphError> { Ok(match self.entity { EntityMarker::Node => { - Arc::new(NodeFilteredGraph::new(graph, self.node_filter(filtered)?)) + let filter = self.node_filter(graph.clone())?; + Arc::new(NodeFilteredGraph::new(graph, filter)) + } + EntityMarker::Edge => { + let filter = self.edge_filter(graph.clone())?; + Arc::new(EdgeExprFilteredGraph::new(graph, filter)) + } + EntityMarker::ExplodedEdge => { + let filter = self.edge_filter(graph.clone())?; + Arc::new(ExplodedEdgeExprFilteredGraph::new(graph, filter)) } - EntityMarker::Edge => Arc::new(EdgeExprFilteredGraph::new( - graph, - self.edge_filter(filtered)?, - )), - EntityMarker::ExplodedEdge => Arc::new(ExplodedEdgeExprFilteredGraph::new( - graph, - self.edge_filter(filtered)?, - )), EntityMarker::Const => return Err(invalid("a constant is not a filter")), }) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { + graph: G, + ) -> Result, GraphError> { if !matches!(self.entity, EntityMarker::Node) { return Err(GraphError::NotNodeFilter); } - self.node_filter(filtered) + self.node_filter(graph) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - Ok(graph) + ) -> Result, GraphError> { + match self.entity { + EntityMarker::Edge => self.edge_filter(graph), + // A node or exploded-edge predicate still says which edges survive: the + // ones the filtered graph keeps. + EntityMarker::Node | EntityMarker::ExplodedEdge => Ok(Arc::new(EdgeExistsOp::new( + self.create_graph_filter(graph)?, + ))), + EntityMarker::Const => Err(invalid("a constant is not a filter")), + } } } @@ -1439,7 +1456,8 @@ fn compile_view(views: &[ViewOp]) -> DynView { /// A filter applied inside a view: the graph is seen through `views` first and /// `inner` runs on that graph, reads included, so `and: [view, pred]` is -/// `graph.view(..).filter(pred)`. +/// `graph.view(..).filter(pred)`. As a per-node or per-edge predicate it also asks +/// that the entity exist in the view, the way the filtered graph would. #[derive(Clone)] struct Viewed { views: Vec, @@ -1451,52 +1469,56 @@ impl Viewed { &self, graph: G, ) -> Result, GraphError> { - compile_view(&self.views).dyn_filter_graph_view(Arc::new(graph)) + compile_view(&self.views).create_dyn_graph_filter(Arc::new(graph)) } } impl CreateFilter for Viewed { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type FilteredGraph<'graph, G> = DynGraphArc<'graph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type NodeFilter<'graph, G> = Arc + 'graph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = DynGraphArc<'graph> + type EdgeFilter<'graph, G> + = DynEdgeFilter<'graph> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { + ) -> Result, GraphError> { let viewed = self.view(graph)?; - self.inner.create_dyn_filter(viewed, Arc::new(filtered)) + self.inner.create_dyn_graph_filter(viewed) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { + ) -> Result, GraphError> { let viewed = self.view(graph)?; - self.inner - .create_dyn_node_filter(viewed, Arc::new(filtered)) + let inside = self.inner.create_dyn_node_filter(viewed.clone())?; + Ok(Arc::new(NodeExistsOp::new(viewed).and(inside))) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { + ) -> Result, GraphError> { let viewed = self.view(graph)?; - self.inner.dyn_filter_graph_view(viewed) + let inside = self.inner.create_dyn_edge_filter(viewed.clone())?; + Ok(Arc::new(AndEdgeOp { + left: EdgeExistsOp::new(viewed), + right: inside, + })) } } @@ -1516,40 +1538,42 @@ fn combine( /// A tree is a filter in its own right: applying it compiles it first. impl CreateFilter for FilterExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type FilteredGraph<'graph, G> = DynGraphArc<'graph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; + type NodeFilter<'graph, G> + = Arc + 'graph> + where + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = DynGraphArc<'graph> + type EdgeFilter<'graph, G> + = DynEdgeFilter<'graph> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - self.compile()?.create_filter(graph, filtered) + ) -> Result, GraphError> { + self.compile()?.create_graph_filter(graph) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - self.compile()?.create_node_filter(graph, filtered) + ) -> Result, GraphError> { + self.compile()?.create_node_filter(graph) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - self.compile()?.filter_graph_view(graph) + ) -> Result, GraphError> { + self.compile()?.create_edge_filter(graph) } } diff --git a/raphtory/src/db/graph/views/filter/model/expr/convert.rs b/raphtory/src/db/graph/views/filter/model/expr/convert.rs index d287a265c3..b481968a5d 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/convert.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/convert.rs @@ -35,14 +35,14 @@ use crate::{ node_expr::{ AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, ConstExpr, DegreeExpr, EntityExpr, FirstExpr, LastExpr, LenExpr, Marker, MaxExpr, MinExpr, PropValueSetExpr, - StringExpr, SumExpr, TemporalPropExpr, UnaryExpr, + Scoped, StringExpr, SumExpr, TemporalPropExpr, UnaryExpr, }, node_filter::NodeFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, MetadataExpr, PropertyExpr, }, - CreateFilter, + CreateFilter, DynEdgeFilter, }, }, errors::GraphError, @@ -52,7 +52,7 @@ use raphtory_api::core::entities::{ properties::prop::{IntoProp, Prop}, GID, }; -use std::sync::Arc; +use std::{fmt, fmt::Display, sync::Arc}; // ── factories ──────────────────────────────────────────────────────────────── @@ -64,6 +64,11 @@ pub trait FactoryLeaf: Clone { fn views(&self) -> Vec; } +/// The leaf types whose reads describe an edge: the plain edge and the exploded edge. +pub trait EdgeLeafKind: Leaf {} +impl EdgeLeafKind for EdgeLeaf {} +impl EdgeLeafKind for ExplodedEdgeLeaf {} + impl FactoryLeaf for NodeFilter { type Leaf = NodeLeaf; @@ -525,6 +530,41 @@ where } } +/// A unit predicate scoped to a factory's view chain: the read carries the views. +impl> ToFilterExpr for Scoped { + fn to_filter_expr(&self) -> FilterExpr { + let mut expr = self.inner.to_expr(); + for op in self.view.views() { + expr.push_view(op); + } + V::Leaf::filter(expr) + } +} + +impl> Display for Scoped { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.to_filter_expr().fmt(f) + } +} + +impl ToFilterExpr for IsActiveNode { + fn to_filter_expr(&self) -> FilterExpr { + NodeLeaf::filter(self.to_expr()) + } +} + +macro_rules! edge_unit_to_filter_expr { + ($($t:ident),* $(,)?) => {$( + impl ToFilterExpr for $t { + fn to_filter_expr(&self) -> FilterExpr { + EdgeLeaf::filter(ToExpr::::to_expr(self)) + } + } + )*}; +} + +edge_unit_to_filter_expr!(IsActiveEdge, IsValidEdge, IsDeletedEdge, IsSelfLoopEdge); + /// A typed predicate is applied by converting it to its tree and compiling /// that: one compiler, one set of checks, whatever built the filter. macro_rules! compile_through_tree { @@ -533,43 +573,43 @@ macro_rules! compile_through_tree { where Self: ToFilterExpr + Clone + Send + Sync + 'static, { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = DynGraphArc<'graph> - where - Self: 'graph; + type FilteredGraph<'graph, G> + = DynGraphArc<'graph> + where + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; + type NodeFilter<'graph, G> + = Arc + 'graph> + where + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = DynGraphArc<'graph> - where - Self: 'graph, - G: GraphView + 'graph; + type EdgeFilter<'graph, G> + = DynEdgeFilter<'graph> + where + Self: 'graph, + G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - self.to_filter_expr().compile()?.create_filter(graph, filtered) + ) -> Result, GraphError> { + self.to_filter_expr().compile()?.create_graph_filter(graph) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - self.to_filter_expr() - .compile()? - .create_node_filter(graph, filtered) + ) -> Result, GraphError> { + self.to_filter_expr().compile()?.create_node_filter(graph) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - self.to_filter_expr().compile()?.filter_graph_view(graph) + ) -> Result, GraphError> { + self.to_filter_expr().compile()?.create_edge_filter(graph) } } )+}; @@ -583,4 +623,10 @@ compile_through_tree! { impl for AnyExpr; impl for AllExpr; impl for EdgeEndpointWrapper; + impl for Scoped; + impl<> for IsActiveNode; + impl<> for IsActiveEdge; + impl<> for IsValidEdge; + impl<> for IsDeletedEdge; + impl<> for IsSelfLoopEdge; } 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 cdb9d10b95..f55835d430 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -3,11 +3,12 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::filter::{ model::{ + edge_expr::ops::EdgeExistsOp, latest_filter::Latest, layered_filter::Layered, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - CombinedFilter, InternalViewWrapOps, Wrap, + CombinedFilter, InternalViewWrapOps, }, CreateFilter, }, @@ -25,14 +26,6 @@ impl std::fmt::Display for GraphFilter { } } -impl Wrap for GraphFilter { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - impl InternalViewWrapOps for GraphFilter { type Window = Windowed; @@ -42,37 +35,43 @@ impl InternalViewWrapOps for GraphFilter { } impl CreateFilter for GraphFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = F; - - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NodeExistsOp; - type FilteredGraph<'graph, G> = G where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + type NodeFilter<'graph, G> + = NodeExistsOp + where + Self: 'graph, + G: GraphView + 'graph; + + type EdgeFilter<'graph, G> + = EdgeExistsOp + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(filtered) + graph: G, + ) -> Result, GraphError> { + Ok(graph) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(NodeExistsOp::new(filtered)) + graph: G, + ) -> Result, GraphError> { + Ok(NodeExistsOp::new(graph)) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - Ok(graph) + ) -> Result, GraphError> { + Ok(EdgeExistsOp::new(graph)) } } 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 47b33a2023..a6a0983389 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 @@ -1,15 +1,4 @@ -use crate::{ - db::{ - api::state::ops::{filter::NodeExistsOp, GraphView}, - graph::views::{ - filter::{ - edge_filtered_graph::EdgeFilteredGraph, model::ComposableFilter, CreateFilter, - }, - is_active_graph::IsActiveGraph, - }, - }, - errors::GraphError, -}; +use crate::db::{api::state::ops::GraphView, graph::views::filter::model::ComposableFilter}; use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -21,51 +10,6 @@ impl fmt::Display for IsActiveEdge { } } -impl CreateFilter for IsActiveEdge { - type EntityFiltered<'graph, G, F> - = EdgeFilteredGraph> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type NodeFilter<'graph, G, F> - = NodeExistsOp> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(EdgeFilteredGraph::new(graph, IsActiveGraph::new(filtered))) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(NodeExistsOp::new(IsActiveGraph::new(filtered))) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - impl ComposableFilter for IsActiveEdge {} // ── expr layer: the predicate as a boolean expression over the eval view ── 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 b23676417d..fd2ad0e4ae 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 @@ -1,11 +1,6 @@ -use crate::{ - db::{ - api::state::ops::{GraphView, HistoryOp, Map, NodeOp}, - graph::views::filter::{ - model::ComposableFilter, node_filtered_graph::NodeFilteredGraph, CreateFilter, - }, - }, - errors::GraphError, +use crate::db::{ + api::state::ops::{GraphView, HistoryOp, NodeOp}, + graph::views::filter::model::ComposableFilter, }; use std::fmt; @@ -18,53 +13,6 @@ impl fmt::Display for IsActiveNode { } } -impl CreateFilter for IsActiveNode { - type EntityFiltered<'graph, G, F> - = NodeFilteredGraph> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type NodeFilter<'graph, G, F> - = Map, bool> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - let op = self.create_node_filter(graph.clone(), filtered)?; - Ok(NodeFilteredGraph::new(graph, op)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - let op = HistoryOp::new(filtered).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 {} // ── expr layer: the predicate as a boolean expression over the eval view ── 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 4a6f4b71c9..d272a5d5ba 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 @@ -1,15 +1,4 @@ -use crate::{ - db::{ - api::state::ops::{filter::NodeExistsOp, GraphView}, - graph::views::{ - filter::{ - edge_filtered_graph::EdgeFilteredGraph, model::ComposableFilter, CreateFilter, - }, - is_deleted_graph::IsDeletedGraph, - }, - }, - errors::GraphError, -}; +use crate::db::{api::state::ops::GraphView, graph::views::filter::model::ComposableFilter}; use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -21,51 +10,6 @@ impl fmt::Display for IsDeletedEdge { } } -impl CreateFilter for IsDeletedEdge { - type EntityFiltered<'graph, G, F> - = EdgeFilteredGraph> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type NodeFilter<'graph, G, F> - = NodeExistsOp> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(EdgeFilteredGraph::new(graph, IsDeletedGraph::new(filtered))) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(NodeExistsOp::new(IsDeletedGraph::new(filtered))) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - impl ComposableFilter for IsDeletedEdge {} // ── expr layer: the predicate as a boolean expression over the eval view ── 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 4bc4c7469b..d0f9239a68 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 @@ -1,13 +1,4 @@ -use crate::{ - db::{ - api::state::ops::{filter::NodeExistsOp, GraphView}, - graph::views::{ - filter::{model::ComposableFilter, CreateFilter}, - is_self_loop_graph::IsSelfLoopGraph, - }, - }, - errors::GraphError, -}; +use crate::db::{api::state::ops::GraphView, graph::views::filter::model::ComposableFilter}; use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -19,52 +10,6 @@ impl fmt::Display for IsSelfLoopEdge { } } -impl CreateFilter for IsSelfLoopEdge { - type EntityFiltered<'graph, G, F> - = IsSelfLoopGraph - // self loop doesn't depend on view filtering, can simplify - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type NodeFilter<'graph, G, F> - = NodeExistsOp> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - _filtered: F, - ) -> Result, GraphError> { - Ok(IsSelfLoopGraph::new(graph)) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - _filtered: F, - ) -> Result, GraphError> { - Ok(NodeExistsOp::new(IsSelfLoopGraph::new(graph))) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - impl ComposableFilter for IsSelfLoopEdge {} // ── expr layer: the predicate as a boolean expression over the eval view ── 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 766bbbb6ac..29d8c3f926 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 @@ -1,15 +1,4 @@ -use crate::{ - db::{ - api::state::ops::{filter::NodeExistsOp, GraphView}, - graph::views::{ - filter::{ - edge_filtered_graph::EdgeFilteredGraph, model::ComposableFilter, CreateFilter, - }, - valid_graph::ValidGraph, - }, - }, - errors::GraphError, -}; +use crate::db::{api::state::ops::GraphView, graph::views::filter::model::ComposableFilter}; use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -21,51 +10,6 @@ impl fmt::Display for IsValidEdge { } } -impl CreateFilter for IsValidEdge { - type EntityFiltered<'graph, G, F> - = EdgeFilteredGraph> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type NodeFilter<'graph, G, F> - = NodeExistsOp> - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(EdgeFilteredGraph::new(graph, ValidGraph::new(filtered))) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - Ok(NodeExistsOp::new(ValidGraph::new(filtered))) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - impl ComposableFilter for IsValidEdge {} // ── expr layer: the predicate as a boolean expression over the eval view ── 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 32cb7f7e65..d4f801d98a 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -1,14 +1,11 @@ use crate::{ db::{ - api::view::internal::GraphView, + api::{state::ops::filter::NodeExistsOp, view::internal::GraphView}, graph::views::{ filter::{ model::{ - is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, windowed_filter::Windowed, CombinedFilter, - ComposableFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, - NodeViewFilterOps, Wrap, + edge_expr::ops::EdgeExistsOp, graph_filter::GraphFilterOps, + windowed_filter::Windowed, ComposableFilter, CreateView, InternalViewWrapOps, }, CreateFilter, }, @@ -47,94 +44,52 @@ impl InternalViewWrapOps for Latest { } } -impl CreateFilter for Latest { - type EntityFiltered<'graph, G, F> - = T::EntityFiltered<'graph, G, F> +/// A view wrapper applied as a filter: the inner filter's view is applied to the +/// graph and this view on top of it, in the order the chain was written. The nodes +/// and edges it selects are the ones that exist in the resulting view. +impl CreateFilter for Latest { + type FilteredGraph<'graph, G> + = WindowedGraph> where - G: GraphView + TimeOps<'graph> + 'graph, - F: GraphView + TimeOps<'graph> + 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G, F> - = T::NodeFilter<'graph, G, F> + type NodeFilter<'graph, G> + = NodeExistsOp>> where - G: GraphView + TimeOps<'graph> + 'graph, - F: GraphView + TimeOps<'graph> + 'graph; + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = WindowedGraph> + type EdgeFilter<'graph, G> + = EdgeExistsOp>> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G, F>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> - where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_filter(graph, filtered) + ) -> Result, GraphError> { + Ok(self.inner.create_graph_filter(graph)?.latest()) } - fn create_node_filter<'graph, G, F>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> - where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_node_filter(graph, filtered) + ) -> Result, GraphError> { + Ok(NodeExistsOp::new(self.create_graph_filter(graph)?)) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - Ok(self.inner.filter_graph_view(graph)?.latest()) + ) -> Result, GraphError> { + Ok(EdgeExistsOp::new(self.create_graph_filter(graph)?)) } } impl ComposableFilter for Latest {} -impl Wrap for Latest { - type Wrapped = Latest; - fn wrap(&self, value: T) -> Self::Wrapped { - Latest::new(value) - } -} - -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()) - } -} - // ── expr-layer view construction ── impl CreateView for 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 dd13b93dd5..dd10c66446 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -1,13 +1,11 @@ use crate::{ db::{ - api::view::internal::GraphView, + api::{state::ops::filter::NodeExistsOp, view::internal::GraphView}, graph::views::{ filter::{ model::{ - is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, CombinedFilter, ComposableFilter, - EdgeViewFilterOps, InternalViewWrapOps, NodeViewFilterOps, Wrap, + edge_expr::ops::EdgeExistsOp, graph_filter::GraphFilterOps, ComposableFilter, + InternalViewWrapOps, }, CreateFilter, }, @@ -75,96 +73,54 @@ impl InternalViewWrapOps for Layered { } } -impl CreateFilter for Layered { - type EntityFiltered<'graph, G, F> - = T::EntityFiltered<'graph, G, F> +/// A view wrapper applied as a filter: the inner filter's view is applied to the +/// graph and this view on top of it, in the order the chain was written. The nodes +/// and edges it selects are the ones that exist in the resulting view. +impl CreateFilter for Layered { + type FilteredGraph<'graph, G> + = LayeredGraph> where - G: GraphView + 'graph, - F: GraphView + 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G, F> - = T::NodeFilter<'graph, G, F> + type NodeFilter<'graph, G> + = NodeExistsOp>> where - G: GraphView + 'graph, - F: GraphView + 'graph; + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = LayeredGraph> + type EdgeFilter<'graph, G> + = EdgeExistsOp>> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G, F>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> - where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_filter(graph, filtered) + ) -> Result, GraphError> { + Ok(self + .inner + .create_graph_filter(graph)? + .layers(self.layer.clone())?) } - fn create_node_filter<'graph, G, F>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> - where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_node_filter(graph, filtered) + ) -> Result, GraphError> { + Ok(NodeExistsOp::new(self.create_graph_filter(graph)?)) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - self.inner - .filter_graph_view(graph)? - .layers(self.layer.clone()) + ) -> Result, GraphError> { + Ok(EdgeExistsOp::new(self.create_graph_filter(graph)?)) } } impl ComposableFilter for Layered {} -impl Wrap for Layered { - type Wrapped = Layered; - - fn wrap(&self, value: T) -> Self::Wrapped { - Layered::new(self.layer.clone(), value) - } -} - -impl 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()) - } -} - // ── expr layer: the layer view scopes any inner expression (per-expression view) ── // Nesting order of chained views is pinned by the view-semantics tests. diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 99a7de1588..47db1de837 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -31,24 +31,28 @@ pub use crate::{ use crate::{ db::{ api::{ - state::{ - ops::{filter::NO_FILTER, Const}, - NodeOp, - }, + state::NodeOp, view::{internal::DynGraphArc, BoxableGraphView}, }, graph::views::{ - filter::model::{ - 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}, - snapshot_filter::{SnapshotAt, SnapshotLatest}, - windowed_filter::Windowed, + filter::{ + model::{ + expr::{ + convert::{EdgeLeafKind, FactoryLeaf}, + NodeLeaf, + }, + 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, Scoped}, + snapshot_filter::{SnapshotAt, SnapshotLatest}, + windowed_filter::Windowed, + }, + DynEdgeFilter, }, layer_graph::LayeredGraph, }, @@ -86,65 +90,12 @@ pub mod property_filter; pub mod snapshot_filter; pub mod windowed_filter; -#[derive(Debug, Copy, Clone)] -pub struct Unfiltered; - -impl CreateFilter for Unfiltered { - type EntityFiltered<'graph, G, F> - = G - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - type NodeFilter<'graph, G, F> - = Const - where - Self: 'graph, - G: GraphView + 'graph, - F: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G - where - Self: 'graph, - G: GraphView + 'graph; - - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - graph: G, - _filtered: F, - ) -> Result, GraphError> { - Ok(graph) - } - - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( - self, - _graph: G, - _filtered: F, - ) -> Result, GraphError> { - Ok(NO_FILTER) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } -} - pub trait Wrap { type Wrapped; fn wrap(&self, value: T) -> Self::Wrapped; } -impl Wrap for Arc { - type Wrapped = S::Wrapped; - fn wrap(&self, value: T) -> Self::Wrapped { - self.deref().wrap(value) - } -} - pub trait ComposableFilter: Sized { fn and(self, other: F) -> AndFilter { AndFilter { @@ -166,90 +117,86 @@ pub trait ComposableFilter: Sized { } pub trait DynCreateFilter: Send + Sync + 'static { - fn create_dyn_filter<'graph>( + fn create_dyn_graph_filter<'graph>( &self, graph: DynGraphArc<'graph>, - filtered: DynGraphArc<'graph>, ) -> Result, GraphError>; fn create_dyn_node_filter<'graph>( &self, graph: DynGraphArc<'graph>, - filtered: DynGraphArc<'graph>, ) -> Result + 'graph>, GraphError>; - fn dyn_filter_graph_view<'graph>( + fn create_dyn_edge_filter<'graph>( &self, graph: DynGraphArc<'graph>, - ) -> Result, GraphError>; + ) -> Result, GraphError>; } impl DynCreateFilter for T where T: CombinedFilter, { - fn create_dyn_filter<'graph>( + fn create_dyn_graph_filter<'graph>( &self, graph: DynGraphArc<'graph>, - filtered: DynGraphArc<'graph>, ) -> Result, GraphError> { - Ok(Arc::new(self.clone().create_filter(graph, filtered)?)) + Ok(Arc::new(self.clone().create_graph_filter(graph)?)) } fn create_dyn_node_filter<'graph>( &self, graph: DynGraphArc<'graph>, - filtered: DynGraphArc<'graph>, ) -> Result + 'graph>, GraphError> { - Ok(Arc::new(self.clone().create_node_filter(graph, filtered)?)) + Ok(Arc::new(self.clone().create_node_filter(graph)?)) } - fn dyn_filter_graph_view<'graph>( + fn create_dyn_edge_filter<'graph>( &self, graph: DynGraphArc<'graph>, - ) -> Result, GraphError> { - Ok(Arc::new(self.clone().filter_graph_view(graph)?)) + ) -> Result, GraphError> { + Ok(Arc::new(self.clone().create_edge_filter(graph)?)) } } impl CreateFilter for Arc { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type FilteredGraph<'graph, G> = DynGraphArc<'graph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = - Arc + 'graph>; + type NodeFilter<'graph, G> + = Arc + 'graph> + where + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = DynGraphArc<'graph> + type EdgeFilter<'graph, G> + = DynEdgeFilter<'graph> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - self.deref() - .create_dyn_filter(Arc::new(graph), Arc::new(filtered)) + ) -> Result, GraphError> { + self.deref().create_dyn_graph_filter(Arc::new(graph)) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - self.deref() - .create_dyn_node_filter(Arc::new(graph), Arc::new(filtered)) + ) -> Result, GraphError> { + self.deref().create_dyn_node_filter(Arc::new(graph)) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - self.deref().dyn_filter_graph_view(Arc::new(graph)) + ) -> Result, GraphError> { + self.deref().create_dyn_edge_filter(Arc::new(graph)) } } @@ -749,18 +696,43 @@ pub type DynFilter = Arc; impl ComposableFilter for DynFilter {} impl ComposableFilter for DynView {} -pub trait EdgeViewFilterOps: ViewWrapOps { - type Output: CombinedFilter; - - fn is_active(&self) -> Self::Output; +/// The unit predicates of an edge or exploded-edge factory, each scoped to the +/// factory's view chain. +pub trait EdgeViewFilterOps: FactoryLeaf + CreateView +where + Self::Leaf: EdgeLeafKind, +{ + fn is_active(&self) -> Scoped { + Scoped { + view: self.clone(), + inner: IsActiveEdge, + } + } - fn is_valid(&self) -> Self::Output; + fn is_valid(&self) -> Scoped { + Scoped { + view: self.clone(), + inner: IsValidEdge, + } + } - fn is_deleted(&self) -> Self::Output; + fn is_deleted(&self) -> Scoped { + Scoped { + view: self.clone(), + inner: IsDeletedEdge, + } + } - fn is_self_loop(&self) -> Self::Output; + fn is_self_loop(&self) -> Scoped { + Scoped { + view: self.clone(), + inner: IsSelfLoopEdge, + } + } } +impl EdgeViewFilterOps for T where T::Leaf: EdgeLeafKind {} + // ───────────────────────────────────────────────────────────────────────────── // EntityExprFilterOps — comparison and set operators on any EntityExpr // ───────────────────────────────────────────────────────────────────────────── @@ -1037,10 +1009,18 @@ pub fn comparable_set_values(lhs_pt: &PropType, values: Vec) -> Vec pub trait CombinedFilter: CreateFilter + Clone + Send + Sync + 'static {} -pub trait NodeViewFilterOps: ViewWrapOps { - type Output: CombinedFilter; - - fn is_active(&self) -> Self::Output; +/// The unit predicates of a node factory, each scoped to the factory's view chain: +/// `NodeFilter.window(1, 5).is_active()` asks whether the node is active inside that +/// window. +pub trait NodeViewFilterOps: FactoryLeaf + CreateView { + fn is_active(&self) -> Scoped { + Scoped { + view: self.clone(), + inner: IsActiveNode, + } + } } +impl + CreateView> NodeViewFilterOps for T {} + impl CombinedFilter for T {} 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 index 62e125bf22..cd2f874f60 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -16,7 +16,7 @@ pub mod ops; #[cfg(test)] mod tests; -use crate::db::graph::views::filter::model::{edge_expr::EdgeOp, EntityMarker}; +use crate::db::graph::views::filter::model::{edge_expr::EdgeOp, ComposableFilter, EntityMarker}; pub use dyn_expr::*; pub use exprs::*; pub use filters::*; @@ -130,6 +130,8 @@ impl EntityExpr for Scoped { } } +impl ComposableFilter for Scoped {} + impl CreateOp for Scoped { fn create_node_op<'g, G: GraphView + 'g>( &self, 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 4af1cbb7a8..bd5602aa21 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 @@ -6,6 +6,7 @@ use crate::{ view::{ filter_ops::Select, internal::{CoreGraphOps, NodeList}, + Filter, }, }, graph::views::filter::{ @@ -39,12 +40,10 @@ fn build_test_graph() -> Graph { fn filtered_names(filter: F, g: Graph) -> Vec where F: CreateFilter, - for<'graph> F::EntityFiltered<'graph, Graph, F::FilteredGraph<'graph, Graph>>: - GraphViewOps<'graph>, + for<'graph> F::FilteredGraph<'graph, Graph>: GraphViewOps<'graph>, { - let fg = filter.filter_graph_view(g.clone()).unwrap(); - let mut names: Vec = filter - .create_filter(g, fg) + let mut names: Vec = g + .filter(filter) .unwrap() .nodes() .iter() @@ -201,12 +200,10 @@ fn build_temporal_graph() -> Graph { fn temporal_filtered_names(filter: F, g: Graph) -> Vec where F: CreateFilter, - for<'graph> F::EntityFiltered<'graph, Graph, F::FilteredGraph<'graph, Graph>>: - GraphViewOps<'graph>, + for<'graph> F::FilteredGraph<'graph, Graph>: GraphViewOps<'graph>, { - let fg = filter.filter_graph_view(g.clone()).unwrap(); - let mut names: Vec = filter - .create_filter(g, fg) + let mut names: Vec = g + .filter(filter) .unwrap() .nodes() .iter() @@ -349,12 +346,10 @@ fn temporal_expr_ops_blanket_any() { fn windowed_filtered_names(filter: F, g: Graph) -> Vec where F: CreateFilter, - for<'graph> F::EntityFiltered<'graph, Graph, F::FilteredGraph<'graph, Graph>>: - GraphViewOps<'graph>, + for<'graph> F::FilteredGraph<'graph, Graph>: GraphViewOps<'graph>, { - let fg = filter.filter_graph_view(g.clone()).unwrap(); - let mut names: Vec = filter - .create_filter(g, fg) + let mut names: Vec = g + .filter(filter) .unwrap() .nodes() .iter() @@ -456,12 +451,10 @@ fn build_layered_temporal_graph() -> Graph { fn layered_filtered_names(filter: F, g: Graph) -> Vec where F: CreateFilter, - for<'graph> F::EntityFiltered<'graph, Graph, F::FilteredGraph<'graph, Graph>>: - GraphViewOps<'graph>, + for<'graph> F::FilteredGraph<'graph, Graph>: GraphViewOps<'graph>, { - let fg = filter.filter_graph_view(g.clone()).unwrap(); - let mut names: Vec = filter - .create_filter(g, fg) + let mut names: Vec = g + .filter(filter) .unwrap() .nodes() .iter() @@ -560,7 +553,7 @@ fn string_op_on_numeric_prop_returns_error() { let filter = NodeFilter .property("score") .starts_with(Prop::Str("x".into())); - let result = filter.create_filter(g.clone(), g); + let result = g.filter(filter); assert!( result.is_err(), "expected Err for string op on numeric property" @@ -590,11 +583,10 @@ fn ordering_op_on_bool_prop_orders_false_before_true() { /// The nodes a compiled node filter will actually visit. fn filter_domain(filter: F, g: &Graph) -> NodeList where - F: CreateFilter + Clone, - for<'g> F::NodeFilter<'g, Graph, F::FilteredGraph<'g, Graph>>: NodeOp, + F: CreateFilter, + for<'g> F::NodeFilter<'g, Graph>: NodeOp, { - let fg = filter.clone().filter_graph_view(g.clone()).unwrap(); - let op = filter.create_node_filter(g.clone(), fg).unwrap(); + let op = filter.create_node_filter(g.clone()).unwrap(); op.domain(&g.core_graph().lock()) } 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 d23521654a..47dddc75d1 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 @@ -6,14 +6,13 @@ use crate::{ NodeStateValue, TypedNodeState, }, graph::views::filter::model::{ - is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, node_expr::{exprs::DegreeExpr, EntityExpr}, node_state_filter::NodeStateBoolColOp, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - CombinedFilter, CreateView, EntityMarker, InternalViewWrapOps, NodeViewFilterOps, + CreateView, EntityMarker, InternalViewWrapOps, }, }, errors::GraphError, @@ -37,20 +36,12 @@ impl InternalViewWrapOps for NodeFilter { } } -impl NodeViewFilterOps for NodeFilter { - type Output = T; - - fn is_active(&self) -> Self::Output { - IsActiveNode - } -} - // ── expr-layer factory ── pub trait NodeFilterFactory: InternalViewWrapOps + CreateView + EntityExpr { - type NodeWindow: NodeFilterFactory + NodeViewFilterOps; + type NodeWindow: NodeFilterFactory; #[inline] fn id(&self) -> Id { Id @@ -116,22 +107,22 @@ impl NodeFilterFactory for NodeFilter { type NodeWindow = Self::Window; } -impl NodeFilterFactory for Windowed { +impl NodeFilterFactory for Windowed { type NodeWindow = T::NodeWindow; } -impl NodeFilterFactory for Latest { +impl NodeFilterFactory for Latest { type NodeWindow = Self::Window; } -impl NodeFilterFactory for SnapshotAt { +impl NodeFilterFactory for SnapshotAt { type NodeWindow = Self::Window; } -impl NodeFilterFactory for SnapshotLatest { +impl NodeFilterFactory for SnapshotLatest { type NodeWindow = Self::Window; } -impl NodeFilterFactory for Layered { +impl NodeFilterFactory for Layered { type NodeWindow = Self::Window; } 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 94a3d16c56..27055bd049 100644 --- a/raphtory/src/db/graph/views/filter/model/not_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/not_filter.rs @@ -5,7 +5,9 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - model::ComposableFilter, not_filtered_graph::NotFilteredGraph, CreateFilter, + model::{edge_expr::ops::NotEdgeOp, ComposableFilter}, + not_filtered_graph::NotFilteredGraph, + CreateFilter, }, }, errors::GraphError, @@ -24,51 +26,43 @@ impl Display for NotFilter { impl ComposableFilter for NotFilter {} impl CreateFilter for NotFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = NotFilteredGraph>> + type FilteredGraph<'graph, G> + = NotFilteredGraph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = NotOp>> + type NodeFilter<'graph, G> + = NotOp> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = G + type EdgeFilter<'graph, G> + = NotEdgeOp> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - let f = self.0.filter_graph_view(filtered.clone())?; - let filter = self.0.create_filter(filtered, f)?; + ) -> Result, GraphError> { + let filter = self.0.create_graph_filter(graph.clone())?; Ok(NotFilteredGraph { graph, filter }) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, - _graph: G, - filtered: F, - ) -> Result, GraphError> - where - Self: 'graph, - { - let f = self.0.filter_graph_view(filtered.clone())?; - Ok(self.0.create_node_filter(filtered, f)?.not()) + graph: G, + ) -> Result, GraphError> { + Ok(self.0.create_node_filter(graph)?.not()) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> - where - Self: 'graph, - { - Ok(graph) + ) -> Result, GraphError> { + Ok(NotEdgeOp(self.0.create_edge_filter(graph)?)) } } 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 167b0a52bd..3283bf76e4 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -5,7 +5,9 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - model::ComposableFilter, or_filtered_graph::OrFilteredGraph, CreateFilter, + model::{edge_expr::ops::OrEdgeOp, ComposableFilter}, + or_filtered_graph::OrFilteredGraph, + CreateFilter, }, }, errors::GraphError, @@ -27,59 +29,48 @@ impl Display for OrFilter { impl ComposableFilter for OrFilter {} impl CreateFilter for OrFilter { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = OrFilteredGraph< - G, - L::EntityFiltered<'graph, F, L::FilteredGraph<'graph, F>>, - R::EntityFiltered<'graph, F, R::FilteredGraph<'graph, F>>, - > + type FilteredGraph<'graph, G> + = OrFilteredGraph, R::FilteredGraph<'graph, G>> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> - = OrOp< - L::NodeFilter<'graph, F, L::FilteredGraph<'graph, F>>, - R::NodeFilter<'graph, F, R::FilteredGraph<'graph, F>>, - > + type NodeFilter<'graph, G> + = OrOp, R::NodeFilter<'graph, G>> where - Self: 'graph; - type FilteredGraph<'graph, G> - = G + Self: 'graph, + G: GraphView + 'graph; + + type EdgeFilter<'graph, G> + = OrEdgeOp, R::EdgeFilter<'graph, G>> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - let l = self.left.filter_graph_view(filtered.clone())?; - let r = self.right.filter_graph_view(filtered.clone())?; - let left = self.left.create_filter(filtered.clone(), l)?; - let right = self.right.create_filter(filtered, r)?; + ) -> Result, GraphError> { + let left = self.left.create_graph_filter(graph.clone())?; + let right = self.right.create_graph_filter(graph.clone())?; Ok(OrFilteredGraph { graph, left, right }) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, - _graph: G, - filtered: F, - ) -> Result, GraphError> { - let l = self.left.filter_graph_view(filtered.clone())?; - let r = self.right.filter_graph_view(filtered.clone())?; - let left = self.left.create_node_filter(filtered.clone(), l)?; - let right = self.right.create_node_filter(filtered.clone(), r)?; + graph: G, + ) -> Result, GraphError> { + 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, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> - where - Self: 'graph, - { - Ok(graph) + ) -> Result, GraphError> { + let left = self.left.create_edge_filter(graph.clone())?; + let right = self.right.create_edge_filter(graph)?; + Ok(OrEdgeOp { left, right }) } } 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 f7f80a4e23..78f9177cf8 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -1,14 +1,14 @@ use crate::{ db::{ - api::view::{internal::GraphView, time::TimeOps}, + api::{ + state::ops::filter::NodeExistsOp, + view::{internal::GraphView, time::TimeOps}, + }, graph::views::{ filter::{ model::{ - is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, windowed_filter::Windowed, CombinedFilter, - ComposableFilter, CreateView, EdgeViewFilterOps, InternalViewWrapOps, - NodeViewFilterOps, Wrap, + edge_expr::ops::EdgeExistsOp, graph_filter::GraphFilterOps, + windowed_filter::Windowed, ComposableFilter, CreateView, InternalViewWrapOps, }, CreateFilter, }, @@ -50,97 +50,55 @@ impl InternalViewWrapOps for SnapshotAt { } } -impl CreateFilter for SnapshotAt { - type EntityFiltered<'graph, G, F> - = T::EntityFiltered<'graph, G, F> +/// A view wrapper applied as a filter: the inner filter's view is applied to the +/// graph and this view on top of it, in the order the chain was written. The nodes +/// and edges it selects are the ones that exist in the resulting view. +impl CreateFilter for SnapshotAt { + type FilteredGraph<'graph, G> + = WindowedGraph> where - G: GraphView + 'graph, - F: GraphView + 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G, F> - = T::NodeFilter<'graph, G, F> + type NodeFilter<'graph, G> + = NodeExistsOp>> where - G: GraphView + 'graph, - F: GraphView + 'graph; + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = WindowedGraph> + type EdgeFilter<'graph, G> + = EdgeExistsOp>> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G, F>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> - where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_filter(graph, filtered) + ) -> Result, GraphError> { + Ok(self + .inner + .create_graph_filter(graph)? + .snapshot_at(self.time)) } - fn create_node_filter<'graph, G, F>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> - where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_node_filter(graph, filtered) + ) -> Result, GraphError> { + Ok(NodeExistsOp::new(self.create_graph_filter(graph)?)) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - Ok(self.inner.filter_graph_view(graph)?.snapshot_at(self.time)) + ) -> Result, GraphError> { + Ok(EdgeExistsOp::new(self.create_graph_filter(graph)?)) } } impl ComposableFilter for SnapshotAt {} -impl Wrap for SnapshotAt { - type Wrapped = SnapshotAt; - fn wrap(&self, value: T) -> Self::Wrapped { - SnapshotAt { - time: self.time, - inner: value, - } - } -} - -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, @@ -167,93 +125,52 @@ impl InternalViewWrapOps for SnapshotLatest { } } -impl CreateFilter for SnapshotLatest { - type EntityFiltered<'graph, G, F> - = T::EntityFiltered<'graph, G, F> +/// A view wrapper applied as a filter: the inner filter's view is applied to the +/// graph and this view on top of it, in the order the chain was written. The nodes +/// and edges it selects are the ones that exist in the resulting view. +impl CreateFilter for SnapshotLatest { + type FilteredGraph<'graph, G> + = WindowedGraph> where - G: GraphView + 'graph, - F: GraphView + 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G, F> - = T::NodeFilter<'graph, G, F> + type NodeFilter<'graph, G> + = NodeExistsOp>> where - G: GraphView + 'graph, - F: GraphView + 'graph; - type FilteredGraph<'graph, G> - = WindowedGraph> + Self: 'graph, + G: GraphView + 'graph; + + type EdgeFilter<'graph, G> + = EdgeExistsOp>> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G, F>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> - where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_filter(graph, filtered) + ) -> Result, GraphError> { + Ok(self.inner.create_graph_filter(graph)?.snapshot_latest()) } - fn create_node_filter<'graph, G, F>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> - where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_node_filter(graph, filtered) + ) -> Result, GraphError> { + Ok(NodeExistsOp::new(self.create_graph_filter(graph)?)) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - Ok(self.inner.filter_graph_view(graph)?.snapshot_latest()) + ) -> Result, GraphError> { + Ok(EdgeExistsOp::new(self.create_graph_filter(graph)?)) } } impl ComposableFilter for SnapshotLatest {} -impl Wrap for SnapshotLatest { - type Wrapped = SnapshotLatest; - fn wrap(&self, value: T) -> Self::Wrapped { - SnapshotLatest::new(value) - } -} - -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()) - } -} - // ── expr-layer view construction ── impl CreateView for SnapshotAt { 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 63ea6a73a2..5bf0369450 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -1,13 +1,11 @@ use crate::{ db::{ - api::view::internal::GraphView, + api::{state::ops::filter::NodeExistsOp, view::internal::GraphView}, graph::views::{ filter::{ model::{ - is_active_edge_filter::IsActiveEdge, is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, CombinedFilter, ComposableFilter, CreateView, - EdgeViewFilterOps, InternalViewWrapOps, NodeViewFilterOps, Wrap, + edge_expr::ops::EdgeExistsOp, graph_filter::GraphFilterOps, ComposableFilter, + CreateView, InternalViewWrapOps, }, CreateFilter, }, @@ -72,97 +70,54 @@ impl InternalViewWrapOps for Windowed { } } -impl CreateFilter for Windowed { - type EntityFiltered<'graph, G, F> - = T::EntityFiltered<'graph, G, F> - where - G: GraphView + 'graph, - F: GraphView + 'graph; - - type NodeFilter<'graph, G, F> - = T::NodeFilter<'graph, G, F> - where - G: GraphView + 'graph, - F: GraphView + 'graph; - +/// A view wrapper applied as a filter: the inner filter's view is applied to the +/// graph and this view on top of it, in the order the chain was written. The nodes +/// and edges it selects are the ones that exist in the resulting view. +impl CreateFilter for Windowed { type FilteredGraph<'graph, G> = WindowedGraph> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G, F>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> + type NodeFilter<'graph, G> + = NodeExistsOp>> where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_filter(graph, filtered) - } + Self: 'graph, + G: GraphView + 'graph; - fn create_node_filter<'graph, G, F>( - self, - graph: G, - filtered: F, - ) -> Result, GraphError> + type EdgeFilter<'graph, G> + = EdgeExistsOp>> where - G: GraphView + 'graph, - F: GraphView + 'graph, - { - self.inner.create_node_filter(graph, filtered) - } + Self: 'graph, + G: GraphView + 'graph; - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_graph_filter<'graph, G: GraphView + 'graph>( + self, graph: G, ) -> Result, GraphError> { Ok(self .inner - .filter_graph_view(graph)? + .create_graph_filter(graph)? .window(self.start, self.end)) } -} - -impl ComposableFilter for Windowed {} - -impl Wrap for Windowed { - type Wrapped = Windowed; - fn wrap(&self, value: T) -> Self::Wrapped { - Windowed::new(self.start, self.end, value) + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + Ok(NodeExistsOp::new(self.create_graph_filter(graph)?)) } -} -impl NodeViewFilterOps for Windowed { - type Output = Windowed>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + Ok(EdgeExistsOp::new(self.create_graph_filter(graph)?)) } } -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()) - } -} +impl ComposableFilter for Windowed {} // ── expr-layer view construction ── diff --git a/raphtory/src/db/graph/views/is_active_graph.rs b/raphtory/src/db/graph/views/is_active_graph.rs deleted file mode 100644 index 78d9206887..0000000000 --- a/raphtory/src/db/graph/views/is_active_graph.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::{ - db::{ - api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - view::internal::{ - EdgeTimeSemanticsOps, Immutable, InheritEdgeFilterOps, InheritEdgeHistoryFilter, - InheritExplodedEdgeFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, - InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, - InheritTimeSemantics, InternalEdgeLayerFilterOps, Static, - }, - }, - graph::views::layer_graph::LayeredGraph, - }, - prelude::GraphViewOps, -}; -use raphtory_api::{ - core::entities::{LayerId, LayerIds}, - inherit::Base, -}; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeEntryRef}; - -#[derive(Copy, Clone, Debug)] -pub struct IsActiveGraph { - graph: G, -} - -impl<'graph, G: GraphViewOps<'graph>> Base for IsActiveGraph { - type Base = G; - - fn base(&self) -> &Self::Base { - &self.graph - } -} - -impl<'graph, G: GraphViewOps<'graph>> IsActiveGraph { - pub fn new(graph: G) -> Self { - Self { graph } - } -} - -impl Static for IsActiveGraph {} -impl Immutable for IsActiveGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritNodeHistoryFilter for IsActiveGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeHistoryFilter for IsActiveGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritStorageOps for IsActiveGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritCoreGraphOps for IsActiveGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritLayerOps for IsActiveGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritListOps for IsActiveGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritMaterialize for IsActiveGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritPropertiesOps for IsActiveGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritNodePropertySchemaOps for IsActiveGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgePropertySchemaOps for IsActiveGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritNodeFilterOps for IsActiveGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritTimeSemantics for IsActiveGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeFilterOps for IsActiveGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritExplodedEdgeFilterOps for IsActiveGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InternalEdgeLayerFilterOps for IsActiveGraph { - fn internal_edge_layer_filtered(&self) -> bool { - true - } - - fn internal_layer_filter_edge_list_trusted(&self) -> bool { - false - } - - fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: LayerId) -> bool { - let time_semantics = self.graph.edge_time_semantics(); - time_semantics.edge_is_active(edge, LayeredGraph::new(&self.graph, LayerIds::One(layer))) - && self.graph.internal_filter_edge_layer(edge, layer) - } -} diff --git a/raphtory/src/db/graph/views/is_deleted_graph.rs b/raphtory/src/db/graph/views/is_deleted_graph.rs deleted file mode 100644 index 852e0ddd03..0000000000 --- a/raphtory/src/db/graph/views/is_deleted_graph.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::{ - db::api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - view::internal::{ - EdgeTimeSemanticsOps, Immutable, InheritEdgeHistoryFilter, InheritEdgeLayerFilterOps, - InheritExplodedEdgeFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, - InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, - InheritTimeSemantics, InternalEdgeFilterOps, Static, - }, - }, - prelude::GraphViewOps, -}; -use raphtory_api::{core::entities::LayerIds, inherit::Base}; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeEntryRef}; - -#[derive(Copy, Clone, Debug)] -pub struct IsDeletedGraph { - graph: G, -} - -impl<'graph, G: GraphViewOps<'graph>> Base for IsDeletedGraph { - type Base = G; - - fn base(&self) -> &Self::Base { - &self.graph - } -} - -impl<'graph, G: GraphViewOps<'graph>> IsDeletedGraph { - pub fn new(graph: G) -> Self { - Self { graph } - } -} - -impl Static for IsDeletedGraph {} -impl Immutable for IsDeletedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritNodeHistoryFilter for IsDeletedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeHistoryFilter for IsDeletedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritStorageOps for IsDeletedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritCoreGraphOps for IsDeletedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritLayerOps for IsDeletedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritListOps for IsDeletedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritMaterialize for IsDeletedGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritPropertiesOps for IsDeletedGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritNodePropertySchemaOps for IsDeletedGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgePropertySchemaOps for IsDeletedGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritNodeFilterOps for IsDeletedGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritTimeSemantics for IsDeletedGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritExplodedEdgeFilterOps for IsDeletedGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeLayerFilterOps for IsDeletedGraph {} - -/// An edge is deleted only when *no* layer of the current view still holds it -/// alive, which is what `EdgeView::is_deleted` reports. -/// -/// This has to be the whole-edge filter rather than the per-layer one: an edge -/// passes a layer filter when *any* of its layers passes, so testing layers -/// individually would answer "some layer has a deletion" instead. The two -/// readings diverge as soon as an edge's layers disagree — a deletion recorded -/// on a layer the edge was never added to (which `delete_edge` does by default, -/// tombstoning `_default`) would then report an edge as deleted while it is -/// still alive on another layer, and while `is_deleted()` says it is not. -/// -/// For the same reason the deletion test ignores the layer ids it is handed: -/// they scope the caller's question, and answering within a narrower scope -/// would give that same partial reading. Only the delegated filter below can -/// use them. -impl<'graph, G: GraphViewOps<'graph>> InternalEdgeFilterOps for IsDeletedGraph { - fn internal_edge_filtered(&self) -> bool { - true - } - - fn internal_edge_list_trusted(&self) -> bool { - false - } - - fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { - let time_semantics = self.graph.edge_time_semantics(); - time_semantics.edge_is_deleted(edge, &self.graph) - && self.graph.internal_filter_edge(edge, layer_ids) - } -} diff --git a/raphtory/src/db/graph/views/is_self_loop_graph.rs b/raphtory/src/db/graph/views/is_self_loop_graph.rs deleted file mode 100644 index 08ab431ddc..0000000000 --- a/raphtory/src/db/graph/views/is_self_loop_graph.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{ - db::api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - view::internal::{ - Immutable, InheritEdgeHistoryFilter, InheritEdgeLayerFilterOps, - InheritExplodedEdgeFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, - InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, - InheritTimeSemantics, InternalEdgeFilterOps, Static, - }, - }, - prelude::GraphViewOps, -}; -use raphtory_api::{core::entities::LayerIds, inherit::Base}; -use raphtory_storage::{ - core_ops::InheritCoreGraphOps, - graph::edges::{edge_ref::EdgeEntryRef, edge_storage_ops::EdgeStorageOps}, -}; - -#[derive(Copy, Clone, Debug)] -pub struct IsSelfLoopGraph { - graph: G, -} - -impl<'graph, G: GraphViewOps<'graph>> Base for IsSelfLoopGraph { - type Base = G; - - fn base(&self) -> &Self::Base { - &self.graph - } -} - -impl<'graph, G: GraphViewOps<'graph>> IsSelfLoopGraph { - pub fn new(graph: G) -> Self { - Self { graph } - } -} - -impl Static for IsSelfLoopGraph {} -impl Immutable for IsSelfLoopGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritNodeHistoryFilter for IsSelfLoopGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeHistoryFilter for IsSelfLoopGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritStorageOps for IsSelfLoopGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritCoreGraphOps for IsSelfLoopGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritLayerOps for IsSelfLoopGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritListOps for IsSelfLoopGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritMaterialize for IsSelfLoopGraph {} -impl<'graph, G: GraphViewOps<'graph>> InheritPropertiesOps for IsSelfLoopGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritNodePropertySchemaOps for IsSelfLoopGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgePropertySchemaOps for IsSelfLoopGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritNodeFilterOps for IsSelfLoopGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritTimeSemantics for IsSelfLoopGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InternalEdgeFilterOps for IsSelfLoopGraph { - fn internal_edge_filtered(&self) -> bool { - true - } - - fn internal_edge_list_trusted(&self) -> bool { - false - } - - fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { - edge.src() == edge.dst() && self.graph.internal_filter_edge(edge, layer_ids) - } -} - -impl<'graph, G: GraphViewOps<'graph>> InheritExplodedEdgeFilterOps for IsSelfLoopGraph {} - -impl<'graph, G: GraphViewOps<'graph>> InheritEdgeLayerFilterOps for IsSelfLoopGraph {} diff --git a/raphtory/src/db/graph/views/mod.rs b/raphtory/src/db/graph/views/mod.rs index 332782115d..e83dfa6bbf 100644 --- a/raphtory/src/db/graph/views/mod.rs +++ b/raphtory/src/db/graph/views/mod.rs @@ -1,9 +1,6 @@ pub mod cached_view; pub mod deletion_graph; pub mod filter; -pub mod is_active_graph; -pub mod is_deleted_graph; -pub mod is_self_loop_graph; pub mod layer_graph; pub mod node_subgraph; pub mod property_redacted_graph; diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index e94c7a2cbe..e454bd2a6b 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -4,7 +4,7 @@ use crate::{ state::NodeOp, view::internal::{DynGraphArc, GraphView}, }, - graph::views::filter::{model::expr::FilterExpr, CreateFilter}, + graph::views::filter::{model::expr::FilterExpr, CreateFilter, DynEdgeFilter}, }, errors::GraphError, python::filter::node_expr::PyExpr, @@ -114,42 +114,42 @@ pub(crate) fn no_view(filter: &FilterExpr) -> PyResult<()> { } impl CreateFilter for PyFilterExpr { - type EntityFiltered<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type FilteredGraph<'graph, G> = DynGraphArc<'graph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> + type NodeFilter<'graph, G> = Arc + 'graph> where - Self: 'graph; + Self: 'graph, + G: GraphView + 'graph; - type FilteredGraph<'graph, G> - = DynGraphArc<'graph> + type EdgeFilter<'graph, G> + = DynEdgeFilter<'graph> where Self: 'graph, G: GraphView + 'graph; - fn create_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_graph_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - self.0.create_filter(graph, filtered) + ) -> Result, GraphError> { + self.0.create_graph_filter(graph) } - fn create_node_filter<'graph, G: GraphView + 'graph, F: GraphView + 'graph>( + fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, - filtered: F, - ) -> Result, GraphError> { - self.0.create_node_filter(graph, filtered) + ) -> Result, GraphError> { + self.0.create_node_filter(graph) } - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, graph: G, - ) -> Result, GraphError> { - self.0.filter_graph_view(graph) + ) -> Result, GraphError> { + self.0.create_edge_filter(graph) } } From bdb798fb59f6bddaa946def2003b80ed404f6fff Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:41:20 +0100 Subject: [PATCH 131/155] erase the filter factories through a marker instead of a blanket on Arc The typed factories carry Static and get their erased trait from the blanket impl; the trait objects forward each call through the vtable. Both factory families name the type a window over them is, so the erased window can wrap the typed factory directly and the copied clamp goes. --- .../graph/views/filter/model/dyn_factory.rs | 120 ++++++++++++++---- .../graph/views/filter/model/edge_filter.rs | 32 +++-- .../filter/model/exploded_edge_filter.rs | 7 +- .../graph/views/filter/model/latest_filter.rs | 7 +- .../views/filter/model/layered_filter.rs | 7 +- .../src/db/graph/views/filter/model/mod.rs | 8 +- .../views/filter/model/node_filter/mod.rs | 14 +- .../views/filter/model/snapshot_filter.rs | 9 +- .../views/filter/model/windowed_filter.rs | 7 +- 9 files changed, 165 insertions(+), 46 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/dyn_factory.rs b/raphtory/src/db/graph/views/filter/model/dyn_factory.rs index a247d96cfa..071b120ae4 100644 --- a/raphtory/src/db/graph/views/filter/model/dyn_factory.rs +++ b/raphtory/src/db/graph/views/filter/model/dyn_factory.rs @@ -4,16 +4,23 @@ //! form an open family of generic types. Anything that builds a filter from //! runtime data — the python bindings, a deserialised filter tree — needs one //! type to hold whichever factory the data names, so each family is erased -//! behind a trait object here. Views wrap the *erased* factory, which keeps -//! the set of concrete types finite; wrapping the typed factory would ask the -//! compiler for a vtable per wrapper combination. - -use crate::db::graph::views::filter::model::{ - after_bounds, at_bounds, before_bounds, - node_expr::{DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr}, - windowed_filter::Windowed, - CreateView, DynCreateView, DynPropertyExprFactory, EdgeFilterFactory, EntityMarker, - InternalViewWrapOps, NodeFilterFactory, PropertyExprFactory, ViewWrapOps, +//! behind a trait object here. The typed factories carry the [`Static`] marker +//! and get the erased trait through a blanket impl; the trait object itself +//! does not, and forwards each call through its vtable instead. +//! +//! A view wraps the *erased* factory, which keeps the set of concrete types +//! finite: wrapping the typed one would ask the compiler for a vtable per +//! wrapper combination. Windows are the exception, because a window over a +//! window merges into one, so the typed wrapper never nests. + +use crate::db::{ + api::view::internal::Static, + graph::views::filter::model::{ + after_bounds, at_bounds, before_bounds, + node_expr::{DynCreateOp, DynEntityExpr, DynTemporal, EntityExpr}, + CreateView, DynCreateView, DynPropertyExprFactory, EdgeFilterFactory, EntityMarker, + InternalViewWrapOps, NodeFilterFactory, PropertyExprFactory, ViewWrapOps, + }, }; use raphtory_api::core::storage::timeindex::EventTime; use std::sync::Arc; @@ -37,20 +44,50 @@ pub trait DynNodeFilterFactory: impl InternalViewWrapOps for Arc { type Window = Arc; - // Both calls dispatch through the vtable explicitly: plain method syntax - // would select the DynNodeFilterFactory blanket on Arc itself and loop. fn bounds(&self) -> (EventTime, EventTime) { - self.as_ref().dyn_bounds() + self.dyn_bounds() } fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { + self.dyn_build_window(start, end) + } +} + +impl DynNodeFilterFactory for Arc { + fn dyn_id(&self) -> Arc { + self.as_ref().dyn_id() + } + fn dyn_name(&self) -> Arc { + self.as_ref().dyn_name() + } + fn dyn_node_type(&self) -> Arc { + self.as_ref().dyn_node_type() + } + fn dyn_degree(&self) -> Arc { + self.as_ref().dyn_degree() + } + fn dyn_in_degree(&self) -> Arc { + self.as_ref().dyn_in_degree() + } + fn dyn_out_degree(&self) -> Arc { + self.as_ref().dyn_out_degree() + } + fn dyn_metadata(&self, name: String) -> Arc { + self.as_ref().dyn_metadata(name) + } + + fn dyn_build_window(&self, start: EventTime, end: EventTime) -> Arc { self.as_ref().dyn_build_window(start, end) } + + fn dyn_bounds(&self) -> (EventTime, EventTime) { + self.as_ref().dyn_bounds() + } } impl DynNodeFilterFactory for T where - T: NodeFilterFactory + Send + Sync + 'static, + T: NodeFilterFactory + Static + Send + Sync + 'static, { fn dyn_id(&self) -> Arc { Arc::new(self.id()) @@ -103,19 +140,55 @@ pub trait DynEdgeFilterFactory: DynEntityExpr + DynCreateView + Send + Sync + 's fn dyn_layer(&self, layers: Vec) -> Arc; } -impl EdgeFilterFactory for Arc {} +impl EdgeFilterFactory for Arc { + type EdgeWindow = Self; +} impl InternalViewWrapOps for Arc { type Window = Arc; fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { + self.dyn_window(start, end) + } +} + +impl DynEdgeFilterFactory for Arc { + fn dyn_property(&self, name: String) -> Arc { + self.as_ref().dyn_property(name) + } + fn dyn_metadata(&self, name: String) -> Arc { + self.as_ref().dyn_metadata(name) + } + + fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { self.as_ref().dyn_window(start, end) } + fn dyn_at(&self, time: EventTime) -> Arc { + self.as_ref().dyn_at(time) + } + fn dyn_after(&self, time: EventTime) -> Arc { + self.as_ref().dyn_after(time) + } + fn dyn_before(&self, time: EventTime) -> Arc { + self.as_ref().dyn_before(time) + } + fn dyn_latest(&self) -> Arc { + self.as_ref().dyn_latest() + } + fn dyn_snapshot_at(&self, time: EventTime) -> Arc { + self.as_ref().dyn_snapshot_at(time) + } + fn dyn_snapshot_latest(&self) -> Arc { + self.as_ref().dyn_snapshot_latest() + } + fn dyn_layer(&self, layers: Vec) -> Arc { + self.as_ref().dyn_layer(layers) + } } impl DynEdgeFilterFactory for T where - T: EdgeFilterFactory + ViewWrapOps + CreateView + EntityExpr + Clone, + T: EdgeFilterFactory + Static + ViewWrapOps + CreateView + EntityExpr + Clone, T: Send + Sync + 'static, ::Marker: Into, { @@ -126,15 +199,8 @@ where Arc::new(PropertyExprFactory::metadata(self, name)) } - // The window wrapper is constructed over the erased factory directly: - // routing through ViewWrapOps::window would dispatch straight back into - // this method through the erased build_window. fn dyn_window(&self, start: EventTime, end: EventTime) -> Arc { - let dyn_self: Arc = Arc::new(self.clone()); - let (old_start, old_end) = self.bounds(); - let end = end.min(old_end); - let start = start.max(old_start).min(end); - Arc::new(Windowed::new(start, end, dyn_self)) + Arc::new(self.clone().window(start, end)) } fn dyn_at(&self, time: EventTime) -> Arc { let (start, end) = at_bounds(time); @@ -148,9 +214,9 @@ where let (start, end) = before_bounds(time); self.dyn_window(start, end) } - // Same erasure trick as dyn_window: wrapping the erased factory keeps the - // set of vtable-instantiated types finite; wrapping `self` directly would - // materialise a vtable for every wrapper combination. + // These wrap the erased factory (see the module docs): `latest` over + // `latest` does not merge the way a window does, so wrapping `self` + // directly would materialise a vtable for every wrapper combination. fn dyn_latest(&self) -> Arc { let dyn_self: Arc = Arc::new(self.clone()); Arc::new(dyn_self.latest()) 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 2774ae00a7..066723f153 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -1,6 +1,6 @@ use crate::{ db::{ - api::view::internal::GraphView, + api::view::internal::{GraphView, Static}, graph::views::filter::model::{ edge_expr::{ops::EdgeEndpointNodeOp, EdgeOp}, latest_filter::Latest, @@ -22,6 +22,8 @@ use std::{fmt, fmt::Display, sync::Arc}; #[derive(Clone, Debug, Copy, Default, PartialEq, Eq)] pub struct EdgeFilter; +impl Static for EdgeFilter {} + impl From for EntityMarker { fn from(_value: EdgeFilter) -> Self { EntityMarker::Edge @@ -172,13 +174,27 @@ use crate::db::{ }, }; -impl EdgeFilterFactory for EdgeFilter {} -impl EdgeFilterFactory for ExplodedEdgeFilter {} -impl EdgeFilterFactory for Windowed {} -impl EdgeFilterFactory for Latest {} -impl EdgeFilterFactory for Layered {} -impl EdgeFilterFactory for SnapshotAt {} -impl EdgeFilterFactory for SnapshotLatest {} +impl EdgeFilterFactory for EdgeFilter { + type EdgeWindow = Windowed; +} +impl EdgeFilterFactory for ExplodedEdgeFilter { + type EdgeWindow = Windowed; +} +impl EdgeFilterFactory for Windowed { + type EdgeWindow = T::EdgeWindow; +} +impl EdgeFilterFactory for Latest { + type EdgeWindow = Windowed>; +} +impl EdgeFilterFactory for Layered { + type EdgeWindow = Layered; +} +impl EdgeFilterFactory for SnapshotAt { + type EdgeWindow = Windowed>; +} +impl EdgeFilterFactory for SnapshotLatest { + type EdgeWindow = Windowed>; +} // ── expr layer: temporal chains on endpoint properties ── 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 826c55b32b..7a885213ff 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 @@ -1,11 +1,14 @@ -use crate::db::graph::views::filter::model::{ - windowed_filter::Windowed, EntityMarker, InternalViewWrapOps, +use crate::db::{ + api::view::internal::Static, + graph::views::filter::model::{windowed_filter::Windowed, EntityMarker, InternalViewWrapOps}, }; use raphtory_api::core::storage::timeindex::EventTime; #[derive(Clone, Debug, Copy, Default, PartialEq, Eq)] pub struct ExplodedEdgeFilter; +impl Static for ExplodedEdgeFilter {} + impl From for EntityMarker { fn from(_value: ExplodedEdgeFilter) -> Self { EntityMarker::ExplodedEdge 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 d4f801d98a..600a3f1560 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -1,6 +1,9 @@ use crate::{ db::{ - api::{state::ops::filter::NodeExistsOp, view::internal::GraphView}, + api::{ + state::ops::filter::NodeExistsOp, + view::internal::{GraphView, Static}, + }, graph::views::{ filter::{ model::{ @@ -23,6 +26,8 @@ pub struct Latest { pub inner: M, } +impl Static for Latest {} + impl Latest { #[inline] pub fn new(inner: M) -> Self { 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 dd10c66446..bc36e7e5f8 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -1,6 +1,9 @@ use crate::{ db::{ - api::{state::ops::filter::NodeExistsOp, view::internal::GraphView}, + api::{ + state::ops::filter::NodeExistsOp, + view::internal::{GraphView, Static}, + }, graph::views::{ filter::{ model::{ @@ -24,6 +27,8 @@ pub struct Layered { pub inner: M, } +impl Static for Layered {} + impl Display for Layered { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "LAYER[{}]({})", layer_label(&self.layer), self.inner) diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 47db1de837..7de94ced87 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -37,6 +37,7 @@ use crate::{ graph::views::{ filter::{ model::{ + dyn_factory::DynEdgeFilterFactory, expr::{ convert::{EdgeLeafKind, FactoryLeaf}, NodeLeaf, @@ -388,7 +389,12 @@ impl EntityAggOps for MetadataExpr { /// /// Disjoint from `NodeFilterFactory`: no type implements both, so `PropertyExpr` /// can have two separate sets of comparison methods gated on each. -pub trait EdgeFilterFactory: PropertyExprFactory + Clone {} +pub trait EdgeFilterFactory: + PropertyExprFactory + InternalViewWrapOps + Clone +{ + /// The factory a window over this one is; erased factories window to themselves. + type EdgeWindow: EdgeFilterFactory + DynEdgeFilterFactory; +} // ───────────────────────────────────────────────────────────────────────────── // PropertyExpr / MetadataExpr — EdgeExpr impls 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 47dddc75d1..49a1ca7404 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 @@ -1,11 +1,15 @@ use crate::{ api::core::Direction, db::{ - api::state::{ - ops::node::{Id, Name, Type}, - NodeStateValue, TypedNodeState, + api::{ + state::{ + ops::node::{Id, Name, Type}, + NodeStateValue, TypedNodeState, + }, + view::internal::Static, }, graph::views::filter::model::{ + dyn_factory::DynNodeFilterFactory, latest_filter::Latest, layered_filter::Layered, node_expr::{exprs::DegreeExpr, EntityExpr}, @@ -22,6 +26,8 @@ use raphtory_api::core::storage::timeindex::EventTime; #[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] pub struct NodeFilter; +impl Static for NodeFilter {} + impl From for EntityMarker { fn from(_value: NodeFilter) -> Self { EntityMarker::Node @@ -41,7 +47,7 @@ impl InternalViewWrapOps for NodeFilter { pub trait NodeFilterFactory: InternalViewWrapOps + CreateView + EntityExpr { - type NodeWindow: NodeFilterFactory; + type NodeWindow: NodeFilterFactory + DynNodeFilterFactory; #[inline] fn id(&self) -> Id { Id 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 78f9177cf8..f91467a2a5 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -2,7 +2,10 @@ use crate::{ db::{ api::{ state::ops::filter::NodeExistsOp, - view::{internal::GraphView, time::TimeOps}, + view::{ + internal::{GraphView, Static}, + time::TimeOps, + }, }, graph::views::{ filter::{ @@ -26,6 +29,8 @@ pub struct SnapshotAt { pub inner: M, } +impl Static for SnapshotAt {} + impl SnapshotAt { #[inline] pub fn new(time: T, inner: M) -> Self { @@ -104,6 +109,8 @@ pub struct SnapshotLatest { pub inner: M, } +impl Static for SnapshotLatest {} + impl SnapshotLatest { #[inline] pub fn new(inner: M) -> Self { 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 5bf0369450..effd6602be 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -1,6 +1,9 @@ use crate::{ db::{ - api::{state::ops::filter::NodeExistsOp, view::internal::GraphView}, + api::{ + state::ops::filter::NodeExistsOp, + view::internal::{GraphView, Static}, + }, graph::views::{ filter::{ model::{ @@ -28,6 +31,8 @@ pub struct Windowed { pub inner: M, } +impl Static for Windowed {} + impl Display for Windowed { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( From 68d731e4b7295d64ad32292ec4649257fb3546b2 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:04:40 +0100 Subject: [PATCH 132/155] hand edge ops the storage entry and ask them three explicit questions An EdgeOp is applied to the edge as a whole, to the edge in one layer, or to one exploded instance, each with the storage entry the filtered graph already holds. The reads EdgeView does for a value, a history, a metadata entry and the validity checks move into edge_reads, shared by EdgeView and the ops, so neither looks the entry up twice. --- raphtory/src/db/api/view/edge.rs | 68 +-- raphtory/src/db/graph/edge.rs | 97 +---- raphtory/src/db/graph/edge_reads.rs | 168 +++++++ raphtory/src/db/graph/mod.rs | 1 + .../views/filter/edge_expr_filtered_graph.rs | 24 +- .../exploded_edge_expr_filtered_graph.rs | 24 +- .../graph/views/filter/model/edge_expr/mod.rs | 46 +- .../graph/views/filter/model/edge_expr/ops.rs | 411 ++++++++++++++---- .../graph/views/filter/model/expr/compile.rs | 99 ++++- .../views/filter/model/is_self_loop_filter.rs | 4 +- .../graph/views/filter/model/node_expr/ops.rs | 32 +- 11 files changed, 682 insertions(+), 292 deletions(-) create mode 100644 raphtory/src/db/graph/edge_reads.rs diff --git a/raphtory/src/db/api/view/edge.rs b/raphtory/src/db/api/view/edge.rs index 85ca5cfc9e..78458a1e2b 100644 --- a/raphtory/src/db/api/view/edge.rs +++ b/raphtory/src/db/api/view/edge.rs @@ -14,6 +14,7 @@ use crate::{ }, graph::{ edge::{edge_valid_layer, EdgeView}, + edge_reads::{self, EdgeAt}, views::layer_graph::LayeredGraph, }, }, @@ -260,25 +261,8 @@ impl<'graph, E: BaseEdgeViewOps<'graph>> EdgeViewOps<'graph> for E { /// boolean: fn is_valid(&self) -> Self::ValueType { self.map(|g, e| { - if edge_valid_layer(g, e) { - let time_semantics = g.edge_time_semantics(); - let edge = g.core_edge(Either::Right(e)); - match e.time() { - None => match e.layer() { - None => time_semantics.edge_is_valid(edge.as_ref(), g), - Some(layer) => time_semantics.edge_is_valid( - edge.as_ref(), - LayeredGraph::new(g, LayerIds::One(layer)), - ), - }, - Some(t) => { - let layer = e.layer().expect("exploded edge should have layer"); - time_semantics.edge_is_valid_exploded(edge.as_ref(), g, t, layer) - } - } - } else { - false - } + let edge = g.core_edge(Either::Right(e)); + edge_reads::is_valid(g, edge.as_ref(), EdgeAt::of(e)) }) } @@ -286,25 +270,8 @@ impl<'graph, E: BaseEdgeViewOps<'graph>> EdgeViewOps<'graph> for E { /// boolean: fn is_deleted(&self) -> Self::ValueType { self.map(|g, e| { - if edge_valid_layer(g, e) { - let time_semantics = g.edge_time_semantics(); - let edge = g.core_edge(Either::Right(e)); - match e.time() { - None => match e.layer() { - None => time_semantics.edge_is_deleted(edge.as_ref(), g), - Some(layer) => time_semantics.edge_is_deleted( - edge.as_ref(), - LayeredGraph::new(g, LayerIds::One(layer)), - ), - }, - Some(t) => { - let layer = e.layer().expect("exploded edge should have layer"); - time_semantics.edge_is_deleted_exploded(edge.as_ref(), g, t, layer) - } - } - } else { - false - } + let edge = g.core_edge(Either::Right(e)); + edge_reads::is_deleted(g, edge.as_ref(), EdgeAt::of(e)) }) } @@ -357,28 +324,9 @@ impl<'graph, E: BaseEdgeViewOps<'graph>> EdgeViewOps<'graph> for E { /// Returns: /// bool: fn is_active(&self) -> Self::ValueType { - self.map(move |g, e| { - if edge_valid_layer(g, e) { - let edge = g.core_edge(Either::Right(e)); - let time_semantics = g.edge_time_semantics(); - match e.time() { - None => match e.layer() { - None => time_semantics.edge_is_active(edge.as_ref(), g), - Some(layer_id) => time_semantics.edge_is_active( - edge.as_ref(), - LayeredGraph::new(g, LayerIds::One(layer_id)), - ), - }, - Some(t) => time_semantics.edge_is_active_exploded( - edge.as_ref(), - g, - t, - e.layer().expect("exploded edge should have layer"), - ), - } - } else { - false - } + self.map(|g, e| { + let edge = g.core_edge(Either::Right(e)); + edge_reads::is_active(g, edge.as_ref(), EdgeAt::of(e)) }) } diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index 98fdb1c05c..ee42765a67 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -25,7 +25,12 @@ use crate::{ IntoDynamic, StaticGraphViewOps, }, }, - graph::{edges::Edges, node::NodeView, views::layer_graph::LayeredGraph}, + graph::{ + edge_reads::{self, EdgeAt}, + edges::Edges, + node::NodeView, + views::layer_graph::LayeredGraph, + }, }, errors::{into_graph_err, GraphError}, prelude::*, @@ -560,23 +565,8 @@ impl<'graph, G: GraphViewOps<'graph> + EdgePropertySchemaOps> InternalMetadataOp } fn get_metadata(&self, id: usize) -> Option { - if edge_valid_layer(&self.graph, self.edge) { - let time_semantics = self.graph.edge_time_semantics(); - match self.edge.layer() { - None => time_semantics.edge_metadata( - self.graph.core_edge(Either::Right(self.edge)).as_ref(), - &self.graph, - id, - ), - Some(layer) => time_semantics.edge_metadata( - self.graph.core_edge(Either::Right(self.edge)).as_ref(), - LayeredGraph::new(&self.graph, LayerIds::One(layer)), - id, - ), - } - } else { - None - } + let edge = self.graph.core_edge(Either::Right(self.edge)); + edge_reads::metadata(&self.graph, edge.as_ref(), EdgeAt::of(self.edge), id) } } @@ -590,71 +580,18 @@ impl InternalTemporalPropertyViewOps for EdgeView { } fn temporal_value(&self, id: usize) -> Option { - if edge_valid_layer(&self.graph, self.edge) { - let time_semantics = self.graph.edge_time_semantics(); - let edge = self.graph.core_edge(Either::Right(self.edge)); - match self.edge.time() { - None => match self.edge.layer() { - None => time_semantics.temporal_edge_prop_last(edge.as_ref(), &self.graph, id), - Some(layer) => time_semantics.temporal_edge_prop_last( - edge.as_ref(), - LayeredGraph::new(&self.graph, LayerIds::One(layer)), - id, - ), - }, - Some(t) => { - let layer = self.edge.layer().expect("exploded edge should have layer"); - time_semantics.temporal_edge_prop_exploded( - edge.as_ref(), - &self.graph, - id, - t, - layer, - ) - } - } - } else { - None - } + let edge = self.graph.core_edge(Either::Right(self.edge)); + edge_reads::temporal_value(&self.graph, edge.as_ref(), EdgeAt::of(self.edge), id) } fn temporal_iter(&self, id: usize) -> BoxedLIter<'_, (EventTime, Prop)> { - if edge_valid_layer(&self.graph, self.edge) { - let time_semantics = self.graph.edge_time_semantics(); - let edge = self.graph.core_edge(Either::Right(self.edge)); - let graph = &self.graph; - match self.edge.time() { - None => match self.edge.layer() { - None => GenLockedIter::from(edge, move |edge| { - time_semantics - .temporal_edge_prop_hist(edge.as_ref(), graph, graph.layer_ids(), id) - .into_dyn_boxed() - }) - .into_dyn_boxed(), - Some(layer) => { - let layer_ids = LayerIds::One(layer); - GenLockedIter::from((edge, layer_ids), move |(edge, layer_ids)| { - time_semantics - .temporal_edge_prop_hist(edge.as_ref(), graph, layer_ids, id) - .into_dyn_boxed() - }) - .into_dyn_boxed() - } - } - .map(|(t, _, v)| (t, v)) - .into_dyn_boxed(), - Some(t) => { - let layer = self.edge.layer().expect("Exploded edge should have layer"); - time_semantics - .temporal_edge_prop_exploded(edge.as_ref(), &self.graph, id, t, layer) - .map(|v| (t, v)) - .into_iter() - .into_dyn_boxed() - } - } - } else { - iter::empty().into_dyn_boxed() - } + let edge = self.graph.core_edge(Either::Right(self.edge)); + let graph = &self.graph; + let at = EdgeAt::of(self.edge); + GenLockedIter::from(edge, move |edge| { + edge_reads::temporal_hist(graph, edge.as_ref(), at, id) + }) + .into_dyn_boxed() } fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<'_, (EventTime, Prop)> { diff --git a/raphtory/src/db/graph/edge_reads.rs b/raphtory/src/db/graph/edge_reads.rs new file mode 100644 index 0000000000..c3b904f21b --- /dev/null +++ b/raphtory/src/db/graph/edge_reads.rs @@ -0,0 +1,168 @@ +//! Reads on one edge, given its storage entry and which part of the edge the +//! question is about: the edge as a whole, the edge in one layer, or one +//! exploded instance. `EdgeView` and the compiled edge ops share them, so both +//! answer the same way and neither looks the entry up a second time. + +use crate::{ + core::{entities::LayerIds, utils::iter::GenLockedIter}, + db::{ + api::view::{ + internal::{EdgeTimeSemanticsOps, GraphView}, + BoxedLIter, IntoDynBoxed, + }, + graph::views::layer_graph::LayeredGraph, + }, +}; +use raphtory_api::core::{ + entities::{edges::edge_ref::EdgeRef, properties::prop::Prop, LayerId}, + storage::timeindex::EventTime, +}; +use std::iter; +use storage::EdgeEntryRef; + +/// Which part of an edge a read is about. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(crate) enum EdgeAt { + Whole, + Layer(LayerId), + Exploded(LayerId, EventTime), +} + +impl EdgeAt { + /// The part an edge reference points at: its layer and time stamps, if any. + pub(crate) fn of(edge: EdgeRef) -> Self { + match (edge.layer(), edge.time()) { + (Some(layer), Some(t)) => EdgeAt::Exploded(layer, t), + (Some(layer), None) => EdgeAt::Layer(layer), + (None, _) => EdgeAt::Whole, + } + } + + /// A read through a layer the view does not show has no answer. + fn visible(self, graph: &G) -> bool { + match self { + EdgeAt::Whole => true, + EdgeAt::Layer(layer) | EdgeAt::Exploded(layer, _) => graph.layer_ids().contains(&layer), + } + } +} + +/// The latest value of temporal property `id`. +pub(crate) fn temporal_value( + graph: &G, + edge: EdgeEntryRef, + at: EdgeAt, + id: usize, +) -> Option { + if !at.visible(graph) { + return None; + } + let time_semantics = graph.edge_time_semantics(); + match at { + EdgeAt::Whole => time_semantics.temporal_edge_prop_last(edge, graph, id), + EdgeAt::Layer(layer) => time_semantics.temporal_edge_prop_last( + edge, + LayeredGraph::new(graph, LayerIds::One(layer)), + id, + ), + EdgeAt::Exploded(layer, t) => { + time_semantics.temporal_edge_prop_exploded(edge, graph, id, t, layer) + } + } +} + +/// The history of temporal property `id`, oldest first. +pub(crate) fn temporal_hist<'a, G: GraphView>( + graph: &'a G, + edge: EdgeEntryRef<'a>, + at: EdgeAt, + id: usize, +) -> BoxedLIter<'a, (EventTime, Prop)> { + if !at.visible(graph) { + return iter::empty().into_dyn_boxed(); + } + let time_semantics = graph.edge_time_semantics(); + match at { + EdgeAt::Whole => time_semantics + .temporal_edge_prop_hist(edge, graph, graph.layer_ids(), id) + .map(|(t, _, v)| (t, v)) + .into_dyn_boxed(), + EdgeAt::Layer(layer) => { + GenLockedIter::from((edge, LayerIds::One(layer)), move |(edge, layer_ids)| { + time_semantics + .temporal_edge_prop_hist(*edge, graph, layer_ids, id) + .map(|(t, _, v)| (t, v)) + .into_dyn_boxed() + }) + .into_dyn_boxed() + } + EdgeAt::Exploded(layer, t) => time_semantics + .temporal_edge_prop_exploded(edge, graph, id, t, layer) + .map(|v| (t, v)) + .into_iter() + .into_dyn_boxed(), + } +} + +/// The value of metadata entry `id`; an exploded instance shares its layer's. +pub(crate) fn metadata( + graph: &G, + edge: EdgeEntryRef, + at: EdgeAt, + id: usize, +) -> Option { + if !at.visible(graph) { + return None; + } + let time_semantics = graph.edge_time_semantics(); + match at { + EdgeAt::Whole => time_semantics.edge_metadata(edge, graph, id), + EdgeAt::Layer(layer) | EdgeAt::Exploded(layer, _) => { + time_semantics.edge_metadata(edge, LayeredGraph::new(graph, LayerIds::One(layer)), id) + } + } +} + +pub(crate) fn is_active(graph: &G, edge: EdgeEntryRef, at: EdgeAt) -> bool { + if !at.visible(graph) { + return false; + } + let time_semantics = graph.edge_time_semantics(); + match at { + EdgeAt::Whole => time_semantics.edge_is_active(edge, graph), + EdgeAt::Layer(layer) => { + time_semantics.edge_is_active(edge, LayeredGraph::new(graph, LayerIds::One(layer))) + } + EdgeAt::Exploded(layer, t) => time_semantics.edge_is_active_exploded(edge, graph, t, layer), + } +} + +pub(crate) fn is_valid(graph: &G, edge: EdgeEntryRef, at: EdgeAt) -> bool { + if !at.visible(graph) { + return false; + } + let time_semantics = graph.edge_time_semantics(); + match at { + EdgeAt::Whole => time_semantics.edge_is_valid(edge, graph), + EdgeAt::Layer(layer) => { + time_semantics.edge_is_valid(edge, LayeredGraph::new(graph, LayerIds::One(layer))) + } + EdgeAt::Exploded(layer, t) => time_semantics.edge_is_valid_exploded(edge, graph, t, layer), + } +} + +pub(crate) fn is_deleted(graph: &G, edge: EdgeEntryRef, at: EdgeAt) -> bool { + if !at.visible(graph) { + return false; + } + let time_semantics = graph.edge_time_semantics(); + match at { + EdgeAt::Whole => time_semantics.edge_is_deleted(edge, graph), + EdgeAt::Layer(layer) => { + time_semantics.edge_is_deleted(edge, LayeredGraph::new(graph, LayerIds::One(layer))) + } + EdgeAt::Exploded(layer, t) => { + time_semantics.edge_is_deleted_exploded(edge, graph, t, layer) + } + } +} diff --git a/raphtory/src/db/graph/mod.rs b/raphtory/src/db/graph/mod.rs index 8d393764e3..2720f8c679 100644 --- a/raphtory/src/db/graph/mod.rs +++ b/raphtory/src/db/graph/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; pub mod assertions; pub mod edge; +pub(crate) mod edge_reads; pub mod edges; pub mod graph; pub mod node; diff --git a/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs index 6cc383ad08..6753b43468 100644 --- a/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs @@ -19,21 +19,18 @@ use crate::{ use either::Either; use raphtory_api::{ core::{ - entities::{edges::edge_ref::EdgeRef, LayerId, ELID}, + entities::{LayerId, ELID}, storage::timeindex::EventTime, }, inherit::Base, }; -use raphtory_storage::{ - core_ops::{CoreGraphOps, InheritCoreGraphOps}, - graph::edges::edge_storage_ops::EdgeStorageOps, -}; +use raphtory_storage::core_ops::{CoreGraphOps, InheritCoreGraphOps}; use storage::EdgeEntryRef; /// Edge-filtered graph: hides edges that fail the predicate `filter`. /// -/// Parallel to `NodeFilteredGraph` but for edges: `internal_filter_edge` evaluates -/// `filter.apply(storage, edge_ref)` in O(1) after a single compile step. +/// Parallel to `NodeFilteredGraph` but for edges: `internal_filter_edge` hands the +/// storage entry it is given straight to `filter.apply`. #[derive(Clone)] pub struct EdgeExprFilteredGraph { pub(crate) graph: G, @@ -128,11 +125,9 @@ impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> if !self.filter.filters_exploded() || eid.is_deletion() { return true; } - let edge_ref: EdgeRef = self.core_edge(Either::Left(eid.eid())).out_ref(); - self.filter.apply_exploded( - self.graph.core_graph(), - edge_ref.at_layer(eid.layer()).at(t), - ) + let edge = self.core_edge(Either::Left(eid.eid())); + self.filter + .apply_exploded(self.graph.core_graph(), edge.as_ref(), eid.layer(), t) } fn node_filter_includes_exploded_edge_filter(&self) -> bool { @@ -157,7 +152,7 @@ impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> Internal !self.filter.filters_exploded() || self .filter - .apply_exploded(self.graph.core_graph(), edge.out_ref().at_layer(layer)) + .apply_layer(self.graph.core_graph(), edge, layer) } fn node_filter_includes_edge_layer_filter(&self) -> bool { @@ -183,7 +178,6 @@ impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> Internal if !self.graph.internal_filter_edge(edge, layer_ids) { return false; } - let edge_ref: EdgeRef = edge.out_ref(); - self.filter.apply(self.graph.core_graph(), edge_ref) + self.filter.apply(self.graph.core_graph(), edge) } } diff --git a/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs b/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs index e3792cc686..3c8867addd 100644 --- a/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs @@ -18,21 +18,13 @@ use crate::{ }; use either::Either; use raphtory_api::{ - core::{ - entities::{edges::edge_ref::EdgeRef, ELID}, - storage::timeindex::EventTime, - }, + core::{entities::ELID, storage::timeindex::EventTime}, inherit::Base, }; -use raphtory_storage::{ - core_ops::{CoreGraphOps, InheritCoreGraphOps}, - graph::edges::edge_storage_ops::EdgeStorageOps, -}; +use raphtory_storage::core_ops::{CoreGraphOps, InheritCoreGraphOps}; -/// Edge-filtered graph: hides edges that fail the predicate `filter`. -/// -/// Parallel to `NodeFilteredGraph` but for edges: `internal_filter_edge` evaluates -/// `filter.apply(storage, edge_ref)` in O(1) after a single compile step. +/// Exploded-edge-filtered graph: hides the exploded instances that fail the +/// predicate `filter`, which is asked about each instance through `apply_exploded`. #[derive(Clone)] pub struct ExplodedEdgeExprFilteredGraph { pub(crate) graph: G, @@ -125,11 +117,9 @@ impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> if eid.is_deletion() { return true; } - let edge_ref: EdgeRef = self.core_edge(Either::Left(eid.eid())).out_ref(); - self.filter.apply( - self.graph.core_graph(), - edge_ref.at_layer(eid.layer()).at(t), - ) + let edge = self.core_edge(Either::Left(eid.eid())); + self.filter + .apply_exploded(self.graph.core_graph(), edge.as_ref(), eid.layer(), t) } } impl<'graph, G: GraphViewOps<'graph>, F: EdgeOp + Clone> InheritEdgeLayerFilterOps diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs index 17cab31dd0..3b7c5fb336 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -19,22 +19,44 @@ //! └──────────────────────────────────────────────────────────┘ //! ``` -use raphtory_api::core::entities::{edges::edge_ref::EdgeRef, properties::prop::PropType}; +use raphtory_api::core::{ + entities::{properties::prop::PropType, LayerId}, + storage::timeindex::EventTime, +}; use raphtory_storage::graph::graph::GraphStorage; +use storage::EdgeEntryRef; pub mod ops; // ───────────────────────────────────────────────────────────────────────────── -// EdgeOp — compiled evaluator: EdgeRef → typed value +// EdgeOp — compiled evaluator: storage entry → typed value // ───────────────────────────────────────────────────────────────────────────── -/// A compiled edge evaluator: given an [`EdgeRef`], returns a typed value. +/// A compiled edge evaluator: given an edge's storage entry, returns a typed value. /// /// Parallel to [`NodeOp`] — same contract but the subject is an edge. pub trait EdgeOp: Send + Sync { type Output: Clone + Send + Sync; - fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Self::Output; + /// The value for the edge as a whole. + fn apply(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> Self::Output; + + /// The value for the edge seen in one layer. + fn apply_layer( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Self::Output; + + /// The value for one exploded instance of the edge. + fn apply_exploded( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Self::Output; fn prop_type(&self) -> PropType { PropType::Empty @@ -45,20 +67,10 @@ pub trait EdgeOp: Send + Sync { None } - /// Whether the answer depends on which layer or exploded instance of the edge is - /// asked about. A plain op answers for the edge as a whole, and a filtered graph - /// consults it once per edge. + /// Whether the answer differs between the edge as a whole and one of its + /// layers or exploded instances. A plain op answers for the edge as a whole, + /// and a filtered graph consults it once per edge. fn filters_exploded(&self) -> bool { false } - - /// The answer for one layer, or one exploded instance, of an edge: `edge` carries - /// the layer and, for an instance, the time. An op that answers per edge sees the - /// edge as a whole. - fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> Self::Output { - self.apply( - storage, - EdgeRef::new(edge.pid(), edge.src(), edge.dst(), edge.dir()), - ) - } } diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index a3a6616ebc..d96bc79197 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -1,25 +1,23 @@ -//! Runtime edge evaluators — given an EdgeRef, return a typed value. +//! Runtime edge evaluators — given an edge's storage entry, return a typed value. //! //! Parallel to `node_expr/ops.rs` — same design, different subject. -use crate::{ - db::{ - api::{ - properties::internal::{InternalMetadataOps, InternalTemporalPropertyViewOps}, - state::ops::Const, - view::internal::{FilterOps, GraphView}, - }, - graph::edge::EdgeView, +use crate::db::{ + api::{ + state::ops::Const, + view::internal::{FilterOps, GraphView}, }, - prelude::EdgeViewOps, + graph::edge_reads::{self, EdgeAt}, }; -use either::Either; -use raphtory_api::core::entities::{ - edges::edge_ref::EdgeRef, - properties::prop::{Prop, PropType}, - ELID, +use raphtory_api::core::{ + entities::{ + properties::prop::{Prop, PropType}, + LayerId, ELID, + }, + storage::timeindex::EventTime, }; -use raphtory_storage::{core_ops::CoreGraphOps, graph::graph::GraphStorage}; +use raphtory_storage::graph::{edges::edge_storage_ops::EdgeStorageOps, graph::GraphStorage}; +use storage::EdgeEntryRef; use super::EdgeOp; use crate::db::{api::state::ops::NodeOp, graph::views::filter::model::edge_filter::Endpoint}; @@ -33,10 +31,24 @@ use std::sync::Arc; impl<'a, V: Clone + Send + Sync> EdgeOp for Arc + 'a> { type Output = V; - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> V { + fn apply(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> V { self.as_ref().apply(storage, edge) } + fn apply_layer(&self, storage: &GraphStorage, edge: EdgeEntryRef, layer: LayerId) -> V { + self.as_ref().apply_layer(storage, edge, layer) + } + + fn apply_exploded( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> V { + self.as_ref().apply_exploded(storage, edge, layer, t) + } + fn prop_type(&self) -> PropType { self.as_ref().prop_type() } @@ -48,10 +60,6 @@ impl<'a, V: Clone + Send + Sync> EdgeOp for Arc + 'a> { fn filters_exploded(&self) -> bool { self.as_ref().filters_exploded() } - - fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> V { - self.as_ref().apply_exploded(storage, edge) - } } // ───────────────────────────────────────────────────────────────────────────── @@ -61,7 +69,21 @@ impl<'a, V: Clone + Send + Sync> EdgeOp for Arc + 'a> { impl EdgeOp for Const { type Output = V; - fn apply(&self, _storage: &GraphStorage, _edge: EdgeRef) -> V { + fn apply(&self, _storage: &GraphStorage, _edge: EdgeEntryRef) -> V { + self.0.clone() + } + + fn apply_layer(&self, _storage: &GraphStorage, _edge: EdgeEntryRef, _layer: LayerId) -> V { + self.0.clone() + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + _edge: EdgeEntryRef, + _layer: LayerId, + _t: EventTime, + ) -> V { self.0.clone() } @@ -83,8 +105,27 @@ pub(crate) struct EdgePropOp { impl EdgeOp for EdgePropOp { type Output = Option; - fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { - EdgeView::new(&self.graph, edge).temporal_value(self.prop_id) + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + edge_reads::temporal_value(&self.graph, edge, EdgeAt::Whole, self.prop_id) + } + + fn apply_layer( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + edge_reads::temporal_value(&self.graph, edge, EdgeAt::Layer(layer), self.prop_id) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + edge_reads::temporal_value(&self.graph, edge, EdgeAt::Exploded(layer, t), self.prop_id) } fn prop_type(&self) -> PropType { @@ -109,8 +150,27 @@ pub(crate) struct EdgeMetaOp { impl EdgeOp for EdgeMetaOp { type Output = Option; - fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { - EdgeView::new(&self.graph, edge).get_metadata(self.prop_id) + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + edge_reads::metadata(&self.graph, edge, EdgeAt::Whole, self.prop_id) + } + + fn apply_layer( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + edge_reads::metadata(&self.graph, edge, EdgeAt::Layer(layer), self.prop_id) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + edge_reads::metadata(&self.graph, edge, EdgeAt::Exploded(layer, t), self.prop_id) } // No declared type: the runtime shape depends on the edge's layers (a @@ -128,6 +188,15 @@ pub(crate) struct TemporalEdgePropOp { pub(crate) prop_id: usize, } +impl TemporalEdgePropOp { + fn history(&self, edge: EdgeEntryRef, at: EdgeAt) -> Option { + let vals: Vec = edge_reads::temporal_hist(&self.graph, edge, at, self.prop_id) + .map(|(_, v)| v) + .collect(); + Some(Prop::List(PropArray::from(vals))) + } +} + impl EdgeOp for TemporalEdgePropOp { type Output = Option; @@ -139,12 +208,27 @@ impl EdgeOp for TemporalEdgePropOp { .map_or(PropType::Empty, |dt| PropType::List(Box::new(dt))) } - fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { - let vals: Vec = EdgeView::new(&self.graph, edge) - .temporal_iter(self.prop_id) - .map(|(_, v)| v) - .collect(); - Some(Prop::List(PropArray::from(vals))) + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + self.history(edge, EdgeAt::Whole) + } + + fn apply_layer( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + self.history(edge, EdgeAt::Layer(layer)) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + self.history(edge, EdgeAt::Exploded(layer, t)) } } @@ -162,6 +246,16 @@ pub(crate) struct EdgeEndpointNodeOp<'g> { pub(crate) endpoint: Endpoint, } +impl<'g> EdgeEndpointNodeOp<'g> { + fn at_endpoint(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + let vid = match self.endpoint { + Endpoint::Src => edge.src(), + Endpoint::Dst => edge.dst(), + }; + self.node_op.apply(storage, vid) + } +} + impl<'g> EdgeOp for EdgeEndpointNodeOp<'g> { type Output = Option; @@ -173,12 +267,27 @@ impl<'g> EdgeOp for EdgeEndpointNodeOp<'g> { self.node_op.const_value() } - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { - let vid = match self.endpoint { - Endpoint::Src => edge.src(), - Endpoint::Dst => edge.dst(), - }; - self.node_op.apply(storage, vid) + fn apply(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + self.at_endpoint(storage, edge) + } + + fn apply_layer( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + _layer: LayerId, + ) -> Option { + self.at_endpoint(storage, edge) + } + + fn apply_exploded( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + _layer: LayerId, + _t: EventTime, + ) -> Option { + self.at_endpoint(storage, edge) } } @@ -197,8 +306,39 @@ pub(crate) struct IsActiveEdgePropOp { impl EdgeOp for IsActiveEdgePropOp { type Output = Option; - fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { - Some(Prop::Bool(EdgeView::new(&self.graph, edge).is_active())) + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + Some(Prop::Bool(edge_reads::is_active( + &self.graph, + edge, + EdgeAt::Whole, + ))) + } + + fn apply_layer( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + Some(Prop::Bool(edge_reads::is_active( + &self.graph, + edge, + EdgeAt::Layer(layer), + ))) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + Some(Prop::Bool(edge_reads::is_active( + &self.graph, + edge, + EdgeAt::Exploded(layer, t), + ))) } fn prop_type(&self) -> PropType { @@ -214,8 +354,39 @@ pub(crate) struct IsValidEdgePropOp { impl EdgeOp for IsValidEdgePropOp { type Output = Option; - fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { - Some(Prop::Bool(EdgeView::new(&self.graph, edge).is_valid())) + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + Some(Prop::Bool(edge_reads::is_valid( + &self.graph, + edge, + EdgeAt::Whole, + ))) + } + + fn apply_layer( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + Some(Prop::Bool(edge_reads::is_valid( + &self.graph, + edge, + EdgeAt::Layer(layer), + ))) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + Some(Prop::Bool(edge_reads::is_valid( + &self.graph, + edge, + EdgeAt::Exploded(layer, t), + ))) } fn prop_type(&self) -> PropType { @@ -231,8 +402,39 @@ pub(crate) struct IsDeletedEdgePropOp { impl EdgeOp for IsDeletedEdgePropOp { type Output = Option; - fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { - Some(Prop::Bool(EdgeView::new(&self.graph, edge).is_deleted())) + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + Some(Prop::Bool(edge_reads::is_deleted( + &self.graph, + edge, + EdgeAt::Whole, + ))) + } + + fn apply_layer( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + Some(Prop::Bool(edge_reads::is_deleted( + &self.graph, + edge, + EdgeAt::Layer(layer), + ))) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + Some(Prop::Bool(edge_reads::is_deleted( + &self.graph, + edge, + EdgeAt::Exploded(layer, t), + ))) } fn prop_type(&self) -> PropType { @@ -241,15 +443,32 @@ impl EdgeOp for IsDeletedEdgePropOp { } #[derive(Clone)] -pub(crate) struct IsSelfLoopEdgePropOp { - pub(crate) graph: G, -} +pub(crate) struct IsSelfLoopEdgePropOp; -impl EdgeOp for IsSelfLoopEdgePropOp { +impl EdgeOp for IsSelfLoopEdgePropOp { type Output = Option; - fn apply(&self, _storage: &GraphStorage, edge: EdgeRef) -> Option { - Some(Prop::Bool(EdgeView::new(&self.graph, edge).is_self_loop())) + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + Some(Prop::Bool(edge.src() == edge.dst())) + } + + fn apply_layer( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + _layer: LayerId, + ) -> Option { + Some(Prop::Bool(edge.src() == edge.dst())) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + _layer: LayerId, + _t: EventTime, + ) -> Option { + Some(Prop::Bool(edge.src() == edge.dst())) } fn prop_type(&self) -> PropType { @@ -276,9 +495,23 @@ impl EdgeExistsOp { impl EdgeOp for EdgeExistsOp { type Output = bool; - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> bool { + self.graph.filter_edge(edge) + } + + fn apply_layer(&self, _storage: &GraphStorage, edge: EdgeEntryRef, layer: LayerId) -> bool { + self.graph.filter_edge_layer(edge, layer) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> bool { self.graph - .filter_edge(storage.core_edge(Either::Right(edge)).as_ref()) + .filter_exploded_edge(ELID::new(edge.eid(), layer), t) } fn prop_type(&self) -> PropType { @@ -288,18 +521,6 @@ impl EdgeOp for EdgeExistsOp { fn filters_exploded(&self) -> bool { true } - - fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - match (edge.layer(), edge.time()) { - (Some(layer), Some(t)) => self - .graph - .filter_exploded_edge(ELID::new(edge.pid(), layer), t), - (Some(layer), None) => self - .graph - .filter_edge_layer(storage.core_edge(Either::Right(edge)).as_ref(), layer), - (None, _) => self.apply(storage, edge), - } - } } #[derive(Debug, Clone)] @@ -311,10 +532,25 @@ pub struct AndEdgeOp { impl, R: EdgeOp> EdgeOp for AndEdgeOp { type Output = bool; - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + fn apply(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> bool { self.left.apply(storage, edge) && self.right.apply(storage, edge) } + fn apply_layer(&self, storage: &GraphStorage, edge: EdgeEntryRef, layer: LayerId) -> bool { + self.left.apply_layer(storage, edge, layer) && self.right.apply_layer(storage, edge, layer) + } + + fn apply_exploded( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> bool { + self.left.apply_exploded(storage, edge, layer, t) + && self.right.apply_exploded(storage, edge, layer, t) + } + fn prop_type(&self) -> PropType { PropType::Bool } @@ -330,10 +566,6 @@ impl, R: EdgeOp> EdgeOp for AndEdgeOp bool { self.left.filters_exploded() || self.right.filters_exploded() } - - fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - self.left.apply_exploded(storage, edge) && self.right.apply_exploded(storage, edge) - } } #[derive(Debug, Clone)] @@ -345,10 +577,25 @@ pub struct OrEdgeOp { impl, R: EdgeOp> EdgeOp for OrEdgeOp { type Output = bool; - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + fn apply(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> bool { self.left.apply(storage, edge) || self.right.apply(storage, edge) } + fn apply_layer(&self, storage: &GraphStorage, edge: EdgeEntryRef, layer: LayerId) -> bool { + self.left.apply_layer(storage, edge, layer) || self.right.apply_layer(storage, edge, layer) + } + + fn apply_exploded( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> bool { + self.left.apply_exploded(storage, edge, layer, t) + || self.right.apply_exploded(storage, edge, layer, t) + } + fn prop_type(&self) -> PropType { PropType::Bool } @@ -364,10 +611,6 @@ impl, R: EdgeOp> EdgeOp for OrEdgeOp bool { self.left.filters_exploded() || self.right.filters_exploded() } - - fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - self.left.apply_exploded(storage, edge) || self.right.apply_exploded(storage, edge) - } } #[derive(Debug, Clone)] @@ -376,10 +619,24 @@ pub struct NotEdgeOp(pub(crate) T); impl> EdgeOp for NotEdgeOp { type Output = bool; - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + fn apply(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> bool { !self.0.apply(storage, edge) } + fn apply_layer(&self, storage: &GraphStorage, edge: EdgeEntryRef, layer: LayerId) -> bool { + !self.0.apply_layer(storage, edge, layer) + } + + fn apply_exploded( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> bool { + !self.0.apply_exploded(storage, edge, layer, t) + } + fn prop_type(&self) -> PropType { PropType::Bool } @@ -391,8 +648,4 @@ impl> EdgeOp for NotEdgeOp { fn filters_exploded(&self) -> bool { self.0.filters_exploded() } - - fn apply_exploded(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { - !self.0.apply_exploded(storage, edge) - } } diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index 69b503e3d6..c9ed393ffb 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -63,14 +63,15 @@ use crate::{ }; use raphtory_api::core::{ entities::{ - edges::edge_ref::EdgeRef, properties::prop::{Prop, PropType}, - GID, VID, + LayerId, GID, VID, }, + storage::timeindex::EventTime, Direction, }; use raphtory_storage::graph::graph::GraphStorage; use std::{fmt::Debug, sync::Arc}; +use storage::EdgeEntryRef; fn invalid(msg: impl Into) -> GraphError { GraphError::InvalidFilter(msg.into()) @@ -684,7 +685,7 @@ fn truthy(v: &Option) -> bool { // ── runtime ops ────────────────────────────────────────────────────────────── macro_rules! value_ops { - ($op_trait:ident, $id:ty, $binary:ident, $unary:ident, $nary:ident $(, $domain:item)?) => { + ($op_trait:ident, $id:ty, $binary:ident, $unary:ident, $nary:ident $(, $extra:item)*) => { struct $binary<'g, K> { left: Arc> + 'g>, right: Arc> + 'g>, @@ -692,17 +693,29 @@ macro_rules! value_ops { out: PropType, } + impl<'g, K> $binary<'g, K> + where + K: Fn(Option, Option) -> Option, + { + fn eval( + &self, + read: impl Fn(&dyn $op_trait>) -> Option, + ) -> Option { + (self.kernel)(read(self.left.as_ref()), read(self.right.as_ref())) + } + } + impl<'g, K> $op_trait for $binary<'g, K> where K: Fn(Option, Option) -> Option + Send + Sync, { type Output = Option; - $($domain)? + $($extra)* fn prop_type(&self) -> PropType { self.out.clone() } fn apply(&self, storage: &GraphStorage, id: $id) -> Option { - (self.kernel)(self.left.apply(storage, id), self.right.apply(storage, id)) + self.eval(|op| op.apply(storage, id)) } } @@ -712,17 +725,29 @@ macro_rules! value_ops { out: PropType, } + impl<'g, K> $unary<'g, K> + where + K: Fn(Option) -> Option, + { + fn eval( + &self, + read: impl Fn(&dyn $op_trait>) -> Option, + ) -> Option { + (self.kernel)(read(self.inner.as_ref())) + } + } + impl<'g, K> $op_trait for $unary<'g, K> where K: Fn(Option) -> Option + Send + Sync, { type Output = Option; - $($domain)? + $($extra)* fn prop_type(&self) -> PropType { self.out.clone() } fn apply(&self, storage: &GraphStorage, id: $id) -> Option { - (self.kernel)(self.inner.apply(storage, id)) + self.eval(|op| op.apply(storage, id)) } } @@ -732,19 +757,28 @@ macro_rules! value_ops { all: bool, } + impl<'g> $nary<'g> { + fn eval( + &self, + read: impl Fn(&dyn $op_trait>) -> Option, + ) -> Option { + let hit = if self.all { + self.items.iter().all(|item| truthy(&read(item.as_ref()))) + } else { + self.items.iter().any(|item| truthy(&read(item.as_ref()))) + }; + Some(Prop::Bool(hit)) + } + } + impl<'g> $op_trait for $nary<'g> { type Output = Option; - $($domain)? + $($extra)* fn prop_type(&self) -> PropType { PropType::Bool } fn apply(&self, storage: &GraphStorage, id: $id) -> Option { - let hit = if self.all { - self.items.iter().all(|item| truthy(&item.apply(storage, id))) - } else { - self.items.iter().any(|item| truthy(&item.apply(storage, id))) - }; - Some(Prop::Bool(hit)) + self.eval(|op| op.apply(storage, id)) } } }; @@ -762,10 +796,27 @@ value_ops!( ); value_ops!( EdgeOp, - EdgeRef, + EdgeEntryRef, BinaryValueEdgeOp, UnaryValueEdgeOp, - NaryBoolEdgeOp + NaryBoolEdgeOp, + fn apply_layer( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + self.eval(|op| op.apply_layer(storage, edge, layer)) + }, + fn apply_exploded( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + self.eval(|op| op.apply_exploded(storage, edge, layer, t)) + } ); /// Adapts a yes/no edge value to the plain boolean the filtered graphs consume. @@ -776,9 +827,23 @@ struct TruthyEdgeOp<'g> { impl<'g> EdgeOp for TruthyEdgeOp<'g> { type Output = bool; - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> bool { + fn apply(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> bool { truthy(&self.inner.apply(storage, edge)) } + + fn apply_layer(&self, storage: &GraphStorage, edge: EdgeEntryRef, layer: LayerId) -> bool { + truthy(&self.inner.apply_layer(storage, edge, layer)) + } + + fn apply_exploded( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> bool { + truthy(&self.inner.apply_exploded(storage, edge, layer, t)) + } } fn cmp_kernel( 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 d0f9239a68..45d56508a9 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 @@ -41,8 +41,8 @@ impl EntityExpr for IsSelfLoopEdge { impl CreateOp for IsSelfLoopEdge { fn create_edge_op<'g, G: GraphView + 'g>( &self, - graph: G, + _graph: G, ) -> Result> + 'g>, crate::errors::GraphError> { - Ok(Arc::new(IsSelfLoopEdgePropOp { graph })) + Ok(Arc::new(IsSelfLoopEdgePropOp)) } } 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 index b7d8800d78..915acb9726 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -54,13 +54,16 @@ use crate::{ prelude::GraphViewOps, }; use bigdecimal::BigDecimal; -use raphtory_api::core::entities::{ - edges::edge_ref::EdgeRef, - properties::prop::{IntoProp, Prop, PropArray, PropType}, - GidType, GID, VID, +use raphtory_api::core::{ + entities::{ + properties::prop::{IntoProp, Prop, PropArray, PropType}, + GidType, LayerId, GID, VID, + }, + storage::timeindex::EventTime, }; use raphtory_storage::graph::graph::GraphStorage; use std::sync::Arc; +use storage::EdgeEntryRef; // ───────────────────────────────────────────────────────────────────────────── // NodePropOp — latest property value by pre-resolved column ID // ───────────────────────────────────────────────────────────────────────────── @@ -327,9 +330,28 @@ macro_rules! impl_agg_entity_op { ($out_pt)(self.inner.prop_type()) } - fn apply(&self, storage: &GraphStorage, edge: EdgeRef) -> Option { + fn apply(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> Option { ($body)(self.inner.apply(storage, edge)) } + + fn apply_layer( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + ($body)(self.inner.apply_layer(storage, edge, layer)) + } + + fn apply_exploded( + &self, + storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + ($body)(self.inner.apply_exploded(storage, edge, layer, t)) + } } }; } From eb8ea8ed9365bb3e4d7a921864d2ef2cf045871b Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:22:00 +0100 Subject: [PATCH 133/155] borrow the graph in node reads and drop the WithPropType wrapper The property and metadata node ops look their node up through a borrow instead of cloning the graph handle per node. Name, Type and Degree declare their own property type, and the id read is a typed op of its own, so the wrapper that annotated them goes. --- raphtory/src/db/api/state/ops/node.rs | 14 +++++- .../views/filter/model/node_expr/exprs.rs | 35 +++++---------- .../graph/views/filter/model/node_expr/ops.rs | 45 ++++++++++--------- 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/raphtory/src/db/api/state/ops/node.rs b/raphtory/src/db/api/state/ops/node.rs index 90fee1ff6e..d9234d959c 100644 --- a/raphtory/src/db/api/state/ops/node.rs +++ b/raphtory/src/db/api/state/ops/node.rs @@ -4,7 +4,7 @@ pub(crate) use crate::db::api::{ view::internal::{filtered_node::FilteredNodeStorageOps, FilterOps, FilterState, GraphView}, }; use raphtory_api::core::{ - entities::{GID, VID}, + entities::{properties::prop::PropType, GID, VID}, storage::arc_str::ArcStr, Direction, }; @@ -34,6 +34,10 @@ impl NodeOp for Name { NodeList::All } + fn prop_type(&self) -> PropType { + PropType::Str + } + fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { storage.node_name(node) } @@ -97,6 +101,10 @@ impl NodeOp for Type { NodeList::All } + fn prop_type(&self) -> PropType { + PropType::Str + } + fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { storage.node_type(node) } @@ -163,6 +171,10 @@ impl NodeOp for Degree { self.view.node_list() } + fn prop_type(&self) -> PropType { + PropType::U64 + } + fn apply(&self, storage: &GraphStorage, node: VID) -> usize { let node = storage.core_node(node); if matches!(self.view.filter_state(), FilterState::Neither) { 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 1e89278dc0..08c683b061 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 @@ -63,8 +63,8 @@ use super::{ ops::{ - AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, SumNodeOp, - TemporalNodePropOp, WithPropType, + AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NodeIdOp, SumNodeOp, + TemporalNodePropOp, }, AvgEdgeOp, CreateOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, PredicateLhs, SumEdgeOp, @@ -87,7 +87,7 @@ use crate::{ use raphtory_api::core::{ entities::{ properties::prop::{IntoProp, Prop, PropType}, - GidType, GID, + GID, }, storage::arc_str::ArcStr, Direction, @@ -132,14 +132,8 @@ impl CreateOp for Id { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let pt = match graph.id_type() { - Some(GidType::Str) => PropType::Str, - Some(GidType::U64) => PropType::U64, - None => PropType::Empty, - }; - Ok(Arc::new(WithPropType { - inner: Id.map(|a| Some(a.into_prop())), - pt, + Ok(Arc::new(NodeIdOp { + id_type: graph.id_type(), })) } } @@ -180,10 +174,7 @@ impl CreateOp for Name { &self, _graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(WithPropType { - inner: Name.map(|a| Some(a.into_prop())), - pt: PropType::Str, - })) + Ok(Arc::new(Name.map(|a| Some(a.into_prop())))) } } @@ -208,10 +199,9 @@ impl CreateOp for Type { ) -> Result> + 'g>, GraphError> { // Untyped nodes carry the storage's default type key, so a type test // sees the same key the node-type mask is built over. - Ok(Arc::new(WithPropType { - inner: Type.map(|a| Some(a.map_or_else(|| Prop::str("_default"), |b| b.into_prop()))), - pt: PropType::Str, - })) + Ok(Arc::new(Type.map(|a| { + Some(a.map_or_else(|| Prop::str("_default"), |b| b.into_prop())) + }))) } } @@ -482,14 +472,13 @@ impl CreateOp for DegreeExpr { &self, graph: G, ) -> Result> + 'g>, GraphError> { - Ok(Arc::new(WithPropType { - inner: Degree { + Ok(Arc::new( + Degree { dir: self.dir, view: self.view_expr.create_view(graph)?, } .map(|a| Some(Prop::U64(a as u64))), - pt: PropType::U64, - })) + )) } } 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 index 915acb9726..98c23c5cf5 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -41,7 +41,7 @@ use crate::{ db::{ api::{ properties::PropertiesOps, - state::ops::NodeOp, + state::ops::{Id, NodeOp}, view::{ internal::{GraphView, NodeList}, NodeViewOps, @@ -86,7 +86,10 @@ 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) + (&&self.graph) + .node(node)? + .properties() + .get_by_id(self.prop_id) } fn prop_type(&self) -> PropType { @@ -120,7 +123,10 @@ 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) + (&&self.graph) + .node(node)? + .metadata() + .get_by_id(self.prop_id) } fn prop_type(&self) -> PropType { @@ -133,36 +139,31 @@ impl NodeOp for NodeMetaOp { } // ───────────────────────────────────────────────────────────────────────────── -// WithPropType — annotates an op with a type only known at compile time +// NodeIdOp — the node id as a value, typed by the graph's id type // ───────────────────────────────────────────────────────────────────────────── #[derive(Clone)] -pub(crate) struct WithPropType { - pub(crate) inner: T, - pub(crate) pt: PropType, +pub(crate) struct NodeIdOp { + pub(crate) id_type: Option, } -impl NodeOp for WithPropType { - type Output = T::Output; +impl NodeOp for NodeIdOp { + type Output = Option; - fn domain(&self, storage: &GraphStorage) -> NodeList { - self.inner.domain(storage) + fn domain(&self, _storage: &GraphStorage) -> NodeList { + NodeList::All } fn prop_type(&self) -> PropType { - self.pt.clone() - } - - fn const_value(&self) -> Option { - self.inner.const_value() - } - - fn const_value_in_domain(&self, storage: &GraphStorage) -> Option { - self.inner.const_value_in_domain(storage) + match self.id_type { + Some(GidType::Str) => PropType::Str, + Some(GidType::U64) => PropType::U64, + None => PropType::Empty, + } } - fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { - self.inner.apply(storage, node) + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + Some(Id.apply(storage, node).into_prop()) } } From 743785c956f32df4bb883ae4ade3df659298f1f9 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:31:48 +0100 Subject: [PATCH 134/155] hash set membership and resolve named ids when the filter is built is_in tests membership in a HashSet of HashableProp instead of scanning a Vec. A predicate that names nodes by id resolves them to VIDs once, and the id-domain op hands the finished NodeList over on every domain call instead of resolving again. --- .../graph/views/filter/model/expr/compile.rs | 40 +++++++++++++------ .../graph/views/filter/model/node_expr/ops.rs | 19 +++------ 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index c9ed393ffb..8590b21a96 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -63,14 +63,15 @@ use crate::{ }; use raphtory_api::core::{ entities::{ - properties::prop::{Prop, PropType}, + properties::prop::{prop_hashable::HashableProp, Prop, PropType}, LayerId, GID, VID, }, storage::timeindex::EventTime, Direction, }; +use raphtory_core::entities::nodes::node_ref::AsNodeRef; use raphtory_storage::graph::graph::GraphStorage; -use std::{fmt::Debug, sync::Arc}; +use std::{collections::HashSet, fmt::Debug, sync::Arc}; use storage::EdgeEntryRef; fn invalid(msg: impl Into) -> GraphError { @@ -875,11 +876,11 @@ fn set_kernel( negated: bool, shape: Shape, ) -> impl Fn(Option) -> Option + Clone { - let values = Arc::new(values); + let values: Arc> = + Arc::new(values.into_iter().map(HashableProp).collect()); move |v| { let member = |v: Option| { - let v = v?; - let present = values.iter().any(|x| x.equals(&v)); + let present = values.contains(&HashableProp(v?)); Some(Prop::Bool(present != negated)) }; match shape { @@ -1289,24 +1290,37 @@ impl Predicate { }) } + /// The nodes the predicate names outright, resolved once so the node filter + /// can start from them. `None` when it names none, or names an id the graph's + /// id type cannot match: the filter then scans every node. + fn named_nodes(&self, graph: &G) -> Option { + let id_type = graph.id_type(); + let elems = self + .ids + .as_ref()? + .iter() + .map(|v| gid_for_id_lookup(id_type, v)) + .collect::>>()? + .into_iter() + .filter_map(|gid| graph.internalise_node(gid.as_node_ref())) + .collect(); + Some(NodeList::List { elems }) + } + fn node_filter<'graph, G: GraphView + 'graph>( &self, graph: G, ) -> Result + 'graph>, GraphError> { - let id_type = graph.id_type(); + let nodes = self.named_nodes(&graph); let op = self.inner.create_node_op(graph)?; require_bool( &resolved_prop_type(self.inner.prop_type(), op.prop_type()), "a filter", )?; let filter: Arc + 'graph> = Arc::new(op.map(|v| truthy(&v))); - let gids: Option> = self - .ids - .as_ref() - .and_then(|ids| ids.iter().map(|v| gid_for_id_lookup(id_type, v)).collect()); - Ok(match gids { - Some(gids) => Arc::new(IdDomainNodeOp { - gids: Arc::from(gids), + Ok(match nodes { + Some(nodes) => Arc::new(IdDomainNodeOp { + nodes, inner: filter, }), None => filter, 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 index 98c23c5cf5..92fe480766 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -37,7 +37,6 @@ use super::EdgeOp; use crate::{ - core::entities::nodes::node_ref::AsNodeRef, db::{ api::{ properties::PropertiesOps, @@ -572,12 +571,12 @@ pub(crate) fn prop_as_gid(value: &Prop) -> Option { } } -/// Wraps a compiled boolean filter whose only possible matches are the nodes -/// with the given ids: `domain` resolves them directly instead of scanning -/// every node. An id that does not exist simply resolves to nothing. +/// Wraps a compiled boolean filter whose only possible matches are `nodes`, the +/// nodes it names by id, resolved when the filter was built: `domain` hands them +/// over instead of scanning every node. #[derive(Clone)] pub struct IdDomainNodeOp<'g> { - pub(crate) gids: Arc<[GID]>, + pub(crate) nodes: NodeList, pub(crate) inner: Arc + 'g>, } @@ -588,14 +587,8 @@ impl<'g> NodeOp for IdDomainNodeOp<'g> { self.inner.apply(storage, node) } - fn domain(&self, storage: &GraphStorage) -> NodeList { - NodeList::List { - elems: self - .gids - .iter() - .filter_map(|gid| storage.internalise_node(gid.as_node_ref())) - .collect(), - } + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.nodes.clone() } fn const_value(&self) -> Option { From 9528fbe32512bfd1090ec76d9b91f9c641a6a331 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:00:52 +0100 Subject: [PATCH 135/155] tidy the expression engine: reverse last, fewer allocations, defaults PropArray iteration is double-ended so last() reads from the back. The list aggregation hands its closure a borrowed iterator instead of a collected Vec, and the client error message joins without one. The EntityAggOps methods are trait defaults. Removes a commented-out impl block and a stale turbofish in the docs. --- .../entities/properties/prop/prop_array.rs | 30 +++---- raphtory-graphql/src/client/error.rs | 7 +- raphtory/src/db/api/state/ops/mod.rs | 17 ---- .../src/db/graph/views/filter/model/mod.rs | 48 +---------- .../views/filter/model/node_expr/exprs.rs | 80 ++++++------------- .../graph/views/filter/model/node_expr/ops.rs | 2 +- .../filter/model/property_filter/evaluate.rs | 7 +- 7 files changed, 47 insertions(+), 144 deletions(-) diff --git a/raphtory-api/src/core/entities/properties/prop/prop_array.rs b/raphtory-api/src/core/entities/properties/prop/prop_array.rs index 1a98eccad5..55f75ac282 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_array.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_array.rs @@ -2,7 +2,7 @@ use crate::{ core::entities::properties::prop::{ unify_types, ArrowRow, DirectConvert, Prop, PropType, EMPTY_MAP_FIELD_NAME, }, - iter::{BoxedLIter, IntoDynBoxed}, + iter::{BoxedLDIter, IntoDynDBoxed}, }; use arrow_array::{ cast::AsArray, types::*, Array, ArrayRef, ArrowPrimitiveType, OffsetSizeTrait, PrimitiveArray, @@ -110,9 +110,9 @@ impl PropArray { self.iter_all().flatten() } - pub fn iter_all(&self) -> BoxedLIter<'_, Option> { + pub fn iter_all(&self) -> BoxedLDIter<'_, Option> { match self { - PropArray::Vec(ps) => ps.iter().cloned().map(Some).into_dyn_boxed(), + PropArray::Vec(ps) => ps.iter().cloned().map(Some).into_dyn_dboxed(), PropArray::Array(arr) => { let dtype = arr.data_type(); match dtype { @@ -120,7 +120,7 @@ impl PropArray { .as_boolean() .iter() .map(|p| p.map(Prop::Bool)) - .into_dyn_boxed(), + .into_dyn_dboxed(), DataType::Int32 => as_primitive_iter::(arr), DataType::Int64 => as_primitive_iter::(arr), DataType::UInt8 => as_primitive_iter::(arr), @@ -142,53 +142,53 @@ impl PropArray { DataType::Struct(_) => as_struct_iter(arr), DataType::List(_) => as_list_iter::(arr), DataType::LargeList(_) => as_list_iter::(arr), - _ => std::iter::empty().into_dyn_boxed(), + _ => std::iter::empty().into_dyn_dboxed(), } } } } } -fn as_primitive_iter(arr: &ArrayRef) -> BoxedLIter<'_, Option> { +fn as_primitive_iter(arr: &ArrayRef) -> BoxedLDIter<'_, Option> { arr.as_primitive_opt::() .into_iter() .flat_map(|primitive_array| { let dt = arr.data_type(); primitive_array.iter().map(|v| v.map(|v| TT::prop(v, dt))) }) - .into_dyn_boxed() + .into_dyn_dboxed() } -fn as_str_iter(arr: &ArrayRef) -> BoxedLIter<'_, Option> { +fn as_str_iter(arr: &ArrayRef) -> BoxedLDIter<'_, Option> { match arr.data_type() { DataType::Utf8 => arr .as_string::() .into_iter() .map(|opt_str| opt_str.map(|s| Prop::str(s.to_string()))) - .into_dyn_boxed(), + .into_dyn_dboxed(), DataType::LargeUtf8 => arr .as_string::() .into_iter() .map(|opt_str| opt_str.map(|s| Prop::str(s.to_string()))) - .into_dyn_boxed(), + .into_dyn_dboxed(), DataType::Utf8View => arr .as_string_view() .into_iter() .map(|opt_str| opt_str.map(|s| Prop::str(s.to_string()))) - .into_dyn_boxed(), + .into_dyn_dboxed(), _ => panic!("as_str_iter called on non-string array"), } } -fn as_struct_iter(arr: &ArrayRef) -> BoxedLIter<'_, Option> { +fn as_struct_iter(arr: &ArrayRef) -> BoxedLDIter<'_, Option> { let arr = arr.as_struct(); (0..arr.len()) .map(|row| (!arr.is_null(row)).then(|| ArrowRow::new(arr, row))) .map(|arrow_row| arrow_row.and_then(|row| row.into_prop())) - .into_dyn_boxed() + .into_dyn_dboxed() } -fn as_list_iter(arr: &ArrayRef) -> BoxedLIter<'_, Option> { +fn as_list_iter(arr: &ArrayRef) -> BoxedLDIter<'_, Option> { let arr = arr.as_list::(); (0..arr.len()) .map(|i| { @@ -200,7 +200,7 @@ fn as_list_iter(arr: &ArrayRef) -> BoxedLIter<'_, Option Client None => e.to_string(), }; let message = match errors { - JsonValue::Array(errors) => errors - .iter() - .map(error_text) - .collect::>() - .join("\n\t"), + JsonValue::Array(errors) => errors.iter().map(error_text).join("\n\t"), _ => error_text(errors), }; diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 4ba24058c3..67d9c3c6e6 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -47,23 +47,6 @@ pub trait NodeOp: Send + Sync { } } -// impl EntityExpr for T { -// type Marker = NodeFilter; -// -// fn entity(&self) -> Self::Marker { -// NodeFilter -// } -// } -// -// impl CreateOp for T { -// fn create_node_op<'g, G: GraphView + 'g>( -// &self, -// _graph: G, -// ) -> Result> + 'g>, GraphError> { -// Ok(Arc::new(self.clone())) -// } -// } - pub trait IntoArrowNodeOp: NodeOp + Sized { fn into_arrow_node_op>(self) -> ArrowMap { ArrowMap { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 7de94ced87..49a55953f0 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -332,54 +332,10 @@ impl PropertyExpr { /// Aggregators apply to the latest value of a property when it is list-valued; /// scalar values are rejected at filter-build time (`require_aggregable`). -impl EntityAggOps for PropertyExpr { - 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 EntityAggOps for PropertyExpr {} /// As for [`PropertyExpr`]: aggregation over a list-valued metadata field. -impl EntityAggOps for MetadataExpr { - 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 EntityAggOps for MetadataExpr {} // ───────────────────────────────────────────────────────────────────────────── // EdgeFilterFactory — marker for edge-side filter factory types 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 08c683b061..c2966fa8b1 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 @@ -11,7 +11,7 @@ //! ```rust,ignore //! NodeFilter.id() // Id — e.g. .eq(GID::Str("v1".into())) //! NodeFilter.name() // Name — e.g. .eq("Alice") -//! NodeFilter.node_type() // Type — e.g. .is_some::() +//! NodeFilter.node_type() // Type — e.g. .is_some() //! ``` //! //! # Degree expressions @@ -26,7 +26,7 @@ //! //! ```rust,ignore //! NodeFilter.property("age") // Property — e.g. .gt(30i64) -//! NodeFilter.property("score").is_some::() // nodes where "score" is set +//! NodeFilter.property("score").is_some() // nodes where "score" is set //! NodeFilter.metadata("region") // Metadata — e.g. .eq(Prop::Str("EU".into())) //! ``` //! @@ -511,29 +511,7 @@ impl EntityExpr for TemporalPropE impl PredicateLhs for TemporalPropExpr {} -impl EntityAggOps for TemporalPropExpr { - 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 EntityAggOps for TemporalPropExpr {} impl CreateOp for TemporalPropExpr { fn create_node_op<'g, G: GraphView + 'g>( @@ -587,13 +565,27 @@ impl CreateOp for Te // ───────────────────────────────────────────────────────────────────────────── pub trait EntityAggOps: EntityExpr + Sized { - fn sum(self) -> SumExpr; - fn avg(self) -> AvgExpr; - fn min(self) -> MinExpr; - fn max(self) -> MaxExpr; - fn first(self) -> FirstExpr; - fn last(self) -> LastExpr; - fn len(self) -> LenExpr; + 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) + } } macro_rules! impl_agg_expr { @@ -640,29 +632,7 @@ macro_rules! impl_agg_expr { impl PredicateLhs for $expr {} - impl EntityAggOps for $expr { - 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 EntityAggOps for $expr {} }; } 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 index 92fe480766..ad8a4ecbc0 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -457,7 +457,7 @@ impl_agg_entity_op!( |vals| { // Pick the last temporal entry as-is (whether scalar or list). match vals? { - Prop::List(x) => x.iter_all().filter_map(|v| v).last(), + Prop::List(x) => x.iter_all().rev().find_map(|v| v), _ => None, } } diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index f37ad78690..93c0d4535a 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -409,7 +409,7 @@ pub fn scan_f64_sum_count>( /// properties and applies `op` per element list. pub fn aggregate_list_values( vals: Option, - op: &dyn Fn(Box>) -> Option, + op: &dyn Fn(Box + '_>) -> Option, ) -> Option { match vals? { Prop::List(x) => match x.dtype() { @@ -421,10 +421,7 @@ pub fn aggregate_list_values( .collect(); Some(Prop::List(s)) } - _ => { - let items: Vec = x.iter().collect(); - op(Box::new(items.into_iter())) - } + _ => op(Box::new(x.iter())), }, _ => None, } From 958ccab10dbbebf269342cd97ddfbeac314f5494 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:11:10 +0100 Subject: [PATCH 136/155] aggregate endpoint reads through EntityAggOps Edge.src()/dst() reads take sum, avg, min, max, first, last and len from the same trait as node reads, so they work on any list-valued property instead of only a temporal one. Drops the pre-comparison any/all that nothing called. --- raphtory-tests/tests/test_filters.rs | 4 +- .../graph/views/filter/model/edge_filter.rs | 59 ++----------------- 2 files changed, 7 insertions(+), 56 deletions(-) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index fd1376a8d0..d1e83ca5b7 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -8089,8 +8089,8 @@ mod test_edge_filter { init_edges_graph_with_str_ids_del, init_nodes_graph, IdentityGraphTransformer, }; use raphtory::db::graph::views::filter::model::{ - edge_filter::EdgeFilter, ComposableFilter, EdgeViewFilterOps, EntityExprFilterOps, - NodeFilterFactory, PropertyExprFactory, ViewWrapOps, + edge_filter::EdgeFilter, ComposableFilter, EdgeViewFilterOps, EntityAggOps, + EntityExprFilterOps, PropertyExprFactory, ViewWrapOps, }; use raphtory_tests::assertions::{ assert_filter_edges_results, assert_select_edges_results, TestGraphVariants, TestVariants, 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 066723f153..c9f9bb4323 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -196,12 +196,9 @@ impl EdgeFilterFactory for SnapshotLatest type EdgeWindow = Windowed>; } -// ── expr layer: temporal chains on endpoint properties ── +// ── expr layer: temporal and aggregated reads on endpoint properties ── -use crate::db::graph::views::filter::model::node_expr::{ - AllExpr, AnyExpr, AvgExpr, EntityAggOps, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, - SumExpr, TemporalPropExpr, -}; +use crate::db::graph::views::filter::model::node_expr::{EntityAggOps, TemporalPropExpr}; impl EdgeEndpointWrapper> { #[inline] @@ -210,52 +207,6 @@ impl EdgeEndpointWrapper - EdgeEndpointWrapper> -{ - #[inline] - pub fn sum(self) -> EdgeEndpointWrapper>> { - let endpoint = self.endpoint; - EdgeEndpointWrapper::new(self.inner.sum(), endpoint) - } - #[inline] - pub fn avg(self) -> EdgeEndpointWrapper>> { - let endpoint = self.endpoint; - EdgeEndpointWrapper::new(self.inner.avg(), endpoint) - } - #[inline] - pub fn min(self) -> EdgeEndpointWrapper>> { - let endpoint = self.endpoint; - EdgeEndpointWrapper::new(self.inner.min(), endpoint) - } - #[inline] - pub fn max(self) -> EdgeEndpointWrapper>> { - let endpoint = self.endpoint; - EdgeEndpointWrapper::new(self.inner.max(), endpoint) - } - #[inline] - pub fn first(self) -> EdgeEndpointWrapper>> { - let endpoint = self.endpoint; - EdgeEndpointWrapper::new(self.inner.first(), endpoint) - } - #[inline] - pub fn last(self) -> EdgeEndpointWrapper>> { - let endpoint = self.endpoint; - EdgeEndpointWrapper::new(self.inner.last(), endpoint) - } - #[inline] - pub fn len(self) -> EdgeEndpointWrapper>> { - let endpoint = self.endpoint; - EdgeEndpointWrapper::new(self.inner.len(), endpoint) - } - #[inline] - pub fn any(self) -> EdgeEndpointWrapper>> { - let endpoint = self.endpoint; - EdgeEndpointWrapper::new(AnyExpr(self.inner), endpoint) - } - #[inline] - pub fn all(self) -> EdgeEndpointWrapper>> { - let endpoint = self.endpoint; - EdgeEndpointWrapper::new(AllExpr(self.inner), endpoint) - } -} +/// Aggregations on an endpoint read come from the same trait as on a node read, +/// so they apply to any list-valued property, temporal or not. +impl EntityAggOps for EdgeEndpointWrapper {} From bf813b46487e4851a100e828f0738562f61f1b5e Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:00:05 +0100 Subject: [PATCH 137/155] hand the filter tree over instead of cloning it in the remote client The Python wrapper is received by value at every remote filter and subscript site, so into_tree moves the tree out. The borrowing accessor had no other caller. --- raphtory-graphql/src/python/client/mod.rs | 4 ++-- raphtory-graphql/src/python/client/remote_edge.rs | 2 +- raphtory-graphql/src/python/client/remote_edges.rs | 4 ++-- raphtory-graphql/src/python/client/remote_graph.rs | 2 +- raphtory-graphql/src/python/client/remote_nested_edges.rs | 4 ++-- raphtory-graphql/src/python/client/remote_node.rs | 2 +- raphtory-graphql/src/python/client/remote_nodes.rs | 4 ++-- raphtory-graphql/src/python/client/remote_path_from_graph.rs | 4 ++-- raphtory-graphql/src/python/client/remote_path_from_node.rs | 4 ++-- raphtory/src/python/filter/filter_expr.rs | 5 +++-- 10 files changed, 18 insertions(+), 17 deletions(-) diff --git a/raphtory-graphql/src/python/client/mod.rs b/raphtory-graphql/src/python/client/mod.rs index 2f326d845c..b80b20f9b9 100644 --- a/raphtory-graphql/src/python/client/mod.rs +++ b/raphtory-graphql/src/python/client/mod.rs @@ -44,8 +44,8 @@ pub(crate) mod view_ops; /// local `Nodes.__getitem__` raises, so one `except` clause catches it on /// either backend — and at the same moment: locally the rejection happens at /// subscript time, not at first read. -pub(crate) fn node_subscript(filter: &PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); +pub(crate) fn node_subscript(filter: PyFilterExpr) -> PyResult { + let tree = filter.into_tree(); if tree.tests_edges() { return Err(adapt_err_value(&GraphError::NotNodeFilter)); } diff --git a/raphtory-graphql/src/python/client/remote_edge.rs b/raphtory-graphql/src/python/client/remote_edge.rs index 4a35d3f94a..a011c6ccb9 100644 --- a/raphtory-graphql/src/python/client/remote_edge.rs +++ b/raphtory-graphql/src/python/client/remote_edge.rs @@ -54,7 +54,7 @@ impl PyRemoteEdge { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemoteEdge::new(self.edge.filter(tree)?)) } diff --git a/raphtory-graphql/src/python/client/remote_edges.rs b/raphtory-graphql/src/python/client/remote_edges.rs index 6b07b35799..b45f1e503a 100644 --- a/raphtory-graphql/src/python/client/remote_edges.rs +++ b/raphtory-graphql/src/python/client/remote_edges.rs @@ -87,7 +87,7 @@ impl PyRemoteEdges { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemoteEdges::new(self.edges.filter(tree)?)) } @@ -106,7 +106,7 @@ impl PyRemoteEdges { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemoteEdges::new(self.edges.select(tree)?)) } diff --git a/raphtory-graphql/src/python/client/remote_graph.rs b/raphtory-graphql/src/python/client/remote_graph.rs index feb47d8b48..e548474398 100644 --- a/raphtory-graphql/src/python/client/remote_graph.rs +++ b/raphtory-graphql/src/python/client/remote_graph.rs @@ -71,7 +71,7 @@ impl PyRemoteGraph { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemoteGraph { graph: Arc::new(self.graph.filter(tree)?), }) diff --git a/raphtory-graphql/src/python/client/remote_nested_edges.rs b/raphtory-graphql/src/python/client/remote_nested_edges.rs index 82e5e264d4..85bc2c9148 100644 --- a/raphtory-graphql/src/python/client/remote_nested_edges.rs +++ b/raphtory-graphql/src/python/client/remote_nested_edges.rs @@ -60,7 +60,7 @@ impl PyRemoteNestedEdges { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemoteNestedEdges::new(self.edges.filter(tree)?)) } @@ -79,7 +79,7 @@ impl PyRemoteNestedEdges { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemoteNestedEdges::new(self.edges.select(tree)?)) } diff --git a/raphtory-graphql/src/python/client/remote_node.rs b/raphtory-graphql/src/python/client/remote_node.rs index 9a0b0c4c5c..3d595f9e47 100644 --- a/raphtory-graphql/src/python/client/remote_node.rs +++ b/raphtory-graphql/src/python/client/remote_node.rs @@ -58,7 +58,7 @@ impl PyRemoteNode { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemoteNode::new(self.node.filter(tree)?)) } diff --git a/raphtory-graphql/src/python/client/remote_nodes.rs b/raphtory-graphql/src/python/client/remote_nodes.rs index c98a75613d..f7b1b0915e 100644 --- a/raphtory-graphql/src/python/client/remote_nodes.rs +++ b/raphtory-graphql/src/python/client/remote_nodes.rs @@ -71,7 +71,7 @@ impl PyRemoteNodes { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemoteNodes::new(self.nodes.filter(tree)?)) } @@ -94,7 +94,7 @@ impl PyRemoteNodes { /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { Ok(PyRemoteNodes::new( - self.nodes.select(node_subscript(&filter)?)?, + self.nodes.select(node_subscript(filter)?)?, )) } diff --git a/raphtory-graphql/src/python/client/remote_path_from_graph.rs b/raphtory-graphql/src/python/client/remote_path_from_graph.rs index 22b1f4112d..e9fd23784b 100644 --- a/raphtory-graphql/src/python/client/remote_path_from_graph.rs +++ b/raphtory-graphql/src/python/client/remote_path_from_graph.rs @@ -61,7 +61,7 @@ impl PyRemotePathFromGraph { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemotePathFromGraph::new(self.path.filter(tree)?)) } @@ -84,7 +84,7 @@ impl PyRemotePathFromGraph { /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { Ok(PyRemotePathFromGraph::new( - self.path.select(node_subscript(&filter)?)?, + self.path.select(node_subscript(filter)?)?, )) } diff --git a/raphtory-graphql/src/python/client/remote_path_from_node.rs b/raphtory-graphql/src/python/client/remote_path_from_node.rs index 6d7755aea2..fdd7142802 100644 --- a/raphtory-graphql/src/python/client/remote_path_from_node.rs +++ b/raphtory-graphql/src/python/client/remote_path_from_node.rs @@ -59,7 +59,7 @@ impl PyRemotePathFromNode { /// ValueError: if the filter has no server-side form because it reads /// in-process state (`by_state_column`). pub fn filter(&self, filter: PyFilterExpr) -> PyResult { - let tree = filter.tree().clone(); + let tree = filter.into_tree(); Ok(PyRemotePathFromNode::new(self.path.filter(tree)?)) } @@ -82,7 +82,7 @@ impl PyRemotePathFromNode { /// in-process state (`by_state_column`). fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { Ok(PyRemotePathFromNode::new( - self.path.select(node_subscript(&filter)?)?, + self.path.select(node_subscript(filter)?)?, )) } diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index e454bd2a6b..0055d20a74 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -28,8 +28,9 @@ use std::sync::Arc; pub struct PyFilterExpr(pub FilterExpr); impl PyFilterExpr { - pub fn tree(&self) -> &FilterExpr { - &self.0 + /// The tree itself, for a caller that owns the wrapper and is done with it. + pub fn into_tree(self) -> FilterExpr { + self.0 } } From f7dc7919e0f285443921cce6334f612971703e50 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:11:41 +0100 Subject: [PATCH 138/155] print the offending value plainly in the comparison error The message names the value with Display instead of its Rust debug form. The schema descriptions call the input a filter expression, the edge expression tests join filter_tests, and the parity test for expression right-hand sides drops its fixture guard. --- .../test_base_install/test_filters/test_edge_filter.py | 4 ++-- .../test_base_install/test_filters/test_node_filter.py | 4 ++-- .../test_graphql/parity/test_parity_filters.py | 9 +-------- .../test_filters/test_graph_edges_property_filter.py | 10 +++++----- .../test_filters/test_graph_nodes_property_filter.py | 10 +++++----- .../test_graphql/test_filters/test_node_filter_gql.py | 4 ++-- .../test_filters/test_nodes_property_filter.py | 10 +++++----- raphtory-graphql/schema.graphql | 10 +++++----- raphtory-graphql/src/model/graph/filtering.rs | 10 +++++----- .../tests/{ => filter_tests}/edge_expr_filter.rs | 0 raphtory-tests/tests/filter_tests/mod.rs | 1 + raphtory/src/db/graph/views/filter/model/mod.rs | 2 +- 12 files changed, 34 insertions(+), 40 deletions(-) rename raphtory-tests/tests/{ => filter_tests}/edge_expr_filter.rs (100%) diff --git a/python/tests/test_base_install/test_filters/test_edge_filter.py b/python/tests/test_base_install/test_filters/test_edge_filter.py index 777f7b7813..aecc1016d2 100644 --- a/python/tests/test_base_install/test_filters/test_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_filter.py @@ -502,7 +502,7 @@ def check(graph): filter_expr = filter.Edge.src().id() == 3 with pytest.raises( Exception, - match=r"Invalid filter: value I64\(3\) of type I64 cannot be compared with Str", + match=r"Invalid filter: value 3 of type I64 cannot be compared with Str", ): graph.filter(filter_expr).nodes.id @@ -515,7 +515,7 @@ def check(graph): filter_expr = filter.Edge.src().id() == "3" with pytest.raises( Exception, - match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be compared with U64', + match=r'value 3 of type Str cannot be compared with U64', ): graph.filter(filter_expr).nodes.id diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index 230c0ab386..3a905d69b5 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -775,7 +775,7 @@ def check(graph): filter_expr = filter.Node.id() == 3 with pytest.raises( Exception, - match=r"Invalid filter: value I64\(3\) of type I64 cannot be compared with Str", + match=r"Invalid filter: value 3 of type I64 cannot be compared with Str", ): graph.filter(filter_expr).nodes.id @@ -788,7 +788,7 @@ def check(graph): filter_expr = filter.Node.id() == "3" with pytest.raises( Exception, - match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be compared with U64', + match=r'value 3 of type Str cannot be compared with U64', ): graph.filter(filter_expr).nodes.id diff --git a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index f1e0ec7177..8b5c9d51f0 100644 --- a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py +++ b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py @@ -1127,16 +1127,9 @@ def test_rejected_expr_parity_at_nodes_filter(filter_pair, name): def test_expression_rhs_agrees_on_both_sides(filter_pair, site): """`degree() > in_degree()` has no constant on the right, which the old wire grammar could not say. It is a tree now, so it runs remotely and must - give the local answer. The local side is asserted to differ from a filter - every node passes, so what is compared is a real filter.""" + give the local answer.""" read = EXPR_RHS_SITES[site] expr = f.Node.degree() > f.Node.in_degree() - everything = f.Node.degree() >= 0 - - local = read(filter_pair.local, expr) - assert local != read( - filter_pair.local, everything - ), f"{site}: the expression narrows nothing" assert_parity(filter_pair, lambda g: read(g, expr)) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py index 7988181c98..0296d25738 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py @@ -139,7 +139,7 @@ def test_graph_edge_property_filter_not_equal_type_error(graph): } """ expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be compared with Bool" + "Invalid filter: value 1 of type I64 cannot be compared with Bool" ) run_graphql_error_test(query, expected_error_message, graph) @@ -202,7 +202,7 @@ def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -269,7 +269,7 @@ def test_graph_edge_property_filter_less_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -331,7 +331,7 @@ def test_graph_edge_property_filter_greater_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -393,7 +393,7 @@ def test_graph_edge_property_filter_less_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index 9b0c08829d..f9b57dfb8c 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -124,7 +124,7 @@ def test_graph_node_property_filter_not_equal_type_error(graph): } """ expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be compared with Bool" + "Invalid filter: value 1 of type I64 cannot be compared with Bool" ) run_graphql_error_test(query, expected_error_message, graph) @@ -181,7 +181,7 @@ def test_graph_node_property_filter_greater_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -243,7 +243,7 @@ def test_graph_node_property_filter_less_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -299,7 +299,7 @@ def test_graph_node_property_filter_greater_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -357,7 +357,7 @@ def test_graph_node_property_filter_less_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py index fd8051ef1a..9d4458a983 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py @@ -92,7 +92,7 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql2(graph): } """ expected_error_message = ( - "Invalid filter: value U64(1) of type U64 cannot be compared with Str" + "Invalid filter: value 1 of type U64 cannot be compared with Str" ) run_graphql_error_test(query, expected_error_message, graph) @@ -632,7 +632,7 @@ def test_filter_nodes_degree_string_constants_gql(graph): """ run_graphql_error_test( query, - 'Invalid filter: value Str(ArcStr("4")) of type Str cannot be compared with U64', + 'Invalid filter: value 4 of type Str cannot be compared with U64', graph, ) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py index a2369ec552..74dc3918a7 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py @@ -208,7 +208,7 @@ def test_node_property_filter_not_equal_type_error(graph): } """ expected_error_message = ( - "Invalid filter: value I64(1) of type I64 cannot be compared with Bool" + "Invalid filter: value 1 of type I64 cannot be compared with Bool" ) run_graphql_error_test(query, expected_error_message, graph) @@ -275,7 +275,7 @@ def test_node_property_filter_greater_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -343,7 +343,7 @@ def test_node_property_filter_less_than_or_equal_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -405,7 +405,7 @@ def test_node_property_filter_greater_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) @@ -469,7 +469,7 @@ def test_node_property_filter_less_than_type_error(graph): } } """ - expected_error_message = 'Invalid filter: value Str(ArcStr("shivam")) of type Str cannot be compared with I64' + expected_error_message = 'Invalid filter: value shivam of type Str cannot be compared with I64' run_graphql_error_test(query, expected_error_message, graph) diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index ed4f855af6..28012e1cc9 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -1269,7 +1269,7 @@ input EdgeViewCollection @oneOf { """ shrinkEnd: TimeInput """ - A filter tree; the entity it tests is written in the tree. + A filter expression; the entity it tests is written in the expression. """ filter: FilterExpr } @@ -1694,7 +1694,7 @@ input EdgesViewCollection @oneOf { """ shrinkEnd: TimeInput """ - A filter tree; the entity it tests is written in the tree. + A filter expression; the entity it tests is written in the expression. """ filter: FilterExpr } @@ -2451,7 +2451,7 @@ input GraphViewCollection @oneOf { """ shrinkEnd: TimeInput """ - A filter tree; the entity it tests is written in the tree. + A filter expression; the entity it tests is written in the expression. """ filter: FilterExpr } @@ -5052,7 +5052,7 @@ input NodeViewCollection @oneOf { """ shrinkEnd: TimeInput """ - A filter tree; the entity it tests is written in the tree. + A filter expression; the entity it tests is written in the expression. """ filter: FilterExpr } @@ -5505,7 +5505,7 @@ input NodesViewCollection @oneOf { """ shrinkEnd: TimeInput """ - A filter tree; the entity it tests is written in the tree. + A filter expression; the entity it tests is written in the expression. """ filter: FilterExpr """ diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 0873c79e18..55d6d76dae 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -64,7 +64,7 @@ pub enum GraphViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// A filter tree; the entity it tests is written in the tree. + /// A filter expression; the entity it tests is written in the expression. Filter(GqlFilter), } @@ -96,7 +96,7 @@ pub enum NodesViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// A filter tree; the entity it tests is written in the tree. + /// A filter expression; the entity it tests is written in the expression. Filter(GqlFilter), /// List of types. TypeFilter(Vec), @@ -130,7 +130,7 @@ pub enum NodeViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// A filter tree; the entity it tests is written in the tree. + /// A filter expression; the entity it tests is written in the expression. Filter(GqlFilter), } @@ -162,7 +162,7 @@ pub enum EdgesViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// A filter tree; the entity it tests is written in the tree. + /// A filter expression; the entity it tests is written in the expression. Filter(GqlFilter), } @@ -194,7 +194,7 @@ pub enum EdgeViewCollection { ShrinkStart(GqlTimeInput), /// Set the window end to a specified time. ShrinkEnd(GqlTimeInput), - /// A filter tree; the entity it tests is written in the tree. + /// A filter expression; the entity it tests is written in the expression. Filter(GqlFilter), } diff --git a/raphtory-tests/tests/edge_expr_filter.rs b/raphtory-tests/tests/filter_tests/edge_expr_filter.rs similarity index 100% rename from raphtory-tests/tests/edge_expr_filter.rs rename to raphtory-tests/tests/filter_tests/edge_expr_filter.rs diff --git a/raphtory-tests/tests/filter_tests/mod.rs b/raphtory-tests/tests/filter_tests/mod.rs index 4f55652379..aec87ae09b 100644 --- a/raphtory-tests/tests/filter_tests/mod.rs +++ b/raphtory-tests/tests/filter_tests/mod.rs @@ -1,6 +1,7 @@ use raphtory::{db::api::view::StaticGraphViewOps, prelude::*}; mod cached_view; +mod edge_expr_filter; mod edge_property_filter; mod exploded_edge_property_filter; mod node_property_filter; diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 49a55953f0..e8ca542a03 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -916,7 +916,7 @@ pub fn validate_const_comparable( match value { Some(v) if !lhs_pt.is_comparable_with(&v.dtype()) => { Err(GraphError::InvalidFilter(format!( - "value {:?} of type {} cannot be compared with {}", + "value {} of type {} cannot be compared with {}", v, v.dtype(), lhs_pt From 9eceb4130f0792fe35b713c485f52207ef4011b5 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:17:08 +0100 Subject: [PATCH 139/155] name the two ways a grant's row filter can fail to apply access_filter_applies returns an AccessFilterError: DoesNotApply when the filter does not fit the graph, Load when the graph could not be read, instead of a Result nested inside a Result. --- raphtory-graphql/src/data.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/raphtory-graphql/src/data.rs b/raphtory-graphql/src/data.rs index 79eba965e4..6c5ebfabcf 100644 --- a/raphtory-graphql/src/data.rs +++ b/raphtory-graphql/src/data.rs @@ -375,10 +375,9 @@ impl Data { .await } - /// Whether `filter` can be applied to the graph at `path`. `Ok(Err(_))` says the - /// filter itself does not fit that graph (a value of the wrong type for a property, - /// say); `Err(_)` says the graph could not be loaded. Policies use it to tell a - /// per-caller value that cannot be compared from a grant that is wrong. + /// Whether `filter` can be applied to the graph at `path`. Policies use it to + /// tell a per-caller value that cannot be compared from a grant that is wrong: + /// the error says which of the two it was. /// /// # ⚠ Does no permission check — the caller must already have authorised `path`. /// Loading and error reporting here would otherwise reveal whether a graph exists. @@ -386,15 +385,18 @@ impl Data { &self, path: &str, filter: &GqlFilter, - ) -> Result, GQLError> { + ) -> Result<(), AccessFilterError> { let graph = self .get_graph_unchecked(path) - .await? + .await + .map_err(AccessFilterError::Load)? .graph() .clone() .into_dynamic(); let filter = filter.clone(); - Ok(blocking_compute(move || compile_row_filter(graph, filter).map(|_| ())).await) + blocking_compute(move || compile_row_filter(graph, filter).map(|_| ())) + .await + .map_err(AccessFilterError::DoesNotApply) } /// Test-only: direct graph load without permission checks. @@ -945,6 +947,17 @@ fn apply_row_filter_sync( /// /// The filter means what it means everywhere else: `and` is an intersection, and a /// predicate that should be evaluated inside a view carries that view on its read. +/// Why a grant's row filter cannot be applied to a graph. +#[derive(thiserror::Error, Debug)] +pub enum AccessFilterError { + /// The filter does not fit the graph: a value of the wrong type for a property, say. + #[error("the filter does not fit the graph: {0}")] + DoesNotApply(#[source] GraphError), + /// The graph could not be loaded. + #[error(transparent)] + Load(GQLError), +} + fn compile_row_filter(graph: DynamicGraph, filter: GqlFilter) -> Result { Ok(graph.filter(filter)?.into_dynamic()) } From 34ca6af0b4f0fa1c1347d5bff09565e2b5488bad Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:23:43 +0100 Subject: [PATCH 140/155] one ViewCollection input for nodes, edges and paths The per-entity copies only differed in the filter type they carried, and there is one filter type now. PathFromNode and PathFromGraph gain the defaultLayer and filter entries the other collections had. The graph and nodes inputs keep their own lists for the entries only they take. --- raphtory-graphql/schema.graphql | 303 ++++-------------- raphtory-graphql/src/model/graph/edge.rs | 34 +- raphtory-graphql/src/model/graph/edges.rs | 34 +- raphtory-graphql/src/model/graph/filtering.rs | 96 +----- .../src/model/graph/nested_edges.rs | 34 +- raphtory-graphql/src/model/graph/node.rs | 34 +- .../src/model/graph/path_from_graph.rs | 42 +-- .../src/model/graph/path_from_node.rs | 42 +-- 8 files changed, 180 insertions(+), 439 deletions(-) diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 28012e1cc9..4c80d82ed8 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -853,7 +853,7 @@ type Edge { """ Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. """ - views: [EdgeViewCollection!]! + views: [ViewCollection!]! ): Edge! """ Returns the earliest time of an edge. @@ -1215,65 +1215,6 @@ input EdgeSortBy { property: String } -input EdgeViewCollection @oneOf { - """ - Contains only the default layer. - """ - defaultLayer: Boolean - """ - Latest time. - """ - latest: Boolean - """ - Snapshot at latest time. - """ - snapshotLatest: Boolean - """ - Snapshot at specified time. - """ - snapshotAt: TimeInput - """ - List of included layers. - """ - layers: [String!] - """ - List of excluded layers. - """ - excludeLayers: [String!] - """ - Single excluded layer. - """ - excludeLayer: String - """ - Window between a start and end time. - """ - window: Window - """ - View at a specified time. - """ - at: TimeInput - """ - View before a specified time (end exclusive). - """ - before: TimeInput - """ - View after a specified time (start exclusive). - """ - after: TimeInput - """ - Set the window start to a specified time. - """ - shrinkStart: TimeInput - """ - Set the window end to a specified time. - """ - shrinkEnd: TimeInput - """ - A filter expression; the entity it tests is written in the expression. - """ - filter: FilterExpr -} - """ Views applied to every read inside `expr`, in list order. """ @@ -1493,7 +1434,7 @@ type Edges { """ Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. """ - views: [EdgesViewCollection!]! + views: [ViewCollection!]! ): Edges! """ Expand each edge into one edge per update: if `A->B` has three updates, it @@ -1640,65 +1581,6 @@ type Edges { ): Edges! } -input EdgesViewCollection @oneOf { - """ - Contains only the default layer. - """ - defaultLayer: Boolean - """ - Latest time. - """ - latest: Boolean - """ - Snapshot at latest time. - """ - snapshotLatest: Boolean - """ - Snapshot at specified time. - """ - snapshotAt: TimeInput - """ - List of included layers. - """ - layers: [String!] - """ - List of excluded layers. - """ - excludeLayers: [String!] - """ - Single excluded layer. - """ - excludeLayer: String - """ - Window between a start and end time. - """ - window: Window - """ - View at a specified time. - """ - at: TimeInput - """ - View before a specified time (end exclusive). - """ - before: TimeInput - """ - View after a specified time (start exclusive). - """ - after: TimeInput - """ - Set the window start to a specified time. - """ - shrinkStart: TimeInput - """ - Set the window end to a specified time. - """ - shrinkEnd: TimeInput - """ - A filter expression; the entity it tests is written in the expression. - """ - filter: FilterExpr -} - """ A lazy sequence of per-window edge collections, produced by `edges.rolling` / `edges.expanding`. Each entry is an `Edges` collection @@ -4111,7 +3993,7 @@ type NestedEdges { """ Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. """ - views: [EdgesViewCollection!]! + views: [ViewCollection!]! ): NestedEdges! """ Returns the earliest time that this NestedEdges is valid or None if valid for all times. @@ -4416,7 +4298,7 @@ type Node { """ end: TimeInput! ): Node! - applyViews(views: [NodeViewCollection!]!): Node! + applyViews(views: [ViewCollection!]!): Node! """ Returns the earliest time that the node exists. """ @@ -4998,65 +4880,6 @@ node, or a collection of nodes. """ union NodeStateValue = NodeStateProp | Node | Nodes -input NodeViewCollection @oneOf { - """ - Contains only the default layer. - """ - defaultLayer: Boolean - """ - View at the latest time. - """ - latest: Boolean - """ - Snapshot at latest time. - """ - snapshotLatest: Boolean - """ - Snapshot at specified time. - """ - snapshotAt: TimeInput - """ - List of included layers. - """ - layers: [String!] - """ - List of excluded layers. - """ - excludeLayers: [String!] - """ - Single excluded layer. - """ - excludeLayer: String - """ - Window between a start and end time. - """ - window: Window - """ - View at a specified time. - """ - at: TimeInput - """ - View before a specified time (end exclusive). - """ - before: TimeInput - """ - View after a specified time (start exclusive). - """ - after: TimeInput - """ - Set the window start to a specified time. - """ - shrinkStart: TimeInput - """ - Set the window end to a specified time. - """ - shrinkEnd: TimeInput - """ - A filter expression; the entity it tests is written in the expression. - """ - filter: FilterExpr -} - """ Views applied to every read inside `expr`, in list order. """ @@ -5804,7 +5627,7 @@ type PathFromGraph { """ Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. """ - views: [PathFromNodeViewCollection!]! + views: [ViewCollection!]! ): PathFromGraph! """ Narrow the neighbour set to nodes matching `expr`. The filter sticks to @@ -6127,7 +5950,7 @@ type PathFromNode { """ Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. """ - views: [PathFromNodeViewCollection!]! + views: [ViewCollection!]! ): PathFromNode! """ Narrow the neighbour set to nodes matching `expr`. The filter sticks to @@ -6206,57 +6029,6 @@ type PathFromNode { outEdges: Edges! } -input PathFromNodeViewCollection @oneOf { - """ - Latest time. - """ - latest: Boolean - """ - Latest snapshot. - """ - snapshotLatest: Boolean - """ - Time. - """ - snapshotAt: TimeInput - """ - List of layers. - """ - layers: [String!] - """ - List of excluded layers. - """ - excludeLayers: [String!] - """ - Single layer to exclude. - """ - excludeLayer: String - """ - Window between a start and end time. - """ - window: Window - """ - View at a specified time. - """ - at: TimeInput - """ - View before a specified time (end exclusive). - """ - before: TimeInput - """ - View after a specified time (start exclusive). - """ - after: TimeInput - """ - Set the window start to a specified time. - """ - shrinkStart: TimeInput - """ - Set the window end to a specified time. - """ - shrinkEnd: TimeInput -} - """ A lazy sequence of per-window neighbour sets, produced by `neighbours.rolling` / `neighbours.expanding` (or the in/out variants). @@ -7210,6 +6982,69 @@ input VectorisedGraphWindow { end: TimeInput! } +""" +A view operation on a node, an edge or a collection of either; the +same list for all of them now that one filter type serves every entity. +""" +input ViewCollection @oneOf { + """ + Contains only the default layer. + """ + defaultLayer: Boolean + """ + View at the latest time. + """ + latest: Boolean + """ + Snapshot at latest time. + """ + snapshotLatest: Boolean + """ + Snapshot at specified time. + """ + snapshotAt: TimeInput + """ + List of included layers. + """ + layers: [String!] + """ + List of excluded layers. + """ + excludeLayers: [String!] + """ + Single excluded layer. + """ + excludeLayer: String + """ + Window between a start and end time. + """ + window: Window + """ + View at a specified time. + """ + at: TimeInput + """ + View before a specified time (end exclusive). + """ + before: TimeInput + """ + View after a specified time (start exclusive). + """ + after: TimeInput + """ + Set the window start to a specified time. + """ + shrinkStart: TimeInput + """ + Set the window end to a specified time. + """ + shrinkEnd: TimeInput + """ + A filter expression; the entity it tests is written in the expression. + """ + filter: FilterExpr +} + """ One view restriction, applied in list order. """ diff --git a/raphtory-graphql/src/model/graph/edge.rs b/raphtory-graphql/src/model/graph/edge.rs index 7501115c0c..2f371d2184 100644 --- a/raphtory-graphql/src/model/graph/edge.rs +++ b/raphtory-graphql/src/model/graph/edge.rs @@ -2,7 +2,7 @@ use crate::{ model::graph::{ edges::GqlEdges, filter_expr_input::GqlFilter, - filtering::EdgeViewCollection, + filtering::ViewCollection, history::GqlHistory, node::GqlNode, node_id::GqlNodeId, @@ -262,49 +262,47 @@ impl GqlEdge { #[graphql( desc = "Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result." )] - views: Vec, + views: Vec, ) -> Result { let mut return_view: GqlEdge = self.ee.clone().into(); for view in views { return_view = match view { - EdgeViewCollection::DefaultLayer(apply) => { + ViewCollection::DefaultLayer(apply) => { if apply { return_view.default_layer().await } else { return_view } } - EdgeViewCollection::Layers(layers) => return_view.layers(layers).await, + ViewCollection::Layers(layers) => return_view.layers(layers).await, - EdgeViewCollection::ExcludeLayers(layers) => { - return_view.exclude_layers(layers).await - } - EdgeViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, + ViewCollection::ExcludeLayers(layers) => return_view.exclude_layers(layers).await, + ViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, - EdgeViewCollection::Latest(apply) => { + ViewCollection::Latest(apply) => { if apply { return_view.latest().await } else { return_view } } - EdgeViewCollection::SnapshotLatest(apply) => { + ViewCollection::SnapshotLatest(apply) => { if apply { return_view.snapshot_latest().await } else { return_view } } - EdgeViewCollection::SnapshotAt(at) => return_view.snapshot_at(at).await, - EdgeViewCollection::Window(window) => { + ViewCollection::SnapshotAt(at) => return_view.snapshot_at(at).await, + ViewCollection::Window(window) => { return_view.window(window.start, window.end).await } - EdgeViewCollection::At(at) => return_view.at(at).await, - EdgeViewCollection::Before(time) => return_view.before(time).await, - EdgeViewCollection::After(time) => return_view.after(time).await, - EdgeViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, - EdgeViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - EdgeViewCollection::Filter(filter) => return_view.filter(filter).await?, + ViewCollection::At(at) => return_view.at(at).await, + ViewCollection::Before(time) => return_view.before(time).await, + ViewCollection::After(time) => return_view.after(time).await, + ViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, + ViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, + ViewCollection::Filter(filter) => return_view.filter(filter).await?, } } Ok(return_view) diff --git a/raphtory-graphql/src/model/graph/edges.rs b/raphtory-graphql/src/model/graph/edges.rs index 740aca5ca1..89774f9108 100644 --- a/raphtory-graphql/src/model/graph/edges.rs +++ b/raphtory-graphql/src/model/graph/edges.rs @@ -4,7 +4,7 @@ use crate::{ collection::{check_list_allowed, check_page_limit}, edge::GqlEdge, filter_expr_input::GqlFilter, - filtering::EdgesViewCollection, + filtering::ViewCollection, path_from_node::GqlPathFromNode, timeindex::{GqlEventTime, GqlTimeInput}, windowset::GqlEdgesWindowSet, @@ -250,47 +250,45 @@ impl GqlEdges { #[graphql( desc = "Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result." )] - views: Vec, + views: Vec, ) -> Result { let mut return_view: GqlEdges = self.update(self.ee.clone()); for view in views { return_view = match view { - EdgesViewCollection::DefaultLayer(apply) => { + ViewCollection::DefaultLayer(apply) => { if apply { return_view.default_layer().await } else { return_view } } - EdgesViewCollection::Latest(apply) => { + ViewCollection::Latest(apply) => { if apply { return_view.latest().await } else { return_view } } - EdgesViewCollection::SnapshotLatest(apply) => { + ViewCollection::SnapshotLatest(apply) => { if apply { return_view.snapshot_latest().await } else { return_view } } - EdgesViewCollection::SnapshotAt(at) => return_view.snapshot_at(at).await, - EdgesViewCollection::Layers(layers) => return_view.layers(layers).await, - EdgesViewCollection::ExcludeLayers(layers) => { - return_view.exclude_layers(layers).await - } - EdgesViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, - EdgesViewCollection::Window(window) => { + ViewCollection::SnapshotAt(at) => return_view.snapshot_at(at).await, + ViewCollection::Layers(layers) => return_view.layers(layers).await, + ViewCollection::ExcludeLayers(layers) => return_view.exclude_layers(layers).await, + ViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, + ViewCollection::Window(window) => { return_view.window(window.start, window.end).await } - EdgesViewCollection::At(at) => return_view.at(at).await, - EdgesViewCollection::Before(time) => return_view.before(time).await, - EdgesViewCollection::After(time) => return_view.after(time).await, - EdgesViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, - EdgesViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - EdgesViewCollection::Filter(filter) => return_view.filter(filter).await?, + ViewCollection::At(at) => return_view.at(at).await, + ViewCollection::Before(time) => return_view.before(time).await, + ViewCollection::After(time) => return_view.after(time).await, + ViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, + ViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, + ViewCollection::Filter(filter) => return_view.filter(filter).await?, } } diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 55d6d76dae..55958d4058 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -102,8 +102,10 @@ pub enum NodesViewCollection { TypeFilter(Vec), } +/// A view operation on a node, an edge or a collection of either; the +/// same list for all of them now that one filter type serves every entity. #[derive(OneOfInput, Clone, Debug)] -pub enum NodeViewCollection { +pub enum ViewCollection { /// Contains only the default layer. DefaultLayer(bool), /// View at the latest time. @@ -134,98 +136,6 @@ pub enum NodeViewCollection { Filter(GqlFilter), } -#[derive(OneOfInput, Clone, Debug)] -pub enum EdgesViewCollection { - /// Contains only the default layer. - DefaultLayer(bool), - /// Latest time. - Latest(bool), - /// Snapshot at latest time. - SnapshotLatest(bool), - /// Snapshot at specified time. - SnapshotAt(GqlTimeInput), - /// List of included layers. - Layers(Vec), - /// List of excluded layers. - ExcludeLayers(Vec), - /// Single excluded layer. - ExcludeLayer(String), - /// Window between a start and end time. - Window(Window), - /// View at a specified time. - At(GqlTimeInput), - /// View before a specified time (end exclusive). - Before(GqlTimeInput), - /// View after a specified time (start exclusive). - After(GqlTimeInput), - /// Set the window start to a specified time. - ShrinkStart(GqlTimeInput), - /// Set the window end to a specified time. - ShrinkEnd(GqlTimeInput), - /// A filter expression; the entity it tests is written in the expression. - Filter(GqlFilter), -} - -#[derive(OneOfInput, Clone, Debug)] -pub enum EdgeViewCollection { - /// Contains only the default layer. - DefaultLayer(bool), - /// Latest time. - Latest(bool), - /// Snapshot at latest time. - SnapshotLatest(bool), - /// Snapshot at specified time. - SnapshotAt(GqlTimeInput), - /// List of included layers. - Layers(Vec), - /// List of excluded layers. - ExcludeLayers(Vec), - /// Single excluded layer. - ExcludeLayer(String), - /// Window between a start and end time. - Window(Window), - /// View at a specified time. - At(GqlTimeInput), - /// View before a specified time (end exclusive). - Before(GqlTimeInput), - /// View after a specified time (start exclusive). - After(GqlTimeInput), - /// Set the window start to a specified time. - ShrinkStart(GqlTimeInput), - /// Set the window end to a specified time. - ShrinkEnd(GqlTimeInput), - /// A filter expression; the entity it tests is written in the expression. - Filter(GqlFilter), -} - -#[derive(OneOfInput, Clone, Debug)] -pub enum PathFromNodeViewCollection { - /// Latest time. - Latest(bool), - /// Latest snapshot. - SnapshotLatest(bool), - /// Time. - SnapshotAt(GqlTimeInput), - /// List of layers. - Layers(Vec), - /// List of excluded layers. - ExcludeLayers(Vec), - /// Single layer to exclude. - ExcludeLayer(String), - /// Window between a start and end time. - Window(Window), - /// View at a specified time. - At(GqlTimeInput), - /// View before a specified time (end exclusive). - Before(GqlTimeInput), - /// View after a specified time (start exclusive). - After(GqlTimeInput), - /// Set the window start to a specified time. - ShrinkStart(GqlTimeInput), - /// Set the window end to a specified time. - ShrinkEnd(GqlTimeInput), -} - /// Boolean expression over a property value. /// /// `PropCondition` is the `where` of a namespace metagraph filter: how one diff --git a/raphtory-graphql/src/model/graph/nested_edges.rs b/raphtory-graphql/src/model/graph/nested_edges.rs index 7efa031af1..97e8b89313 100644 --- a/raphtory-graphql/src/model/graph/nested_edges.rs +++ b/raphtory-graphql/src/model/graph/nested_edges.rs @@ -3,7 +3,7 @@ use crate::{ collection::{check_list_allowed, check_page_limit}, edges::GqlEdges, filter_expr_input::GqlFilter, - filtering::EdgesViewCollection, + filtering::ViewCollection, path_from_graph::GqlPathFromGraph, timeindex::{GqlEventTime, GqlTimeInput}, }, @@ -187,47 +187,45 @@ impl GqlNestedEdges { #[graphql( desc = "Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result." )] - views: Vec, + views: Vec, ) -> Result { let mut return_view: GqlNestedEdges = self.update(self.edges.clone()); for view in views { return_view = match view { - EdgesViewCollection::DefaultLayer(apply) => { + ViewCollection::DefaultLayer(apply) => { if apply { return_view.default_layer().await } else { return_view } } - EdgesViewCollection::Latest(apply) => { + ViewCollection::Latest(apply) => { if apply { return_view.latest().await } else { return_view } } - EdgesViewCollection::SnapshotLatest(apply) => { + ViewCollection::SnapshotLatest(apply) => { if apply { return_view.snapshot_latest().await } else { return_view } } - EdgesViewCollection::SnapshotAt(at) => return_view.snapshot_at(at).await, - EdgesViewCollection::Layers(layers) => return_view.layers(layers).await, - EdgesViewCollection::ExcludeLayers(layers) => { - return_view.exclude_layers(layers).await - } - EdgesViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, - EdgesViewCollection::Window(window) => { + ViewCollection::SnapshotAt(at) => return_view.snapshot_at(at).await, + ViewCollection::Layers(layers) => return_view.layers(layers).await, + ViewCollection::ExcludeLayers(layers) => return_view.exclude_layers(layers).await, + ViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, + ViewCollection::Window(window) => { return_view.window(window.start, window.end).await } - EdgesViewCollection::At(at) => return_view.at(at).await, - EdgesViewCollection::Before(time) => return_view.before(time).await, - EdgesViewCollection::After(time) => return_view.after(time).await, - EdgesViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, - EdgesViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - EdgesViewCollection::Filter(filter) => return_view.filter(filter).await?, + ViewCollection::At(at) => return_view.at(at).await, + ViewCollection::Before(time) => return_view.before(time).await, + ViewCollection::After(time) => return_view.after(time).await, + ViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, + ViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, + ViewCollection::Filter(filter) => return_view.filter(filter).await?, } } diff --git a/raphtory-graphql/src/model/graph/node.rs b/raphtory-graphql/src/model/graph/node.rs index fcb30bde3a..20edf3633b 100644 --- a/raphtory-graphql/src/model/graph/node.rs +++ b/raphtory-graphql/src/model/graph/node.rs @@ -2,7 +2,7 @@ use crate::{ model::graph::{ edges::GqlEdges, filter_expr_input::GqlFilter, - filtering::NodeViewCollection, + filtering::ViewCollection, history::GqlHistory, node_id::GqlNodeId, nodes::GqlNodes, @@ -246,46 +246,44 @@ impl GqlNode { self.vv.shrink_end(end.into_time()).into() } - pub async fn apply_views(&self, views: Vec) -> Result { + pub async fn apply_views(&self, views: Vec) -> Result { let mut return_view: GqlNode = self.vv.clone().into(); for view in views { return_view = match view { - NodeViewCollection::DefaultLayer(apply) => { + ViewCollection::DefaultLayer(apply) => { if apply { return_view.default_layer().await } else { return_view } } - NodeViewCollection::Latest(apply) => { + ViewCollection::Latest(apply) => { if apply { return_view.latest().await } else { return_view } } - NodeViewCollection::SnapshotLatest(apply) => { + ViewCollection::SnapshotLatest(apply) => { if apply { return_view.snapshot_latest().await } else { return_view } } - NodeViewCollection::SnapshotAt(at) => return_view.snapshot_at(at).await, - NodeViewCollection::Layers(layers) => return_view.layers(layers).await, - NodeViewCollection::ExcludeLayers(layers) => { - return_view.exclude_layers(layers).await - } - NodeViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, - NodeViewCollection::Window(window) => { + ViewCollection::SnapshotAt(at) => return_view.snapshot_at(at).await, + ViewCollection::Layers(layers) => return_view.layers(layers).await, + ViewCollection::ExcludeLayers(layers) => return_view.exclude_layers(layers).await, + ViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, + ViewCollection::Window(window) => { return_view.window(window.start, window.end).await } - NodeViewCollection::At(at) => return_view.at(at).await, - NodeViewCollection::Before(time) => return_view.before(time).await, - NodeViewCollection::After(time) => return_view.after(time).await, - NodeViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, - NodeViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - NodeViewCollection::Filter(filter) => return_view.filter(filter).await?, + ViewCollection::At(at) => return_view.at(at).await, + ViewCollection::Before(time) => return_view.before(time).await, + ViewCollection::After(time) => return_view.after(time).await, + ViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, + ViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, + ViewCollection::Filter(filter) => return_view.filter(filter).await?, } } Ok(return_view) diff --git a/raphtory-graphql/src/model/graph/path_from_graph.rs b/raphtory-graphql/src/model/graph/path_from_graph.rs index 34274323ca..a4025095ff 100644 --- a/raphtory-graphql/src/model/graph/path_from_graph.rs +++ b/raphtory-graphql/src/model/graph/path_from_graph.rs @@ -2,7 +2,7 @@ use crate::{ model::graph::{ collection::{check_list_allowed, check_page_limit}, filter_expr_input::GqlFilter, - filtering::PathFromNodeViewCollection, + filtering::ViewCollection, history::GqlHistory, nested_edges::GqlNestedEdges, path_from_node::GqlPathFromNode, @@ -451,43 +451,45 @@ impl GqlPathFromGraph { #[graphql( desc = "Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result." )] - views: Vec, + views: Vec, ) -> Result { let mut return_view: GqlPathFromGraph = self.clone(); for view in views { return_view = match view { - PathFromNodeViewCollection::Layers(layers) => return_view.layers(layers).await, - PathFromNodeViewCollection::ExcludeLayers(layers) => { - return_view.exclude_layers(layers).await - } - PathFromNodeViewCollection::ExcludeLayer(layer) => { - return_view.exclude_layer(layer).await - } - PathFromNodeViewCollection::Window(window) => { + ViewCollection::Layers(layers) => return_view.layers(layers).await, + ViewCollection::ExcludeLayers(layers) => return_view.exclude_layers(layers).await, + ViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, + ViewCollection::Window(window) => { return_view.window(window.start, window.end).await } - PathFromNodeViewCollection::ShrinkStart(time) => { - return_view.shrink_start(time).await - } - PathFromNodeViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - PathFromNodeViewCollection::At(time) => return_view.at(time).await, - PathFromNodeViewCollection::SnapshotLatest(apply) => { + ViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, + ViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, + ViewCollection::At(time) => return_view.at(time).await, + ViewCollection::SnapshotLatest(apply) => { if apply { return_view.snapshot_latest().await } else { return_view } } - PathFromNodeViewCollection::SnapshotAt(time) => return_view.snapshot_at(time).await, - PathFromNodeViewCollection::Latest(apply) => { + ViewCollection::SnapshotAt(time) => return_view.snapshot_at(time).await, + ViewCollection::Latest(apply) => { if apply { return_view.latest().await } else { return_view } } - PathFromNodeViewCollection::Before(time) => return_view.before(time).await, - PathFromNodeViewCollection::After(time) => return_view.after(time).await, + ViewCollection::Before(time) => return_view.before(time).await, + ViewCollection::After(time) => return_view.after(time).await, + ViewCollection::DefaultLayer(apply) => { + if apply { + return_view.default_layer().await + } else { + return_view + } + } + ViewCollection::Filter(filter) => return_view.filter(filter).await?, } } Ok(return_view) diff --git a/raphtory-graphql/src/model/graph/path_from_node.rs b/raphtory-graphql/src/model/graph/path_from_node.rs index c4e2bab2a4..291523f1ec 100644 --- a/raphtory-graphql/src/model/graph/path_from_node.rs +++ b/raphtory-graphql/src/model/graph/path_from_node.rs @@ -3,7 +3,7 @@ use crate::{ collection::{check_list_allowed, check_page_limit}, edges::GqlEdges, filter_expr_input::GqlFilter, - filtering::PathFromNodeViewCollection, + filtering::ViewCollection, history::GqlHistory, node::GqlNode, timeindex::{GqlEventTime, GqlTimeInput}, @@ -417,43 +417,45 @@ impl GqlPathFromNode { #[graphql( desc = "Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result." )] - views: Vec, + views: Vec, ) -> Result { let mut return_view: GqlPathFromNode = self.clone(); for view in views { return_view = match view { - PathFromNodeViewCollection::Layers(layers) => return_view.layers(layers).await, - PathFromNodeViewCollection::ExcludeLayers(layers) => { - return_view.exclude_layers(layers).await - } - PathFromNodeViewCollection::ExcludeLayer(layer) => { - return_view.exclude_layer(layer).await - } - PathFromNodeViewCollection::Window(window) => { + ViewCollection::Layers(layers) => return_view.layers(layers).await, + ViewCollection::ExcludeLayers(layers) => return_view.exclude_layers(layers).await, + ViewCollection::ExcludeLayer(layer) => return_view.exclude_layer(layer).await, + ViewCollection::Window(window) => { return_view.window(window.start, window.end).await } - PathFromNodeViewCollection::ShrinkStart(time) => { - return_view.shrink_start(time).await - } - PathFromNodeViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - PathFromNodeViewCollection::At(time) => return_view.at(time).await, - PathFromNodeViewCollection::SnapshotLatest(apply) => { + ViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, + ViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, + ViewCollection::At(time) => return_view.at(time).await, + ViewCollection::SnapshotLatest(apply) => { if apply { return_view.snapshot_latest().await } else { return_view } } - PathFromNodeViewCollection::SnapshotAt(time) => return_view.snapshot_at(time).await, - PathFromNodeViewCollection::Latest(apply) => { + ViewCollection::SnapshotAt(time) => return_view.snapshot_at(time).await, + ViewCollection::Latest(apply) => { if apply { return_view.latest().await } else { return_view } } - PathFromNodeViewCollection::Before(time) => return_view.before(time).await, - PathFromNodeViewCollection::After(time) => return_view.after(time).await, + ViewCollection::Before(time) => return_view.before(time).await, + ViewCollection::After(time) => return_view.after(time).await, + ViewCollection::DefaultLayer(apply) => { + if apply { + return_view.default_layer().await + } else { + return_view + } + } + ViewCollection::Filter(filter) => return_view.filter(filter).await?, } } Ok(return_view) From ebedb02bd5f88492d479934cc910ce6888be1883 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:41:58 +0100 Subject: [PATCH 141/155] make a filter's repr the Python that builds it repr prints the module-qualified expression, so eval rebuilds the filter after `import raphtory`; constants use Python's own literals and parentheses follow Python precedence. Every factory, read and expression prints this way, and the docs and stubs follow. --- docs/user-guide/views/6_filtering.md | 5 +- python/python/raphtory/filter/__init__.pyi | 18 + python/tests/test_base_install/test_repr.py | 44 +- raphtory/src/python/filter/edge_expr.rs | 22 +- .../src/python/filter/exploded_edge_expr.rs | 11 +- raphtory/src/python/filter/filter_expr.rs | 9 +- raphtory/src/python/filter/graph_filter.rs | 11 +- raphtory/src/python/filter/mod.rs | 1 + raphtory/src/python/filter/node_expr.rs | 20 +- raphtory/src/python/filter/repr.rs | 413 ++++++++++++++++++ 10 files changed, 532 insertions(+), 22 deletions(-) create mode 100644 raphtory/src/python/filter/repr.rs diff --git a/docs/user-guide/views/6_filtering.md b/docs/user-guide/views/6_filtering.md index ea66f3d1c2..c96cfff17b 100644 --- a/docs/user-guide/views/6_filtering.md +++ b/docs/user-guide/views/6_filtering.md @@ -174,7 +174,8 @@ assert [n.degree() for n in narrowed] == [1, 1, 1] ## Seeing what a filter will do -`repr()` prints the tree. It is the same tree a remote graph sends, so there is no separate +`repr()` is the Python that builds the expression, module-qualified, so `eval` rebuilds it +after `import raphtory`. A remote graph sends the same expression, so there is no separate server-side form to check. /// tab | :fontawesome-brands-python: Python @@ -187,7 +188,7 @@ print(repr(filter.Node.window(0, 2).property("score") > 4)) !!! output ``` - FilterExpr(WINDOW[0..2](score) > 4) + raphtory.filter.Node.window(0, 2).property('score') > 4 ``` ## Cybersecurity scenario diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index af186f6fe5..a5af132117 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -399,6 +399,9 @@ class Expr(object): class PropertyExpr(Expr): """A property read, which can switch to the property's history with `temporal()`.""" + def __repr__(self): + """Return repr(self).""" + def temporal(self) -> filter.Expr: """ Switches from the property's latest value to its full temporal history, @@ -636,6 +639,9 @@ class NodeFilter(object): view, and its own view methods narrow it further. """ + def __repr__(self): + """Return repr(self).""" + def after(self, time: int) -> filter.NodeFilter: """ Restricts node evaluation to times strictly after the given time. @@ -1025,6 +1031,9 @@ class EdgeFilter(object): evaluate within that view, and its own view methods narrow it further. """ + def __repr__(self): + """Return repr(self).""" + def after(self, time: int) -> filter.EdgeFilter: """ Restricts edge evaluation to times strictly after the given time. @@ -1203,6 +1212,9 @@ class EdgeEndpoint(object): Edge.src().property("country") == "UK" """ + def __repr__(self): + """Return repr(self).""" + def id(self) -> filter.Expr: """ Selects the endpoint node ID field for filtering. @@ -1436,6 +1448,9 @@ class ExplodedEdgeFilter(object): within that view, and its own view methods narrow it further. """ + def __repr__(self): + """Return repr(self).""" + def after(self, time: int) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to times strictly after the given time. @@ -1713,6 +1728,9 @@ class GraphFilter(FilterExpr): with, and its own view methods narrow it further. """ + def __repr__(self): + """Return repr(self).""" + def after(self, time: int) -> filter.GraphFilter: """ Restricts evaluation to times strictly after the given time. diff --git a/python/tests/test_base_install/test_repr.py b/python/tests/test_base_install/test_repr.py index f9de20051d..6c48a73964 100644 --- a/python/tests/test_base_install/test_repr.py +++ b/python/tests/test_base_install/test_repr.py @@ -66,12 +66,14 @@ def test_persistent_graph(self): class FilterExprReprTest(TestCase): - """`repr` shows the expression tree, which is both what runs locally and - what a server receives.""" + """`repr` is the Python that builds the expression, module-qualified, so + `eval` rebuilds it after `import raphtory`.""" - def test_repr_shows_the_recorded_wire_form(self): + def test_repr_is_the_python_that_builds_the_expression(self): expr = filter.Node.window(0, 5).property("score") > 4 - self.assertEqual(repr(expr), "Expr(WINDOW[0..5](score) > 4)") + self.assertEqual( + repr(expr), "raphtory.filter.Node.window(0, 5).property('score') > 4" + ) def test_repr_shows_temporal_ops_and_combinators(self): expr = (filter.Node.property("score").temporal().sum() > 10) & ~( @@ -79,9 +81,39 @@ def test_repr_shows_temporal_ops_and_combinators(self): ) self.assertEqual( repr(expr), - "Expr((SUM(TEMPORAL(score)) > 10) AND (NOT(name == carol)))", + "(raphtory.filter.Node.property('score').temporal().sum() > 10)" + " & ~(raphtory.filter.Node.name() == 'carol')", ) def test_repr_shows_expressions_on_both_sides(self): expr = filter.Node.degree() > filter.Node.in_degree() - self.assertEqual(repr(expr), "Expr(degree > in_degree)") + self.assertEqual( + repr(expr), + "raphtory.filter.Node.degree() > raphtory.filter.Node.in_degree()", + ) + + def test_repr_round_trips_through_eval(self): + import raphtory + + cases = [ + filter.Node.window(0, 5).property("score") > 4, + filter.Node.property("p").temporal().starts_with("Go").all(), + filter.Node.layer("work").property("p").is_in(["a", "b", 3]) + | filter.Node.metadata("m").is_some(), + (filter.Node.name() == "a") + & (filter.Node.name() == "b") + & (filter.Node.name() == "c"), + filter.Edge.window(1, 4).src().property("p").temporal().len() >= 2, + filter.Edge.dst().name().fuzzy_search("bob", 1, True), + filter.Edge.is_valid() & filter.Edge.layers(["a", "b"]).is_active(), + filter.ExplodedEdge.property("p") == 3.5, + filter.Graph.window(0, 5).latest(), + filter.Graph.window(0, 5) & (filter.Node.name() != "x"), + filter.Node.property("s") == "it's", + filter.Node.window((3, 2), 9).is_active(), + filter.Node.property("p"), + filter.Edge.at(3).src(), + ] + for expr in cases: + text = repr(expr) + self.assertEqual(repr(eval(text, {"raphtory": raphtory})), text) diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index 4d058220f7..e1436817a7 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -4,11 +4,14 @@ use crate::{ expr::{EdgeLeaf, Expr, Field, Leaf, NodeExpr, NodeLeaf, ViewOp}, }, python::{ - filter::node_expr::{PyExpr, PyPropertyExpr, Typed}, + filter::{ + node_expr::{PyExpr, PyPropertyExpr, Typed}, + repr, + }, types::iterable::FromIterable, }, }; -use pyo3::{pyclass, pymethods}; +use pyo3::{pyclass, pymethods, PyResult, Python}; use raphtory_api::core::storage::timeindex::EventTime; /// Entry point for filtering an edge endpoint (source or destination). @@ -61,6 +64,17 @@ impl PyEdgeEndpoint { #[pymethods] impl PyEdgeEndpoint { + fn __repr__(&self, py: Python<'_>) -> PyResult { + let end = match self.endpoint { + Endpoint::Src => "src", + Endpoint::Dst => "dst", + }; + Ok(format!( + "{}.{end}()", + repr::factory(py, "Edge", &self.views)? + )) + } + /// Selects the endpoint node ID field for filtering. /// /// Returns: @@ -144,6 +158,10 @@ pub struct PyEdgeFilter(pub(crate) Vec); #[pymethods] impl PyEdgeFilter { + fn __repr__(&self, py: Python<'_>) -> PyResult { + repr::factory(py, "Edge", &self.0) + } + /// Selects the edge **source endpoint** for filtering. /// /// Returns: diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index 46c29092c8..6888495d37 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -1,11 +1,14 @@ use crate::{ db::graph::views::filter::model::expr::{ExplodedEdgeLeaf, Expr, Leaf, ViewOp}, python::{ - filter::node_expr::{PyExpr, PyPropertyExpr, Typed}, + filter::{ + node_expr::{PyExpr, PyPropertyExpr, Typed}, + repr, + }, types::iterable::FromIterable, }, }; -use pyo3::{pyclass, pymethods}; +use pyo3::{pyclass, pymethods, PyResult, Python}; use raphtory_api::core::storage::timeindex::EventTime; /// An exploded-edge filter scoped to a view. @@ -45,6 +48,10 @@ impl PyExplodedEdgeFilter { #[pymethods] impl PyExplodedEdgeFilter { + fn __repr__(&self, py: Python<'_>) -> PyResult { + repr::factory(py, "ExplodedEdge", &self.0) + } + /// Filters an exploded edge property by name. /// /// The property may be static or temporal depending on the query context. diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 0055d20a74..24306cc921 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -7,7 +7,7 @@ use crate::{ graph::views::filter::{model::expr::FilterExpr, CreateFilter, DynEdgeFilter}, }, errors::GraphError, - python::filter::node_expr::PyExpr, + python::filter::{node_expr::PyExpr, repr}, }; use pyo3::{exceptions::PyTypeError, prelude::*, Borrowed}; use std::sync::Arc; @@ -96,9 +96,10 @@ impl PyFilterExpr { Ok(PyFilterExpr(FilterExpr::Not(Box::new(self.0.clone())))) } - /// Shows the filter tree: what runs locally and what a server receives. - fn __repr__(&self) -> String { - format!("FilterExpr({})", self.0) + /// The Python expression that builds this filter, module-qualified, so + /// `eval` rebuilds it after `import raphtory`. + fn __repr__(&self, py: Python<'_>) -> PyResult { + repr::filter(py, &self.0) } } diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index dea8bce08b..fb22151b2c 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -1,8 +1,11 @@ use crate::{ db::graph::views::filter::model::expr::{FilterExpr, ViewOp}, - python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, + python::{ + filter::{filter_expr::PyFilterExpr, repr}, + types::iterable::FromIterable, + }, }; -use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python}; +use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, PyResult, Python}; use raphtory_api::core::storage::timeindex::EventTime; /// A graph-level view scope. @@ -33,6 +36,10 @@ impl PyGraphFilter { #[pymethods] impl PyGraphFilter { + fn __repr__(&self, py: Python<'_>) -> PyResult { + repr::factory(py, "Graph", &self.0) + } + /// Restricts evaluation to events within a time window. /// /// The window is inclusive of `start` and exclusive of `end`. diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 08d7b0e67e..b552447e60 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -19,6 +19,7 @@ pub mod exploded_edge_expr; pub mod filter_expr; pub mod graph_filter; pub mod node_expr; +pub(crate) mod repr; impl From> for Layer { fn from(iter: FromIterable) -> Self { diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 50a3b8fb26..97bb0ed905 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -10,7 +10,10 @@ use crate::{ }, errors::GraphError, python::{ - filter::filter_expr::{no_view, ExprOrFilter, PyFilterExpr}, + filter::{ + filter_expr::{no_view, ExprOrFilter, PyFilterExpr}, + repr, + }, graph::node_state::PyOutputNodeState, types::iterable::FromIterable, }, @@ -576,9 +579,10 @@ impl PyExpr { PyExpr(map_typed!(self.0.clone(), |e| Expr::Not(Box::new(e)))) } - /// Shows the expression tree: what runs locally and what a server receives. - fn __repr__(&self) -> String { - format!("Expr({})", self.0) + /// The Python expression that builds this one, module-qualified, so `eval` + /// rebuilds it after `import raphtory`. + fn __repr__(&self, py: Python<'_>) -> PyResult { + repr::typed(py, &self.0) } } @@ -593,6 +597,10 @@ impl PyPropertyExpr { fn temporal(&self) -> PyExpr { PyExpr(self.history.clone()) } + + fn __repr__(&self, py: Python<'_>) -> PyResult { + repr::typed(py, &self.latest) + } } /// A node filter scoped to a view. @@ -645,6 +653,10 @@ impl PyNodeFilter { #[pymethods] impl PyNodeFilter { + fn __repr__(&self, py: Python<'_>) -> PyResult { + repr::factory(py, "Node", &self.0) + } + /// Selects the node ID field for filtering. /// /// Returns: diff --git a/raphtory/src/python/filter/repr.rs b/raphtory/src/python/filter/repr.rs new file mode 100644 index 0000000000..d86fcc5026 --- /dev/null +++ b/raphtory/src/python/filter/repr.rs @@ -0,0 +1,413 @@ +//! The Python form of a filter: the expression a user would write to build it +//! again, module-qualified so `eval` rebuilds it after `import raphtory`. + +use crate::{ + db::graph::views::filter::model::expr::{ + Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, FilterExpr, NodeLeaf, StrOp, ViewOp, + }, + python::filter::node_expr::Typed, +}; +use pyo3::{prelude::*, types::PyString}; +use raphtory_api::core::{ + entities::properties::prop::Prop, + storage::timeindex::{AsTime, EventTime}, + Direction, +}; + +const MODULE: &str = "raphtory.filter"; + +/// What a rendered piece is, for the caller that puts it inside something else: +/// a chain (`raphtory.filter.Node.property('p').sum()`, or a `~` negation, which +/// binds tighter than `&` and `|`) can take a method call or an operator as it +/// is; a comparison or an `&`/`|` combination needs parentheses. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Shape { + Chain, + Compound, +} + +struct Rendered { + text: String, + shape: Shape, +} + +impl Rendered { + fn chain(text: String) -> Self { + Rendered { + text, + shape: Shape::Chain, + } + } + + fn compound(text: String) -> Self { + Rendered { + text, + shape: Shape::Compound, + } + } + + /// The text as an operand or a receiver: parenthesised unless it is a chain. + fn atom(&self) -> String { + match self.shape { + Shape::Chain => self.text.clone(), + Shape::Compound => format!("({})", self.text), + } + } +} + +pub(crate) fn typed(py: Python<'_>, expr: &Typed) -> PyResult { + Ok(match expr { + Typed::Node(e) => render(py, e, Entity::Node)?.text, + Typed::Edge(e) => render(py, e, Entity::Edge)?.text, + Typed::ExplodedEdge(e) => render(py, e, Entity::ExplodedEdge)?.text, + }) +} + +pub(crate) fn filter(py: Python<'_>, filter: &FilterExpr) -> PyResult { + Ok(render_filter(py, filter)?.text) +} + +/// A factory: an entity root seen through `views`. +pub(crate) fn factory(py: Python<'_>, root: &str, views: &[ViewOp]) -> PyResult { + Ok(format!("{MODULE}.{root}{}", view_chain(py, views)?)) +} + +#[derive(Clone, Copy)] +enum Entity { + Node, + Edge, + ExplodedEdge, +} + +impl Entity { + fn root(self) -> &'static str { + match self { + Entity::Node => "Node", + Entity::Edge => "Edge", + Entity::ExplodedEdge => "ExplodedEdge", + } + } +} + +/// Every read renders the same way: the entity root, its views, then the read. +trait Leaf { + fn read(&self, py: Python<'_>, entity: Entity) -> PyResult; +} + +fn render_filter(py: Python<'_>, filter: &FilterExpr) -> PyResult { + Ok(match filter { + FilterExpr::Node(e) => render(py, e, Entity::Node)?, + FilterExpr::Edge(e) => render(py, e, Entity::Edge)?, + FilterExpr::ExplodedEdge(e) => render(py, e, Entity::ExplodedEdge)?, + FilterExpr::View(ops) => Rendered::chain(factory(py, "Graph", ops)?), + FilterExpr::And(items) => combined( + flat(items, |i| match i { + FilterExpr::And(inner) => Some(inner), + _ => None, + }) + .into_iter() + .map(|i| render_filter(py, i)), + " & ", + )?, + FilterExpr::Or(items) => combined( + flat(items, |i| match i { + FilterExpr::Or(inner) => Some(inner), + _ => None, + }) + .into_iter() + .map(|i| render_filter(py, i)), + " | ", + )?, + FilterExpr::Not(e) => Rendered::chain(format!("~{}", render_filter(py, e)?.atom())), + FilterExpr::Opaque(_) => Rendered::chain("".to_owned()), + }) +} + +fn render(py: Python<'_>, expr: &Expr, entity: Entity) -> PyResult { + Ok(match expr { + Expr::Const(v) => Rendered::chain(literal(py, v)?), + Expr::Read(leaf) => Rendered::chain(leaf.read(py, entity)?), + Expr::Agg(agg, e) => { + let name = match agg { + Agg::Sum => "sum", + Agg::Avg => "avg", + Agg::Min => "min", + Agg::Max => "max", + Agg::First => "first", + Agg::Last => "last", + Agg::Len => "len", + }; + Rendered::chain(format!("{}.{name}()", render(py, e, entity)?.atom())) + } + Expr::Cmp(op, l, r) => { + let sym = match op { + CmpOp::Eq => "==", + CmpOp::Ne => "!=", + CmpOp::Lt => "<", + CmpOp::Le => "<=", + CmpOp::Gt => ">", + CmpOp::Ge => ">=", + }; + let l = render(py, l, entity)?.atom(); + let r = render(py, r, entity)?.atom(); + Rendered::compound(format!("{l} {sym} {r}")) + } + Expr::Str(op, l, r) => { + let l = render(py, l, entity)?.atom(); + let r = render(py, r, entity)?.atom(); + Rendered::chain(match op { + StrOp::StartsWith => format!("{l}.starts_with({r})"), + StrOp::EndsWith => format!("{l}.ends_with({r})"), + StrOp::Contains => format!("{l}.contains({r})"), + StrOp::NotContains => format!("{l}.not_contains({r})"), + StrOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => format!( + "{l}.fuzzy_search({r}, {levenshtein_distance}, {})", + py_bool(*prefix_match) + ), + }) + } + Expr::In { + expr, + values, + negated, + } => { + let items = values + .iter() + .map(|v| literal(py, v)) + .collect::>>()? + .join(", "); + let name = if *negated { "is_not_in" } else { "is_in" }; + Rendered::chain(format!( + "{}.{name}([{items}])", + render(py, expr, entity)?.atom() + )) + } + Expr::IsSome(e) => Rendered::chain(format!("{}.is_some()", render(py, e, entity)?.atom())), + Expr::IsNone(e) => Rendered::chain(format!("{}.is_none()", render(py, e, entity)?.atom())), + Expr::Any(e) => Rendered::chain(format!("{}.any()", render(py, e, entity)?.atom())), + Expr::All(e) => Rendered::chain(format!("{}.all()", render(py, e, entity)?.atom())), + Expr::And(items) => combined( + flat(items, |i| match i { + Expr::And(inner) => Some(inner), + _ => None, + }) + .into_iter() + .map(|i| render(py, i, entity)), + " & ", + )?, + Expr::Or(items) => combined( + flat(items, |i| match i { + Expr::Or(inner) => Some(inner), + _ => None, + }) + .into_iter() + .map(|i| render(py, i, entity)), + " | ", + )?, + Expr::Not(e) => Rendered::chain(format!("~{}", render(py, e, entity)?.atom())), + }) +} + +/// The operands of a combination, with any nested combination of the same +/// operator opened up, so `a & b & c` reads back the way it was written. +fn flat<'a, T>(items: &'a [T], same: impl Fn(&'a T) -> Option<&'a Vec> + Copy) -> Vec<&'a T> { + items + .iter() + .flat_map(|item| match same(item) { + Some(inner) => flat(inner, same), + None => vec![item], + }) + .collect() +} + +fn combined(items: impl Iterator>, sep: &str) -> PyResult { + let parts = items + .map(|item| item.map(|r| r.atom())) + .collect::>>()?; + Ok(Rendered::compound(parts.join(sep))) +} + +impl Leaf for NodeLeaf { + fn read(&self, py: Python<'_>, entity: Entity) -> PyResult { + let (views, tail) = node_tail(py, self)?; + Ok(format!("{}{tail}", factory(py, entity.root(), views)?)) + } +} + +/// The views a node read carries and the call that reads it, apart, because an +/// endpoint read puts `.src()` between them. +fn node_tail<'a>(py: Python<'_>, leaf: &'a NodeLeaf) -> PyResult<(&'a [ViewOp], String)> { + Ok(match leaf { + NodeLeaf::Field { views, field } => { + let name = match field { + Field::Id => "id", + Field::Name => "name", + Field::NodeType => "node_type", + }; + (views, format!(".{name}()")) + } + NodeLeaf::Degree { views, direction } => { + let name = match direction { + Direction::BOTH => "degree", + Direction::IN => "in_degree", + Direction::OUT => "out_degree", + }; + (views, format!(".{name}()")) + } + NodeLeaf::Property { + views, + name, + temporal, + } => (views, property(py, name, *temporal)?), + NodeLeaf::Metadata { views, name } => (views, format!(".metadata({})", py_str(py, name)?)), + NodeLeaf::IsActive { views } => (views, ".is_active()".to_owned()), + }) +} + +impl Leaf for EdgeLeaf { + fn read(&self, py: Python<'_>, entity: Entity) -> PyResult { + let root = entity.root(); + Ok(match self { + EdgeLeaf::Property { + views, + name, + temporal, + } => format!( + "{}{}", + factory(py, root, views)?, + property(py, name, *temporal)? + ), + EdgeLeaf::Metadata { views, name } => { + format!( + "{}.metadata({})", + factory(py, root, views)?, + py_str(py, name)? + ) + } + EdgeLeaf::IsActive { views } => format!("{}.is_active()", factory(py, root, views)?), + EdgeLeaf::IsValid { views } => format!("{}.is_valid()", factory(py, root, views)?), + EdgeLeaf::IsDeleted { views } => format!("{}.is_deleted()", factory(py, root, views)?), + EdgeLeaf::IsSelfLoop { views } => { + format!("{}.is_self_loop()", factory(py, root, views)?) + } + EdgeLeaf::Src(inner) => endpoint(py, root, "src", inner)?, + EdgeLeaf::Dst(inner) => endpoint(py, root, "dst", inner)?, + }) + } +} + +/// `Edge.src()` reads a node field or property; the views on that read are the +/// edge factory's, so they sit before `.src()`. +fn endpoint(py: Python<'_>, root: &str, end: &str, inner: &Expr) -> PyResult { + Ok(match inner { + Expr::Read(leaf) => { + let (views, tail) = node_tail(py, leaf)?; + format!("{}.{end}(){tail}", factory(py, root, views)?) + } + other => format!( + "{}.{end}()<{}>", + factory(py, root, &[])?, + render(py, other, Entity::Node)?.text + ), + }) +} + +impl Leaf for ExplodedEdgeLeaf { + fn read(&self, py: Python<'_>, entity: Entity) -> PyResult { + let root = entity.root(); + Ok(match self { + ExplodedEdgeLeaf::Property { + views, + name, + temporal, + } => format!( + "{}{}", + factory(py, root, views)?, + property(py, name, *temporal)? + ), + ExplodedEdgeLeaf::Metadata { views, name } => { + format!( + "{}.metadata({})", + factory(py, root, views)?, + py_str(py, name)? + ) + } + ExplodedEdgeLeaf::IsActive { views } => { + format!("{}.is_active()", factory(py, root, views)?) + } + ExplodedEdgeLeaf::IsValid { views } => { + format!("{}.is_valid()", factory(py, root, views)?) + } + ExplodedEdgeLeaf::IsDeleted { views } => { + format!("{}.is_deleted()", factory(py, root, views)?) + } + ExplodedEdgeLeaf::IsSelfLoop { views } => { + format!("{}.is_self_loop()", factory(py, root, views)?) + } + }) + } +} + +fn property(py: Python<'_>, name: &str, temporal: bool) -> PyResult { + let temporal = if temporal { ".temporal()" } else { "" }; + Ok(format!(".property({}){temporal}", py_str(py, name)?)) +} + +fn view_chain(py: Python<'_>, views: &[ViewOp]) -> PyResult { + views + .iter() + .map(|op| { + Ok(match op { + ViewOp::Window { start, end } => { + format!(".window({}, {})", time(start), time(end)) + } + ViewOp::At(t) => format!(".at({})", time(t)), + ViewOp::After(t) => format!(".after({})", time(t)), + ViewOp::Before(t) => format!(".before({})", time(t)), + ViewOp::Latest => ".latest()".to_owned(), + ViewOp::SnapshotAt(t) => format!(".snapshot_at({})", time(t)), + ViewOp::SnapshotLatest => ".snapshot_latest()".to_owned(), + ViewOp::Layers(names) => match names.as_slice() { + [name] => format!(".layer({})", py_str(py, name)?), + names => format!( + ".layers([{}])", + names + .iter() + .map(|n| py_str(py, n)) + .collect::>>()? + .join(", ") + ), + }, + }) + }) + .collect::>>() + .map(|parts| parts.concat()) +} + +/// A time the way a user passes it: the timestamp, or a `(t, event_id)` pair +/// when the event id matters. +fn time(t: &EventTime) -> String { + match t.i() { + 0 => t.t().to_string(), + i => format!("({}, {i})", t.t()), + } +} + +fn literal(py: Python<'_>, value: &Prop) -> PyResult { + Ok(value.clone().into_pyobject(py)?.repr()?.to_string()) +} + +fn py_str(py: Python<'_>, s: &str) -> PyResult { + Ok(PyString::new(py, s).repr()?.to_string()) +} + +fn py_bool(b: bool) -> &'static str { + if b { + "True" + } else { + "False" + } +} From 08b85f326fffe863caa572e9f4ac0a710d4d35ba Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:10:28 +0100 Subject: [PATCH 142/155] add earliest and latest as the update-level aggregations first, last and len reduce the innermost list like the other aggregations, so on a list-valued history they answer per update. earliest and latest pick an update of a temporal history as it is and refuse anything else. Tests that chained an aggregation after first or last on a list-valued history meant the update and now say so. --- docs/user-guide/views/6_filtering.md | 5 + python/python/raphtory/filter/__init__.pyi | 28 +- .../test_edge_endpoint_filters.py | 2 +- .../test_filters/test_edge_filter.py | 2 +- .../test_filters/test_edge_property_filter.py | 56 ++-- .../test_filters/test_node_filter.py | 2 +- .../test_filters/test_node_property_filter.py | 105 +++++-- .../test_graphql/test_filter_expr_grammar.py | 7 + .../test_remote_graph_transport.py | 22 +- .../entities/properties/prop/prop_array.rs | 2 +- raphtory-graphql/schema.graphql | 24 ++ .../src/model/graph/filter_expr_input.rs | 8 + .../tests/filter_tests/edge_expr_filter.rs | 12 +- raphtory-tests/tests/test_filters.rs | 296 +++++++++--------- .../graph/views/filter/model/expr/compile.rs | 32 ++ .../graph/views/filter/model/expr/convert.rs | 8 +- .../graph/views/filter/model/expr/display.rs | 2 + .../db/graph/views/filter/model/expr/mod.rs | 6 + .../db/graph/views/filter/model/expr/tests.rs | 94 ++++++ .../views/filter/model/node_expr/exprs.rs | 16 +- .../graph/views/filter/model/node_expr/ops.rs | 39 ++- .../filter/model/property_filter/evaluate.rs | 2 +- raphtory/src/python/filter/node_expr.rs | 28 +- raphtory/src/python/filter/repr.rs | 2 + 24 files changed, 544 insertions(+), 256 deletions(-) diff --git a/docs/user-guide/views/6_filtering.md b/docs/user-guide/views/6_filtering.md index c96cfff17b..fecdaeade6 100644 --- a/docs/user-guide/views/6_filtering.md +++ b/docs/user-guide/views/6_filtering.md @@ -125,6 +125,11 @@ then turns the history back into one value: `.sum()`, `.avg()`, `.min()`, `.max( `.last()`, `.len()`. Comparing the history itself gives one answer per value; `.any()` and `.all()`, written after the comparison, ask whether any, or every, answer holds. +Two aggregates pick an update rather than reduce one: `.earliest()` and `.latest()` return the +first and last update of a history as they are. That only matters for a list-valued property, +where the aggregates above work inside each list: `.temporal().first()` is the first element of +every update, one answer per update, while `.temporal().earliest()` is the whole first list. + /// tab | :fontawesome-brands-python: Python ```{.python continuation} diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index a5af132117..76772fea0a 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -195,6 +195,15 @@ class Expr(object): filter.Expr: """ + def earliest(self) -> filter.Expr: + """ + The earliest update of a temporal history, whatever its type: on a + list-valued property that is the whole first list. + + Returns: + filter.Expr: + """ + def ends_with(self, other: str | filter.Expr) -> filter.Expr: """ Checks whether the string value ends with the given suffix. @@ -220,7 +229,9 @@ class Expr(object): def first(self) -> filter.Expr: """ - Selects the first element when the value is list-like. + Selects the first element of each innermost list. On the history of a + list-valued property that is one answer per update; `earliest()` picks + the first update instead. Returns: filter.Expr: @@ -303,7 +314,18 @@ class Expr(object): def last(self) -> filter.Expr: """ - Selects the last element when the value is list-like. + Selects the last element of each innermost list. On the history of a + list-valued property that is one answer per update; `latest()` picks + the last update instead. + + Returns: + filter.Expr: + """ + + def latest(self) -> filter.Expr: + """ + The latest update of a temporal history, whatever its type: on a + list-valued property that is the whole last list. Returns: filter.Expr: @@ -322,7 +344,7 @@ class Expr(object): def len(self) -> filter.Expr: """ - Selects the number of elements when the value is list-like. + Selects the number of elements of each innermost list. Returns: filter.Expr: diff --git a/python/tests/test_base_install/test_filters/test_edge_endpoint_filters.py b/python/tests/test_base_install/test_filters/test_edge_endpoint_filters.py index 593a05e323..2cc5d8ab11 100644 --- a/python/tests/test_base_install/test_filters/test_edge_endpoint_filters.py +++ b/python/tests/test_base_install/test_filters/test_edge_endpoint_filters.py @@ -55,7 +55,7 @@ def check(graph): @with_variants(create_test_graph) def test_edges_src_property_temporal_sum(): def check(graph): - expr = filter.Edge.src().property("prop6").temporal().last().sum() == 12 + expr = filter.Edge.src().property("prop6").temporal().latest().sum() == 12 result = sorted(graph.filter(expr).edges.id) expected = sorted([("a", "d")]) assert result == expected diff --git a/python/tests/test_base_install/test_filters/test_edge_filter.py b/python/tests/test_base_install/test_filters/test_edge_filter.py index aecc1016d2..805a2e2896 100644 --- a/python/tests/test_base_install/test_filters/test_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_filter.py @@ -515,7 +515,7 @@ def check(graph): filter_expr = filter.Edge.src().id() == "3" with pytest.raises( Exception, - match=r'value 3 of type Str cannot be compared with U64', + match=r"value 3 of type Str cannot be compared with U64", ): graph.filter(filter_expr).nodes.id diff --git a/python/tests/test_base_install/test_filters/test_edge_property_filter.py b/python/tests/test_base_install/test_filters/test_edge_property_filter.py index ea5bbfe67a..36d6aef588 100644 --- a/python/tests/test_base_install/test_filters/test_edge_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_property_filter.py @@ -766,9 +766,9 @@ def check(graph): # ------ last ------ @with_variants(create_test_graph2) -def test_edge_property_temporal_last_sum_u64s(): +def test_edge_property_temporal_latest_sum_u64s(): def check(graph): - expr = filter.Edge.property("p_u64s").temporal().last().sum() == Prop.u64(30) + expr = filter.Edge.property("p_u64s").temporal().latest().sum() == Prop.u64(30) pairs = _pairs(graph.filter(expr).edges) assert pairs == {("b", "c"), ("c", "d")} @@ -776,9 +776,9 @@ def check(graph): @with_variants(create_test_graph2) -def test_edge_property_temporal_last_avg_i32s(): +def test_edge_property_temporal_latest_avg_i32s(): def check(graph): - expr = filter.Edge.property("p_i32s").temporal().last().avg() == Prop.f64( + expr = filter.Edge.property("p_i32s").temporal().latest().avg() == Prop.f64( 0.6666666666666666 ) pairs = _pairs(graph.filter(expr).edges) @@ -788,9 +788,9 @@ def check(graph): @with_variants(create_test_graph2) -def test_edge_property_temporal_last_min_u8s(): +def test_edge_property_temporal_latest_min_u8s(): def check(graph): - expr = filter.Edge.property("p_u8s").temporal().last().min() == Prop.u8(1) + expr = filter.Edge.property("p_u8s").temporal().latest().min() == Prop.u8(1) pairs = _pairs(graph.filter(expr).edges) assert pairs == {("a", "b")} @@ -798,9 +798,9 @@ def check(graph): @with_variants(create_test_graph2) -def test_edge_property_temporal_last_max_f64s(): +def test_edge_property_temporal_latest_max_f64s(): def check(graph): - expr = filter.Edge.property("p_f64s").temporal().last().max() == Prop.f64(1.5) + expr = filter.Edge.property("p_f64s").temporal().latest().max() == Prop.f64(1.5) pairs = _pairs(graph.filter(expr).edges) assert pairs == {("d", "a")} @@ -808,9 +808,9 @@ def check(graph): @with_variants(create_test_graph2) -def test_edge_property_temporal_last_len_u64s(): +def test_edge_property_temporal_latest_len_u64s(): def check(graph): - expr = filter.Edge.property("p_u64s").temporal().last().len() == Prop.u64(2) + expr = filter.Edge.property("p_u64s").temporal().latest().len() == Prop.u64(2) pairs = _pairs(graph.filter(expr).edges) assert pairs == {("a", "b"), ("b", "c"), ("d", "a")} @@ -872,9 +872,9 @@ def check(graph): # ------ first ------ @with_variants(create_test_graph2) -def test_edge_property_temporal_first_sum_u8s(): +def test_edge_property_temporal_earliest_sum_u8s(): def check(graph): - expr = filter.Edge.property("p_u8s").temporal().first().sum() == Prop.u64(6) + expr = filter.Edge.property("p_u8s").temporal().earliest().sum() == Prop.u64(6) pairs = _pairs(graph.filter(expr).edges) assert pairs == {("a", "b")} @@ -882,9 +882,11 @@ def check(graph): @with_variants(create_test_graph2) -def test_edge_property_temporal_first_avg_u64s(): +def test_edge_property_temporal_earliest_avg_u64s(): def check(graph): - expr = filter.Edge.property("p_u64s").temporal().first().avg() == Prop.f64(30.0) + expr = filter.Edge.property("p_u64s").temporal().earliest().avg() == Prop.f64( + 30.0 + ) pairs = _pairs(graph.filter(expr).edges) assert pairs == {("c", "d")} @@ -892,9 +894,11 @@ def check(graph): @with_variants(create_test_graph2) -def test_edge_property_temporal_first_min_i32s(): +def test_edge_property_temporal_earliest_min_i32s(): def check(graph): - expr = filter.Edge.property("p_i32s").temporal().first().min() == Prop.i32(-2) + expr = filter.Edge.property("p_i32s").temporal().earliest().min() == Prop.i32( + -2 + ) pairs = _pairs(graph.filter(expr).edges) assert pairs == {("a", "b")} @@ -902,9 +906,11 @@ def check(graph): @with_variants(create_test_graph2) -def test_edge_property_temporal_first_max_f64s(): +def test_edge_property_temporal_earliest_max_f64s(): def check(graph): - expr = filter.Edge.property("p_f64s").temporal().first().max() == Prop.f64(1.5) + expr = filter.Edge.property("p_f64s").temporal().earliest().max() == Prop.f64( + 1.5 + ) pairs = _pairs(graph.filter(expr).edges) assert pairs == {("d", "a")} @@ -912,9 +918,9 @@ def check(graph): @with_variants(create_test_graph2) -def test_edge_property_temporal_first_len_u32s(): +def test_edge_property_temporal_earliest_len_u32s(): def check(graph): - expr = filter.Edge.property("p_u32s").temporal().first().len() == Prop.u64(0) + expr = filter.Edge.property("p_u32s").temporal().earliest().len() == Prop.u64(0) pairs = _pairs(graph.filter(expr).edges) assert pairs == {("c", "d")} @@ -1080,19 +1086,21 @@ def check(graph): @with_variants(create_test_graph2) -def test_edge_temporal_property_first_any(): +def test_edge_temporal_property_earliest_any(): def check(graph): - expr = (filter.Edge.property("p_u64s").temporal().first() == Prop.u64(2)).any() + expr = ( + filter.Edge.property("p_u64s").temporal().earliest() == Prop.u64(2) + ).any() assert _pairs(graph.filter(expr).edges) == {("a", "b")} return check @with_variants(create_test_graph2) -def test_edge_temporal_property_first_all(): +def test_edge_temporal_property_earliest_all(): def check(graph): expr = ( - filter.Edge.property("p_bools").temporal().first() == Prop.bool(True) + filter.Edge.property("p_bools").temporal().earliest() == Prop.bool(True) ).all() assert _pairs(graph.filter(expr).edges) == {("b", "c")} diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index 3a905d69b5..8a96d5aad2 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -788,7 +788,7 @@ def check(graph): filter_expr = filter.Node.id() == "3" with pytest.raises( Exception, - match=r'value 3 of type Str cannot be compared with U64', + match=r"value 3 of type Str cannot be compared with U64", ): graph.filter(filter_expr).nodes.id diff --git a/python/tests/test_base_install/test_filters/test_node_property_filter.py b/python/tests/test_base_install/test_filters/test_node_property_filter.py index 800efb2ac8..d846f539b3 100644 --- a/python/tests/test_base_install/test_filters/test_node_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_property_filter.py @@ -361,9 +361,9 @@ def check(graph): @with_variants(create_test_graph) -def test_filter_nodes_for_temporal_last_property_sum(): +def test_filter_nodes_for_temporal_latest_property_sum(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().last().sum() == 12 + filter_expr = filter.Node.property("prop6").temporal().latest().sum() == 12 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -374,7 +374,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_last_property_avg(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().last().avg() == 4.0 + filter_expr = filter.Node.property("prop6").temporal().latest().avg() == 4.0 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -385,7 +385,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_last_property_min(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().last().min() == 3 + filter_expr = filter.Node.property("prop6").temporal().latest().min() == 3 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -396,7 +396,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_last_property_max(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().last().max() == 5 + filter_expr = filter.Node.property("prop6").temporal().latest().max() == 5 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -405,11 +405,11 @@ def check(graph): @with_variants(create_test_graph) -def test_filter_nodes_for_temporal_last_property_len(): +def test_filter_nodes_for_temporal_latest_property_len(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().last().len() == Prop.u64( - 3 - ) + filter_expr = filter.Node.property( + "prop6" + ).temporal().latest().len() == Prop.u64(3) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -572,9 +572,9 @@ def check(graph): @with_variants(create_test_graph) -def test_filter_nodes_for_temporal_first_property_sum(): +def test_filter_nodes_for_temporal_earliest_property_sum(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().first().sum() == 6 + filter_expr = filter.Node.property("prop6").temporal().earliest().sum() == 6 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -583,9 +583,9 @@ def check(graph): @with_variants(create_test_graph) -def test_filter_nodes_for_temporal_first_property_avg(): +def test_filter_nodes_for_temporal_earliest_property_avg(): def check(graph): - filter_expr = filter.Node.property("prop5").temporal().first().avg() == 2.0 + filter_expr = filter.Node.property("prop5").temporal().earliest().avg() == 2.0 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -596,7 +596,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_first_property_min(): def check(graph): - filter_expr = filter.Node.property("prop5").temporal().first().min() == 1 + filter_expr = filter.Node.property("prop5").temporal().earliest().min() == 1 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -607,7 +607,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_for_temporal_first_property_max(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().first().max() == 3 + filter_expr = filter.Node.property("prop6").temporal().earliest().max() == 3 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -616,11 +616,11 @@ def check(graph): @with_variants(create_test_graph) -def test_filter_nodes_for_temporal_first_property_len(): +def test_filter_nodes_for_temporal_earliest_property_len(): def check(graph): filter_expr = filter.Node.property( "prop6" - ).temporal().first().len() == Prop.u64(3) + ).temporal().earliest().len() == Prop.u64(3) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -1165,10 +1165,12 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_temporal_window_sum_ge(): def check(graph): - expr = filter.Node.window(1, 2).property("prop5").temporal().last().sum() >= 12 + expr = ( + filter.Node.window(1, 2).property("prop5").temporal().latest().sum() >= 12 + ) assert sorted(graph.filter(expr).nodes.id) == ["c"] - expr = filter.Node.window(1, 2).property("prop5").temporal().last().sum() >= 6 + expr = filter.Node.window(1, 2).property("prop5").temporal().latest().sum() >= 6 assert sorted(graph.filter(expr).nodes.id) == ["a", "c"] return check @@ -1178,10 +1180,10 @@ def check(graph): def test_filter_nodes_two_windows_and(): def check(graph): filter1 = ( - filter.Node.window(1, 2).property("prop5").temporal().first().sum() == 6 + filter.Node.window(1, 2).property("prop5").temporal().earliest().sum() == 6 ) filter2 = ( - filter.Node.window(2, 3).property("prop6").temporal().last().sum() == 12 + filter.Node.window(2, 3).property("prop6").temporal().latest().sum() == 12 ) assert sorted(graph.filter(filter1 & filter2).nodes.id) == ["a"] @@ -1227,10 +1229,10 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_at(): def check(graph): - expr = filter.Node.at(1).property("prop5").temporal().last().sum() >= 10 + expr = filter.Node.at(1).property("prop5").temporal().latest().sum() >= 10 assert sorted(graph.filter(expr).nodes.id) == ["c"] - expr = filter.Node.at(1).property("prop5").temporal().last().sum() == 6 + expr = filter.Node.at(1).property("prop5").temporal().latest().sum() == 6 assert sorted(graph.filter(expr).nodes.id) == ["a"] return check @@ -1239,10 +1241,10 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_before(): def check(graph): - expr = filter.Node.before(2).property("prop5").temporal().last().sum() == 6 + expr = filter.Node.before(2).property("prop5").temporal().latest().sum() == 6 assert sorted(graph.filter(expr).nodes.id) == ["a"] - expr = filter.Node.before(2).property("prop5").temporal().last().sum() >= 10 + expr = filter.Node.before(2).property("prop5").temporal().latest().sum() >= 10 assert sorted(graph.filter(expr).nodes.id) == ["c"] return check @@ -1260,7 +1262,7 @@ def check(graph): ): graph.filter(expr).nodes.id - expr = filter.Node.after(1).property("prop6").temporal().last().sum() == 12 + expr = filter.Node.after(1).property("prop6").temporal().latest().sum() == 12 assert sorted(graph.filter(expr).nodes.id) == ["a"] return check @@ -1269,7 +1271,7 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_latest(): def check(graph): - expr = filter.Node.latest().property("prop6").temporal().last().sum() == 12 + expr = filter.Node.latest().property("prop6").temporal().latest().sum() == 12 assert sorted(graph.filter(expr).nodes.id) == ["a"] expr = filter.Node.latest().property("prop5").temporal().sum() >= 0 @@ -1287,11 +1289,13 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_snapshot_at(): def check(graph): - expr = filter.Node.snapshot_at(1).property("prop5").temporal().last().sum() == 6 + expr = ( + filter.Node.snapshot_at(1).property("prop5").temporal().latest().sum() == 6 + ) assert sorted(graph.filter(expr).nodes.id) == ["a"] expr = ( - filter.Node.snapshot_at(1).property("prop5").temporal().last().sum() >= 10 + filter.Node.snapshot_at(1).property("prop5").temporal().latest().sum() >= 10 ) assert sorted(graph.filter(expr).nodes.id) == ["c"] @@ -1311,7 +1315,7 @@ def check(graph): def test_filter_nodes_snapshot_latest(): def check(graph): expr = ( - filter.Node.snapshot_latest().property("prop6").temporal().last().sum() + filter.Node.snapshot_latest().property("prop6").temporal().latest().sum() == 12 ) assert sorted(graph.filter(expr).nodes.id) == ["a"] @@ -1332,7 +1336,12 @@ def check(graph): def test_filter_nodes_window_latest(): def check(graph): expr = ( - filter.Node.window(1, 3).latest().property("prop6").temporal().last().sum() + filter.Node.window(1, 3) + .latest() + .property("prop6") + .temporal() + .latest() + .sum() == 12 ) assert sorted(graph.filter(expr).nodes.id) == ["a"] @@ -1344,7 +1353,12 @@ def check(graph): def test_filter_nodes_latest_window(): def check(graph): expr = ( - filter.Node.latest().window(1, 3).property("prop6").temporal().last().sum() + filter.Node.latest() + .window(1, 3) + .property("prop6") + .temporal() + .latest() + .sum() == 12 ) assert sorted(graph.filter(expr).nodes.id) == ["a"] @@ -1376,3 +1390,30 @@ def check(graph): assert sorted(graph.filter(expr).nodes.id) == [1] return check + + +def test_first_reduces_each_update_and_earliest_picks_one(): + """On a list-valued history the aggregates work inside every update, one + answer per update; `earliest()` and `latest()` are the updates themselves.""" + from raphtory import Graph + + graph = Graph() + graph.add_node(0, 0, {"list": [1, 2, 4]}) + graph.add_node(1, 0, {"list": [3, 2, 1]}) + history = filter.Node.property("list").temporal() + + def ids(expr): + return [n.id for n in graph.filter(expr).nodes] + + # max, first and len answer per update, so a qualifier chooses the updates. + assert ids((history.max() == 3).any()) == [0] + assert ids((history.first() == 3).any()) == [0] + assert ids((history.first() == 1).all()) == [] + assert ids((history.len() == 3).all()) == [0] + # earliest and latest are whole updates. + assert ids(history.earliest() == [1, 2, 4]) == [0] + assert ids(history.latest() == [3, 2, 1]) == [0] + assert ids(history.latest().sum() == 6) == [0] + # An update is only a thing on a history. + with pytest.raises(Exception, match="pick an update of a temporal history"): + graph.filter(filter.Node.property("list").latest() == [3, 2, 1]).nodes.id diff --git a/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py b/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py index b0abe01729..c712a6d7f4 100644 --- a/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py +++ b/python/tests/test_base_install/test_graphql/test_filter_expr_grammar.py @@ -109,10 +109,17 @@ def test_temporal_aggregates_and_qualifiers(): # The qualifier follows the comparison: one answer per update, any must hold. any_high = node({"any": {"gt": {"lhs": history, "rhs": const({"f64": 4.0})}}}) two_updates = node({"eq": {"lhs": {"len": history}, "rhs": const({"u64": 2})}}) + # earliest / latest are updates of the history, not reductions of it. + started_low = node( + {"lt": {"lhs": {"earliest": history}, "rhs": const({"f64": 2.0})}} + ) + ended_low = node({"lt": {"lhs": {"latest": history}, "rhs": const({"f64": 3.0})}}) with graphql_client(g) as client: assert node_names(client, total) == ["alice"] assert node_names(client, any_high) == ["alice", "bob"] assert node_names(client, two_updates) == ["bob", "dave"] + assert node_names(client, started_low) == ["dave"] + assert node_names(client, ended_low) == ["bob", "dave"] def test_edge_reads_through_an_endpoint_keep_the_edge_views(): diff --git a/python/tests/test_base_install/test_graphql/test_remote_graph_transport.py b/python/tests/test_base_install/test_graphql/test_remote_graph_transport.py index af6c2152f5..c972216e58 100644 --- a/python/tests/test_base_install/test_graphql/test_remote_graph_transport.py +++ b/python/tests/test_base_install/test_graphql/test_remote_graph_transport.py @@ -2083,16 +2083,14 @@ def test_filter_by_node_id_keeps_membership_but_getitem_narrows(): def test_temporal_multi_op_filter_preserves_op_order_e2e(): - """End-to-end guard that a multi-op temporal filter keeps its op-order - through the wire — the client serializes it via `apply_ops_to_condition` - (filtering.rs), so an inversion there would corrupt the query. - - On a list-valued temporal property, `.first().sum()` is shape-valid: First - picks the first snapshot's list, Sum reduces it to a scalar. The inversion - `.sum().first()` reduces a sequence-of-lists (→ None) and can never match, - so any op-order flip in the wire turns `["n"]` into `[]`. Uses the narrowing - `graph.filter()` path (not sticky `nodes.filter`) with a distractor node, - and pins the remote result against a local twin. + """End-to-end guard that a multi-op temporal filter keeps its op order + through the wire. + + On a list-valued temporal property, `.earliest().sum()` picks the first + update's list and reduces it to a scalar. The inversion `.sum().earliest()` + is refused (`earliest` needs a history), so any op-order flip in the wire + cannot pass. Uses the narrowing `graph.filter()` path with a distractor + node, and pins the remote result against a local twin. """ from raphtory import Graph from raphtory.filter import Node @@ -2104,8 +2102,8 @@ def build(g): g.add_node(0, "d", properties={"x": [8, 9]}) g.add_node(1, "d", properties={"x": [10, 11]}) - first_sum_3 = Node.property("x").temporal().first().sum() == 3 - first_sum_17 = Node.property("x").temporal().first().sum() == 17 + first_sum_3 = Node.property("x").temporal().earliest().sum() == 3 + first_sum_17 = Node.property("x").temporal().earliest().sum() == 17 local = Graph() build(local) diff --git a/raphtory-api/src/core/entities/properties/prop/prop_array.rs b/raphtory-api/src/core/entities/properties/prop/prop_array.rs index 55f75ac282..66033d5d12 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_array.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_array.rs @@ -106,7 +106,7 @@ impl PropArray { } // TODO: need something that returns PropRef instead to avoid allocations - pub fn iter(&self) -> impl Iterator + '_ { + pub fn iter(&self) -> impl DoubleEndedIterator + '_ { self.iter_all().flatten() } diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 4c80d82ed8..94a08626a1 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -1112,6 +1112,14 @@ input EdgeExpr @oneOf { first: EdgeExpr last: EdgeExpr len: EdgeExpr + """ + The earliest update of a temporal history. + """ + earliest: EdgeExpr + """ + The latest update of a temporal history. + """ + latest: EdgeExpr eq: EdgeCmp ne: EdgeCmp lt: EdgeCmp @@ -1715,6 +1723,14 @@ input ExplodedEdgeExpr @oneOf { first: ExplodedEdgeExpr last: ExplodedEdgeExpr len: ExplodedEdgeExpr + """ + The earliest update of a temporal history. + """ + earliest: ExplodedEdgeExpr + """ + The latest update of a temporal history. + """ + latest: ExplodedEdgeExpr eq: ExplodedEdgeCmp ne: ExplodedEdgeCmp lt: ExplodedEdgeCmp @@ -4484,6 +4500,14 @@ input NodeExpr @oneOf { first: NodeExpr last: NodeExpr len: NodeExpr + """ + The earliest update of a temporal history. + """ + earliest: NodeExpr + """ + The latest update of a temporal history. + """ + latest: NodeExpr eq: NodeCmp ne: NodeCmp lt: NodeCmp diff --git a/raphtory-graphql/src/model/graph/filter_expr_input.rs b/raphtory-graphql/src/model/graph/filter_expr_input.rs index 63332ea64c..3c88dcd5c8 100644 --- a/raphtory-graphql/src/model/graph/filter_expr_input.rs +++ b/raphtory-graphql/src/model/graph/filter_expr_input.rs @@ -203,6 +203,10 @@ macro_rules! entity_expr_input { First(Wrapped<$expr>), Last(Wrapped<$expr>), Len(Wrapped<$expr>), + /// The earliest update of a temporal history. + Earliest(Wrapped<$expr>), + /// The latest update of a temporal history. + Latest(Wrapped<$expr>), Eq($cmp), Ne($cmp), Lt($cmp), @@ -286,6 +290,8 @@ macro_rules! entity_expr_input { $expr::First(e) => Expr::Agg(Agg::First, inner(e)?), $expr::Last(e) => Expr::Agg(Agg::Last, inner(e)?), $expr::Len(e) => Expr::Agg(Agg::Len, inner(e)?), + $expr::Earliest(e) => Expr::Agg(Agg::Earliest, inner(e)?), + $expr::Latest(e) => Expr::Agg(Agg::Latest, inner(e)?), $expr::Eq(c) => cmp(CmpOp::Eq, c)?, $expr::Ne(c) => cmp(CmpOp::Ne, c)?, $expr::Lt(c) => cmp(CmpOp::Lt, c)?, @@ -360,6 +366,8 @@ macro_rules! entity_expr_input { Expr::Agg(Agg::First, e) => $expr::First(inner(e)?), Expr::Agg(Agg::Last, e) => $expr::Last(inner(e)?), Expr::Agg(Agg::Len, e) => $expr::Len(inner(e)?), + Expr::Agg(Agg::Earliest, e) => $expr::Earliest(inner(e)?), + Expr::Agg(Agg::Latest, e) => $expr::Latest(inner(e)?), Expr::Cmp(CmpOp::Eq, l, r) => $expr::Eq(cmp(l, r)?), Expr::Cmp(CmpOp::Ne, l, r) => $expr::Ne(cmp(l, r)?), Expr::Cmp(CmpOp::Lt, l, r) => $expr::Lt(cmp(l, r)?), diff --git a/raphtory-tests/tests/filter_tests/edge_expr_filter.rs b/raphtory-tests/tests/filter_tests/edge_expr_filter.rs index 1bb0a70f26..9d5f498fe3 100644 --- a/raphtory-tests/tests/filter_tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/filter_tests/edge_expr_filter.rs @@ -536,9 +536,9 @@ fn test_edge_quantified_all_is_not_in() { // ───────────────────────────────────────────────────────────────────────────── #[test] -fn test_edge_aggregated_last_then_sum() { +fn test_edge_aggregated_latest_then_sum() { // Property is list-valued at each timestamp. - // .last() picks the last snapshot's list, .sum() reduces it to a scalar. + // .latest() picks the last snapshot's list, .sum() reduces it to a scalar. let g = Graph::new(); // A->B: last snapshot = [3,4,5], sum = 12 (> 10) g.add_edge( @@ -576,7 +576,7 @@ fn test_edge_aggregated_last_then_sum() { let filter = EdgeFilter .property("score") .temporal() - .last() + .latest() .sum() .gt(10i64); let result = g.filter(filter).unwrap(); @@ -724,9 +724,9 @@ fn test_edge_aggregated_last_then_any_is_in() { } #[test] -fn test_edge_aggregated_last_then_all_contains() { +fn test_edge_aggregated_latest_then_all_contains() { // Property is list-valued at each timestamp. - // .last() picks the last snapshot's list, .contains("rock").all() checks all elements contain "rock". + // .latest() picks the last snapshot's list, .contains("rock").all() checks all elements contain "rock". let g = Graph::new(); // A->B: last snapshot = ["rock","rock-n-roll"] — all contain "rock" g.add_edge( @@ -764,7 +764,7 @@ fn test_edge_aggregated_last_then_all_contains() { let filter = EdgeFilter .property("tag") .temporal() - .last() + .latest() .contains("rock") .all(); let result = g.filter(filter).unwrap(); diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index d1e83ca5b7..dedbb3da05 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -5520,11 +5520,11 @@ mod test_node_property_filter_agg { // ------ Temporal last: SUM ------ #[test] - fn test_node_property_temporal_last_sum_u8s() { + fn test_node_property_temporal_latest_sum_u8s() { let filter = NodeFilter .property("p_u8s") .temporal() - .last() + .latest() .sum() .eq(Prop::U64(10)); let expected = vec!["n1"]; @@ -5532,11 +5532,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_sum_u16s() { + fn test_node_property_temporal_latest_sum_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .last() + .latest() .sum() .eq(Prop::U64(6)); let expected = vec!["n3", "n10"]; @@ -5544,11 +5544,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_sum_u32s() { + fn test_node_property_temporal_latest_sum_u32s() { let filter = NodeFilter .property("p_u32s") .temporal() - .last() + .latest() .sum() .eq(Prop::U64(10)); let expected = vec!["n1"]; @@ -5556,11 +5556,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_sum_u64s() { + fn test_node_property_temporal_latest_sum_u64s() { let filter = NodeFilter .property("p_u64s") .temporal() - .last() + .latest() .sum() .eq(Prop::U64(60)); let expected = vec!["n4"]; @@ -5568,11 +5568,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_sum_i32s() { + fn test_node_property_temporal_latest_sum_i32s() { let filter = NodeFilter .property("p_i32s") .temporal() - .last() + .latest() .sum() .eq(Prop::I64(60)); let expected = vec!["n4"]; @@ -5580,11 +5580,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_sum_i64s() { + fn test_node_property_temporal_latest_sum_i64s() { let filter = NodeFilter .property("p_i64s") .temporal() - .last() + .latest() .sum() .eq(Prop::I64(0)); let expected = vec!["n3", "n10"]; @@ -5592,11 +5592,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_sum_f32s() { + fn test_node_property_temporal_latest_sum_f32s() { let filter = NodeFilter .property("p_f32s") .temporal() - .last() + .latest() .sum() .eq(Prop::F64(6.5)); let expected = vec!["n3", "n10"]; @@ -5604,11 +5604,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_sum_f64s() { + fn test_node_property_temporal_latest_sum_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .last() + .latest() .sum() .eq(Prop::F64(90.0)); let expected = vec!["n3", "n10"]; @@ -5617,11 +5617,11 @@ mod test_node_property_filter_agg { // ------ Temporal last: AVG ------ #[test] - fn test_node_property_temporal_last_avg_u8s() { + fn test_node_property_temporal_latest_avg_u8s() { let filter = NodeFilter .property("p_u8s") .temporal() - .last() + .latest() .avg() .eq(Prop::F64(2.5)); let expected = vec!["n1"]; @@ -5629,11 +5629,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_avg_u16s() { + fn test_node_property_temporal_latest_avg_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .last() + .latest() .avg() .eq(Prop::F64(2.0)); let expected = vec!["n3", "n10"]; @@ -5641,11 +5641,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_avg_u32s() { + fn test_node_property_temporal_latest_avg_u32s() { let filter = NodeFilter .property("p_u32s") .temporal() - .last() + .latest() .avg() .eq(Prop::F64(2.5)); let expected = vec!["n1"]; @@ -5653,11 +5653,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_avg_u64s() { + fn test_node_property_temporal_latest_avg_u64s() { let filter = NodeFilter .property("p_u64s") .temporal() - .last() + .latest() .avg() .eq(Prop::F64(20.0)); let expected = vec!["n4"]; @@ -5665,11 +5665,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_avg_i32s() { + fn test_node_property_temporal_latest_avg_i32s() { let filter = NodeFilter .property("p_i32s") .temporal() - .last() + .latest() .avg() .eq(Prop::F64(0.6666666666666666)); let expected = vec!["n6"]; @@ -5677,11 +5677,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_avg_i64s() { + fn test_node_property_temporal_latest_avg_i64s() { let filter = NodeFilter .property("p_i64s") .temporal() - .last() + .latest() .avg() .eq(Prop::F64(0.0)); let expected = vec!["n3", "n10"]; @@ -5689,11 +5689,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_avg_f32s() { + fn test_node_property_temporal_latest_avg_f32s() { let filter = NodeFilter .property("p_f32s") .temporal() - .last() + .latest() .avg() .eq(Prop::F64(20.0)); let expected = vec!["n4"]; @@ -5701,11 +5701,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_avg_f64s() { + fn test_node_property_temporal_latest_avg_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .last() + .latest() .avg() .eq(Prop::F64(45.0)); let expected = vec!["n3", "n10"]; @@ -5714,11 +5714,11 @@ mod test_node_property_filter_agg { // ------ Temporal last: MIN ------ #[test] - fn test_node_property_temporal_last_min_u8s() { + fn test_node_property_temporal_latest_min_u8s() { let filter = NodeFilter .property("p_u8s") .temporal() - .last() + .latest() .min() .eq(Prop::U8(1)); let expected = vec!["n1", "n3", "n10"]; @@ -5726,11 +5726,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_min_u16s() { + fn test_node_property_temporal_latest_min_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .last() + .latest() .min() .eq(Prop::U16(1)); let expected = vec!["n1", "n3", "n10"]; @@ -5738,11 +5738,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_min_u32s() { + fn test_node_property_temporal_latest_min_u32s() { let filter = NodeFilter .property("p_u32s") .temporal() - .last() + .latest() .min() .eq(Prop::U32(1)); let expected = vec!["n1", "n3", "n10"]; @@ -5750,11 +5750,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_min_u64s() { + fn test_node_property_temporal_latest_min_u64s() { let filter = NodeFilter .property("p_u64s") .temporal() - .last() + .latest() .min() .eq(Prop::U64(10)); let expected = vec!["n4"]; @@ -5762,11 +5762,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_min_i32s() { + fn test_node_property_temporal_latest_min_i32s() { let filter = NodeFilter .property("p_i32s") .temporal() - .last() + .latest() .min() .eq(Prop::I32(-2)); let expected = vec!["n6"]; @@ -5786,11 +5786,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_min_f32s() { + fn test_node_property_temporal_latest_min_f32s() { let filter = NodeFilter .property("p_f32s") .temporal() - .last() + .latest() .min() .eq(Prop::F32(10.0)); let expected = vec!["n4"]; @@ -5798,11 +5798,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_min_f64s() { + fn test_node_property_temporal_latest_min_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .last() + .latest() .min() .eq(Prop::F64(40.0)); let expected = vec!["n3", "n10"]; @@ -5811,11 +5811,11 @@ mod test_node_property_filter_agg { // ------ Temporal last: MAX ------ #[test] - fn test_node_property_temporal_last_max_u8s() { + fn test_node_property_temporal_latest_max_u8s() { let filter = NodeFilter .property("p_u8s") .temporal() - .last() + .latest() .max() .eq(Prop::U8(4)); let expected = vec!["n1"]; @@ -5823,11 +5823,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_max_u16s() { + fn test_node_property_temporal_latest_max_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .last() + .latest() .max() .eq(Prop::U16(3)); let expected = vec!["n3", "n10"]; @@ -5871,11 +5871,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_max_i64s() { + fn test_node_property_temporal_latest_max_i64s() { let filter = NodeFilter .property("p_i64s") .temporal() - .last() + .latest() .max() .eq(Prop::I64(2)); let expected = vec!["n3", "n10"]; @@ -5895,11 +5895,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_max_f64s() { + fn test_node_property_temporal_latest_max_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .last() + .latest() .max() .eq(Prop::F64(50.0)); let expected = vec!["n1", "n2", "n3", "n10"]; @@ -5908,11 +5908,11 @@ mod test_node_property_filter_agg { // ------ Temporal last: LEN ------ #[test] - fn test_node_property_temporal_last_len_u8s() { + fn test_node_property_temporal_latest_len_u8s() { let filter = NodeFilter .property("p_u8s") .temporal() - .last() + .latest() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n10"]; @@ -5920,11 +5920,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_len_u16s() { + fn test_node_property_temporal_latest_len_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .last() + .latest() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n10"]; @@ -5932,11 +5932,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_len_u32s() { + fn test_node_property_temporal_latest_len_u32s() { let filter = NodeFilter .property("p_u32s") .temporal() - .last() + .latest() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n10"]; @@ -5944,11 +5944,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_len_u64s() { + fn test_node_property_temporal_latest_len_u64s() { let filter = NodeFilter .property("p_u64s") .temporal() - .last() + .latest() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n4", "n10"]; @@ -5956,11 +5956,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_len_i32s() { + fn test_node_property_temporal_latest_len_i32s() { let filter = NodeFilter .property("p_i32s") .temporal() - .last() + .latest() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n4", "n6", "n10"]; @@ -5968,11 +5968,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_len_i64s() { + fn test_node_property_temporal_latest_len_i64s() { let filter = NodeFilter .property("p_i64s") .temporal() - .last() + .latest() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n10"]; @@ -5980,11 +5980,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_len_f32s() { + fn test_node_property_temporal_latest_len_f32s() { let filter = NodeFilter .property("p_f32s") .temporal() - .last() + .latest() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n4", "n10"]; @@ -5992,11 +5992,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_last_len_f64s() { + fn test_node_property_temporal_latest_len_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .last() + .latest() .len() .eq(Prop::U64(2)); let expected = vec!["n3", "n10"]; @@ -6490,11 +6490,11 @@ mod test_node_property_filter_agg { // ------ Temporal first: SUM ------ #[test] - fn test_node_property_temporal_first_sum_u8s() { + fn test_node_property_temporal_earliest_sum_u8s() { let filter = NodeFilter .property("p_u8s") .temporal() - .first() + .earliest() .sum() .eq(Prop::U64(6)); let expected = vec!["n1", "n10", "n3"]; @@ -6502,11 +6502,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_sum_u16s() { + fn test_node_property_temporal_earliest_sum_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .first() + .earliest() .sum() .eq(Prop::U64(6)); let expected = vec!["n1", "n10", "n3"]; @@ -6514,11 +6514,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_sum_u32s() { + fn test_node_property_temporal_earliest_sum_u32s() { let filter = NodeFilter .property("p_u32s") .temporal() - .first() + .earliest() .sum() .eq(Prop::U64(6)); let expected = vec!["n1", "n10", "n3"]; @@ -6526,11 +6526,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_sum_u64s() { + fn test_node_property_temporal_earliest_sum_u64s() { let filter = NodeFilter .property("p_u64s") .temporal() - .first() + .earliest() .sum() .eq(Prop::U64(6)); let expected = vec!["n1", "n10", "n3", "n4"]; @@ -6538,11 +6538,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_sum_i32s() { + fn test_node_property_temporal_earliest_sum_i32s() { let filter = NodeFilter .property("p_i32s") .temporal() - .first() + .earliest() .sum() .eq(Prop::I64(6)); let expected = vec!["n1", "n10", "n3", "n4"]; @@ -6550,11 +6550,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_sum_i64s() { + fn test_node_property_temporal_earliest_sum_i64s() { let filter = NodeFilter .property("p_i64s") .temporal() - .first() + .earliest() .sum() .eq(Prop::I64(0)); let expected = vec!["n3", "n10"]; @@ -6562,11 +6562,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_sum_f32s() { + fn test_node_property_temporal_earliest_sum_f32s() { let filter = NodeFilter .property("p_f32s") .temporal() - .first() + .earliest() .sum() .eq(Prop::F64(6.5)); let expected = vec!["n1", "n10", "n3", "n4"]; @@ -6574,11 +6574,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_sum_f64s() { + fn test_node_property_temporal_earliest_sum_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .first() + .earliest() .sum() .eq(Prop::F64(90.0)); let expected = vec!["n1", "n10", "n3"]; @@ -6587,11 +6587,11 @@ mod test_node_property_filter_agg { // ------ Temporal first: AVG ------ #[test] - fn test_node_property_temporal_first_avg_u8s() { + fn test_node_property_temporal_earliest_avg_u8s() { let filter = NodeFilter .property("p_u8s") .temporal() - .first() + .earliest() .avg() .eq(Prop::F64(2.0)); let expected = vec!["n1", "n10", "n3"]; @@ -6599,11 +6599,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_avg_u16s() { + fn test_node_property_temporal_earliest_avg_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .first() + .earliest() .avg() .eq(Prop::F64(2.0)); let expected = vec!["n1", "n10", "n3"]; @@ -6611,11 +6611,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_avg_u32s() { + fn test_node_property_temporal_earliest_avg_u32s() { let filter = NodeFilter .property("p_u32s") .temporal() - .first() + .earliest() .avg() .eq(Prop::F64(2.0)); let expected = vec!["n1", "n10", "n3"]; @@ -6623,11 +6623,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_avg_u64s() { + fn test_node_property_temporal_earliest_avg_u64s() { let filter = NodeFilter .property("p_u64s") .temporal() - .first() + .earliest() .avg() .eq(Prop::F64(2.0)); let expected = vec!["n1", "n10", "n3", "n4"]; @@ -6635,11 +6635,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_avg_i32s() { + fn test_node_property_temporal_earliest_avg_i32s() { let filter = NodeFilter .property("p_i32s") .temporal() - .first() + .earliest() .avg() .eq(Prop::F64(2.0)); let expected = vec!["n1", "n10", "n3", "n4"]; @@ -6647,11 +6647,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_avg_i64s() { + fn test_node_property_temporal_earliest_avg_i64s() { let filter = NodeFilter .property("p_i64s") .temporal() - .first() + .earliest() .avg() .eq(Prop::F64(0.0)); let expected = vec!["n3", "n10"]; @@ -6659,11 +6659,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_avg_f32s() { + fn test_node_property_temporal_earliest_avg_f32s() { let filter = NodeFilter .property("p_f32s") .temporal() - .first() + .earliest() .avg() .eq(Prop::F64(2.1666666666666665)); let expected = vec!["n1", "n10", "n3", "n4"]; @@ -6671,11 +6671,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_avg_f64s() { + fn test_node_property_temporal_earliest_avg_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .first() + .earliest() .avg() .eq(Prop::F64(45.0)); let expected = vec!["n1", "n10", "n3"]; @@ -6696,11 +6696,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_min_u16s() { + fn test_node_property_temporal_earliest_min_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .first() + .earliest() .min() .eq(Prop::U16(1)); let expected = vec!["n1", "n10"]; @@ -6708,11 +6708,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_min_u32s() { + fn test_node_property_temporal_earliest_min_u32s() { let filter = NodeFilter .property("p_u32s") .temporal() - .first() + .earliest() .min() .eq(Prop::U32(1)); let expected = vec!["n1", "n10"]; @@ -6720,11 +6720,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_min_u64s() { + fn test_node_property_temporal_earliest_min_u64s() { let filter = NodeFilter .property("p_u64s") .temporal() - .first() + .earliest() .min() .eq(Prop::U64(1)); let expected = vec!["n1", "n10", "n2", "n4", "n5"]; @@ -6744,11 +6744,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_min_i64s() { + fn test_node_property_temporal_earliest_min_i64s() { let filter = NodeFilter .property("p_i64s") .temporal() - .first() + .earliest() .min() .eq(Prop::I64(-3)); let expected = vec!["n10", "n3"]; @@ -6768,11 +6768,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_min_f64s() { + fn test_node_property_temporal_earliest_min_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .first() + .earliest() .min() .eq(Prop::F64(30.0)); let expected = vec!["n2", "n3"]; @@ -6781,11 +6781,11 @@ mod test_node_property_filter_agg { // ------ Temporal first: MAX ------ #[test] - fn test_node_property_temporal_first_max_u8s() { + fn test_node_property_temporal_earliest_max_u8s() { let filter = NodeFilter .property("p_u8s") .temporal() - .first() + .earliest() .max() .eq(Prop::U8(3)); let expected = vec!["n1", "n10"]; @@ -6793,11 +6793,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_max_u16s() { + fn test_node_property_temporal_earliest_max_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .first() + .earliest() .max() .eq(Prop::U16(3)); let expected = vec!["n1", "n10"]; @@ -6805,11 +6805,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_max_u32s() { + fn test_node_property_temporal_earliest_max_u32s() { let filter = NodeFilter .property("p_u32s") .temporal() - .first() + .earliest() .max() .eq(Prop::U32(3)); let expected = vec!["n1", "n10"]; @@ -6817,11 +6817,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_max_u64s() { + fn test_node_property_temporal_earliest_max_u64s() { let filter = NodeFilter .property("p_u64s") .temporal() - .first() + .earliest() .max() .eq(Prop::U64(4)); let expected = vec!["n2"]; @@ -6829,11 +6829,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_max_i32s() { + fn test_node_property_temporal_earliest_max_i32s() { let filter = NodeFilter .property("p_i32s") .temporal() - .first() + .earliest() .max() .eq(Prop::I32(3)); let expected = vec!["n1", "n10", "n4", "n6"]; @@ -6841,11 +6841,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_max_i64s() { + fn test_node_property_temporal_earliest_max_i64s() { let filter = NodeFilter .property("p_i64s") .temporal() - .first() + .earliest() .max() .eq(Prop::I64(2)); let expected = vec!["n10"]; @@ -6853,11 +6853,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_max_f32s() { + fn test_node_property_temporal_earliest_max_f32s() { let filter = NodeFilter .property("p_f32s") .temporal() - .first() + .earliest() .max() .eq(Prop::F32(3.5)); let expected = vec!["n1", "n10", "n4"]; @@ -6865,11 +6865,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_max_f64s() { + fn test_node_property_temporal_earliest_max_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .first() + .earliest() .max() .eq(Prop::F64(50.0)); let expected = vec!["n1", "n10", "n2"]; @@ -6878,11 +6878,11 @@ mod test_node_property_filter_agg { // ------ Temporal first: LEN ------ #[test] - fn test_node_property_temporal_first_len_u8s() { + fn test_node_property_temporal_earliest_len_u8s() { let filter = NodeFilter .property("p_u8s") .temporal() - .first() + .earliest() .len() .eq(Prop::U64(3)); let expected = vec!["n1", "n10", "n3"]; @@ -6890,11 +6890,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_len_u16s() { + fn test_node_property_temporal_earliest_len_u16s() { let filter = NodeFilter .property("p_u16s") .temporal() - .first() + .earliest() .len() .eq(Prop::U64(3)); let expected = vec!["n1", "n10", "n3"]; @@ -6902,11 +6902,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_len_u32s() { + fn test_node_property_temporal_earliest_len_u32s() { let filter = NodeFilter .property("p_u32s") .temporal() - .first() + .earliest() .len() .eq(Prop::U64(3)); let expected = vec!["n1", "n10", "n3"]; @@ -6914,11 +6914,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_len_u64s() { + fn test_node_property_temporal_earliest_len_u64s() { let filter = NodeFilter .property("p_u64s") .temporal() - .first() + .earliest() .len() .eq(Prop::U64(4)); let expected = vec!["n2"]; @@ -6926,11 +6926,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_len_i32s() { + fn test_node_property_temporal_earliest_len_i32s() { let filter = NodeFilter .property("p_i32s") .temporal() - .first() + .earliest() .len() .eq(Prop::U64(3)); let expected = vec!["n1", "n10", "n3", "n4", "n6"]; @@ -6938,11 +6938,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_len_i64s() { + fn test_node_property_temporal_earliest_len_i64s() { let filter = NodeFilter .property("p_i64s") .temporal() - .first() + .earliest() .len() .eq(Prop::U64(2)); let expected = vec!["n5"]; @@ -6950,11 +6950,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_len_f32s() { + fn test_node_property_temporal_earliest_len_f32s() { let filter = NodeFilter .property("p_f32s") .temporal() - .first() + .earliest() .len() .eq(Prop::U64(3)); let expected = vec!["n1", "n10", "n3", "n4"]; @@ -6962,11 +6962,11 @@ mod test_node_property_filter_agg { } #[test] - fn test_node_property_temporal_first_len_f64s() { + fn test_node_property_temporal_earliest_len_f64s() { let filter = NodeFilter .property("p_f64s") .temporal() - .first() + .earliest() .len() .eq(Prop::U64(2)); let expected = vec!["n1", "n10", "n3"]; @@ -7876,11 +7876,11 @@ mod test_node_property_filter_agg { // ------ Temporal First: any ------ #[test] - fn test_node_temporal_property_first_any() { + fn test_node_temporal_property_earliest_any() { let filter = NodeFilter .property("p_bools") .temporal() - .first() + .earliest() .eq(false) .any(); let expected = vec!["n1", "n10", "n2", "n3", "n4"]; @@ -7902,11 +7902,11 @@ mod test_node_property_filter_agg { // ------ Temporal last: any ------ #[test] - fn test_node_temporal_property_last_any() { + fn test_node_temporal_property_latest_any() { let filter = NodeFilter .property("p_f32s") .temporal() - .last() + .latest() .eq(Prop::F32(3.5)) .any(); let expected = vec!["n1", "n10", "n3"]; diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index 8590b21a96..a3a237617c 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -91,6 +91,9 @@ pub trait Leaf: Clone + Debug + PartialEq + Send + Sync + 'static { /// Whether the read is scoped by a view. fn has_view(&self) -> bool; + /// Whether the read is the history of a temporal property. + fn is_temporal(&self) -> bool; + /// The latest value of a property, or its history when `temporal`, seen /// through `views`. fn property(views: Vec, name: String, temporal: bool) -> Self; @@ -218,6 +221,10 @@ impl Leaf for NodeLeaf { !self.views().is_empty() } + fn is_temporal(&self) -> bool { + matches!(self, NodeLeaf::Property { temporal: true, .. }) + } + fn property(views: Vec, name: String, temporal: bool) -> Self { NodeLeaf::Property { views, @@ -325,6 +332,14 @@ impl Leaf for EdgeLeaf { } } + fn is_temporal(&self) -> bool { + match self { + EdgeLeaf::Property { temporal, .. } => *temporal, + EdgeLeaf::Src(inner) | EdgeLeaf::Dst(inner) => inner.is_temporal_history(), + _ => false, + } + } + fn property(views: Vec, name: String, temporal: bool) -> Self { EdgeLeaf::Property { views, @@ -401,6 +416,10 @@ impl Leaf for ExplodedEdgeLeaf { !self.views().is_empty() } + fn is_temporal(&self) -> bool { + matches!(self, ExplodedEdgeLeaf::Property { temporal: true, .. }) + } + fn property(views: Vec, name: String, temporal: bool) -> Self { ExplodedEdgeLeaf::Property { views, @@ -453,6 +472,11 @@ impl ExplodedEdgeLeaf { // ── values ─────────────────────────────────────────────────────────────────── impl Expr { + /// Whether this is the history of a temporal property, read as is. + pub fn is_temporal_history(&self) -> bool { + matches!(self, Expr::Read(leaf) if leaf.is_temporal()) + } + /// The erased, compilable value this expression stands for. pub fn compile_value(&self) -> Result, GraphError> { let entity = L::ENTITY; @@ -460,6 +484,12 @@ impl Expr { Expr::Const(value) => Arc::new(value.clone()), Expr::Read(leaf) => leaf.compile()?, Expr::Agg(agg, inner) => { + if matches!(agg, Agg::Earliest | Agg::Latest) && !inner.is_temporal_history() { + return Err(invalid( + "earliest() and latest() pick an update of a temporal history; use \ + first() or last() for the elements of a list", + )); + } let op = inner.compile_value()?; match agg { Agg::Sum => Arc::new(op.sum()), @@ -469,6 +499,8 @@ impl Expr { Agg::First => Arc::new(op.first()), Agg::Last => Arc::new(op.last()), Agg::Len => Arc::new(op.len()), + Agg::Earliest => Arc::new(op.earliest()), + Agg::Latest => Arc::new(op.latest()), } } Expr::Cmp(op, lhs, rhs) => Arc::new(CmpExpr { diff --git a/raphtory/src/db/graph/views/filter/model/expr/convert.rs b/raphtory/src/db/graph/views/filter/model/expr/convert.rs index b481968a5d..c2d5bfdeb6 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/convert.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/convert.rs @@ -33,9 +33,9 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_expr::{ - AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, ConstExpr, DegreeExpr, EntityExpr, - FirstExpr, LastExpr, LenExpr, Marker, MaxExpr, MinExpr, PropValueSetExpr, - Scoped, StringExpr, SumExpr, TemporalPropExpr, UnaryExpr, + AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, ConstExpr, DegreeExpr, EarliestExpr, + EntityExpr, FirstExpr, LastExpr, LatestExpr, LenExpr, Marker, MaxExpr, MinExpr, + PropValueSetExpr, Scoped, StringExpr, SumExpr, TemporalPropExpr, UnaryExpr, }, node_filter::NodeFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, @@ -372,6 +372,8 @@ agg_to_expr!( FirstExpr => First, LastExpr => Last, LenExpr => Len, + EarliestExpr => Earliest, + LatestExpr => Latest, ); impl> ToExpr for AnyExpr { diff --git a/raphtory/src/db/graph/views/filter/model/expr/display.rs b/raphtory/src/db/graph/views/filter/model/expr/display.rs index c3c8be9d15..6625c8de14 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/display.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/display.rs @@ -129,6 +129,8 @@ impl Display for Expr { Agg::First => "FIRST", Agg::Last => "LAST", Agg::Len => "LEN", + Agg::Earliest => "EARLIEST", + Agg::Latest => "LATEST", }; write!(f, "{name}({e})") } diff --git a/raphtory/src/db/graph/views/filter/model/expr/mod.rs b/raphtory/src/db/graph/views/filter/model/expr/mod.rs index 2e152ec17e..4d01275609 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/mod.rs @@ -61,6 +61,10 @@ pub enum Field { } /// A reduction over a list-valued expression. +/// +/// `Sum`, `Avg`, `Min`, `Max`, `First`, `Last` and `Len` reduce the innermost +/// lists: on a history of list values they answer per update. `Earliest` and +/// `Latest` pick one update of a temporal history, whatever its type. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Agg { @@ -71,6 +75,8 @@ pub enum Agg { First, Last, Len, + Earliest, + Latest, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/raphtory/src/db/graph/views/filter/model/expr/tests.rs b/raphtory/src/db/graph/views/filter/model/expr/tests.rs index 2f213234fe..c04850111a 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/tests.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/tests.rs @@ -514,3 +514,97 @@ fn before_and_at_agree_with_the_graph_views() { ["eve->fay"] ); } + +/// Aggregates reduce the innermost list, so on a list-valued history they +/// answer per update; `earliest` and `latest` pick an update as it is. +#[test] +fn aggregates_reduce_inside_each_update_and_earliest_picks_one() { + let g = graph(); + let scores = |agg: Agg| Expr::Agg(agg, Box::new(history("scores"))); + // eve.scores is [1, 2] at 0 and [5, 5] at 1. + let eve = ["eve"]; + let none: [&str; 0] = []; + assert_eq!( + nodes( + &g, + &node(Expr::Any(Box::new(cmp( + CmpOp::Eq, + scores(Agg::First), + c(1i64) + )))) + ), + eve + ); + assert_eq!( + nodes( + &g, + &node(Expr::All(Box::new(cmp( + CmpOp::Eq, + scores(Agg::Last), + c(5i64) + )))) + ), + none + ); + assert_eq!( + nodes( + &g, + &node(Expr::Any(Box::new(cmp( + CmpOp::Eq, + scores(Agg::Len), + c(2i64) + )))) + ), + eve + ); + let list = |items: &[i64]| { + Prop::List( + items + .iter() + .map(|v| Prop::I64(*v)) + .collect::>() + .into(), + ) + }; + assert_eq!( + nodes( + &g, + &node(cmp( + CmpOp::Eq, + scores(Agg::Earliest), + Expr::Const(list(&[1, 2])) + )) + ), + eve + ); + assert_eq!( + nodes( + &g, + &node(cmp( + CmpOp::Eq, + scores(Agg::Latest), + Expr::Const(list(&[5, 5])) + )) + ), + eve + ); + // On a scalar history the two readings agree. + let score = |agg: Agg| Expr::Agg(agg, Box::new(history("score"))); + assert_eq!( + nodes(&g, &node(cmp(CmpOp::Eq, score(Agg::Earliest), c(3.0)))), + nodes(&g, &node(cmp(CmpOp::Eq, score(Agg::First), c(3.0)))) + ); + // An update is only a thing on a temporal history. + let msg = error( + &g, + &node(cmp( + CmpOp::Eq, + Expr::Agg(Agg::Latest, Box::new(prop("scores"))), + c(1i64), + )), + ); + assert!( + msg.contains("earliest() and latest() pick an update"), + "{msg}" + ); +} 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 c2966fa8b1..480bcca490 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 @@ -63,11 +63,11 @@ use super::{ ops::{ - AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NodeIdOp, SumNodeOp, - TemporalNodePropOp, + AvgNodeOp, EarliestNodeOp, FirstNodeOp, LastNodeOp, LatestNodeOp, LenNodeOp, MaxNodeOp, + MinNodeOp, NodeIdOp, SumNodeOp, TemporalNodePropOp, }, - AvgEdgeOp, CreateOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, - PredicateLhs, SumEdgeOp, + AvgEdgeOp, CreateOp, EarliestEdgeOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LatestEdgeOp, + LenEdgeOp, MaxEdgeOp, MinEdgeOp, PredicateLhs, SumEdgeOp, }; use crate::{ db::{ @@ -586,6 +586,12 @@ pub trait EntityAggOps: EntityExpr + Sized { fn len(self) -> LenExpr { LenExpr(self) } + fn earliest(self) -> EarliestExpr { + EarliestExpr(self) + } + fn latest(self) -> LatestExpr { + LatestExpr(self) + } } macro_rules! impl_agg_expr { @@ -643,6 +649,8 @@ impl_agg_expr!(MaxExpr, MaxNodeOp, MaxEdgeOp, "max()"); impl_agg_expr!(FirstExpr, FirstNodeOp, FirstEdgeOp, "first()"); impl_agg_expr!(LastExpr, LastNodeOp, LastEdgeOp, "last()"); impl_agg_expr!(LenExpr, LenNodeOp, LenEdgeOp, "len()"); +impl_agg_expr!(EarliestExpr, EarliestNodeOp, EarliestEdgeOp, "earliest()"); +impl_agg_expr!(LatestExpr, LatestNodeOp, LatestEdgeOp, "latest()"); // `any()` / `all()` after a comparison: they collapse an element-wise result. impl_agg_expr!(AnyExpr); impl_agg_expr!(AllExpr); 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 index ad8a4ecbc0..e04b07ca70 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -440,28 +440,35 @@ impl_agg_entity_op!( FirstNodeOp, FirstEdgeOp, |pt| agg_out_type(pt, None), - |vals| { - // Pick the first temporal entry as-is (whether scalar or list). - // aggregate_values would recurse into list entries and pick the first - // *element* within each entry, which is wrong for list-typed properties. - match vals? { - Prop::List(x) => x.iter_all().find_map(|v| v), - _ => None, - } - } + |vals| { aggregate_list_values(vals, &|mut pi| pi.next()) } ); impl_agg_entity_op!( LastNodeOp, LastEdgeOp, |pt| agg_out_type(pt, None), - |vals| { - // Pick the last temporal entry as-is (whether scalar or list). - match vals? { - Prop::List(x) => x.iter_all().rev().find_map(|v| v), - _ => None, - } - } + |vals| { aggregate_list_values(vals, &|mut pi| pi.next_back()) } ); +/// The type one update of a history has: the history's element type. +fn update_type(pt: PropType) -> PropType { + match pt { + PropType::List(inner) => *inner, + other => other, + } +} +impl_agg_entity_op!(EarliestNodeOp, EarliestEdgeOp, update_type, |vals| { + // The earliest update as it is, scalar or list. + match vals? { + Prop::List(x) => x.iter_all().find_map(|v| v), + _ => None, + } +}); +impl_agg_entity_op!(LatestNodeOp, LatestEdgeOp, update_type, |vals| { + // The latest update as it is, scalar or list. + match vals? { + Prop::List(x) => x.iter_all().rev().find_map(|v| v), + _ => None, + } +}); impl_agg_entity_op!( LenNodeOp, LenEdgeOp, diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index 93c0d4535a..5a3f6cc413 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -409,7 +409,7 @@ pub fn scan_f64_sum_count>( /// properties and applies `op` per element list. pub fn aggregate_list_values( vals: Option, - op: &dyn Fn(Box + '_>) -> Option, + op: &dyn Fn(Box + '_>) -> Option, ) -> Option { match vals? { Prop::List(x) => match x.dtype() { diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 97bb0ed905..c4089d2d6e 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -543,7 +543,9 @@ impl PyExpr { self.agg(Agg::Max) } - /// Selects the first element when the value is list-like. + /// Selects the first element of each innermost list. On the history of a + /// list-valued property that is one answer per update; `earliest()` picks + /// the first update instead. /// /// Returns: /// filter.Expr: @@ -551,7 +553,9 @@ impl PyExpr { self.agg(Agg::First) } - /// Selects the last element when the value is list-like. + /// Selects the last element of each innermost list. On the history of a + /// list-valued property that is one answer per update; `latest()` picks + /// the last update instead. /// /// Returns: /// filter.Expr: @@ -559,7 +563,7 @@ impl PyExpr { self.agg(Agg::Last) } - /// Selects the number of elements when the value is list-like. + /// Selects the number of elements of each innermost list. /// /// Returns: /// filter.Expr: @@ -567,6 +571,24 @@ impl PyExpr { self.agg(Agg::Len) } + /// The earliest update of a temporal history, whatever its type: on a + /// list-valued property that is the whole first list. + /// + /// Returns: + /// filter.Expr: + fn earliest(&self) -> PyExpr { + self.agg(Agg::Earliest) + } + + /// The latest update of a temporal history, whatever its type: on a + /// list-valued property that is the whole last list. + /// + /// Returns: + /// filter.Expr: + fn latest(&self) -> PyExpr { + self.agg(Agg::Latest) + } + fn __and__<'py>(&self, py: Python<'py>, other: ExprOrFilter) -> PyResult> { self.combine(py, other, true) } diff --git a/raphtory/src/python/filter/repr.rs b/raphtory/src/python/filter/repr.rs index d86fcc5026..c55c828bd5 100644 --- a/raphtory/src/python/filter/repr.rs +++ b/raphtory/src/python/filter/repr.rs @@ -136,6 +136,8 @@ fn render(py: Python<'_>, expr: &Expr, entity: Entity) -> PyResult "first", Agg::Last => "last", Agg::Len => "len", + Agg::Earliest => "earliest", + Agg::Latest => "latest", }; Rendered::chain(format!("{}.{name}()", render(py, e, entity)?.atom())) } From 5f1dace99494c013ce5af5acfad1c9492fcd9724 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:01:19 +0100 Subject: [PATCH 143/155] answer indexed property tests from the storage index A node predicate on a plain property, metadata entry, latest update or name pattern, with a constant on the other side, asks the storage for its index candidates when the filter is built and starts from them instead of every node; a history under any() asks for every value ever held. apply still runs on each candidate, so the index only narrows. --- .../graph/views/filter/model/expr/compile.rs | 400 ++++++++++++++++-- .../graph/views/filter/model/node_expr/ops.rs | 12 +- 2 files changed, 377 insertions(+), 35 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index a3a237617c..f89352c3a0 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -16,9 +16,9 @@ use crate::{ api::{ state::{ ops::{filter::NodeExistsOp, NodeFilterOp}, - NodeOp, + Index, NodeOp, }, - view::internal::{DynGraphArc, GraphView, NodeList}, + view::internal::{DynGraphArc, GraphView, InnerFilterOps, NodeList}, }, graph::views::filter::{ edge_expr_filtered_graph::EdgeExprFilteredGraph, @@ -43,7 +43,7 @@ use crate::{ node_expr::{ ops::{ broadcast_binary, broadcast_unary, gid_for_id_lookup, AllEdgeOp, AllNodeOp, - AnyEdgeOp, AnyNodeOp, IdDomainNodeOp, + AnyEdgeOp, AnyNodeOp, DomainNodeOp, }, CreateOp, DynCreateOp, EntityExpr, Scoped, }, @@ -63,14 +63,17 @@ use crate::{ }; use raphtory_api::core::{ entities::{ - properties::prop::{prop_hashable::HashableProp, Prop, PropType}, + properties::{ + meta::NODE_ID_PROP_ID, + prop::{prop_hashable::HashableProp, Prop, PropType}, + }, LayerId, GID, VID, }, storage::timeindex::EventTime, Direction, }; use raphtory_core::entities::nodes::node_ref::AsNodeRef; -use raphtory_storage::graph::graph::GraphStorage; +use raphtory_storage::graph::graph::{GraphStorage, NodePropPredicate, NodePropSemantics}; use std::{collections::HashSet, fmt::Debug, sync::Arc}; use storage::EdgeEntryRef; @@ -1308,50 +1311,53 @@ impl CreateOp for BoolNotExpr { struct Predicate { entity: EntityMarker, inner: Arc, - /// Node ids the predicate names outright (`id == v`, `id in [..]`), so the - /// node filter can start from those nodes instead of scanning every one. - ids: Option>, + /// Where the node filter can start instead of at every node: the nodes the + /// predicate names by id, or the candidates a property index hands over. + pushdown: Option, } impl Predicate { - fn new(expr: &Expr, ids: Option>) -> Result { + fn new(expr: &Expr, pushdown: Option) -> Result { Ok(Predicate { entity: L::ENTITY, inner: expr.compile_value()?, - ids, + pushdown, }) } - /// The nodes the predicate names outright, resolved once so the node filter - /// can start from them. `None` when it names none, or names an id the graph's - /// id type cannot match: the filter then scans every node. - fn named_nodes(&self, graph: &G) -> Option { - let id_type = graph.id_type(); - let elems = self - .ids - .as_ref()? - .iter() - .map(|v| gid_for_id_lookup(id_type, v)) - .collect::>>()? - .into_iter() - .filter_map(|gid| graph.internalise_node(gid.as_node_ref())) - .collect(); - Some(NodeList::List { elems }) + /// The nodes the filter starts from, resolved once against `graph`. `None` + /// when nothing narrows it: the filter then scans every node. Whatever comes + /// back is a superset of the matches, and `apply` still runs on each node. + fn narrowed_domain(&self, graph: &G) -> Option { + match self.pushdown.as_ref()? { + Pushdown::Ids(ids) => { + let id_type = graph.id_type(); + let elems = ids + .iter() + .map(|v| gid_for_id_lookup(id_type, v)) + .collect::>>()? + .into_iter() + .filter_map(|gid| graph.internalise_node(gid.as_node_ref())) + .collect(); + Some(NodeList::List { elems }) + } + Pushdown::Index(query) => query.candidates(graph), + } } fn node_filter<'graph, G: GraphView + 'graph>( &self, graph: G, ) -> Result + 'graph>, GraphError> { - let nodes = self.named_nodes(&graph); - let op = self.inner.create_node_op(graph)?; + let op = self.inner.create_node_op(graph.clone())?; + let nodes = self.narrowed_domain(&graph); require_bool( &resolved_prop_type(self.inner.prop_type(), op.prop_type()), "a filter", )?; let filter: Arc + 'graph> = Arc::new(op.map(|v| truthy(&v))); Ok(match nodes { - Some(nodes) => Arc::new(IdDomainNodeOp { + Some(nodes) => Arc::new(DomainNodeOp { nodes, inner: filter, }), @@ -1438,6 +1444,216 @@ impl CreateFilter for Predicate { } } +/// What lets a node predicate start somewhere smaller than every node. +#[derive(Clone)] +enum Pushdown { + /// `id == v` or `id in [..]` on the bare id field: those nodes. + Ids(Vec), + /// A test a property index can answer with a candidate set. + Index(IndexQuery), +} + +/// A property test in the shape the storage's index answers: one read on one +/// side, one constant on the other, no view on the read. +#[derive(Clone)] +struct IndexQuery { + read: IndexedRead, + test: IndexTest, + /// The read is the whole history under `any()`: any value ever held may match. + ever: bool, +} + +#[derive(Clone)] +enum IndexedRead { + Property { + name: String, + metadata: bool, + }, + /// The node name is its external id, which the id index covers. + Name, +} + +#[derive(Clone)] +enum IndexTest { + Eq(Prop), + Lt(Prop), + Le(Prop), + Gt(Prop), + Ge(Prop), + In(HashSet), + StartsWith(String), + EndsWith(String), + Contains(String), +} + +impl IndexTest { + fn predicate(&self) -> NodePropPredicate<'_> { + match self { + IndexTest::Eq(v) => NodePropPredicate::Eq(v), + IndexTest::Lt(v) => NodePropPredicate::Lt(v), + IndexTest::Le(v) => NodePropPredicate::Le(v), + IndexTest::Gt(v) => NodePropPredicate::Gt(v), + IndexTest::Ge(v) => NodePropPredicate::Ge(v), + IndexTest::In(values) => NodePropPredicate::In(values), + IndexTest::StartsWith(s) => NodePropPredicate::StartsWith(s), + IndexTest::EndsWith(s) => NodePropPredicate::EndsWith(s), + IndexTest::Contains(s) => NodePropPredicate::Contains(s), + } + } + + fn is_pattern(&self) -> bool { + matches!( + self, + IndexTest::StartsWith(_) | IndexTest::EndsWith(_) | IndexTest::Contains(_) + ) + } +} + +impl IndexQuery { + /// The candidates the graph's index has for this test, or `None` when no + /// index can serve it. A restricted view's latest value can differ from the + /// global one, so under a window or layer the query asks for every value + /// ever held, a superset, and drops the index's exactness claim. + fn candidates(&self, graph: &G) -> Option { + let plain_view = !graph.window_filtered() && !graph.is_layer_filtered(); + let (prop_id, metadata, semantics, exact_allowed) = match &self.read { + IndexedRead::Property { name, metadata } => { + let prop_id = graph.node_meta().get_prop_id(name, *metadata)?; + let (semantics, exact) = match (self.ever, plain_view) { + (true, plain) => (NodePropSemantics::Ever, plain), + (false, true) => (NodePropSemantics::Latest, true), + (false, false) => (NodePropSemantics::Ever, false), + }; + (prop_id, *metadata, semantics, exact) + } + IndexedRead::Name => (NODE_ID_PROP_ID, true, NodePropSemantics::Latest, false), + }; + let mut candidates = graph.core_graph().node_prop_candidates( + prop_id, + metadata, + &self.test.predicate(), + semantics, + )?; + candidates.exact &= exact_allowed; + // index candidates come ascending and deduplicated, as `from_sorted` needs + Some(NodeList::List { + elems: Index::from_sorted(candidates.vids, candidates.exact), + }) + } +} + +/// How a node predicate can be narrowed, if at all. +fn pushdown(expr: &NodeExpr) -> Option { + if let Some(ids) = named_ids(expr) { + return Some(Pushdown::Ids(ids)); + } + let (inner, ever) = match expr { + Expr::Any(inner) => (&**inner, true), + other => (other, false), + }; + let (read, test) = index_test(inner, ever)?; + // The id index answers patterns on the name; equality on it is a scan. + if matches!(read, IndexedRead::Name) && !test.is_pattern() { + return None; + } + Some(Pushdown::Index(IndexQuery { read, test, ever })) +} + +/// A comparison, string test or membership with an indexable read on one side +/// and a constant on the other. +fn index_test(expr: &NodeExpr, ever: bool) -> Option<(IndexedRead, IndexTest)> { + match expr { + Expr::Cmp(op, l, r) => { + let (read, value, op) = match (&**l, &**r) { + (read, Expr::Const(v)) => (indexed_read(read, ever)?, v, *op), + (Expr::Const(v), read) => (indexed_read(read, ever)?, v, flipped(*op)), + _ => return None, + }; + let test = match op { + CmpOp::Eq => IndexTest::Eq(value.clone()), + CmpOp::Lt => IndexTest::Lt(value.clone()), + CmpOp::Le => IndexTest::Le(value.clone()), + CmpOp::Gt => IndexTest::Gt(value.clone()), + CmpOp::Ge => IndexTest::Ge(value.clone()), + CmpOp::Ne => return None, + }; + Some((read, test)) + } + Expr::Str(op, l, r) => { + let read = indexed_read(l, ever)?; + let Expr::Const(Prop::Str(s)) = &**r else { + return None; + }; + let test = match op { + StrOp::StartsWith => IndexTest::StartsWith(s.to_string()), + StrOp::EndsWith => IndexTest::EndsWith(s.to_string()), + StrOp::Contains => IndexTest::Contains(s.to_string()), + StrOp::NotContains | StrOp::FuzzySearch { .. } => return None, + }; + Some((read, test)) + } + Expr::In { + expr, + values, + negated: false, + } => { + let read = indexed_read(expr, ever)?; + let values = values.iter().cloned().map(HashableProp).collect(); + Some((read, IndexTest::In(values))) + } + _ => None, + } +} + +/// A read the index covers: a property, a metadata entry or the name, without +/// a view. Under `any()` it is the property's history; otherwise its latest +/// value, which is also what the latest update of the history is. +fn indexed_read(expr: &NodeExpr, ever: bool) -> Option { + match expr { + Expr::Read(NodeLeaf::Property { + views, + name, + temporal, + }) if views.is_empty() && *temporal == ever => Some(IndexedRead::Property { + name: name.clone(), + metadata: false, + }), + Expr::Read(NodeLeaf::Metadata { views, name }) if views.is_empty() && !ever => { + Some(IndexedRead::Property { + name: name.clone(), + metadata: true, + }) + } + Expr::Read(NodeLeaf::Field { + views, + field: Field::Name, + }) if views.is_empty() && !ever => Some(IndexedRead::Name), + Expr::Agg(Agg::Latest, inner) if !ever => match &**inner { + Expr::Read(NodeLeaf::Property { + views, + name, + temporal: true, + }) if views.is_empty() => Some(IndexedRead::Property { + name: name.clone(), + metadata: false, + }), + _ => None, + }, + _ => None, + } +} + +/// The comparison with its sides swapped. +fn flipped(op: CmpOp) -> CmpOp { + match op { + CmpOp::Lt => CmpOp::Gt, + CmpOp::Le => CmpOp::Ge, + CmpOp::Gt => CmpOp::Lt, + CmpOp::Ge => CmpOp::Le, + same => same, + } +} + /// The node ids a node predicate names outright, if it is `id == v` or /// `id in [..]` on the bare id field. fn named_ids(expr: &NodeExpr) -> Option> { @@ -1522,7 +1738,7 @@ impl FilterExpr { /// A filter below the top level: every node but a view. fn compile_nested(&self) -> Result, GraphError> { Ok(match self { - FilterExpr::Node(expr) => Arc::new(Predicate::new(expr, named_ids(expr))?), + FilterExpr::Node(expr) => Arc::new(Predicate::new(expr, pushdown(expr))?), FilterExpr::Edge(expr) => Arc::new(Predicate::new(expr, None)?), FilterExpr::ExplodedEdge(expr) => Arc::new(Predicate::new(expr, None)?), FilterExpr::View(_) => { @@ -1688,3 +1904,129 @@ impl CreateFilter for FilterExpr { self.compile()?.create_edge_filter(graph) } } + +#[cfg(test)] +mod pushdown_tests { + use super::*; + use raphtory_api::core::entities::properties::prop::IntoProp; + + fn prop(name: &str, temporal: bool) -> NodeExpr { + Expr::Read(NodeLeaf::property(Vec::new(), name.to_owned(), temporal)) + } + + fn c(v: impl IntoProp) -> NodeExpr { + Expr::Const(v.into_prop()) + } + + fn index_of(expr: &NodeExpr) -> Option<(String, bool, bool)> { + match pushdown(expr)? { + Pushdown::Index(q) => Some(( + match q.read { + IndexedRead::Property { name, metadata } => { + if metadata { + format!("metadata {name}") + } else { + name + } + } + IndexedRead::Name => "name".to_owned(), + }, + q.ever, + q.test.is_pattern(), + )), + Pushdown::Ids(_) => None, + } + } + + #[test] + fn plain_property_tests_reach_the_index_from_either_side() { + let gt = Expr::Cmp(CmpOp::Gt, Box::new(prop("score", false)), Box::new(c(4i64))); + assert_eq!(index_of(>), Some(("score".to_owned(), false, false))); + let flipped = Expr::Cmp(CmpOp::Lt, Box::new(c(4i64)), Box::new(prop("score", false))); + assert!(matches!( + pushdown(&flipped), + Some(Pushdown::Index(IndexQuery { + test: IndexTest::Gt(_), + .. + })) + )); + let contains = Expr::Str( + StrOp::Contains, + Box::new(prop("name", false)), + Box::new(c("acme")), + ); + assert_eq!(index_of(&contains), Some(("name".to_owned(), false, true))); + let members = Expr::In { + expr: Box::new(prop("tag", false)), + values: vec!["a".into_prop(), "b".into_prop()], + negated: false, + }; + assert_eq!(index_of(&members), Some(("tag".to_owned(), false, false))); + } + + #[test] + fn a_history_under_any_asks_for_every_value_ever_held() { + let any = Expr::Any(Box::new(Expr::Cmp( + CmpOp::Eq, + Box::new(prop("score", true)), + Box::new(c(4i64)), + ))); + assert_eq!(index_of(&any), Some(("score".to_owned(), true, false))); + // The latest update of a history is the property's latest value. + let latest = Expr::Cmp( + CmpOp::Eq, + Box::new(Expr::Agg(Agg::Latest, Box::new(prop("score", true)))), + Box::new(c(4i64)), + ); + assert_eq!(index_of(&latest), Some(("score".to_owned(), false, false))); + } + + #[test] + fn what_the_index_cannot_answer_scans() { + let viewed = Expr::Cmp( + CmpOp::Eq, + Box::new(Expr::Read(NodeLeaf::property( + vec![ViewOp::Latest], + "score".to_owned(), + false, + ))), + Box::new(c(4i64)), + ); + assert!(pushdown(&viewed).is_none()); + let ne = Expr::Cmp(CmpOp::Ne, Box::new(prop("score", false)), Box::new(c(4i64))); + assert!(pushdown(&ne).is_none()); + let not_in = Expr::In { + expr: Box::new(prop("tag", false)), + values: vec!["a".into_prop()], + negated: true, + }; + assert!(pushdown(¬_in).is_none()); + let name_eq = Expr::Cmp( + CmpOp::Eq, + Box::new(Expr::Read(NodeLeaf::Field { + views: Vec::new(), + field: Field::Name, + })), + Box::new(c("bob")), + ); + assert!(pushdown(&name_eq).is_none()); + let name_prefix = Expr::Str( + StrOp::StartsWith, + Box::new(Expr::Read(NodeLeaf::Field { + views: Vec::new(), + field: Field::Name, + })), + Box::new(c("bo")), + ); + assert_eq!( + index_of(&name_prefix), + Some(("name".to_owned(), false, true)) + ); + let both_sides = Expr::Cmp( + CmpOp::Eq, + Box::new(prop("a", false)), + Box::new(prop("b", false)), + ); + assert!(pushdown(&both_sides).is_none()); + } +} 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 index e04b07ca70..6858842b28 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -547,7 +547,7 @@ pub fn broadcast_binary( } // ───────────────────────────────────────────────────────────────────────────── -// IdDomainNodeOp<'g> — id comparisons resolve their domain directly +// DomainNodeOp — a filter whose domain was worked out when it was built // ───────────────────────────────────────────────────────────────────────────── /// The GID to look up for an id comparison against `value`, or `None` when the @@ -578,16 +578,16 @@ pub(crate) fn prop_as_gid(value: &Prop) -> Option { } } -/// Wraps a compiled boolean filter whose only possible matches are `nodes`, the -/// nodes it names by id, resolved when the filter was built: `domain` hands them -/// over instead of scanning every node. +/// Wraps a compiled boolean filter whose matches all lie in `nodes`, worked out +/// when the filter was built from the ids it names or a property index: `domain` +/// hands them over instead of scanning every node, and `apply` still decides. #[derive(Clone)] -pub struct IdDomainNodeOp<'g> { +pub struct DomainNodeOp<'g> { pub(crate) nodes: NodeList, pub(crate) inner: Arc + 'g>, } -impl<'g> NodeOp for IdDomainNodeOp<'g> { +impl<'g> NodeOp for DomainNodeOp<'g> { type Output = bool; fn apply(&self, storage: &GraphStorage, node: VID) -> bool { From 2a2efb8bfcbb2b80ba8bc3e21b09a8a51e5bb936 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:09:28 +0100 Subject: [PATCH 144/155] stream temporal histories in aggregations and any/all tests An aggregation directly over a temporal property, or any()/all() over a comparison of one with a constant, no longer collects the history into a list first. The compiler pairs these shapes with a history stream: the aggregation folds the values as they come, reading from the end it needs, and the qualifier stops at the first value that decides the answer. The list read remains for every other consumer, and the list ops share the same reduction kernels. --- raphtory/src/db/graph/edge_reads.rs | 35 +- .../graph/views/filter/model/expr/compile.rs | 376 +++++++++++- .../db/graph/views/filter/model/expr/mod.rs | 2 + .../graph/views/filter/model/expr/stream.rs | 571 ++++++++++++++++++ .../views/filter/model/node_expr/dyn_expr.rs | 23 + .../views/filter/model/node_expr/exprs.rs | 31 +- .../graph/views/filter/model/node_expr/ops.rs | 189 +++--- 7 files changed, 1139 insertions(+), 88 deletions(-) create mode 100644 raphtory/src/db/graph/views/filter/model/expr/stream.rs diff --git a/raphtory/src/db/graph/edge_reads.rs b/raphtory/src/db/graph/edge_reads.rs index c3b904f21b..e31503c50f 100644 --- a/raphtory/src/db/graph/edge_reads.rs +++ b/raphtory/src/db/graph/edge_reads.rs @@ -22,7 +22,7 @@ use storage::EdgeEntryRef; /// Which part of an edge a read is about. #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub(crate) enum EdgeAt { +pub enum EdgeAt { Whole, Layer(LayerId), Exploded(LayerId, EventTime), @@ -104,6 +104,39 @@ pub(crate) fn temporal_hist<'a, G: GraphView>( } } +/// The history of temporal property `id`, newest first. +pub(crate) fn temporal_hist_rev<'a, G: GraphView>( + graph: &'a G, + edge: EdgeEntryRef<'a>, + at: EdgeAt, + id: usize, +) -> BoxedLIter<'a, (EventTime, Prop)> { + if !at.visible(graph) { + return iter::empty().into_dyn_boxed(); + } + let time_semantics = graph.edge_time_semantics(); + match at { + EdgeAt::Whole => time_semantics + .temporal_edge_prop_hist_rev(edge, graph, graph.layer_ids(), id) + .map(|(t, _, v)| (t, v)) + .into_dyn_boxed(), + EdgeAt::Layer(layer) => { + GenLockedIter::from((edge, LayerIds::One(layer)), move |(edge, layer_ids)| { + time_semantics + .temporal_edge_prop_hist_rev(*edge, graph, layer_ids, id) + .map(|(t, _, v)| (t, v)) + .into_dyn_boxed() + }) + .into_dyn_boxed() + } + EdgeAt::Exploded(layer, t) => time_semantics + .temporal_edge_prop_exploded(edge, graph, id, t, layer) + .map(|v| (t, v)) + .into_iter() + .into_dyn_boxed(), + } +} + /// The value of metadata entry `id`; an exploded instance shares its layer's. pub(crate) fn metadata( graph: &G, diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index f89352c3a0..5c2d560cfa 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -8,8 +8,11 @@ //! on the entity the expression belongs to. use super::{ - Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, FilterExpr, NodeExpr, NodeLeaf, StrOp, - ViewOp, + stream::{ + StreamedAggEdgeOp, StreamedAggNodeOp, StreamedQualEdgeOp, StreamedQualNodeOp, ValueTest, + }, + Agg, CmpOp, DynCreateHistory, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, FilterExpr, NodeExpr, + NodeLeaf, StrOp, ViewOp, }; use crate::{ db::{ @@ -50,9 +53,9 @@ use crate::{ node_filter::NodeFilter, not_filter::NotFilter, or_filter::OrFilter, - resolved_prop_type, validate_binary_op, validate_const_comparable, - validate_string_op, validate_types_comparable, DynCreateFilter, DynView, - EntityMarker, ViewWrapOps, + require_aggregable, resolved_prop_type, validate_binary_op, + validate_const_comparable, validate_string_op, validate_types_comparable, + DynCreateFilter, DynView, EntityMarker, ViewWrapOps, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, DynEdgeFilter, @@ -91,6 +94,10 @@ pub trait Leaf: Clone + Debug + PartialEq + Send + Sync + 'static { /// The erased value this read produces. fn compile(&self) -> Result, GraphError>; + /// The history this read walks, when it is the history of a temporal + /// property; a consumer that streams it need not build the list. + fn compile_history(&self) -> Option>; + /// Whether the read is scoped by a view. fn has_view(&self) -> bool; @@ -220,6 +227,17 @@ impl Leaf for NodeLeaf { }) } + fn compile_history(&self) -> Option> { + match self { + NodeLeaf::Property { + views, + name, + temporal: true, + } => Some(node_factory(views).dyn_property(name.clone()).history()), + _ => None, + } + } + fn has_view(&self) -> bool { !self.views().is_empty() } @@ -323,6 +341,29 @@ impl Leaf for EdgeLeaf { }) } + fn compile_history(&self) -> Option> { + match self { + EdgeLeaf::Property { + views, + name, + temporal: true, + } => Some( + edge_factory(false, views) + .dyn_property(name.clone()) + .history(), + ), + EdgeLeaf::Src(inner) => Some(Arc::new(EdgeEndpointWrapper::new( + inner.history()?, + Endpoint::Src, + ))), + EdgeLeaf::Dst(inner) => Some(Arc::new(EdgeEndpointWrapper::new( + inner.history()?, + Endpoint::Dst, + ))), + _ => None, + } + } + fn has_view(&self) -> bool { match self { EdgeLeaf::Property { views, .. } @@ -415,6 +456,21 @@ impl Leaf for ExplodedEdgeLeaf { }) } + fn compile_history(&self) -> Option> { + match self { + ExplodedEdgeLeaf::Property { + views, + name, + temporal: true, + } => Some( + edge_factory(true, views) + .dyn_property(name.clone()) + .history(), + ), + _ => None, + } + } + fn has_view(&self) -> bool { !self.views().is_empty() } @@ -480,6 +536,46 @@ impl Expr { matches!(self, Expr::Read(leaf) if leaf.is_temporal()) } + /// The history this expression reads as is, for a consumer that walks + /// it instead of taking the list. + fn history(&self) -> Option> { + match self { + Expr::Read(leaf) => leaf.compile_history(), + _ => None, + } + } + + /// `any()`/`all()` over a comparison of a history with a constant walks + /// the history: the test to apply to each value, and the history. + fn streamed_test(&self) -> Option<(Arc, QualTest)> { + match self { + Expr::Cmp(op, lhs, rhs) => match (&**lhs, &**rhs) { + (read, Expr::Const(constant)) => Some(( + read.history()?, + QualTest::Cmp(binary_op(*op), constant.clone()), + )), + (Expr::Const(constant), read) => Some(( + read.history()?, + QualTest::Cmp(binary_op(flipped(*op)), constant.clone()), + )), + _ => None, + }, + Expr::Str(op, lhs, rhs) => match &**rhs { + Expr::Const(constant) => Some(( + lhs.history()?, + QualTest::Str(string_op(op), constant.clone()), + )), + _ => None, + }, + Expr::In { + expr, + values, + negated, + } => Some((expr.history()?, QualTest::In(values.clone(), *negated))), + _ => None, + } + } + /// The erased, compilable value this expression stands for. pub fn compile_value(&self) -> Result, GraphError> { let entity = L::ENTITY; @@ -493,6 +589,13 @@ impl Expr { first() or last() for the elements of a list", )); } + if let Some(history) = inner.history() { + return Ok(Arc::new(StreamedAggExpr { + history, + agg: *agg, + entity, + })); + } let op = inner.compile_value()?; match agg { Agg::Sum => Arc::new(op.sum()), @@ -538,16 +641,24 @@ impl Expr { op: UnaryOp::IsNone, entity, }), - Expr::Any(inner) => Arc::new(QualExpr { - inner: inner.compile_value()?, - all: false, - entity, - }), - Expr::All(inner) => Arc::new(QualExpr { - inner: inner.compile_value()?, - all: true, - entity, - }), + Expr::Any(inner) | Expr::All(inner) => { + let all = matches!(self, Expr::All(_)); + let fallback = Arc::new(QualExpr { + inner: inner.compile_value()?, + all, + entity, + }); + match inner.streamed_test() { + Some((history, test)) => Arc::new(StreamedQualExpr { + history, + test, + all, + fallback, + entity, + }), + None => fallback, + } + } Expr::And(items) => Arc::new(BoolCombineExpr { items: items .iter() @@ -1187,6 +1298,146 @@ impl CreateOp for QualExpr { } } +/// An aggregation directly over a history: walks the history instead of +/// taking it as a list. +#[derive(Clone)] +struct StreamedAggExpr { + history: Arc, + agg: Agg, + entity: EntityMarker, +} + +impl EntityExpr for StreamedAggExpr { + type Marker = EntityMarker; + + fn entity(&self) -> EntityMarker { + self.entity + } +} + +impl StreamedAggExpr { + fn check(&self, history_type: &PropType) -> Result<(), GraphError> { + let name = match self.agg { + Agg::Sum => "sum()", + Agg::Avg => "avg()", + Agg::Min => "min()", + Agg::Max => "max()", + Agg::First => "first()", + Agg::Last => "last()", + Agg::Len => "len()", + Agg::Earliest => "earliest()", + Agg::Latest => "latest()", + }; + require_aggregable(history_type, name) + } +} + +impl CreateOp for StreamedAggExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let history = self.history.create_node_history(Arc::new(graph))?; + self.check(&history.history_type())?; + Ok(Arc::new(StreamedAggNodeOp::new(history, self.agg))) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let history = self.history.create_edge_history(Arc::new(graph))?; + self.check(&history.history_type())?; + Ok(Arc::new(StreamedAggEdgeOp::new(history, self.agg))) + } +} + +/// A test of each history value against a constant, before the property's +/// type is known. +#[derive(Clone)] +enum QualTest { + Cmp(BinaryOp, Prop), + Str(StringOp, Prop), + In(Vec, bool), +} + +impl QualTest { + /// The test to run on each value of a history of `history_type`, when + /// the history's element-wise result is one yes/no answer per value. + /// Anything else (a nested list, a mismatch) is left to the list path, + /// which reports it the way it always has. + fn value_test(&self, history_type: &PropType) -> Option { + let one_per_value = |(out, shape): (PropType, Shape)| { + shape == Shape::Elementwise && out == list(PropType::Bool) + }; + match self { + QualTest::Cmp(op, constant) => { + let shape = + comparison_shape(op, history_type, &constant.dtype(), Some(constant)).ok()?; + one_per_value(shape).then(|| ValueTest::Cmp(*op, constant.clone())) + } + QualTest::Str(op, constant) => { + let shape = string_shape(history_type, &constant.dtype(), Some(constant)).ok()?; + one_per_value(shape).then(|| ValueTest::Str(op.clone(), constant.clone())) + } + QualTest::In(values, negated) => { + let (out, shape, members) = set_shape(history_type, values); + one_per_value((out, shape)).then(|| { + ValueTest::In( + Arc::new(members.into_iter().map(HashableProp).collect()), + *negated, + ) + }) + } + } + } +} + +/// `any()` / `all()` over a comparison of a history with a constant: walks +/// the history and stops at the first value that decides the answer. +#[derive(Clone)] +struct StreamedQualExpr { + history: Arc, + test: QualTest, + all: bool, + /// The list path, for a history this test cannot walk value by value. + fallback: Arc, + entity: EntityMarker, +} +entity_expr!(StreamedQualExpr); + +impl CreateOp for StreamedQualExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let history = self.history.create_node_history(Arc::new(graph.clone()))?; + match self.test.value_test(&history.history_type()) { + Some(test) => Ok(Arc::new(StreamedQualNodeOp { + history, + test, + all: self.all, + })), + None => self.fallback.create_node_op(graph), + } + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let history = self.history.create_edge_history(Arc::new(graph.clone()))?; + match self.test.value_test(&history.history_type()) { + Some(test) => Ok(Arc::new(StreamedQualEdgeOp { + history, + test, + all: self.all, + })), + None => self.fallback.create_edge_op(graph), + } + } +} + /// `and` / `or` of yes/no values. #[derive(Clone)] struct BoolCombineExpr { @@ -1905,6 +2156,101 @@ impl CreateFilter for FilterExpr { } } +#[cfg(test)] +mod streaming_tests { + use super::*; + use raphtory_api::core::entities::properties::prop::IntoProp; + + fn prop(name: &str, temporal: bool) -> NodeExpr { + Expr::Read(NodeLeaf::property(Vec::new(), name.to_owned(), temporal)) + } + + fn c(v: impl IntoProp) -> NodeExpr { + Expr::Const(v.into_prop()) + } + + fn cmp(op: CmpOp, l: NodeExpr, r: NodeExpr) -> NodeExpr { + Expr::Cmp(op, Box::new(l), Box::new(r)) + } + + #[test] + fn a_history_read_walks_and_a_latest_value_does_not() { + assert!(prop("score", true).history().is_some()); + assert!(prop("score", false).history().is_none()); + assert!(Expr::Agg(Agg::Sum, Box::new(prop("score", true))) + .history() + .is_none()); + } + + #[test] + fn a_history_compared_with_a_constant_walks_from_either_side() { + let gt = cmp(CmpOp::Gt, prop("score", true), c(4i64)); + assert!(matches!( + gt.streamed_test(), + Some((_, QualTest::Cmp(BinaryOp::Gt, Prop::I64(4)))) + )); + let mirrored = cmp(CmpOp::Lt, c(4i64), prop("score", true)); + assert!(matches!( + mirrored.streamed_test(), + Some((_, QualTest::Cmp(BinaryOp::Gt, Prop::I64(4)))) + )); + let contains = Expr::Str( + StrOp::Contains, + Box::new(prop("name", true)), + Box::new(c("a")), + ); + assert!(matches!( + contains.streamed_test(), + Some((_, QualTest::Str(StringOp::Contains, _))) + )); + let is_in = Expr::In { + expr: Box::new(prop("score", true)), + values: vec![1i64.into_prop()], + negated: true, + }; + assert!(matches!( + is_in.streamed_test(), + Some((_, QualTest::In(_, true))) + )); + } + + #[test] + fn anything_else_under_any_keeps_the_list() { + let latest = cmp(CmpOp::Gt, prop("score", false), c(4i64)); + assert!(latest.streamed_test().is_none()); + let two_reads = cmp(CmpOp::Gt, prop("score", true), prop("other", true)); + assert!(two_reads.streamed_test().is_none()); + let aggregated = cmp( + CmpOp::Gt, + Expr::Agg(Agg::Sum, Box::new(prop("score", true))), + c(4i64), + ); + assert!(aggregated.streamed_test().is_none()); + } + + #[test] + fn only_a_one_answer_per_value_test_walks() { + let history = list(PropType::I64); + let gt = QualTest::Cmp(BinaryOp::Gt, 4i64.into_prop()); + assert!(gt.value_test(&history).is_some()); + // A constant list compares against the whole history, not each value. + let whole = QualTest::Cmp(BinaryOp::Eq, Prop::list([1i64, 2i64])); + assert!(whole.value_test(&history).is_none()); + // A mismatch is left to the list path, which reports it. + assert!(gt.value_test(&list(PropType::Str)).is_none()); + // A history of lists answers per element, not per value. + assert!(gt.value_test(&list(list(PropType::I64))).is_none()); + // A set no value can be in still answers per value, as the list path does. + let none = QualTest::In(vec!["x".into_prop()], false); + assert!( + matches!(none.value_test(&history), Some(ValueTest::In(members, false)) if members.is_empty()) + ); + // An empty set is a whole-history test, which the list path refuses. + let empty = QualTest::In(Vec::new(), false); + assert!(empty.value_test(&history).is_none()); + } +} + #[cfg(test)] mod pushdown_tests { use super::*; diff --git a/raphtory/src/db/graph/views/filter/model/expr/mod.rs b/raphtory/src/db/graph/views/filter/model/expr/mod.rs index 4d01275609..c57fd902a3 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/mod.rs @@ -24,11 +24,13 @@ mod compile; pub mod convert; mod display; +mod stream; #[cfg(test)] mod tests; pub use compile::Leaf; pub use convert::{FactoryLeaf, MarkerLeaf, ToExpr, ToFilterExpr}; +pub use stream::{DynCreateHistory, EdgeHistory, NodeHistory}; use super::DynCreateFilter; use raphtory_api::core::{ diff --git a/raphtory/src/db/graph/views/filter/model/expr/stream.rs b/raphtory/src/db/graph/views/filter/model/expr/stream.rs new file mode 100644 index 0000000000..7f3cc32034 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/expr/stream.rs @@ -0,0 +1,571 @@ +//! Aggregations and `any()`/`all()` tests that walk a temporal history +//! instead of collecting it into a list first. +//! +//! A history read on its own still produces a list, because a list is the +//! only value the engine can hand to an arbitrary consumer. When the compiler +//! sees an aggregation directly over a history, or `any()`/`all()` over a +//! comparison of a history with a constant, it builds one of the ops here +//! instead: they ask the history for a stream, from whichever end the +//! question needs, and stop as soon as the answer is known. + +use crate::{ + core::utils::iter::GenLockedIter, + db::{ + api::{ + state::ops::NodeOp, + view::{ + internal::{DynGraphArc, GraphView, NodeList}, + BoxedLIter, IntoDynBoxed, NodeViewOps, + }, + }, + graph::{ + edge_reads::{self, EdgeAt}, + views::filter::model::{ + edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, + edge_filter::{EdgeEndpointWrapper, Endpoint}, + filter_operator::{BinaryOp, Comparable, StringComparable, StringOp}, + node_expr::ops::{agg_out_pt, fold_values, reduce_list, TemporalNodePropOp}, + property_filter::evaluate::aggregate_list_values, + }, + }, + }, + errors::GraphError, + prelude::GraphViewOps, +}; +use raphtory_api::core::{ + entities::{ + properties::prop::{prop_hashable::HashableProp, Prop, PropType}, + LayerId, VID, + }, + storage::timeindex::EventTime, +}; +use raphtory_storage::graph::{edges::edge_storage_ops::EdgeStorageOps, graph::GraphStorage}; +use std::{collections::HashSet, iter, sync::Arc}; +use storage::EdgeEntryRef; + +use super::Agg; + +// ── histories ──────────────────────────────────────────────────────────────── + +/// One property's history on a node, as a stream from either end. +pub trait NodeHistory: Send + Sync { + /// The type the history has when read as one value: a list of its + /// element type. + fn history_type(&self) -> PropType; + fn domain(&self) -> NodeList; + /// Oldest first. + fn values<'a>(&'a self, node: VID) -> BoxedLIter<'a, Prop>; + /// Newest first. + fn values_rev<'a>(&'a self, node: VID) -> BoxedLIter<'a, Prop>; +} + +/// One property's history on an edge, as a stream from either end, for the +/// edge as a whole, one layer, or one exploded instance. +pub trait EdgeHistory: Send + Sync { + fn history_type(&self) -> PropType; + fn values<'a>(&'a self, edge: EdgeEntryRef<'a>, at: EdgeAt) -> BoxedLIter<'a, Prop>; + fn values_rev<'a>(&'a self, edge: EdgeEntryRef<'a>, at: EdgeAt) -> BoxedLIter<'a, Prop>; +} + +impl NodeHistory for TemporalNodePropOp { + fn history_type(&self) -> PropType { + NodeOp::prop_type(self) + } + + fn domain(&self) -> NodeList { + self.graph.node_list() + } + + fn values<'a>(&'a self, node: VID) -> BoxedLIter<'a, Prop> { + match (&&self.graph) + .node(node) + .and_then(|n| n.properties().temporal().get_by_id(self.prop_id)) + { + Some(history) => GenLockedIter::from(history, |h| h.values()).into_dyn_boxed(), + None => iter::empty().into_dyn_boxed(), + } + } + + fn values_rev<'a>(&'a self, node: VID) -> BoxedLIter<'a, Prop> { + match (&&self.graph) + .node(node) + .and_then(|n| n.properties().temporal().get_by_id(self.prop_id)) + { + Some(history) => GenLockedIter::from(history, |h| h.values_rev()).into_dyn_boxed(), + None => iter::empty().into_dyn_boxed(), + } + } +} + +impl EdgeHistory for TemporalEdgePropOp { + fn history_type(&self) -> PropType { + EdgeOp::prop_type(self) + } + + fn values<'a>(&'a self, edge: EdgeEntryRef<'a>, at: EdgeAt) -> BoxedLIter<'a, Prop> { + edge_reads::temporal_hist(&self.graph, edge, at, self.prop_id) + .map(|(_, v)| v) + .into_dyn_boxed() + } + + fn values_rev<'a>(&'a self, edge: EdgeEntryRef<'a>, at: EdgeAt) -> BoxedLIter<'a, Prop> { + edge_reads::temporal_hist_rev(&self.graph, edge, at, self.prop_id) + .map(|(_, v)| v) + .into_dyn_boxed() + } +} + +/// A node history read at an edge's source or destination. +struct EndpointHistory<'g> { + node: Arc, + endpoint: Endpoint, +} + +impl<'g> EndpointHistory<'g> { + fn node_of(&self, edge: EdgeEntryRef) -> VID { + match self.endpoint { + Endpoint::Src => edge.src(), + Endpoint::Dst => edge.dst(), + } + } +} + +impl<'g> EdgeHistory for EndpointHistory<'g> { + fn history_type(&self) -> PropType { + self.node.history_type() + } + + fn values<'a>(&'a self, edge: EdgeEntryRef<'a>, _at: EdgeAt) -> BoxedLIter<'a, Prop> { + self.node.values(self.node_of(edge)) + } + + fn values_rev<'a>(&'a self, edge: EdgeEntryRef<'a>, _at: EdgeAt) -> BoxedLIter<'a, Prop> { + self.node.values_rev(self.node_of(edge)) + } +} + +/// Builds a history against a graph: the erased form of a temporal read. +pub trait DynCreateHistory: Send + Sync + 'static { + fn create_node_history<'g>( + &self, + graph: DynGraphArc<'g>, + ) -> Result, GraphError>; + + fn create_edge_history<'g>( + &self, + graph: DynGraphArc<'g>, + ) -> Result, GraphError>; +} + +impl DynCreateHistory for Arc { + fn create_node_history<'g>( + &self, + graph: DynGraphArc<'g>, + ) -> Result, GraphError> { + self.as_ref().create_node_history(graph) + } + + fn create_edge_history<'g>( + &self, + graph: DynGraphArc<'g>, + ) -> Result, GraphError> { + self.as_ref().create_edge_history(graph) + } +} + +/// A node history read through an endpoint is an edge history; it has no +/// node form. +impl DynCreateHistory for EdgeEndpointWrapper { + fn create_node_history<'g>( + &self, + _graph: DynGraphArc<'g>, + ) -> Result, GraphError> { + Err(GraphError::InvalidFilter( + "an endpoint read is an edge expression".to_string(), + )) + } + + fn create_edge_history<'g>( + &self, + graph: DynGraphArc<'g>, + ) -> Result, GraphError> { + Ok(Arc::new(EndpointHistory { + node: self.inner.create_node_history(graph)?, + endpoint: self.endpoint(), + })) + } +} + +// ── reductions ─────────────────────────────────────────────────────────────── + +/// Whether the updates of a history of this type are lists themselves. +fn updates_are_lists(history_type: &PropType) -> bool { + matches!(history_type, PropType::List(elem) if matches!(**elem, PropType::List(_))) +} + +/// One aggregation over a history, reading from the end the aggregation +/// needs. When the updates are lists, each reduces on its own, one answer +/// per update, as the list path does. +fn reduce<'a>( + agg: Agg, + per_update: bool, + values: impl FnOnce() -> BoxedLIter<'a, Prop>, + values_rev: impl FnOnce() -> BoxedLIter<'a, Prop>, +) -> Option { + match agg { + Agg::Earliest => values().next(), + Agg::Latest => values_rev().next(), + _ if per_update => Some(Prop::List( + values() + .filter_map(|update| { + aggregate_list_values(Some(update), &|items| reduce_list(agg, items)) + }) + .collect(), + )), + Agg::Last => values_rev().next(), + _ => fold_values(agg, values()), + } +} + +#[derive(Clone)] +pub(crate) struct StreamedAggNodeOp<'g> { + history: Arc, + agg: Agg, + per_update: bool, +} + +impl<'g> StreamedAggNodeOp<'g> { + pub(crate) fn new(history: Arc, agg: Agg) -> Self { + let per_update = updates_are_lists(&history.history_type()); + Self { + history, + agg, + per_update, + } + } +} + +impl<'g> NodeOp for StreamedAggNodeOp<'g> { + type Output = Option; + + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.history.domain() + } + + fn prop_type(&self) -> PropType { + agg_out_pt(self.agg, self.history.history_type()) + } + + fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { + reduce( + self.agg, + self.per_update, + || self.history.values(node), + || self.history.values_rev(node), + ) + } +} + +#[derive(Clone)] +pub(crate) struct StreamedAggEdgeOp<'g> { + history: Arc, + agg: Agg, + per_update: bool, +} + +impl<'g> StreamedAggEdgeOp<'g> { + pub(crate) fn new(history: Arc, agg: Agg) -> Self { + let per_update = updates_are_lists(&history.history_type()); + Self { + history, + agg, + per_update, + } + } + + fn at(&self, edge: EdgeEntryRef, at: EdgeAt) -> Option { + reduce( + self.agg, + self.per_update, + || self.history.values(edge, at), + || self.history.values_rev(edge, at), + ) + } +} + +impl<'g> EdgeOp for StreamedAggEdgeOp<'g> { + type Output = Option; + + fn prop_type(&self) -> PropType { + agg_out_pt(self.agg, self.history.history_type()) + } + + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + self.at(edge, EdgeAt::Whole) + } + + fn apply_layer( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + self.at(edge, EdgeAt::Layer(layer)) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + self.at(edge, EdgeAt::Exploded(layer, t)) + } +} + +// ── qualified tests ────────────────────────────────────────────────────────── + +/// A test of one history value against a constant. +#[derive(Clone)] +pub(crate) enum ValueTest { + Cmp(BinaryOp, Prop), + Str(StringOp, Prop), + In(Arc>, bool), +} + +impl ValueTest { + fn holds(&self, value: Prop) -> bool { + match self { + ValueTest::Cmp(op, constant) => Prop::binary_cmp(op, &value, constant), + ValueTest::Str(op, constant) => Prop::string_cmp(op, &value, constant), + ValueTest::In(members, negated) => members.contains(&HashableProp(value)) != *negated, + } + } +} + +/// Whether the test holds for any value of the stream, or for every one. An +/// empty history has no value the test holds for, so `all` is false there +/// too. +fn qualified(test: &ValueTest, all: bool, mut values: BoxedLIter<'_, Prop>) -> Option { + let hit = if all { + let mut seen = false; + let every = values.all(|v| { + seen = true; + test.holds(v) + }); + seen && every + } else { + values.any(|v| test.holds(v)) + }; + Some(Prop::Bool(hit)) +} + +#[derive(Clone)] +pub(crate) struct StreamedQualNodeOp<'g> { + pub(crate) history: Arc, + pub(crate) test: ValueTest, + pub(crate) all: bool, +} + +impl<'g> NodeOp for StreamedQualNodeOp<'g> { + type Output = Option; + + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.history.domain() + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { + qualified(&self.test, self.all, self.history.values(node)) + } +} + +#[derive(Clone)] +pub(crate) struct StreamedQualEdgeOp<'g> { + pub(crate) history: Arc, + pub(crate) test: ValueTest, + pub(crate) all: bool, +} + +impl<'g> StreamedQualEdgeOp<'g> { + fn at(&self, edge: EdgeEntryRef, at: EdgeAt) -> Option { + qualified(&self.test, self.all, self.history.values(edge, at)) + } +} + +impl<'g> EdgeOp for StreamedQualEdgeOp<'g> { + type Output = Option; + + fn prop_type(&self) -> PropType { + PropType::Bool + } + + fn apply(&self, _storage: &GraphStorage, edge: EdgeEntryRef) -> Option { + self.at(edge, EdgeAt::Whole) + } + + fn apply_layer( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + ) -> Option { + self.at(edge, EdgeAt::Layer(layer)) + } + + fn apply_exploded( + &self, + _storage: &GraphStorage, + edge: EdgeEntryRef, + layer: LayerId, + t: EventTime, + ) -> Option { + self.at(edge, EdgeAt::Exploded(layer, t)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use raphtory_api::core::entities::properties::prop::IntoProp; + + fn stream(values: &[i64]) -> BoxedLIter<'static, Prop> { + values + .iter() + .map(|v| v.into_prop()) + .collect::>() + .into_iter() + .into_dyn_boxed() + } + + fn reduced(agg: Agg, values: &'static [i64]) -> Option { + reduce( + agg, + false, + || stream(values), + || { + stream(values) + .collect::>() + .into_iter() + .rev() + .into_dyn_boxed() + }, + ) + } + + #[test] + fn each_aggregation_reads_from_the_end_it_needs() { + let history = &[3i64, 1, 2]; + assert_eq!(reduced(Agg::Earliest, history), Some(3i64.into_prop())); + assert_eq!(reduced(Agg::First, history), Some(3i64.into_prop())); + assert_eq!(reduced(Agg::Latest, history), Some(2i64.into_prop())); + assert_eq!(reduced(Agg::Last, history), Some(2i64.into_prop())); + assert_eq!(reduced(Agg::Min, history), Some(1i64.into_prop())); + assert_eq!(reduced(Agg::Max, history), Some(3i64.into_prop())); + assert_eq!(reduced(Agg::Sum, history), Some(6i64.into_prop())); + assert_eq!(reduced(Agg::Avg, history), Some(2f64.into_prop())); + assert_eq!(reduced(Agg::Len, history), Some(3u64.into_prop())); + } + + #[test] + fn an_empty_history_has_no_aggregate_and_a_zero_length() { + for agg in [ + Agg::Earliest, + Agg::Latest, + Agg::First, + Agg::Last, + Agg::Min, + Agg::Max, + Agg::Sum, + Agg::Avg, + ] { + assert_eq!(reduced(agg, &[]), None, "{agg:?}"); + } + assert_eq!(reduced(Agg::Len, &[]), Some(0u64.into_prop())); + } + + #[test] + fn a_history_of_lists_reduces_each_update() { + let updates = || { + vec![ + Prop::list([1i64, 2]), + Prop::list([5i64]), + Prop::list(Vec::::new()), + ] + .into_iter() + .into_dyn_boxed() + }; + let history_type = PropType::List(Box::new(PropType::List(Box::new(PropType::I64)))); + assert!(updates_are_lists(&history_type)); + assert!(!updates_are_lists(&PropType::List(Box::new(PropType::I64)))); + let sums = reduce(Agg::Sum, true, updates, updates); + assert_eq!(sums, Some(Prop::list([3i64, 5]))); + let latest = reduce(Agg::Latest, true, updates, || { + updates() + .collect::>() + .into_iter() + .rev() + .into_dyn_boxed() + }); + assert_eq!(latest, Some(Prop::list(Vec::::new()))); + let lasts = reduce(Agg::Last, true, updates, || { + updates() + .collect::>() + .into_iter() + .rev() + .into_dyn_boxed() + }); + assert_eq!(lasts, Some(Prop::list([2i64, 5]))); + } + + #[test] + fn any_and_all_stop_at_the_deciding_value() { + let gt = ValueTest::Cmp(BinaryOp::Gt, 2i64.into_prop()); + let mut pulled = 0usize; + let counted = Box::new([1i64, 5, 7].into_iter().map(|v| { + pulled += 1; + v.into_prop() + })) as BoxedLIter<'_, Prop>; + assert_eq!(qualified(>, false, counted), Some(Prop::Bool(true))); + assert_eq!(pulled, 2); + + let mut pulled = 0usize; + let counted = Box::new([5i64, 1, 7].into_iter().map(|v| { + pulled += 1; + v.into_prop() + })) as BoxedLIter<'_, Prop>; + assert_eq!(qualified(>, true, counted), Some(Prop::Bool(false))); + assert_eq!(pulled, 2); + } + + #[test] + fn an_empty_history_satisfies_neither_any_nor_all() { + let gt = ValueTest::Cmp(BinaryOp::Gt, 2i64.into_prop()); + assert_eq!(qualified(>, false, stream(&[])), Some(Prop::Bool(false))); + assert_eq!(qualified(>, true, stream(&[])), Some(Prop::Bool(false))); + } + + #[test] + fn the_set_test_honours_negation() { + let members: Arc> = + Arc::new([HashableProp(1i64.into_prop())].into_iter().collect()); + let is_in = ValueTest::In(members.clone(), false); + let not_in = ValueTest::In(members, true); + assert_eq!( + qualified(&is_in, false, stream(&[2, 1])), + Some(Prop::Bool(true)) + ); + assert_eq!( + qualified(¬_in, true, stream(&[2, 1])), + Some(Prop::Bool(false)) + ); + assert_eq!( + qualified(¬_in, true, stream(&[2, 3])), + Some(Prop::Bool(true)) + ); + } +} diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 3b54ddce49..1bca3ea7ce 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -11,6 +11,7 @@ use crate::{ graph::views::filter::model::{ edge_expr::EdgeOp, edge_filter::EdgeEndpointWrapper, + expr::DynCreateHistory, node_expr::{ AvgExpr, CreateOp, EntityAggOps, EntityExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, SumExpr, @@ -44,13 +45,21 @@ impl>> DynEntityExpr for E { } pub trait DynTemporal: DynCreateOp { + /// The history as one list value. fn temporal(&self) -> Arc; + + /// The history as a stream, for a consumer that walks it. + fn history(&self) -> Arc; } impl DynTemporal for PropertyExpr { fn temporal(&self) -> Arc { Arc::new(self.temporal()) } + + fn history(&self) -> Arc { + Arc::new(self.temporal()) + } } impl DynTemporal for EdgeEndpointWrapper> @@ -61,6 +70,13 @@ where fn temporal(&self) -> Arc { Arc::new(self.temporal()) } + + fn history(&self) -> Arc { + Arc::new(EdgeEndpointWrapper::new( + self.inner.temporal(), + self.endpoint(), + )) + } } /// An endpoint read built from an erased node value: switching to the history @@ -72,6 +88,13 @@ impl DynTemporal for EdgeEndpointWrapper> { self.endpoint(), )) } + + fn history(&self) -> Arc { + Arc::new(EdgeEndpointWrapper::new( + self.inner.history(), + self.endpoint(), + )) + } } pub trait DynCreateOp: DynEntityExpr { 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 480bcca490..0fe1be936a 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 @@ -73,10 +73,11 @@ use crate::{ db::{ api::{ state::ops::{Const, Degree, Id, Name, NodeOp, Type}, - view::internal::GraphView, + view::internal::{DynGraphArc, GraphView}, }, graph::views::filter::model::{ edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, + expr::{DynCreateHistory, EdgeHistory, NodeHistory}, filter_operator::Comparable, node_filter::NodeFilter, require_aggregable, resolved_prop_type, ComposableFilter, CreateView, EntityMarker, @@ -513,6 +514,34 @@ impl PredicateLhs for TemporalPro impl EntityAggOps for TemporalPropExpr {} +impl DynCreateHistory + for TemporalPropExpr +{ + fn create_node_history<'g>( + &self, + graph: DynGraphArc<'g>, + ) -> Result, 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(TemporalNodePropOp { graph, prop_id })) + } + + fn create_edge_history<'g>( + &self, + graph: DynGraphArc<'g>, + ) -> Result, GraphError> { + let prop_id = graph + .edge_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(TemporalEdgePropOp { graph, prop_id })) + } +} + impl CreateOp for TemporalPropExpr { fn create_node_op<'g, G: GraphView + 'g>( &self, 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 index 6858842b28..80108d5aa4 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -46,8 +46,11 @@ use crate::{ NodeViewOps, }, }, - graph::views::filter::model::property_filter::evaluate::{ - aggregate_list_values, scan_f64_sum_count, scan_i64_sum, scan_u64_sum, + graph::views::filter::model::{ + expr::Agg, + property_filter::evaluate::{ + aggregate_list_values, scan_f64_sum_count, scan_i64_sum, scan_u64_sum, + }, }, }, prelude::GraphViewOps, @@ -172,9 +175,12 @@ impl NodeOp for NodeIdOp { /// Internal op produced by [`TemporalPropertyExpr::create_node_op`] — not constructed directly. /// -/// Collects all recorded values within the current view window into a `Some(Prop::List([...]))`. -/// That list is then consumed by aggregator ops (`SumNodeOp`, `LenNodeOp`, …) or -/// compared element-wise, one answer per element, for `.any()`/`.all()` to collapse. +/// Collects all recorded values within the current view window into a `Some(Prop::List([...]))` +/// for a consumer that needs the history as one value. An aggregation or an +/// `any()`/`all()` test written directly over the history does not go through +/// this list: the compiler pairs it with the op's [`NodeHistory`] stream instead. +/// +/// [`NodeHistory`]: crate::db::graph::views::filter::model::expr::NodeHistory #[derive(Clone)] pub(crate) struct TemporalNodePropOp { pub(crate) graph: G, @@ -356,11 +362,24 @@ macro_rules! impl_agg_entity_op { }; } -impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |pt| sum_out_type(pt), |vals| { - aggregate_list_values(vals, &|pi| { - let mut vals = pi.peekable(); - let inner = vals.peek()?.dtype(); - match inner { +/// One reduction over the elements of one list, `Last` from the back. +pub(crate) fn reduce_list( + agg: Agg, + mut items: Box + '_>, +) -> Option { + match agg { + Agg::Last => items.next_back(), + _ => fold_values(agg, items), + } +} + +/// One reduction over a stream of values, front to back. A caller that can +/// read the values from the back answers `Last` and `Latest` itself instead +/// of walking to the end. +pub(crate) fn fold_values(agg: Agg, vals: impl Iterator) -> Option { + let mut vals = vals.peekable(); + match agg { + Agg::Sum => match vals.peek()?.dtype() { PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { let (promoted, s64, s128, _) = scan_u64_sum(vals)?; Some(if promoted { @@ -387,66 +406,84 @@ impl_agg_entity_op!(SumNodeOp, SumEdgeOp, |pt| sum_out_type(pt), |vals| { PropType::F32 => scan_f64_sum_count(vals).map(|(sum, _)| Prop::F32(sum as f32)), PropType::F64 => scan_f64_sum_count(vals).map(|(sum, _)| Prop::F64(sum)), _ => None, + }, + Agg::Avg => match vals.peek()?.dtype() { + PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { + let (promoted, s64, s128, count) = scan_u64_sum(vals)?; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) + } + PropType::I32 | PropType::I64 => { + let (promoted, s64, s128, count) = scan_i64_sum(vals)?; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) + } + PropType::F32 | PropType::F64 => { + let (sum, count) = scan_f64_sum_count(vals)?; + Some(Prop::F64(sum / (count as f64))) + } + _ => None, + }, + Agg::Min => { + let first = vals.next()?; + vals.fold(Some(first), |acc, v| acc.and_then(|a| a.min(v))) } - }) -}); + Agg::Max => { + let first = vals.next()?; + vals.fold(Some(first), |acc, v| acc.and_then(|a| a.max(v))) + } + Agg::First | Agg::Earliest => vals.next(), + Agg::Last | Agg::Latest => vals.last(), + Agg::Len => Some(vals.count().into_prop()), + } +} +/// The type `agg` produces from a value of type `pt`. +pub(crate) fn agg_out_pt(agg: Agg, pt: PropType) -> PropType { + match agg { + Agg::Sum => sum_out_type(pt), + Agg::Avg => agg_out_type(pt, Some(PropType::F64)), + Agg::Min | Agg::Max | Agg::First | Agg::Last => agg_out_type(pt, None), + Agg::Len => agg_out_type(pt, Some(PropType::U64)), + Agg::Earliest | Agg::Latest => update_type(pt), + } +} + +impl_agg_entity_op!( + SumNodeOp, + SumEdgeOp, + |pt| agg_out_pt(Agg::Sum, pt), + |vals| { aggregate_list_values(vals, &|pi| reduce_list(Agg::Sum, pi)) } +); impl_agg_entity_op!( AvgNodeOp, AvgEdgeOp, - |pt| agg_out_type(pt, Some(PropType::F64)), - |vals| { - aggregate_list_values(vals, &|pi| { - let mut vals = pi.peekable(); - let inner = vals.peek()?.dtype(); - match inner { - PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { - let (promoted, s64, s128, count) = scan_u64_sum(vals)?; - let s = if promoted { s128 as f64 } else { s64 as f64 }; - Some(Prop::F64(s / (count as f64))) - } - - PropType::I32 | PropType::I64 => { - let (promoted, s64, s128, count) = scan_i64_sum(vals)?; - let s = if promoted { s128 as f64 } else { s64 as f64 }; - Some(Prop::F64(s / (count as f64))) - } - - PropType::F32 | PropType::F64 => { - let (sum, count) = scan_f64_sum_count(vals)?; - Some(Prop::F64(sum / (count as f64))) - } - - _ => None, - } - }) - } + |pt| agg_out_pt(Agg::Avg, pt), + |vals| { aggregate_list_values(vals, &|pi| reduce_list(Agg::Avg, pi)) } +); +impl_agg_entity_op!( + MinNodeOp, + MinEdgeOp, + |pt| agg_out_pt(Agg::Min, pt), + |vals| { aggregate_list_values(vals, &|pi| reduce_list(Agg::Min, pi)) } +); +impl_agg_entity_op!( + MaxNodeOp, + MaxEdgeOp, + |pt| agg_out_pt(Agg::Max, pt), + |vals| { aggregate_list_values(vals, &|pi| reduce_list(Agg::Max, pi)) } ); -impl_agg_entity_op!(MinNodeOp, MinEdgeOp, |pt| agg_out_type(pt, None), |vals| { - aggregate_list_values(vals, &|pi| { - let mut it = pi; - let first = it.next()?; - it.fold(Some(first), |acc, v| acc.and_then(|a| a.min(v))) - }) -}); -impl_agg_entity_op!(MaxNodeOp, MaxEdgeOp, |pt| agg_out_type(pt, None), |vals| { - aggregate_list_values(vals, &|pi| { - let mut it = pi; - let first = it.next()?; - it.fold(Some(first), |acc, v| acc.and_then(|a| a.max(v))) - }) -}); impl_agg_entity_op!( FirstNodeOp, FirstEdgeOp, - |pt| agg_out_type(pt, None), - |vals| { aggregate_list_values(vals, &|mut pi| pi.next()) } + |pt| agg_out_pt(Agg::First, pt), + |vals| { aggregate_list_values(vals, &|pi| reduce_list(Agg::First, pi)) } ); impl_agg_entity_op!( LastNodeOp, LastEdgeOp, - |pt| agg_out_type(pt, None), - |vals| { aggregate_list_values(vals, &|mut pi| pi.next_back()) } + |pt| agg_out_pt(Agg::Last, pt), + |vals| { aggregate_list_values(vals, &|pi| reduce_list(Agg::Last, pi)) } ); /// The type one update of a history has: the history's element type. fn update_type(pt: PropType) -> PropType { @@ -455,25 +492,35 @@ fn update_type(pt: PropType) -> PropType { other => other, } } -impl_agg_entity_op!(EarliestNodeOp, EarliestEdgeOp, update_type, |vals| { - // The earliest update as it is, scalar or list. - match vals? { - Prop::List(x) => x.iter_all().find_map(|v| v), - _ => None, +impl_agg_entity_op!( + EarliestNodeOp, + EarliestEdgeOp, + |pt| agg_out_pt(Agg::Earliest, pt), + |vals| { + // The earliest update as it is, scalar or list. + match vals? { + Prop::List(x) => x.iter_all().find_map(|v| v), + _ => None, + } } -}); -impl_agg_entity_op!(LatestNodeOp, LatestEdgeOp, update_type, |vals| { - // The latest update as it is, scalar or list. - match vals? { - Prop::List(x) => x.iter_all().rev().find_map(|v| v), - _ => None, +); +impl_agg_entity_op!( + LatestNodeOp, + LatestEdgeOp, + |pt| agg_out_pt(Agg::Latest, pt), + |vals| { + // The latest update as it is, scalar or list. + match vals? { + Prop::List(x) => x.iter_all().rev().find_map(|v| v), + _ => None, + } } -}); +); impl_agg_entity_op!( LenNodeOp, LenEdgeOp, - |pt| agg_out_type(pt, Some(PropType::U64)), - |vals| { aggregate_list_values(vals, &|pi| Some(pi.count().into_prop())) } + |pt| agg_out_pt(Agg::Len, pt), + |vals| { aggregate_list_values(vals, &|pi| reduce_list(Agg::Len, pi)) } ); impl_agg_entity_op!( AnyNodeOp, From 0aefa41d7bd4024de5ebab744cc563bda3d73b95 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:32:21 +0100 Subject: [PATCH 145/155] answer the node and edge questions of a filter separately A filtered graph answers which nodes stay and which edges stay, and a node predicate answers the second by "both ends stayed". The compiler now splits every filter tree into its direct answer to each question, negates an answer for `not`, and compiles the two answers as an `and`, so `not` never flips an answer the filter did not give, a chain of filters gives the `and`'s result, and the per-edge form of a filter agrees with its graph. A node collection refuses a filter that tests edges. Typed `not()` compiles through the same tree; the negated graph type is gone. --- docs/user-guide/views/6_filtering.md | 6 +- python/python/raphtory/filter/__init__.pyi | 8 + .../test_edges_collection_filter.py | 142 +++++------- raphtory-graphql/schema.graphql | 12 + .../src/model/graph/filter_expr_input.rs | 5 + raphtory/src/db/graph/views/filter/mod.rs | 1 - .../db/graph/views/filter/model/and_filter.rs | 15 +- .../graph/views/filter/model/edge_expr/ops.rs | 46 +--- .../graph/views/filter/model/expr/compile.rs | 126 ++++++++++- .../graph/views/filter/model/expr/convert.rs | 4 +- .../db/graph/views/filter/model/expr/mod.rs | 9 + .../db/graph/views/filter/model/expr/tests.rs | 214 ++++++++++++++++++ .../graph/views/filter/model/node_expr/ops.rs | 6 + .../db/graph/views/filter/model/not_filter.rs | 49 ++-- .../db/graph/views/filter/model/or_filter.rs | 15 +- .../graph/views/filter/not_filtered_graph.rs | 138 ----------- raphtory/src/python/filter/filter_expr.rs | 5 + raphtory/src/python/filter/node_expr.rs | 3 + 18 files changed, 511 insertions(+), 293 deletions(-) delete mode 100644 raphtory/src/db/graph/views/filter/not_filtered_graph.rs diff --git a/docs/user-guide/views/6_filtering.md b/docs/user-guide/views/6_filtering.md index fecdaeade6..27f1b1d832 100644 --- a/docs/user-guide/views/6_filtering.md +++ b/docs/user-guide/views/6_filtering.md @@ -88,7 +88,11 @@ Use the bitwise operators: `&` for *and*, `|` for *or*, `~` for *not*. Python's `not` do not work on filter expressions. `~f` selects everything `f` did not select. A node without the property is not selected by -`property("score") > 4`, so it *is* selected by `~(property("score") > 4)`. +`property("score") > 4`, so it *is* selected by `~(property("score") > 4)`. A negated node +predicate is still a node predicate: on edges it keeps the edges between the nodes that fail it. +Negating a combination negates its node tests and its edge tests: `~(a & b)` of two node tests is +`~a | ~b`, and `~(node_test & edge_test)` keeps the nodes that fail the node test and, between +them, the edges that fail the edge test. /// tab | :fontawesome-brands-python: Python diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 76772fea0a..b8dbd929f1 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -93,6 +93,11 @@ class FilterExpr(object): Anywhere a filter is expected, a yes/no [`Expr`] is accepted too: it is the filter on its own entity. + + `&`, `|` and `~` combine filters. `~` keeps what the filter drops: a negated + node test keeps the nodes that fail it and the edges between them, and a + combination is negated test by test, node tests on nodes and edge tests on + edges. A view combines with `&` only. """ def __and__(self, value): @@ -118,6 +123,9 @@ class Expr(object): A value expression: a field, degree, property, metadata entry, an aggregate over one, or a yes/no built from them. Comparing it to a value or to another expression gives a yes/no [`Expr`], which is a filter on its entity. + + `~` on a yes/no is the opposite yes/no: a node without the property fails + `property("score") > 4`, so it passes `~(property("score") > 4)`. """ def __and__(self, value): diff --git a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py index c58e9b179b..3c39b8f023 100644 --- a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py +++ b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py @@ -1,10 +1,13 @@ """Edge-collection filtering across every filter type and combination. Singles are checked against the graph-level filter and the chained-view references. Combinations -are checked against set algebra over the single-filter results (`&` = intersection, `|` = union, -`~` = complement), which is how node collections already behave. The combination classes that are -known broken on edges are pinned by `test_broken_combination_classes_are_still_broken` — when a fix -lands, that test fails and the class moves into the working set by deleting its rule below. +are checked against set algebra over the single-filter results, question by question: a filter +answers "which nodes stay" and "which edges stay", a node predicate answers the second by "both +ends stayed", and `&`, `|`, `~` combine the direct answers. Within one kind that is plain set +algebra (`~` of an edge predicate keeps the other edges; `~` of a node predicate keeps the nodes +that fail it and the edges between them; `~(a & b)` is `~a | ~b`, `~(a | b)` is `~a & ~b`). +Across kinds, `&` intersects, `~(a & b)` negates each answer, and `|` leaves both questions open +(every edge), as the graph filter has always done; that last reading is inherited, not chosen here. """ from itertools import combinations @@ -74,28 +77,10 @@ def _or_is_refused(a, b): return a in VIEWS or b in VIEWS -def _or_is_broken(a, b): - # An `or` mixing edge- and node-kind operands returns every edge. - return _or_is_refused(a, b) or _kind(a) != _kind(b) - - def _not_is_refused(a): return a in VIEWS -def _not_is_broken(a): - # `~node-filter` distributes the negation into the endpoints instead of complementing the - # matching edge set. - return _not_is_refused(a) or a in NODE_KIND - - -def _not_composite_is_broken(a, b): - # `~(A & B)` and `~(A | B)`: a composite with a view is refused under `~`; negating a - # composite with a node-kind operand reaches the endpoint wrappers. Only a composite of - # two edge predicates survives. - return _kind(a) != "edge" or _kind(b) != "edge" - - def _ids(collection): return frozenset(e.id for e in collection) @@ -134,6 +119,18 @@ def _both_endpoints(graph, names): } +def _all_names(graph): + return {node.name for node in graph.nodes} + + +def _node_sets(graph): + """The node set each node-kind atom keeps.""" + return { + "node_prop": _node_scores(graph, 15), + "node_name": {"b", "c"}, + } + + def _predicate_references(graph): return { "edge_prop": { @@ -141,8 +138,8 @@ def _predicate_references(graph): }, "src": {e.id for e in graph.edges if e.src.name == "a"}, "dst": {e.id for e in graph.edges if e.dst.name == "c"}, - "node_prop": _both_endpoints(graph, _node_scores(graph, 15)), - "node_name": _both_endpoints(graph, {"b", "c"}), + "node_prop": _both_endpoints(graph, _node_sets(graph)["node_prop"]), + "node_name": _both_endpoints(graph, _node_sets(graph)["node_name"]), "is_valid": {e.id for e in graph.edges if e.is_valid()}, "is_deleted": {e.id for e in graph.edges if e.is_deleted()}, "is_active": {e.id for e in graph.edges if e.is_active()}, @@ -170,28 +167,21 @@ def _singles(graph): return singles -def _assert_discriminating(graph, single, names): - """Reject reference sets that cannot tell a right answer from a wrong one. - - The set-algebra expectations below are derived from single-filter results, so - a single filter that selects everything (or nothing) makes the derived - expectation degenerate: `EVERYTHING & X == X` is equally consistent with a - correct `and` and with one that dropped a term. That is not hypothetical — - on a build where a single view filter fails open, every `view & pred` - expectation collapses onto the predicate alone, so a broken combination - matches its expectation and the pins below would report it as fixed. - - Asserting up front that each baseline is a proper subset keeps the pins - honest wherever this file is run, instead of only on a build where the - singles happen to be correct. - """ +def _negations(graph, single): + """What `~atom` selects on edges: the other edges for an edge predicate; for a node + predicate, the edges between the nodes that fail it.""" every = _ids(graph.edges) - for name in names: - assert single[name], f"baseline edges[{name}] selects nothing on this build" - assert single[name] != every, ( - f"baseline edges[{name}] selects every edge on this build, so any " - f"expectation derived from it cannot discriminate" - ) + node_sets = _node_sets(graph) + negated = {} + for name, ids in single.items(): + if name in VIEWS: + continue + if name in NODE_KIND: + outside = _all_names(graph) - node_sets[name] + negated[name] = frozenset(_both_endpoints(graph, outside)) + else: + negated[name] = every - ids + return negated @with_variants(_init) @@ -236,31 +226,43 @@ def check(graph): def test_working_combinations_follow_set_algebra(): def check(graph): atoms, single = _atoms(), _singles(graph) + negated = _negations(graph, single) every = _ids(graph.edges) cases = [] for a, b in combinations(atoms, 2): if not _and_is_broken(a, b): cases.append((f"{a} & {b}", atoms[a] & atoms[b], single[a] & single[b])) - if not _or_is_broken(a, b): + if _or_is_refused(a, b): + continue + if _kind(a) == _kind(b): cases.append((f"{a} | {b}", atoms[a] | atoms[b], single[a] | single[b])) - if not _not_composite_is_broken(a, b): cases.append( ( f"~({a} & {b})", ~(atoms[a] & atoms[b]), - every - (single[a] & single[b]), + negated[a] | negated[b], ) ) cases.append( ( f"~({a} | {b})", ~(atoms[a] | atoms[b]), - every - (single[a] | single[b]), + negated[a] & negated[b], ) ) + else: + cases.append((f"{a} | {b}", atoms[a] | atoms[b], every)) + cases.append( + ( + f"~({a} & {b})", + ~(atoms[a] & atoms[b]), + negated[a] & negated[b], + ) + ) + cases.append((f"~({a} | {b})", ~(atoms[a] | atoms[b]), every)) for a in atoms: - if not _not_is_broken(a): - cases.append((f"~{a}", ~atoms[a], every - single[a])) + if not _not_is_refused(a): + cases.append((f"~{a}", ~atoms[a], negated[a])) cases.append( ( "layer & (edge_prop | src)", @@ -434,41 +436,3 @@ def check(graph): build() return check - - -@with_variants(_init) -def test_broken_combination_classes_are_still_broken(): - """One discriminating representative per known-broken class. When a class is fixed this fails: - delete its `_*_is_broken` rule above so the combinations join the set-algebra test. - """ - - def check(graph): - atoms, single = _atoms(), _singles(graph) - every = _ids(graph.edges) - _assert_discriminating( - graph, - single, - ["window", "layer", "edge_prop", "node_prop", "node_name"], - ) - representatives = { - "or of mixed kinds returns every edge": ( - atoms["edge_prop"] | atoms["node_prop"], - single["edge_prop"] | single["node_prop"], - ), - "not of a node filter is not the complement": ( - ~atoms["node_name"], - every - single["node_name"], - ), - } - fixed = [] - for label, (expr, want) in representatives.items(): - if ( - _ids(graph.edges[expr]) == want - and _ids(graph.filter(expr).edges) == want - ): - fixed.append(label) - assert ( - not fixed - ), f"now FIXED: {fixed} — move the class into the working set by deleting its rule" - - return check diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 94a08626a1..a1b0795bbf 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -1798,8 +1798,19 @@ input FilterExpr @oneOf { A graph-level view with no predicate: the result is the view. """ view: [ViewOp!] + """ + Every leg holds. A view leg applies first and the others run inside it. + """ and: [FilterExpr!] + """ + Any leg holds. Node legs combine on nodes and edge legs on edges; a leg + of the other kind leaves that side unconstrained. No view legs. + """ or: [FilterExpr!] + """ + The filter that keeps what the inner one drops: a negated node filter + keeps the nodes that fail it and the edges between them. No views. + """ not: FilterExpr } @@ -7127,3 +7138,4 @@ schema { query: QueryRoot mutation: MutRoot } + diff --git a/raphtory-graphql/src/model/graph/filter_expr_input.rs b/raphtory-graphql/src/model/graph/filter_expr_input.rs index 3c88dcd5c8..4d1abdf6a0 100644 --- a/raphtory-graphql/src/model/graph/filter_expr_input.rs +++ b/raphtory-graphql/src/model/graph/filter_expr_input.rs @@ -611,8 +611,13 @@ pub enum GqlFilter { ExplodedEdge(GqlExplodedEdgeExpr), /// A graph-level view with no predicate: the result is the view. View(Vec), + /// Every leg holds. A view leg applies first and the others run inside it. And(Vec), + /// Any leg holds. Node legs combine on nodes and edge legs on edges; a leg + /// of the other kind leaves that side unconstrained. No view legs. Or(Vec), + /// The filter that keeps what the inner one drops: a negated node filter + /// keeps the nodes that fail it and the edges between them. No views. Not(Wrapped), } diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 97ccc72362..a54992c982 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -21,7 +21,6 @@ pub mod exploded_edge_filtered_graph; pub mod exploded_edge_node_filtered_graph; pub mod model; pub mod node_filtered_graph; -pub mod not_filtered_graph; pub mod or_filtered_graph; pub struct Exists; 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 827e6e3ac9..6f1069d13b 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -6,7 +6,11 @@ use crate::{ }, graph::views::filter::{ and_filtered_graph::AndFilteredGraph, - model::{edge_expr::ops::AndEdgeOp, ComposableFilter}, + model::{ + edge_expr::ops::AndEdgeOp, + expr::{FilterExpr, ToFilterExpr}, + ComposableFilter, + }, CreateFilter, }, }, @@ -20,6 +24,15 @@ pub struct AndFilter { pub(crate) right: R, } +impl ToFilterExpr for AndFilter { + fn to_filter_expr(&self) -> FilterExpr { + FilterExpr::And(vec![ + self.left.to_filter_expr(), + self.right.to_filter_expr(), + ]) + } +} + impl Display for AndFilter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({} AND {})", self.left, self.right) diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index d96bc79197..ee179affcc 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -182,6 +182,15 @@ impl EdgeOp for EdgeMetaOp { // TemporalEdgePropOp — all temporal values for a property in the view window // ───────────────────────────────────────────────────────────────────────────── +/// Internal op produced by [`TemporalPropExpr::create_edge_op`] — not constructed directly. +/// +/// Collects the property's history for the edge, one of its layers or one +/// exploded instance into a `Some(Prop::List([...]))` for a consumer that +/// needs it as one value. An aggregation or an `any()`/`all()` test written +/// directly over the history streams it through [`EdgeHistory`] instead. +/// +/// [`TemporalPropExpr::create_edge_op`]: crate::db::graph::views::filter::model::node_expr::TemporalPropExpr +/// [`EdgeHistory`]: crate::db::graph::views::filter::model::expr::EdgeHistory #[derive(Clone)] pub(crate) struct TemporalEdgePropOp { pub(crate) graph: G, @@ -612,40 +621,3 @@ impl, R: EdgeOp> EdgeOp for OrEdgeOp(pub(crate) T); - -impl> EdgeOp for NotEdgeOp { - type Output = bool; - - fn apply(&self, storage: &GraphStorage, edge: EdgeEntryRef) -> bool { - !self.0.apply(storage, edge) - } - - fn apply_layer(&self, storage: &GraphStorage, edge: EdgeEntryRef, layer: LayerId) -> bool { - !self.0.apply_layer(storage, edge, layer) - } - - fn apply_exploded( - &self, - storage: &GraphStorage, - edge: EdgeEntryRef, - layer: LayerId, - t: EventTime, - ) -> bool { - !self.0.apply_exploded(storage, edge, layer, t) - } - - fn prop_type(&self) -> PropType { - PropType::Bool - } - - fn const_value(&self) -> Option { - self.0.const_value().map(|v| !v) - } - - fn filters_exploded(&self) -> bool { - self.0.filters_exploded() - } -} diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index 5c2d560cfa..40a7ab44fb 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -12,7 +12,7 @@ use super::{ StreamedAggEdgeOp, StreamedAggNodeOp, StreamedQualEdgeOp, StreamedQualNodeOp, ValueTest, }, Agg, CmpOp, DynCreateHistory, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, FilterExpr, NodeExpr, - NodeLeaf, StrOp, ViewOp, + NodeLeaf, OpaqueFilter, StrOp, ViewOp, }; use crate::{ db::{ @@ -1987,33 +1987,133 @@ impl FilterExpr { } /// A filter below the top level: every node but a view. + /// + /// A filtered graph answers two questions, which nodes stay and which + /// edges stay, and every filter answers both: a node predicate answers + /// the first directly and the second by "both ends stayed", an edge + /// predicate the reverse. `and`, `or` and `not` combine the direct + /// answers, question by question, so `name == "b" | name == "c"` keeps + /// the edge b→c, `not` never flips an answer the filter did not give, and + /// the per-node and per-edge forms of a filter agree with its graph. fn compile_nested(&self) -> Result, GraphError> { + let nodes = self.answer(Question::Nodes)?; + let edges = self.answer(Question::Edges)?; + Ok(match (nodes, edges) { + (Some(nodes), Some(edges)) => Arc::new(AndFilter { + left: nodes.compile_answer()?, + right: edges.compile_answer()?, + }), + (Some(answer), None) | (None, Some(answer)) => answer.compile_answer()?, + (None, None) => Arc::new(GraphFilter), + }) + } + + /// This filter's direct answer to `question`, as a tree over that + /// question's entities only; `None` when the filter leaves it open. An + /// `or` with a leg that leaves the question open leaves it open. `not` + /// negates the answer, and a view has no answer the engine can give. + fn answer(&self, question: Question) -> Result, GraphError> { + Ok(match self { + FilterExpr::Node(_) | FilterExpr::Opaque(_) => { + (question == Question::Nodes).then(|| self.clone()) + } + FilterExpr::Edge(_) | FilterExpr::ExplodedEdge(_) => { + (question == Question::Edges).then(|| self.clone()) + } + FilterExpr::View(_) => return Err(view_below_top_level()), + FilterExpr::And(items) => { + let mut answers: Vec = items + .iter() + .map(|item| item.answer(question)) + .collect::, _>>()? + .into_iter() + .flatten() + .collect(); + match answers.len() { + 0 => None, + 1 => answers.pop(), + _ => Some(FilterExpr::And(answers)), + } + } + FilterExpr::Or(items) => { + let answers = items + .iter() + .map(|item| item.answer(question)) + .collect::>, _>>()?; + answers.map(|mut answers| { + if answers.len() == 1 { + answers.pop().unwrap() + } else { + FilterExpr::Or(answers) + } + }) + } + FilterExpr::Not(inner) => inner.answer(question)?.map(|a| a.negated()).transpose()?, + }) + } + + /// The negation of an answer, pushed down to its predicates: `not(and)` + /// is the `or` of the negations, `not(or)` the `and`, and a negated + /// predicate is a predicate on the same entity. + fn negated(&self) -> Result { + fn not(expr: &Expr) -> Expr { + Expr::Not(Box::new(expr.clone())) + } + Ok(match self { + FilterExpr::Node(expr) => FilterExpr::Node(not(expr)), + FilterExpr::Edge(expr) => FilterExpr::Edge(not(expr)), + FilterExpr::ExplodedEdge(expr) => FilterExpr::ExplodedEdge(not(expr)), + FilterExpr::View(_) => return Err(view_below_top_level()), + FilterExpr::And(items) => { + FilterExpr::Or(items.iter().map(Self::negated).collect::>()?) + } + FilterExpr::Or(items) => { + FilterExpr::And(items.iter().map(Self::negated).collect::>()?) + } + FilterExpr::Not(inner) => (**inner).clone(), + FilterExpr::Opaque(filter) => { + FilterExpr::Opaque(OpaqueFilter(Arc::new(NotFilter(filter.0.clone())))) + } + }) + } + + /// Compile one question's answer: a tree over one kind of entity. + fn compile_answer(&self) -> Result, GraphError> { Ok(match self { FilterExpr::Node(expr) => Arc::new(Predicate::new(expr, pushdown(expr))?), FilterExpr::Edge(expr) => Arc::new(Predicate::new(expr, None)?), FilterExpr::ExplodedEdge(expr) => Arc::new(Predicate::new(expr, None)?), - FilterExpr::View(_) => { - return Err(invalid( - "a view applies to the whole filter: use it alone or as a leg of the \ - top-level `and`, not under `or` or `not`", - )) - } + FilterExpr::View(_) => return Err(view_below_top_level()), FilterExpr::And(items) => combine( - items.iter().map(Self::compile_nested), + items.iter().map(Self::compile_answer), "and", |left, right| Arc::new(AndFilter { left, right }), )?, FilterExpr::Or(items) => combine( - items.iter().map(Self::compile_nested), + items.iter().map(Self::compile_answer), "or", |left, right| Arc::new(OrFilter { left, right }), )?, - FilterExpr::Not(inner) => Arc::new(NotFilter(inner.compile_nested()?)), + FilterExpr::Not(inner) => inner.negated()?.compile_answer()?, FilterExpr::Opaque(filter) => filter.0.clone(), }) } } +/// One of the two questions a filtered graph answers. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Question { + Nodes, + Edges, +} + +fn view_below_top_level() -> GraphError { + invalid( + "a view applies to the whole filter: use it alone or as a leg of the top-level `and`, \ + not under `or` or `not`", + ) +} + /// The graph-level view a chain of view ops describes, applied in order. fn compile_view(views: &[ViewOp]) -> DynView { let mut v: DynView = Arc::new(GraphFilter); @@ -2141,10 +2241,16 @@ impl CreateFilter for FilterExpr { self.compile()?.create_graph_filter(graph) } + /// An edge test says nothing about which nodes belong in a node + /// collection, so a filter that tests edges anywhere is refused here, + /// whatever it compiles to. fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, ) -> Result, GraphError> { + if self.tests_edges() { + return Err(GraphError::NotNodeFilter); + } self.compile()?.create_node_filter(graph) } diff --git a/raphtory/src/db/graph/views/filter/model/expr/convert.rs b/raphtory/src/db/graph/views/filter/model/expr/convert.rs index c2d5bfdeb6..2845d4397f 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/convert.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/convert.rs @@ -38,6 +38,7 @@ use crate::{ PropValueSetExpr, Scoped, StringExpr, SumExpr, TemporalPropExpr, UnaryExpr, }, node_filter::NodeFilter, + not_filter::NotFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, MetadataExpr, PropertyExpr, @@ -604,7 +605,7 @@ macro_rules! compile_through_tree { self, graph: G, ) -> Result, GraphError> { - self.to_filter_expr().compile()?.create_node_filter(graph) + self.to_filter_expr().create_node_filter(graph) } fn create_edge_filter<'graph, G: GraphView + 'graph>( @@ -626,6 +627,7 @@ compile_through_tree! { impl for AllExpr; impl for EdgeEndpointWrapper; impl for Scoped; + impl for NotFilter; impl<> for IsActiveNode; impl<> for IsActiveEdge; impl<> for IsValidEdge; diff --git a/raphtory/src/db/graph/views/filter/model/expr/mod.rs b/raphtory/src/db/graph/views/filter/model/expr/mod.rs index c57fd902a3..bec1083dde 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/mod.rs @@ -278,6 +278,14 @@ pub type ExplodedEdgeExpr = Expr; /// The filter itself: a yes/no on one kind of entity, a view, or a /// combination of filters. +/// +/// A filtered graph answers two questions, which nodes stay and which edges +/// stay. A node predicate answers the first directly and the second by "both +/// ends stayed"; an edge predicate the reverse. `And`, `Or` and `Not` combine +/// the direct answers question by question: `name == "b" | name == "c"` keeps +/// the edge b→c, `Not` of a node predicate keeps the nodes that fail it and the +/// edges between them, `Not(And(node, edge))` negates each answer, and an `Or` +/// with a leg that leaves a question open leaves it open. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum FilterExpr { @@ -288,6 +296,7 @@ pub enum FilterExpr { View(Vec), And(Vec), Or(Vec), + /// The filter that keeps what the inner one drops, one question at a time. Not(Box), /// A filter over in-process state (a node-state column) that has no wire /// form: it runs where it was built and cannot be sent anywhere. diff --git a/raphtory/src/db/graph/views/filter/model/expr/tests.rs b/raphtory/src/db/graph/views/filter/model/expr/tests.rs index c04850111a..2e73d28925 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/tests.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/tests.rs @@ -7,6 +7,7 @@ use crate::{ ViewWrapOps, }, }, + errors::GraphError, prelude::{AdditionOps, EdgeViewOps, Graph, GraphViewOps, NodeViewOps, TimeOps, NO_PROPS}, }; use raphtory_api::core::{ @@ -608,3 +609,216 @@ fn aggregates_reduce_inside_each_update_and_earliest_picks_one() { "{msg}" ); } + +/// A filter answers two questions, which nodes stay and which edges stay; +/// `and`, `or` and `not` combine the direct answers question by question. A +/// negated node predicate keeps the nodes that fail it and the edges between +/// them; an `or` with a leg that leaves a question open leaves it open; the +/// per-edge form of every filter agrees with its graph; and a chain of +/// filters gives the `and`'s answer. +#[test] +fn filters_answer_the_node_and_edge_questions_separately() { + use crate::db::api::view::{DynamicGraph, IntoDynamic, Select}; + let g = graph(); + let score_gt = |v: f64| node(cmp(CmpOp::Gt, prop("score"), c(v))); + let score_lt = |v: f64| node(cmp(CmpOp::Lt, prop("score"), c(v))); + let ec = |v: i64| -> EdgeExpr { Expr::Const(v.into()) }; + let w_gt = |v: i64| FilterExpr::Edge(cmp(CmpOp::Gt, edge_prop("w"), ec(v))); + let not = |f: FilterExpr| FilterExpr::Not(Box::new(f)); + let names = |v: &DynamicGraph| { + let mut n: Vec = v.nodes().iter().map(|n| n.name()).collect(); + n.sort(); + n + }; + let eids = |v: &DynamicGraph| { + let mut e: Vec = v + .edges() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + e.sort(); + e + }; + let all_nodes = ["alice", "bob", "carol", "dave", "eve"]; + let all_edges = ["alice->bob", "bob->carol", "carol->dave"]; + // alice.score 9 · bob 2 · dave 1 · carol, eve none · alice→bob w=2 · bob→carol w=1 · carol→dave w=3 + let cases: Vec<(&str, FilterExpr, &[&str], &[&str])> = vec![ + ( + "not(edge)", + not(w_gt(2)), + &all_nodes, + &["alice->bob", "bob->carol"], + ), + ( + "not(node)", + not(score_gt(4.0)), + &["bob", "carol", "dave", "eve"], + &["bob->carol", "carol->dave"], + ), + ("not(not(node))", not(not(score_gt(4.0))), &["alice"], &[]), + ( + "not(and(node, node)) = or(not, not)", + not(FilterExpr::And(vec![score_gt(1.5), score_lt(8.0)])), + &["alice", "carol", "dave", "eve"], + &["carol->dave"], + ), + ( + "not(or(node, node)) = and(not, not)", + not(FilterExpr::Or(vec![score_gt(4.0), score_lt(1.5)])), + &["bob", "carol", "eve"], + &["bob->carol"], + ), + ( + "not(and(node, edge)): each answer negated", + not(FilterExpr::And(vec![score_gt(4.0), w_gt(2)])), + &["bob", "carol", "dave", "eve"], + &["bob->carol"], + ), + ( + "or(node, edge): both questions left open", + FilterExpr::Or(vec![score_gt(4.0), w_gt(2)]), + &all_nodes, + &all_edges, + ), + ( + "not(or(node, edge)): still open", + not(FilterExpr::Or(vec![score_gt(4.0), w_gt(2)])), + &all_nodes, + &all_edges, + ), + ( + "and(node, or(node, edge)): the open or drops out", + FilterExpr::And(vec![ + score_gt(1.5), + FilterExpr::Or(vec![score_gt(4.0), w_gt(2)]), + ]), + &["alice", "bob"], + &["alice->bob"], + ), + ]; + let base = g.filter(score_gt(1.5)).unwrap().into_dynamic(); + assert_eq!(names(&base), ["alice", "bob"]); + assert_eq!(eids(&base), ["alice->bob"]); + for (label, f, want_nodes, want_edges) in cases { + let alone = g.filter(f.clone()).unwrap().into_dynamic(); + assert_eq!(names(&alone), want_nodes, "{label}: nodes"); + assert_eq!(eids(&alone), want_edges, "{label}: edges"); + let mut selected: Vec = g + .edges() + .select(f.clone()) + .unwrap() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + selected.sort(); + assert_eq!(selected, want_edges, "{label}: edges.select"); + // Chained after the base, the answer is the `and`'s: the base's + // entities that the filter keeps. + let chained = base.filter(f.clone()).unwrap().into_dynamic(); + let anded = g + .filter(FilterExpr::And(vec![score_gt(1.5), f.clone()])) + .unwrap() + .into_dynamic(); + assert_eq!(names(&chained), names(&anded), "{label}: chained nodes"); + assert_eq!(eids(&chained), eids(&anded), "{label}: chained edges"); + let want_chained_nodes: Vec<&str> = want_nodes + .iter() + .copied() + .filter(|n| ["alice", "bob"].contains(n)) + .collect(); + assert_eq!( + names(&chained), + want_chained_nodes, + "{label}: chained nodes" + ); + let want_chained_edges: Vec<&str> = want_edges + .iter() + .copied() + .filter(|e| *e == "alice->bob") + .collect(); + assert_eq!(eids(&chained), want_chained_edges, "{label}: chained edges"); + } +} + +/// `not` over an exploded-edge predicate negates each instance. +#[test] +fn not_over_an_exploded_predicate_keeps_the_other_instances() { + let g = graph(); + // alice→bob w=1@1 w=2@4 · bob→carol w=1@2 · carol→dave w=3@6 + let w_gt_1 = FilterExpr::ExplodedEdge(cmp( + CmpOp::Gt, + Expr::Read(ExplodedEdgeLeaf::Property { + views: vec![], + name: "w".into(), + temporal: false, + }), + Expr::Const(1i64.into()), + )); + let not = FilterExpr::Not(Box::new(w_gt_1.clone())); + assert_eq!(edges(&g, &w_gt_1), ["alice->bob", "carol->dave"]); + assert_eq!(edges(&g, ¬), ["alice->bob", "bob->carol"]); + let instances = |f: &FilterExpr| { + let mut i: Vec<(String, i64)> = g + .filter(f.clone()) + .unwrap() + .edges() + .explode() + .iter() + .map(|e| (e.src().name(), e.time().unwrap().0)) + .collect(); + i.sort(); + i + }; + assert_eq!( + instances(&w_gt_1), + [("alice".to_string(), 4), ("carol".to_string(), 6)] + ); + assert_eq!( + instances(¬), + [("alice".to_string(), 1), ("bob".to_string(), 2)] + ); +} + +/// A view under `not` is refused, before and after the push-down. +#[test] +fn a_view_under_not_is_refused_inside_a_composite_too() { + let g = graph(); + let win = FilterExpr::View(vec![window(0, 5)]); + let pred = node(cmp(CmpOp::Gt, prop("score"), c(1.5))); + let f = FilterExpr::Not(Box::new(FilterExpr::And(vec![win, pred]))); + assert!(error(&g, &f).contains("view")); +} + +/// A node collection refuses a filter that tests edges anywhere in it, even +/// one that compiles to "every node". +#[test] +fn a_node_collection_refuses_a_filter_that_tests_edges() { + use crate::db::api::view::Select; + let g = graph(); + let score_gt = |v: f64| node(cmp(CmpOp::Gt, prop("score"), c(v))); + let w_gt_2 = FilterExpr::Edge(cmp(CmpOp::Gt, edge_prop("w"), Expr::Const(2i64.into()))); + let refused = |f: FilterExpr| { + matches!( + g.nodes().select(f).map(|_| ()), + Err(GraphError::NotNodeFilter) + ) + }; + assert!(refused(FilterExpr::Or(vec![score_gt(4.0), w_gt_2.clone()]))); + assert!(refused(FilterExpr::And(vec![ + score_gt(4.0), + w_gt_2.clone() + ]))); + assert!(refused(FilterExpr::Not(Box::new(FilterExpr::And(vec![ + score_gt(4.0), + w_gt_2 + ]))))); + let mut both: Vec = g + .nodes() + .select(FilterExpr::Or(vec![score_gt(4.0), score_gt(1.5)])) + .unwrap() + .iter() + .map(|n| n.name()) + .collect(); + both.sort(); + assert_eq!(both, ["alice", "bob"]); +} 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 index 80108d5aa4..8437a1f13a 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -24,6 +24,12 @@ //! apply: collect Prop::List temporal values, then aggregate_list_values(Sum) //! ``` //! +//! The tree compiler avoids that list where it can: an aggregation written +//! directly over a history, or `any()`/`all()` over a comparison of one with a +//! constant, becomes a streamed op (`model::expr::stream`) that walks the +//! history's values and shares the reduction kernels below (`fold_values`, +//! `reduce_list`). +//! //! # Quantified evaluation //! //! A comparison against a list-valued side gives one answer per element; 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 27055bd049..06c90b5ffa 100644 --- a/raphtory/src/db/graph/views/filter/model/not_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/not_filter.rs @@ -1,19 +1,28 @@ use crate::{ db::{ api::{ - state::ops::{filter::NotOp, NodeFilterOp}, - view::internal::GraphView, + state::{ops::NodeFilterOp, NodeOp}, + view::internal::{DynGraphArc, GraphView}, }, graph::views::filter::{ - model::{edge_expr::ops::NotEdgeOp, ComposableFilter}, - not_filtered_graph::NotFilteredGraph, - CreateFilter, + model::{ + edge_expr::ops::EdgeExistsOp, + expr::{FilterExpr, ToFilterExpr}, + ComposableFilter, DynCreateFilter, + }, + node_filtered_graph::NodeFilteredGraph, + CreateFilter, DynEdgeFilter, }, }, errors::GraphError, }; -use std::{fmt, fmt::Display}; +use std::{fmt, fmt::Display, sync::Arc}; +/// The filter that keeps what `T` drops. +/// +/// A typed filter negates through its tree: the compiler pushes the `not` +/// down to the entity predicates, so a negated node predicate is a node +/// predicate and the same entity rules apply either way round. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NotFilter(pub T); @@ -25,21 +34,29 @@ impl Display for NotFilter { impl ComposableFilter for NotFilter {} -impl CreateFilter for NotFilter { +impl ToFilterExpr for NotFilter { + fn to_filter_expr(&self) -> FilterExpr { + FilterExpr::Not(Box::new(self.0.to_filter_expr())) + } +} + +/// An erased filter over in-process node state has no tree to push the `not` +/// through; it is a node predicate, so its negation is the negated node op. +impl CreateFilter for NotFilter> { type FilteredGraph<'graph, G> - = NotFilteredGraph> + = DynGraphArc<'graph> where Self: 'graph, G: GraphView + 'graph; type NodeFilter<'graph, G> - = NotOp> + = Arc + 'graph> where Self: 'graph, G: GraphView + 'graph; type EdgeFilter<'graph, G> - = NotEdgeOp> + = DynEdgeFilter<'graph> where Self: 'graph, G: GraphView + 'graph; @@ -48,21 +65,25 @@ impl CreateFilter for NotFilter { self, graph: G, ) -> Result, GraphError> { - let filter = self.0.create_graph_filter(graph.clone())?; - Ok(NotFilteredGraph { graph, filter }) + let filter = self.create_node_filter(graph.clone())?; + Ok(Arc::new(NodeFilteredGraph::new(graph, filter))) } fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, ) -> Result, GraphError> { - Ok(self.0.create_node_filter(graph)?.not()) + Ok(Arc::new( + self.0.create_dyn_node_filter(Arc::new(graph))?.not(), + )) } fn create_edge_filter<'graph, G: GraphView + 'graph>( self, graph: G, ) -> Result, GraphError> { - Ok(NotEdgeOp(self.0.create_edge_filter(graph)?)) + Ok(Arc::new(EdgeExistsOp::new( + self.create_graph_filter(graph)?, + ))) } } 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 3283bf76e4..2dc5b1a228 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -5,7 +5,11 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - model::{edge_expr::ops::OrEdgeOp, ComposableFilter}, + model::{ + edge_expr::ops::OrEdgeOp, + expr::{FilterExpr, ToFilterExpr}, + ComposableFilter, + }, or_filtered_graph::OrFilteredGraph, CreateFilter, }, @@ -20,6 +24,15 @@ pub struct OrFilter { pub(crate) right: R, } +impl ToFilterExpr for OrFilter { + fn to_filter_expr(&self) -> FilterExpr { + FilterExpr::Or(vec![ + self.left.to_filter_expr(), + self.right.to_filter_expr(), + ]) + } +} + impl Display for OrFilter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({} OR {})", self.left, self.right) diff --git a/raphtory/src/db/graph/views/filter/not_filtered_graph.rs b/raphtory/src/db/graph/views/filter/not_filtered_graph.rs deleted file mode 100644 index 5327d0722f..0000000000 --- a/raphtory/src/db/graph/views/filter/not_filtered_graph.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::{ - db::api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - view::internal::{ - FilterOps, GraphView, Immutable, InheritEdgeHistoryFilter, InheritLayerOps, - InheritListOps, InheritMaterialize, InheritNodeHistoryFilter, InheritStorageOps, - InheritTimeSemantics, InternalEdgeFilterOps, InternalEdgeLayerFilterOps, - InternalExplodedEdgeFilterOps, InternalNodeFilterOps, Static, - }, - }, - prelude::GraphViewOps, -}; -use raphtory_api::{ - core::{ - entities::{LayerId, LayerIds, ELID}, - storage::timeindex::EventTime, - }, - inherit::Base, -}; -use raphtory_storage::{ - core_ops::InheritCoreGraphOps, - graph::{edges::edge_ref::EdgeEntryRef, nodes::node_ref::NodeStorageRef}, -}; - -#[derive(Debug, Clone)] -pub struct NotFilteredGraph { - pub(crate) graph: G, - pub(crate) filter: T, -} - -impl Base for NotFilteredGraph { - type Base = G; - - fn base(&self) -> &Self::Base { - &self.graph - } -} - -impl Static for NotFilteredGraph {} -impl Immutable for NotFilteredGraph {} - -impl<'graph, G: GraphViewOps<'graph>, T> InheritCoreGraphOps for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritStorageOps for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritLayerOps for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritListOps for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritMaterialize for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritPropertiesOps for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritNodePropertySchemaOps for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritEdgePropertySchemaOps for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritTimeSemantics for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritNodeHistoryFilter for NotFilteredGraph {} -impl<'graph, G: GraphViewOps<'graph>, T> InheritEdgeHistoryFilter for NotFilteredGraph {} - -impl InternalNodeFilterOps for NotFilteredGraph { - fn internal_nodes_filtered(&self) -> bool { - self.graph.internal_nodes_filtered() || self.filter.internal_nodes_filtered() - } - - #[inline] - fn internal_filter_node(&self, node: NodeStorageRef, layer_ids: &LayerIds) -> bool { - self.graph.internal_filter_node(node, layer_ids) && { - !self.filter.internal_nodes_filtered() - || !self - .filter - .internal_filter_node(node, self.filter.layer_ids()) - } - } -} - -impl<'graph, G: GraphViewOps<'graph>, T: GraphView> InternalEdgeLayerFilterOps - for NotFilteredGraph -{ - fn internal_edge_layer_filtered(&self) -> bool { - self.graph.internal_edge_layer_filtered() || self.filter.internal_edge_layer_filtered() - } - - fn internal_layer_filter_edge_list_trusted(&self) -> bool { - false - } - - fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: LayerId) -> bool { - self.graph.internal_filter_edge_layer(edge, layer) && { - !self.filter.internal_edge_layer_filtered() - || !self.filter.internal_filter_edge_layer(edge, layer) - } - } -} - -impl<'graph, G: GraphViewOps<'graph>, T: GraphView> InternalExplodedEdgeFilterOps - for NotFilteredGraph -{ - fn internal_exploded_edge_filtered(&self) -> bool { - self.graph.internal_exploded_edge_filtered() - || self.filter.internal_exploded_edge_filtered() - } - - fn internal_exploded_filter_edge_list_trusted(&self) -> bool { - false - } - - fn internal_filter_exploded_edge( - &self, - eid: ELID, - t: EventTime, - _layer_ids: &LayerIds, - ) -> bool { - self.graph.filter_exploded_edge(eid, t) && { - !self.filter.internal_exploded_edge_filtered() - || !self.filter.filter_exploded_edge(eid, t) - } - } -} - -impl<'graph, G: GraphViewOps<'graph>, T: GraphView> InternalEdgeFilterOps - for NotFilteredGraph -{ - #[inline] - fn internal_edge_filtered(&self) -> bool { - self.graph.internal_edge_filtered() || self.filter.internal_edge_filtered() - } - - #[inline] - fn internal_edge_list_trusted(&self) -> bool { - false - } - - #[inline] - fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { - self.graph.internal_filter_edge(edge, layer_ids) && { - !self.filter.internal_edge_filtered() - || !self - .filter - .internal_filter_edge(edge, self.filter.layer_ids()) - } - } -} diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 24306cc921..3c02900107 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -17,6 +17,11 @@ use std::sync::Arc; /// /// Anywhere a filter is expected, a yes/no [`Expr`] is accepted too: it is the /// filter on its own entity. +/// +/// `&`, `|` and `~` combine filters. `~` keeps what the filter drops: a negated +/// node test keeps the nodes that fail it and the edges between them, and a +/// combination is negated test by test, node tests on nodes and edge tests on +/// edges. A view combines with `&` only. #[pyclass( frozen, name = "FilterExpr", diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index c4089d2d6e..6ea81e8a73 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -127,6 +127,9 @@ impl std::fmt::Display for Typed { /// A value expression: a field, degree, property, metadata entry, an aggregate /// over one, or a yes/no built from them. Comparing it to a value or to another /// expression gives a yes/no [`Expr`], which is a filter on its entity. +/// +/// `~` on a yes/no is the opposite yes/no: a node without the property fails +/// `property("score") > 4`, so it passes `~(property("score") > 4)`. #[pyclass( frozen, subclass, From c61fd37fa23933e0bb7bc7156a813395668ae489 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:52:01 +0100 Subject: [PATCH 146/155] spell the benchmark's filters in the entity-keyed grammar The name scan's select is a node filter under the `node` key with the field read as a plain field, and the stress test's views are the one `ViewCollection` input, so the bench type-checks against the client generated from the current schema. --- graphql-bench/src/bench.ts | 8 +- graphql-bench/src/stress-test.ts | 4 +- .../views/filter/edge_node_filtered_graph.rs | 90 -------------- .../filter/exploded_edge_filtered_graph.rs | 75 ----------- .../exploded_edge_node_filtered_graph.rs | 116 ------------------ 5 files changed, 7 insertions(+), 286 deletions(-) delete mode 100644 raphtory/src/db/graph/views/filter/edge_node_filtered_graph.rs delete mode 100644 raphtory/src/db/graph/views/filter/exploded_edge_filtered_graph.rs delete mode 100644 raphtory/src/db/graph/views/filter/exploded_edge_node_filtered_graph.rs diff --git a/graphql-bench/src/bench.ts b/graphql-bench/src/bench.ts index 885c9e7f2e..c74c510247 100644 --- a/graphql-bench/src/bench.ts +++ b/graphql-bench/src/bench.ts @@ -363,9 +363,11 @@ export function heavyNameScan() { nodes: { __args: { select: { - contains: { - lhs: { read: { entity: "NODE", target: { field: "NAME" } } }, - rhs: { const: { str: "99999" } }, + node: { + contains: { + lhs: { field: "NAME" }, + rhs: { const: { str: "99999" } }, + }, }, }, }, diff --git a/graphql-bench/src/stress-test.ts b/graphql-bench/src/stress-test.ts index 708ed18ef7..b6aee1111b 100644 --- a/graphql-bench/src/stress-test.ts +++ b/graphql-bench/src/stress-test.ts @@ -4,7 +4,7 @@ import { GraphGenqlSelection, EdgeGenqlSelection, NodeGenqlSelection, - PathFromNodeViewCollection, + ViewCollection, } from "./__generated"; import { defineOp, @@ -523,7 +523,7 @@ function randomLayer() { function randomView(rate: ViewRate) { const [start, end] = [randomTime(), randomTime()].sort((a, b) => a - b); - const views: PathFromNodeViewCollection[] = [ + const views: ViewCollection[] = [ ...randomAppend(rate.latest, { latest: true }), ...randomAppend(rate.layer, { layers: [randomLayer()] }), ...randomAppend(rate.window, { window: { start, end } }), diff --git a/raphtory/src/db/graph/views/filter/edge_node_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_node_filtered_graph.rs deleted file mode 100644 index bddfe0b4df..0000000000 --- a/raphtory/src/db/graph/views/filter/edge_node_filtered_graph.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::db::{ - api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - state::ops::NodeFilterOp, - view::internal::{ - GraphView, Immutable, InheritEdgeHistoryFilter, InheritEdgeLayerFilterOps, - InheritExplodedEdgeFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, - InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, - InheritTimeSemantics, InternalEdgeFilterOps, Static, - }, - }, - graph::views::filter::model::edge_filter::Endpoint, -}; -use raphtory_api::{core::entities::LayerIds, inherit::Base}; -use raphtory_storage::{ - core_ops::InheritCoreGraphOps, - graph::edges::{edge_ref::EdgeEntryRef, edge_storage_ops::EdgeStorageOps}, -}; - -#[derive(Debug, Clone)] -pub struct EdgeNodeFilteredGraph { - graph: G, - endpoint: Endpoint, - filter: F, -} - -impl EdgeNodeFilteredGraph { - #[inline] - pub fn new(graph: G, endpoint: Endpoint, filter: F) -> Self { - Self { - graph, - endpoint, - filter, - } - } -} - -impl Base for EdgeNodeFilteredGraph { - type Base = G; - #[inline] - fn base(&self) -> &Self::Base { - &self.graph - } -} - -impl Static for EdgeNodeFilteredGraph {} -impl Immutable for EdgeNodeFilteredGraph {} - -impl InheritCoreGraphOps for EdgeNodeFilteredGraph {} -impl InheritStorageOps for EdgeNodeFilteredGraph {} -impl InheritLayerOps for EdgeNodeFilteredGraph {} -impl InheritListOps for EdgeNodeFilteredGraph {} -impl InheritMaterialize for EdgeNodeFilteredGraph {} -impl InheritNodeFilterOps for EdgeNodeFilteredGraph {} -impl InheritPropertiesOps for EdgeNodeFilteredGraph {} -impl InheritNodePropertySchemaOps for EdgeNodeFilteredGraph {} -impl InheritEdgePropertySchemaOps for EdgeNodeFilteredGraph {} -impl InheritTimeSemantics for EdgeNodeFilteredGraph {} -impl InheritNodeHistoryFilter for EdgeNodeFilteredGraph {} -impl InheritEdgeHistoryFilter for EdgeNodeFilteredGraph {} -impl InheritEdgeLayerFilterOps for EdgeNodeFilteredGraph {} -impl InheritExplodedEdgeFilterOps for EdgeNodeFilteredGraph {} - -impl InternalEdgeFilterOps for EdgeNodeFilteredGraph { - #[inline] - fn internal_edge_filtered(&self) -> bool { - true - } - - #[inline] - fn internal_edge_list_trusted(&self) -> bool { - false - } - - #[inline] - fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { - if !self.graph.internal_filter_edge(edge, layer_ids) { - return false; - } - - let vid = match self.endpoint { - Endpoint::Src => edge.src(), - Endpoint::Dst => edge.dst(), - }; - - self.filter.apply(self.graph.core_graph(), vid) - } -} diff --git a/raphtory/src/db/graph/views/filter/exploded_edge_filtered_graph.rs b/raphtory/src/db/graph/views/filter/exploded_edge_filtered_graph.rs deleted file mode 100644 index a737cbeb42..0000000000 --- a/raphtory/src/db/graph/views/filter/exploded_edge_filtered_graph.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::db::api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - state::ops::GraphView, - view::internal::{ - FilterOps, Immutable, InheritEdgeFilterOps, InheritEdgeHistoryFilter, - InheritEdgeLayerFilterOps, InheritListOps, InheritMaterialize, InheritNodeFilterOps, - InheritNodeHistoryFilter, InheritStorageOps, InheritTimeSemantics, - InternalExplodedEdgeFilterOps, Static, - }, -}; -use raphtory_api::{ - core::{ - entities::{LayerIds, ELID}, - storage::timeindex::EventTime, - }, - inherit::Base, -}; -use raphtory_storage::{core_ops::InheritCoreGraphOps, layer_ops::InheritLayerOps}; - -#[derive(Debug, Clone)] -pub struct ExplodedEdgeFilteredGraph { - base: G, - filter: F, -} - -impl ExplodedEdgeFilteredGraph { - pub fn new(base: G, filter: F) -> Self { - Self { base, filter } - } -} - -impl Base for ExplodedEdgeFilteredGraph { - type Base = G; - - fn base(&self) -> &Self::Base { - &self.base - } -} - -impl Static for ExplodedEdgeFilteredGraph {} -impl Immutable for ExplodedEdgeFilteredGraph {} - -impl InheritCoreGraphOps for ExplodedEdgeFilteredGraph {} -impl InheritStorageOps for ExplodedEdgeFilteredGraph {} -impl InheritLayerOps for ExplodedEdgeFilteredGraph {} -impl InheritListOps for ExplodedEdgeFilteredGraph {} -impl InheritMaterialize for ExplodedEdgeFilteredGraph {} -impl InheritNodeFilterOps for ExplodedEdgeFilteredGraph {} -impl InheritPropertiesOps for ExplodedEdgeFilteredGraph {} -impl InheritNodePropertySchemaOps for ExplodedEdgeFilteredGraph {} -impl InheritEdgePropertySchemaOps for ExplodedEdgeFilteredGraph {} -impl InheritTimeSemantics for ExplodedEdgeFilteredGraph {} -impl InheritNodeHistoryFilter for ExplodedEdgeFilteredGraph {} -impl InheritEdgeHistoryFilter for ExplodedEdgeFilteredGraph {} - -impl InheritEdgeLayerFilterOps for ExplodedEdgeFilteredGraph {} - -impl InheritEdgeFilterOps for ExplodedEdgeFilteredGraph {} - -impl InternalExplodedEdgeFilterOps for ExplodedEdgeFilteredGraph { - fn internal_exploded_edge_filtered(&self) -> bool { - self.base.internal_exploded_edge_filtered() || self.filter.filtered() - } - - fn internal_exploded_filter_edge_list_trusted(&self) -> bool { - false - } - - fn internal_filter_exploded_edge(&self, eid: ELID, t: EventTime, layer_ids: &LayerIds) -> bool { - self.base.internal_filter_exploded_edge(eid, t, layer_ids) - && self.filter.filter_exploded_edge(eid, t) - } -} diff --git a/raphtory/src/db/graph/views/filter/exploded_edge_node_filtered_graph.rs b/raphtory/src/db/graph/views/filter/exploded_edge_node_filtered_graph.rs deleted file mode 100644 index f145851b90..0000000000 --- a/raphtory/src/db/graph/views/filter/exploded_edge_node_filtered_graph.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::db::{ - api::{ - properties::internal::{ - InheritEdgePropertySchemaOps, InheritNodePropertySchemaOps, InheritPropertiesOps, - }, - state::ops::NodeFilterOp, - view::internal::{ - GraphView, Immutable, InheritEdgeFilterOps, InheritEdgeHistoryFilter, - InheritEdgeLayerFilterOps, InheritLayerOps, InheritListOps, InheritMaterialize, - InheritNodeFilterOps, InheritNodeHistoryFilter, InheritStorageOps, - InheritTimeSemantics, InternalExplodedEdgeFilterOps, Static, - }, - }, - graph::views::filter::model::edge_filter::Endpoint, -}; -use either::Either; -use raphtory_api::{ - core::{ - entities::{LayerIds, ELID}, - storage::timeindex::EventTime, - }, - inherit::Base, -}; -use raphtory_storage::{ - core_ops::InheritCoreGraphOps, graph::edges::edge_storage_ops::EdgeStorageOps, -}; - -#[derive(Debug, Clone)] -pub struct ExplodedEdgeNodeFilteredGraph { - graph: G, - endpoint: Endpoint, - filter: F, -} - -impl ExplodedEdgeNodeFilteredGraph { - #[inline] - pub fn new(graph: G, endpoint: Endpoint, filter: F) -> Self { - Self { - graph, - endpoint, - filter, - } - } -} - -impl Base for ExplodedEdgeNodeFilteredGraph { - type Base = G; - #[inline] - fn base(&self) -> &Self::Base { - &self.graph - } -} - -impl Static for ExplodedEdgeNodeFilteredGraph {} -impl Immutable for ExplodedEdgeNodeFilteredGraph {} - -impl InheritCoreGraphOps for ExplodedEdgeNodeFilteredGraph {} -impl InheritStorageOps for ExplodedEdgeNodeFilteredGraph {} -impl InheritLayerOps for ExplodedEdgeNodeFilteredGraph {} -impl InheritListOps for ExplodedEdgeNodeFilteredGraph {} -impl InheritMaterialize for ExplodedEdgeNodeFilteredGraph {} -impl InheritNodeFilterOps for ExplodedEdgeNodeFilteredGraph {} -impl InheritPropertiesOps for ExplodedEdgeNodeFilteredGraph {} -impl InheritNodePropertySchemaOps - for ExplodedEdgeNodeFilteredGraph -{ -} -impl InheritEdgePropertySchemaOps - for ExplodedEdgeNodeFilteredGraph -{ -} -impl InheritTimeSemantics for ExplodedEdgeNodeFilteredGraph {} -impl InheritNodeHistoryFilter - for ExplodedEdgeNodeFilteredGraph -{ -} -impl InheritEdgeHistoryFilter - for ExplodedEdgeNodeFilteredGraph -{ -} -impl InheritEdgeLayerFilterOps - for ExplodedEdgeNodeFilteredGraph -{ -} -impl InheritEdgeFilterOps for ExplodedEdgeNodeFilteredGraph {} - -impl InternalExplodedEdgeFilterOps - for ExplodedEdgeNodeFilteredGraph -{ - #[inline] - fn internal_exploded_edge_filtered(&self) -> bool { - true - } - - #[inline] - fn internal_exploded_filter_edge_list_trusted(&self) -> bool { - false - } - - #[inline] - fn internal_filter_exploded_edge(&self, eid: ELID, t: EventTime, layer_ids: &LayerIds) -> bool { - if !self.graph.internal_filter_exploded_edge(eid, t, layer_ids) { - return false; - } - - let edge = self.graph.core_edge(Either::Left(eid.eid())); - - let vid = match self.endpoint { - Endpoint::Src => edge.src(), - Endpoint::Dst => edge.dst(), - }; - - // TODO: Fix me! Apply needs to take a graph view as input - self.filter.apply(self.graph.core_graph(), vid) - } -} From 9d6584dcf7a731a05128a2c32994da7a6e828817 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:52:01 +0100 Subject: [PATCH 147/155] describe every filter input in the schema and take filters by value Every new input type, field and variant of the filter grammar carries a description, the resolver doc examples spell a window as a view, and the generated API reference follows. A wrapped input is unboxed instead of cloned at each level of the tree conversion, and the resolvers apply a `GqlFilter` directly rather than through a precompiled detour, so the node-collection refusal of edge-testing filters applies on the server as well as in the client. --- docs/reference/graphql/graphql_API.md | 1800 ++++++++++------- raphtory-graphql/schema.graphql | 414 +++- .../src/client/graphql_transport.rs | 28 +- raphtory-graphql/src/data.rs | 7 +- raphtory-graphql/src/model/graph/edge.rs | 5 +- raphtory-graphql/src/model/graph/edges.rs | 13 +- .../src/model/graph/filter_expr_input.rs | 95 +- raphtory-graphql/src/model/graph/filtering.rs | 13 +- raphtory-graphql/src/model/graph/graph.rs | 17 +- .../src/model/graph/namespace_filtering.rs | 7 +- .../src/model/graph/nested_edges.rs | 5 +- raphtory-graphql/src/model/graph/node.rs | 19 +- raphtory-graphql/src/model/graph/nodes.rs | 18 +- .../src/model/graph/path_from_graph.rs | 5 +- .../src/model/graph/path_from_node.rs | 11 +- 15 files changed, 1571 insertions(+), 886 deletions(-) diff --git a/docs/reference/graphql/graphql_API.md b/docs/reference/graphql/graphql_API.md index 9574546ecf..339849d0ac 100644 --- a/docs/reference/graphql/graphql_API.md +++ b/docs/reference/graphql/graphql_API.md @@ -2531,7 +2531,7 @@ Takes a specified selection of views and applies them in given order. - + - + - + - + @@ -9992,7 +9992,7 @@ Takes a specified selection of views and applies them in given order. - + - + +
views[EdgeViewCollection!]![ViewCollection!]! Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. @@ -3350,7 +3350,7 @@ Takes a specified selection of views and applies them in order given.
views[EdgesViewCollection!]![ViewCollection!]! Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. @@ -7151,7 +7151,7 @@ Takes a specified selection of views and applies them in order given.
views[EdgesViewCollection!]![ViewCollection!]! Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. @@ -7731,7 +7731,7 @@ Proposed new end (TimeInput); ignored if it would widen the window.
views[NodeViewCollection!]![ViewCollection!]!
views[PathFromNodeViewCollection!]![ViewCollection!]! Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. @@ -10652,7 +10652,7 @@ Takes a specified selection of views and applies them in given order.
views[PathFromNodeViewCollection!]![ViewCollection!]! Ordered list of view operations; each entry is a one-of variant (`window`, `layer`, `filter`, ...) applied to the running result. @@ -11804,32 +11804,6 @@ Optional `{start, end}` to restrict matches to edges active in that interval. ## Inputs -### Cmp - -Two expressions to compare. - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
lhsExpr!
rhsExpr!
- ### EdgeAddition @@ -11885,7 +11859,9 @@ Metadata.
-### EdgeSortBy +### EdgeCmp + +Two expressions to compare. @@ -11897,207 +11873,296 @@ Metadata. - - - + + + - - - + + + + +
reverseBoolean - -Reverse order. Applies to the `time` / `property` keys; the node keys -(`src` / `dst` / `neighbour`) carry their own `reverse` inside the -nested `NodeSortBy` and ignore this flag. - -lhsEdgeExpr!
srcNodeSortBy +rhsEdgeExpr!
-Sort by the source node. +### EdgeExpr -
+ + + + + + + - - + + - - + + - - + + - + - -
FieldTypeDescription
dstNodeSortByconstValue -Sort by the destination node. +A literal.
neighbourNodeSortBypropertyString -Sort by the neighbour node: the endpoint that is NOT the node these -edges were traversed from (the destination for a graph-level edge -collection). +The latest value of a property.
timeSortByTimetemporalPropertyString -Time +The history of a property, as a list.
propertymetadata String -Property +A metadata entry.
- -### EdgeViewCollection - - - - - - - - - - - + - + - + - - + + - - + + - - + + - - + + - - - + + + - - - + + + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + +
FieldTypeDescription
defaultLayerisActive Boolean -Contains only the default layer. +Whether the entity is active; written `isActive: true`.
latestisValid Boolean -Latest time. +Whether the edge is valid; written `isValid: true`.
snapshotLatestisDeleted Boolean -Snapshot at latest time. +Whether the edge is deleted; written `isDeleted: true`.
snapshotAtTimeInputisSelfLoopBoolean -Snapshot at specified time. +Whether the edge is a self loop; written `isSelfLoop: true`.
layers[String!]srcNodeExpr -List of included layers. +A node expression evaluated on the edge's source node.
excludeLayers[String!]dstNodeExpr -List of excluded layers. +A node expression evaluated on the edge's destination node.
excludeLayerStringviewedEdgeViewed -Single excluded layer. +Views applied to every read inside.
windowWindow - -Window between a start and end time. - -sumEdgeExpr
atTimeInput - -View at a specified time. - -avgEdgeExpr
beforeTimeInput - -View before a specified time (end exclusive). - -minEdgeExpr
afterTimeInputmaxEdgeExpr
firstEdgeExpr
lastEdgeExpr
lenEdgeExpr
earliestEdgeExpr -View after a specified time (start exclusive). +The earliest update of a temporal history.
shrinkStartTimeInputlatestEdgeExpr -Set the window start to a specified time. +The latest update of a temporal history.
shrinkEndTimeInputeqEdgeCmp
neEdgeCmp
ltEdgeCmp
leEdgeCmp
gtEdgeCmp
geEdgeCmp
startsWithEdgeCmp
endsWithEdgeCmp
containsEdgeCmp
notContainsEdgeCmp
fuzzySearchEdgeFuzzyCmp
isInEdgeMembership
isNotInEdgeMembership
isSomeEdgeExpr
isNoneEdgeExpr
anyEdgeExpr -Set the window end to a specified time. +Holds when the element-wise result inside holds for any element.
filterFilterExprallEdgeExpr -A filter tree; the entity it tests is written in the tree. +Holds when the element-wise result inside holds for every element.
and[EdgeExpr!]
or[EdgeExpr!]
notEdgeExpr
-### EdgesViewCollection +### EdgeFuzzyCmp + +A fuzzy string match: `lhs` is within `levenshteinDistance` edits of +`rhs`, optionally matching by prefix. @@ -12109,130 +12174,149 @@ A filter tree; the entity it tests is written in the tree. - - - + + + - - - + + + - - - + + + - - - + + + + +
defaultLayerBoolean - -Contains only the default layer. - -lhsEdgeExpr!
latestBoolean - -Latest time. - -rhsEdgeExpr!
snapshotLatestBoolean - -Snapshot at latest time. - -levenshteinDistanceInt!
snapshotAtTimeInput +prefixMatchBoolean!
-Snapshot at specified time. +### EdgeMembership - +A membership test. `values` is a list; a policy may also leave a single +placeholder here (`{"var": …}`) that resolves to the list per caller. + + + + + + + + + - - - + + + - - - + + + + +
FieldTypeDescription
layers[String!] - -List of included layers. - -exprEdgeExpr!
excludeLayers[String!] +valuesValue!
-List of excluded layers. +### EdgeSortBy - + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - +
FieldTypeDescription
excludeLayerStringreverseBoolean -Single excluded layer. +Reverse order. Applies to the `time` / `property` keys; the node keys +(`src` / `dst` / `neighbour`) carry their own `reverse` inside the +nested `NodeSortBy` and ignore this flag.
windowWindowsrcNodeSortBy -Window between a start and end time. +Sort by the source node.
atTimeInputdstNodeSortBy -View at a specified time. +Sort by the destination node.
beforeTimeInputneighbourNodeSortBy -View before a specified time (end exclusive). +Sort by the neighbour node: the endpoint that is NOT the node these +edges were traversed from (the destination for a graph-level edge +collection).
afterTimeInputtimeSortByTime -View after a specified time (start exclusive). +Time
shrinkStartTimeInputpropertyString -Set the window start to a specified time. +Property
shrinkEndTimeInput +
-Set the window end to a specified time. +### EdgeViewed - +Views applied to every read inside `expr`, in list order. + + + + + + + + + + + + + + - - - + + +
FieldTypeDescription
views[ViewOp!]!
filterFilterExpr - -A filter tree; the entity it tests is written in the tree. - -exprEdgeExpr!
@@ -12260,9 +12344,9 @@ OpenAI embedding models or compatible providers -### Expr +### ExplodedEdgeCmp -A value: what stands on either side of a comparison. +Two expressions to compare. @@ -12274,7 +12358,31 @@ A value: what stands on either side of a comparison. - + + + + + + + + + + +
constlhsExplodedEdgeExpr!
rhsExplodedEdgeExpr!
+ +### ExplodedEdgeExpr + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - -
FieldTypeDescription
const Value @@ -12283,218 +12391,334 @@ A literal.
readReadpropertyString + +The latest value of a property. + +
temporalPropertyString + +The history of a property, as a list. + +
metadataString + +A metadata entry. + +
isActiveBoolean + +Whether the entity is active; written `isActive: true`. + +
isValidBoolean + +Whether the edge update is valid; written `isValid: true`. + +
isDeletedBoolean + +Whether the edge update is deleted; written `isDeleted: true`. + +
isSelfLoopBoolean -A field, degree, property or metadata read from an entity. +Whether the edge update is a self loop; written `isSelfLoop: true`.
temporalExprviewedExplodedEdgeViewed -The full history of a property instead of its latest value. +Views applied to every read inside.
sumExprsumExplodedEdgeExpr
avgExpravgExplodedEdgeExpr
minExprminExplodedEdgeExpr
maxExprmaxExplodedEdgeExpr
firstExprfirstExplodedEdgeExpr
lastExprlastExplodedEdgeExpr
lenExprlenExplodedEdgeExpr
anyExprearliestExplodedEdgeExpr -The predicate holds if it holds for any element. +The earliest update of a temporal history.
allExprlatestExplodedEdgeExpr -The predicate holds if it holds for every element. +The latest update of a temporal history.
- -### FilterExpr - -The filter itself: a yes/no over an entity. - - - - - - + + + - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + - - + + - - + + + + + +
FieldTypeDescriptioneqExplodedEdgeCmp
eqCmpneExplodedEdgeCmp
neCmpltExplodedEdgeCmp
ltCmpleExplodedEdgeCmp
leCmpgtExplodedEdgeCmp
gtCmpgeExplodedEdgeCmp
geCmpstartsWithExplodedEdgeCmp
startsWithCmpendsWithExplodedEdgeCmp
endsWithCmpcontainsExplodedEdgeCmp
containsCmpnotContainsExplodedEdgeCmp
notContainsCmpfuzzySearchExplodedEdgeFuzzyCmp
fuzzySearchFuzzyCmpisInExplodedEdgeMembership
isSomeExprisNotInExplodedEdgeMembership
isNoneExprisSomeExplodedEdgeExpr
isInMembershipisNoneExplodedEdgeExpr
isNotInMembershipanyExplodedEdgeExpr + +Holds when the element-wise result inside holds for any element. + +
allExplodedEdgeExpr + +Holds when the element-wise result inside holds for every element. + +
and[ExplodedEdgeExpr!]
isActiveScopeor[ExplodedEdgeExpr!]
isValidScopenotExplodedEdgeExpr
+ +### ExplodedEdgeFuzzyCmp + +A fuzzy string match: `lhs` is within `levenshteinDistance` edits of +`rhs`, optionally matching by prefix. + + + + + + + + + + + + + - - + + - - + + - - - + + + + +
FieldTypeDescription
lhsExplodedEdgeExpr!
isDeletedScoperhsExplodedEdgeExpr!
isSelfLoopScopelevenshteinDistanceInt!
view[ViewOp!] +prefixMatchBoolean!
-A graph-level view with no predicate: the result is the view. +### ExplodedEdgeMembership - +A membership test. `values` is a list; a policy may also leave a single +placeholder here (`{"var": …}`) that resolves to the list per caller. + + + + + + + + + - - + + - - + + + +
FieldTypeDescription
and[FilterExpr!]exprExplodedEdgeExpr!
or[FilterExpr!]valuesValue!
+ +### ExplodedEdgeViewed + +Views applied to every read inside `expr`, in list order. + + + - - + + + + + + + + + + + + + +
notFilterExprFieldTypeDescription
views[ViewOp!]!
exprExplodedEdgeExpr!
-### FuzzyCmp +### FilterExpr -A fuzzy string match: `lhs` is within `levenshteinDistance` edits of -`rhs`, optionally matching by prefix. +The filter itself: a yes/no over one kind of entity, a view, or a combination. @@ -12506,24 +12730,57 @@ A fuzzy string match: `lhs` is within `levenshteinDistance` edits of - - + + + + + + + - - + + - - - + + + + + + + + + + + + + - - - + + +
lhsExpr!nodeNodeExpr
edgeEdgeExpr
rhsExpr!explodedEdgeExplodedEdgeExpr
levenshteinDistanceInt!view[ViewOp!] + +A graph-level view with no predicate: the result is the view. + +
and[FilterExpr!] + +Every leg holds. A view leg applies first and the others run inside it. + +
or[FilterExpr!] + +Any leg holds. Node legs combine on nodes and edge legs on edges; a leg +of the other kind leaves that side unconstrained. No view legs. + +
prefixMatchBoolean!notFilterExpr + +The filter that keeps what the inner one drops: a negated node filter +keeps the nodes that fail it and the edges between them. No views. + +
@@ -12742,7 +12999,7 @@ Set the window end to a specified time.
FilterExpr -A filter tree; the entity it tests is written in the tree. +A filter expression; the entity it tests is written in the expression. @@ -12781,33 +13038,6 @@ Destination node id (string or non-negative integer). -### Membership - -A membership test. `values` is a list; a policy may also leave a single -placeholder here (`{"var": …}`) that resolves to the list per caller. - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
exprExpr!
valuesValue!
- ### MetaGraphCondition One condition on a graph, testing either a built-in attribute or a metadata @@ -13149,7 +13379,9 @@ Layer. -### NodeSortBy +### NodeCmp + +Two expressions to compare. @@ -13161,198 +13393,419 @@ Layer. - - + + + + + + + + + + +
reverseBooleanlhsNodeExpr!
rhsNodeExpr!
+ +### NodeExpr + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - -
FieldTypeDescription
constValue -Reverse order +A literal.
idBooleanpropertyString -Unique Id +The latest value of a property.
nameBooleantemporalPropertyString -Node name +The history of a property, as a list.
typeBooleanmetadataString -Node type. Untyped nodes sort first (before any named type). +A metadata entry.
timeSortByTimeisActiveBoolean -Time +Whether the entity is active; written `isActive: true`.
propertyStringfieldNodeFieldName -Property +A built-in node field.
+ +degree +DegreeDirection + -### NodeViewCollection +The node's degree in a direction. - - - - - - + - - - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + + + + + + + + + + + + + +
FieldTypeDescription
defaultLayerBooleanviewedNodeViewed -Contains only the default layer. +Views applied to every read inside.
latestBooleansumNodeExpr
avgNodeExpr
minNodeExpr
maxNodeExpr
firstNodeExpr
lastNodeExpr
lenNodeExpr
earliestNodeExpr -View at the latest time. +The earliest update of a temporal history.
snapshotLatestBooleanlatestNodeExpr -Snapshot at latest time. +The latest update of a temporal history.
snapshotAtTimeInputeqNodeCmp
neNodeCmp
ltNodeCmp
leNodeCmp
gtNodeCmp
geNodeCmp
startsWithNodeCmp
endsWithNodeCmp
containsNodeCmp
notContainsNodeCmp
fuzzySearchNodeFuzzyCmp
isInNodeMembership
isNotInNodeMembership
isSomeNodeExpr
isNoneNodeExpr
anyNodeExpr -Snapshot at specified time. +Holds when the element-wise result inside holds for any element.
layers[String!]allNodeExpr -List of included layers. +Holds when the element-wise result inside holds for every element.
excludeLayers[String!]and[NodeExpr!]
or[NodeExpr!]
notNodeExpr
+ +### NodeFuzzyCmp + +A fuzzy string match: `lhs` is within `levenshteinDistance` edits of +`rhs`, optionally matching by prefix. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
lhsNodeExpr!
rhsNodeExpr!
levenshteinDistanceInt!
prefixMatchBoolean!
+ +### NodeMembership + +A membership test. `values` is a list; a policy may also leave a single +placeholder here (`{"var": …}`) that resolves to the list per caller. + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
exprNodeExpr!
valuesValue!
+ +### NodeSortBy + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - - - +
FieldTypeDescription
reverseBoolean -List of excluded layers. +Reverse order
excludeLayerStringidBoolean -Single excluded layer. +Unique Id
windowWindownameBoolean -Window between a start and end time. +Node name
atTimeInputtypeBoolean -View at a specified time. +Node type. Untyped nodes sort first (before any named type).
beforeTimeInputtimeSortByTime -View before a specified time (end exclusive). +Time
afterTimeInputpropertyString -View after a specified time (start exclusive). +Property
shrinkStartTimeInput +
-Set the window start to a specified time. +### NodeViewed - +Views applied to every read inside `expr`, in list order. + + + + + + + + + - - - + + + - - - + + +
FieldTypeDescription
shrinkEndTimeInput - -Set the window end to a specified time. - -views[ViewOp!]!
filterFilterExpr - -A filter tree; the entity it tests is written in the tree. - -exprNodeExpr!
@@ -13490,7 +13943,7 @@ Set the window end to a specified time. FilterExpr -A filter tree; the entity it tests is written in the tree. +A filter expression; the entity it tests is written in the expression. @@ -13577,128 +14030,6 @@ Value. -### PathFromNodeViewCollection - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
latestBoolean - -Latest time. - -
snapshotLatestBoolean - -Latest snapshot. - -
snapshotAtTimeInput - -Time. - -
layers[String!] - -List of layers. - -
excludeLayers[String!] - -List of excluded layers. - -
excludeLayerString - -Single layer to exclude. - -
windowWindow - -Window between a start and end time. - -
atTimeInput - -View at a specified time. - -
beforeTimeInput - -View before a specified time (end exclusive). - -
afterTimeInput - -View after a specified time (start exclusive). - -
shrinkStartTimeInput - -Set the window start to a specified time. - -
shrinkEndTimeInput - -Set the window end to a specified time. - -
- ### PropCondition Boolean expression over a property value. @@ -13940,147 +14271,45 @@ Applies the nested condition to the **sum** of a numeric list-like property. -avg -PropCondition - - -Applies the nested condition to the **average** of a numeric list-like property. - - - - -min -PropCondition - - -Applies the nested condition to the **minimum** element of a list-like property. - - - - -max -PropCondition - - -Applies the nested condition to the **maximum** element of a list-like property. - - - - -len -PropCondition - - -Applies the nested condition to the **length** of a list-like property. - - - - - - -### PropertyInput - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
keyString! - -Key. - -
valueValue! - -Value. - -
- -### Read - -A value read from an entity. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
entityEntity!
views[ViewOp!]
endpointEndpoint
targetTarget!
- -### Scope +avg +PropCondition + -Where a value is read: the entity, the views to read it through, and for an -edge optionally one of its endpoint nodes. +Applies the nested condition to the **average** of a numeric list-like property. - - - - - - + - - - - - + + + - - - + + + - - - + + +
FieldTypeDescription
entityEntity!minPropCondition + +Applies the nested condition to the **minimum** element of a list-like property. + +
views[ViewOp!]maxPropCondition + +Applies the nested condition to the **maximum** element of a list-like property. + +
endpointEndpointlenPropCondition + +Applies the nested condition to the **length** of a list-like property. + +
-### Seeds - -How the initially infected nodes are chosen. +### PropertyInput @@ -14092,38 +14321,29 @@ How the initially infected nodes are chosen. - - - - - - - + + - - + +
nodes[NodeId!] - -Infect exactly these nodes. - -
numberIntkeyString! -Infect this many randomly chosen nodes. +Key.
probabilityFloatvalueValue! -Infect this fraction of the nodes, chosen at random. +Value.
-### Target +### Seeds -What a read selects on its entity. +How the initially infected nodes are chosen. @@ -14135,38 +14355,29 @@ What a read selects on its entity. - - - - - - - + + - - + + - - + + @@ -14463,6 +14674,149 @@ Exclusive upper bound of the search window.
fieldNodeFieldName - -A built-in node field. - -
degreeDegreeDirectionnodes[NodeId!] -A node degree in a direction. +Infect exactly these nodes.
propertyStringnumberInt -A property, by name. +Infect this many randomly chosen nodes.
metadataStringprobabilityFloat -A metadata entry, by name. +Infect this fraction of the nodes, chosen at random.
+### ViewCollection + +A view operation on a node, an edge or a collection of either; the +same list for all of them now that one filter type serves every entity. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
defaultLayerBoolean + +Contains only the default layer. + +
latestBoolean + +View at the latest time. + +
snapshotLatestBoolean + +Snapshot at latest time. + +
snapshotAtTimeInput + +Snapshot at specified time. + +
layers[String!] + +List of included layers. + +
excludeLayers[String!] + +List of excluded layers. + +
excludeLayerString + +Single excluded layer. + +
windowWindow + +Window between a start and end time. + +
atTimeInput + +View at a specified time. + +
beforeTimeInput + +View before a specified time (end exclusive). + +
afterTimeInput + +View after a specified time (start exclusive). + +
shrinkStartTimeInput + +Set the window start to a specified time. + +
shrinkEndTimeInput + +Set the window end to a specified time. + +
filterFilterExpr + +A filter expression; the entity it tests is written in the expression. + +
+ ### ViewOp One view restriction, applied in list order. @@ -14692,56 +15046,6 @@ Edge direction to follow during traversal. -### Endpoint - -Which end of an edge a read looks at. - - - - - - - - - - - - - - - - - - -
ValueDescription
SRC
DST
- -### Entity - -The kind of thing a filter tests: a node, an edge, or one edge update. - - - - - - - - - - - - - - - - - - - - - - -
ValueDescription
NODE
EDGE
EXPLODED_EDGE
- ### GraphType diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index a1b0795bbf..47bb9b0d78 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -1056,10 +1056,20 @@ input EdgeAddition { Two expressions to compare. """ input EdgeCmp { + """ + The left side. + """ lhs: EdgeExpr! + """ + The right side. + """ rhs: EdgeExpr! } +""" +A value or yes/no on one entity: a read, an aggregate over one, a +comparison or test, or a combination of yes/nos. +""" input EdgeExpr @oneOf { """ A literal. @@ -1105,12 +1115,33 @@ input EdgeExpr @oneOf { Views applied to every read inside. """ viewed: EdgeViewed + """ + The sum of the innermost list. + """ sum: EdgeExpr + """ + The mean of the innermost list. + """ avg: EdgeExpr + """ + The smallest element of the innermost list. + """ min: EdgeExpr + """ + The largest element of the innermost list. + """ max: EdgeExpr + """ + The first element of the innermost list. + """ first: EdgeExpr + """ + The last element of the innermost list. + """ last: EdgeExpr + """ + The number of elements of the innermost list. + """ len: EdgeExpr """ The earliest update of a temporal history. @@ -1120,20 +1151,65 @@ input EdgeExpr @oneOf { The latest update of a temporal history. """ latest: EdgeExpr + """ + `lhs == rhs`. + """ eq: EdgeCmp + """ + `lhs != rhs`. + """ ne: EdgeCmp + """ + `lhs < rhs`. + """ lt: EdgeCmp + """ + `lhs <= rhs`. + """ le: EdgeCmp + """ + `lhs > rhs`. + """ gt: EdgeCmp + """ + `lhs >= rhs`. + """ ge: EdgeCmp + """ + The string `lhs` starts with the string `rhs`. + """ startsWith: EdgeCmp + """ + The string `lhs` ends with the string `rhs`. + """ endsWith: EdgeCmp + """ + The string `lhs` contains the string `rhs`. + """ contains: EdgeCmp + """ + The string `lhs` does not contain the string `rhs`. + """ notContains: EdgeCmp + """ + The string `lhs` is within an edit distance of `rhs`. + """ fuzzySearch: EdgeFuzzyCmp + """ + `expr` is one of `values`. + """ isIn: EdgeMembership + """ + `expr` is none of `values`. + """ isNotIn: EdgeMembership + """ + The value is present. + """ isSome: EdgeExpr + """ + The value is absent. + """ isNone: EdgeExpr """ Holds when the element-wise result inside holds for any element. @@ -1143,8 +1219,17 @@ input EdgeExpr @oneOf { Holds when the element-wise result inside holds for every element. """ all: EdgeExpr + """ + Every yes/no inside holds. + """ and: [EdgeExpr!] + """ + Any yes/no inside holds. + """ or: [EdgeExpr!] + """ + The yes/no inside does not hold. + """ not: EdgeExpr } @@ -1153,9 +1238,21 @@ A fuzzy string match: `lhs` is within `levenshteinDistance` edits of `rhs`, optionally matching by prefix. """ input EdgeFuzzyCmp { + """ + The string to test. + """ lhs: EdgeExpr! + """ + The string to match. + """ rhs: EdgeExpr! + """ + The largest edit distance that still matches. + """ levenshteinDistance: Int! + """ + Whether a match on a prefix counts. + """ prefixMatch: Boolean! } @@ -1164,7 +1261,13 @@ A membership test. `values` is a list; a policy may also leave a single placeholder here (`{"var": …}`) that resolves to the list per caller. """ input EdgeMembership { + """ + The value to look for. + """ expr: EdgeExpr! + """ + The values it may be one of. + """ values: Value! } @@ -1227,7 +1330,13 @@ input EdgeSortBy { Views applied to every read inside `expr`, in list order. """ input EdgeViewed { + """ + The views, applied in list order. + """ views: [ViewOp!]! + """ + The expression read inside them. + """ expr: EdgeExpr! } @@ -1548,7 +1657,7 @@ type Edges { E.g. restricting everything to a specific week: ```text - edges { filter(expr: {window: {start: 1234, end: 5678}}) { + edges { filter(expr: {view: [{window: {start: 1234, end: 5678}}]}) { list { src { neighbours { list { name } } } } # neighbours still windowed } } ``` @@ -1570,9 +1679,9 @@ type Edges { neighbours on Wednesday: ```text - edges { select(expr: {window: {...monday...}}) { - list { src { select(expr: {window: {...tuesday...}}) { - neighbours { select(expr: {window: {...wednesday...}}) { + edges { select(expr: {view: [{window: {...monday...}}]}) { + list { src { select(expr: {view: [{window: {...tuesday...}}]}) { + neighbours { select(expr: {view: [{window: {...wednesday...}}]}) { neighbours { list { name } } } } } } } @@ -1675,10 +1784,20 @@ type EventTime { Two expressions to compare. """ input ExplodedEdgeCmp { + """ + The left side. + """ lhs: ExplodedEdgeExpr! + """ + The right side. + """ rhs: ExplodedEdgeExpr! } +""" +A value or yes/no on one entity: a read, an aggregate over one, a +comparison or test, or a combination of yes/nos. +""" input ExplodedEdgeExpr @oneOf { """ A literal. @@ -1716,12 +1835,33 @@ input ExplodedEdgeExpr @oneOf { Views applied to every read inside. """ viewed: ExplodedEdgeViewed + """ + The sum of the innermost list. + """ sum: ExplodedEdgeExpr + """ + The mean of the innermost list. + """ avg: ExplodedEdgeExpr + """ + The smallest element of the innermost list. + """ min: ExplodedEdgeExpr + """ + The largest element of the innermost list. + """ max: ExplodedEdgeExpr + """ + The first element of the innermost list. + """ first: ExplodedEdgeExpr + """ + The last element of the innermost list. + """ last: ExplodedEdgeExpr + """ + The number of elements of the innermost list. + """ len: ExplodedEdgeExpr """ The earliest update of a temporal history. @@ -1731,20 +1871,65 @@ input ExplodedEdgeExpr @oneOf { The latest update of a temporal history. """ latest: ExplodedEdgeExpr + """ + `lhs == rhs`. + """ eq: ExplodedEdgeCmp + """ + `lhs != rhs`. + """ ne: ExplodedEdgeCmp + """ + `lhs < rhs`. + """ lt: ExplodedEdgeCmp + """ + `lhs <= rhs`. + """ le: ExplodedEdgeCmp + """ + `lhs > rhs`. + """ gt: ExplodedEdgeCmp + """ + `lhs >= rhs`. + """ ge: ExplodedEdgeCmp + """ + The string `lhs` starts with the string `rhs`. + """ startsWith: ExplodedEdgeCmp + """ + The string `lhs` ends with the string `rhs`. + """ endsWith: ExplodedEdgeCmp + """ + The string `lhs` contains the string `rhs`. + """ contains: ExplodedEdgeCmp + """ + The string `lhs` does not contain the string `rhs`. + """ notContains: ExplodedEdgeCmp + """ + The string `lhs` is within an edit distance of `rhs`. + """ fuzzySearch: ExplodedEdgeFuzzyCmp + """ + `expr` is one of `values`. + """ isIn: ExplodedEdgeMembership + """ + `expr` is none of `values`. + """ isNotIn: ExplodedEdgeMembership + """ + The value is present. + """ isSome: ExplodedEdgeExpr + """ + The value is absent. + """ isNone: ExplodedEdgeExpr """ Holds when the element-wise result inside holds for any element. @@ -1754,8 +1939,17 @@ input ExplodedEdgeExpr @oneOf { Holds when the element-wise result inside holds for every element. """ all: ExplodedEdgeExpr + """ + Every yes/no inside holds. + """ and: [ExplodedEdgeExpr!] + """ + Any yes/no inside holds. + """ or: [ExplodedEdgeExpr!] + """ + The yes/no inside does not hold. + """ not: ExplodedEdgeExpr } @@ -1764,9 +1958,21 @@ A fuzzy string match: `lhs` is within `levenshteinDistance` edits of `rhs`, optionally matching by prefix. """ input ExplodedEdgeFuzzyCmp { + """ + The string to test. + """ lhs: ExplodedEdgeExpr! + """ + The string to match. + """ rhs: ExplodedEdgeExpr! + """ + The largest edit distance that still matches. + """ levenshteinDistance: Int! + """ + Whether a match on a prefix counts. + """ prefixMatch: Boolean! } @@ -1775,7 +1981,13 @@ A membership test. `values` is a list; a policy may also leave a single placeholder here (`{"var": …}`) that resolves to the list per caller. """ input ExplodedEdgeMembership { + """ + The value to look for. + """ expr: ExplodedEdgeExpr! + """ + The values it may be one of. + """ values: Value! } @@ -1783,7 +1995,13 @@ input ExplodedEdgeMembership { Views applied to every read inside `expr`, in list order. """ input ExplodedEdgeViewed { + """ + The views, applied in list order. + """ views: [ViewOp!]! + """ + The expression read inside them. + """ expr: ExplodedEdgeExpr! } @@ -1791,8 +2009,17 @@ input ExplodedEdgeViewed { The filter itself: a yes/no over one kind of entity, a view, or a combination. """ input FilterExpr @oneOf { + """ + A yes/no over nodes. + """ node: NodeExpr + """ + A yes/no over edges. + """ edge: EdgeExpr + """ + A yes/no over exploded edges, one per update of an edge. + """ explodedEdge: ExplodedEdgeExpr """ A graph-level view with no predicate: the result is the view. @@ -2290,6 +2517,10 @@ enum GraphType { EVENT } +""" +A view operation on a graph, applied in list order; graph-level ops such as +`subgraph` and `valid` live here only. +""" input GraphViewCollection @oneOf { """ Contains only the default layer. @@ -2961,8 +3192,7 @@ input MetaGraphCondition { """ metadataKey: String """ - Condition applied to the value, using the same grammar as property - filters elsewhere in the schema. Names, paths and metadata strings are + Condition applied to the value. Names, paths and metadata strings are tested as strings; counts and timestamps as integers. """ where: PropCondition! @@ -4395,15 +4625,15 @@ type Node { """ inDegree: Int! """ - Nodes that can reach this one via out-edges. `select` is a general filter expression — a node - filter, an edge filter, or a graph (layer/window) filter — scoping which nodes/edges the walk + Nodes that can reach this one via out-edges. `select` is a filter expression — node and edge + predicates, views, and their `and`/`or`/`not` combinations — scoping which nodes/edges the walk steps through. The returned nodes are on the full graph so their other-layer neighbours stay queryable. """ inComponent(select: FilterExpr): Nodes! """ - Nodes reachable from this one via out-edges. `select` is a general filter expression — a node - filter, an edge filter, or a graph (layer/window) filter — scoping which nodes/edges the walk + Nodes reachable from this one via out-edges. `select` is a filter expression — node and edge + predicates, views, and their `and`/`or`/`not` combinations — scoping which nodes/edges the walk steps through. The returned nodes are on the full (unfiltered) graph, so their other-layer neighbours remain queryable. """ @@ -4467,10 +4697,20 @@ input NodeAddition { Two expressions to compare. """ input NodeCmp { + """ + The left side. + """ lhs: NodeExpr! + """ + The right side. + """ rhs: NodeExpr! } +""" +A value or yes/no on one entity: a read, an aggregate over one, a +comparison or test, or a combination of yes/nos. +""" input NodeExpr @oneOf { """ A literal. @@ -4504,12 +4744,33 @@ input NodeExpr @oneOf { Views applied to every read inside. """ viewed: NodeViewed + """ + The sum of the innermost list. + """ sum: NodeExpr + """ + The mean of the innermost list. + """ avg: NodeExpr + """ + The smallest element of the innermost list. + """ min: NodeExpr + """ + The largest element of the innermost list. + """ max: NodeExpr + """ + The first element of the innermost list. + """ first: NodeExpr + """ + The last element of the innermost list. + """ last: NodeExpr + """ + The number of elements of the innermost list. + """ len: NodeExpr """ The earliest update of a temporal history. @@ -4519,20 +4780,65 @@ input NodeExpr @oneOf { The latest update of a temporal history. """ latest: NodeExpr + """ + `lhs == rhs`. + """ eq: NodeCmp + """ + `lhs != rhs`. + """ ne: NodeCmp + """ + `lhs < rhs`. + """ lt: NodeCmp + """ + `lhs <= rhs`. + """ le: NodeCmp + """ + `lhs > rhs`. + """ gt: NodeCmp + """ + `lhs >= rhs`. + """ ge: NodeCmp + """ + The string `lhs` starts with the string `rhs`. + """ startsWith: NodeCmp + """ + The string `lhs` ends with the string `rhs`. + """ endsWith: NodeCmp + """ + The string `lhs` contains the string `rhs`. + """ contains: NodeCmp + """ + The string `lhs` does not contain the string `rhs`. + """ notContains: NodeCmp + """ + The string `lhs` is within an edit distance of `rhs`. + """ fuzzySearch: NodeFuzzyCmp + """ + `expr` is one of `values`. + """ isIn: NodeMembership + """ + `expr` is none of `values`. + """ isNotIn: NodeMembership + """ + The value is present. + """ isSome: NodeExpr + """ + The value is absent. + """ isNone: NodeExpr """ Holds when the element-wise result inside holds for any element. @@ -4542,8 +4848,17 @@ input NodeExpr @oneOf { Holds when the element-wise result inside holds for every element. """ all: NodeExpr + """ + Every yes/no inside holds. + """ and: [NodeExpr!] + """ + Any yes/no inside holds. + """ or: [NodeExpr!] + """ + The yes/no inside does not hold. + """ not: NodeExpr } @@ -4551,8 +4866,17 @@ input NodeExpr @oneOf { A built-in node field. """ enum NodeFieldName { + """ + The node's id. + """ ID + """ + The node's name. + """ NAME + """ + The node's type. + """ NODE_TYPE } @@ -4561,9 +4885,21 @@ A fuzzy string match: `lhs` is within `levenshteinDistance` edits of `rhs`, optionally matching by prefix. """ input NodeFuzzyCmp { + """ + The string to test. + """ lhs: NodeExpr! + """ + The string to match. + """ rhs: NodeExpr! + """ + The largest edit distance that still matches. + """ levenshteinDistance: Int! + """ + Whether a match on a prefix counts. + """ prefixMatch: Boolean! } @@ -4579,7 +4915,13 @@ A membership test. `values` is a list; a policy may also leave a single placeholder here (`{"var": …}`) that resolves to the list per caller. """ input NodeMembership { + """ + The value to look for. + """ expr: NodeExpr! + """ + The values it may be one of. + """ values: Value! } @@ -4919,7 +5261,13 @@ union NodeStateValue = NodeStateProp | Node | Nodes Views applied to every read inside `expr`, in list order. """ input NodeViewed { + """ + The views, applied in list order. + """ views: [ViewOp!]! + """ + The expression read inside them. + """ expr: NodeExpr! } @@ -5244,7 +5592,7 @@ type Nodes { E.g. restricting everything to a specific week: ```text - nodes { filter(expr: {window: {start: 1234, end: 5678}}) { + nodes { filter(expr: {view: [{window: {start: 1234, end: 5678}}]}) { list { neighbours { list { name } } } # neighbours still windowed } } ``` @@ -5266,9 +5614,9 @@ type Nodes { neighbours active on Wednesday: ```text - nodes { select(expr: {window: {...monday...}}) { - list { neighbours { select(expr: {window: {...tuesday...}}) { - list { neighbours { select(expr: {window: {...wednesday...}}) { + nodes { select(expr: {view: [{window: {...monday...}}]}) { + list { neighbours { select(expr: {view: [{window: {...tuesday...}}]}) { + list { neighbours { select(expr: {view: [{window: {...wednesday...}}]}) { list { name } } } } } } } @@ -5309,6 +5657,10 @@ type Nodes { outEdges(select: FilterExpr): NestedEdges! } +""" +A view operation on a node collection, applied in list order; `typeFilter` +keeps the nodes of the given types. +""" input NodesViewCollection @oneOf { """ Contains only the default layer. @@ -5996,7 +6348,7 @@ type PathFromNode { E.g. restricting the whole traversal to a specific week: ```text - node(name: "A") { neighbours { filter(expr: {window: {...week...}}) { + node(name: "A") { neighbours { filter(expr: {view: [{window: {...week...}}]}) { list { neighbours { list { name } } } # further hops still windowed } } } ``` @@ -6017,8 +6369,8 @@ type PathFromNode { Monday, then *their* neighbours active on Tuesday: ```text - node(name: "A") { neighbours { select(expr: {window: {...monday...}}) { - list { neighbours { select(expr: {window: {...tuesday...}}) { + node(name: "A") { neighbours { select(expr: {view: [{window: {...monday...}}]}) { + list { neighbours { select(expr: {view: [{window: {...tuesday...}}]}) { list { name } } } } } } } @@ -7018,8 +7370,7 @@ input VectorisedGraphWindow { } """ -A view operation on a node, an edge or a collection of either; the -same list for all of them now that one filter type serves every entity. +A view operation on a node, an edge or a collection of either. """ input ViewCollection @oneOf { """ @@ -7084,13 +7435,37 @@ input ViewCollection @oneOf { One view restriction, applied in list order. """ input ViewOp @oneOf { + """ + Between `start` (inclusive) and `end` (exclusive). + """ window: Window + """ + At one time. + """ at: TimeInput + """ + Strictly after a time. + """ after: TimeInput + """ + Strictly before a time. + """ before: TimeInput + """ + At the latest time; written `latest: true`. + """ latest: Boolean + """ + Everything up to and including a time; written `snapshotAt: t`. + """ snapshotAt: TimeInput + """ + Everything up to the latest time; written `snapshotLatest: true`. + """ snapshotLatest: Boolean + """ + Only the named layers. + """ layers: [String!] } @@ -7138,4 +7513,3 @@ schema { query: QueryRoot mutation: MutRoot } - diff --git a/raphtory-graphql/src/client/graphql_transport.rs b/raphtory-graphql/src/client/graphql_transport.rs index 03eff0e8e1..da468d9add 100644 --- a/raphtory-graphql/src/client/graphql_transport.rs +++ b/raphtory-graphql/src/client/graphql_transport.rs @@ -4741,7 +4741,7 @@ mod tests { #[test] fn property_key_rides_json_variable_intact() { // A quote-bearing property KEY is carried as JSON data too. - let filter = node_prop("wei\"rd".into(), GqlNodeExpr::Eq, GqlValue::Str("v".into())); + let filter = node_prop("wei\"rd", GqlNodeExpr::Eq, GqlValue::Str("v".into())); let mut vars = VarCollector::default(); vars.add_filter(&filter).unwrap(); let json = serde_json::to_string(&vars.vars["f0"]).unwrap(); @@ -4811,7 +4811,7 @@ mod tests { GqlValue::F64(f64::INFINITY), GqlValue::F32(f32::NEG_INFINITY), ] { - let filter = node_prop("x".into(), GqlNodeExpr::Eq, bad); + let filter = node_prop("x", GqlNodeExpr::Eq, bad); let mut vars = VarCollector::default(); assert!(matches!( vars.add_filter(&filter), @@ -4820,7 +4820,7 @@ mod tests { } // A finite float serializes fine. - let filter = node_prop("x".into(), GqlNodeExpr::Eq, GqlValue::F64(1.5)); + let filter = node_prop("x", GqlNodeExpr::Eq, GqlValue::F64(1.5)); let mut vars = VarCollector::default(); assert!(vars.add_filter(&filter).is_ok()); } @@ -4953,13 +4953,6 @@ mod tests { #[tokio::test] async fn test_filtered_collect_matches_columnar_reads() { use crate::{client::remote_client::RemoteClient, server::GraphServer}; - use raphtory::{ - db::{ - api::storage::storage::Config, - graph::views::filter::model::expr::{CmpOp, Expr, FilterExpr, NodeLeaf}, - }, - prelude::Prop, - }; use reqwest::Url; use std::collections::HashMap as Map; use tempfile::tempdir; @@ -4988,7 +4981,7 @@ mod tests { rg.add_edge(2i64, "b", "c", NO_PROPS, None).await.unwrap(); rg.add_edge(3i64, "c", "a", NO_PROPS, None).await.unwrap(); - let score_gt_15 = node_prop("score".into(), GqlNodeExpr::Gt, GqlValue::I64(15)); + let score_gt_15 = node_prop("score", GqlNodeExpr::Gt, GqlValue::I64(15)); // Membership: filter keeps every node addressable — including `a`, // which fails the filter itself. @@ -5049,18 +5042,7 @@ mod tests { ); // select() narrows membership only — handles see the unfiltered graph. - // Passed as a tree to pin that tree-typed callers satisfy the widened - // `TryInto` bound. - let score_gt_15_tree = FilterExpr::Node(Expr::Cmp( - CmpOp::Gt, - Box::new(Expr::Read(NodeLeaf::Property { - views: Vec::new(), - name: "score".into(), - temporal: false, - })), - Box::new(Expr::Const(Prop::I64(15))), - )); - let selected = rg.nodes().select(score_gt_15_tree).unwrap(); + let selected = rg.nodes().select(tree(score_gt_15.clone())).unwrap(); let mut selected_ids = selected.id().await.unwrap(); selected_ids.sort(); assert_eq!( diff --git a/raphtory-graphql/src/data.rs b/raphtory-graphql/src/data.rs index 6c5ebfabcf..176d74e9a8 100644 --- a/raphtory-graphql/src/data.rs +++ b/raphtory-graphql/src/data.rs @@ -943,10 +943,6 @@ fn apply_row_filter_sync( }) } -/// The graph under a row filter, or the reason the filter cannot be applied to it. -/// -/// The filter means what it means everywhere else: `and` is an intersection, and a -/// predicate that should be evaluated inside a view carries that view on its read. /// Why a grant's row filter cannot be applied to a graph. #[derive(thiserror::Error, Debug)] pub enum AccessFilterError { @@ -958,6 +954,9 @@ pub enum AccessFilterError { Load(GQLError), } +/// The graph under a row filter. The filter means what it means everywhere +/// else: `and` is an intersection, and a predicate that should be evaluated +/// inside a view carries that view on its read. fn compile_row_filter(graph: DynamicGraph, filter: GqlFilter) -> Result { Ok(graph.filter(filter)?.into_dynamic()) } diff --git a/raphtory-graphql/src/model/graph/edge.rs b/raphtory-graphql/src/model/graph/edge.rs index 2f371d2184..4cf0a537ed 100644 --- a/raphtory-graphql/src/model/graph/edge.rs +++ b/raphtory-graphql/src/model/graph/edge.rs @@ -18,7 +18,7 @@ use raphtory::{ core::utils::time::TryIntoInterval, db::{ api::view::{DynamicGraph, EdgeViewOps, Filter, IntoDynamic, StaticGraphViewOps}, - graph::{edge::EdgeView, views::filter::model::DynFilter}, + graph::edge::EdgeView, }, errors::GraphError, prelude::{LayerOps, TimeOps}, @@ -489,8 +489,7 @@ impl GqlEdge { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: DynFilter = expr.try_into()?; - let filtered = self_clone.ee.filter(filter)?; + let filtered = self_clone.ee.filter(expr)?; Ok(self_clone.update(filtered.into_dynamic())) }) .await diff --git a/raphtory-graphql/src/model/graph/edges.rs b/raphtory-graphql/src/model/graph/edges.rs index 89774f9108..8c574d68e6 100644 --- a/raphtory-graphql/src/model/graph/edges.rs +++ b/raphtory-graphql/src/model/graph/edges.rs @@ -20,7 +20,7 @@ use raphtory::{ core::utils::time::TryIntoInterval, db::{ api::view::{DynamicGraph, Filter, Select}, - graph::{edges::Edges, views::filter::model::DynFilter}, + graph::edges::Edges, }, errors::GraphError, prelude::*, @@ -469,7 +469,7 @@ impl GqlEdges { /// E.g. restricting everything to a specific week: /// /// ```text - /// edges { filter(expr: {window: {start: 1234, end: 5678}}) { + /// edges { filter(expr: {view: [{window: {start: 1234, end: 5678}}]}) { /// list { src { neighbours { list { name } } } } # neighbours still windowed /// } } /// ``` @@ -485,8 +485,7 @@ impl GqlEdges { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: DynFilter = expr.try_into()?; - let filtered = self_clone.ee.filter(filter)?; + let filtered = self_clone.ee.filter(expr)?; Ok(self_clone.update(filtered.into_dyn())) }) .await @@ -500,9 +499,9 @@ impl GqlEdges { /// neighbours on Wednesday: /// /// ```text - /// edges { select(expr: {window: {...monday...}}) { - /// list { src { select(expr: {window: {...tuesday...}}) { - /// neighbours { select(expr: {window: {...wednesday...}}) { + /// edges { select(expr: {view: [{window: {...monday...}}]}) { + /// list { src { select(expr: {view: [{window: {...tuesday...}}]}) { + /// neighbours { select(expr: {view: [{window: {...wednesday...}}]}) { /// neighbours { list { name } } /// } } /// } } } diff --git a/raphtory-graphql/src/model/graph/filter_expr_input.rs b/raphtory-graphql/src/model/graph/filter_expr_input.rs index 4d1abdf6a0..eacdcfc263 100644 --- a/raphtory-graphql/src/model/graph/filter_expr_input.rs +++ b/raphtory-graphql/src/model/graph/filter_expr_input.rs @@ -27,12 +27,9 @@ use raphtory::{ view::internal::{DynGraphArc, GraphView}, }, graph::views::filter::{ - model::{ - expr::{ - self, Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, Leaf, NodeLeaf, - StrOp, ViewOp, OPAQUE_FILTER_ERROR, - }, - DynFilter, + model::expr::{ + self, Agg, CmpOp, EdgeLeaf, ExplodedEdgeLeaf, Expr, Field, Leaf, NodeLeaf, StrOp, + ViewOp, OPAQUE_FILTER_ERROR, }, CreateFilter, DynEdgeFilter, }, @@ -53,8 +50,11 @@ use std::{ops::Deref, sync::Arc}; #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[graphql(name = "NodeFieldName")] pub enum GqlNodeField { + /// The node's id. Id, + /// The node's name. Name, + /// The node's type. NodeType, } @@ -63,13 +63,21 @@ pub enum GqlNodeField { #[serde(rename_all = "camelCase")] #[graphql(name = "ViewOp")] pub enum GqlViewOp { + /// Between `start` (inclusive) and `end` (exclusive). Window(Window), + /// At one time. At(GqlTimeInput), + /// Strictly after a time. After(GqlTimeInput), + /// Strictly before a time. Before(GqlTimeInput), + /// At the latest time; written `latest: true`. Latest(bool), + /// Everything up to and including a time; written `snapshotAt: t`. SnapshotAt(GqlTimeInput), + /// Everything up to the latest time; written `snapshotLatest: true`. SnapshotLatest(bool), + /// Only the named layers. Layers(Vec), } @@ -141,7 +149,9 @@ macro_rules! entity_expr_input { #[serde(rename_all = "camelCase")] #[graphql(name = $cmp_name)] pub struct $cmp { + /// The left side. pub lhs: Wrapped<$expr>, + /// The right side. pub rhs: Wrapped<$expr>, } @@ -152,9 +162,13 @@ macro_rules! entity_expr_input { #[serde(rename_all = "camelCase")] #[graphql(name = $fuzzy_name)] pub struct $fuzzy { + /// The string to test. pub lhs: Wrapped<$expr>, + /// The string to match. pub rhs: Wrapped<$expr>, + /// The largest edit distance that still matches. pub levenshtein_distance: usize, + /// Whether a match on a prefix counts. pub prefix_match: bool, } @@ -165,7 +179,9 @@ macro_rules! entity_expr_input { #[serde(rename_all = "camelCase")] #[graphql(name = $membership_name)] pub struct $membership { + /// The value to look for. pub expr: Wrapped<$expr>, + /// The values it may be one of. pub values: Value, } @@ -175,10 +191,14 @@ macro_rules! entity_expr_input { #[serde(rename_all = "camelCase")] #[graphql(name = $viewed_name)] pub struct $viewed { + /// The views, applied in list order. pub views: Vec, + /// The expression read inside them. pub expr: Wrapped<$expr>, } + /// A value or yes/no on one entity: a read, an aggregate over one, a + /// comparison or test, or a combination of yes/nos. #[derive(OneOfInput, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[graphql(name = $expr_name)] @@ -196,38 +216,63 @@ macro_rules! entity_expr_input { $( $(#[$own_meta])* $own($own_ty), )* /// Views applied to every read inside. Viewed(Wrapped<$viewed>), + /// The sum of the innermost list. Sum(Wrapped<$expr>), + /// The mean of the innermost list. Avg(Wrapped<$expr>), + /// The smallest element of the innermost list. Min(Wrapped<$expr>), + /// The largest element of the innermost list. Max(Wrapped<$expr>), + /// The first element of the innermost list. First(Wrapped<$expr>), + /// The last element of the innermost list. Last(Wrapped<$expr>), + /// The number of elements of the innermost list. Len(Wrapped<$expr>), /// The earliest update of a temporal history. Earliest(Wrapped<$expr>), /// The latest update of a temporal history. Latest(Wrapped<$expr>), + /// `lhs == rhs`. Eq($cmp), + /// `lhs != rhs`. Ne($cmp), + /// `lhs < rhs`. Lt($cmp), + /// `lhs <= rhs`. Le($cmp), + /// `lhs > rhs`. Gt($cmp), + /// `lhs >= rhs`. Ge($cmp), + /// The string `lhs` starts with the string `rhs`. StartsWith($cmp), + /// The string `lhs` ends with the string `rhs`. EndsWith($cmp), + /// The string `lhs` contains the string `rhs`. Contains($cmp), + /// The string `lhs` does not contain the string `rhs`. NotContains($cmp), + /// The string `lhs` is within an edit distance of `rhs`. FuzzySearch($fuzzy), + /// `expr` is one of `values`. IsIn($membership), + /// `expr` is none of `values`. IsNotIn($membership), + /// The value is present. IsSome(Wrapped<$expr>), + /// The value is absent. IsNone(Wrapped<$expr>), /// Holds when the element-wise result inside holds for any element. Any(Wrapped<$expr>), /// Holds when the element-wise result inside holds for every element. All(Wrapped<$expr>), + /// Every yes/no inside holds. And(Vec<$expr>), + /// Any yes/no inside holds. Or(Vec<$expr>), + /// The yes/no inside does not hold. Not(Wrapped<$expr>), } @@ -236,25 +281,25 @@ macro_rules! entity_expr_input { fn try_from(e: $expr) -> Result { let inner = |w: Wrapped<$expr>| -> Result>, GraphError> { - Ok(Box::new(Expr::try_from(w.deref().clone())?)) + Ok(Box::new(Expr::try_from(w.into_inner())?)) }; let cmp = |op: CmpOp, c: $cmp| -> Result, GraphError> { Ok(Expr::Cmp( op, - Box::new(Expr::try_from(c.lhs.deref().clone())?), - Box::new(Expr::try_from(c.rhs.deref().clone())?), + Box::new(Expr::try_from(c.lhs.into_inner())?), + Box::new(Expr::try_from(c.rhs.into_inner())?), )) }; let str_op = |op: StrOp, c: $cmp| -> Result, GraphError> { Ok(Expr::Str( op, - Box::new(Expr::try_from(c.lhs.deref().clone())?), - Box::new(Expr::try_from(c.rhs.deref().clone())?), + Box::new(Expr::try_from(c.lhs.into_inner())?), + Box::new(Expr::try_from(c.rhs.into_inner())?), )) }; let members = |m: $membership, negated: bool| -> Result, GraphError> { Ok(Expr::In { - expr: Box::new(Expr::try_from(m.expr.deref().clone())?), + expr: Box::new(Expr::try_from(m.expr.into_inner())?), values: member_values(m.values, negated)?, negated, }) @@ -276,8 +321,8 @@ macro_rules! entity_expr_input { convert(v)? } )* $expr::Viewed(v) => { - let v = v.deref().clone(); - let mut expr = Expr::try_from(v.expr.deref().clone())?; + let v = v.into_inner(); + let mut expr = Expr::try_from(v.expr.into_inner())?; for op in view_ops(Some(v.views))? { expr.push_view(op); } @@ -307,8 +352,8 @@ macro_rules! entity_expr_input { levenshtein_distance: f.levenshtein_distance, prefix_match: f.prefix_match, }, - Box::new(Expr::try_from(f.lhs.deref().clone())?), - Box::new(Expr::try_from(f.rhs.deref().clone())?), + Box::new(Expr::try_from(f.lhs.into_inner())?), + Box::new(Expr::try_from(f.rhs.into_inner())?), ), $expr::IsIn(m) => members(m, false)?, $expr::IsNotIn(m) => members(m, true)?, @@ -481,11 +526,11 @@ entity_expr_input! { }, /// A node expression evaluated on the edge's source node. Src(Wrapped) => |e: Wrapped| { - Ok(Expr::Read(EdgeLeaf::Src(Box::new(Expr::try_from(e.deref().clone())?)))) + Ok(Expr::Read(EdgeLeaf::Src(Box::new(Expr::try_from(e.into_inner())?)))) }, /// A node expression evaluated on the edge's destination node. Dst(Wrapped) => |e: Wrapped| { - Ok(Expr::Read(EdgeLeaf::Dst(Box::new(Expr::try_from(e.deref().clone())?)))) + Ok(Expr::Read(EdgeLeaf::Dst(Box::new(Expr::try_from(e.into_inner())?)))) }, } } @@ -606,8 +651,11 @@ impl GqlExplodedEdgeExpr { #[serde(rename_all = "camelCase")] #[graphql(name = "FilterExpr")] pub enum GqlFilter { + /// A yes/no over nodes. Node(GqlNodeExpr), + /// A yes/no over edges. Edge(GqlEdgeExpr), + /// A yes/no over exploded edges, one per update of an edge. ExplodedEdge(GqlExplodedEdgeExpr), /// A graph-level view with no predicate: the result is the view. View(Vec), @@ -733,7 +781,7 @@ impl TryFrom for expr::FilterExpr { .map(F::try_from) .collect::, _>>()?, ), - GqlFilter::Not(inner) => F::Not(Box::new(inner.deref().clone().try_into()?)), + GqlFilter::Not(inner) => F::Not(Box::new(inner.into_inner().try_into()?)), }) } } @@ -775,15 +823,6 @@ impl TryFrom for GqlFilter { } } -/// The compiled filter, for callers that apply one filter to several handles. -impl TryFrom for DynFilter { - type Error = GraphError; - - fn try_from(value: GqlFilter) -> Result { - expr::FilterExpr::try_from(value)?.compile() - } -} - impl CreateFilter for GqlFilter { type FilteredGraph<'graph, G> = DynGraphArc<'graph> diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 55958d4058..3d6bd00ac4 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -28,6 +28,8 @@ pub struct Window { pub end: GqlTimeInput, } +/// A view operation on a graph, applied in list order; graph-level ops such as +/// `subgraph` and `valid` live here only. #[derive(OneOfInput, Clone, Debug)] pub enum GraphViewCollection { /// Contains only the default layer. @@ -68,6 +70,8 @@ pub enum GraphViewCollection { Filter(GqlFilter), } +/// A view operation on a node collection, applied in list order; `typeFilter` +/// keeps the nodes of the given types. #[derive(OneOfInput, Clone, Debug)] pub enum NodesViewCollection { /// Contains only the default layer. @@ -102,8 +106,7 @@ pub enum NodesViewCollection { TypeFilter(Vec), } -/// A view operation on a node, an edge or a collection of either; the -/// same list for all of them now that one filter type serves every entity. +/// A view operation on a node, an edge or a collection of either. #[derive(OneOfInput, Clone, Debug)] pub enum ViewCollection { /// Contains only the default layer. @@ -274,6 +277,12 @@ impl From for Wrapped { } } +impl Wrapped { + pub fn into_inner(self) -> T { + *self.0 + } +} + impl Deref for Wrapped { type Target = T; fn deref(&self) -> &Self::Target { diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index bdef0bf007..d9468735b8 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -34,17 +34,14 @@ use raphtory::{ filter_ops::Select, DynamicGraph, Filter, IntoDynamic, NodeViewOps, StaticGraphViewOps, TimeOps, }, - graph::{node::NodeView, views::filter::model::DynFilter}, + graph::node::NodeView, }, errors::GraphError, prelude::*, }; use raphtory_api::core::{storage::timeindex::AsTime, utils::time::IntoTime}; use raphtory_storage::core_ops::CoreGraphOps; -use std::{ - collections::HashSet, - convert::{Into, TryInto}, -}; +use std::{collections::HashSet, convert::Into}; /// A view of a Raphtory graph. Every field here returns either data from the /// view or a derived view (`window`, `layer`, `at`, `filter`, ...) that you can @@ -543,12 +540,7 @@ impl GqlGraph { let nn = self.graph.nodes(); if let Some(sel) = select { - let nf = sel; - let narrowed = blocking_compute({ - let nn_clone = nn.clone(); - move || nn_clone.select(nf) - }) - .await?; + let narrowed = blocking_compute(move || nn.select(sel)).await?; return Ok(GqlNodes::new(narrowed)); } @@ -726,8 +718,7 @@ impl GqlGraph { let Some(expr) = expr else { return Ok(self_clone.clone()); }; - let filter: DynFilter = expr.try_into()?; - let filtered_graph = self_clone.graph.filter(filter)?; + let filtered_graph = self_clone.graph.filter(expr)?; Ok(GqlGraph::new( self_clone.path.clone(), filtered_graph.into_dynamic(), diff --git a/raphtory-graphql/src/model/graph/namespace_filtering.rs b/raphtory-graphql/src/model/graph/namespace_filtering.rs index a6efb1d539..c429a0fab4 100644 --- a/raphtory-graphql/src/model/graph/namespace_filtering.rs +++ b/raphtory-graphql/src/model/graph/namespace_filtering.rs @@ -7,8 +7,8 @@ //! //! Graphs are described entirely by their metadata, and which keys matter is up //! to whoever wrote the graph. So rather than a fixed set of filterable and -//! sortable columns, both filter and sort address metadata by key: filters reuse -//! the same `PropCondition` grammar as graph property filters, and sorts either +//! sortable columns, both filter and sort address metadata by key: filters use +//! the `PropCondition` grammar over one value, and sorts either //! name a built-in field or a metadata key, with an optional explicit value order //! for keys holding a small vocabulary (`"critical"`, `"high"`, ...) whose //! natural ordering is not alphabetical. @@ -82,8 +82,7 @@ pub struct MetaGraphCondition { pub field: Option, /// Metadata key to test. pub metadata_key: Option, - /// Condition applied to the value, using the same grammar as property - /// filters elsewhere in the schema. Names, paths and metadata strings are + /// Condition applied to the value. Names, paths and metadata strings are /// tested as strings; counts and timestamps as integers. pub where_: PropCondition, /// Overrides the result when the graph has no value for the target. diff --git a/raphtory-graphql/src/model/graph/nested_edges.rs b/raphtory-graphql/src/model/graph/nested_edges.rs index 97e8b89313..5e17c0c7a0 100644 --- a/raphtory-graphql/src/model/graph/nested_edges.rs +++ b/raphtory-graphql/src/model/graph/nested_edges.rs @@ -14,7 +14,7 @@ use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use raphtory::{ db::{ api::view::{filter_ops::Select, DynamicGraph, Filter}, - graph::{edges::NestedEdges, views::filter::model::DynFilter}, + graph::edges::NestedEdges, }, errors::GraphError, prelude::*, @@ -402,8 +402,7 @@ impl GqlNestedEdges { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: DynFilter = expr.try_into()?; - let filtered = self_clone.edges.filter(filter)?; + let filtered = self_clone.edges.filter(expr)?; Ok(self_clone.update(filtered.into_dyn())) }) .await diff --git a/raphtory-graphql/src/model/graph/node.rs b/raphtory-graphql/src/model/graph/node.rs index 20edf3633b..4f7418095d 100644 --- a/raphtory-graphql/src/model/graph/node.rs +++ b/raphtory-graphql/src/model/graph/node.rs @@ -25,7 +25,7 @@ use raphtory::{ properties::dyn_props::DynProperties, view::{filter_ops::Select, Filter, *}, }, - graph::{node::NodeView, views::filter::model::DynFilter}, + graph::node::NodeView, }, errors::GraphError, prelude::NodeStateOps, @@ -400,17 +400,16 @@ impl GqlNode { blocking_compute(move || self_clone.vv.in_degree()).await } - /// Nodes that can reach this one via out-edges. `select` is a general filter expression — a node - /// filter, an edge filter, or a graph (layer/window) filter — scoping which nodes/edges the walk + /// Nodes that can reach this one via out-edges. `select` is a filter expression — node and edge + /// predicates, views, and their `and`/`or`/`not` combinations — scoping which nodes/edges the walk /// steps through. The returned nodes are on the full graph so their other-layer neighbours stay /// queryable. pub async fn in_component(&self, select: Option) -> Result { let self_clone = self.clone(); match select { Some(select) => { - let filter: DynFilter = select.try_into()?; blocking_compute(move || { - in_component_filtered(self_clone.vv.clone(), filter) + in_component_filtered(self_clone.vv.clone(), select) .map(|state| GqlNodes::new(state.nodes())) }) .await @@ -422,17 +421,16 @@ impl GqlNode { } } - /// Nodes reachable from this one via out-edges. `select` is a general filter expression — a node - /// filter, an edge filter, or a graph (layer/window) filter — scoping which nodes/edges the walk + /// Nodes reachable from this one via out-edges. `select` is a filter expression — node and edge + /// predicates, views, and their `and`/`or`/`not` combinations — scoping which nodes/edges the walk /// steps through. The returned nodes are on the full (unfiltered) graph, so their other-layer /// neighbours remain queryable. pub async fn out_component(&self, select: Option) -> Result { let self_clone = self.clone(); match select { Some(select) => { - let filter: DynFilter = select.try_into()?; blocking_compute(move || { - out_component_filtered(self_clone.vv.clone(), filter) + out_component_filtered(self_clone.vv.clone(), select) .map(|state| GqlNodes::new(state.nodes())) }) .await @@ -522,8 +520,7 @@ impl GqlNode { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: DynFilter = expr.try_into()?; - let filtered = self_clone.vv.filter(filter)?; + let filtered = self_clone.vv.filter(expr)?; Ok(self_clone.update(filtered.into_dynamic())) }) .await diff --git a/raphtory-graphql/src/model/graph/nodes.rs b/raphtory-graphql/src/model/graph/nodes.rs index bc03cc64ca..b3dd51993f 100644 --- a/raphtory-graphql/src/model/graph/nodes.rs +++ b/raphtory-graphql/src/model/graph/nodes.rs @@ -24,10 +24,7 @@ use raphtory::{ state::ops::DynNodeFilter, view::{filter_ops::Select, DynamicGraph, Filter}, }, - graph::{ - nodes::{IntoDynNodes, Nodes}, - views::filter::model::DynFilter, - }, + graph::nodes::{IntoDynNodes, Nodes}, }, errors::GraphError, prelude::*, @@ -307,7 +304,7 @@ impl GqlNodes { NodesViewCollection::After(time) => return_view.after(time).await, NodesViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, NodesViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - NodesViewCollection::Filter(node_filter) => return_view.filter(node_filter).await?, + NodesViewCollection::Filter(filter) => return_view.filter(filter).await?, NodesViewCollection::TypeFilter(types) => return_view.type_filter(types).await, } } @@ -506,7 +503,7 @@ impl GqlNodes { /// E.g. restricting everything to a specific week: /// /// ```text - /// nodes { filter(expr: {window: {start: 1234, end: 5678}}) { + /// nodes { filter(expr: {view: [{window: {start: 1234, end: 5678}}]}) { /// list { neighbours { list { name } } } # neighbours still windowed /// } } /// ``` @@ -522,8 +519,7 @@ impl GqlNodes { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: DynFilter = expr.try_into()?; - let filtered = self_clone.nn.filter(filter)?; + let filtered = self_clone.nn.filter(expr)?; Ok(self_clone.update(filtered.into_dyn())) }) .await @@ -537,9 +533,9 @@ impl GqlNodes { /// neighbours active on Wednesday: /// /// ```text - /// nodes { select(expr: {window: {...monday...}}) { - /// list { neighbours { select(expr: {window: {...tuesday...}}) { - /// list { neighbours { select(expr: {window: {...wednesday...}}) { + /// nodes { select(expr: {view: [{window: {...monday...}}]}) { + /// list { neighbours { select(expr: {view: [{window: {...tuesday...}}]}) { + /// list { neighbours { select(expr: {view: [{window: {...wednesday...}}]}) { /// list { name } /// } } } /// } } } diff --git a/raphtory-graphql/src/model/graph/path_from_graph.rs b/raphtory-graphql/src/model/graph/path_from_graph.rs index a4025095ff..677d1331bc 100644 --- a/raphtory-graphql/src/model/graph/path_from_graph.rs +++ b/raphtory-graphql/src/model/graph/path_from_graph.rs @@ -15,7 +15,7 @@ use dynamic_graphql::{ResolvedObject, ResolvedObjectFields, Scalar, ScalarValue} use raphtory::{ db::{ api::view::{filter_ops::Select, DynamicGraph, Filter}, - graph::{path::PathFromGraph, views::filter::model::DynFilter}, + graph::path::PathFromGraph, }, errors::GraphError, prelude::*, @@ -510,8 +510,7 @@ impl GqlPathFromGraph { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: DynFilter = expr.try_into()?; - let filtered = self_clone.nn.filter(filter)?; + let filtered = self_clone.nn.filter(expr)?; Ok(self_clone.update(filtered.into_dyn())) }) .await diff --git a/raphtory-graphql/src/model/graph/path_from_node.rs b/raphtory-graphql/src/model/graph/path_from_node.rs index 291523f1ec..9ae7f10706 100644 --- a/raphtory-graphql/src/model/graph/path_from_node.rs +++ b/raphtory-graphql/src/model/graph/path_from_node.rs @@ -18,7 +18,7 @@ use raphtory::{ core::utils::time::TryIntoInterval, db::{ api::view::{filter_ops::Select, DynamicGraph, Filter}, - graph::{path::PathFromNode, views::filter::model::DynFilter}, + graph::path::PathFromNode, }, errors::GraphError, prelude::*, @@ -469,7 +469,7 @@ impl GqlPathFromNode { /// E.g. restricting the whole traversal to a specific week: /// /// ```text - /// node(name: "A") { neighbours { filter(expr: {window: {...week...}}) { + /// node(name: "A") { neighbours { filter(expr: {view: [{window: {...week...}}]}) { /// list { neighbours { list { name } } } # further hops still windowed /// } } } /// ``` @@ -485,8 +485,7 @@ impl GqlPathFromNode { ) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: DynFilter = expr.try_into()?; - let filtered = self_clone.nn.filter(filter)?; + let filtered = self_clone.nn.filter(expr)?; Ok(self_clone.update(filtered.into_dyn())) }) .await @@ -499,8 +498,8 @@ impl GqlPathFromNode { /// Monday, then *their* neighbours active on Tuesday: /// /// ```text - /// node(name: "A") { neighbours { select(expr: {window: {...monday...}}) { - /// list { neighbours { select(expr: {window: {...tuesday...}}) { + /// node(name: "A") { neighbours { select(expr: {view: [{window: {...monday...}}]}) { + /// list { neighbours { select(expr: {view: [{window: {...tuesday...}}]}) { /// list { name } /// } } } /// } } } From e6f313ac2ab4345bb00cd0783d3a8bc63cfdb93a Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:52:01 +0100 Subject: [PATCH 148/155] drop the filter engine's dead paths and settle list nesting once Three filtered-graph types, four error variants and the node-id and view-presence probes had no callers left; they are gone. A list aggregation decides whether its updates are lists from the first element instead of unifying every element's type per value. A node's type is never absent, so presence tests on it are refused. An exploded edge reference without a layer is a bug, not a whole-edge read. Display implementations write to the formatter, and an invalid operator is named in words in the error. --- raphtory/src/db/graph/edge_reads.rs | 3 +- raphtory/src/db/graph/views/filter/mod.rs | 3 -- .../graph/views/filter/model/expr/compile.rs | 44 --------------- .../graph/views/filter/model/expr/display.rs | 54 ++++++++++--------- .../db/graph/views/filter/model/expr/mod.rs | 22 -------- .../src/db/graph/views/filter/model/mod.rs | 3 +- .../views/filter/model/node_expr/dyn_expr.rs | 10 ---- .../views/filter/model/node_expr/exprs.rs | 13 ++--- .../graph/views/filter/model/node_expr/mod.rs | 7 --- .../filter/model/property_filter/evaluate.rs | 13 ++--- raphtory/src/errors.rs | 12 ----- 11 files changed, 47 insertions(+), 137 deletions(-) diff --git a/raphtory/src/db/graph/edge_reads.rs b/raphtory/src/db/graph/edge_reads.rs index e31503c50f..13f298993b 100644 --- a/raphtory/src/db/graph/edge_reads.rs +++ b/raphtory/src/db/graph/edge_reads.rs @@ -34,7 +34,8 @@ impl EdgeAt { match (edge.layer(), edge.time()) { (Some(layer), Some(t)) => EdgeAt::Exploded(layer, t), (Some(layer), None) => EdgeAt::Layer(layer), - (None, _) => EdgeAt::Whole, + (None, None) => EdgeAt::Whole, + (None, Some(_)) => unreachable!("an exploded edge reference carries its layer"), } } diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index a54992c982..899d91e3a5 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -15,10 +15,7 @@ use std::sync::Arc; pub mod and_filtered_graph; pub mod edge_expr_filtered_graph; -pub mod edge_node_filtered_graph; mod exploded_edge_expr_filtered_graph; -pub mod exploded_edge_filtered_graph; -pub mod exploded_edge_node_filtered_graph; pub mod model; pub mod node_filtered_graph; pub mod or_filtered_graph; diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index 40a7ab44fb..233ab32460 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -98,9 +98,6 @@ pub trait Leaf: Clone + Debug + PartialEq + Send + Sync + 'static { /// property; a consumer that streams it need not build the list. fn compile_history(&self) -> Option>; - /// Whether the read is scoped by a view. - fn has_view(&self) -> bool; - /// Whether the read is the history of a temporal property. fn is_temporal(&self) -> bool; @@ -238,10 +235,6 @@ impl Leaf for NodeLeaf { } } - fn has_view(&self) -> bool { - !self.views().is_empty() - } - fn is_temporal(&self) -> bool { matches!(self, NodeLeaf::Property { temporal: true, .. }) } @@ -272,16 +265,6 @@ impl Leaf for NodeLeaf { } impl NodeLeaf { - fn views(&self) -> &[ViewOp] { - match self { - NodeLeaf::Field { views, .. } - | NodeLeaf::Degree { views, .. } - | NodeLeaf::Property { views, .. } - | NodeLeaf::Metadata { views, .. } - | NodeLeaf::IsActive { views } => views, - } - } - fn views_mut(&mut self) -> &mut Vec { match self { NodeLeaf::Field { views, .. } @@ -364,18 +347,6 @@ impl Leaf for EdgeLeaf { } } - fn has_view(&self) -> bool { - match self { - EdgeLeaf::Property { views, .. } - | EdgeLeaf::Metadata { views, .. } - | EdgeLeaf::IsActive { views } - | EdgeLeaf::IsValid { views } - | EdgeLeaf::IsDeleted { views } - | EdgeLeaf::IsSelfLoop { views } => !views.is_empty(), - EdgeLeaf::Src(inner) | EdgeLeaf::Dst(inner) => inner.has_view(), - } - } - fn is_temporal(&self) -> bool { match self { EdgeLeaf::Property { temporal, .. } => *temporal, @@ -471,10 +442,6 @@ impl Leaf for ExplodedEdgeLeaf { } } - fn has_view(&self) -> bool { - !self.views().is_empty() - } - fn is_temporal(&self) -> bool { matches!(self, ExplodedEdgeLeaf::Property { temporal: true, .. }) } @@ -505,17 +472,6 @@ impl Leaf for ExplodedEdgeLeaf { } impl ExplodedEdgeLeaf { - fn views(&self) -> &[ViewOp] { - match self { - ExplodedEdgeLeaf::Property { views, .. } - | ExplodedEdgeLeaf::Metadata { views, .. } - | ExplodedEdgeLeaf::IsActive { views } - | ExplodedEdgeLeaf::IsValid { views } - | ExplodedEdgeLeaf::IsDeleted { views } - | ExplodedEdgeLeaf::IsSelfLoop { views } => views, - } - } - fn views_mut(&mut self) -> &mut Vec { match self { ExplodedEdgeLeaf::Property { views, .. } diff --git a/raphtory/src/db/graph/views/filter/model/expr/display.rs b/raphtory/src/db/graph/views/filter/model/expr/display.rs index 6625c8de14..24398e4b68 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/display.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/display.rs @@ -28,12 +28,13 @@ fn views(f: &mut fmt::Formatter<'_>, views: &[ViewOp], inner: &dyn Display) -> f if views.is_empty() { return write!(f, "{inner}"); } - let chain = views - .iter() - .map(ToString::to_string) - .collect::>() - .join(" . "); - write!(f, "{chain}({inner})") + for (i, view) in views.iter().enumerate() { + if i > 0 { + f.write_str(" . ")?; + } + write!(f, "{view}")?; + } + write!(f, "({inner})") } fn property(f: &mut fmt::Formatter<'_>, v: &[ViewOp], name: &str, temporal: bool) -> fmt::Result { @@ -163,13 +164,15 @@ impl Display for Expr { values, negated, } => { - let items = values - .iter() - .map(ToString::to_string) - .collect::>() - .join(", "); let name = if *negated { "NOT IN" } else { "IN" }; - write!(f, "{expr} {name} [{items}]") + write!(f, "{expr} {name} [")?; + for (i, value) in values.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; + } + write!(f, "{value}")?; + } + f.write_str("]") } Expr::IsSome(e) => write!(f, "IS_SOME({e})"), Expr::IsNone(e) => write!(f, "IS_NONE({e})"), @@ -183,12 +186,13 @@ impl Display for Expr { } fn joined(f: &mut fmt::Formatter<'_>, items: &[T], sep: &str) -> fmt::Result { - let parts = items - .iter() - .map(|i| format!("({i})")) - .collect::>() - .join(sep); - write!(f, "{parts}") + for (i, item) in items.iter().enumerate() { + if i > 0 { + f.write_str(sep)?; + } + write!(f, "({item})")?; + } + Ok(()) } impl Display for FilterExpr { @@ -198,12 +202,14 @@ impl Display for FilterExpr { FilterExpr::Edge(e) => write!(f, "EDGE({e})"), FilterExpr::ExplodedEdge(e) => write!(f, "EXPLODED_EDGE({e})"), FilterExpr::View(ops) => { - let chain = ops - .iter() - .map(ToString::to_string) - .collect::>() - .join(" . "); - write!(f, "VIEW({chain})") + f.write_str("VIEW(")?; + for (i, op) in ops.iter().enumerate() { + if i > 0 { + f.write_str(" . ")?; + } + write!(f, "{op}")?; + } + f.write_str(")") } FilterExpr::And(items) => joined(f, items, " AND "), FilterExpr::Or(items) => joined(f, items, " OR "), diff --git a/raphtory/src/db/graph/views/filter/model/expr/mod.rs b/raphtory/src/db/graph/views/filter/model/expr/mod.rs index bec1083dde..6021fe430e 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/mod.rs @@ -303,28 +303,6 @@ pub enum FilterExpr { Opaque(OpaqueFilter), } -impl Expr { - /// Whether a view appears in any read of this expression. - pub fn has_view(&self) -> bool - where - L: Leaf, - { - match self { - Expr::Const(_) => false, - Expr::Read(leaf) => leaf.has_view(), - Expr::Agg(_, e) - | Expr::IsSome(e) - | Expr::IsNone(e) - | Expr::Any(e) - | Expr::All(e) - | Expr::Not(e) => e.has_view(), - Expr::In { expr, .. } => expr.has_view(), - Expr::Cmp(_, l, r) | Expr::Str(_, l, r) => l.has_view() || r.has_view(), - Expr::And(items) | Expr::Or(items) => items.iter().any(Self::has_view), - } - } -} - impl FilterExpr { /// Whether a graph-level view appears anywhere in this filter. pub fn has_view(&self) -> bool { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index e8ca542a03..be62bcba24 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -868,8 +868,7 @@ pub fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), Gra other => other.to_string(), }; return Err(GraphError::InvalidFilter(format!( - "operator {:?} is not valid for {} properties", - op, kind + "operator {op} is not valid for {kind} properties" ))); } Ok(()) diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 1bca3ea7ce..01206c353e 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -98,8 +98,6 @@ impl DynTemporal for EdgeEndpointWrapper> { } pub trait DynCreateOp: DynEntityExpr { - fn dyn_selects_node_id(&self) -> bool; - fn dyn_create_node_op<'g>( &self, graph: Arc, @@ -112,10 +110,6 @@ pub trait DynCreateOp: DynEntityExpr { } impl DynCreateOp for E { - fn dyn_selects_node_id(&self) -> bool { - self.selects_node_id() - } - fn dyn_create_node_op<'g>( &self, graph: Arc, @@ -148,10 +142,6 @@ impl EntityExpr for Arc { } impl CreateOp for Arc { - fn selects_node_id(&self) -> bool { - self.as_ref().dyn_selects_node_id() - } - fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, 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 0fe1be936a..7b0fa18535 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 @@ -11,7 +11,7 @@ //! ```rust,ignore //! NodeFilter.id() // Id — e.g. .eq(GID::Str("v1".into())) //! NodeFilter.name() // Name — e.g. .eq("Alice") -//! NodeFilter.node_type() // Type — e.g. .is_some() +//! NodeFilter.node_type() // Type — e.g. .eq("user") //! ``` //! //! # Degree expressions @@ -102,7 +102,7 @@ use std::sync::Arc; // All map their native types into Option via into_prop(): // NodeFilter.id() uses Id — produces Option (GID mapped to Prop) // NodeFilter.name() uses Name — produces Option (String as Prop::Str) -// NodeFilter.node_type() uses Type — produces Option (ArcStr as Prop::Str, None if unset) +// NodeFilter.node_type() uses Type — produces Option (ArcStr as Prop::Str, "_default" if unset) // ───────────────────────────────────────────────────────────────────────────── #[derive(Copy, Clone, Debug, Default)] @@ -125,10 +125,6 @@ impl EntityExpr for Id { impl PredicateLhs for Id {} impl CreateOp for Id { - fn selects_node_id(&self) -> bool { - true - } - fn create_node_op<'g, G: GraphView + 'g>( &self, graph: G, @@ -186,6 +182,11 @@ impl EntityExpr for Type { NodeFilter } + /// Every node has a type; an unset one reads as `"_default"`. + fn nullable(&self) -> bool { + false + } + fn prop_type(&self) -> PropType { PropType::Str } 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 index cd2f874f60..35ac927758 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -45,13 +45,6 @@ pub use ops::*; /// ``` /// pub trait CreateOp: EntityExpr + Clone + Send + Sync + 'static { - /// Whether this expression selects the node id field. Comparisons against - /// constants use it to narrow the evaluation domain to the named nodes - /// instead of scanning every node. - fn selects_node_id(&self) -> bool { - false - } - /// Compile the expression against a specific graph view. /// /// Any name→ID resolution (property, metadata) happens here, once. diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index 5a3f6cc413..f97c2b940b 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -412,17 +412,18 @@ pub fn aggregate_list_values( op: &dyn Fn(Box + '_>) -> Option, ) -> Option { match vals? { - Prop::List(x) => match x.dtype() { - PropType::List(_) => { + Prop::List(x) => { + let nested = matches!(x.iter_all().next(), Some(Some(Prop::List(_)))); + if nested { let s = x .iter_all() - .map(|y| aggregate_list_values(y, op)) - .flatten() + .filter_map(|y| aggregate_list_values(y, op)) .collect(); Some(Prop::List(s)) + } else { + op(Box::new(x.iter())) } - _ => op(Box::new(x.iter())), - }, + } _ => None, } } diff --git a/raphtory/src/errors.rs b/raphtory/src/errors.rs index 8a9fb8805b..c8f81ade74 100644 --- a/raphtory/src/errors.rs +++ b/raphtory/src/errors.rs @@ -329,9 +329,6 @@ pub enum GraphError { #[error("System time error: {0}")] SystemTimeError(#[from] SystemTimeError), - #[error("Property filtering not implemented on PersistentGraph yet")] - PropertyFilteringNotImplemented, - #[error("Expected a {0} for {1} operator")] ExpectedValueForOperator(String, String), @@ -389,15 +386,6 @@ pub enum GraphError { #[error("Value cannot be empty.")] EmptyValue, - #[error("Filter must contain at least one filter condition.")] - ParsingError, - - #[error("Node filter is not supported for edge filtering")] - NodeFilterIsNotEdgeFilter, - - #[error("Only property filters are supported for exploded edge filtering")] - NotExplodedEdgeFilter, - #[error("Your window and step must be of the same type: duration (string) or epoch (int)")] MismatchedIntervalTypes, From 1eb975aca7226d222c92c7af79ec23638939fb56 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:52:02 +0100 Subject: [PATCH 149/155] rebuild every filter repr and import only what the stubs use A negated expression brackets itself when it is the receiver of a method call, and a list constant prints as a list literal, so `eval` of a repr rebuilds the same filter. Reflected operators and conversions the tree already provides are gone. Docstrings name the property read, the time input type and the layer view as they behave, and the generated filter stub imports the three names it references. --- docs/user-guide/views/6_filtering.md | 6 +- python/python/raphtory/filter/__init__.pyi | 302 +++++++----------- raphtory/src/python/filter/edge_expr.rs | 28 +- .../src/python/filter/exploded_edge_expr.rs | 32 +- raphtory/src/python/filter/filter_expr.rs | 13 +- raphtory/src/python/filter/graph_filter.rs | 28 +- raphtory/src/python/filter/mod.rs | 7 - raphtory/src/python/filter/node_expr.rs | 68 ++-- raphtory/src/python/filter/repr.rs | 60 +++- stub_gen/stub_gen.py | 22 +- 10 files changed, 261 insertions(+), 305 deletions(-) diff --git a/docs/user-guide/views/6_filtering.md b/docs/user-guide/views/6_filtering.md index 27f1b1d832..de89c4af78 100644 --- a/docs/user-guide/views/6_filtering.md +++ b/docs/user-guide/views/6_filtering.md @@ -55,7 +55,7 @@ their own: `.is_valid()`, `.is_deleted()`, `.is_active()`, `.is_self_loop()`. ## How you compare -A read is an [Expr][raphtory.filter.Expr]. Comparing it gives a `FilterExpr`. +A read is an [Expr][raphtory.filter.Expr]. Comparing it gives a yes/no `Expr`, which is accepted anywhere a filter is. | compare with | meaning | |---|---| @@ -77,8 +77,8 @@ assert sorted(n.name for n in g.filter(high).nodes) == ["alice", "bob"] missing = filter.Node.property("score").is_none() assert [n.name for n in g.filter(missing).nodes] == ["carol"] -more_out_than_in = filter.Node.degree() > filter.Node.in_degree() -assert sorted(n.name for n in g.filter(more_out_than_in).nodes) == ["alice", "bob"] +has_an_out_neighbour = filter.Node.degree() > filter.Node.in_degree() +assert sorted(n.name for n in g.filter(has_an_out_neighbour).nodes) == ["alice", "bob"] ``` /// diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index b8dbd929f1..e20879490a 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -10,49 +10,7 @@ from __future__ import annotations ############################################################################### from typing import * -from raphtory import ( - GraphView, - PersistentGraph, - _GraphEncoder, - Nodes, - PathFromNode, - PathFromGraph, - MutableNode, - Edges, - NestedEdges, - MutableEdge, - SortByTime, - NodeSortBy, - EdgeSortBy, - Properties, - PyPropValueList, - PropType, - Metadata, - MetadataView, - TemporalProperties, - PropertiesView, - TemporalProperty, - EventTime, - OptionalEventTime, - History, - HistoryTimestamp, - HistoryDateTime, - HistoryEventId, - Intervals, - WindowSet, - Prop, - version, - graphql, - algorithms, - graph_loader, - graph_gen, - vectors, - node_state, - filter, - iterables, - nullmodels, - plottingutils, -) +from raphtory import Metadata, Prop, filter from raphtory.algorithms import * from raphtory.vectors import * from raphtory.node_state import * @@ -71,27 +29,13 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = [ - "FilterExpr", - "Expr", - "PropertyExpr", - "Node", - "NodeFilter", - "Edge", - "EdgeFilter", - "EdgeEndpoint", - "ExplodedEdge", - "ExplodedEdgeFilter", - "Graph", - "GraphFilter", -] - -class FilterExpr(object): +__all__ = ['FilterExpr', 'Expr', 'PropertyExpr', 'Node', 'NodeFilter', 'Edge', 'EdgeFilter', 'EdgeEndpoint', 'ExplodedEdge', 'ExplodedEdgeFilter', 'Graph', 'GraphFilter'] +class FilterExpr(object): """ A filter as a tree. The same tree runs locally, is sent to a server, and is what `repr` prints, so there is nothing to keep in step. - Anywhere a filter is expected, a yes/no [`Expr`] is accepted too: it is the + Anywhere a filter is expected, a yes/no `Expr` is accepted too: it is the filter on its own entity. `&`, `|` and `~` combine filters. `~` keeps what the filter drops: a negated @@ -118,11 +62,11 @@ class FilterExpr(object): def __ror__(self, value): """Return value|self.""" -class Expr(object): +class Expr(object): """ A value expression: a field, degree, property, metadata entry, an aggregate over one, or a yes/no built from them. Comparing it to a value or to another - expression gives a yes/no [`Expr`], which is a filter on its entity. + expression gives a yes/no `Expr`, which is a filter on its entity. `~` on a yes/no is the opposite yes/no: a node without the property fails `property("score") > 4`, so it passes `~(property("score") > 4)`. @@ -245,9 +189,7 @@ class Expr(object): filter.Expr: """ - def fuzzy_search( - self, other: str | filter.Expr, levenshtein_distance: int, prefix_match: bool - ) -> filter.Expr: + def fuzzy_search(self, other: str | filter.Expr, levenshtein_distance: int, prefix_match: bool) -> filter.Expr: """ Checks whether the string value is within a Levenshtein distance of the given text. @@ -426,7 +368,7 @@ class Expr(object): filter.Expr: """ -class PropertyExpr(Expr): +class PropertyExpr(Expr): """A property read, which can switch to the property's history with `temporal()`.""" def __repr__(self): @@ -434,54 +376,54 @@ class PropertyExpr(Expr): def temporal(self) -> filter.Expr: """ - Switches from the property's latest value to its full temporal history, - unlocking the aggregate chain (`sum`, `avg`, `min`, `max`, ...) and the - element-wise comparisons `any()` / `all()` collapse. + Switches from the property's latest value to its full history, a list + that the aggregates (`sum`, `avg`, `min`, `max`, ...) reduce and that a + comparison tests element by element, for `any()` / `all()` to collapse. Returns: filter.Expr: """ -class Node(object): +class Node(object): """ Entry point for constructing node filter expressions. Every method is static: `Node.property("age") > 30` selects nodes directly, and the view methods (`window`, `latest`, `layer`, ...) return a - [`NodeFilter`] scoped to that view for further chaining. + `NodeFilter` scoped to that view for further chaining. """ @staticmethod - def after(time: int) -> filter.NodeFilter: + def after(time: TimeInput) -> filter.NodeFilter: """ Restricts node evaluation to times strictly after the given time. Arguments: - time (int): Lower time bound. + time (TimeInput): Lower time bound. Returns: filter.NodeFilter: """ @staticmethod - def at(time: int) -> filter.NodeFilter: + def at(time: TimeInput) -> filter.NodeFilter: """ Restricts node evaluation to a single point in time. Arguments: - time (int): Event time. + time (TimeInput): Event time. Returns: filter.NodeFilter: """ @staticmethod - def before(time: int) -> filter.NodeFilter: + def before(time: TimeInput) -> filter.NodeFilter: """ Restricts node evaluation to times strictly before the given time. Arguments: - time (int): Upper time bound. + time (TimeInput): Upper time bound. Returns: filter.NodeFilter: @@ -548,7 +490,7 @@ class Node(object): @staticmethod def layer(layer: str) -> filter.NodeFilter: """ - Restricts evaluation to nodes belonging to the given layer. + Reads through a view of the given layer. Arguments: layer (str): Layer name. @@ -615,7 +557,7 @@ class Node(object): """ Filters a node property by name. - The property may be static or temporal depending on the query context. + Reads the property's latest value; `temporal()` switches to its history. Arguments: name (str): Property key. @@ -625,12 +567,12 @@ class Node(object): """ @staticmethod - def snapshot_at(time: int) -> filter.NodeFilter: + def snapshot_at(time: TimeInput) -> filter.NodeFilter: """ Evaluates filters against a snapshot of the graph at a given time. Arguments: - time (int): Snapshot time. + time (TimeInput): Snapshot time. Returns: filter.NodeFilter: @@ -646,25 +588,25 @@ class Node(object): """ @staticmethod - def window(start: int, end: int) -> filter.NodeFilter: + def window(start: TimeInput, end: TimeInput) -> filter.NodeFilter: """ Restricts node evaluation to the given time window. The window is inclusive of `start` and exclusive of `end`. Arguments: - start (int): Start time. - end (int): End time. + start (TimeInput): Start time. + end (TimeInput): End time. Returns: filter.NodeFilter: """ -class NodeFilter(object): +class NodeFilter(object): """ A node filter scoped to a view. - Obtained from the view methods on [`Node`] (`Node.window(...)`, + Obtained from the view methods on `Node` (`Node.window(...)`, `Node.latest()`, ...); its field and property methods evaluate within that view, and its own view methods narrow it further. """ @@ -672,34 +614,34 @@ class NodeFilter(object): def __repr__(self): """Return repr(self).""" - def after(self, time: int) -> filter.NodeFilter: + def after(self, time: TimeInput) -> filter.NodeFilter: """ Restricts node evaluation to times strictly after the given time. Arguments: - time (int): Lower time bound. + time (TimeInput): Lower time bound. Returns: filter.NodeFilter: """ - def at(self, time: int) -> filter.NodeFilter: + def at(self, time: TimeInput) -> filter.NodeFilter: """ Restricts node evaluation to a single point in time. Arguments: - time (int): Event time. + time (TimeInput): Event time. Returns: filter.NodeFilter: """ - def before(self, time: int) -> filter.NodeFilter: + def before(self, time: TimeInput) -> filter.NodeFilter: """ Restricts node evaluation to times strictly before the given time. Arguments: - time (int): Upper time bound. + time (TimeInput): Upper time bound. Returns: filter.NodeFilter: @@ -759,7 +701,7 @@ class NodeFilter(object): def layer(self, layer: str) -> filter.NodeFilter: """ - Restricts evaluation to nodes belonging to the given layer. + Reads through a view of the given layer. Arguments: layer (str): Layer name. @@ -820,7 +762,7 @@ class NodeFilter(object): """ Filters a node property by name. - The property may be static or temporal depending on the query context. + Reads the property's latest value; `temporal()` switches to its history. Arguments: name (str): Property key. @@ -829,12 +771,12 @@ class NodeFilter(object): filter.PropertyExpr: """ - def snapshot_at(self, time: int) -> filter.NodeFilter: + def snapshot_at(self, time: TimeInput) -> filter.NodeFilter: """ Evaluates filters against a snapshot of the graph at a given time. Arguments: - time (int): Snapshot time. + time (TimeInput): Snapshot time. Returns: filter.NodeFilter: @@ -848,60 +790,60 @@ class NodeFilter(object): filter.NodeFilter: """ - def window(self, start: int, end: int) -> filter.NodeFilter: + def window(self, start: TimeInput, end: TimeInput) -> filter.NodeFilter: """ Restricts node evaluation to the given time window. The window is inclusive of `start` and exclusive of `end`. Arguments: - start (int): Start time. - end (int): End time. + start (TimeInput): Start time. + end (TimeInput): End time. Returns: filter.NodeFilter: """ -class Edge(object): +class Edge(object): """ Entry point for constructing edge filter expressions. Every method is static: `Edge.src().name() == "alice"` selects edges - directly, and the view methods return an [`EdgeFilter`] scoped to that + directly, and the view methods return an `EdgeFilter` scoped to that view for further chaining. """ @staticmethod - def after(time: int) -> filter.EdgeFilter: + def after(time: TimeInput) -> filter.EdgeFilter: """ Restricts edge evaluation to times strictly after the given time. Arguments: - time (int): Lower time bound. + time (TimeInput): Lower time bound. Returns: filter.EdgeFilter: """ @staticmethod - def at(time: int) -> filter.EdgeFilter: + def at(time: TimeInput) -> filter.EdgeFilter: """ Restricts edge evaluation to a single point in time. Arguments: - time (int): Event time. + time (TimeInput): Event time. Returns: filter.EdgeFilter: """ @staticmethod - def before(time: int) -> filter.EdgeFilter: + def before(time: TimeInput) -> filter.EdgeFilter: """ Restricts edge evaluation to times strictly before the given time. Arguments: - time (int): Upper time bound. + time (TimeInput): Upper time bound. Returns: filter.EdgeFilter: @@ -1010,12 +952,12 @@ class Edge(object): """ @staticmethod - def snapshot_at(time: int) -> filter.EdgeFilter: + def snapshot_at(time: TimeInput) -> filter.EdgeFilter: """ Evaluates edge predicates against a snapshot of the graph at a given time. Arguments: - time (int): Snapshot time. + time (TimeInput): Snapshot time. Returns: filter.EdgeFilter: @@ -1040,23 +982,23 @@ class Edge(object): """ @staticmethod - def window(start: int, end: int) -> filter.EdgeFilter: + def window(start: TimeInput, end: TimeInput) -> filter.EdgeFilter: """ Restricts edge evaluation to the given time window. Arguments: - start (int): Start time. - end (int): End time. + start (TimeInput): Start time. + end (TimeInput): End time. Returns: filter.EdgeFilter: """ -class EdgeFilter(object): +class EdgeFilter(object): """ An edge filter scoped to a view. - Obtained from the view methods on [`Edge`] (`Edge.window(...)`, + Obtained from the view methods on `Edge` (`Edge.window(...)`, `Edge.layer(...)`, ...); its endpoint, property and structural predicates evaluate within that view, and its own view methods narrow it further. """ @@ -1064,34 +1006,34 @@ class EdgeFilter(object): def __repr__(self): """Return repr(self).""" - def after(self, time: int) -> filter.EdgeFilter: + def after(self, time: TimeInput) -> filter.EdgeFilter: """ Restricts edge evaluation to times strictly after the given time. Arguments: - time (int): Lower time bound. + time (TimeInput): Lower time bound. Returns: filter.EdgeFilter: """ - def at(self, time: int) -> filter.EdgeFilter: + def at(self, time: TimeInput) -> filter.EdgeFilter: """ Restricts edge evaluation to a single point in time. Arguments: - time (int): Event time. + time (TimeInput): Event time. Returns: filter.EdgeFilter: """ - def before(self, time: int) -> filter.EdgeFilter: + def before(self, time: TimeInput) -> filter.EdgeFilter: """ Restricts edge evaluation to times strictly before the given time. Arguments: - time (int): Upper time bound. + time (TimeInput): Upper time bound. Returns: filter.EdgeFilter: @@ -1189,12 +1131,12 @@ class EdgeFilter(object): filter.PropertyExpr: """ - def snapshot_at(self, time: int) -> filter.EdgeFilter: + def snapshot_at(self, time: TimeInput) -> filter.EdgeFilter: """ Evaluates edge predicates against a snapshot of the graph at a given time. Arguments: - time (int): Snapshot time. + time (TimeInput): Snapshot time. Returns: filter.EdgeFilter: @@ -1216,19 +1158,19 @@ class EdgeFilter(object): filter.EdgeEndpoint: """ - def window(self, start: int, end: int) -> filter.EdgeFilter: + def window(self, start: TimeInput, end: TimeInput) -> filter.EdgeFilter: """ Restricts edge evaluation to the given time window. Arguments: - start (int): Start time. - end (int): End time. + start (TimeInput): Start time. + end (TimeInput): End time. Returns: filter.EdgeFilter: """ -class EdgeEndpoint(object): +class EdgeEndpoint(object): """ Entry point for filtering an edge endpoint (source or destination). @@ -1291,45 +1233,45 @@ class EdgeEndpoint(object): filter.PropertyExpr: """ -class ExplodedEdge(object): +class ExplodedEdge(object): """ Entry point for constructing exploded-edge filter expressions. Every method is static; the view methods return an - [`ExplodedEdgeFilter`] scoped to that view for further chaining. + `ExplodedEdgeFilter` scoped to that view for further chaining. """ @staticmethod - def after(time: int) -> filter.ExplodedEdgeFilter: + def after(time: TimeInput) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to times strictly after the given time. Arguments: - time (int): Lower time bound. + time (TimeInput): Lower time bound. Returns: filter.ExplodedEdgeFilter: """ @staticmethod - def at(time: int) -> filter.ExplodedEdgeFilter: + def at(time: TimeInput) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to a single point in time. Arguments: - time (int): Event time. + time (TimeInput): Event time. Returns: filter.ExplodedEdgeFilter: """ @staticmethod - def before(time: int) -> filter.ExplodedEdgeFilter: + def before(time: TimeInput) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to times strictly before the given time. Arguments: - time (int): Upper time bound. + time (TimeInput): Upper time bound. Returns: filter.ExplodedEdgeFilter: @@ -1423,7 +1365,7 @@ class ExplodedEdge(object): """ Filters an exploded edge property by name. - The property may be static or temporal depending on the query context. + Reads the property's latest value; `temporal()` switches to its history. Arguments: name (str): Property key. @@ -1433,12 +1375,12 @@ class ExplodedEdge(object): """ @staticmethod - def snapshot_at(time: int) -> filter.ExplodedEdgeFilter: + def snapshot_at(time: TimeInput) -> filter.ExplodedEdgeFilter: """ Evaluates exploded edge predicates against a snapshot of the graph at a given time. Arguments: - time (int): Snapshot time. + time (TimeInput): Snapshot time. Returns: filter.ExplodedEdgeFilter: @@ -1454,61 +1396,61 @@ class ExplodedEdge(object): """ @staticmethod - def window(start: int, end: int) -> filter.ExplodedEdgeFilter: + def window(start: TimeInput, end: TimeInput) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to the given time window. The window is inclusive of `start` and exclusive of `end`. Arguments: - start (int): Start time. - end (int): End time. + start (TimeInput): Start time. + end (TimeInput): End time. Returns: filter.ExplodedEdgeFilter: """ -class ExplodedEdgeFilter(object): +class ExplodedEdgeFilter(object): """ An exploded-edge filter scoped to a view. An exploded edge is one temporal event of an edge, addressed individually rather than as the edge aggregated across time. Obtained from the view - methods on [`ExplodedEdge`]; its property and structural predicates evaluate + methods on `ExplodedEdge`; its property and structural predicates evaluate within that view, and its own view methods narrow it further. """ def __repr__(self): """Return repr(self).""" - def after(self, time: int) -> filter.ExplodedEdgeFilter: + def after(self, time: TimeInput) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to times strictly after the given time. Arguments: - time (int): Lower time bound. + time (TimeInput): Lower time bound. Returns: filter.ExplodedEdgeFilter: """ - def at(self, time: int) -> filter.ExplodedEdgeFilter: + def at(self, time: TimeInput) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to a single point in time. Arguments: - time (int): Event time. + time (TimeInput): Event time. Returns: filter.ExplodedEdgeFilter: """ - def before(self, time: int) -> filter.ExplodedEdgeFilter: + def before(self, time: TimeInput) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to times strictly before the given time. Arguments: - time (int): Upper time bound. + time (TimeInput): Upper time bound. Returns: filter.ExplodedEdgeFilter: @@ -1593,7 +1535,7 @@ class ExplodedEdgeFilter(object): """ Filters an exploded edge property by name. - The property may be static or temporal depending on the query context. + Reads the property's latest value; `temporal()` switches to its history. Arguments: name (str): Property key. @@ -1602,12 +1544,12 @@ class ExplodedEdgeFilter(object): filter.PropertyExpr: """ - def snapshot_at(self, time: int) -> filter.ExplodedEdgeFilter: + def snapshot_at(self, time: TimeInput) -> filter.ExplodedEdgeFilter: """ Evaluates exploded edge predicates against a snapshot of the graph at a given time. Arguments: - time (int): Snapshot time. + time (TimeInput): Snapshot time. Returns: filter.ExplodedEdgeFilter: @@ -1621,59 +1563,59 @@ class ExplodedEdgeFilter(object): filter.ExplodedEdgeFilter: """ - def window(self, start: int, end: int) -> filter.ExplodedEdgeFilter: + def window(self, start: TimeInput, end: TimeInput) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to the given time window. The window is inclusive of `start` and exclusive of `end`. Arguments: - start (int): Start time. - end (int): End time. + start (TimeInput): Start time. + end (TimeInput): End time. Returns: filter.ExplodedEdgeFilter: """ -class Graph(object): +class Graph(object): """ Entry point for graph-level view filters. - Every method is static and returns a [`GraphFilter`] carrying the view, + Every method is static and returns a `GraphFilter` carrying the view, which composes with node and edge predicates. """ @staticmethod - def after(time: int) -> filter.GraphFilter: + def after(time: TimeInput) -> filter.GraphFilter: """ Restricts evaluation to times strictly after the given time. Arguments: - time (int): Lower time bound. + time (TimeInput): Lower time bound. Returns: filter.GraphFilter: """ @staticmethod - def at(time: int) -> filter.GraphFilter: + def at(time: TimeInput) -> filter.GraphFilter: """ Restricts evaluation to a single point in time. Arguments: - time (int): Event time. + time (TimeInput): Event time. Returns: filter.GraphFilter: """ @staticmethod - def before(time: int) -> filter.GraphFilter: + def before(time: TimeInput) -> filter.GraphFilter: """ Restricts evaluation to times strictly before the given time. Arguments: - time (int): Upper time bound. + time (TimeInput): Upper time bound. Returns: filter.GraphFilter: @@ -1713,12 +1655,12 @@ class Graph(object): """ @staticmethod - def snapshot_at(time: int) -> filter.GraphFilter: + def snapshot_at(time: TimeInput) -> filter.GraphFilter: """ Evaluates filters against a snapshot of the graph at a given time. Arguments: - time (int): Snapshot time. + time (TimeInput): Snapshot time. Returns: filter.GraphFilter: @@ -1734,25 +1676,25 @@ class Graph(object): """ @staticmethod - def window(start: int, end: int) -> filter.GraphFilter: + def window(start: TimeInput, end: TimeInput) -> filter.GraphFilter: """ Restricts evaluation to events within a time window. The window is inclusive of `start` and exclusive of `end`. Arguments: - start (int): Start time. - end (int): End time. + start (TimeInput): Start time. + end (TimeInput): End time. Returns: filter.GraphFilter: """ -class GraphFilter(FilterExpr): +class GraphFilter(FilterExpr): """ A graph-level view scope. - Obtained from the view methods on [`Graph`] (`Graph.window(...)`, + Obtained from the view methods on `Graph` (`Graph.window(...)`, `Graph.latest()`, ...). It carries no node or edge predicate of its own: it fixes the temporal and layer scope that node and edge predicates compose with, and its own view methods narrow it further. @@ -1761,34 +1703,34 @@ class GraphFilter(FilterExpr): def __repr__(self): """Return repr(self).""" - def after(self, time: int) -> filter.GraphFilter: + def after(self, time: TimeInput) -> filter.GraphFilter: """ Restricts evaluation to times strictly after the given time. Arguments: - time (int): Lower time bound. + time (TimeInput): Lower time bound. Returns: filter.GraphFilter: """ - def at(self, time: int) -> filter.GraphFilter: + def at(self, time: TimeInput) -> filter.GraphFilter: """ Restricts evaluation to a single point in time. Arguments: - time (int): Event time. + time (TimeInput): Event time. Returns: filter.GraphFilter: """ - def before(self, time: int) -> filter.GraphFilter: + def before(self, time: TimeInput) -> filter.GraphFilter: """ Restricts evaluation to times strictly before the given time. Arguments: - time (int): Upper time bound. + time (TimeInput): Upper time bound. Returns: filter.GraphFilter: @@ -1824,12 +1766,12 @@ class GraphFilter(FilterExpr): filter.GraphFilter: """ - def snapshot_at(self, time: int) -> filter.GraphFilter: + def snapshot_at(self, time: TimeInput) -> filter.GraphFilter: """ Evaluates filters against a snapshot of the graph at a given time. Arguments: - time (int): Snapshot time. + time (TimeInput): Snapshot time. Returns: filter.GraphFilter: @@ -1843,15 +1785,15 @@ class GraphFilter(FilterExpr): filter.GraphFilter: """ - def window(self, start: int, end: int) -> filter.GraphFilter: + def window(self, start: TimeInput, end: TimeInput) -> filter.GraphFilter: """ Restricts evaluation to events within a time window. The window is inclusive of `start` and exclusive of `end`. Arguments: - start (int): Start time. - end (int): End time. + start (TimeInput): Start time. + end (TimeInput): End time. Returns: filter.GraphFilter: diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs index e1436817a7..5b8ad011f7 100644 --- a/raphtory/src/python/filter/edge_expr.rs +++ b/raphtory/src/python/filter/edge_expr.rs @@ -150,7 +150,7 @@ impl PyEdgeFilter { /// An edge filter scoped to a view. /// -/// Obtained from the view methods on [`Edge`] (`Edge.window(...)`, +/// Obtained from the view methods on `Edge` (`Edge.window(...)`, /// `Edge.layer(...)`, ...); its endpoint, property and structural predicates /// evaluate within that view, and its own view methods narrow it further. #[pyclass(frozen, name = "EdgeFilter", module = "raphtory.filter")] @@ -209,8 +209,8 @@ impl PyEdgeFilter { /// Restricts edge evaluation to the given time window. /// /// Arguments: - /// start (int): Start time. - /// end (int): End time. + /// start (TimeInput): Start time. + /// end (TimeInput): End time. /// /// Returns: /// filter.EdgeFilter: @@ -221,7 +221,7 @@ impl PyEdgeFilter { /// Restricts edge evaluation to a single point in time. /// /// Arguments: - /// time (int): Event time. + /// time (TimeInput): Event time. /// /// Returns: /// filter.EdgeFilter: @@ -232,7 +232,7 @@ impl PyEdgeFilter { /// Restricts edge evaluation to times strictly after the given time. /// /// Arguments: - /// time (int): Lower time bound. + /// time (TimeInput): Lower time bound. /// /// Returns: /// filter.EdgeFilter: @@ -243,7 +243,7 @@ impl PyEdgeFilter { /// Restricts edge evaluation to times strictly before the given time. /// /// Arguments: - /// time (int): Upper time bound. + /// time (TimeInput): Upper time bound. /// /// Returns: /// filter.EdgeFilter: @@ -262,7 +262,7 @@ impl PyEdgeFilter { /// Evaluates edge predicates against a snapshot of the graph at a given time. /// /// Arguments: - /// time (int): Snapshot time. + /// time (TimeInput): Snapshot time. /// /// Returns: /// filter.EdgeFilter: @@ -344,7 +344,7 @@ impl PyEdgeFilter { /// Entry point for constructing edge filter expressions. /// /// Every method is static: `Edge.src().name() == "alice"` selects edges -/// directly, and the view methods return an [`EdgeFilter`] scoped to that +/// directly, and the view methods return an `EdgeFilter` scoped to that /// view for further chaining. #[pyclass(frozen, name = "Edge", module = "raphtory.filter")] pub struct PyEdge; @@ -396,8 +396,8 @@ impl PyEdge { /// Restricts edge evaluation to the given time window. /// /// Arguments: - /// start (int): Start time. - /// end (int): End time. + /// start (TimeInput): Start time. + /// end (TimeInput): End time. /// /// Returns: /// filter.EdgeFilter: @@ -409,7 +409,7 @@ impl PyEdge { /// Restricts edge evaluation to a single point in time. /// /// Arguments: - /// time (int): Event time. + /// time (TimeInput): Event time. /// /// Returns: /// filter.EdgeFilter: @@ -421,7 +421,7 @@ impl PyEdge { /// Restricts edge evaluation to times strictly after the given time. /// /// Arguments: - /// time (int): Lower time bound. + /// time (TimeInput): Lower time bound. /// /// Returns: /// filter.EdgeFilter: @@ -433,7 +433,7 @@ impl PyEdge { /// Restricts edge evaluation to times strictly before the given time. /// /// Arguments: - /// time (int): Upper time bound. + /// time (TimeInput): Upper time bound. /// /// Returns: /// filter.EdgeFilter: @@ -454,7 +454,7 @@ impl PyEdge { /// Evaluates edge predicates against a snapshot of the graph at a given time. /// /// Arguments: - /// time (int): Snapshot time. + /// time (TimeInput): Snapshot time. /// /// Returns: /// filter.EdgeFilter: diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs index 6888495d37..61fa467498 100644 --- a/raphtory/src/python/filter/exploded_edge_expr.rs +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -15,7 +15,7 @@ use raphtory_api::core::storage::timeindex::EventTime; /// /// An exploded edge is one temporal event of an edge, addressed individually /// rather than as the edge aggregated across time. Obtained from the view -/// methods on [`ExplodedEdge`]; its property and structural predicates evaluate +/// methods on `ExplodedEdge`; its property and structural predicates evaluate /// within that view, and its own view methods narrow it further. #[pyclass(frozen, name = "ExplodedEdgeFilter", module = "raphtory.filter")] pub struct PyExplodedEdgeFilter(pub(crate) Vec); @@ -54,7 +54,7 @@ impl PyExplodedEdgeFilter { /// Filters an exploded edge property by name. /// - /// The property may be static or temporal depending on the query context. + /// Reads the property's latest value; `temporal()` switches to its history. /// /// Arguments: /// name (str): Property key. @@ -83,8 +83,8 @@ impl PyExplodedEdgeFilter { /// The window is inclusive of `start` and exclusive of `end`. /// /// Arguments: - /// start (int): Start time. - /// end (int): End time. + /// start (TimeInput): Start time. + /// end (TimeInput): End time. /// /// Returns: /// filter.ExplodedEdgeFilter: @@ -95,7 +95,7 @@ impl PyExplodedEdgeFilter { /// Restricts exploded edge evaluation to a single point in time. /// /// Arguments: - /// time (int): Event time. + /// time (TimeInput): Event time. /// /// Returns: /// filter.ExplodedEdgeFilter: @@ -106,7 +106,7 @@ impl PyExplodedEdgeFilter { /// Restricts exploded edge evaluation to times strictly after the given time. /// /// Arguments: - /// time (int): Lower time bound. + /// time (TimeInput): Lower time bound. /// /// Returns: /// filter.ExplodedEdgeFilter: @@ -117,7 +117,7 @@ impl PyExplodedEdgeFilter { /// Restricts exploded edge evaluation to times strictly before the given time. /// /// Arguments: - /// time (int): Upper time bound. + /// time (TimeInput): Upper time bound. /// /// Returns: /// filter.ExplodedEdgeFilter: @@ -136,7 +136,7 @@ impl PyExplodedEdgeFilter { /// Evaluates exploded edge predicates against a snapshot of the graph at a given time. /// /// Arguments: - /// time (int): Snapshot time. + /// time (TimeInput): Snapshot time. /// /// Returns: /// filter.ExplodedEdgeFilter: @@ -218,7 +218,7 @@ impl PyExplodedEdgeFilter { /// Entry point for constructing exploded-edge filter expressions. /// /// Every method is static; the view methods return an -/// [`ExplodedEdgeFilter`] scoped to that view for further chaining. +/// `ExplodedEdgeFilter` scoped to that view for further chaining. #[pyclass(frozen, name = "ExplodedEdge", module = "raphtory.filter")] pub struct PyExplodedEdge; @@ -226,7 +226,7 @@ pub struct PyExplodedEdge; impl PyExplodedEdge { /// Filters an exploded edge property by name. /// - /// The property may be static or temporal depending on the query context. + /// Reads the property's latest value; `temporal()` switches to its history. /// /// Arguments: /// name (str): Property key. @@ -257,8 +257,8 @@ impl PyExplodedEdge { /// The window is inclusive of `start` and exclusive of `end`. /// /// Arguments: - /// start (int): Start time. - /// end (int): End time. + /// start (TimeInput): Start time. + /// end (TimeInput): End time. /// /// Returns: /// filter.ExplodedEdgeFilter: @@ -270,7 +270,7 @@ impl PyExplodedEdge { /// Restricts exploded edge evaluation to a single point in time. /// /// Arguments: - /// time (int): Event time. + /// time (TimeInput): Event time. /// /// Returns: /// filter.ExplodedEdgeFilter: @@ -282,7 +282,7 @@ impl PyExplodedEdge { /// Restricts exploded edge evaluation to times strictly after the given time. /// /// Arguments: - /// time (int): Lower time bound. + /// time (TimeInput): Lower time bound. /// /// Returns: /// filter.ExplodedEdgeFilter: @@ -294,7 +294,7 @@ impl PyExplodedEdge { /// Restricts exploded edge evaluation to times strictly before the given time. /// /// Arguments: - /// time (int): Upper time bound. + /// time (TimeInput): Upper time bound. /// /// Returns: /// filter.ExplodedEdgeFilter: @@ -315,7 +315,7 @@ impl PyExplodedEdge { /// Evaluates exploded edge predicates against a snapshot of the graph at a given time. /// /// Arguments: - /// time (int): Snapshot time. + /// time (TimeInput): Snapshot time. /// /// Returns: /// filter.ExplodedEdgeFilter: diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 3c02900107..86cf0b87af 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -15,7 +15,7 @@ use std::sync::Arc; /// A filter as a tree. The same tree runs locally, is sent to a server, and is /// what `repr` prints, so there is nothing to keep in step. /// -/// Anywhere a filter is expected, a yes/no [`Expr`] is accepted too: it is the +/// Anywhere a filter is expected, a yes/no `Expr` is accepted too: it is the /// filter on its own entity. /// /// `&`, `|` and `~` combine filters. `~` keeps what the filter drops: a negated @@ -78,10 +78,6 @@ impl PyFilterExpr { PyFilterExpr(FilterExpr::And(vec![self.0.clone(), other.into_filter()])) } - pub fn __rand__(&self, other: ExprOrFilter) -> Self { - PyFilterExpr(FilterExpr::And(vec![other.into_filter(), self.0.clone()])) - } - pub fn __or__(&self, other: ExprOrFilter) -> PyResult { let other = other.into_filter(); no_view(&self.0)?; @@ -89,13 +85,6 @@ impl PyFilterExpr { Ok(PyFilterExpr(FilterExpr::Or(vec![self.0.clone(), other]))) } - pub fn __ror__(&self, other: ExprOrFilter) -> PyResult { - let other = other.into_filter(); - no_view(&self.0)?; - no_view(&other)?; - Ok(PyFilterExpr(FilterExpr::Or(vec![other, self.0.clone()]))) - } - fn __invert__(&self) -> PyResult { no_view(&self.0)?; Ok(PyFilterExpr(FilterExpr::Not(Box::new(self.0.clone())))) diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index fb22151b2c..403584b93f 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -10,7 +10,7 @@ use raphtory_api::core::storage::timeindex::EventTime; /// A graph-level view scope. /// -/// Obtained from the view methods on [`Graph`] (`Graph.window(...)`, +/// Obtained from the view methods on `Graph` (`Graph.window(...)`, /// `Graph.latest()`, ...). It carries no node or edge predicate of its own: it /// fixes the temporal and layer scope that node and edge predicates compose /// with, and its own view methods narrow it further. @@ -45,8 +45,8 @@ impl PyGraphFilter { /// The window is inclusive of `start` and exclusive of `end`. /// /// Arguments: - /// start (int): Start time. - /// end (int): End time. + /// start (TimeInput): Start time. + /// end (TimeInput): End time. /// /// Returns: /// filter.GraphFilter: @@ -57,7 +57,7 @@ impl PyGraphFilter { /// Restricts evaluation to a single point in time. /// /// Arguments: - /// time (int): Event time. + /// time (TimeInput): Event time. /// /// Returns: /// filter.GraphFilter: @@ -68,7 +68,7 @@ impl PyGraphFilter { /// Restricts evaluation to times strictly after the given time. /// /// Arguments: - /// time (int): Lower time bound. + /// time (TimeInput): Lower time bound. /// /// Returns: /// filter.GraphFilter: @@ -79,7 +79,7 @@ impl PyGraphFilter { /// Restricts evaluation to times strictly before the given time. /// /// Arguments: - /// time (int): Upper time bound. + /// time (TimeInput): Upper time bound. /// /// Returns: /// filter.GraphFilter: @@ -98,7 +98,7 @@ impl PyGraphFilter { /// Evaluates filters against a snapshot of the graph at a given time. /// /// Arguments: - /// time (int): Snapshot time. + /// time (TimeInput): Snapshot time. /// /// Returns: /// filter.GraphFilter: @@ -139,7 +139,7 @@ impl PyGraphFilter { /// Entry point for graph-level view filters. /// -/// Every method is static and returns a [`GraphFilter`] carrying the view, +/// Every method is static and returns a `GraphFilter` carrying the view, /// which composes with node and edge predicates. #[pyclass(frozen, name = "Graph", module = "raphtory.filter")] pub struct PyGraph; @@ -151,8 +151,8 @@ impl PyGraph { /// The window is inclusive of `start` and exclusive of `end`. /// /// Arguments: - /// start (int): Start time. - /// end (int): End time. + /// start (TimeInput): Start time. + /// end (TimeInput): End time. /// /// Returns: /// filter.GraphFilter: @@ -164,7 +164,7 @@ impl PyGraph { /// Restricts evaluation to a single point in time. /// /// Arguments: - /// time (int): Event time. + /// time (TimeInput): Event time. /// /// Returns: /// filter.GraphFilter: @@ -176,7 +176,7 @@ impl PyGraph { /// Restricts evaluation to times strictly after the given time. /// /// Arguments: - /// time (int): Lower time bound. + /// time (TimeInput): Lower time bound. /// /// Returns: /// filter.GraphFilter: @@ -188,7 +188,7 @@ impl PyGraph { /// Restricts evaluation to times strictly before the given time. /// /// Arguments: - /// time (int): Upper time bound. + /// time (TimeInput): Upper time bound. /// /// Returns: /// filter.GraphFilter: @@ -209,7 +209,7 @@ impl PyGraph { /// Evaluates filters against a snapshot of the graph at a given time. /// /// Arguments: - /// time (int): Snapshot time. + /// time (TimeInput): Snapshot time. /// /// Returns: /// filter.GraphFilter: diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index b552447e60..42d2a734a0 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -12,7 +12,6 @@ use pyo3::{ prelude::{PyModule, PyModuleMethods}, Bound, PyErr, Python, }; -use raphtory_api::core::entities::Layer; pub mod edge_expr; pub mod exploded_edge_expr; @@ -21,12 +20,6 @@ pub mod graph_filter; pub mod node_expr; pub(crate) mod repr; -impl From> for Layer { - fn from(iter: FromIterable) -> Self { - iter.into_iter().collect::>().into() - } -} - pub fn base_filter_module(py: Python<'_>) -> Result, PyErr> { let filter_module = PyModule::new(py, "filter")?; diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs index 6ea81e8a73..39b7c6c597 100644 --- a/raphtory/src/python/filter/node_expr.rs +++ b/raphtory/src/python/filter/node_expr.rs @@ -1,8 +1,8 @@ use crate::{ db::graph::views::filter::model::{ expr::{ - Agg, CmpOp, EdgeExpr, ExplodedEdgeExpr, Expr, Field, FilterExpr, Leaf, NodeExpr, - NodeLeaf, OpaqueFilter, StrOp, ViewOp, + Agg, CmpOp, EdgeExpr, EdgeLeaf, ExplodedEdgeExpr, ExplodedEdgeLeaf, Expr, Field, + FilterExpr, Leaf, NodeExpr, NodeLeaf, OpaqueFilter, StrOp, ViewOp, }, node_expr::DynCreateOp, node_state_filter::NodeStateBoolColOp, @@ -102,31 +102,15 @@ impl Typed { pub(crate) fn into_filter(self) -> FilterExpr { match self { Typed::Node(e) => NodeLeaf::filter(e), - Typed::Edge(e) => as Into>::into(e), - Typed::ExplodedEdge(e) => FilterExpr::ExplodedEdge(e), - } - } -} - -impl From for FilterExpr { - fn from(e: EdgeExpr) -> Self { - FilterExpr::Edge(e) - } -} - -impl std::fmt::Display for Typed { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Typed::Node(e) => e.fmt(f), - Typed::Edge(e) => e.fmt(f), - Typed::ExplodedEdge(e) => e.fmt(f), + Typed::Edge(e) => EdgeLeaf::filter(e), + Typed::ExplodedEdge(e) => ExplodedEdgeLeaf::filter(e), } } } /// A value expression: a field, degree, property, metadata entry, an aggregate /// over one, or a yes/no built from them. Comparing it to a value or to another -/// expression gives a yes/no [`Expr`], which is a filter on its entity. +/// expression gives a yes/no `Expr`, which is a filter on its entity. /// /// `~` on a yes/no is the opposite yes/no: a node without the property fails /// `property("score") > 4`, so it passes `~(property("score") > 4)`. @@ -613,9 +597,9 @@ impl PyExpr { #[pymethods] impl PyPropertyExpr { - /// Switches from the property's latest value to its full temporal history, - /// unlocking the aggregate chain (`sum`, `avg`, `min`, `max`, ...) and the - /// element-wise comparisons `any()` / `all()` collapse. + /// Switches from the property's latest value to its full history, a list + /// that the aggregates (`sum`, `avg`, `min`, `max`, ...) reduce and that a + /// comparison tests element by element, for `any()` / `all()` to collapse. /// /// Returns: /// filter.Expr: @@ -630,7 +614,7 @@ impl PyPropertyExpr { /// A node filter scoped to a view. /// -/// Obtained from the view methods on [`Node`] (`Node.window(...)`, +/// Obtained from the view methods on `Node` (`Node.window(...)`, /// `Node.latest()`, ...); its field and property methods evaluate within that /// view, and its own view methods narrow it further. #[pyclass(frozen, name = "NodeFilter", module = "raphtory.filter")] @@ -732,7 +716,7 @@ impl PyNodeFilter { /// Filters a node property by name. /// - /// The property may be static or temporal depending on the query context. + /// Reads the property's latest value; `temporal()` switches to its history. /// /// Arguments: /// name (str): Property key. @@ -761,8 +745,8 @@ impl PyNodeFilter { /// The window is inclusive of `start` and exclusive of `end`. /// /// Arguments: - /// start (int): Start time. - /// end (int): End time. + /// start (TimeInput): Start time. + /// end (TimeInput): End time. /// /// Returns: /// filter.NodeFilter: @@ -773,7 +757,7 @@ impl PyNodeFilter { /// Restricts node evaluation to a single point in time. /// /// Arguments: - /// time (int): Event time. + /// time (TimeInput): Event time. /// /// Returns: /// filter.NodeFilter: @@ -784,7 +768,7 @@ impl PyNodeFilter { /// Restricts node evaluation to times strictly after the given time. /// /// Arguments: - /// time (int): Lower time bound. + /// time (TimeInput): Lower time bound. /// /// Returns: /// filter.NodeFilter: @@ -795,7 +779,7 @@ impl PyNodeFilter { /// Restricts node evaluation to times strictly before the given time. /// /// Arguments: - /// time (int): Upper time bound. + /// time (TimeInput): Upper time bound. /// /// Returns: /// filter.NodeFilter: @@ -814,7 +798,7 @@ impl PyNodeFilter { /// Evaluates filters against a snapshot of the graph at a given time. /// /// Arguments: - /// time (int): Snapshot time. + /// time (TimeInput): Snapshot time. /// /// Returns: /// filter.NodeFilter: @@ -830,7 +814,7 @@ impl PyNodeFilter { self.with_view(ViewOp::SnapshotLatest) } - /// Restricts evaluation to nodes belonging to the given layer. + /// Reads through a view of the given layer. /// /// Arguments: /// layer (str): Layer name. @@ -879,7 +863,7 @@ impl PyNodeFilter { /// /// Every method is static: `Node.property("age") > 30` selects nodes /// directly, and the view methods (`window`, `latest`, `layer`, ...) return a -/// [`NodeFilter`] scoped to that view for further chaining. +/// `NodeFilter` scoped to that view for further chaining. #[pyclass(frozen, name = "Node", module = "raphtory.filter")] pub struct PyNode; @@ -941,7 +925,7 @@ impl PyNode { /// Filters a node property by name. /// - /// The property may be static or temporal depending on the query context. + /// Reads the property's latest value; `temporal()` switches to its history. /// /// Arguments: /// name (str): Property key. @@ -972,8 +956,8 @@ impl PyNode { /// The window is inclusive of `start` and exclusive of `end`. /// /// Arguments: - /// start (int): Start time. - /// end (int): End time. + /// start (TimeInput): Start time. + /// end (TimeInput): End time. /// /// Returns: /// filter.NodeFilter: @@ -985,7 +969,7 @@ impl PyNode { /// Restricts node evaluation to a single point in time. /// /// Arguments: - /// time (int): Event time. + /// time (TimeInput): Event time. /// /// Returns: /// filter.NodeFilter: @@ -997,7 +981,7 @@ impl PyNode { /// Restricts node evaluation to times strictly after the given time. /// /// Arguments: - /// time (int): Lower time bound. + /// time (TimeInput): Lower time bound. /// /// Returns: /// filter.NodeFilter: @@ -1009,7 +993,7 @@ impl PyNode { /// Restricts node evaluation to times strictly before the given time. /// /// Arguments: - /// time (int): Upper time bound. + /// time (TimeInput): Upper time bound. /// /// Returns: /// filter.NodeFilter: @@ -1030,7 +1014,7 @@ impl PyNode { /// Evaluates filters against a snapshot of the graph at a given time. /// /// Arguments: - /// time (int): Snapshot time. + /// time (TimeInput): Snapshot time. /// /// Returns: /// filter.NodeFilter: @@ -1048,7 +1032,7 @@ impl PyNode { PyNodeFilter::root().snapshot_latest() } - /// Restricts evaluation to nodes belonging to the given layer. + /// Reads through a view of the given layer. /// /// Arguments: /// layer (str): Layer name. diff --git a/raphtory/src/python/filter/repr.rs b/raphtory/src/python/filter/repr.rs index c55c828bd5..4a3e43841e 100644 --- a/raphtory/src/python/filter/repr.rs +++ b/raphtory/src/python/filter/repr.rs @@ -22,8 +22,13 @@ const MODULE: &str = "raphtory.filter"; /// is; a comparison or an `&`/`|` combination needs parentheses. #[derive(Clone, Copy, PartialEq, Eq)] enum Shape { + /// A dotted chain: safe as an operand and as a receiver. Chain, + /// An operator expression: bracketed as an operand and as a receiver. Compound, + /// A `~` expression: safe as an operand, bracketed as a receiver, since + /// `~a.b()` is `~(a.b())` in Python. + Unary, } struct Rendered { @@ -46,13 +51,30 @@ impl Rendered { } } - /// The text as an operand or a receiver: parenthesised unless it is a chain. + fn unary(text: String) -> Self { + Rendered { + text, + shape: Shape::Unary, + } + } + + /// The text as an operand of `&`, `|` or a comparison: parenthesised + /// unless it is a chain or a `~`. fn atom(&self) -> String { match self.shape { - Shape::Chain => self.text.clone(), + Shape::Chain | Shape::Unary => self.text.clone(), Shape::Compound => format!("({})", self.text), } } + + /// The text as the receiver of a method call: parenthesised unless it is + /// a chain. + fn receiver(&self) -> String { + match self.shape { + Shape::Chain => self.text.clone(), + Shape::Compound | Shape::Unary => format!("({})", self.text), + } + } } pub(crate) fn typed(py: Python<'_>, expr: &Typed) -> PyResult { @@ -118,7 +140,7 @@ fn render_filter(py: Python<'_>, filter: &FilterExpr) -> PyResult { .map(|i| render_filter(py, i)), " | ", )?, - FilterExpr::Not(e) => Rendered::chain(format!("~{}", render_filter(py, e)?.atom())), + FilterExpr::Not(e) => Rendered::unary(format!("~{}", render_filter(py, e)?.atom())), FilterExpr::Opaque(_) => Rendered::chain("".to_owned()), }) } @@ -139,7 +161,7 @@ fn render(py: Python<'_>, expr: &Expr, entity: Entity) -> PyResult "earliest", Agg::Latest => "latest", }; - Rendered::chain(format!("{}.{name}()", render(py, e, entity)?.atom())) + Rendered::chain(format!("{}.{name}()", render(py, e, entity)?.receiver())) } Expr::Cmp(op, l, r) => { let sym = match op { @@ -184,13 +206,17 @@ fn render(py: Python<'_>, expr: &Expr, entity: Entity) -> PyResult Rendered::chain(format!("{}.is_some()", render(py, e, entity)?.atom())), - Expr::IsNone(e) => Rendered::chain(format!("{}.is_none()", render(py, e, entity)?.atom())), - Expr::Any(e) => Rendered::chain(format!("{}.any()", render(py, e, entity)?.atom())), - Expr::All(e) => Rendered::chain(format!("{}.all()", render(py, e, entity)?.atom())), + Expr::IsSome(e) => { + Rendered::chain(format!("{}.is_some()", render(py, e, entity)?.receiver())) + } + Expr::IsNone(e) => { + Rendered::chain(format!("{}.is_none()", render(py, e, entity)?.receiver())) + } + Expr::Any(e) => Rendered::chain(format!("{}.any()", render(py, e, entity)?.receiver())), + Expr::All(e) => Rendered::chain(format!("{}.all()", render(py, e, entity)?.receiver())), Expr::And(items) => combined( flat(items, |i| match i { Expr::And(inner) => Some(inner), @@ -209,7 +235,7 @@ fn render(py: Python<'_>, expr: &Expr, entity: Entity) -> PyResult Rendered::chain(format!("~{}", render(py, e, entity)?.atom())), + Expr::Not(e) => Rendered::unary(format!("~{}", render(py, e, entity)?.atom())), }) } @@ -398,8 +424,20 @@ fn time(t: &EventTime) -> String { } } +/// A constant as Python source. A list is written as a list literal, since +/// the Python value it becomes is an array whose `repr` needs numpy. fn literal(py: Python<'_>, value: &Prop) -> PyResult { - Ok(value.clone().into_pyobject(py)?.repr()?.to_string()) + match value { + Prop::List(items) => { + let items = items + .iter() + .map(|item| literal(py, &item)) + .collect::>>()? + .join(", "); + Ok(format!("[{items}]")) + } + other => Ok(other.into_pyobject(py)?.repr()?.to_string()), + } } fn py_str(py: Python<'_>, s: &str) -> PyResult { diff --git a/stub_gen/stub_gen.py b/stub_gen/stub_gen.py index 9bf9030406..69b7fd93bf 100644 --- a/stub_gen/stub_gen.py +++ b/stub_gen/stub_gen.py @@ -430,12 +430,15 @@ def not_this_module_import(line: str, full_name: str) -> bool: ) -def shadow_free_import(line: str, module: ModuleType, defined: list[str]) -> str: +def shadow_free_import( + line: str, module: ModuleType, defined: list[str], body: str +) -> str: """A `from X import *` that carries a name this module binds to a different object is a redefinition to the type checker, which keeps the imported one and types every use of the local class as the foreign one. Such an import - is spelled out without the clashing names. A name both modules bind to the - same object (a class re-exported from two places) is not a clash. + is spelled out as the names the stub actually uses, minus the clashing + ones. A name both modules bind to the same object (a class re-exported + from two places) is not a clash. """ star = re.fullmatch(r"from ([\w.]+) import \*", line) if star is None: @@ -451,8 +454,14 @@ def shadow_free_import(line: str, module: ModuleType, defined: list[str]) -> str } if not clashing: return line - kept = ", ".join(n for n in exported if n not in clashing) - return f"from {star[1]} import {kept}" + used = [ + n + for n in exported + if n not in clashing and re.search(rf"\b{re.escape(n)}\b", body) + ] + if not used: + return "" + return f"from {star[1]} import {', '.join(used)}" def gen_module( @@ -484,8 +493,9 @@ def gen_module( loader = getattr(obj, "__loader__", None) if loader is None or isinstance(loader, ExtensionFileLoader): modules.append((obj, obj_name)) + body = "\n".join(stubs) valid_imports = ( - shadow_free_import(line, module, all_names) + shadow_free_import(line, module, all_names, body) for line in imports if not_this_module_import(line, full_name) ) From 6852e1da146fad2b679e68c02ec7c4c6debaffb9 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:52:02 +0100 Subject: [PATCH 150/155] run the filter tests on every graph variant again The node-layer tests use their layered fixture and run; the edge property tests cover the persistent variants as they did before; the latest-update tests use data that the innermost-list reduction would answer differently; and the remaining assertions name the message they expect instead of any exception. --- .../test_filters/test_edge_property_filter.py | 8 +- .../test_edges_collection_filter.py | 6 +- .../test_filters/test_exploded_edge_filter.py | 26 +- .../test_filters/test_node_filter.py | 4 +- .../parity/test_parity_filters.py | 3 - .../tests/filter_tests/edge_expr_filter.rs | 62 +-- raphtory-tests/tests/test_filters.rs | 355 ++++++++++++------ 7 files changed, 298 insertions(+), 166 deletions(-) diff --git a/python/tests/test_base_install/test_filters/test_edge_property_filter.py b/python/tests/test_base_install/test_filters/test_edge_property_filter.py index 36d6aef588..5501d0ac6e 100644 --- a/python/tests/test_base_install/test_filters/test_edge_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_property_filter.py @@ -1038,12 +1038,12 @@ def test_edge_unsupported_ops_agg(): def check(graph): # STARTS_WITH on SUM expr = filter.Edge.property("p_u64s").sum().starts_with("abc") - with pytest.raises(Exception) as _: + with pytest.raises(Exception, match="string operator requires a Str property"): graph.filter(expr) # ENDS_WITH on AVG expr = filter.Edge.property("p_u64s").avg().ends_with("abc") - with pytest.raises(Exception) as _: + with pytest.raises(Exception, match="string operator requires a Str property"): graph.filter(expr) # is_none/is_some after an aggregation are meaningful: the aggregate @@ -1056,12 +1056,12 @@ def check(graph): # CONTAINS on LEN expr = filter.Edge.property("p_u64s").len().contains("abc") - with pytest.raises(Exception) as _: + with pytest.raises(Exception, match="string operator requires a Str property"): graph.filter(expr) # NOT_CONTAINS on SUM expr = filter.Edge.property("p_u64s").sum().not_contains("abc") - with pytest.raises(Exception) as _: + with pytest.raises(Exception, match="string operator requires a Str property"): graph.filter(expr) return check diff --git a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py index 3c39b8f023..b6795d110c 100644 --- a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py +++ b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py @@ -65,7 +65,7 @@ def _kind(name): return "view" if name in VIEWS else ("node" if name in NODE_KIND else "edge") -def _and_is_broken(a, b): +def _view_applies_first_under_and(a, b): # `view & X` is not set algebra: the view applies first and `X` runs inside it # (`test_a_view_applies_first_under_and`), so it has no set-derived expectation here. return a in VIEWS or b in VIEWS @@ -223,14 +223,14 @@ def check(graph): @with_variants(_init) -def test_working_combinations_follow_set_algebra(): +def test_combinations_follow_set_algebra(): def check(graph): atoms, single = _atoms(), _singles(graph) negated = _negations(graph, single) every = _ids(graph.edges) cases = [] for a, b in combinations(atoms, 2): - if not _and_is_broken(a, b): + if not _view_applies_first_under_and(a, b): cases.append((f"{a} & {b}", atoms[a] & atoms[b], single[a] & single[b])) if _or_is_refused(a, b): continue diff --git a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py index fd469e4ded..627ba09c4f 100644 --- a/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py @@ -1,6 +1,8 @@ from raphtory import Graph, PersistentGraph from raphtory import EventTime from raphtory import filter +import re + import pytest from datetime import datetime @@ -577,11 +579,9 @@ def test_all_property_types(GraphClass): ), ] - for i, (make_expr, message) in enumerate(nonsense_filter_cases): - with pytest.raises(Exception) as e: - print(len(g.filter(make_expr()).edges.explode())) - print(e.value) - assert message in str(e.value) + for make_expr, message in nonsense_filter_cases: + with pytest.raises(Exception, match=re.escape(message)): + g.filter(make_expr()) # A string constant never compares against a numeric property, whether or # not it happens to parse as a number. @@ -671,11 +671,11 @@ def test_all_property_types(GraphClass): ( lambda: filter.ExplodedEdge.property("active").contains(2), "cannot be compared with Str", - ), # should fail on contains not type + ), # a string constant never compares with a non-string property ( lambda: filter.ExplodedEdge.property("active").not_contains(3), "cannot be compared with Str", - ), # should fail on contains not type + ), # a string constant never compares with a non-string property # # Datetimes (created) ( lambda: filter.ExplodedEdge.property("created") == 2, @@ -704,11 +704,11 @@ def test_all_property_types(GraphClass): ( lambda: filter.ExplodedEdge.property("created").contains(2), "cannot be compared with Str", - ), # should fail on contains not type + ), # a string constant never compares with a non-string property ( lambda: filter.ExplodedEdge.property("created").not_contains(3), "cannot be compared with Str", - ), # should fail on contains not type + ), # a string constant never compares with a non-string property # # Lists (tags) ( lambda: filter.ExplodedEdge.property("tags") == 2, @@ -737,11 +737,11 @@ def test_all_property_types(GraphClass): ( lambda: filter.ExplodedEdge.property("tags").contains(2), "cannot be compared with Str", - ), # should fail on contains not type + ), # a string constant never compares with a non-string property ( lambda: filter.ExplodedEdge.property("tags").not_contains(3), "cannot be compared with Str", - ), # should fail on contains not type + ), # a string constant never compares with a non-string property # # Dicts (meta) ( lambda: filter.ExplodedEdge.property("meta") == 2, @@ -770,11 +770,11 @@ def test_all_property_types(GraphClass): ( lambda: filter.ExplodedEdge.property("meta").contains(2), "cannot be compared with Str", - ), # should fail on contains not type + ), # a string constant never compares with a non-string property ( lambda: filter.ExplodedEdge.property("meta").not_contains(3), "cannot be compared with Str", - ), # should fail on contains not type + ), # a string constant never compares with a non-string property ] for i, (make_expr, message) in enumerate(wrong_types): diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index 8a96d5aad2..d5bd01dade 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -947,8 +947,8 @@ def test_filter_nodes_for_node_name_all_is_invalid(): def check(graph): # The expression builds (the python surface is one Expr type); applying # it rejects the qualifier on a scalar field. - with pytest.raises(Exception, match=r"cannot be compared with Str"): - filter_expr = (filter.Node.name() == True).all() + filter_expr = (filter.Node.name() == "N1").all() + with pytest.raises(Exception, match=r"any\(\)/all\(\) collapse"): graph.filter(filter_expr).nodes.id return check diff --git a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index 8b5c9d51f0..c692a875ec 100644 --- a/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py +++ b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py @@ -1065,9 +1065,6 @@ def test_getitem_narrows_membership_where_filter_rescopes(filter_pair): "reject.unknown_property": lambda: f.Node.property("nope") > 1, "reject.unknown_metadata": lambda: f.Node.metadata("nope") > 1, "reject.degree_vs_str": lambda: f.Node.degree() > "x", - # A view applies to the whole filter: it composes with `&` only (#2718 decided). - "reject.view_or": lambda: f.Graph.at(3) | f.Graph.at(5), - "reject.view_not": lambda: ~f.Graph.layer("knows"), # `avg` is F64 and `len` is U64, so neither accepts a plain Python int here. } diff --git a/raphtory-tests/tests/filter_tests/edge_expr_filter.rs b/raphtory-tests/tests/filter_tests/edge_expr_filter.rs index 9d5f498fe3..bf7cdfa4ef 100644 --- a/raphtory-tests/tests/filter_tests/edge_expr_filter.rs +++ b/raphtory-tests/tests/filter_tests/edge_expr_filter.rs @@ -158,7 +158,7 @@ fn test_edge_temporal_len_combined_with_and() { } // ───────────────────────────────────────────────────────────────────────────── -// String ops via EdgeExprFilterOps (generic) and EdgeAggregated convenience +// String ops on aggregated reads // ───────────────────────────────────────────────────────────────────────────── fn band_graph() -> Graph { @@ -277,12 +277,12 @@ fn test_edge_aggregated_first_starts_with_str_convenience() { // ───────────────────────────────────────────────────────────────────────────── // Set ops — PropValueSetExpr (linear scan, Option) and -// SetEdgeFilter (HashSet, Option) +// set membership // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_edge_property_is_in_prop_values() { - // Path A: EdgePropertyExprOps::is_in — PropValueSetExpr + // is_in on the property read let g = band_graph(); let filter = EdgeFilter .property("band") @@ -303,7 +303,7 @@ fn test_edge_property_is_not_in_prop_values() { #[test] fn test_edge_aggregated_last_is_in_prop_values() { - // Path A via EdgeAggregated convenience + // is_in on the aggregated read let g = Graph::new(); g.add_edge(1, "A", "B", [("tag", Prop::str("rock"))], None) .unwrap(); @@ -342,7 +342,7 @@ fn test_edge_aggregated_last_is_not_in_prop_values() { } // ───────────────────────────────────────────────────────────────────────────── -// Step 4: EdgeQuantified string ops (any/all + contains/starts_with/ends_with) +// Quantified string ops (any/all after contains/starts_with/ends_with) // ───────────────────────────────────────────────────────────────────────────── fn genre_graph() -> Graph { @@ -453,7 +453,7 @@ fn test_edge_quantified_all_starts_with() { } // ───────────────────────────────────────────────────────────────────────────── -// Step 4: EdgeQuantified set ops (any/all + is_in/is_not_in) +// Quantified set ops (any/all after is_in/is_not_in) // ───────────────────────────────────────────────────────────────────────────── #[test] @@ -532,7 +532,7 @@ fn test_edge_quantified_all_is_not_in() { } // ───────────────────────────────────────────────────────────────────────────── -// Step 5: Re-aggregation chains on EdgeAggregated +// Re-aggregation chains on aggregated reads // ───────────────────────────────────────────────────────────────────────────── #[test] @@ -627,9 +627,11 @@ fn test_edge_aggregated_first_then_ends_with() { #[test] fn test_edge_aggregated_last_then_len() { // Property is list-valued at each timestamp. - // .last() picks the last snapshot's list, .len() returns its length. + // .latest() picks the latest update's list, .len() returns its length. + // A->B has three updates, so counting updates (what .last().len() + // would do) gives 3, not 2. let g = Graph::new(); - // A->B: last snapshot = [20, 30], len = 2 + // A->B: latest update = [20, 30], len = 2 g.add_edge( 1, "A", @@ -642,6 +644,14 @@ fn test_edge_aggregated_last_then_len() { 2, "A", "B", + [("score", Prop::List(vec![Prop::I64(15)].into()))], + None, + ) + .unwrap(); + g.add_edge( + 3, + "A", + "B", [( "score", Prop::List(vec![Prop::I64(20), Prop::I64(30)].into()), @@ -649,7 +659,7 @@ fn test_edge_aggregated_last_then_len() { None, ) .unwrap(); - // C->D: last snapshot = [5, 10, 15], len = 3 + // C->D: latest (and only) update = [5, 10, 15], len = 3 g.add_edge( 1, "C", @@ -665,7 +675,7 @@ fn test_edge_aggregated_last_then_len() { let filter = EdgeFilter .property("score") .temporal() - .last() + .latest() .len() .eq(2usize); let result = g.filter(filter).unwrap(); @@ -675,9 +685,11 @@ fn test_edge_aggregated_last_then_len() { #[test] fn test_edge_aggregated_last_then_any_is_in() { // Property is list-valued at each timestamp. - // .last() picks the last snapshot's list, .is_in([...]).any() checks if any element is in the set. + // .latest() picks the latest update's list, .is_in([...]).any() checks if any element is in the set. + // "metal" is not the last element of its list, so the last element of + // each update (what .last() would give) never matches. let g = Graph::new(); - // A->B: last snapshot = ["folk","metal"] — "metal" ∈ {"metal","blues"} + // A->B: latest update = ["metal","folk"] — "metal" ∈ {"metal","blues"} g.add_edge( 1, "A", @@ -695,12 +707,12 @@ fn test_edge_aggregated_last_then_any_is_in() { "B", [( "tag", - Prop::List(vec![Prop::str("folk"), Prop::str("metal")].into()), + Prop::List(vec![Prop::str("metal"), Prop::str("folk")].into()), )], None, ) .unwrap(); - // C->D: last (and only) snapshot = ["jazz","pop"] — neither in {"metal","blues"} + // C->D: latest (and only) update = ["jazz","pop"] — neither in {"metal","blues"} g.add_edge( 1, "C", @@ -716,7 +728,7 @@ fn test_edge_aggregated_last_then_any_is_in() { let filter = EdgeFilter .property("tag") .temporal() - .last() + .latest() .is_in([Prop::str("metal"), Prop::str("blues")]) .any(); let result = g.filter(filter).unwrap(); @@ -791,7 +803,7 @@ fn test_edge_aggregated_last_then_is_in() { } // ───────────────────────────────────────────────────────────────────────────── -// Gap 1: EdgePropertyExprOps &str convenience methods (no Prop:: wrapper) +// &str constants without a Prop:: wrapper // ───────────────────────────────────────────────────────────────────────────── #[test] @@ -827,7 +839,7 @@ fn test_edge_property_not_contains_str_literal() { } // ───────────────────────────────────────────────────────────────────────────── -// Gap 2: is_true / is_false on EdgePropertyExprOps +// Bool comparisons // ───────────────────────────────────────────────────────────────────────────── fn active_graph() -> Graph { @@ -858,7 +870,7 @@ fn test_edge_property_is_false() { } // ───────────────────────────────────────────────────────────────────────────── -// Gap 3: EdgeQuantified re-aggregation chains (.any().sum(), .all().min(), etc.) +// Re-aggregation chains under any()/all() // ───────────────────────────────────────────────────────────────────────────── #[test] @@ -1013,7 +1025,9 @@ fn test_edge_quantified_any_any_contains() { #[test] fn test_edge_quantified_any_last_is_in() { let g = Graph::new(); - // A->B: last snapshot (t=2) has "metal" as an element + // A->B: the latest update (t=2) has "metal" as an element, not as its + // last element, so the last element of each update (what .last() would + // give) never matches. g.add_edge( 1, "A", @@ -1031,12 +1045,12 @@ fn test_edge_quantified_any_last_is_in() { "B", [( "tag", - Prop::List(vec![Prop::str("pop"), Prop::str("metal")].into()), + Prop::List(vec![Prop::str("metal"), Prop::str("pop")].into()), )], None, ) .unwrap(); - // C->D: last (and only) snapshot has no element in {"metal"} + // C->D: the latest (and only) update has no element in {"metal"} g.add_edge( 1, "C", @@ -1049,11 +1063,11 @@ fn test_edge_quantified_any_last_is_in() { ) .unwrap(); - // last temporal snapshot's list — any element is in {"metal"} + // the latest update's list — any element is in {"metal"} let filter = EdgeFilter .property("tag") .temporal() - .last() + .latest() .is_in([Prop::str("metal")]) .any(); let result = g.filter(filter).unwrap(); diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs index dedbb3da05..6d449f1274 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -300,7 +300,6 @@ mod test_property_semantics { #[test] fn test_property_semantics() { - // TODO: Const properties not supported for disk_graph. let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( @@ -612,8 +611,6 @@ mod test_property_semantics { #[test] fn test_metadata_semantics() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - // TODO: Const properties not supported for disk_graph. let filter = EdgeFilter.metadata("p1").eq(1u64); let expected_results = vec![ "N1->N2", "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N14->N15", "N15->N1", @@ -680,7 +677,6 @@ mod test_property_semantics { #[test] fn test_temporal_any_semantics() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p1").temporal().eq(1u64).any(); let expected_results = vec![ "N1->N2", "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", "N7->N8", "N8->N9", @@ -696,7 +692,6 @@ mod test_property_semantics { #[test] fn test_temporal_any_semantics_for_event_ids() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p1").temporal().lt(2u64).any(); let expected_results = vec![ "N1->N2", "N16->N15", "N17->N16", "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", @@ -713,7 +708,6 @@ mod test_property_semantics { #[test] fn test_temporal_latest_semantics() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( @@ -727,7 +721,6 @@ mod test_property_semantics { #[test] fn test_temporal_latest_semantics_for_event_ids() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1->N2", "N16->N15", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; @@ -742,7 +735,6 @@ mod test_property_semantics { #[test] fn test_property_semantics() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p1").ge(2u64); let expected_results = vec![ "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N2->N3", "N5->N6", "N8->N9", @@ -759,8 +751,6 @@ mod test_property_semantics { #[test] fn test_property_semantics_for_event_ids() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - // TODO: Const properties not supported for disk_graph. let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N16->N15", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; @@ -824,7 +814,6 @@ mod test_property_semantics { #[test] fn test_property_semantics_only_temporal() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. // For this graph there won't be any metadata index for property name "p1". fn init_graph< G: StaticGraphViewOps @@ -1012,6 +1001,154 @@ fn init_nodes_graph< graph } +fn init_nodes_layers_graph< + G: StaticGraphViewOps + + AdditionOps + + InternalAdditionOps + + InternalPropertyAdditionOps + + PropertyAdditionOps, +>( + graph: G, +) -> G { + let nodes = [ + ( + 1, + "1", + vec![ + ("p1", "shivam_kapoor".into_prop()), + ("p9", 5u64.into_prop()), + ("p10", "Paper_airplane".into_prop()), + ("p20", "Gold_ship".into_prop()), + ("p30", "Gold_ship".into_prop()), + ("p40", 5u64.into_prop()), + ], + Some("fire_nation"), + ), + ( + 2, + "2", + vec![ + ("p1", "prop12".into_prop()), + ("p2", 2u64.into_prop()), + ("p10", "Paper_ship".into_prop()), + ("p20", "Gold_boat".into_prop()), + ("p30", "Old_boat".into_prop()), + ("p40", 10u64.into_prop()), + ], + Some("air_nomads"), + ), + ( + 3, + "2", + vec![ + ("p20", "Gold_ship".into_prop()), + ("p30", "Gold_ship".into_prop()), + ("p40", 15u64.into_prop()), + ], + Some("air_nomads"), + ), + ( + 4, + "2", + vec![ + ("p20", "Gold_ship".into_prop()), + ("p30", "Gold_ship".into_prop()), + ("p40", 20u64.into_prop()), + ], + Some("air_nomads"), + ), + ( + 3, + "1", + vec![ + ("p1", "shivam_kapoor".into_prop()), + ("p9", 5u64.into_prop()), + ("p20", "Gold_ship".into_prop()), + ("p30", "Gold_ship".into_prop()), + ("p40", 10u64.into_prop()), + ], + Some("fire_nation"), + ), + ( + 3, + "3", + vec![ + ("p2", 6u64.into_prop()), + ("p3", 1u64.into_prop()), + ("p10", "Paper_airplane".into_prop()), + ], + Some("fire_nation"), + ), + ( + 4, + "1", + vec![ + ("p1", "shivam_kapoor".into_prop()), + ("p9", 5u64.into_prop()), + ("p20", "Gold_ship".into_prop()), + ("p30", "Gold_ship".into_prop()), + ("p40", 15u64.into_prop()), + ], + Some("fire_nation"), + ), + ( + 3, + "4", + vec![ + ("p4", "pometry".into_prop()), + ("p20", "Gold_ship".into_prop()), + ("p30", "Gold_ship".into_prop()), + ], + None, + ), + ( + 4, + "4", + vec![ + ("p5", 12u64.into_prop()), + ("p20", "Gold_boat".into_prop()), + ("p30", "Old_ship".into_prop()), + ], + None, + ), + ]; + + for (time, id, props, node_type) in nodes { + graph.add_node(time, id, props, None, node_type).unwrap(); + } + + let metadata = [ + ( + "1", + vec![ + ("m1", "pometry".into_prop()), + ("m2", "raphtory".into_prop()), + ], + ), + ("2", vec![("m1", "raphtory".into_prop())]), + ( + "3", + vec![ + ("m2", "pometry".into_prop()), + ("m3", "raphtory".into_prop()), + ], + ), + ( + "4", + vec![ + ("m3", "pometry".into_prop()), + ("m4", "raphtory".into_prop()), + ], + ), + ]; + + for (node_id, md) in metadata { + graph.node(node_id).unwrap().add_metadata(md).unwrap(); + } + + graph +} + fn init_nodes_graph_with_num_ids< G: StaticGraphViewOps + AdditionOps @@ -2778,7 +2915,7 @@ mod test_node_filter { } mod test_node_property_filter { - use crate::{init_nodes_graph, IdentityGraphTransformer}; + use crate::{init_nodes_graph, init_nodes_layers_graph, IdentityGraphTransformer}; use raphtory::{ db::graph::views::filter::model::{ graph_filter::GraphFilter, node_filter::NodeFilter, windowed_filter::Windowed, @@ -4083,12 +4220,12 @@ mod test_node_property_filter { } #[test] - #[ignore] // TODO: Enable this when node layer is supported fn test_graph_filter_layer() { + // Note: Default layer is currently always included for nodes! let filter = GraphFilter.layer("fire_nation"); - let expected_results = vec!["1", "3"]; + let expected_results = vec!["1", "3", "4"]; assert_filter_nodes_results( - init_nodes_graph, + init_nodes_layers_graph, IdentityGraphTransformer, filter.clone(), &expected_results, @@ -4096,9 +4233,9 @@ mod test_node_property_filter { ); let filter = GraphFilter.layer("air_nomads"); - let expected_results = vec!["2"]; + let expected_results = vec!["2", "4"]; assert_filter_nodes_results( - init_nodes_graph, + init_nodes_layers_graph, IdentityGraphTransformer, filter.clone(), &expected_results, @@ -4107,22 +4244,21 @@ mod test_node_property_filter { } #[test] - #[ignore] // TODO: Enable this when node layer is supported fn test_graph_filter_window_then_layer() { let filter = GraphFilter.window(1, 3).layer("fire_nation"); - let expected_results = vec!["1", "3"]; + let expected_results = vec!["1"]; assert_filter_nodes_results( - init_nodes_graph, + init_nodes_layers_graph, IdentityGraphTransformer, filter.clone(), &expected_results, TestVariants::All, ); - let filter = GraphFilter.window(4, 4).layer("air_nomads"); + let filter = GraphFilter.window(2, 3).layer("air_nomads"); let expected_results = vec!["2"]; assert_filter_nodes_results( - init_nodes_graph, + init_nodes_layers_graph, IdentityGraphTransformer, filter.clone(), &expected_results, @@ -4131,12 +4267,21 @@ mod test_node_property_filter { } #[test] - #[ignore] // TODO: Enable this when node layer is supported fn test_graph_filter_layer_then_window() { let filter = GraphFilter.layer("fire_nation").window(1, 3); - let expected_results = vec!["1", "3"]; + let expected_results = vec!["1"]; assert_filter_nodes_results( - init_nodes_graph, + init_nodes_layers_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + let filter = GraphFilter.layer("air_nomads").window(2, 3); + let expected_results = vec!["2"]; + assert_filter_nodes_results( + init_nodes_layers_graph, IdentityGraphTransformer, filter.clone(), &expected_results, @@ -4303,7 +4448,7 @@ mod test_node_property_filter { } } -// TODO: delete when search is dropped and graphql composite path is gone +/// Typed `and`/`or`/`not` composition over node filters. mod composite_node_filter_tests { use raphtory_api::core::Direction; @@ -4565,10 +4710,6 @@ mod composite_node_filter_tests { // ↑ ↑ ↑ // ∃ leaf ∀ d2 ∀ d1/timestamp // -// This is the OPPOSITE of the old broken convention (.all().eq(x).any()) -// where quantifiers before the comparison received raw typed values and -// checked `elem == Bool(true)`, always returning false for non-Bool types. -// // temporal() semantics: iterates only timestamps where the property IS defined. // A node with the property absent at some timestamps is unaffected by those gaps. mod test_node_property_filter_agg { @@ -9116,7 +9257,6 @@ mod test_edge_property_filter { #[test] fn test_filter_edges_for_property_eq() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p2").eq(2u64); let expected_results = vec!["2->3"]; assert_filter_edges_results( @@ -9124,7 +9264,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p30").temporal().first().eq("Old_boat"); @@ -9134,7 +9274,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p20").temporal().eq("Gold_ship").all(); @@ -9144,13 +9284,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_ne() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p2").ne(2u64); let expected_results = vec![ "1->2", @@ -9164,7 +9303,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p30").temporal().first().ne("Old_boat"); @@ -9174,7 +9313,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p30").temporal().ne("Classic").all(); @@ -9184,13 +9323,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_lt() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p2").lt(10u64); let expected_results = vec![ "1->2", @@ -9205,7 +9343,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().first().lt(5u64); @@ -9215,7 +9353,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().lt(10u64).all(); @@ -9232,13 +9370,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_le() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p2").le(6u64); let expected_results = vec![ "1->2", @@ -9253,7 +9390,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().first().le(3u64); @@ -9263,7 +9400,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().le(5u64).all(); @@ -9273,13 +9410,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_gt() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p2").gt(2u64); let expected_results = vec![ "1->2", @@ -9293,7 +9429,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().first().gt(5u64); @@ -9308,7 +9444,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().gt(5u64).all(); @@ -9323,13 +9459,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_ge() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p2").ge(2u64); let expected_results = vec![ "1->2", @@ -9344,7 +9479,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().first().ge(6u64); @@ -9359,7 +9494,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().ge(6u64).all(); @@ -9374,13 +9509,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_in() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p2").is_in(vec![Prop::U64(6)]); let expected_results = vec![ "2->1", @@ -9393,7 +9527,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9411,7 +9545,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9430,7 +9564,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9449,13 +9583,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_not_in() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p2").is_not_in(vec![Prop::U64(6)]); let expected_results = vec!["1->2", "2->3"]; assert_filter_edges_results( @@ -9463,7 +9596,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9477,7 +9610,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9491,13 +9624,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_is_some() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p2").is_some(); let expected_results = vec![ "1->2", @@ -9512,7 +9644,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().first().is_some(); @@ -9529,13 +9661,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_is_none() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. Search API uses filter API internally for this filter. let filter = EdgeFilter.property("p2").is_none(); let expected_results = Vec::<&str>::new(); assert_filter_edges_results( @@ -9543,7 +9674,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p2").temporal().first().is_none(); @@ -9553,13 +9684,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_starts_with() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p10").starts_with("Pa"); let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; assert_filter_edges_results( @@ -9567,7 +9697,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9581,7 +9711,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9595,7 +9725,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9609,7 +9739,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9623,7 +9753,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9637,13 +9767,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_ends_with() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p10").ends_with("lane"); let expected_results: Vec<&str> = vec!["1->2", "2->1"]; assert_filter_edges_results( @@ -9651,7 +9780,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9665,7 +9794,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9679,7 +9808,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9693,7 +9822,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9707,7 +9836,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9721,13 +9850,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_contains() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p10").contains("Paper"); let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; assert_filter_edges_results( @@ -9735,7 +9863,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9749,7 +9877,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9763,7 +9891,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9777,7 +9905,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter.property("p20").temporal().contains("ship").all(); @@ -9787,13 +9915,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_for_property_contains_not() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter.property("p10").not_contains("ship"); let expected_results: Vec<&str> = vec!["1->2", "2->1"]; assert_filter_edges_results( @@ -9801,7 +9928,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9815,7 +9942,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9829,7 +9956,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9843,7 +9970,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter @@ -9857,13 +9984,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_filter_edges_by_fuzzy_search() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. // TODO: Enable these test for event_disk_graph, persistent_disk_graph once string property is fixed. let filter = EdgeFilter.property("p1").fuzzy_search("shiv", 2, true); let expected_results: Vec<&str> = vec!["1->2"]; @@ -9872,7 +9998,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); let filter = EdgeFilter.property("p1").fuzzy_search("ShiV", 2, true); @@ -9882,7 +10008,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); let filter = EdgeFilter.property("p1").fuzzy_search("shiv", 2, false); @@ -9892,13 +10018,12 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); } #[test] fn test_filter_edges_for_not_property() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. Search API uses filter API internally for this filter. let filter = EdgeFilter.property("p2").ne(2u64).not(); let expected_results = vec!["2->3"]; assert_filter_edges_results( @@ -9906,7 +10031,7 @@ mod test_edge_property_filter { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } @@ -10406,7 +10531,7 @@ mod test_edge_property_filter { } } -// TODO: delete when search is dropped and graphql composite path is gone +/// Typed `and`/`or`/`not` composition over edge filters. mod composite_edge_filter_tests { use raphtory::{ db::graph::views::filter::model::{ @@ -10423,7 +10548,6 @@ mod composite_edge_filter_tests { #[test] fn test_filter_edge_for_src_dst() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter::src() .name() .eq("3") @@ -10434,13 +10558,12 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } #[test] fn test_unique_results_from_composite_filters() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. let filter = EdgeFilter .property("p2") .ge(2u64) @@ -10484,7 +10607,6 @@ mod composite_edge_filter_tests { #[test] fn test_composite_filter_edges() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. // TODO: Enable these test for event_disk_graph, persistent_disk_graph once string property is fixed. let filter = EdgeFilter .property("p2") @@ -10496,7 +10618,7 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); let filter = EdgeFilter @@ -10509,7 +10631,7 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); let filter = EdgeFilter.property("p1").eq("pometry").or(EdgeFilter @@ -10527,7 +10649,7 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); let filter = EdgeFilter::src() @@ -10540,7 +10662,7 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); let filter = EdgeFilter @@ -10553,7 +10675,7 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); let filter = EdgeFilter::src() @@ -10566,7 +10688,7 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); let filter = EdgeFilter::dst() @@ -10579,7 +10701,7 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = EdgeFilter::src() @@ -10593,13 +10715,12 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - vec![TestGraphVariants::Graph], + TestVariants::All, ); } #[test] fn test_not_composite_filter_edges() { - // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. Search API uses filter API internally for this filter. let filter = NotFilter( EdgeFilter::src() .name() @@ -10619,7 +10740,7 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); let filter = NotFilter( @@ -10641,7 +10762,7 @@ mod composite_edge_filter_tests { IdentityGraphTransformer, filter.clone(), &expected_results, - TestVariants::EventOnly, + TestVariants::All, ); } } From 60dc917d07025eba797c8e12fb9538133cce2ed1 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:26:31 +0100 Subject: [PATCH 151/155] decide view membership and erasure once, not per read An erased graph is passed through instead of boxed again at every level of a compiled filter; a node read under a view checks the node's membership once when the op is built; and the per-edge form of a node predicate reports exploded filtering only when its view restricts layers, instances or time. --- .../tests/{ => filter_tests}/test_filters.rs | 0 .../db/api/storage/graph/storage_ops/mod.rs | 7 +- raphtory/src/db/api/storage/storage.rs | 6 +- raphtory/src/db/api/view/history.rs | 10 +- .../src/db/api/view/internal/into_dynamic.rs | 50 +++++++- raphtory/src/db/api/view/internal/mod.rs | 8 +- .../graph/views/filter/model/edge_expr/ops.rs | 115 +++++++++++++++++- .../graph/views/filter/model/expr/compile.rs | 91 +++++++++++++- .../graph/views/filter/model/expr/stream.rs | 11 +- .../src/db/graph/views/filter/model/mod.rs | 62 ++++++++-- .../views/filter/model/node_expr/dyn_expr.rs | 4 +- .../views/filter/model/node_expr/exprs.rs | 13 +- .../graph/views/filter/model/node_expr/ops.rs | 43 +++++-- .../views/filter/model/node_expr/tests.rs | 61 +++++++++- .../db/graph/views/filter/model/not_filter.rs | 4 +- .../filter/model/property_filter/evaluate.rs | 5 +- raphtory/src/python/filter/mod.rs | 15 +-- 17 files changed, 442 insertions(+), 63 deletions(-) rename raphtory-tests/tests/{ => filter_tests}/test_filters.rs (100%) diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/filter_tests/test_filters.rs similarity index 100% rename from raphtory-tests/tests/test_filters.rs rename to raphtory-tests/tests/filter_tests/test_filters.rs diff --git a/raphtory/src/db/api/storage/graph/storage_ops/mod.rs b/raphtory/src/db/api/storage/graph/storage_ops/mod.rs index fff4197fa5..e99d4c0d8e 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/mod.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/mod.rs @@ -1,4 +1,7 @@ -use crate::db::api::{storage::storage::Storage, view::internal::InternalStorageOps}; +use crate::db::api::{ + storage::storage::Storage, + view::internal::{InternalStorageOps, Static}, +}; use raphtory_storage::graph::graph::GraphStorage; use std::path::Path; @@ -11,6 +14,8 @@ pub mod property_schema; pub mod time_props; pub mod time_semantics; +impl Static for GraphStorage {} + impl InternalStorageOps for GraphStorage { fn get_storage(&self) -> Option<&Storage> { None diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index a30f590c5a..2ad6f15300 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -1,7 +1,9 @@ use crate::{ core::entities::nodes::node_ref::NodeRef, db::api::view::{ - internal::{InheritEdgeHistoryFilter, InheritNodeHistoryFilter, InternalStorageOps}, + internal::{ + InheritEdgeHistoryFilter, InheritNodeHistoryFilter, InternalStorageOps, Static, + }, Base, InheritViewOps, }, errors::{into_graph_err, GraphError}, @@ -207,6 +209,8 @@ impl InheritEdgeHistoryFilter for Storage {} impl InheritViewOps for Storage {} +impl Static for Storage {} + #[derive(Clone)] pub struct StorageWriteSession<'a> { session: UnlockedSession<'a>, diff --git a/raphtory/src/db/api/view/history.rs b/raphtory/src/db/api/view/history.rs index 4ea68c1306..ecc268d27d 100644 --- a/raphtory/src/db/api/view/history.rs +++ b/raphtory/src/db/api/view/history.rs @@ -12,8 +12,8 @@ use crate::{ LazyNodeState, }, view::{ - internal::{EdgeTimeSemanticsOps, NodeTimeSemanticsOps}, - BaseNodeViewOps, BoxableGraphView, BoxedLIter, IntoDynBoxed, + internal::{EdgeTimeSemanticsOps, GraphView, NodeTimeSemanticsOps}, + BaseNodeViewOps, BoxedLIter, IntoDynBoxed, }, }, graph::{ @@ -521,7 +521,7 @@ impl<'graph, G: GraphViewOps<'graph> + Send + Sync + Send + Sync> InternalHistor impl + Send + Sync> IntoArcDynHistoryOps for NodeView<'static, G> {} -impl InternalHistoryOps for EdgeView { +impl InternalHistoryOps for EdgeView { fn iter(&self) -> BoxedLIter<'_, EventTime> { let g = &self.graph; let e = self.edge; @@ -622,7 +622,7 @@ impl InternalHistoryOps for EdgeView { } } -impl IntoArcDynHistoryOps for EdgeView {} +impl IntoArcDynHistoryOps for EdgeView {} impl< 'graph, @@ -1114,7 +1114,7 @@ impl InternalHistoryOps for DeletionHistory { impl IntoArcDynHistoryOps for DeletionHistory {} -impl InternalDeletionOps for EdgeView { +impl InternalDeletionOps for EdgeView { fn iter(&self) -> BoxedLIter<'_, EventTime> { let g = &self.graph; let e = self.edge; diff --git a/raphtory/src/db/api/view/internal/into_dynamic.rs b/raphtory/src/db/api/view/internal/into_dynamic.rs index f54c6d4604..e784a50e26 100644 --- a/raphtory/src/db/api/view/internal/into_dynamic.rs +++ b/raphtory/src/db/api/view/internal/into_dynamic.rs @@ -1,5 +1,5 @@ use crate::db::api::view::{ - internal::{DynamicGraph, InternalFilter, Static}, + internal::{DynGraphArc, DynamicGraph, InternalFilter, Static}, BoxableGraphView, StaticGraphViewOps, }; use std::sync::Arc; @@ -26,6 +26,54 @@ impl IntoDynamic for Arc { } } +/// Erase a graph view into a `DynGraphArc`. A view that is already an erased +/// `Arc` is handed back as it is instead of being boxed a second time. +pub trait IntoDynGraphArc { + fn into_dyn_graph_arc<'graph>(self) -> DynGraphArc<'graph> + where + Self: 'graph; +} + +impl IntoDynGraphArc for G { + #[inline] + fn into_dyn_graph_arc<'graph>(self) -> DynGraphArc<'graph> + where + Self: 'graph, + { + Arc::new(self) + } +} + +impl<'a> IntoDynGraphArc for Arc { + #[inline] + fn into_dyn_graph_arc<'graph>(self) -> DynGraphArc<'graph> + where + Self: 'graph, + { + self + } +} + +impl IntoDynGraphArc for Arc { + #[inline] + fn into_dyn_graph_arc<'graph>(self) -> DynGraphArc<'graph> + where + Self: 'graph, + { + self + } +} + +impl IntoDynGraphArc for DynamicGraph { + #[inline] + fn into_dyn_graph_arc<'graph>(self) -> DynGraphArc<'graph> + where + Self: 'graph, + { + self.0 + } +} + pub trait IntoDynHop: InternalFilter<'static, Graph: IntoDynamic> { fn into_dyn_hop(self) -> Self::Filtered; } diff --git a/raphtory/src/db/api/view/internal/mod.rs b/raphtory/src/db/api/view/internal/mod.rs index 891596b8ae..6eff10681d 100644 --- a/raphtory/src/db/api/view/internal/mod.rs +++ b/raphtory/src/db/api/view/internal/mod.rs @@ -31,7 +31,7 @@ mod wrapped_graph; pub use edge_filter_ops::*; pub use filter::*; pub use filter_ops::*; -pub use into_dynamic::{IntoDynHop, IntoDynamic}; +pub use into_dynamic::{IntoDynGraphArc, IntoDynHop, IntoDynamic}; pub use list_ops::*; pub use materialize::*; pub use node_filter_ops::*; @@ -86,9 +86,9 @@ impl< { } -pub trait GraphView: BoxableGraphView + Sized + Clone {} +pub trait GraphView: BoxableGraphView + Sized + Clone + IntoDynGraphArc {} -impl GraphView for T {} +impl GraphView for T {} impl InheritNodeFilterOps for G {} @@ -232,6 +232,8 @@ impl InheritEdgeHistoryFilter for DynamicGraph {} impl<'graph1, 'graph2: 'graph1, G: GraphView + 'graph2> InheritViewOps for &'graph1 G {} +impl Static for &G {} + impl<'graph1, 'graph2: 'graph1, G: GraphView + 'graph2> InheritStorageOps for &'graph1 G {} impl<'graph1, 'graph2: 'graph1, G: GraphView + 'graph2> InheritNodeHistoryFilter for &'graph1 G {} diff --git a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs index ee179affcc..574f572eb6 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -5,7 +5,7 @@ use crate::db::{ api::{ state::ops::Const, - view::internal::{FilterOps, GraphView}, + view::internal::{FilterOps, GraphView, InnerFilterOps}, }, graph::edge_reads::{self, EdgeAt}, }; @@ -527,8 +527,14 @@ impl EdgeOp for EdgeExistsOp { PropType::Bool } + /// Layers and exploded instances of an edge can only fall out of `graph` on + /// their own when it restricts layers, filters layers or exploded instances, + /// or has a window; otherwise the per-edge answer holds for all of them. fn filters_exploded(&self) -> bool { - true + self.graph.is_layer_filtered() + || self.graph.internal_edge_layer_filtered() + || self.graph.internal_exploded_edge_filtered() + || self.graph.window_filtered() } } @@ -621,3 +627,108 @@ impl, R: EdgeOp> EdgeOp for OrEdgeOp Graph { + let g = Graph::new(); + g.add_edge(1, "a", "b", NO_PROPS, Some("x")).unwrap(); + g.add_edge(7, "a", "b", NO_PROPS, Some("y")).unwrap(); + g.add_edge(2, "b", "c", NO_PROPS, Some("x")).unwrap(); + g + } + + #[test] + fn plain_node_predicate_is_decided_per_edge() { + let g = graph(); + let op = NodeFilter + .name() + .ne("c") + .create_edge_filter(g.clone()) + .unwrap(); + assert!(!op.filters_exploded()); + + let view = EdgeExprFilteredGraph::new(g.clone(), op); + assert!(!view.internal_exploded_edge_filtered()); + assert!(!view.internal_edge_layer_filtered()); + assert!(view.internal_exploded_filter_edge_list_trusted()); + assert!(view.internal_layer_filter_edge_list_trusted()); + + let selected = g + .edges() + .select(NodeFilter.name().ne("c")) + .unwrap() + .explode() + .iter() + .map(|e| (e.src().name(), e.dst().name(), e.time().unwrap().t())) + .collect::>(); + assert_eq!( + selected, + vec![ + ("a".to_string(), "b".to_string(), 1), + ("a".to_string(), "b".to_string(), 7) + ] + ); + } + + #[test] + fn windowed_or_layered_view_is_asked_per_instance() { + let g = graph(); + let windowed = GraphFilter + .window(0, 5) + .create_edge_filter(g.clone()) + .unwrap(); + assert!(windowed.filters_exploded()); + let view = EdgeExprFilteredGraph::new(g.clone(), windowed); + assert!(view.internal_exploded_edge_filtered()); + assert!(!view.internal_exploded_filter_edge_list_trusted()); + + let layered = NodeFilter + .name() + .eq("a") + .create_edge_filter(g.layers("x").unwrap()) + .unwrap(); + assert!(layered.filters_exploded()); + + let selected = g + .edges() + .select(GraphFilter.window(0, 5)) + .unwrap() + .explode() + .iter() + .map(|e| (e.src().name(), e.dst().name(), e.time().unwrap().t())) + .collect::>(); + assert_eq!( + selected, + vec![ + ("a".to_string(), "b".to_string(), 1), + ("b".to_string(), "c".to_string(), 2) + ] + ); + } +} diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index 233ab32460..d5777ffad5 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -1293,7 +1293,9 @@ impl CreateOp for StreamedAggExpr { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let history = self.history.create_node_history(Arc::new(graph))?; + let history = self + .history + .create_node_history(graph.into_dyn_graph_arc())?; self.check(&history.history_type())?; Ok(Arc::new(StreamedAggNodeOp::new(history, self.agg))) } @@ -1302,7 +1304,9 @@ impl CreateOp for StreamedAggExpr { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let history = self.history.create_edge_history(Arc::new(graph))?; + let history = self + .history + .create_edge_history(graph.into_dyn_graph_arc())?; self.check(&history.history_type())?; Ok(Arc::new(StreamedAggEdgeOp::new(history, self.agg))) } @@ -1367,7 +1371,9 @@ impl CreateOp for StreamedQualExpr { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let history = self.history.create_node_history(Arc::new(graph.clone()))?; + let history = self + .history + .create_node_history(graph.clone().into_dyn_graph_arc())?; match self.test.value_test(&history.history_type()) { Some(test) => Ok(Arc::new(StreamedQualNodeOp { history, @@ -1382,7 +1388,9 @@ impl CreateOp for StreamedQualExpr { &self, graph: G, ) -> Result> + 'g>, GraphError> { - let history = self.history.create_edge_history(Arc::new(graph.clone()))?; + let history = self + .history + .create_edge_history(graph.clone().into_dyn_graph_arc())?; match self.test.value_test(&history.history_type()) { Some(test) => Ok(Arc::new(StreamedQualEdgeOp { history, @@ -2103,7 +2111,7 @@ impl Viewed { &self, graph: G, ) -> Result, GraphError> { - compile_view(&self.views).create_dyn_graph_filter(Arc::new(graph)) + compile_view(&self.views).create_dyn_graph_filter(graph.into_dyn_graph_arc()) } } @@ -2438,3 +2446,76 @@ mod pushdown_tests { assert!(pushdown(&both_sides).is_none()); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + db::api::view::internal::CoreGraphOps, + prelude::{AdditionOps, Graph}, + }; + use std::sync::Mutex; + + /// A leaf that records the address of the erased graph it is compiled against. + #[derive(Clone, Default)] + struct GraphProbe { + seen: Arc>>, + } + + // Only addresses are stored, never dereferenced. + unsafe impl Send for GraphProbe {} + unsafe impl Sync for GraphProbe {} + + impl EntityExpr for GraphProbe { + type Marker = EntityMarker; + + fn entity(&self) -> EntityMarker { + EntityMarker::Node + } + } + + impl CreateOp for GraphProbe { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let erased = graph.clone().into_dyn_graph_arc(); + self.seen + .lock() + .unwrap() + .push(Arc::as_ptr(&erased) as *const ()); + Prop::I64(2).create_node_op(graph) + } + } + + /// `leaf > 1` as a node predicate: the erased predicate, the erased comparison + /// and the erased leaf each hand the graph on; the leaf must receive the very + /// `Arc` the caller passed in, not a fresh box around it per level. + #[test] + fn erased_levels_share_one_graph_arc() { + let g = Graph::new(); + g.add_node(0, "n", [("a", Prop::I64(2))], None, None) + .unwrap(); + let probe = GraphProbe::default(); + let cmp: Arc = Arc::new(CmpExpr { + op: BinaryOp::Gt, + lhs: Arc::new(probe.clone()), + rhs: Arc::new(Prop::I64(1)), + entity: EntityMarker::Node, + }); + let predicate: Arc = Arc::new(Predicate { + entity: EntityMarker::Node, + inner: cmp, + pushdown: None, + }); + + let base: DynGraphArc<'static> = Arc::new(g.clone()); + let op = predicate.create_node_filter(base.clone()).unwrap(); + assert!(op.apply(g.core_graph(), VID(0))); + + assert_eq!( + *probe.seen.lock().unwrap(), + vec![Arc::as_ptr(&base) as *const ()] + ); + } +} diff --git a/raphtory/src/db/graph/views/filter/model/expr/stream.rs b/raphtory/src/db/graph/views/filter/model/expr/stream.rs index 7f3cc32034..68ad36ac2b 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/stream.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/stream.rs @@ -24,13 +24,14 @@ use crate::{ edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, edge_filter::{EdgeEndpointWrapper, Endpoint}, filter_operator::{BinaryOp, Comparable, StringComparable, StringOp}, - node_expr::ops::{agg_out_pt, fold_values, reduce_list, TemporalNodePropOp}, + node_expr::ops::{ + agg_out_pt, fold_values, reduce_list, view_node, TemporalNodePropOp, + }, property_filter::evaluate::aggregate_list_values, }, }, }, errors::GraphError, - prelude::GraphViewOps, }; use raphtory_api::core::{ entities::{ @@ -77,8 +78,7 @@ impl NodeHistory for TemporalNodePropOp { } fn values<'a>(&'a self, node: VID) -> BoxedLIter<'a, Prop> { - match (&&self.graph) - .node(node) + match view_node(&self.graph, self.in_view, node) .and_then(|n| n.properties().temporal().get_by_id(self.prop_id)) { Some(history) => GenLockedIter::from(history, |h| h.values()).into_dyn_boxed(), @@ -87,8 +87,7 @@ impl NodeHistory for TemporalNodePropOp { } fn values_rev<'a>(&'a self, node: VID) -> BoxedLIter<'a, Prop> { - match (&&self.graph) - .node(node) + match view_node(&self.graph, self.in_view, node) .and_then(|n| n.properties().temporal().get_by_id(self.prop_id)) { Some(history) => GenLockedIter::from(history, |h| h.values_rev()).into_dyn_boxed(), diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index be62bcba24..efeab7bf55 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -32,7 +32,10 @@ use crate::{ db::{ api::{ state::NodeOp, - view::{internal::DynGraphArc, BoxableGraphView}, + view::{ + internal::{DynGraphArc, IntoDynGraphArc}, + BoxableGraphView, + }, }, graph::views::{ filter::{ @@ -142,7 +145,10 @@ where &self, graph: DynGraphArc<'graph>, ) -> Result, GraphError> { - Ok(Arc::new(self.clone().create_graph_filter(graph)?)) + Ok(self + .clone() + .create_graph_filter(graph)? + .into_dyn_graph_arc()) } fn create_dyn_node_filter<'graph>( @@ -183,21 +189,24 @@ impl CreateFilter for Arc { self, graph: G, ) -> Result, GraphError> { - self.deref().create_dyn_graph_filter(Arc::new(graph)) + self.deref() + .create_dyn_graph_filter(graph.into_dyn_graph_arc()) } fn create_node_filter<'graph, G: GraphView + 'graph>( self, graph: G, ) -> Result, GraphError> { - self.deref().create_dyn_node_filter(Arc::new(graph)) + self.deref() + .create_dyn_node_filter(graph.into_dyn_graph_arc()) } fn create_edge_filter<'graph, G: GraphView + 'graph>( self, graph: G, ) -> Result, GraphError> { - self.deref().create_dyn_edge_filter(Arc::new(graph)) + self.deref() + .create_dyn_edge_filter(graph.into_dyn_graph_arc()) } } @@ -246,7 +255,11 @@ impl CreateOp for Pr .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 })) + Ok(Arc::new(NodePropOp { + graph, + prop_id, + in_view: self.view_expr.narrows(), + })) } fn create_edge_op<'g, G: GraphView + 'g>( @@ -272,7 +285,11 @@ impl CreateOp for Me .get_prop_id(&self.name, true) .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; let graph = self.view_expr.create_view(graph)?; - Ok(Arc::new(NodeMetaOp { graph, prop_id })) + Ok(Arc::new(NodeMetaOp { + graph, + prop_id, + in_view: self.view_expr.narrows(), + })) } fn create_edge_op<'g, G: GraphView + 'g>( @@ -487,6 +504,14 @@ pub trait CreateView: Clone + Send + Sync + 'static { &self, view: G, ) -> Result, GraphError>; + + /// Whether the view can hide an entity the incoming graph shows. A read + /// through a view that cannot is only ever asked about entities the + /// enclosing filter has already found in that graph, so it need not check + /// them again. + fn narrows(&self) -> bool { + true + } } pub trait DynCreateView: Send + Sync + 'static { @@ -494,6 +519,8 @@ pub trait DynCreateView: Send + Sync + 'static { &self, view: Arc, ) -> Result, GraphError>; + + fn dyn_narrows(&self) -> bool; } impl DynCreateView for T { @@ -501,7 +528,11 @@ impl DynCreateView for T { &self, view: Arc, ) -> Result, GraphError> { - Ok(Arc::new(self.create_view(view)?)) + Ok(self.create_view(view)?.into_dyn_graph_arc()) + } + + fn dyn_narrows(&self) -> bool { + self.narrows() } } @@ -512,7 +543,11 @@ impl CreateView for Arc { &self, view: G, ) -> Result, GraphError> { - self.deref().dyn_create_view(Arc::new(view)) + self.deref().dyn_create_view(view.into_dyn_graph_arc()) + } + + fn narrows(&self) -> bool { + self.deref().dyn_narrows() } } @@ -525,6 +560,9 @@ impl CreateView for NodeFilter { ) -> Result, GraphError> { Ok(view) } + fn narrows(&self) -> bool { + false + } } impl EntityExpr for NodeFilter { @@ -543,6 +581,9 @@ impl CreateView for EdgeFilter { ) -> Result, GraphError> { Ok(view) } + fn narrows(&self) -> bool { + false + } } impl EntityExpr for EdgeFilter { @@ -561,6 +602,9 @@ impl CreateView for ExplodedEdgeFilter { ) -> Result, GraphError> { Ok(view) } + fn narrows(&self) -> bool { + false + } } impl EntityExpr for ExplodedEdgeFilter { diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs index 01206c353e..160b001248 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -146,14 +146,14 @@ impl CreateOp for Arc { &self, graph: G, ) -> Result> + 'g>, GraphError> { - self.deref().dyn_create_node_op(Arc::new(graph)) + self.deref().dyn_create_node_op(graph.into_dyn_graph_arc()) } fn create_edge_op<'g, G: GraphView + 'g>( &self, graph: G, ) -> Result> + 'g>, GraphError> { - self.deref().dyn_create_edge_op(Arc::new(graph)) + self.deref().dyn_create_edge_op(graph.into_dyn_graph_arc()) } } 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 7b0fa18535..5cf4dbbdce 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 @@ -527,7 +527,11 @@ impl DynCreateHistor .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(TemporalNodePropOp { graph, prop_id })) + Ok(Arc::new(TemporalNodePropOp { + graph, + prop_id, + in_view: self.view_expr.narrows(), + })) } fn create_edge_history<'g>( @@ -554,7 +558,12 @@ impl CreateOp for Te .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; let graph = self.view_expr.create_view(graph)?; Ok(Arc::new( - TemporalNodePropOp { graph, prop_id }.map(|a| Some(a)), + TemporalNodePropOp { + graph, + prop_id, + in_view: self.view_expr.narrows(), + } + .map(|a| Some(a)), )) } 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 index 8437a1f13a..d3adabe87a 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -52,10 +52,13 @@ use crate::{ NodeViewOps, }, }, - graph::views::filter::model::{ - expr::Agg, - property_filter::evaluate::{ - aggregate_list_values, scan_f64_sum_count, scan_i64_sum, scan_u64_sum, + graph::{ + node::NodeView, + views::filter::model::{ + expr::Agg, + property_filter::evaluate::{ + aggregate_list_values, scan_f64_sum_count, scan_i64_sum, scan_u64_sum, + }, }, }, }, @@ -84,6 +87,25 @@ use storage::EdgeEntryRef; pub(crate) struct NodePropOp { pub(crate) graph: G, pub(crate) prop_id: usize, + /// Whether `graph` is a view of the read's own, see [`view_node`]. + pub(crate) in_view: bool, +} + +/// The node as `graph` sees it. When `graph` is the graph the enclosing filter +/// runs on, that filter has already decided the node belongs to it, so the node +/// is read as it is. When it is a view of the read's own (`in_view`), the node +/// may be missing from it, and a read of a missing node is `None`. +#[inline] +pub(crate) fn view_node( + graph: &G, + in_view: bool, + node: VID, +) -> Option> { + if in_view { + (&graph).node(node) + } else { + Some(NodeView::new_internal(graph, node)) + } } impl NodeOp for NodePropOp { @@ -94,8 +116,7 @@ impl NodeOp for NodePropOp { type Output = Option; fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { - (&&self.graph) - .node(node)? + view_node(&self.graph, self.in_view, node)? .properties() .get_by_id(self.prop_id) } @@ -121,6 +142,8 @@ impl NodeOp for NodePropOp { pub(crate) struct NodeMetaOp { pub(crate) graph: G, pub(crate) prop_id: usize, + /// Whether `graph` is a view of the read's own, see [`view_node`]. + pub(crate) in_view: bool, } impl NodeOp for NodeMetaOp { @@ -131,8 +154,7 @@ impl NodeOp for NodeMetaOp { type Output = Option; fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { - (&&self.graph) - .node(node)? + view_node(&self.graph, self.in_view, node)? .metadata() .get_by_id(self.prop_id) } @@ -191,6 +213,8 @@ impl NodeOp for NodeIdOp { pub(crate) struct TemporalNodePropOp { pub(crate) graph: G, pub(crate) prop_id: usize, + /// Whether `graph` is a view of the read's own, see [`view_node`]. + pub(crate) in_view: bool, } impl NodeOp for TemporalNodePropOp { @@ -209,8 +233,7 @@ impl NodeOp for TemporalNodePropOp { type Output = Prop; fn apply(&self, _storage: &GraphStorage, node: VID) -> Prop { - let vals: Vec = (&&self.graph) - .node(node) + let vals: Vec = view_node(&self.graph, self.in_view, node) .and_then(|n| { n.properties() .temporal() 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 bd5602aa21..d56ead2487 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 @@ -11,8 +11,8 @@ use crate::{ }, graph::views::filter::{ model::{ - filter_operator::BinaryOp, node_filter::NodeFilter, PropertyExprFactory, - ViewWrapOps, + dyn_factory::DynNodeFilterFactory, filter_operator::BinaryOp, + node_filter::NodeFilter, CreateView, PropertyExprFactory, ViewWrapOps, }, CreateFilter, }, @@ -27,6 +27,8 @@ use raphtory_api::core::{ Direction, }; +use std::sync::Arc; + // 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 { @@ -738,3 +740,58 @@ fn numeric_constants_beyond_the_property_width_compare_by_value() { all ); } + +// ── Reads through a view on a node the view does not show ───────────────── + +/// early: p=1 @1, m=1 · late: p=1 @6, m=1 · layered: p=1 @1 +/// +/// ```text +/// time 0 1 2 3 4 5 6 +/// early ├────●───────────────────┤ +/// late ├────────────────────────┤ ● +/// └─ window [0, 5) ────────┘ +/// ``` +fn build_view_membership_graph() -> Graph { + let g = Graph::new(); + g.add_node(1, "early", [("p", 1i64)], None, None).unwrap(); + g.add_node(6, "late", [("p", 1i64)], None, None).unwrap(); + for name in ["early", "late"] { + g.node(name).unwrap().add_metadata([("m", 1i64)]).unwrap(); + } + g.add_node(1, "layered", [("p", 1i64)], None, None).unwrap(); + g +} + +#[test] +fn windowed_property_read_skips_node_absent_from_window() { + let g = build_view_membership_graph(); + let filter = NodeFilter.window(0, 5).property("p").eq(1i64); + assert_eq!(filtered_names(filter, g), vec!["early", "layered"]); +} + +#[test] +fn windowed_metadata_read_skips_node_absent_from_window() { + let g = build_view_membership_graph(); + let filter = NodeFilter.window(0, 5).metadata("m").eq(1i64); + assert_eq!(filtered_names(filter, g), vec!["early"]); +} + +#[test] +fn windowed_temporal_read_skips_node_absent_from_window() { + let g = build_view_membership_graph(); + let filter = NodeFilter + .window(0, 5) + .property("p") + .temporal() + .len() + .eq(Prop::U64(0)); + assert_eq!(filtered_names(filter, g), vec!["late"]); +} + +#[test] +fn only_a_real_view_checks_membership() { + assert!(!NodeFilter.narrows()); + assert!(NodeFilter.window(0, 5).narrows()); + let erased: Arc = Arc::new(NodeFilter); + assert!(!erased.narrows()); +} 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 06c90b5ffa..08d4d9284e 100644 --- a/raphtory/src/db/graph/views/filter/model/not_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/not_filter.rs @@ -74,7 +74,9 @@ impl CreateFilter for NotFilter> { graph: G, ) -> Result, GraphError> { Ok(Arc::new( - self.0.create_dyn_node_filter(Arc::new(graph))?.not(), + self.0 + .create_dyn_node_filter(graph.into_dyn_graph_arc())? + .not(), )) } diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs index f97c2b940b..f4e2981312 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/evaluate.rs @@ -1,8 +1,5 @@ use crate::{db::graph::views::filter::model::property_filter::Op, prelude::PropertyFilter}; -use raphtory_api::core::{ - entities::properties::prop::{Prop, PropType}, - utils::generalised_reduce, -}; +use raphtory_api::core::{entities::properties::prop::Prop, utils::generalised_reduce}; use std::borrow::Borrow; enum ValueType { diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 42d2a734a0..afbf64fcc7 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -1,12 +1,9 @@ -use crate::python::{ - filter::{ - edge_expr::{PyEdge, PyEdgeEndpoint, PyEdgeFilter}, - exploded_edge_expr::{PyExplodedEdge, PyExplodedEdgeFilter}, - filter_expr::PyFilterExpr, - graph_filter::{PyGraph, PyGraphFilter}, - node_expr::{PyExpr, PyNode, PyNodeFilter, PyPropertyExpr}, - }, - types::iterable::FromIterable, +use crate::python::filter::{ + edge_expr::{PyEdge, PyEdgeEndpoint, PyEdgeFilter}, + exploded_edge_expr::{PyExplodedEdge, PyExplodedEdgeFilter}, + filter_expr::PyFilterExpr, + graph_filter::{PyGraph, PyGraphFilter}, + node_expr::{PyExpr, PyNode, PyNodeFilter, PyPropertyExpr}, }; use pyo3::{ prelude::{PyModule, PyModuleMethods}, From d87ccbe3ab4ce805ddffcbf0b6e4e37b3f4048e4 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:26:32 +0100 Subject: [PATCH 152/155] keep the filter integration tests with their siblings --- raphtory-tests/tests/filter_tests/mod.rs | 1 + .../tests/filter_tests/test_filters.rs | 37 ++++++++++--------- raphtory-tests/tests/test_filters.rs | 1 + 3 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 raphtory-tests/tests/test_filters.rs diff --git a/raphtory-tests/tests/filter_tests/mod.rs b/raphtory-tests/tests/filter_tests/mod.rs index aec87ae09b..945b083f43 100644 --- a/raphtory-tests/tests/filter_tests/mod.rs +++ b/raphtory-tests/tests/filter_tests/mod.rs @@ -7,6 +7,7 @@ mod exploded_edge_property_filter; mod node_property_filter; mod node_type_filter_index; mod subgraph_tests; +mod test_filters; mod test_layers; mod tests_node_type_filtered_subgraph; mod views_test; diff --git a/raphtory-tests/tests/filter_tests/test_filters.rs b/raphtory-tests/tests/filter_tests/test_filters.rs index 6d449f1274..4c539bc5e4 100644 --- a/raphtory-tests/tests/filter_tests/test_filters.rs +++ b/raphtory-tests/tests/filter_tests/test_filters.rs @@ -1,6 +1,9 @@ -pub mod filter_tests; - use raphtory::{db::api::view::StaticGraphViewOps, prelude::*}; +use raphtory_api::core::entities::properties::prop::IntoProp; +use raphtory_storage::mutation::{ + addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, +}; +use raphtory_tests::assertions::GraphTransformer; mod test_composite_filters { use raphtory::{ @@ -116,12 +119,6 @@ mod test_composite_filters { } } -use raphtory_api::core::entities::properties::prop::IntoProp; -use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, -}; -use raphtory_tests::assertions::GraphTransformer; - struct IdentityGraphTransformer; impl GraphTransformer for IdentityGraphTransformer { @@ -133,7 +130,7 @@ impl GraphTransformer for IdentityGraphTransformer { mod test_property_semantics { mod test_node_property_filter_semantics { - use crate::IdentityGraphTransformer; + use crate::filter_tests::test_filters::IdentityGraphTransformer; use raphtory::{ db::{ api::view::{filter_ops::Filter, StaticGraphViewOps}, @@ -415,7 +412,7 @@ mod test_property_semantics { } mod test_edge_property_filter_semantics { - use crate::IdentityGraphTransformer; + use crate::filter_tests::test_filters::IdentityGraphTransformer; use raphtory::{ db::{ api::view::{filter_ops::Filter, EdgeViewOps, StaticGraphViewOps}, @@ -1637,7 +1634,7 @@ fn init_edges_graph_with_str_ids_del< mod test_node_filter { - use crate::{ + use crate::filter_tests::test_filters::{ init_nodes_graph, init_nodes_graph_with_num_ids, init_nodes_graph_with_str_ids, IdentityGraphTransformer, }; @@ -2915,7 +2912,9 @@ mod test_node_filter { } mod test_node_property_filter { - use crate::{init_nodes_graph, init_nodes_layers_graph, IdentityGraphTransformer}; + use crate::filter_tests::test_filters::{ + init_nodes_graph, init_nodes_layers_graph, IdentityGraphTransformer, + }; use raphtory::{ db::graph::views::filter::model::{ graph_filter::GraphFilter, node_filter::NodeFilter, windowed_filter::Windowed, @@ -4452,7 +4451,9 @@ mod test_node_property_filter { mod composite_node_filter_tests { use raphtory_api::core::Direction; - use crate::{init_edges_graph, init_nodes_graph, IdentityGraphTransformer}; + use crate::filter_tests::test_filters::{ + init_edges_graph, init_nodes_graph, IdentityGraphTransformer, + }; use raphtory::{ db::graph::views::filter::model::{ not_filter::NotFilter, ComposableFilter, NodeFilterFactory, PropertyExprFactory, @@ -4713,7 +4714,7 @@ mod composite_node_filter_tests { // temporal() semantics: iterates only timestamps where the property IS defined. // A node with the property absent at some timestamps is unaffected by those gaps. mod test_node_property_filter_agg { - use crate::IdentityGraphTransformer; + use crate::filter_tests::test_filters::IdentityGraphTransformer; use raphtory::{ db::{ api::view::StaticGraphViewOps, @@ -8225,7 +8226,7 @@ mod test_node_property_filter_agg { } mod test_edge_filter { - use crate::{ + use crate::filter_tests::test_filters::{ init_edges_graph, init_edges_graph_with_num_ids, init_edges_graph_with_str_ids, init_edges_graph_with_str_ids_del, init_nodes_graph, IdentityGraphTransformer, }; @@ -9244,7 +9245,9 @@ mod test_edge_filter { } mod test_edge_property_filter { - use crate::{init_edges_graph, init_edges_graph2, IdentityGraphTransformer}; + use crate::filter_tests::test_filters::{ + init_edges_graph, init_edges_graph2, IdentityGraphTransformer, + }; use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, ComposableFilter, PropertyExprFactory, ViewWrapOps, }; @@ -10544,7 +10547,7 @@ mod composite_edge_filter_tests { assert_filter_edges_results, TestGraphVariants, TestVariants, }; - use crate::{init_edges_graph, IdentityGraphTransformer}; + use crate::filter_tests::test_filters::{init_edges_graph, IdentityGraphTransformer}; #[test] fn test_filter_edge_for_src_dst() { diff --git a/raphtory-tests/tests/test_filters.rs b/raphtory-tests/tests/test_filters.rs new file mode 100644 index 0000000000..328a67e88a --- /dev/null +++ b/raphtory-tests/tests/test_filters.rs @@ -0,0 +1 @@ +pub mod filter_tests; From d9f7a49f2a2174bdfa32aaf4de1e209ad5a2ef10 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:59:16 +0100 Subject: [PATCH 153/155] combine node predicates on nodes before reducing to edges A typed `and`/`or` compiles through its tree, so every combination gets the per-question split. The node answer of a combination reduces to edges as "both ends pass the whole answer", not leg by leg, so `name == b | name == c` keeps the edge b->c on the per-edge path. A field read under a view is `None` for a node the view does not contain, the way a property read already is. --- .../test_edges_collection_filter.py | 26 ++- .../test_filters/test_node_filter.py | 26 +++ .../db/graph/views/filter/model/and_filter.rs | 24 ++- .../graph/views/filter/model/expr/compile.rs | 79 +++++++- .../graph/views/filter/model/expr/convert.rs | 22 ++- .../db/graph/views/filter/model/expr/tests.rs | 178 +++++++++++++++++- .../views/filter/model/node_expr/exprs.rs | 59 +++++- .../graph/views/filter/model/node_expr/ops.rs | 30 +++ .../views/filter/model/node_filter/mod.rs | 35 +++- .../db/graph/views/filter/model/or_filter.rs | 24 ++- 10 files changed, 462 insertions(+), 41 deletions(-) diff --git a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py index b6795d110c..f770a08a4c 100644 --- a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py +++ b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py @@ -227,6 +227,7 @@ def test_combinations_follow_set_algebra(): def check(graph): atoms, single = _atoms(), _singles(graph) negated = _negations(graph, single) + node_sets = _node_sets(graph) every = _ids(graph.edges) cases = [] for a, b in combinations(atoms, 2): @@ -234,7 +235,30 @@ def check(graph): cases.append((f"{a} & {b}", atoms[a] & atoms[b], single[a] & single[b])) if _or_is_refused(a, b): continue - if _kind(a) == _kind(b): + if _kind(a) == _kind(b) == "node": + # Node predicates combine on nodes first: an edge stays when both + # ends pass `a or b`, which is more than the union of the two + # both-ends sets (b->c stays for `name == b | name == c`). + either = node_sets[a] | node_sets[b] + both = node_sets[a] & node_sets[b] + cases.append( + (f"{a} | {b}", atoms[a] | atoms[b], _both_endpoints(graph, either)) + ) + cases.append( + ( + f"~({a} & {b})", + ~(atoms[a] & atoms[b]), + _both_endpoints(graph, _all_names(graph) - both), + ) + ) + cases.append( + ( + f"~({a} | {b})", + ~(atoms[a] | atoms[b]), + negated[a] & negated[b], + ) + ) + elif _kind(a) == _kind(b): cases.append((f"{a} | {b}", atoms[a] | atoms[b], single[a] | single[b])) cases.append( ( diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index d5bd01dade..6c4a277cc2 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -828,6 +828,32 @@ def check(graph): return check +def init_early_and_late(graph): + # early ●@1 · late ●@7 (type "kind") + graph.add_node(1, "early") + graph.add_node(7, "late", node_type="kind") + return graph + + +@with_variants(init_early_and_late) +def test_windowed_field_reads_are_none_outside_the_window(): + def check(graph): + window = filter.Node.window(0, 5) + cases = [ + (window.name() == "late", []), + (window.name() == "early", ["early"]), + (window.id() == "late", []), + (window.node_type() == "kind", []), + (window.node_type().is_none(), ["late"]), + (filter.Node.name() == "late", ["late"]), + ] + for filter_expr, expected_ids in cases: + assert sorted(graph.filter(filter_expr).nodes.id) == expected_ids + assert sorted(graph.nodes[filter_expr].id) == expected_ids + + return check + + @with_variants(create_test_graph) def test_filter_nodes_windowed_is_active_not(): def check(graph): diff --git a/raphtory/src/db/graph/views/filter/model/and_filter.rs b/raphtory/src/db/graph/views/filter/model/and_filter.rs index 6f1069d13b..146b83bd1b 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -1,22 +1,25 @@ use crate::{ db::{ api::{ - state::ops::{filter::AndOp, NodeFilterOp}, - view::internal::GraphView, + state::{ + ops::{filter::AndOp, NodeFilterOp}, + NodeOp, + }, + view::internal::{DynGraphArc, GraphView}, }, graph::views::filter::{ and_filtered_graph::AndFilteredGraph, model::{ edge_expr::ops::AndEdgeOp, expr::{FilterExpr, ToFilterExpr}, - ComposableFilter, + ComposableFilter, DynFilter, }, - CreateFilter, + CreateFilter, DynEdgeFilter, }, }, errors::GraphError, }; -use std::{fmt, fmt::Display}; +use std::{fmt, fmt::Display, sync::Arc}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AndFilter { @@ -41,21 +44,24 @@ impl Display for AndFilter { impl ComposableFilter for AndFilter {} -impl CreateFilter for AndFilter { +/// The `and` of two erased filters, the join the tree compiler builds once it +/// has split a filter into its node and edge answers. A typed `and` compiles +/// through its tree instead (see `compile_through_tree!`), so it gets that split. +impl CreateFilter for AndFilter { type FilteredGraph<'graph, G> - = AndFilteredGraph, R::FilteredGraph<'graph, G>> + = AndFilteredGraph, DynGraphArc<'graph>> where Self: 'graph, G: GraphView + 'graph; type NodeFilter<'graph, G> - = AndOp, R::NodeFilter<'graph, G>> + = AndOp + 'graph>, Arc + 'graph>> where Self: 'graph, G: GraphView + 'graph; type EdgeFilter<'graph, G> - = AndEdgeOp, R::EdgeFilter<'graph, G>> + = AndEdgeOp, DynEdgeFilter<'graph>> where Self: 'graph, G: GraphView + 'graph; diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index d5777ffad5..1a91329856 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -1960,18 +1960,36 @@ impl FilterExpr { /// the edge b→c, `not` never flips an answer the filter did not give, and /// the per-node and per-edge forms of a filter agree with its graph. fn compile_nested(&self) -> Result, GraphError> { - let nodes = self.answer(Question::Nodes)?; - let edges = self.answer(Question::Edges)?; + let nodes = self + .answer(Question::Nodes)? + .map(|nodes| nodes.compile_node_answer()) + .transpose()?; + let edges = self + .answer(Question::Edges)? + .map(|edges| edges.compile_answer()) + .transpose()?; Ok(match (nodes, edges) { (Some(nodes), Some(edges)) => Arc::new(AndFilter { - left: nodes.compile_answer()?, - right: edges.compile_answer()?, + left: nodes, + right: edges, }), - (Some(answer), None) | (None, Some(answer)) => answer.compile_answer()?, + (Some(answer), None) | (None, Some(answer)) => answer, (None, None) => Arc::new(GraphFilter), }) } + /// Compile the answer to the node question. An edge stays when both its + /// ends pass the whole answer; the legs of a combined answer each keep + /// their own edges, and an `or` of those would drop an edge whose ends + /// pass different legs, so a combined answer's edges come from its node test. + fn compile_node_answer(&self) -> Result, GraphError> { + let compiled = self.compile_answer()?; + Ok(match self { + FilterExpr::Node(_) => compiled, + _ => Arc::new(NodeAnswer(compiled)), + }) + } + /// This filter's direct answer to `question`, as a tree over that /// question's entities only; `None` when the filter leaves it open. An /// `or` with a leg that leaves the question open leaves it open. `not` @@ -2164,6 +2182,57 @@ impl CreateFilter for Viewed { } } +/// A combined answer to the node question: the nodes its node test keeps, and +/// the edges whose ends it keeps both. +#[derive(Clone)] +struct NodeAnswer(Arc); + +impl CreateFilter for NodeAnswer { + type FilteredGraph<'graph, G> + = DynGraphArc<'graph> + where + Self: 'graph, + G: GraphView + 'graph; + + type NodeFilter<'graph, G> + = Arc + 'graph> + where + Self: 'graph, + G: GraphView + 'graph; + + type EdgeFilter<'graph, G> + = DynEdgeFilter<'graph> + where + Self: 'graph, + G: GraphView + 'graph; + + fn create_graph_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + self.0.create_dyn_graph_filter(graph.into_dyn_graph_arc()) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + self.0.create_dyn_node_filter(graph.into_dyn_graph_arc()) + } + + fn create_edge_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + let nodes = self + .0 + .create_dyn_node_filter(graph.clone().into_dyn_graph_arc())?; + Ok(Arc::new(EdgeExistsOp::new(NodeFilteredGraph::new( + graph, nodes, + )))) + } +} + /// Fold compiled operands pairwise, left to right. An empty list has no /// meaning either way (`and` of nothing is not "everything", `or` of nothing /// is not "nothing" the caller asked for), so it is refused. diff --git a/raphtory/src/db/graph/views/filter/model/expr/convert.rs b/raphtory/src/db/graph/views/filter/model/expr/convert.rs index 2845d4397f..1b7f4ea13a 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/convert.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/convert.rs @@ -22,6 +22,7 @@ use crate::{ }, graph::views::filter::{ model::{ + and_filter::AndFilter, edge_filter::{EdgeEndpointWrapper, EdgeFilter, Endpoint}, exploded_edge_filter::ExplodedEdgeFilter, filter_operator::{BinaryOp, SetOp, StringOp, UnaryOp}, @@ -35,10 +36,12 @@ use crate::{ node_expr::{ AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, ConstExpr, DegreeExpr, EarliestExpr, EntityExpr, FirstExpr, LastExpr, LatestExpr, LenExpr, Marker, MaxExpr, MinExpr, - PropValueSetExpr, Scoped, StringExpr, SumExpr, TemporalPropExpr, UnaryExpr, + NodeFieldExpr, PropValueSetExpr, Scoped, StringExpr, SumExpr, TemporalPropExpr, + UnaryExpr, }, node_filter::NodeFilter, not_filter::NotFilter, + or_filter::OrFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, MetadataExpr, PropertyExpr, @@ -279,6 +282,21 @@ macro_rules! field_to_expr { field_to_expr!(Id => Id, Name => Name, Type => NodeType); +macro_rules! viewed_field_to_expr { + ($($t:ident => $field:ident),* $(,)?) => {$( + impl> ToExpr for NodeFieldExpr { + fn to_expr(&self) -> Expr { + Expr::Read(NodeLeaf::Field { + views: self.view_expr.views(), + field: Field::$field, + }) + } + } + )*}; +} + +viewed_field_to_expr!(Id => Id, Name => Name, Type => NodeType); + impl ToExpr for IsActiveNode { fn to_expr(&self) -> Expr { Expr::Read(NodeLeaf::is_active(Vec::new())) @@ -628,6 +646,8 @@ compile_through_tree! { impl for EdgeEndpointWrapper; impl for Scoped; impl for NotFilter; + impl for AndFilter; + impl for OrFilter; impl<> for IsActiveNode; impl<> for IsActiveEdge; impl<> for IsValidEdge; diff --git a/raphtory/src/db/graph/views/filter/model/expr/tests.rs b/raphtory/src/db/graph/views/filter/model/expr/tests.rs index 2e73d28925..c58cd80438 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/tests.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/tests.rs @@ -1,10 +1,13 @@ use super::*; use crate::{ db::{ - api::view::Filter, - graph::views::filter::model::{ - edge_filter::EdgeFilter, windowed_filter::Windowed, DynCreateFilter, EdgeViewFilterOps, - ViewWrapOps, + api::view::{Filter, Select}, + graph::views::filter::{ + model::{ + edge_filter::EdgeFilter, windowed_filter::Windowed, DynCreateFilter, + EdgeViewFilterOps, ViewWrapOps, + }, + CreateFilter, }, }, errors::GraphError, @@ -674,6 +677,12 @@ fn filters_answer_the_node_and_edge_questions_separately() { &["bob", "carol", "dave", "eve"], &["bob->carol"], ), + ( + "or(node, node): an edge whose ends pass different legs", + FilterExpr::Or(vec![score_gt(8.0), score_lt(3.0)]), + &["alice", "bob", "dave"], + &["alice->bob"], + ), ( "or(node, edge): both questions left open", FilterExpr::Or(vec![score_gt(4.0), w_gt(2)]), @@ -822,3 +831,164 @@ fn a_node_collection_refuses_a_filter_that_tests_edges() { both.sort(); assert_eq!(both, ["alice", "bob"]); } + +/// a→b w=1@1 · b→c w=2@2 +fn chain() -> Graph { + let g = Graph::new(); + g.add_edge(1, "a", "b", [("w", 1i64.into_prop())], None) + .unwrap(); + g.add_edge(2, "b", "c", [("w", 2i64.into_prop())], None) + .unwrap(); + g +} + +fn edge_ids<'graph, G: GraphViewOps<'graph>>(g: &G) -> Vec { + let mut ids: Vec = g + .edges() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + ids.sort(); + ids +} + +fn selected_edges(g: &Graph, filter: &F) -> Vec { + let mut ids: Vec = g + .edges() + .select(filter.clone()) + .unwrap() + .iter() + .map(|e| format!("{}->{}", e.src().name(), e.dst().name())) + .collect(); + ids.sort(); + ids +} + +/// A typed `and`, `or` and `not` answer the node and edge questions +/// separately, like the tree they build: the edge b→c is kept by +/// `name == "b" | name == "c"` because each end passes one leg. +#[test] +fn typed_combinators_answer_the_node_and_edge_questions_separately() { + use crate::db::graph::views::filter::model::{ + node_filter::{NodeFilter, NodeFilterFactory}, + ComposableFilter, EntityExprFilterOps, PropertyExprFactory, + }; + let g = chain(); + let name_is = |n: &'static str| NodeFilter.name().eq(n); + let w_gt_1 = || EdgeFilter.property("w").gt(1i64); + + let or = name_is("b").or(name_is("c")); + assert_eq!(selected_edges(&g, &or), ["b->c"], "or: edges.select"); + assert_eq!( + edge_ids(&g.filter(or.clone()).unwrap()), + ["b->c"], + "or: filter" + ); + + let or_node_edge = name_is("b").or(w_gt_1()); + assert_eq!( + selected_edges(&g, &or_node_edge), + ["a->b", "b->c"], + "or(node, edge): edges.select" + ); + assert_eq!( + edge_ids(&g.filter(or_node_edge.clone()).unwrap()), + ["a->b", "b->c"], + "or(node, edge): filter" + ); + + let and = name_is("a").not().and(w_gt_1()); + assert_eq!(selected_edges(&g, &and), ["b->c"], "and: edges.select"); + assert_eq!( + edge_ids(&g.filter(and.clone()).unwrap()), + ["b->c"], + "and: filter" + ); + + let not_or = name_is("b").or(name_is("c")).not(); + assert!( + selected_edges(&g, ¬_or).is_empty(), + "not(or): edges.select" + ); + assert!( + edge_ids(&g.filter(not_or.clone()).unwrap()).is_empty(), + "not(or): filter" + ); + let names: Vec = g.nodes().select(not_or).unwrap().name().collect(); + assert_eq!(names, ["a"], "not(or): nodes.select"); +} + +/// A field read under a view is a read of the node in that view: a node the +/// view does not hold has no name, id or type there, as it has no properties. +#[test] +fn a_field_read_under_a_view_is_none_for_a_node_outside_it() { + use crate::db::graph::views::filter::model::{ + node_filter::{NodeFilter, NodeFilterFactory}, + EntityExprFilterOps, + }; + // early@1 · late@7 + let g = Graph::new(); + g.add_node(1, "early", NO_PROPS, None, None).unwrap(); + g.add_node(7, "late", NO_PROPS, Some("kind"), None).unwrap(); + let filtered = |f: &dyn Fn() -> Arc| { + let mut n: Vec = g.filter(f()).unwrap().nodes().name().collect(); + n.sort(); + n + }; + let selected = |f: &dyn Fn() -> Arc| { + let mut n: Vec = g.nodes().select(f()).unwrap().name().collect(); + n.sort(); + n + }; + let win = || NodeFilter.window(0, 5); + let cases: Vec<(&str, Box Arc>, &[&str])> = vec![ + ( + "window name == late", + Box::new(move || Arc::new(win().name().eq("late"))), + &[], + ), + ( + "window name == early", + Box::new(move || Arc::new(win().name().eq("early"))), + &["early"], + ), + ( + "window id == late", + Box::new(move || Arc::new(win().id().eq("late"))), + &[], + ), + ( + "window node_type == kind", + Box::new(move || Arc::new(win().node_type().eq("kind"))), + &[], + ), + ( + "window node_type is_none", + Box::new(move || Arc::new(win().node_type().is_none())), + &["late"], + ), + ( + "name == late", + Box::new(|| Arc::new(NodeFilter.name().eq("late"))), + &["late"], + ), + ( + "tree: window name == late", + Box::new(|| { + Arc::new(node(cmp( + CmpOp::Eq, + Expr::Read(NodeLeaf::Field { + views: vec![window(0, 5)], + field: Field::Name, + }), + c("late"), + ))) + }), + &[], + ), + ]; + for (label, f, want) in cases { + assert_eq!(filtered(&*f), want, "{label}: filter"); + assert_eq!(selected(&*f), want, "{label}: nodes.select"); + } +} 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 5cf4dbbdce..a76a6ca10a 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 @@ -63,8 +63,8 @@ use super::{ ops::{ - AvgNodeOp, EarliestNodeOp, FirstNodeOp, LastNodeOp, LatestNodeOp, LenNodeOp, MaxNodeOp, - MinNodeOp, NodeIdOp, SumNodeOp, TemporalNodePropOp, + AvgNodeOp, EarliestNodeOp, FirstNodeOp, InViewNodeOp, LastNodeOp, LatestNodeOp, LenNodeOp, + MaxNodeOp, MinNodeOp, NodeIdOp, SumNodeOp, TemporalNodePropOp, }, AvgEdgeOp, CreateOp, EarliestEdgeOp, EntityExpr, FirstEdgeOp, LastEdgeOp, LatestEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, PredicateLhs, SumEdgeOp, @@ -207,6 +207,61 @@ impl CreateOp for Type { } } +/// A built-in node field (`Id`, `Name` or `Type`) read through a factory's +/// view chain: `NodeFilter.window(1, 5).name()`. The field's value does not +/// change with the view, but a node the view does not hold has no field there, +/// so under a view that can hide nodes the read is `None` for such a node. +#[derive(Clone)] +pub struct NodeFieldExpr { + pub(crate) view_expr: E, + pub(crate) field: F, +} + +impl EntityExpr for NodeFieldExpr +where + E: CreateView, + F: EntityExpr, +{ + type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } + + fn prop_type(&self) -> PropType { + self.field.prop_type() + } + + fn nullable(&self) -> bool { + self.view_expr.narrows() || self.field.nullable() + } +} + +impl PredicateLhs for NodeFieldExpr +where + E: CreateView, + F: EntityExpr, +{ +} + +impl CreateOp for NodeFieldExpr +where + E: CreateView, + F: EntityExpr + CreateOp, +{ + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + if !self.view_expr.narrows() { + return self.field.create_node_op(graph); + } + let graph = self.view_expr.create_view(graph)?; + let read = self.field.create_node_op(graph.clone())?; + Ok(Arc::new(InViewNodeOp { graph, read })) + } +} + // ───────────────────────────────────────────────────────────────────────────── // Constant value expressions — literal RHS values // 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 index d3adabe87a..bfd333d13f 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -168,6 +168,36 @@ impl NodeOp for NodeMetaOp { } } +// ───────────────────────────────────────────────────────────────────────────── +// InViewNodeOp — a read that holds only for nodes the view holds +// ───────────────────────────────────────────────────────────────────────────── + +/// A read through a view of the read's own that does not itself look at the +/// view, such as a node's name: for a node the view does not hold it is `None`, +/// as a property read through the same view would be (see [`view_node`]). +#[derive(Clone)] +pub(crate) struct InViewNodeOp { + pub(crate) graph: G, + pub(crate) read: F, +} + +impl>> NodeOp for InViewNodeOp { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.graph.node_list() + } + + type Output = Option; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Option { + view_node(&self.graph, true, node)?; + self.read.apply(storage, node) + } + + fn prop_type(&self) -> PropType { + self.read.prop_type() + } +} + // ───────────────────────────────────────────────────────────────────────────── // NodeIdOp — the node id as a value, typed by the graph's id type // ───────────────────────────────────────────────────────────────────────────── diff --git a/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs b/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs index 49a1ca7404..38eb33dd5f 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 @@ -12,7 +12,10 @@ use crate::{ dyn_factory::DynNodeFilterFactory, latest_filter::Latest, layered_filter::Layered, - node_expr::{exprs::DegreeExpr, EntityExpr}, + node_expr::{ + exprs::{DegreeExpr, NodeFieldExpr}, + EntityExpr, + }, node_state_filter::NodeStateBoolColOp, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, @@ -48,26 +51,38 @@ pub trait NodeFilterFactory: InternalViewWrapOps + CreateView + EntityExpr { type NodeWindow: NodeFilterFactory + DynNodeFilterFactory; + + /// Selects the node id field for filtering, read through this factory's views. #[inline] - fn id(&self) -> Id { - Id + fn id(&self) -> NodeFieldExpr { + NodeFieldExpr { + view_expr: self.clone(), + field: Id, + } } /// Selects the node name field for filtering. /// - /// Returns `Name` which implements `NodeExprFilterOps` — use `.eq("Alice")`, - /// `.contains("ali")`, `.is_in([…])`, etc. directly on the returned value. + /// Read through this factory's views: a node a view does not hold has no + /// name there. Use `.eq("Alice")`, `.contains("ali")`, `.is_in([…])`, etc. + /// directly on the returned value. #[inline] - fn name(&self) -> Name { - Name + fn name(&self) -> NodeFieldExpr { + NodeFieldExpr { + view_expr: self.clone(), + field: Name, + } } /// Selects the node type field for filtering. /// - /// Returns `Type` which implements `NodeExprFilterOps`. + /// Read through this factory's views, like [`Self::name`]. #[inline] - fn node_type(&self) -> Type { - Type + fn node_type(&self) -> NodeFieldExpr { + NodeFieldExpr { + view_expr: self.clone(), + field: Type, + } } /// Build a filter from a boolean column inside a TypedNodeState. 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 2dc5b1a228..a28afc765d 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -1,22 +1,25 @@ use crate::{ db::{ api::{ - state::ops::{filter::OrOp, NodeFilterOp}, - view::internal::GraphView, + state::{ + ops::{filter::OrOp, NodeFilterOp}, + NodeOp, + }, + view::internal::{DynGraphArc, GraphView}, }, graph::views::filter::{ model::{ edge_expr::ops::OrEdgeOp, expr::{FilterExpr, ToFilterExpr}, - ComposableFilter, + ComposableFilter, DynFilter, }, or_filtered_graph::OrFilteredGraph, - CreateFilter, + CreateFilter, DynEdgeFilter, }, }, errors::GraphError, }; -use std::{fmt, fmt::Display}; +use std::{fmt, fmt::Display, sync::Arc}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct OrFilter { @@ -41,21 +44,24 @@ impl Display for OrFilter { impl ComposableFilter for OrFilter {} -impl CreateFilter for OrFilter { +/// The `or` of two erased filters, the join the tree compiler builds once it +/// has split a filter into its node and edge answers. A typed `or` compiles +/// through its tree instead (see `compile_through_tree!`), so it gets that split. +impl CreateFilter for OrFilter { type FilteredGraph<'graph, G> - = OrFilteredGraph, R::FilteredGraph<'graph, G>> + = OrFilteredGraph, DynGraphArc<'graph>> where Self: 'graph, G: GraphView + 'graph; type NodeFilter<'graph, G> - = OrOp, R::NodeFilter<'graph, G>> + = OrOp + 'graph>, Arc + 'graph>> where Self: 'graph, G: GraphView + 'graph; type EdgeFilter<'graph, G> - = OrEdgeOp, R::EdgeFilter<'graph, G>> + = OrEdgeOp, DynEdgeFilter<'graph>> where Self: 'graph, G: GraphView + 'graph; From 73ad790db111ce572ad521a93620a9e10a35d8c9 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:12:21 +0100 Subject: [PATCH 154/155] port the node-type index tests to the expression API --- .../filter_tests/node_type_filter_index.rs | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/raphtory-tests/tests/filter_tests/node_type_filter_index.rs b/raphtory-tests/tests/filter_tests/node_type_filter_index.rs index 8e5e5d5f33..26eb170dfd 100644 --- a/raphtory-tests/tests/filter_tests/node_type_filter_index.rs +++ b/raphtory-tests/tests/filter_tests/node_type_filter_index.rs @@ -8,18 +8,13 @@ use raphtory::{ }, }, graph::views::filter::{ - model::{ - node_filter::{ops::NodeFilterOps, NodeFilter}, - property_filter::ops::PropertyFilterOps, - ComposableFilter, PropertyFilterFactory, - }, + model::{ComposableFilter, PropertyExprFactory}, CreateFilter, }, }, prelude::*, }; use raphtory_storage::core_ops::CoreGraphOps; -use std::fmt::Debug; use storage::api::node_type_index::NodeTypeIndexOps; #[test] @@ -164,16 +159,12 @@ fn type_index_subgraph_matches_scan() { assert_eq!(sorted_edges(&persons), [("a".into(), "b".into())]); } -fn check_filter_matches_scan( - indexed: &Graph, - scanned: &Graph, - filter: F, -) { +fn check_filter_matches_scan(indexed: &Graph, scanned: &Graph, filter: F) { let i = indexed.filter(filter.clone()).unwrap(); let s = scanned.filter(filter.clone()).unwrap(); - assert_eq!(sorted_names(&i), sorted_names(&s), "{filter:?}"); - assert_eq!(i.count_nodes(), s.count_nodes(), "{filter:?}"); - assert_eq!(sorted_edges(&i), sorted_edges(&s), "{filter:?}"); + assert_eq!(sorted_names(&i), sorted_names(&s)); + assert_eq!(i.count_nodes(), s.count_nodes()); + assert_eq!(sorted_edges(&i), sorted_edges(&s)); let mut i_nodes = indexed .nodes() @@ -189,7 +180,7 @@ fn check_filter_matches_scan( .collect::>(); i_nodes.sort(); s_nodes.sort(); - assert_eq!(i_nodes, s_nodes, "{filter:?}"); + assert_eq!(i_nodes, s_nodes); } #[test] @@ -229,48 +220,53 @@ fn type_index_nodes_type_filter_matches_scan() { fn type_index_combined_filters_match_scan() { let (indexed, scanned) = typed_graphs(); - check_filter_matches_scan(&indexed, &scanned, NodeFilter::node_type().eq("Person")); + check_filter_matches_scan(&indexed, &scanned, NodeFilter.node_type().eq("Person")); check_filter_matches_scan( &indexed, &scanned, - NodeFilter::node_type() + NodeFilter + .node_type() .eq("Person") .and(NodeFilter.property("p").gt(1i64)), ); check_filter_matches_scan( &indexed, &scanned, - NodeFilter::node_type() + NodeFilter + .node_type() .eq("Person") .or(NodeFilter.property("p").gt(3i64)), ); check_filter_matches_scan( &indexed, &scanned, - NodeFilter::node_type() + NodeFilter + .node_type() .is_in(["Person", "City"]) - .or(NodeFilter::name().eq("c")), + .or(NodeFilter.name().eq("c")), ); check_filter_matches_scan( &indexed, &scanned, - NodeFilter::node_type() + NodeFilter + .node_type() .is_in(["Person", "Company"]) - .and(NodeFilter::node_type().is_in(["Company", "City"])), + .and(NodeFilter.node_type().is_in(["Company", "City"])), ); check_filter_matches_scan( &indexed, &scanned, - NodeFilter::name() + NodeFilter + .name() .is_in(["a", "c", "e"]) - .and(NodeFilter::node_type().is_in(["Person", "City"])), + .and(NodeFilter.node_type().is_in(["Person", "City"])), ); - check_filter_matches_scan(&indexed, &scanned, NodeFilter::node_type().ne("Person")); + check_filter_matches_scan(&indexed, &scanned, NodeFilter.node_type().ne("Person")); check_filter_matches_scan( &indexed, &scanned, - NodeFilter::node_type().is_in(["_default", "City"]), + NodeFilter.node_type().is_in(["_default", "City"]), ); // type filter stacked on a property filtered view must not claim exactness From 88ee09d377fa544abb772333e96c95886278bfcf Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:44:40 +0100 Subject: [PATCH 155/155] tidy the prose and tests added with the node/edge split --- .../test_edges_collection_filter.py | 2 +- .../filter_tests/node_type_filter_index.rs | 35 +++++++++++++---- .../tests/filter_tests/test_filters.rs | 13 ++----- .../tests/filter_tests/views_test.rs | 3 +- .../db/graph/views/filter/model/and_filter.rs | 7 ++-- .../graph/views/filter/model/expr/compile.rs | 22 +++++------ .../graph/views/filter/model/expr/stream.rs | 4 +- .../db/graph/views/filter/model/expr/tests.rs | 18 +++------ .../src/db/graph/views/filter/model/mod.rs | 4 +- .../views/filter/model/node_expr/exprs.rs | 4 +- .../graph/views/filter/model/node_expr/ops.rs | 38 ++++++++++--------- .../views/filter/model/node_expr/tests.rs | 7 ++-- .../db/graph/views/filter/model/or_filter.rs | 6 +-- raphtory/src/python/filter/filter_expr.rs | 4 +- 14 files changed, 90 insertions(+), 77 deletions(-) diff --git a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py index f770a08a4c..d8f9257d25 100644 --- a/python/tests/test_base_install/test_filters/test_edges_collection_filter.py +++ b/python/tests/test_base_install/test_filters/test_edges_collection_filter.py @@ -238,7 +238,7 @@ def check(graph): if _kind(a) == _kind(b) == "node": # Node predicates combine on nodes first: an edge stays when both # ends pass `a or b`, which is more than the union of the two - # both-ends sets (b->c stays for `name == b | name == c`). + # both-ends sets (an edge whose ends pass different legs stays). either = node_sets[a] | node_sets[b] both = node_sets[a] & node_sets[b] cases.append( diff --git a/raphtory-tests/tests/filter_tests/node_type_filter_index.rs b/raphtory-tests/tests/filter_tests/node_type_filter_index.rs index 26eb170dfd..1605bb4850 100644 --- a/raphtory-tests/tests/filter_tests/node_type_filter_index.rs +++ b/raphtory-tests/tests/filter_tests/node_type_filter_index.rs @@ -159,12 +159,17 @@ fn type_index_subgraph_matches_scan() { assert_eq!(sorted_edges(&persons), [("a".into(), "b".into())]); } -fn check_filter_matches_scan(indexed: &Graph, scanned: &Graph, filter: F) { +fn check_filter_matches_scan( + indexed: &Graph, + scanned: &Graph, + filter: F, + label: &str, +) { let i = indexed.filter(filter.clone()).unwrap(); let s = scanned.filter(filter.clone()).unwrap(); - assert_eq!(sorted_names(&i), sorted_names(&s)); - assert_eq!(i.count_nodes(), s.count_nodes()); - assert_eq!(sorted_edges(&i), sorted_edges(&s)); + assert_eq!(sorted_names(&i), sorted_names(&s), "{label}"); + assert_eq!(i.count_nodes(), s.count_nodes(), "{label}"); + assert_eq!(sorted_edges(&i), sorted_edges(&s), "{label}"); let mut i_nodes = indexed .nodes() @@ -180,7 +185,7 @@ fn check_filter_matches_scan(indexed: &Graph, scanned: .collect::>(); i_nodes.sort(); s_nodes.sort(); - assert_eq!(i_nodes, s_nodes); + assert_eq!(i_nodes, s_nodes, "{label}"); } #[test] @@ -220,7 +225,12 @@ fn type_index_nodes_type_filter_matches_scan() { fn type_index_combined_filters_match_scan() { let (indexed, scanned) = typed_graphs(); - check_filter_matches_scan(&indexed, &scanned, NodeFilter.node_type().eq("Person")); + check_filter_matches_scan( + &indexed, + &scanned, + NodeFilter.node_type().eq("Person"), + "node_type == \"Person\"", + ); check_filter_matches_scan( &indexed, &scanned, @@ -228,6 +238,7 @@ fn type_index_combined_filters_match_scan() { .node_type() .eq("Person") .and(NodeFilter.property("p").gt(1i64)), + "node_type == \"Person\" and p > 1", ); check_filter_matches_scan( &indexed, @@ -236,6 +247,7 @@ fn type_index_combined_filters_match_scan() { .node_type() .eq("Person") .or(NodeFilter.property("p").gt(3i64)), + "node_type == \"Person\" or p > 3", ); check_filter_matches_scan( &indexed, @@ -244,6 +256,7 @@ fn type_index_combined_filters_match_scan() { .node_type() .is_in(["Person", "City"]) .or(NodeFilter.name().eq("c")), + "node_type in [Person, City] or name == \"c\"", ); check_filter_matches_scan( &indexed, @@ -252,6 +265,7 @@ fn type_index_combined_filters_match_scan() { .node_type() .is_in(["Person", "Company"]) .and(NodeFilter.node_type().is_in(["Company", "City"])), + "node_type in [Person, Company] and node_type in [Company, City]", ); check_filter_matches_scan( &indexed, @@ -260,13 +274,20 @@ fn type_index_combined_filters_match_scan() { .name() .is_in(["a", "c", "e"]) .and(NodeFilter.node_type().is_in(["Person", "City"])), + "name in [a, c, e] and node_type in [Person, City]", ); - check_filter_matches_scan(&indexed, &scanned, NodeFilter.node_type().ne("Person")); + check_filter_matches_scan( + &indexed, + &scanned, + NodeFilter.node_type().ne("Person"), + "node_type != \"Person\"", + ); check_filter_matches_scan( &indexed, &scanned, NodeFilter.node_type().is_in(["_default", "City"]), + "node_type in [_default, City]", ); // type filter stacked on a property filtered view must not claim exactness diff --git a/raphtory-tests/tests/filter_tests/test_filters.rs b/raphtory-tests/tests/filter_tests/test_filters.rs index 4c539bc5e4..19effbfb4d 100644 --- a/raphtory-tests/tests/filter_tests/test_filters.rs +++ b/raphtory-tests/tests/filter_tests/test_filters.rs @@ -8232,7 +8232,7 @@ mod test_edge_filter { }; use raphtory::db::graph::views::filter::model::{ edge_filter::EdgeFilter, ComposableFilter, EdgeViewFilterOps, EntityAggOps, - EntityExprFilterOps, PropertyExprFactory, ViewWrapOps, + EntityExprFilterOps, ViewWrapOps, }; use raphtory_tests::assertions::{ assert_filter_edges_results, assert_select_edges_results, TestGraphVariants, TestVariants, @@ -9254,9 +9254,7 @@ mod test_edge_property_filter { use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; use raphtory_api::core::entities::properties::prop::Prop; - use raphtory_tests::assertions::{ - assert_filter_edges_results, TestGraphVariants, TestVariants, - }; + use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; #[test] fn test_filter_edges_for_property_eq() { @@ -10538,14 +10536,11 @@ mod test_edge_property_filter { mod composite_edge_filter_tests { use raphtory::{ db::graph::views::filter::model::{ - edge_filter::EdgeFilter, not_filter::NotFilter, ComposableFilter, NodeFilterFactory, - PropertyExprFactory, + edge_filter::EdgeFilter, not_filter::NotFilter, ComposableFilter, PropertyExprFactory, }, prelude::EntityExprFilterOps, }; - use raphtory_tests::assertions::{ - assert_filter_edges_results, TestGraphVariants, TestVariants, - }; + use raphtory_tests::assertions::{assert_filter_edges_results, TestVariants}; use crate::filter_tests::test_filters::{init_edges_graph, IdentityGraphTransformer}; diff --git a/raphtory-tests/tests/filter_tests/views_test.rs b/raphtory-tests/tests/filter_tests/views_test.rs index be1158330f..10d9e5ecbe 100644 --- a/raphtory-tests/tests/filter_tests/views_test.rs +++ b/raphtory-tests/tests/filter_tests/views_test.rs @@ -1388,8 +1388,7 @@ mod test_edges_filters_window_graph { db::{ api::view::{filter_ops::Filter, StaticGraphViewOps}, graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::NodeFilterFactory, ComposableFilter, - EntityExprFilterOps, PropertyExprFactory, + edge_filter::EdgeFilter, ComposableFilter, EntityExprFilterOps, PropertyExprFactory, }, }, errors::GraphError, 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 146b83bd1b..bc4d41df15 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -44,9 +44,10 @@ impl Display for AndFilter { impl ComposableFilter for AndFilter {} -/// The `and` of two erased filters, the join the tree compiler builds once it -/// has split a filter into its node and edge answers. A typed `and` compiles -/// through its tree instead (see `compile_through_tree!`), so it gets that split. +/// The `and` of two compiled filters, as the tree compiler builds it (the +/// node and edge answers, the legs of one answer, or the predicates beside a +/// view). A typed `and` compiles through its tree instead (see +/// `compile_through_tree!`). impl CreateFilter for AndFilter { type FilteredGraph<'graph, G> = AndFilteredGraph, DynGraphArc<'graph>> diff --git a/raphtory/src/db/graph/views/filter/model/expr/compile.rs b/raphtory/src/db/graph/views/filter/model/expr/compile.rs index 1a91329856..8c66a8a550 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/compile.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/compile.rs @@ -1979,9 +1979,10 @@ impl FilterExpr { } /// Compile the answer to the node question. An edge stays when both its - /// ends pass the whole answer; the legs of a combined answer each keep - /// their own edges, and an `or` of those would drop an edge whose ends - /// pass different legs, so a combined answer's edges come from its node test. + /// ends pass the whole answer; the legs of any node answer but a single + /// node predicate (a combination, or an opaque filter) each keep their + /// own edges, and an `or` of those would drop an edge whose ends pass + /// different legs, so such an answer's edges come from its node test. fn compile_node_answer(&self) -> Result, GraphError> { let compiled = self.compile_answer()?; Ok(match self { @@ -2182,8 +2183,9 @@ impl CreateFilter for Viewed { } } -/// A combined answer to the node question: the nodes its node test keeps, and -/// the edges whose ends it keeps both. +/// Any node answer but a single node predicate (a combination, or an opaque +/// filter): the nodes its node test keeps, and the edges whose ends it keeps +/// both. #[derive(Clone)] struct NodeAnswer(Arc); @@ -2528,13 +2530,9 @@ mod tests { /// A leaf that records the address of the erased graph it is compiled against. #[derive(Clone, Default)] struct GraphProbe { - seen: Arc>>, + seen: Arc>>, } - // Only addresses are stored, never dereferenced. - unsafe impl Send for GraphProbe {} - unsafe impl Sync for GraphProbe {} - impl EntityExpr for GraphProbe { type Marker = EntityMarker; @@ -2552,7 +2550,7 @@ mod tests { self.seen .lock() .unwrap() - .push(Arc::as_ptr(&erased) as *const ()); + .push(Arc::as_ptr(&erased) as *const () as usize); Prop::I64(2).create_node_op(graph) } } @@ -2584,7 +2582,7 @@ mod tests { assert_eq!( *probe.seen.lock().unwrap(), - vec![Arc::as_ptr(&base) as *const ()] + vec![Arc::as_ptr(&base) as *const () as usize] ); } } diff --git a/raphtory/src/db/graph/views/filter/model/expr/stream.rs b/raphtory/src/db/graph/views/filter/model/expr/stream.rs index 68ad36ac2b..19093db6de 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/stream.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/stream.rs @@ -78,7 +78,7 @@ impl NodeHistory for TemporalNodePropOp { } fn values<'a>(&'a self, node: VID) -> BoxedLIter<'a, Prop> { - match view_node(&self.graph, self.in_view, node) + match view_node(&self.graph, self.narrows, node) .and_then(|n| n.properties().temporal().get_by_id(self.prop_id)) { Some(history) => GenLockedIter::from(history, |h| h.values()).into_dyn_boxed(), @@ -87,7 +87,7 @@ impl NodeHistory for TemporalNodePropOp { } fn values_rev<'a>(&'a self, node: VID) -> BoxedLIter<'a, Prop> { - match view_node(&self.graph, self.in_view, node) + match view_node(&self.graph, self.narrows, node) .and_then(|n| n.properties().temporal().get_by_id(self.prop_id)) { Some(history) => GenLockedIter::from(history, |h| h.values_rev()).into_dyn_boxed(), diff --git a/raphtory/src/db/graph/views/filter/model/expr/tests.rs b/raphtory/src/db/graph/views/filter/model/expr/tests.rs index c58cd80438..d83bac3889 100644 --- a/raphtory/src/db/graph/views/filter/model/expr/tests.rs +++ b/raphtory/src/db/graph/views/filter/model/expr/tests.rs @@ -4,8 +4,11 @@ use crate::{ api::view::{Filter, Select}, graph::views::filter::{ model::{ - edge_filter::EdgeFilter, windowed_filter::Windowed, DynCreateFilter, - EdgeViewFilterOps, ViewWrapOps, + edge_filter::EdgeFilter, + node_filter::{NodeFilter, NodeFilterFactory}, + windowed_filter::Windowed, + ComposableFilter, DynCreateFilter, EdgeViewFilterOps, EntityExprFilterOps, + PropertyExprFactory, ViewWrapOps, }, CreateFilter, }, @@ -621,7 +624,7 @@ fn aggregates_reduce_inside_each_update_and_earliest_picks_one() { /// filters gives the `and`'s answer. #[test] fn filters_answer_the_node_and_edge_questions_separately() { - use crate::db::api::view::{DynamicGraph, IntoDynamic, Select}; + use crate::db::api::view::{DynamicGraph, IntoDynamic}; let g = graph(); let score_gt = |v: f64| node(cmp(CmpOp::Gt, prop("score"), c(v))); let score_lt = |v: f64| node(cmp(CmpOp::Lt, prop("score"), c(v))); @@ -802,7 +805,6 @@ fn a_view_under_not_is_refused_inside_a_composite_too() { /// one that compiles to "every node". #[test] fn a_node_collection_refuses_a_filter_that_tests_edges() { - use crate::db::api::view::Select; let g = graph(); let score_gt = |v: f64| node(cmp(CmpOp::Gt, prop("score"), c(v))); let w_gt_2 = FilterExpr::Edge(cmp(CmpOp::Gt, edge_prop("w"), Expr::Const(2i64.into()))); @@ -869,10 +871,6 @@ fn selected_edges(g: &Graph, filter: &F) -> Vec /// `name == "b" | name == "c"` because each end passes one leg. #[test] fn typed_combinators_answer_the_node_and_edge_questions_separately() { - use crate::db::graph::views::filter::model::{ - node_filter::{NodeFilter, NodeFilterFactory}, - ComposableFilter, EntityExprFilterOps, PropertyExprFactory, - }; let g = chain(); let name_is = |n: &'static str| NodeFilter.name().eq(n); let w_gt_1 = || EdgeFilter.property("w").gt(1i64); @@ -922,10 +920,6 @@ fn typed_combinators_answer_the_node_and_edge_questions_separately() { /// view does not hold has no name, id or type there, as it has no properties. #[test] fn a_field_read_under_a_view_is_none_for_a_node_outside_it() { - use crate::db::graph::views::filter::model::{ - node_filter::{NodeFilter, NodeFilterFactory}, - EntityExprFilterOps, - }; // early@1 · late@7 let g = Graph::new(); g.add_node(1, "early", NO_PROPS, None, None).unwrap(); diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index efeab7bf55..b869e234bc 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -258,7 +258,7 @@ impl CreateOp for Pr Ok(Arc::new(NodePropOp { graph, prop_id, - in_view: self.view_expr.narrows(), + narrows: self.view_expr.narrows(), })) } @@ -288,7 +288,7 @@ impl CreateOp for Me Ok(Arc::new(NodeMetaOp { graph, prop_id, - in_view: self.view_expr.narrows(), + narrows: self.view_expr.narrows(), })) } 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 a76a6ca10a..cee916501c 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 @@ -585,7 +585,7 @@ impl DynCreateHistor Ok(Arc::new(TemporalNodePropOp { graph, prop_id, - in_view: self.view_expr.narrows(), + narrows: self.view_expr.narrows(), })) } @@ -616,7 +616,7 @@ impl CreateOp for Te TemporalNodePropOp { graph, prop_id, - in_view: self.view_expr.narrows(), + narrows: self.view_expr.narrows(), } .map(|a| Some(a)), )) 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 index bfd333d13f..5a0eba97ce 100644 --- a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -87,21 +87,23 @@ use storage::EdgeEntryRef; pub(crate) struct NodePropOp { pub(crate) graph: G, pub(crate) prop_id: usize, - /// Whether `graph` is a view of the read's own, see [`view_node`]. - pub(crate) in_view: bool, + /// Whether the read's own view (e.g. `NodeFilter.window(..)`) can hide + /// nodes the enclosing filter keeps; see [`view_node`]. + pub(crate) narrows: bool, } /// The node as `graph` sees it. When `graph` is the graph the enclosing filter /// runs on, that filter has already decided the node belongs to it, so the node -/// is read as it is. When it is a view of the read's own (`in_view`), the node -/// may be missing from it, and a read of a missing node is `None`. +/// is read as it is. When the read carries its own view that can hide nodes +/// (`narrows`, e.g. `NodeFilter.window(..)`), the node may be missing from it, +/// and a read of a missing node is `None`. #[inline] pub(crate) fn view_node( graph: &G, - in_view: bool, + narrows: bool, node: VID, ) -> Option> { - if in_view { + if narrows { (&graph).node(node) } else { Some(NodeView::new_internal(graph, node)) @@ -116,7 +118,7 @@ impl NodeOp for NodePropOp { type Output = Option; fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { - view_node(&self.graph, self.in_view, node)? + view_node(&self.graph, self.narrows, node)? .properties() .get_by_id(self.prop_id) } @@ -142,8 +144,9 @@ impl NodeOp for NodePropOp { pub(crate) struct NodeMetaOp { pub(crate) graph: G, pub(crate) prop_id: usize, - /// Whether `graph` is a view of the read's own, see [`view_node`]. - pub(crate) in_view: bool, + /// Whether the read's own view (e.g. `NodeFilter.window(..)`) can hide + /// nodes the enclosing filter keeps; see [`view_node`]. + pub(crate) narrows: bool, } impl NodeOp for NodeMetaOp { @@ -154,7 +157,7 @@ impl NodeOp for NodeMetaOp { type Output = Option; fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { - view_node(&self.graph, self.in_view, node)? + view_node(&self.graph, self.narrows, node)? .metadata() .get_by_id(self.prop_id) } @@ -172,9 +175,9 @@ impl NodeOp for NodeMetaOp { // InViewNodeOp — a read that holds only for nodes the view holds // ───────────────────────────────────────────────────────────────────────────── -/// A read through a view of the read's own that does not itself look at the -/// view, such as a node's name: for a node the view does not hold it is `None`, -/// as a property read through the same view would be (see [`view_node`]). +/// A read that does not consult the view itself (e.g. a node's name), taken +/// through a view that can hide nodes: `None` for a node the view does not +/// hold, as a property read through the same view would be. #[derive(Clone)] pub(crate) struct InViewNodeOp { pub(crate) graph: G, @@ -189,7 +192,7 @@ impl>> NodeOp for InViewNodeOp; fn apply(&self, storage: &GraphStorage, node: VID) -> Option { - view_node(&self.graph, true, node)?; + (&self.graph).node(node)?; self.read.apply(storage, node) } @@ -243,8 +246,9 @@ impl NodeOp for NodeIdOp { pub(crate) struct TemporalNodePropOp { pub(crate) graph: G, pub(crate) prop_id: usize, - /// Whether `graph` is a view of the read's own, see [`view_node`]. - pub(crate) in_view: bool, + /// Whether the read's own view (e.g. `NodeFilter.window(..)`) can hide + /// nodes the enclosing filter keeps; see [`view_node`]. + pub(crate) narrows: bool, } impl NodeOp for TemporalNodePropOp { @@ -263,7 +267,7 @@ impl NodeOp for TemporalNodePropOp { type Output = Prop; fn apply(&self, _storage: &GraphStorage, node: VID) -> Prop { - let vals: Vec = view_node(&self.graph, self.in_view, node) + let vals: Vec = view_node(&self.graph, self.narrows, node) .and_then(|n| { n.properties() .temporal() 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 d56ead2487..26afd03fd5 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 @@ -743,12 +743,13 @@ fn numeric_constants_beyond_the_property_width_compare_by_value() { // ── Reads through a view on a node the view does not show ───────────────── -/// early: p=1 @1, m=1 · late: p=1 @6, m=1 · layered: p=1 @1 +/// early: p=1 @1, m=1 · late: p=1 @6, m=1 · no_meta: p=1 @1 /// /// ```text /// time 0 1 2 3 4 5 6 /// early ├────●───────────────────┤ /// late ├────────────────────────┤ ● +/// no_meta ├────●───────────────────┤ /// └─ window [0, 5) ────────┘ /// ``` fn build_view_membership_graph() -> Graph { @@ -758,7 +759,7 @@ fn build_view_membership_graph() -> Graph { for name in ["early", "late"] { g.node(name).unwrap().add_metadata([("m", 1i64)]).unwrap(); } - g.add_node(1, "layered", [("p", 1i64)], None, None).unwrap(); + g.add_node(1, "no_meta", [("p", 1i64)], None, None).unwrap(); g } @@ -766,7 +767,7 @@ fn build_view_membership_graph() -> Graph { fn windowed_property_read_skips_node_absent_from_window() { let g = build_view_membership_graph(); let filter = NodeFilter.window(0, 5).property("p").eq(1i64); - assert_eq!(filtered_names(filter, g), vec!["early", "layered"]); + assert_eq!(filtered_names(filter, g), vec!["early", "no_meta"]); } #[test] 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 a28afc765d..344189812d 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -44,9 +44,9 @@ impl Display for OrFilter { impl ComposableFilter for OrFilter {} -/// The `or` of two erased filters, the join the tree compiler builds once it -/// has split a filter into its node and edge answers. A typed `or` compiles -/// through its tree instead (see `compile_through_tree!`), so it gets that split. +/// The `or` of two compiled legs of one question's answer, as the tree +/// compiler builds it. A typed `or` compiles through its tree instead (see +/// `compile_through_tree!`). impl CreateFilter for OrFilter { type FilteredGraph<'graph, G> = OrFilteredGraph, DynGraphArc<'graph>> diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 86cf0b87af..a66c8ccce8 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -74,11 +74,11 @@ impl ExprOrFilter { #[pymethods] impl PyFilterExpr { - pub fn __and__(&self, other: ExprOrFilter) -> Self { + fn __and__(&self, other: ExprOrFilter) -> Self { PyFilterExpr(FilterExpr::And(vec![self.0.clone(), other.into_filter()])) } - pub fn __or__(&self, other: ExprOrFilter) -> PyResult { + fn __or__(&self, other: ExprOrFilter) -> PyResult { let other = other.into_filter(); no_view(&self.0)?; no_view(&other)?;