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/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 c561a37782..885c9e7f2e 100644 --- a/graphql-bench/src/bench.ts +++ b/graphql-bench/src/bench.ts @@ -361,7 +361,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/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 36ba87efb2..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 * +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 * @@ -31,22 +73,25 @@ from raphtory.iterables import * __all__ = [ "FilterExpr", - "FilterOps", - "PropertyFilterOps", + "Expr", + "PropertyExpr", "Node", - "NodeIdFilterBuilder", - "NodeNameFilterBuilder", - "NodeTypeFilterBuilder", + "NodeFilter", "Edge", + "EdgeFilter", "EdgeEndpoint", - "EdgeEndpointIdFilter", - "EdgeEndpointNameFilter", - "EdgeEndpointTypeFilter", "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.""" @@ -59,19 +104,17 @@ class FilterExpr(object): def __rand__(self, value): """Return value&self.""" + def __repr__(self): + """Return repr(self).""" + def __ror__(self, value): """Return value|self.""" -class FilterOps(object): +class Expr(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). + 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): @@ -92,207 +135,198 @@ class FilterOps(object): def __ne__(self, value): """Return self!=value.""" - def all(self) -> filter.PropertyFilterOps: + def all(self) -> filter.Expr: """ - Requires that **all** elements match when the underlying property is list-like. + Requires that **all** elements match when the value is list-like (a temporal history or a list property). Returns: - filter.PropertyFilterOps: + filter.Expr: """ - def any(self) -> filter.PropertyFilterOps: + def any(self) -> filter.Expr: """ - Requires that **any** element matches when the underlying property is list-like. + Requires that **any** element matches when the value is list-like (a temporal history or a list property). Returns: - filter.PropertyFilterOps: + filter.Expr: """ - def avg(self) -> filter.PropertyFilterOps: + def avg(self) -> filter.Expr: """ - Averages list elements when the underlying property is numeric and list-like. + Averages the elements when the value is numeric and list-like. Returns: - filter.PropertyFilterOps: + filter.Expr: """ - def contains(self, value: Prop) -> filter.FilterExpr: + def contains(self, other: Prop | filter.Expr) -> filter.FilterExpr: """ - Checks whether the property's string representation contains the given value. + Checks whether the value's string representation contains the given value. Arguments: - value (Prop): Substring that must appear within the value. + other (Prop | filter.Expr): Substring that must appear within the value. Returns: - filter.FilterExpr: A filter expression evaluating substring search. + filter.FilterExpr: """ - def ends_with(self, value: Prop) -> filter.FilterExpr: + def ends_with(self, other: Prop | filter.Expr) -> filter.FilterExpr: """ - Checks whether the property's string representation ends with the given value. + Checks whether the value's string representation ends with the given value. Arguments: - value (Prop): Suffix to check for. + other (Prop | filter.Expr): Suffix to check for. Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. + filter.FilterExpr: """ - def first(self) -> filter.PropertyFilterOps: + def first(self) -> filter.Expr: """ - Selects the first element when the underlying property is list-like. + Selects the first element when the value is list-like. Returns: - filter.PropertyFilterOps: + filter.Expr: """ def fuzzy_search( - self, prop_value: str, levenshtein_distance: int, prefix_match: bool + self, other: Prop | filter.Expr, 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. + Performs fuzzy matching against the value's string representation, within a Levenshtein distance and with optional prefix matching. Arguments: - prop_value (str): String to approximately match against. + 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: A filter expression performing approximate text matching. + filter.FilterExpr: """ def is_in(self, values: list[Prop]) -> filter.FilterExpr: """ - Checks whether the property is contained within the specified iterable of values. + Checks whether the value is contained within the given values. Arguments: - values (list[Prop]): Iterable of property values to match against. + values (list[Prop]): Values to match against. Returns: - filter.FilterExpr: A filter expression evaluating membership. + filter.FilterExpr: """ def is_none(self) -> filter.FilterExpr: """ - Checks whether the property value is `None` / missing. + Checks whether the value is `None` / missing. Returns: - filter.FilterExpr: A filter expression evaluating `value is None`. + filter.FilterExpr: """ def is_not_in(self, values: list[Prop]) -> filter.FilterExpr: """ - Checks whether the property is **not** contained within the specified iterable of values. + Checks whether the value is **not** contained within the given values. Arguments: - values (list[Prop]): Iterable of property values to exclude. + values (list[Prop]): Values to exclude. Returns: - filter.FilterExpr: A filter expression evaluating non-membership. + filter.FilterExpr: """ def is_some(self) -> filter.FilterExpr: """ - Checks whether the property value is present (not `None`). + Checks whether the value is present (not `None`). Returns: - filter.FilterExpr: A filter expression evaluating `value is not None`. + filter.FilterExpr: """ - def last(self) -> filter.PropertyFilterOps: + def last(self) -> filter.Expr: """ - Selects the last element when the underlying property is list-like. + Selects the last element when the value is list-like. Returns: - filter.PropertyFilterOps: + filter.Expr: """ - def len(self) -> filter.PropertyFilterOps: + def len(self) -> filter.Expr: """ - Returns the list length when the underlying property is list-like. + Selects the number of elements when the value is list-like. Returns: - filter.PropertyFilterOps: + filter.Expr: """ - def max(self) -> filter.PropertyFilterOps: + def max(self) -> filter.Expr: """ - Returns the maximum list element when the underlying property is list-like. + Selects the maximum element when the value is list-like. Returns: - filter.PropertyFilterOps: + filter.Expr: """ - def min(self) -> filter.PropertyFilterOps: + def min(self) -> filter.Expr: """ - Returns the minimum list element when the underlying property is list-like. + Selects the minimum element when the value is list-like. Returns: - filter.PropertyFilterOps: + filter.Expr: """ - def not_contains(self, value: Prop) -> filter.FilterExpr: + def not_contains(self, other: Prop | filter.Expr) -> filter.FilterExpr: """ - Checks whether the property's string representation **does not** contain the given value. + Checks whether the value's string representation **does not** contain the given value. Arguments: - value (Prop): Substring that must not appear within the value. + other (Prop | filter.Expr): Substring that must not appear within the value. Returns: - filter.FilterExpr: A filter expression evaluating substring exclusion. + filter.FilterExpr: """ - def starts_with(self, value: Prop) -> filter.FilterExpr: + def starts_with(self, other: Prop | filter.Expr) -> filter.FilterExpr: """ - Checks whether the property's string representation starts with the given value. + Checks whether the value's string representation starts with the given value. Arguments: - value (Prop): Prefix to check for. + other (Prop | filter.Expr): Prefix to check for. Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. + filter.FilterExpr: """ - def sum(self) -> filter.PropertyFilterOps: + def sum(self) -> filter.Expr: """ - Sums list elements when the underlying property is numeric and list-like. + Sums the elements when the value is numeric and list-like. Returns: - filter.PropertyFilterOps: + filter.Expr: """ -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). - """ +class PropertyExpr(Expr): + """A property read, which can switch to the property's history with `temporal()`.""" - def temporal(self) -> filter.FilterOps: + def temporal(self) -> filter.Expr: """ - Selects temporal evaluation for the property. + Switches from the property's latest value to its full temporal history, + unlocking the aggregate chain (`sum`, `avg`, `min`, `max`, `any`, ...). Returns: - filter.FilterOps: A property expression builder operating on temporal values. + filter.Expr: """ class Node(object): """ - Constructs node filter expressions. + Entry point for constructing 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. + 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.NodeViewPropsFilterBuilder: + def after(time: int) -> filter.NodeFilter: """ Restricts node evaluation to times strictly after the given time. @@ -300,11 +334,11 @@ class Node(object): time (int): Lower time bound. Returns: - filter.NodeViewPropsFilterBuilder: + filter.NodeFilter: """ @staticmethod - def at(time: int) -> filter.NodeViewPropsFilterBuilder: + def at(time: int) -> filter.NodeFilter: """ Restricts node evaluation to a single point in time. @@ -312,11 +346,11 @@ class Node(object): time (int): Event time. Returns: - filter.NodeViewPropsFilterBuilder: + filter.NodeFilter: """ @staticmethod - def before(time: int) -> filter.NodeViewPropsFilterBuilder: + def before(time: int) -> filter.NodeFilter: """ Restricts node evaluation to times strictly before the given time. @@ -324,7 +358,7 @@ class Node(object): time (int): Upper time bound. Returns: - filter.NodeViewPropsFilterBuilder: + filter.NodeFilter: """ @staticmethod @@ -341,30 +375,30 @@ class Node(object): """ @staticmethod - def degree() -> filter.FilterOps: + def degree() -> filter.Expr: """ Selects total node degree for filtering. Returns: - filter.FilterOps: a builder that selects the node degree for filtering. + filter.Expr: """ @staticmethod - def id() -> filter.NodeIdFilterBuilder: + def id() -> filter.Expr: """ Selects the node ID field for filtering. Returns: - filter.NodeIdFilterBuilder: + filter.Expr: """ @staticmethod - def in_degree() -> filter.FilterOps: + def in_degree() -> filter.Expr: """ Selects incoming node degree for filtering. Returns: - filter.FilterOps: a builder that selects the node degree for filtering. + filter.Expr: """ @staticmethod @@ -377,16 +411,16 @@ class Node(object): """ @staticmethod - def latest() -> filter.NodeViewPropsFilterBuilder: + def latest() -> filter.NodeFilter: """ Evaluates filters against the latest available state of each node. Returns: - filter.NodeViewPropsFilterBuilder: + filter.NodeFilter: """ @staticmethod - def layer(layer: str) -> filter.NodeViewPropsFilterBuilder: + def layer(layer: str) -> filter.NodeFilter: """ Restricts evaluation to nodes belonging to the given layer. @@ -394,11 +428,11 @@ class Node(object): layer (str): Layer name. Returns: - filter.NodeViewPropsFilterBuilder: + filter.NodeFilter: """ @staticmethod - def layers(layers: list[str]) -> filter.NodeViewPropsFilterBuilder: + def layers(layers: list[str]) -> filter.NodeFilter: """ Restricts evaluation to nodes belonging to any of the given layers. @@ -406,11 +440,11 @@ class Node(object): layers (list[str]): Layer names. Returns: - filter.NodeViewPropsFilterBuilder: + filter.NodeFilter: """ @staticmethod - def metadata(name: str) -> filter.FilterOps: + def metadata(name: str) -> filter.Expr: """ Filters a node metadata field by name. @@ -420,38 +454,38 @@ class Node(object): name (str): Metadata key. Returns: - filter.FilterOps: + filter.Expr: """ @staticmethod - def name() -> filter.NodeNameFilterBuilder: + def name() -> filter.Expr: """ Selects the node name field for filtering. Returns: - filter.NodeNameFilterBuilder: + filter.Expr: """ @staticmethod - def node_type() -> filter.NodeTypeFilterBuilder: + def node_type() -> filter.Expr: """ Selects the node type field for filtering. Returns: - filter.NodeTypeFilterBuilder: + filter.Expr: """ @staticmethod - def out_degree() -> filter.FilterOps: + def out_degree() -> filter.Expr: """ Selects outgoing node degree for filtering. Returns: - filter.FilterOps: a builder that selects the node degree for filtering. + filter.Expr: """ @staticmethod - def property(name: str) -> filter.PropertyFilterOps: + def property(name: str) -> filter.PropertyExpr: """ Filters a node property by name. @@ -461,11 +495,11 @@ class Node(object): name (str): Property key. Returns: - filter.PropertyFilterOps: + filter.PropertyExpr: """ @staticmethod - def snapshot_at(time: int) -> filter.NodeViewPropsFilterBuilder: + def snapshot_at(time: int) -> filter.NodeFilter: """ Evaluates filters against a snapshot of the graph at a given time. @@ -473,20 +507,20 @@ class Node(object): time (int): Snapshot time. Returns: - filter.NodeViewPropsFilterBuilder: + filter.NodeFilter: """ @staticmethod - def snapshot_latest() -> filter.NodeViewPropsFilterBuilder: + def snapshot_latest() -> filter.NodeFilter: """ Evaluates filters against the most recent snapshot of the graph. Returns: - filter.NodeViewPropsFilterBuilder: + filter.NodeFilter: """ @staticmethod - def window(start: int, end: int) -> filter.NodeViewPropsFilterBuilder: + def window(start: int, end: int) -> filter.NodeFilter: """ Restricts node evaluation to the given time window. @@ -497,387 +531,222 @@ class Node(object): end (int): End time. Returns: - filter.NodeViewPropsFilterBuilder: + filter.NodeFilter: """ -class NodeIdFilterBuilder(object): +class NodeFilter(object): """ - Filters nodes by their ID value. + A node filter scoped to a view. - 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:") + 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 __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 NodeFilter: + """Create and return a new object. See help(type) for accurate signature.""" - def contains(self, value: str) -> filter.FilterExpr: + def after(self, time: int) -> filter.NodeFilter: """ - Returns a filter expression that checks whether the string - representation of the node ID contains the given substring. + Restricts node evaluation to times strictly after the given time. Arguments: - value (str): Substring that must appear within the value. + time (int): Lower time bound. Returns: - filter.FilterExpr: A filter expression evaluating substring search. + filter.NodeFilter: """ - def ends_with(self, value: str) -> filter.FilterExpr: + def at(self, time: int) -> filter.NodeFilter: """ - Returns a filter expression that checks whether the string - representation of the node ID ends with the given suffix. + Restricts node evaluation to a single point in time. Arguments: - value (str): Suffix to check for. + time (int): Event time. Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. + filter.NodeFilter: """ - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: + def before(self, time: int) -> filter.NodeFilter: """ - 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. + Restricts node evaluation to times strictly before the given time. 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. + time (int): Upper time bound. Returns: - filter.FilterExpr: A filter expression performing approximate text matching. + filter.NodeFilter: """ - def is_in(self, values: list[int]) -> filter.FilterExpr: + def by_state_column(self, state: OutputNodeState, col: str) -> filter.FilterExpr: """ - Returns a filter expression that checks whether the node ID - is contained within the specified iterable of IDs. + Build a node filter from a boolean column of an existing node-state result. Arguments: - values (list[int]): Iterable of node IDs to match against. + 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: A filter expression evaluating membership. + filter.FilterExpr: """ - def is_not_in(self, values: list[int]) -> filter.FilterExpr: + def degree(self) -> filter.Expr: """ - 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. + Selects total node degree for filtering. Returns: - filter.FilterExpr: A filter expression evaluating non-membership. + filter.Expr: """ - def not_contains(self, value: str) -> filter.FilterExpr: + def id(self) -> filter.Expr: """ - 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. + Selects the node ID field for filtering. Returns: - filter.FilterExpr: A filter expression evaluating substring exclusion. + filter.Expr: """ - def starts_with(self, value: str) -> filter.FilterExpr: + def in_degree(self) -> filter.Expr: """ - 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. + Selects incoming node degree for filtering. Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. + filter.Expr: """ -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: + def is_active(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. + Matches nodes that have at least one event in the current view. Returns: - filter.FilterExpr: A filter expression evaluating substring search. + filter.FilterExpr: """ - def ends_with(self, value: str) -> filter.FilterExpr: + def latest(self) -> filter.NodeFilter: """ - Returns a filter expression that checks whether the entity's - string value ends with the specified suffix. - - Arguments: - value (str): Suffix to check for. + Evaluates filters against the latest available state of each node. Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. + filter.NodeFilter: """ - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: + def layer(self, layer: str) -> filter.NodeFilter: """ - Returns a filter expression that performs fuzzy matching - against the entity's string value. - - Uses a specified Levenshtein distance and optional prefix matching. + Restricts evaluation to nodes belonging to the given layer. 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. + layer (str): Layer name. Returns: - filter.FilterExpr: A filter expression performing approximate text matching. + filter.NodeFilter: """ - def is_in(self, values: list[str]) -> filter.FilterExpr: + def layers(self, layers: list[str]) -> filter.NodeFilter: """ - Returns a filter expression that checks whether the entity's - string value is contained within the given iterable of strings. + Restricts evaluation to nodes belonging to any of the given layers. Arguments: - values (list[str]): Iterable of allowed string values. + layers (list[str]): Layer names. Returns: - filter.FilterExpr: A filter expression evaluating membership. - """ - - def is_not_in(self, values: list[str]) -> filter.FilterExpr: + filter.NodeFilter: """ - 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 metadata(self, name: str) -> filter.Expr: """ + Filters a node metadata field by name. - 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. + Metadata is shared across all temporal versions of a node. Arguments: - value (str): Substring that must not appear within the value. + name (str): Metadata key. Returns: - filter.FilterExpr: A filter expression evaluating substring exclusion. + filter.Expr: """ - def starts_with(self, value: str) -> filter.FilterExpr: + def name(self) -> filter.Expr: """ - Returns a filter expression that checks whether the entity's - string value starts with the specified prefix. - - Arguments: - value (str): Prefix to check for. + Selects the node name field for filtering. Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. + filter.Expr: """ -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: + def node_type(self) -> filter.Expr: """ - 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. + Selects the node type field for filtering. Returns: - filter.FilterExpr: A filter expression evaluating substring search. + filter.Expr: """ - def ends_with(self, value: str) -> filter.FilterExpr: + def out_degree(self) -> filter.Expr: """ - Returns a filter expression that checks whether the entity's - string value ends with the specified suffix. - - Arguments: - value (str): Suffix to check for. + Selects outgoing node degree for filtering. Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. + filter.Expr: """ - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: + def property(self, name: str) -> filter.PropertyExpr: """ - Returns a filter expression that performs fuzzy matching - against the entity's string value. + Filters a node property by name. - Uses a specified Levenshtein distance and optional prefix matching. + The property may be static or temporal depending on the query context. 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. + name (str): Property key. Returns: - filter.FilterExpr: A filter expression performing approximate text matching. + filter.PropertyExpr: """ - def is_in(self, values: list[str]) -> filter.FilterExpr: + def snapshot_at(self, time: int) -> filter.NodeFilter: """ - Returns a filter expression that checks whether the entity's - string value is contained within the given iterable of strings. + Evaluates filters against a snapshot of the graph at a given time. Arguments: - values (list[str]): Iterable of allowed string values. + time (int): Snapshot time. Returns: - filter.FilterExpr: A filter expression evaluating membership. + filter.NodeFilter: """ - def is_not_in(self, values: list[str]) -> filter.FilterExpr: + def snapshot_latest(self) -> filter.NodeFilter: """ - 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. + Evaluates filters against the most recent snapshot of the graph. Returns: - filter.FilterExpr: A filter expression evaluating non-membership. - """ - - def not_contains(self, value: str) -> filter.FilterExpr: + filter.NodeFilter: """ - 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 window(self, start: int, end: int) -> filter.NodeFilter: """ + Restricts node evaluation to the given time window. - 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. + The window is inclusive of `start` and exclusive of `end`. Arguments: - value (str): Prefix to check for. + start (int): Start time. + end (int): End time. Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. + filter.NodeFilter: """ 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() + 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.EdgeViewPropsFilterBuilder: + def after(time: int) -> filter.EdgeFilter: """ Restricts edge evaluation to times strictly after the given time. @@ -885,11 +754,11 @@ class Edge(object): time (int): Lower time bound. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.EdgeFilter: """ @staticmethod - def at(time: int) -> filter.EdgeViewPropsFilterBuilder: + def at(time: int) -> filter.EdgeFilter: """ Restricts edge evaluation to a single point in time. @@ -897,11 +766,11 @@ class Edge(object): time (int): Event time. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.EdgeFilter: """ @staticmethod - def before(time: int) -> filter.EdgeViewPropsFilterBuilder: + def before(time: int) -> filter.EdgeFilter: """ Restricts edge evaluation to times strictly before the given time. @@ -909,7 +778,7 @@ class Edge(object): time (int): Upper time bound. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.EdgeFilter: """ @staticmethod @@ -958,16 +827,16 @@ class Edge(object): """ @staticmethod - def latest() -> filter.EdgeViewPropsFilterBuilder: + def latest() -> filter.EdgeFilter: """ Evaluates edge predicates against the latest available edge state. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.EdgeFilter: """ @staticmethod - def layer(layer: str) -> filter.EdgeViewPropsFilterBuilder: + def layer(layer: str) -> filter.EdgeFilter: """ Restricts evaluation to edges belonging to the given layer. @@ -975,11 +844,11 @@ class Edge(object): layer (str): Layer name. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.EdgeFilter: """ @staticmethod - def layers(layers: list[str]) -> filter.EdgeViewPropsFilterBuilder: + def layers(layers: list[str]) -> filter.EdgeFilter: """ Restricts evaluation to edges belonging to any of the given layers. @@ -987,39 +856,35 @@ class Edge(object): layers (list[str]): Layer names. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.EdgeFilter: """ @staticmethod - def metadata(name: str) -> filter.FilterOps: + def metadata(name: str) -> filter.Expr: """ 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: + filter.Expr: """ @staticmethod - def property(name: str) -> filter.PropertyFilterOps: + def property(name: str) -> filter.PropertyExpr: """ 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: + filter.PropertyExpr: """ @staticmethod - def snapshot_at(time: int) -> filter.EdgeViewPropsFilterBuilder: + def snapshot_at(time: int) -> filter.EdgeFilter: """ Evaluates edge predicates against a snapshot of the graph at a given time. @@ -1027,16 +892,16 @@ class Edge(object): time (int): Snapshot time. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.EdgeFilter: """ @staticmethod - def snapshot_latest() -> filter.EdgeViewPropsFilterBuilder: + def snapshot_latest() -> filter.EdgeFilter: """ Evaluates edge predicates against the most recent snapshot of the graph. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.EdgeFilter: """ @staticmethod @@ -1049,459 +914,445 @@ class Edge(object): """ @staticmethod - def window(start: int, end: int) -> filter.EdgeViewPropsFilterBuilder: + def window(start: int, end: int) -> filter.EdgeFilter: """ 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: + filter.EdgeFilter: """ -class EdgeEndpoint(object): +class EdgeFilter(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. + An edge filter scoped to a view. - Examples: - Edge.src().id() == 1 - Edge.dst().name().starts_with("user:") - Edge.src().property("country") == "UK" + 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 id(self) -> filter.EdgeEndpointIdFilter: + def __new__(cls) -> EdgeFilter: + """Create and return a new object. See help(type) for accurate signature.""" + + def after(self, time: int) -> filter.EdgeFilter: """ - Selects the endpoint node ID field for filtering. + Restricts edge evaluation to times strictly after the given time. + + Arguments: + time (int): Lower time bound. Returns: - filter.EdgeEndpointIdFilter: + filter.EdgeFilter: """ - def metadata(self, name: str) -> filter.FilterOps: + def at(self, time: int) -> filter.EdgeFilter: """ - Filters an endpoint node metadata field by name. - - Metadata is shared across all temporal versions of a node. + Restricts edge evaluation to a single point in time. Arguments: - name (str): Metadata key. + time (int): Event time. Returns: - filter.FilterOps: + filter.EdgeFilter: """ - def name(self) -> filter.EdgeEndpointNameFilter: + def before(self, time: int) -> filter.EdgeFilter: """ - Selects the endpoint node name field for filtering. + Restricts edge evaluation to times strictly before the given time. + + Arguments: + time (int): Upper time bound. Returns: - filter.EdgeEndpointNameFilter: + filter.EdgeFilter: """ - def node_type(self) -> filter.EdgeEndpointTypeFilter: + def dst(self) -> filter.EdgeEndpoint: """ - Selects the endpoint node type field for filtering. + Selects the edge **destination endpoint** for filtering. Returns: - filter.EdgeEndpointTypeFilter: + filter.EdgeEndpoint: """ - def property(self, name: str) -> filter.PropertyFilterOps: + def is_active(self) -> filter.FilterExpr: """ - Filters an endpoint node property by name. - - The property may be static or temporal depending on the query context. - - Arguments: - name (str): Property key. + Matches edges that have at least one event in the current view. Returns: - filter.PropertyFilterOps: + filter.FilterExpr: """ -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). + def is_deleted(self) -> filter.FilterExpr: + """ + Matches edges that have been deleted. - Examples: - Edge.src().id() == 1 - Edge.dst().id().is_in([1, 2, 3]) - Edge.src().id().starts_with("user:") - """ + Returns: + filter.FilterExpr: + """ - def __eq__(self, value): - """Return self==value.""" + def is_self_loop(self) -> filter.FilterExpr: + """ + Matches edges that are self-loops (source == destination). - def __ge__(self, value): - """Return self>=value.""" + Returns: + filter.FilterExpr: + """ - def __gt__(self, value): - """Return self>value.""" + def is_valid(self) -> filter.FilterExpr: + """ + Matches edges that are structurally valid in the current view. - def __le__(self, value): - """Return self<=value.""" + Returns: + filter.FilterExpr: + """ - def __lt__(self, value): - """Return self filter.EdgeFilter: + """ + Evaluates edge predicates against the latest available edge state. - def __ne__(self, value): - """Return self!=value.""" + Returns: + filter.EdgeFilter: + """ - def contains(self, value: str) -> filter.FilterExpr: + def layer(self, layer: str) -> filter.EdgeFilter: """ - Checks whether the string representation of the endpoint ID contains the given substring. + Restricts evaluation to edges belonging to the given layer. Arguments: - value (str): Substring to search for. + layer (str): Layer name. Returns: - filter.FilterExpr: A filter expression evaluating substring search. + filter.EdgeFilter: """ - def ends_with(self, value: str) -> filter.FilterExpr: + def layers(self, layers: list[str]) -> filter.EdgeFilter: """ - Checks whether the string representation of the endpoint ID ends with the given suffix. + Restricts evaluation to edges belonging to any of the given layers. Arguments: - value (str): Suffix to check for. + layers (list[str]): Layer names. Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. + filter.EdgeFilter: """ - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: + def metadata(self, name: str) -> filter.Expr: """ - Performs fuzzy matching against the string representation of the endpoint ID. - - Uses a specified Levenshtein distance and optional prefix matching. + Filters an edge metadata field by name. Arguments: - value (str): String to approximately match against. - levenshtein_distance (int): Maximum allowed Levenshtein distance. - prefix_match (bool): Whether to require a matching prefix. + name (str): Metadata key. Returns: - filter.FilterExpr: A filter expression performing approximate text matching. + filter.Expr: """ - def is_in(self, values: list[int]) -> filter.FilterExpr: + def property(self, name: str) -> filter.PropertyExpr: """ - Checks whether the endpoint ID is contained within the specified iterable of IDs. + Filters an edge property by name. Arguments: - values (list[int]): Iterable of node IDs to match against. + name (str): Property key. Returns: - filter.FilterExpr: A filter expression evaluating membership. + filter.PropertyExpr: """ - def is_not_in(self, values: list[int]) -> filter.FilterExpr: + def snapshot_at(self, time: int) -> filter.EdgeFilter: """ - Checks whether the endpoint ID is **not** contained within the specified iterable of IDs. + Evaluates edge predicates against a snapshot of the graph at a given time. Arguments: - values (list[int]): Iterable of node IDs to exclude. + time (int): Snapshot time. Returns: - filter.FilterExpr: A filter expression evaluating non-membership. + filter.EdgeFilter: """ - def not_contains(self, value: str) -> filter.FilterExpr: + def snapshot_latest(self) -> filter.EdgeFilter: """ - Checks whether the string representation of the endpoint ID **does not** contain the given substring. + Evaluates edge predicates against the most recent snapshot of the graph. - Arguments: - value (str): Substring to exclude. + Returns: + filter.EdgeFilter: + """ + + def src(self) -> filter.EdgeEndpoint: + """ + Selects the edge **source endpoint** for filtering. Returns: - filter.FilterExpr: A filter expression evaluating substring exclusion. + filter.EdgeEndpoint: """ - def starts_with(self, value: str) -> filter.FilterExpr: + def window(self, start: int, end: int) -> filter.EdgeFilter: """ - Checks whether the string representation of the endpoint ID starts with the given prefix. + Restricts edge evaluation to the given time window. Arguments: - value (str): Prefix to check for. + start (int): Start time. + end (int): End time. Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. + filter.EdgeFilter: """ -class EdgeEndpointNameFilter(object): +class EdgeEndpoint(object): """ - Filters an edge endpoint by its node name. + Entry point for filtering an edge endpoint (source or destination). - This builder produces `FilterExpr` predicates over the **source** or - **destination** endpoint node name. + 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().name() == "alice" - Edge.dst().name().contains("ali") + Edge.src().id() == 1 + Edge.dst().name().starts_with("user:") + Edge.src().property("country") == "UK" """ - 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: + def id(self) -> filter.Expr: """ - 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. + Selects the endpoint node ID field for filtering. Returns: - filter.FilterExpr: A filter expression evaluating substring search. + filter.Expr: """ - def ends_with(self, value: str) -> filter.FilterExpr: + def metadata(self, name: str) -> filter.Expr: """ - Returns a filter expression that checks whether the entity's - string value ends with the specified suffix. + Filters an endpoint node metadata field by name. Arguments: - value (str): Suffix to check for. + name (str): Metadata key. Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. + filter.Expr: """ - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: + def name(self) -> filter.Expr: """ - Returns a filter expression that performs fuzzy matching - against the entity's string value. + Selects the endpoint node name field for filtering. - Uses a specified Levenshtein distance and optional prefix matching. + Returns: + filter.Expr: + """ - 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. + def node_type(self) -> filter.Expr: + """ + Selects the endpoint node type field for filtering. Returns: - filter.FilterExpr: A filter expression performing approximate text matching. + filter.Expr: """ - def is_in(self, values: list[str]) -> filter.FilterExpr: + def property(self, name: str) -> filter.PropertyExpr: """ - Returns a filter expression that checks whether the entity's - string value is contained within the given iterable of strings. + Filters an endpoint node property by name. Arguments: - values (list[str]): Iterable of allowed string values. + name (str): Property key. Returns: - filter.FilterExpr: A filter expression evaluating membership. + filter.PropertyExpr: """ - def is_not_in(self, values: list[str]) -> filter.FilterExpr: +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: """ - Returns a filter expression that checks whether the entity's - string value is **not** contained within the given iterable of strings. + Restricts exploded edge evaluation to times strictly after the given time. Arguments: - values (list[str]): Iterable of string values to exclude. + time (int): Lower time bound. Returns: - filter.FilterExpr: A filter expression evaluating non-membership. + filter.ExplodedEdgeFilter: """ - def not_contains(self, value: str) -> filter.FilterExpr: + @staticmethod + def at(time: int) -> filter.ExplodedEdgeFilter: """ - Returns a filter expression that checks whether the entity's - string value **does not** contain the given substring. + Restricts exploded edge evaluation to a single point in time. Arguments: - value (str): Substring that must not appear within the value. + time (int): Event time. Returns: - filter.FilterExpr: A filter expression evaluating substring exclusion. + filter.ExplodedEdgeFilter: """ - def starts_with(self, value: str) -> filter.FilterExpr: + @staticmethod + def before(time: int) -> filter.ExplodedEdgeFilter: """ - Returns a filter expression that checks whether the entity's - string value starts with the specified prefix. + Restricts exploded edge evaluation to times strictly before the given time. Arguments: - value (str): Prefix to check for. + time (int): Upper time bound. Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. + filter.ExplodedEdgeFilter: """ -class EdgeEndpointTypeFilter(object): - """ - Filters an edge endpoint by its node type. + @staticmethod + def is_active() -> filter.FilterExpr: + """ + Matches exploded edges that have at least one event in the current view. - This builder produces `FilterExpr` predicates over the **source** or - **destination** endpoint node type. + Returns: + filter.FilterExpr: + """ - Examples: - Edge.src().node_type() == "fire_nation" - Edge.dst().node_type().is_not_in(["air_nomads"]) - """ + @staticmethod + def is_deleted() -> filter.FilterExpr: + """ + Matches exploded edges that have been deleted. - def __eq__(self, value): - """Return self==value.""" + Returns: + filter.FilterExpr: + """ - def __ge__(self, value): - """Return self>=value.""" + @staticmethod + def is_self_loop() -> filter.FilterExpr: + """ + Matches exploded edges that are self-loops (source == destination). - def __gt__(self, value): - """Return self>value.""" + Returns: + filter.FilterExpr: + """ - def __le__(self, value): - """Return self<=value.""" + @staticmethod + def is_valid() -> filter.FilterExpr: + """ + Matches exploded edges that are structurally valid in the current view. - def __lt__(self, value): - """Return self filter.ExplodedEdgeFilter: + """ + Evaluates exploded edge predicates against the latest available state. + + Returns: + filter.ExplodedEdgeFilter: + """ - def contains(self, value: str) -> filter.FilterExpr: + @staticmethod + def layer(layer: str) -> filter.ExplodedEdgeFilter: """ - Returns a filter expression that checks whether the entity's - string value contains the given substring. + Restricts evaluation to exploded edges belonging to the given layer. Arguments: - value (str): Substring that must appear within the value. + layer (str): Layer name. Returns: - filter.FilterExpr: A filter expression evaluating substring search. + filter.ExplodedEdgeFilter: """ - def ends_with(self, value: str) -> filter.FilterExpr: + @staticmethod + def layers(layers: list[str]) -> filter.ExplodedEdgeFilter: """ - Returns a filter expression that checks whether the entity's - string value ends with the specified suffix. + Restricts evaluation to exploded edges belonging to any of the given layers. Arguments: - value (str): Suffix to check for. + layers (list[str]): Layer names. Returns: - filter.FilterExpr: A filter expression evaluating suffix matching. + filter.ExplodedEdgeFilter: """ - def fuzzy_search( - self, value: str, levenshtein_distance: int, prefix_match: bool - ) -> filter.FilterExpr: + @staticmethod + def metadata(name: str) -> filter.Expr: """ - Returns a filter expression that performs fuzzy matching - against the entity's string value. + Filters an exploded edge metadata field by name. - Uses a specified Levenshtein distance and optional prefix matching. + Metadata is shared across all temporal versions of an exploded edge. 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. + name (str): Metadata key. Returns: - filter.FilterExpr: A filter expression performing approximate text matching. + filter.Expr: """ - def is_in(self, values: list[str]) -> filter.FilterExpr: + @staticmethod + def property(name: str) -> filter.PropertyExpr: """ - Returns a filter expression that checks whether the entity's - string value is contained within the given iterable of strings. + Filters an exploded edge property by name. + + The property may be static or temporal depending on the query context. Arguments: - values (list[str]): Iterable of allowed string values. + name (str): Property key. Returns: - filter.FilterExpr: A filter expression evaluating membership. + filter.PropertyExpr: """ - def is_not_in(self, values: list[str]) -> filter.FilterExpr: + @staticmethod + def snapshot_at(time: int) -> filter.ExplodedEdgeFilter: """ - Returns a filter expression that checks whether the entity's - string value is **not** contained within the given iterable of strings. + Evaluates exploded edge predicates against a snapshot of the graph at a given time. Arguments: - values (list[str]): Iterable of string values to exclude. + time (int): Snapshot time. Returns: - filter.FilterExpr: A filter expression evaluating non-membership. + filter.ExplodedEdgeFilter: """ - def not_contains(self, value: str) -> filter.FilterExpr: + @staticmethod + def snapshot_latest() -> filter.ExplodedEdgeFilter: """ - 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. + Evaluates exploded edge predicates against the most recent snapshot of the graph. Returns: - filter.FilterExpr: A filter expression evaluating substring exclusion. + filter.ExplodedEdgeFilter: """ - def starts_with(self, value: str) -> filter.FilterExpr: + @staticmethod + def window(start: int, end: int) -> filter.ExplodedEdgeFilter: """ - Returns a filter expression that checks whether the entity's - string value starts with the specified prefix. + Restricts exploded edge evaluation to the given time window. + + The window is inclusive of `start` and exclusive of `end`. Arguments: - value (str): Prefix to check for. + start (int): Start time. + end (int): End time. Returns: - filter.FilterExpr: A filter expression evaluating prefix matching. + filter.ExplodedEdgeFilter: """ -class ExplodedEdge(object): +class ExplodedEdgeFilter(object): """ - 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() + 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. """ - @staticmethod - def after(time: int) -> filter.EdgeViewPropsFilterBuilder: + 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. @@ -1509,11 +1360,10 @@ class ExplodedEdge(object): time (int): Lower time bound. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.ExplodedEdgeFilter: """ - @staticmethod - def at(time: int) -> filter.EdgeViewPropsFilterBuilder: + def at(self, time: int) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to a single point in time. @@ -1521,11 +1371,10 @@ class ExplodedEdge(object): time (int): Event time. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.ExplodedEdgeFilter: """ - @staticmethod - def before(time: int) -> filter.EdgeViewPropsFilterBuilder: + def before(self, time: int) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to times strictly before the given time. @@ -1533,11 +1382,10 @@ class ExplodedEdge(object): time (int): Upper time bound. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.ExplodedEdgeFilter: """ - @staticmethod - def is_active() -> filter.FilterExpr: + def is_active(self) -> filter.FilterExpr: """ Matches exploded edges that have at least one event in the current view. @@ -1545,8 +1393,7 @@ class ExplodedEdge(object): filter.FilterExpr: """ - @staticmethod - def is_deleted() -> filter.FilterExpr: + def is_deleted(self) -> filter.FilterExpr: """ Matches exploded edges that have been deleted. @@ -1554,8 +1401,7 @@ class ExplodedEdge(object): filter.FilterExpr: """ - @staticmethod - def is_self_loop() -> filter.FilterExpr: + def is_self_loop(self) -> filter.FilterExpr: """ Matches exploded edges that are self-loops (source == destination). @@ -1563,8 +1409,7 @@ class ExplodedEdge(object): filter.FilterExpr: """ - @staticmethod - def is_valid() -> filter.FilterExpr: + def is_valid(self) -> filter.FilterExpr: """ Matches exploded edges that are structurally valid in the current view. @@ -1572,17 +1417,15 @@ class ExplodedEdge(object): filter.FilterExpr: """ - @staticmethod - def latest() -> filter.EdgeViewPropsFilterBuilder: + def latest(self) -> filter.ExplodedEdgeFilter: """ Evaluates exploded edge predicates against the latest available state. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.ExplodedEdgeFilter: """ - @staticmethod - def layer(layer: str) -> filter.EdgeViewPropsFilterBuilder: + def layer(self, layer: str) -> filter.ExplodedEdgeFilter: """ Restricts evaluation to exploded edges belonging to the given layer. @@ -1590,11 +1433,10 @@ class ExplodedEdge(object): layer (str): Layer name. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.ExplodedEdgeFilter: """ - @staticmethod - def layers(layers: list[str]) -> filter.EdgeViewPropsFilterBuilder: + def layers(self, layers: list[str]) -> filter.ExplodedEdgeFilter: """ Restricts evaluation to exploded edges belonging to any of the given layers. @@ -1602,11 +1444,10 @@ class ExplodedEdge(object): layers (list[str]): Layer names. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.ExplodedEdgeFilter: """ - @staticmethod - def metadata(name: str) -> filter.FilterOps: + def metadata(self, name: str) -> filter.Expr: """ Filters an exploded edge metadata field by name. @@ -1616,11 +1457,10 @@ class ExplodedEdge(object): name (str): Metadata key. Returns: - filter.FilterOps: + filter.Expr: """ - @staticmethod - def property(name: str) -> filter.PropertyFilterOps: + def property(self, name: str) -> filter.PropertyExpr: """ Filters an exploded edge property by name. @@ -1630,11 +1470,10 @@ class ExplodedEdge(object): name (str): Property key. Returns: - filter.PropertyFilterOps: + filter.PropertyExpr: """ - @staticmethod - def snapshot_at(time: int) -> filter.EdgeViewPropsFilterBuilder: + def snapshot_at(self, time: int) -> filter.ExplodedEdgeFilter: """ Evaluates exploded edge predicates against a snapshot of the graph at a given time. @@ -1642,20 +1481,18 @@ class ExplodedEdge(object): time (int): Snapshot time. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.ExplodedEdgeFilter: """ - @staticmethod - def snapshot_latest() -> filter.EdgeViewPropsFilterBuilder: + def snapshot_latest(self) -> filter.ExplodedEdgeFilter: """ Evaluates exploded edge predicates against the most recent snapshot of the graph. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.ExplodedEdgeFilter: """ - @staticmethod - def window(start: int, end: int) -> filter.EdgeViewPropsFilterBuilder: + def window(self, start: int, end: int) -> filter.ExplodedEdgeFilter: """ Restricts exploded edge evaluation to the given time window. @@ -1666,30 +1503,19 @@ class ExplodedEdge(object): end (int): End time. Returns: - filter.EdgeViewPropsFilterBuilder: + filter.ExplodedEdgeFilter: """ 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. + Entry point for graph-level view 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() + Every method is static and returns a [`GraphFilter`] carrying the view, + which composes with node and edge predicates. """ @staticmethod - def after(time: int) -> filter.ViewFilterBuilder: + def after(time: int) -> filter.GraphFilter: """ Restricts evaluation to times strictly after the given time. @@ -1697,11 +1523,11 @@ class Graph(object): time (int): Lower time bound. Returns: - filter.ViewFilterBuilder: + filter.GraphFilter: """ @staticmethod - def at(time: int) -> filter.ViewFilterBuilder: + def at(time: int) -> filter.GraphFilter: """ Restricts evaluation to a single point in time. @@ -1709,11 +1535,11 @@ class Graph(object): time (int): Event time. Returns: - filter.ViewFilterBuilder: + filter.GraphFilter: """ @staticmethod - def before(time: int) -> filter.ViewFilterBuilder: + def before(time: int) -> filter.GraphFilter: """ Restricts evaluation to times strictly before the given time. @@ -1721,20 +1547,20 @@ class Graph(object): time (int): Upper time bound. Returns: - filter.ViewFilterBuilder: + filter.GraphFilter: """ @staticmethod - def latest() -> filter.ViewFilterBuilder: + def latest() -> filter.GraphFilter: """ Evaluates filters against the latest available state of the graph. Returns: - filter.ViewFilterBuilder: + filter.GraphFilter: """ @staticmethod - def layer(layer: str) -> filter.ViewFilterBuilder: + def layer(layer: str) -> filter.GraphFilter: """ Restricts evaluation to a single layer. @@ -1742,11 +1568,11 @@ class Graph(object): layer (str): Layer name. Returns: - filter.ViewFilterBuilder: + filter.GraphFilter: """ @staticmethod - def layers(layers: list[str]) -> filter.ViewFilterBuilder: + def layers(layers: list[str]) -> filter.GraphFilter: """ Restricts evaluation to any of the given layers. @@ -1754,11 +1580,11 @@ class Graph(object): layers (list[str]): Layer names. Returns: - filter.ViewFilterBuilder: + filter.GraphFilter: """ @staticmethod - def snapshot_at(time: int) -> filter.ViewFilterBuilder: + def snapshot_at(time: int) -> filter.GraphFilter: """ Evaluates filters against a snapshot of the graph at a given time. @@ -1766,20 +1592,126 @@ class Graph(object): time (int): Snapshot time. Returns: - filter.ViewFilterBuilder: + filter.GraphFilter: """ @staticmethod - def snapshot_latest() -> filter.ViewFilterBuilder: + def snapshot_latest() -> filter.GraphFilter: """ Evaluates filters against the most recent snapshot of the graph. Returns: - filter.ViewFilterBuilder: + filter.GraphFilter: """ @staticmethod - def window(start: int, end: int) -> filter.ViewFilterBuilder: + 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. @@ -1790,5 +1722,5 @@ class Graph(object): end (int): End time. Returns: - filter.ViewFilterBuilder: + filter.GraphFilter: """ diff --git a/python/python/raphtory/graphql/__init__.pyi b/python/python/raphtory/graphql/__init__.pyi index 47d04fa376..80672ff76b 100644 --- a/python/python/raphtory/graphql/__init__.pyi +++ b/python/python/raphtory/graphql/__init__.pyi @@ -750,8 +750,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]: @@ -1346,7 +1346,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: @@ -1804,8 +1805,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: @@ -2304,8 +2305,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: @@ -2773,8 +2774,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: @@ -3230,8 +3231,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: @@ -3685,8 +3686,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: @@ -4144,8 +4145,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/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_filters/test_edge_filter.py b/python/tests/test_base_install/test_filters/test_edge_filter.py index d28cb85978..005db206c2 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 @@ -515,7 +515,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 U64 but got "Str"', + match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be coerced to 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 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_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_exploded_edge_filter.py b/python/tests/test_base_install/test_filters/test_exploded_edge_filter.py index 4fea26f383..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 @@ -406,15 +406,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), @@ -455,397 +446,375 @@ def test_all_property_types(GraphClass): 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. + # 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"): + g.filter(expr).edges.explode() + wrong_types = [ # Integers (weight) ( - filter.ExplodedEdge.property("weight") == "2", - "Wrong type for property weight: expected I64 but actual type is Str", + lambda: filter.ExplodedEdge.property("weight").contains("bo"), + "string operator requires a Str property", ), ( - 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.", - ), - ( - 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, + "cannot be coerced to Bool", ), ( - filter.ExplodedEdge.property("active") > 1, - "Wrong type for property active: expected Bool but actual type is I64", + lambda: filter.ExplodedEdge.property("active") > 1, + "cannot be coerced to Bool", ), ( - 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").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..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 @@ -342,94 +342,98 @@ 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|always has a value" + ): + 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 +1124,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 @@ -1133,7 +1137,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 U64 but got "Str"', + match=r'value Str\(ArcStr\("3"\)\) of type Str cannot be coerced to U64', ): graph.filter(filter_expr).nodes.id @@ -1290,8 +1294,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 +1309,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..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 @@ -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,12 +787,15 @@ def check(graph): @with_variants(create_test_graph) def test_filter_nodes_with_with_qualifier_on_non_string(): def check(graph): - filter_expr = filter.Node.property("prop8").any() == "3" - with pytest.raises( - Exception, - match=r"Wrong type for property prop8: expected I64 but actual type is Str", - ): - 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 @@ -800,12 +803,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 +817,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 +1139,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 +1187,12 @@ 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,12 @@ 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 +1266,12 @@ 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 +1288,12 @@ 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 +1308,12 @@ 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/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_graphdb/test_graphdb.py b/python/tests/test_base_install/test_graphdb/test_graphdb.py index 87fcdd7417..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,31 +2995,3 @@ def datadir(tmpdir, request): except Exception as e: 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/parity/test_parity_filters.py b/python/tests/test_base_install/test_graphql/parity/test_parity_filters.py index afc028fb9b..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. @@ -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 = { @@ -678,19 +697,33 @@ 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", - ), } +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. @@ -1032,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. } @@ -1069,6 +1105,41 @@ def test_rejected_expr_parity_at_nodes_filter(filter_pair, name): ) +# 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: 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_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 != 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 # edge-testing expression identically, so the check runs at every site rather # than only at `graph.nodes`. @@ -1115,31 +1186,8 @@ 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 +# takes FilterExpr, so graph-view / node / mixed expressions narrow membership # the same way local core select does. SUBSCRIPT_GENERAL_EXPRS = [ ( @@ -1241,3 +1289,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/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..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 @@ -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 } @@ -2068,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" } } } } } @@ -2097,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" } } } } } @@ -2259,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 a6a1f531f5..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,19 +43,19 @@ 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 } } } } } """ 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) @@ -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,19 +91,19 @@ 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 } } } } } } """ 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) @@ -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: { bool: true } } - } - } }) { + filterEdges: filter(expr: { + ge: { + lhs: { read: { entity: EDGE, target: { property: "eprop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { edges { list { src { name } dst { name } } } } } } """ - expected_error_message = ( - "Wrong type for property eprop1: expected I64 but actual type is Bool" - ) + 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 = ( - "Wrong type for property eprop1: expected I64 but actual type is Str" - ) + 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 = ( - "Wrong type for property eprop1: expected I64 but actual type is Str" - ) + 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 = ( - "Wrong type for property eprop1: expected I64 but actual type is Str" - ) + 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: { any: { 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 7f4320ffcf..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,19 +32,19 @@ 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 } } } } } """ 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) @@ -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,19 +76,19 @@ 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 } } } } } """ 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) @@ -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: { bool: true } } - } - } }) { + filterNodes: filter(expr: { + ge: { + lhs: { read: { entity: NODE, target: { property: "prop1" } } } + rhs: { const: { str: "shivam" } } + } + }) { nodes { list { name } } } } } """ - expected_error_message = ( - "Wrong type for property prop1: expected I64 but actual type is Bool" - ) + 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 = ( - "Wrong type for property prop1: expected I64 but actual type is Str" - ) + 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 = ( - "Wrong type for property prop1: expected I64 but actual type is Str" - ) + 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 = ( - "Wrong type for property prop1: expected I64 but actual type is Str" - ) + 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 84ca0ececa..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 @@ -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 } } @@ -60,11 +61,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 } } @@ -72,7 +74,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) @@ -85,11 +89,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 } } @@ -107,13 +112,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 } @@ -138,7 +154,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 } @@ -166,7 +184,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 } @@ -213,11 +235,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 }} }} @@ -241,7 +277,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 }} }} }} @@ -265,7 +301,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 ), @@ -273,13 +309,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 ), @@ -287,13 +323,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 ), @@ -301,13 +337,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 ), @@ -315,13 +351,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 ), @@ -329,13 +365,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 ), @@ -343,7 +379,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), ) ) @@ -358,12 +394,13 @@ 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 ), @@ -371,10 +408,7 @@ 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 ), @@ -383,10 +417,7 @@ 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 ), @@ -394,10 +425,7 @@ 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 ), @@ -406,12 +434,7 @@ 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 ), @@ -419,12 +442,7 @@ 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 ), @@ -433,7 +451,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] ), @@ -441,7 +463,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] ), @@ -450,7 +476,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] ), @@ -458,7 +488,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] ), @@ -477,7 +511,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) ), @@ -485,7 +519,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) ), @@ -494,7 +528,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) ), @@ -502,7 +536,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) ), @@ -511,13 +545,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]), ) ) @@ -528,17 +562,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 }} }} @@ -551,7 +585,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 }} }} }} @@ -566,26 +600,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 }} }} @@ -598,7 +632,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 a945608df3..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 @@ -110,7 +104,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) @@ -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 @@ -168,7 +158,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) @@ -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: { bool: true } - } - } - } } + 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 = ( - "Wrong type for property prop1: expected I64 but actual type is Bool" - ) + 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 = ( - "Wrong type for property prop1: expected I64 but actual type is Str" - ) + 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 = ( - "Wrong type for property prop1: expected I64 but actual type is Str" - ) + 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 = ( - "Wrong type for property prop1: expected I64 but actual type is Str" - ) + 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_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_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_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/python/tests/test_base_install/test_repr.py b/python/tests/test_base_install/test_repr.py index 0cbd495b6e..7bff9a43b0 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 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)") + + 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(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)") diff --git a/raphtory-api/src/core/entities/mod.rs b/raphtory-api/src/core/entities/mod.rs index d40e59a83f..b7d23f4064 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. @@ -257,6 +257,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/mod.rs b/raphtory-api/src/core/entities/properties/prop/mod.rs index 803842b233..448dfb2fdb 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 @@ pub mod prop_hashable; mod template; pub use arrow::*; - pub use prop_array::*; pub use prop_enum::*; pub use prop_ref_enum::*; 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 83aec2c640..8f795f5e5f 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs @@ -250,6 +250,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-benchmark/src/algobench_common.rs b/raphtory-benchmark/src/algobench_common.rs index 20b02e68e5..a88d2c74cd 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::*, diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 8e3fd29649..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.) +A built-in node field. """ -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`). -""" -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. 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/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-graphql/src/client/graphql_transport.rs b/raphtory-graphql/src/client/graphql_transport.rs index df61ef89eb..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()); } @@ -4950,6 +4956,20 @@ mod tests { /// degrees a=2 (b, c both match), b=1 (a dropped), c=1 (a dropped). #[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::tree::{ + CmpOp, Entity, Expr, FilterExpr, Scope, Target, + }, + }, + prelude::Prop, + }; + use reqwest::Url; + use std::collections::HashMap as Map; + use tempfile::tempdir; + let tmp_dir = tempdir().unwrap(); let server = GraphServer::new(tmp_dir.path().to_path_buf(), None, Args::default()) .await @@ -4974,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!( @@ -5038,10 +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::try_from(score_gt_15.clone()).unwrap(); - 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!( @@ -5065,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); @@ -5081,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 2627ea1b6e..dac669a927 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/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 a071f80624..0873c79e18 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -1,63 +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::{ - degree_filter::DegreeFilter, - edge_filter::{CompositeEdgeFilter, EdgeFilter}, - exploded_edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, - 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}, - property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, - snapshot_filter::{ - SnapshotAt as SnapshotAtWrap, SnapshotLatest as SnapshotLatestWrap, - }, - windowed_filter::Windowed, - 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_hashable::HashableProp, Prop}, - Layer, GID, - }, - storage::timeindex::{AsTime, EventTime}, - utils::time::IntoTime, - Direction, -}; +use raphtory_api::core::entities::properties::prop::{prop_hashable::HashableProp, 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)] @@ -104,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)] @@ -138,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), } @@ -172,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)] @@ -204,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)] @@ -236,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)] @@ -268,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", - NodeField::NodeName => "node_name", - NodeField::NodeType => "node_type", - } - ) - } -} - -/// 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`, …), @@ -465,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", @@ -505,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().map(HashableProp::from).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 (outermost-first ops → -/// nested `expr` fields). `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.into_iter().rev() { - 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) => 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::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().map(HashableProp::from).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()))) -} - -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), - }) -} - -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( @@ -1846,651 +560,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; - - 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)] @@ -2522,999 +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(|v| Value::try_from(&v.0)) - .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" => GqlNodeFilter::Id, - "node_name" => GqlNodeFilter::Name, - "node_type" => 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::*; - 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] - 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 builder → 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 { - 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::*; - - // `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 - // builder'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()); - } - - // Aggregation ops on a degree filter fail at conversion time with a clear - // message (previously they slipped through and failed at evaluation). - #[test] - fn degree_rejects_aggregation_ops_at_conversion() { - 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"); - }; - assert!( - err.to_string() - .contains("aggregation ops are not supported"), - "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::{ - property_filter::ops::PropertyFilterOps, ComposableFilter, PropertyFilterFactory, - TryAsCompositeFilter, ViewWrapOps, - }; - - 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_round_trips_through_the_wire() { - let prop = || { - ExplodedEdgeFilter - .property("w") - .gt(1i64) - .try_as_composite_exploded_edge_filter() - .unwrap() - }; - 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::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(); - let back = CompositeExplodedEdgeFilter::try_from(gql).unwrap(); - assert_eq!(original, back, "round-trip changed the filter"); - } - } - - // 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 tree = ExplodedEdgeFilter - .property("w") - .gt(1i64) - .try_as_filter_tree() - .unwrap(); - let gql = GqlFilter::try_from(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 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) = CompositeExplodedEdgeFilter::try_from(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::{ - edge_filter::EdgeFilter as EdgeFilterBuilder, graph_filter::GraphFilter, - node_filter::NodeFilter as NodeFilterBuilder, property_filter::ops::PropertyFilterOps, - ComposableFilter, PropertyFilterFactory, TryAsCompositeFilter, ViewWrapOps, - }; - - // 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(_))); - } - - // A mixed node∧edge combination exports structurally and converts to the - // wire form — the case the single-kind exports cannot represent. - #[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(_))); - - 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 graph-view chain exports outermost-first and converts to the nested - // wire form. - #[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 { .. })); - - let gql = GqlFilter::try_from(tree).unwrap(); - let GqlFilter::Graph(GqlGraphFilter::Layers(ref l)) = gql else { - panic!("expected Graph(Layers), got {gql:?}"); - }; - 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..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, @@ -33,12 +34,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::*, @@ -540,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: CompositeNodeFilter = sel.try_into()?; + let nf = sel; let narrowed = blocking_compute({ let nn_clone = nn.clone(); move || nn_clone.select(nf) @@ -574,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: CompositeEdgeFilter = sel.try_into()?; - let narrowed = blocking_compute(move || base.select(ef)).await?; + let narrowed = blocking_compute(move || base.select(sel)).await?; return Ok(GqlEdges::new(narrowed)); } @@ -722,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 { @@ -801,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 bad74a7224..548df3ef25 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 mod filter_expr_input; pub mod filtering; pub mod graph; pub mod history; 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-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 df61812d9a..fcb30bde3a 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, @@ -24,12 +25,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, @@ -289,9 +285,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) @@ -453,33 +447,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: CompositeEdgeFilter = sel.try_into()?; - 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: CompositeEdgeFilter = sel.try_into()?; - 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: CompositeEdgeFilter = sel.try_into()?; - 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)) @@ -488,12 +479,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: CompositeNodeFilter = expr.try_into()?; - 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)) @@ -502,12 +492,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: CompositeNodeFilter = expr.try_into()?; - 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)) @@ -516,12 +505,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: CompositeNodeFilter = expr.try_into()?; - 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 9e22939fac..bc03cc64ca 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, @@ -25,9 +26,7 @@ use raphtory::{ }, graph::{ nodes::{IntoDynNodes, Nodes}, - views::filter::model::{ - edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, DynFilter, - }, + views::filter::model::DynFilter, }, }, errors::GraphError, @@ -308,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, } } @@ -573,12 +570,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: CompositeNodeFilter = expr.try_into()?; - 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)) @@ -587,12 +583,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: CompositeNodeFilter = expr.try_into()?; - 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)) @@ -601,51 +596,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: CompositeNodeFilter = expr.try_into()?; - 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: CompositeEdgeFilter = expr.try_into()?; - 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: CompositeEdgeFilter = expr.try_into()?; - 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: CompositeEdgeFilter = expr.try_into()?; - 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 55c2d6e02c..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, @@ -14,10 +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::{CompositeNodeFilter, DynFilter}, - }, + graph::{path::PathFromGraph, views::filter::model::DynFilter}, }, errors::GraphError, prelude::*, @@ -543,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: CompositeNodeFilter = expr.try_into()?; - 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)) @@ -555,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: CompositeNodeFilter = expr.try_into()?; - 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)) @@ -567,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: CompositeNodeFilter = expr.try_into()?; - 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 4cd62463af..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}, @@ -17,10 +18,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::*, @@ -531,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: CompositeNodeFilter = expr.try_into()?; - 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)) @@ -546,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: CompositeNodeFilter = expr.try_into()?; - 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)) @@ -561,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: CompositeNodeFilter = expr.try_into()?; - 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/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/algo_tests/components.rs b/raphtory-tests/tests/algo_tests/components.rs index b247462e8b..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, TryAsCompositeFilter, ViewWrapOps, - }, + model::{graph_filter::GraphFilter, PropertyExprFactory, ViewWrapOps}, CreateFilter, }, }, @@ -284,7 +281,7 @@ mod in_component_test { assert_eq!(results, correct); } - fn check_node_filtered( + fn check_node_filtered( graph: &Graph, node_id: u64, filter: F, @@ -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/edge_expr_filter.rs b/raphtory-tests/tests/edge_expr_filter.rs new file mode 100644 index 0000000000..61a0c011b2 --- /dev/null +++ b/raphtory-tests/tests/edge_expr_filter.rs @@ -0,0 +1,1061 @@ +use raphtory::{ + db::{ + api::view::Filter, + graph::views::filter::model::{ComposableFilter, EdgeFilter, PropertyExprFactory}, + }, + prelude::*, +}; + +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 — PropValueSetExpr (linear scan, Option) and +// SetEdgeFilter (HashSet, Option) +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +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 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().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"]); +} + +#[test] +fn test_edge_quantified_any_starts_with() { + let g = genre_graph(); + let filter = EdgeFilter + .property("tag") + .temporal() + .starts_with("rock") + .any(); + 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() + .ends_with("roll") + .any(); + 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() + .not_contains("rock") + .any(); + 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().contains("rock").all(); + 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() + .starts_with("rock") + .all(); + 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() + .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"]); +} + +#[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() + .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"]); +} + +#[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() + .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"]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Step 5: Re-aggregation chains on EdgeAggregated +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn test_edge_aggregated_last_then_sum() { + // Property is list-valued at each timestamp. + // .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(); + // 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(); + + 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() { + // Property is list-valued at each timestamp. + // .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(); + // 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(); + + let filter = EdgeFilter + .property("score") + .temporal() + .last() + .len() + .eq(2usize); + let result = g.filter(filter).unwrap(); + 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(); + // 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(); + + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .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(); + // 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(); + + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .contains("rock") + .all(); + 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").eq(Prop::Bool(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").eq(Prop::Bool(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() { + 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(); + // 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(); + + // 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"]); +} + +#[test] +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(); + // 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(); + + // 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 = 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"]); +} + +#[test] +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(); + // 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(); + + // last temporal snapshot's list — any element is in {"metal"} + let filter = EdgeFilter + .property("tag") + .temporal() + .last() + .is_in([Prop::str("metal")]) + .any(); + let result = g.filter(filter).unwrap(); + assert_eq!(sorted_edges(result), vec!["A->B"]); +} 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 45c252859b..d277a089ec 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 deb8f80d0c..8a2dc8785e 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 2be6b8863b..4f55652379 100644 --- a/raphtory-tests/tests/filter_tests/mod.rs +++ b/raphtory-tests/tests/filter_tests/mod.rs @@ -6,7 +6,6 @@ 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/node_property_filter.rs b/raphtory-tests/tests/filter_tests/node_property_filter.rs index 824d36fa37..1a773e49b5 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::{ assertions::assert_edges_equal, views::filter::model::{ - node_filter::{ops::NodeFilterOps, NodeFilter}, - property_filter::ops::PropertyFilterOps, - ComposableFilter, PropertyFilterFactory, + node_filter::{NodeFilter, NodeFilterFactory}, + ComposableFilter, PropertyExprFactory, }, }, }, @@ -34,7 +33,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-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 dd9a8a57c6..0000000000 --- a/raphtory-tests/tests/filter_tests/test_filters.rs +++ /dev/null @@ -1,10897 +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, TryAsCompositeFilter, - 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 + TryAsCompositeFilter + 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() - .try_as_composite_node_filter() - .unwrap(); - let filter = CompositeNodeFilter::Not(Box::new(filter)); - 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, TryAsCompositeFilter, - }, - 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 = 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") - .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 = 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) - .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 = 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("prop1")); - let expected_results = Vec::<&str>::new(); - assert_filter_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, - 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 = 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")); - let expected_results = vec!["1"]; - assert_filter_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, - 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 = 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)) - .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, - ); - - let filter = filter.try_as_composite_node_filter().unwrap(); - 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, TryAsCompositeFilter, - }, - 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 TryAsCompositeFilter + CreateFilter + Clone, - expected: &[&str], - ) { - assert_filter_nodes_results( - init_nodes_graph, - IdentityGraphTransformer, - filter.clone(), - &expected, - All, - ); - } - - #[track_caller] - fn apply_assertion_err( - filter: impl TryAsCompositeFilter + 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, 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() { - let filter = EdgeFilter::src().name().is_not_in(vec!["1"]).not(); - 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, - TryAsCompositeFilter, - }; - 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 = 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(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 = 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) - .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 = 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") - .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 = 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) - .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 = 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") - .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 = 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") - .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 = 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") - .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, - ); - - let filter = filter.try_as_composite_edge_filter().unwrap(); - 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 674e82db50..4a852519c4 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::{ assertions::assert_graph_equal, - views::filter::model::{ - property_filter::ops::PropertyFilterOps, PropertyFilterFactory, - }, + views::filter::model::{EntityExprFilterOps, PropertyExprFactory}, }, }, prelude::*, @@ -211,7 +209,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}; @@ -219,7 +216,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, @@ -303,10 +301,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; @@ -420,7 +415,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/test_filters.rs b/raphtory-tests/tests/test_filters.rs index 328a67e88a..d3b961aa78 100644 --- a/raphtory-tests/tests/test_filters.rs +++ b/raphtory-tests/tests/test_filters.rs @@ -1 +1,10672 @@ pub mod filter_tests; + +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, + }, + prelude::IntoProp, + }; + use raphtory_api::core::{ + entities::properties::prop::{prop_hashable::HashableProp, 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().map(HashableProp::from).collect(), + )) + } + + #[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( + 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 = prop_filter( + FilterOperator::FuzzySearch { + levenshtein_distance: 2, + prefix_match: false, + }, + single("pome"), + ); + assert!(!filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); + + 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 = 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 = 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 = prop_filter(FilterOperator::NotContains, single("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 = 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); + assert!(!res); + } + + #[test] + fn test_is_not_in_match() { + 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); + 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::IdentityGraphTransformer; + use raphtory::{ + db::{ + api::view::{filter_ops::Filter, StaticGraphViewOps}, + graph::views::filter::model::{node_filter::NodeFilter, PropertyExprFactory}, + }, + 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().eq(1u64).any(); + 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().eq(1u64).any(); + 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()).err().expect("expected PropertyMissingError"), + GraphError::PropertyMissingError(ref name) if name == "p1" + )); + assert!(matches!( + graph.persistent_graph().filter(filter).err().expect("expected PropertyMissingError"), + 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::IdentityGraphTransformer; + use raphtory::{ + db::{ + api::view::{filter_ops::Filter, EdgeViewOps, StaticGraphViewOps}, + graph::views::filter::{ + model::{edge_filter::EdgeFilter, PropertyExprFactory}, + 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, TestGraphVariants, 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, + vec![TestGraphVariants::PersistentGraph], + ); + + // 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, + vec![TestGraphVariants::PersistentGraph], + ); + + // 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, + vec![TestGraphVariants::PersistentGraph], + ); + + // 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, + vec![TestGraphVariants::PersistentGraph], + ); + + // 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, + vec![TestGraphVariants::PersistentGraph], + ); + + // 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, + vec![TestGraphVariants::PersistentGraph], + ); + } + + #[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", + "N9->N10", + ]; + assert_filter_edges_results( + init_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + vec![TestGraphVariants::Graph], + ); + } + + #[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() { + // 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", + ]; + assert_filter_edges_results( + init_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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", + "N7->N8", "N8->N9", + ]; + assert_filter_edges_results( + init_graph_for_event_ids, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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( + init_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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"]; + assert_filter_edges_results( + init_graph_for_event_ids, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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", + "N9->N10", + ]; + assert_filter_edges_results( + init_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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"]; + assert_filter_edges_results( + init_graph_for_event_ids, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + vec![TestGraphVariants::Graph], + ); + } + + #[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()).err().expect("expected PropertyMissingError"), + GraphError::PropertyMissingError(ref name) if name == "p1" + )); + assert!(matches!( + graph.persistent_graph().filter(filter).err().expect("expected PropertyMissingError"), + 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::EventOnly, + ); + } + } +} + +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_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::{ + 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::{not_filter::NotFilter, ComposableFilter, NodeViewFilterOps, ViewWrapOps}, + CreateFilter, + }, + }, + errors::GraphError, + prelude::{ + AdditionOps, EntityExprFilterOps, Graph, GraphViewOps, IntoProp, NodeFilter, + NodeFilterFactory, 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.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().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().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 string_invalid_filters { + assert!( + matches!(graph.filter(filter), Err(GraphError::InvalidFilter(_))), + "expected InvalidFilter for string op on numeric degree" + ); + } + } + + 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()), + ]; + 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" + ); + } + } + } + + #[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"); + // node 4 has no node_type; None cannot satisfy a value comparison. + 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"]); + // node 4 has no node_type; None cannot satisfy a value comparison. + 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"); + // node 4 has no node_type; None cannot satisfy a value comparison. + 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() { + // 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, + 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::{init_nodes_graph, IdentityGraphTransformer}; + use raphtory::{ + db::graph::views::filter::model::{ + graph_filter::GraphFilter, node_filter::NodeFilter, windowed_filter::Windowed, + ComposableFilter, PropertyExprFactory, ViewWrapOps, + }, + prelude::{EntityAggOps, EntityExprFilterOps}, + }; + 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().eq("Gold_ship").all(); + 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().ne("Gold_ship").all(); + // 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, + 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().lt(10u64).all(); + 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().le(10u64).all(); + // 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, + 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().gt(1u64).all(); + 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().ge(5u64).all(); + 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() + .is_in(vec![Prop::U64(2)]) + .any(); + 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() + .is_not_in(vec![Prop::U64(2)]) + .all(); + 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() + .starts_with("Pap") + .any(); + 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() + .starts_with("Gold") + .all(); + 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() + .ends_with("ship") + .any(); + 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() + .ends_with("ship") + .all(); + 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() + .contains("Paper") + .any(); + 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().contains("Gold").all(); + 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() + .not_contains("ship") + .any(); + 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() + .not_contains("boat") + .all(); + 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() { + // `.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, + 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() + .eq("Gold_boat") + .any(); + + 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() + .eq("Gold_boat") + .all(); + + 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() + .eq("Paper_airplane") + .any(); + + // 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] + #[ignore] // TODO: Enable this when node layer is supported + fn test_graph_filter_layer() { + let filter = GraphFilter.layer("fire_nation"); + let expected_results = vec!["1", "3"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + let filter = GraphFilter.layer("air_nomads"); + 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_graph_filter_window_then_layer() { + let filter = GraphFilter.window(1, 3).layer("fire_nation"); + let expected_results = vec!["1", "3"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + let filter = GraphFilter.window(4, 4).layer("air_nomads"); + 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_graph_filter_layer_then_window() { + let filter = GraphFilter.layer("fire_nation").window(1, 3); + let expected_results = vec!["1", "3"]; + assert_filter_nodes_results( + init_nodes_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, + ); + } +} + +// 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::{ + not_filter::NotFilter, ComposableFilter, NodeFilterFactory, PropertyExprFactory, + }, + prelude::{EntityExprFilterOps, NodeFilter}, + }; + 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 = 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, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + let filter = NotFilter(NodeFilter.name().eq("2")) + .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, + ); + } +} + +// 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::{ + db::{ + api::view::StaticGraphViewOps, + graph::views::filter::model::{ + node_filter::NodeFilter, CombinedFilter, PropertyExprFactory, + }, + }, + prelude::{ + AdditionOps, EntityAggOps, EntityExprFilterOps, 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: 1, max: 3, 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 + } + + fn apply_assertion(filter: impl CombinedFilter, expected: &[&str]) { + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected, + All, + ); + } + + fn apply_assertion_err(filter: impl CombinedFilter, expected: &str) { + assert_filter_nodes_err( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &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() + .sum() + .eq(Prop::U64(6)) + .all(); + 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() + .sum() + .eq(Prop::U64(6)) + .all(); + 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() + .sum() + .eq(Prop::U64(6)) + .all(); + 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() + .sum() + .eq(Prop::U64(6)) + .all(); + 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() + .sum() + .eq(Prop::I64(6)) + .all(); + 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() + .sum() + .eq(Prop::I64(0)) + .all(); + 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() + .sum() + .eq(Prop::F64(6.5)) + .all(); + 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() + .sum() + .eq(Prop::F64(90.0)) + .all(); + 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() + .avg() + .eq(Prop::F64(2.0)) + .all(); + 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() + .avg() + .eq(Prop::F64(2.0)) + .all(); + 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() + .avg() + .eq(Prop::F64(2.0)) + .all(); + 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() + .avg() + .eq(Prop::F64(2.0)) + .all(); + 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() + .avg() + .eq(Prop::F64(2.0)) + .all(); + 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() + .avg() + .eq(Prop::F64(0.0)) + .all(); + 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() + .avg() + .eq(Prop::F64(2.1666666666666665)) + .all(); + 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() + .avg() + .eq(Prop::F64(45.0)) + .all(); + 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() + .min() + .eq(Prop::U8(1)) + .all(); + 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() + .min() + .eq(Prop::U16(1)) + .all(); + 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() + .min() + .eq(Prop::U32(1)) + .all(); + 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() + .min() + .eq(Prop::U64(1)) + .all(); + 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() + .min() + .eq(Prop::I32(-2)) + .all(); + let expected = vec!["n6"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_all_min_i64s() { + let filter = NodeFilter + .property("p_i64s") + .temporal() + .min() + .eq(Prop::I64(-3)) + .all(); + 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() + .min() + .eq(Prop::F32(1.0)) + .all(); + 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() + .min() + .eq(Prop::F64(30.0)) + .all(); + 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() + .max() + .eq(Prop::U8(3)) + .all(); + let expected = vec!["n10"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_all_max_u16s() { + let filter = NodeFilter + .property("p_u16s") + .temporal() + .max() + .eq(Prop::U16(3)) + .all(); + let expected = vec!["n10"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_all_max_u32s() { + let filter = NodeFilter + .property("p_u32s") + .temporal() + .max() + .eq(Prop::U32(3)) + .all(); + let expected = vec!["n10"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_all_max_u64s() { + let filter = NodeFilter + .property("p_u64s") + .temporal() + .max() + .eq(Prop::U64(4)) + .all(); + let expected = vec!["n2"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_all_max_i32s() { + let filter = NodeFilter + .property("p_i32s") + .temporal() + .max() + .eq(Prop::I32(3)) + .all(); + 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() + .max() + .eq(Prop::I64(2)) + .all(); + let expected = vec!["n10"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_all_max_f32s() { + let filter = NodeFilter + .property("p_f32s") + .temporal() + .max() + .eq(Prop::F32(3.5)) + .all(); + let expected = vec!["n10"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_all_max_f64s() { + let filter = NodeFilter + .property("p_f64s") + .temporal() + .max() + .eq(Prop::F64(50.0)) + .all(); + 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() + .len() + .eq(Prop::U64(3)) + .all(); + 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() + .len() + .eq(Prop::U64(3)) + .all(); + 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() + .len() + .eq(Prop::U64(3)) + .all(); + 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() + .len() + .eq(Prop::U64(4)) + .all(); + let expected = vec!["n2"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_all_len_i32s() { + let filter = NodeFilter + .property("p_i32s") + .temporal() + .len() + .eq(Prop::U64(3)) + .all(); + 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() + .len() + .eq(Prop::U64(2)) + .all(); + let expected = vec!["n5"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_all_len_f32s() { + let filter = NodeFilter + .property("p_f32s") + .temporal() + .len() + .eq(Prop::U64(3)) + .all(); + 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() + .len() + .eq(Prop::U64(2)) + .all(); + 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() + .sum() + .eq(Prop::U64(6)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u8s") + .temporal() + .sum() + .eq(Prop::U64(10)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_sum_u16s() { + let filter = NodeFilter + .property("p_u16s") + .temporal() + .sum() + .eq(Prop::U64(6)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u16s") + .temporal() + .sum() + .eq(Prop::U64(10)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_sum_u32s() { + let filter = NodeFilter + .property("p_u32s") + .temporal() + .sum() + .eq(Prop::U64(6)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u32s") + .temporal() + .sum() + .eq(Prop::U64(10)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_sum_u64s() { + let filter = NodeFilter + .property("p_u64s") + .temporal() + .sum() + .eq(Prop::U64(6)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u64s") + .temporal() + .sum() + .eq(Prop::U64(10)) + .any(); + 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() + .sum() + .eq(Prop::I64(6)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i32s") + .temporal() + .sum() + .eq(Prop::I64(60)) + .any(); + let expected = vec!["n4"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_sum_i64s() { + let filter = NodeFilter + .property("p_i64s") + .temporal() + .sum() + .eq(Prop::I64(0)) + .any(); + let expected = vec!["n3", "n10"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i64s") + .temporal() + .sum() + .eq(Prop::I64(10)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_sum_f32s() { + let filter = NodeFilter + .property("p_f32s") + .temporal() + .sum() + .eq(Prop::F64(6.5)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f32s") + .temporal() + .sum() + .eq(Prop::F64(60.0)) + .any(); + let expected = vec!["n4"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_sum_f64s() { + let filter = NodeFilter + .property("p_f64s") + .temporal() + .sum() + .eq(Prop::F64(90.0)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f64s") + .temporal() + .sum() + .eq(Prop::F64(120.0)) + .any(); + 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() + .avg() + .eq(Prop::F64(2.0)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u8s") + .temporal() + .avg() + .eq(Prop::F64(2.5)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_avg_u16s() { + let filter = NodeFilter + .property("p_u16s") + .temporal() + .avg() + .eq(Prop::F64(2.0)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u16s") + .temporal() + .avg() + .eq(Prop::F64(2.5)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_avg_u32s() { + let filter = NodeFilter + .property("p_u32s") + .temporal() + .avg() + .eq(Prop::F64(2.0)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u32s") + .temporal() + .avg() + .eq(Prop::F64(2.5)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_avg_u64s() { + let filter = NodeFilter + .property("p_u64s") + .temporal() + .avg() + .eq(Prop::F64(2.0)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u64s") + .temporal() + .avg() + .eq(Prop::F64(2.5)) + .any(); + 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() + .avg() + .eq(Prop::F64(2.0)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i32s") + .temporal() + .avg() + .eq(Prop::F64(2.5)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_avg_i64s() { + let filter = NodeFilter + .property("p_i64s") + .temporal() + .avg() + .eq(Prop::F64(0.0)) + .any(); + let expected = vec!["n3", "n10"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i64s") + .temporal() + .avg() + .eq(Prop::F64(2.5)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_avg_f32s() { + let filter = NodeFilter + .property("p_f32s") + .temporal() + .avg() + .eq(Prop::F64(2.1666666666666665)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f32s") + .temporal() + .avg() + .eq(Prop::F64(20.0)) + .any(); + let expected = vec!["n4"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_avg_f64s() { + let filter = NodeFilter + .property("p_f64s") + .temporal() + .avg() + .eq(Prop::F64(45.0)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f64s") + .temporal() + .avg() + .eq(Prop::F64(40.0)) + .any(); + 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() + .min() + .eq(Prop::U8(1)) + .any(); + 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() + .min() + .eq(Prop::U16(1)) + .any(); + 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() + .min() + .eq(Prop::U32(1)) + .any(); + 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() + .min() + .eq(Prop::U64(1)) + .any(); + 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() + .min() + .eq(Prop::I32(-2)) + .any(); + let expected = vec!["n6"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i32s") + .temporal() + .min() + .eq(Prop::I32(10)) + .any(); + let expected = vec!["n4"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_min_i64s() { + let filter = NodeFilter + .property("p_i64s") + .temporal() + .min() + .eq(Prop::I64(-3)) + .any(); + let expected = vec!["n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i64s") + .temporal() + .min() + .eq(Prop::I64(1)) + .any(); + 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() + .min() + .eq(Prop::F32(1.0)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f32s") + .temporal() + .min() + .eq(Prop::F32(10.0)) + .any(); + let expected = vec!["n4"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_min_f64s() { + let filter = NodeFilter + .property("p_f64s") + .temporal() + .min() + .eq(Prop::F64(30.0)) + .any(); + let expected = vec!["n1", "n2", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f64s") + .temporal() + .min() + .eq(Prop::F64(40.0)) + .any(); + 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() + .max() + .eq(Prop::U8(3)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u8s") + .temporal() + .max() + .eq(Prop::U8(4)) + .any(); + 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() + .max() + .eq(Prop::U16(3)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u16s") + .temporal() + .max() + .eq(Prop::U16(4)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_max_u32s() { + let filter = NodeFilter + .property("p_u32s") + .temporal() + .max() + .eq(Prop::U32(3)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u32s") + .temporal() + .max() + .eq(Prop::U32(4)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_max_u64s() { + let filter = NodeFilter + .property("p_u64s") + .temporal() + .max() + .eq(Prop::U64(4)) + .any(); + let expected = vec!["n1", "n2"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u64s") + .temporal() + .max() + .eq(Prop::U64(3)) + .any(); + 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() + .max() + .eq(Prop::I32(3)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4", "n6"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i32s") + .temporal() + .max() + .eq(Prop::I32(30)) + .any(); + let expected = vec!["n4"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_max_i64s() { + let filter = NodeFilter + .property("p_i64s") + .temporal() + .max() + .eq(Prop::I64(2)) + .any(); + let expected = vec!["n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i64s") + .temporal() + .max() + .eq(Prop::I64(2)) + .any(); + 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() + .max() + .eq(Prop::F32(3.5)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f32s") + .temporal() + .max() + .eq(Prop::F32(30.0)) + .any(); + let expected = vec!["n4"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_max_f64s() { + let filter = NodeFilter + .property("p_f64s") + .temporal() + .max() + .eq(Prop::F64(50.0)) + .any(); + 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() + .len() + .is_in(vec![Prop::U64(3)]) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u8s") + .temporal() + .len() + .eq(Prop::U64(4)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_len_u16s() { + let filter = NodeFilter + .property("p_u16s") + .temporal() + .len() + .eq(Prop::U64(3)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u16s") + .temporal() + .len() + .eq(Prop::U64(4)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_len_u32s() { + let filter = NodeFilter + .property("p_u32s") + .temporal() + .len() + .eq(Prop::U64(3)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u32s") + .temporal() + .len() + .eq(Prop::U64(4)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_len_u64s() { + let filter = NodeFilter + .property("p_u64s") + .temporal() + .len() + .eq(Prop::U64(4)) + .any(); + let expected = vec!["n1", "n2"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_u64s") + .temporal() + .len() + .eq(Prop::U64(3)) + .any(); + 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() + .len() + .eq(Prop::U64(3)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4", "n6"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i32s") + .temporal() + .len() + .eq(Prop::U64(4)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_len_i64s() { + let filter = NodeFilter + .property("p_i64s") + .temporal() + .len() + .eq(Prop::U64(2)) + .any(); + let expected = vec!["n5"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_i64s") + .temporal() + .len() + .eq(Prop::U64(4)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_len_f32s() { + let filter = NodeFilter + .property("p_f32s") + .temporal() + .len() + .eq(Prop::U64(3)) + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f32s") + .temporal() + .len() + .eq(Prop::U64(4)) + .any(); + let expected = vec!["n1"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_property_temporal_any_len_f64s() { + let filter = NodeFilter + .property("p_f64s") + .temporal() + .len() + .eq(Prop::U64(2)) + .any(); + let expected = vec!["n1", "n10", "n3"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f64s") + .temporal() + .len() + .eq(Prop::U64(3)) + .any(); + 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() { + // 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"); + apply_assertion_err(filter, expected); + + let filter = NodeFilter.property("p_u64s").avg().ends_with("abc"); + apply_assertion_err(filter, expected); + + let filter = NodeFilter.property("p_u64s").len().contains("abc"); + apply_assertion_err(filter, expected); + + let filter = NodeFilter.property("p_u64s").sum().not_contains("abc"); + 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 --------------- + // ------ Property: any ------ + #[test] + fn test_node_property_any() { + let filter = NodeFilter.property("p_u8s").eq(Prop::U8(3)).any(); + 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").eq(Prop::U64(1)).any(); + let expected = vec!["n10", "n7"]; + apply_assertion(filter, &expected); + } + + // ------ Metadata: all ------ + #[test] + fn test_node_metadata_all() { + let filter = NodeFilter.metadata("p_strs").eq("a").all(); + 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() + .eq(false) + .any(); + 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() + .eq(true) + .all(); + 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() + .eq(Prop::F32(3.5)) + .any(); + 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() + .eq(true) + .all(); + 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() + .eq(Prop::F32(3.5)) + .any() + .any(); + let expected = vec!["n1", "n10", "n3", "n4"]; + apply_assertion(filter, &expected); + + let filter = NodeFilter + .property("p_f32s") + .temporal() + .eq(Prop::F32(30.0)) + .any() + .any(); + 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() + .eq(false) + .all() + .any(); + 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") + .gt(45.0) + .any() + .all() + .all() + .all(); + + 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() + .gt(45.0) + .any() + .all() + .all() + .all() + .all(); + + let expected = vec!["n1", "n3"]; + apply_assertion(filter, &expected); + } + + // ------ Temporal All: any ------ + #[test] + fn test_node_temporal_property_all_any() { + let filter = NodeFilter + .property("p_bools") + .temporal() + .eq(true) + .any() + .all(); + 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() + .eq(true) + .all(); + 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 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(filter, &["n1"]); + + let filter = NodeFilter + .property("p_u16s_max") + .sum() + .eq(Prop::U64(131070)); + apply_assertion(filter, &["n1"]); + + let filter = NodeFilter + .property("p_u32s_max") + .sum() + .eq(Prop::U64(8589934590)); + apply_assertion(filter, &["n1"]); + + // Reductions that return an element keep the element type, so a + // 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(filter, &[]); + + let filter = NodeFilter.property("p_u8s_max").min().eq(Prop::U64(510)); + apply_assertion(filter, &[]); + + 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 { + use crate::{ + 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, ComposableFilter, EdgeViewFilterOps, EntityExprFilterOps, + NodeFilterFactory, PropertyExprFactory, 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() { + let filter = EdgeFilter::src().name().is_not_in(vec!["1"]).not(); + 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, + ); + } + + // Disk graph doesn't support deletions + #[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::{init_edges_graph, init_edges_graph2, IdentityGraphTransformer}; + use raphtory::db::graph::views::filter::model::{ + edge_filter::EdgeFilter, ComposableFilter, PropertyExprFactory, ViewWrapOps, + }; + + use raphtory::prelude::{EntityAggOps, EntityExprFilterOps}; + use raphtory_api::core::entities::properties::prop::Prop; + use raphtory_tests::assertions::{ + assert_filter_edges_results, TestGraphVariants, TestVariants, + }; + + #[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( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter.property("p20").temporal().eq("Gold_ship").all(); + let expected_results = vec!["1->2"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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", + "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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter.property("p30").temporal().ne("Classic").all(); + let expected_results = vec!["1->2", "2->3"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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", + "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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter.property("p2").temporal().lt(10u64).all(); + 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::EventOnly, + ); + } + + #[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", + "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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter.property("p2").temporal().le(5u64).all(); + let expected_results = vec!["1->2", "2->3"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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", + "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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter.property("p2").temporal().gt(5u64).all(); + 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::EventOnly, + ); + } + + #[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", + "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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter.property("p2").temporal().ge(6u64).all(); + 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::EventOnly, + ); + } + + #[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", + "3->1", + "David Gilmour->John Mayer", + "John Mayer->Jimmy Page", + ]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + 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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter + .property("p2") + .temporal() + .is_in(vec![Prop::U64(6)]) + .all(); + 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::EventOnly, + ); + } + + #[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( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter + .property("p2") + .temporal() + .is_not_in(vec![Prop::U64(6)]) + .all(); + let expected_results = vec!["1->2", "2->3"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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", + "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::EventOnly, + ); + + 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::EventOnly, + ); + } + + #[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( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + 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::EventOnly, + ); + } + + #[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( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + let filter = EdgeFilter + .property("p10") + .temporal() + .starts_with("Pape") + .any(); + 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::EventOnly, + ); + + 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::EventOnly, + ); + + 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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter + .property("p20") + .temporal() + .starts_with("Gold") + .all(); + let expected_results: Vec<&str> = vec!["1->2", "2->3"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + let filter = EdgeFilter + .property("p10") + .temporal() + .ends_with("ship") + .any(); + let expected_results: Vec<&str> = vec!["2->3"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + 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::EventOnly, + ); + + 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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter + .property("p20") + .temporal() + .ends_with("ship") + .all(); + let expected_results: Vec<&str> = vec!["1->2"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + let filter = EdgeFilter + .property("p10") + .temporal() + .contains("Paper") + .any(); + 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::EventOnly, + ); + + 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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter.property("p20").temporal().contains("ship").all(); + let expected_results: Vec<&str> = vec!["1->2"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[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( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + let filter = EdgeFilter + .property("p10") + .temporal() + .not_contains("ship") + .any(); + let expected_results: Vec<&str> = vec!["1->2", "2->1"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + 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::EventOnly, + ); + + 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::EventOnly, + ); + + let filter = EdgeFilter + .property("p30") + .temporal() + .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, + ); + } + + #[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"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + vec![TestGraphVariants::Graph], + ); + + 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, + vec![TestGraphVariants::Graph], + ); + + 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, + vec![TestGraphVariants::Graph], + ); + } + + #[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( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::EventOnly, + ); + } + + #[test] + fn test_edges_window_filter() { + let filter = EdgeFilter + .window(1, 3) + .property("p2") + .temporal() + .sum() + .ge(2u64); + + let expected_results = vec!["1->2", "2->3"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + 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() + .eq("Gold_boat") + .any(); + + 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() + .eq("Gold_boat") + .all(); + + 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() + .eq("Paper_airplane") + .any(); + + 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, + ); + } +} + +// 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, not_filter::NotFilter, ComposableFilter, NodeFilterFactory, + PropertyExprFactory, + }, + prelude::EntityExprFilterOps, + }; + use raphtory_tests::assertions::{ + assert_filter_edges_results, TestGraphVariants, TestVariants, + }; + + use crate::{init_edges_graph, IdentityGraphTransformer}; + + #[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") + .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::EventOnly, + ); + } + + #[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) + .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::EventOnly, + ); + + 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::EventOnly, + ); + } + + #[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") + .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, + vec![TestGraphVariants::Graph], + ); + + 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, + vec![TestGraphVariants::Graph], + ); + + 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, + vec![TestGraphVariants::Graph], + ); + + 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, + vec![TestGraphVariants::Graph], + ); + + 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, + vec![TestGraphVariants::Graph], + ); + + 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, + vec![TestGraphVariants::Graph], + ); + + 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::EventOnly, + ); + + 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, + vec![TestGraphVariants::Graph], + ); + } + + #[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() + .eq("13") + .and(EdgeFilter.property("p1").eq("prop1")), + ); + 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::EventOnly, + ); + + let filter = NotFilter( + EdgeFilter::src() + .name() + .eq("13") + .and(NotFilter(EdgeFilter.property("p1").eq("prop1"))), + ); + 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::EventOnly, + ); + } +} diff --git a/raphtory/src/db/api/state/ops/filter.rs b/raphtory/src/db/api/state/ops/filter.rs index 293fd3c046..b3bb6da90f 100644 --- a/raphtory/src/db/api/state/ops/filter.rs +++ b/raphtory/src/db/api/state/ops/filter.rs @@ -1,37 +1,15 @@ use crate::{ db::{ api::{ - state::{ - ops::{Const, Degree, IntoDynNodeOp, NodeOp}, - Index, - }, - view::internal::{GraphView, InnerFilterOps, NodeList}, - }, - graph::{ - create_node_type_filter, - views::filter::model::{ - degree_filter::DegreeFilter, - filter::{Filter, FilterValue}, - node_filter::NodeFilter, - property_filter::{Op, PropertyFilterValue, PropertyRef}, - FilterOperator, - }, + state::ops::{Const, IntoDynNodeOp, NodeOp}, + view::internal::{GraphView, NodeList}, }, + graph::create_node_type_filter, }, - prelude::{GraphViewOps, PropertyFilter}, -}; -use raphtory_api::core::entities::{ - properties::{meta::NODE_ID_PROP_ID, prop::Prop}, - VID, -}; -use raphtory_core::entities::nodes::node_ref::AsNodeRef; -use raphtory_storage::{ - core_ops::CoreGraphOps, - graph::{ - graph::{GraphStorage, NodeGlobalPropCandidates, NodePropPredicate, NodePropSemantics}, - nodes::node_storage_ops::NodeStorageOps, - }, + prelude::GraphViewOps, }; +use raphtory_api::core::entities::VID; +use raphtory_storage::{core_ops::CoreGraphOps, graph::graph::GraphStorage}; use std::sync::Arc; use storage::api::node_type_index::NodeTypeIndexOps; @@ -93,303 +71,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 { - FilterValue::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 { - FilterValue::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, _storage: &GraphStorage) -> 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 NodeNameFilterOp { - /// A node's name is its external id (GID), so an index over it can serve the - /// pattern operators — the ones `domain` would otherwise answer with - /// `All`, i.e. a scan of every node. - fn index_candidates(&self, storage: &GraphStorage) -> Option { - let FilterValue::Single(pattern) = &self.filter.field_value else { - return None; - }; - let predicate = match &self.filter.operator { - FilterOperator::StartsWith => NodePropPredicate::StartsWith(pattern), - FilterOperator::EndsWith => NodePropPredicate::EndsWith(pattern), - FilterOperator::Contains => NodePropPredicate::Contains(pattern), - _ => return None, - }; - let mut candidates = storage.node_prop_candidates( - NODE_ID_PROP_ID, - true, - &predicate, - NodePropSemantics::Latest, - )?; - candidates.exact = false; - Some(candidates) - } -} - -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 { - FilterValue::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 { - FilterValue::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(), - }, - _ => match self.index_candidates(storage) { - Some(candidates) => NodeList::List { - elems: Index::from_sorted(candidates.vids, candidates.exact), - } - .intersection(&NodeList::All), - None => 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, _storage: &GraphStorage) -> 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, - } - } - - /// The storage-level predicate for index pushdown, when the filter shape - /// allows it: no value-transforming ops and a positive operator. The - /// candidates the storage returns are supersets, so `apply` still runs on - /// every candidate (`const_value_in_domain` stays `None`). - fn pushdown_predicate(&self) -> Option> { - match (&self.filter.operator, &self.filter.prop_value) { - (FilterOperator::Eq, PropertyFilterValue::Single(v)) => Some(NodePropPredicate::Eq(v)), - (FilterOperator::Lt, PropertyFilterValue::Single(v)) => Some(NodePropPredicate::Lt(v)), - (FilterOperator::Le, PropertyFilterValue::Single(v)) => Some(NodePropPredicate::Le(v)), - (FilterOperator::Gt, PropertyFilterValue::Single(v)) => Some(NodePropPredicate::Gt(v)), - (FilterOperator::Ge, PropertyFilterValue::Single(v)) => Some(NodePropPredicate::Ge(v)), - (FilterOperator::IsIn, PropertyFilterValue::Set(values)) => { - Some(NodePropPredicate::In(values.as_ref())) - } - (FilterOperator::StartsWith, PropertyFilterValue::Single(Prop::Str(p))) => { - Some(NodePropPredicate::StartsWith(&**p)) - } - (FilterOperator::EndsWith, PropertyFilterValue::Single(Prop::Str(p))) => { - Some(NodePropPredicate::EndsWith(&**p)) - } - (FilterOperator::Contains, PropertyFilterValue::Single(Prop::Str(p))) => { - Some(NodePropPredicate::Contains(&**p)) - } - _ => None, - } - } -} - -impl NodePropertyFilterOp { - /// The value semantics to request from the storage index, plus whether - /// its exactness claim may be kept. Latest-flag candidates are a SUBSET - /// of what windowed or layer-restricted views need (a row's visible - /// latest can differ from its global latest), so restricted views fall - /// back to Ever candidates — a superset for every view — with exactness - /// off. `temporal().any()` is served by Ever directly; aggregating - /// chains are not served. - fn pushdown_semantics(&self) -> Option<(NodePropSemantics, bool)> { - let plain_view = !self.graph.window_filtered() && !self.graph.is_layer_filtered(); - match (&self.filter.prop_ref, self.filter.ops.as_slice()) { - (PropertyRef::Property(_) | PropertyRef::Metadata(_), []) - | (PropertyRef::TemporalProperty(_), [Op::Last]) => Some(if plain_view { - (NodePropSemantics::Latest, true) - } else { - (NodePropSemantics::Ever, false) - }), - (PropertyRef::TemporalProperty(_), [Op::Any]) => { - Some((NodePropSemantics::Ever, plain_view)) - } - _ => None, - } - } - - fn index_candidates(&self, storage: &GraphStorage) -> Option { - let (semantics, exact_allowed) = self.pushdown_semantics()?; - let predicate = self.pushdown_predicate()?; - let metadata = matches!(self.filter.prop_ref, PropertyRef::Metadata(_)); - let mut candidates = - storage.node_prop_candidates(self.prop_id, metadata, &predicate, semantics)?; - candidates.exact &= exact_allowed; - Some(candidates) - } -} - -impl NodeOp for NodePropertyFilterOp { - type Output = bool; - - fn domain(&self, storage: &GraphStorage) -> NodeList { - if let Some(candidates) = self.index_candidates(storage) { - // index candidates are ascending and deduplicated, as `from_sorted` requires - let list = NodeList::List { - elems: Index::from_sorted(candidates.vids, candidates.exact), - }; - return list.intersection(&self.graph.node_list()); - } - // No index could serve this filter, so it has not been applied to - // anything: the inner list may be exact for the filters that built it, - // but it cannot claim to be exact for this one as well. - self.graph.node_list().into_inexact() - } - - 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/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 4a179be28f..8cba0bac14 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -3,21 +3,31 @@ pub mod history; pub mod node; pub mod properties; -use crate::db::api::{ - state::ops::filter::{AndOp, NotOp, OrOp}, - view::internal::NodeList, +use crate::db::{ + api::{ + state::ops::filter::{AndOp, NotOp, OrOp}, + view::internal::NodeList, + }, + graph::views::filter::model::{node_expr::BinaryCmpNodeOp, BinaryOp, Comparable}, }; pub use history::*; pub use node::*; pub use properties::*; -use raphtory_api::core::entities::VID; +use raphtory_api::core::entities::{properties::prop::PropType, VID}; use raphtory_storage::graph::graph::GraphStorage; use serde::{Deserialize, Serialize}; use std::{fmt::Debug, marker::PhantomData, ops::Deref, sync::Arc}; +// this probably needs the 'graph lifetime to make bin_cmp work with ops that capture the graph pub trait NodeOp: Send + Sync { type Output: Clone + Send + Sync; + /// The output type of this operation used for validation. + /// Returns `PropType::Empty` by default (unknown type). + fn prop_type(&self) -> PropType { + PropType::Empty + } + /// The domain of validity for this node op fn domain(&self, _storage: &GraphStorage) -> NodeList; @@ -39,8 +49,42 @@ 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 { +// 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 { @@ -140,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)) } @@ -174,6 +222,10 @@ impl<'a, V: Clone + Send + Sync> NodeOp for Arc + 'a> { self.deref().apply(storage, node) } + fn prop_type(&self) -> PropType { + self.deref().prop_type() + } + fn const_value(&self) -> Option { self.deref().const_value() } diff --git a/raphtory/src/db/api/state/ops/node.rs b/raphtory/src/db/api/state/ops/node.rs index f7bfdaac19..90fee1ff6e 100644 --- a/raphtory/src/db/api/state/ops/node.rs +++ b/raphtory/src/db/api/state/ops/node.rs @@ -83,6 +83,7 @@ pub struct Type; pub struct TypeStruct { node_type: Option, } + impl From> for TypeStruct { fn from(node_type: Option) -> Self { TypeStruct { node_type } diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index ab3b918329..6f705ac856 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/edge_expr_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs new file mode 100644 index 0000000000..e24b758a88 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/edge_expr_filtered_graph.rs @@ -0,0 +1,130 @@ +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/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 new file mode 100644 index 0000000000..e3792cc686 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/exploded_edge_expr_filtered_graph.rs @@ -0,0 +1,143 @@ +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; + } + // 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(), + 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/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 4da35dea85..83cf3f116e 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -11,12 +11,12 @@ use crate::{ }; 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 47da1d031b..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,14 +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, - TryAsCompositeFilter, - }, - CreateFilter, + and_filtered_graph::AndFilteredGraph, model::ComposableFilter, CreateFilter, }, }, errors::GraphError, @@ -95,47 +88,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 deleted file mode 100644 index 28c4fb57c6..0000000000 --- a/raphtory/src/db/graph/views/filter/model/degree_filter.rs +++ /dev/null @@ -1,224 +0,0 @@ -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, TryAsCompositeFilter, - }, - node_filtered_graph::NodeFilteredGraph, - CreateFilter, - }, - }, - errors::GraphError, -}; -use raphtory_api::core::{ - entities::properties::prop::{prop_hashable::HashableProp, PropType}, - Direction, -}; -use std::{collections::HashSet, fmt, fmt::Display, ops::Deref, sync::Arc}; - -#[derive(Clone)] -pub struct DegreeFilterBuilder { - direction: Direction, - ops: Vec, -} - -impl DegreeFilterBuilder { - pub fn new(direction: Direction) -> Self { - Self { - direction, - ops: Vec::new(), - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct DegreeFilter { - pub direction: Direction, - pub operator: FilterOperator, - pub value: PropertyFilterValue, - 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.deref() - .clone() - .cast(PropType::U64) - .map(HashableProp::from) - .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) - } -} - -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()), - Direction::OUT => PropertyRef::Property("out_degree".to_string()), - Direction::BOTH => PropertyRef::Property("degree".to_string()), - } -} - -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 { - 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_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs new file mode 100644 index 0000000000..f9bc339ff6 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/filters.rs @@ -0,0 +1,550 @@ +//! 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::{ + coerce_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_castable, + validate_string_op, validate_types_compatible, 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_castable(&lhs_pt, c.as_ref())?, + None => validate_types_compatible(&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_castable(&lhs_pt, c.as_ref())?, + None => validate_types_compatible(&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_castable(&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_castable(&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 = coerce_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 = coerce_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 new file mode 100644 index 0000000000..b501187f53 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/mod.rs @@ -0,0 +1,50 @@ +//! 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") ← EdgeExpr │ +//! │ .eq(5.0f64) ← BinaryCmpExpr │ +//! └──────────────────────────────────────────────────────────┘ +//! │ 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 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}; + +// ───────────────────────────────────────────────────────────────────────────── +// 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 + } + + /// 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 new file mode 100644 index 0000000000..25d952147d --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/edge_expr/ops.rs @@ -0,0 +1,467 @@ +//! 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, + }, + prelude::EdgeViewOps, +}; +use raphtory_api::core::entities::{ + edges::edge_ref::EdgeRef, + properties::prop::{Prop, PropType}, +}; +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 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 +// ───────────────────────────────────────────────────────────────────────────── + +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) + } + + fn prop_type(&self) -> PropType { + self.as_ref().prop_type() + } + + fn const_value(&self) -> Option { + self.as_ref().const_value() + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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() + } + + fn const_value(&self) -> Option { + Some(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) + } + + // 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. +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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 = 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) + .map(|(_, v)| v) + .collect(); + Some(Prop::List(PropArray::from(vals))) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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 +// +// 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 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(), + Endpoint::Dst => edge.dst(), + }; + 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/edge_filter.rs b/raphtory/src/db/graph/views/filter/model/edge_filter.rs index 049ca1078d..6adfaeefb9 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -1,47 +1,38 @@ use crate::{ db::{ api::{ - state::ops::NotANodeFilter, - view::{ - internal::{DynGraphArc, GraphView}, - BoxableGraphView, - }, + state::ops::{NodeOp, NotANodeFilter}, + view::internal::GraphView, }, graph::views::filter::{ + edge_expr_filtered_graph::EdgeExprFilteredGraph, edge_node_filtered_graph::EdgeNodeFilteredGraph, model::{ - exploded_edge_filter::CompositeExplodedEdgeFilter, + 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_filter::{ - builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder, - }, - CompositeNodeFilter, NodeFilter, - }, - property_filter::{ - builders::{ - MetadataFilterBuilder, PropertyExprBuilderInput, PropertyFilterBuilder, - }, - Op, PropertyFilter, PropertyFilterInput, PropertyRef, - }, + node_expr::{CreateOp, EntityExpr, PredicateLhs}, + node_filter::NodeFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - AndFilter, CombinedFilter, ComposableFilter, EdgeViewFilterOps, EntityMarker, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, - NotFilter, OrFilter, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + CombinedFilter, ComposableFilter, DynFilter, EdgeViewFilterOps, EntityMarker, + InternalViewWrapOps, Wrap, }, CreateFilter, }, }, errors::GraphError, }; -use raphtory_api::core::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 serde::{Deserialize, Serialize}; use std::{fmt, fmt::Display, sync::Arc}; // User facing entry for building edge filters. @@ -82,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, self.entity()) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - MetadataFilterBuilder(property, self.entity()) - } -} - impl EdgeViewFilterOps for EdgeFilter { type Output = T; @@ -120,15 +93,16 @@ 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, } -// 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, @@ -147,6 +121,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 { @@ -157,19 +136,37 @@ impl EdgeEndpointWrapper { } impl EdgeEndpointWrapper { + /// 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) + } + + #[inline] + pub fn name(&self) -> EdgeEndpointWrapper { + self.wrap(Name) + } + #[inline] - pub fn id(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::id(), self.endpoint) + pub fn node_type(&self) -> EdgeEndpointWrapper { + self.wrap(Type) } #[inline] - pub fn name(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::name(), self.endpoint) + pub fn property( + &self, + name: impl Into, + ) -> EdgeEndpointWrapper> { + self.wrap(PropertyExprFactory::property(&self.inner, name)) } #[inline] - pub fn node_type(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::node_type(), self.endpoint) + pub fn metadata( + &self, + name: impl Into, + ) -> EdgeEndpointWrapper> { + self.wrap(PropertyExprFactory::metadata(&self.inner, name)) } } @@ -184,69 +181,7 @@ 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 { - 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 ComposableFilter for EdgeEndpointWrapper where T: Clone + Send + Sync {} impl CreateFilter for EdgeEndpointWrapper { type EntityFiltered<'graph, G, F> @@ -294,86 +229,44 @@ 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) - } +// ── 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(Debug, Clone, PartialEq)] -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), +#[derive(Clone)] +struct EndpointNodeBoolOp<'g> { + endpoint: Endpoint, + node_op: Arc + 'g>, } -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<'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 CompositeEdgeFilter { +impl CreateFilter for EdgeEndpointNodeFilter { 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; + EdgeExprFilteredGraph + 'graph>>; + type NodeFilter<'graph, G: GraphView + 'graph, F: GraphView + 'graph> = NotANodeFilter; type FilteredGraph<'graph, G> - = Arc + = G where Self: 'graph, G: GraphView + 'graph; @@ -383,68 +276,14 @@ impl CreateFilter for CompositeEdgeFilter { 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)?)) - } - } + 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>( @@ -459,59 +298,114 @@ impl CreateFilter for CompositeEdgeFilter { &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)?)) - } - } + Ok(graph) } } -impl TryAsCompositeFilter for CompositeEdgeFilter { - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } +impl ComposableFilter for EdgeEndpointNodeFilter {} - fn try_as_composite_edge_filter(&self) -> Result { - Ok(self.clone()) +// ── expr layer: endpoint expressions bridge node ops into edge ops ── + +impl PredicateLhs for EdgeEndpointWrapper {} + +impl EntityExpr for EdgeEndpointWrapper { + type Marker = EdgeFilter; + fn entity(&self) -> Self::Marker { + EdgeFilter } +} - fn try_as_composite_exploded_edge_filter( +impl CreateOp for EdgeEndpointWrapper { + fn create_edge_op<'g, G: GraphView + 'g>( &self, - ) -> Result { - Err(GraphError::NotSupported) + 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 ── + +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 {} +impl EdgeFilterFactory for ExplodedEdgeFilter {} +impl EdgeFilterFactory for Windowed {} +impl EdgeFilterFactory for Latest {} +impl EdgeFilterFactory for Layered {} +impl EdgeFilterFactory for SnapshotAt {} +impl EdgeFilterFactory for SnapshotLatest {} + +// ── 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, + 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/exploded_edge_filter.rs b/raphtory/src/db/graph/views/filter/model/exploded_edge_filter.rs index 877d4b623e..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 @@ -1,37 +1,13 @@ 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}, - 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, - }, - snapshot_filter::{SnapshotAt, SnapshotLatest}, - windowed_filter::Windowed, - AndFilter, CombinedFilter, EdgeViewFilterOps, EntityMarker, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, - NotFilter, OrFilter, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + 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, }, @@ -39,7 +15,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 +54,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 +112,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> @@ -276,333 +162,3 @@ impl CreateFilter for ExplodedEdgeEndpointWra self.inner.filter_graph_view(graph) } } - -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)] -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: 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/filter.rs b/raphtory/src/db/graph/views/filter/model/filter.rs index bd23a39fb5..01d13af43f 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, } } @@ -209,7 +210,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, } } @@ -217,7 +218,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, } } @@ -225,7 +226,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, } } @@ -238,3 +239,8 @@ impl Filter { 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 1ab7ef82fa..c65ded9408 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -1,7 +1,10 @@ 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; @@ -310,3 +313,242 @@ impl FilterOperator { } } } + +// ── 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; +} + +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, + }, +} + +/// 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 { + 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 { + /// 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::*; + match op { + 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)), + } + } +} + +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/filter_value.rs b/raphtory/src/db/graph/views/filter/model/filter_value.rs new file mode 100644 index 0000000000..ef778604c1 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/filter_value.rs @@ -0,0 +1,50 @@ +use std::{collections::HashSet, fmt, hash::Hash, sync::Arc}; + +/// A generic filter value container used by both property and attribute filters. +/// +/// `T` is the value type being compared against (e.g. `Prop` for stored properties, +/// `usize` for degree, etc.). +#[derive(Debug, Clone)] +pub enum FilterValue { + /// Sentinel for `IS_SOME` / `IS_NONE` operators — no RHS value. + None, + /// Single value for equality/ordering comparisons. + Single(T), + /// Set of values for `IS_IN` / `IS_NOT_IN` comparisons. + Set(Arc>), +} + +impl PartialEq for FilterValue { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (FilterValue::None, FilterValue::None) => true, + (FilterValue::Single(a), FilterValue::Single(b)) => a == b, + (FilterValue::Set(a), FilterValue::Set(b)) => a == b, + _ => false, + } + } +} + +impl Eq for FilterValue {} + +impl fmt::Display for FilterValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FilterValue::None => write!(f, ""), + FilterValue::Single(v) => write!(f, "{}", v), + FilterValue::Set(vs) => { + let mut sorted: Vec<&T> = vs.iter().collect(); + sorted.sort(); + write!( + f, + "[{}]", + sorted + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(", ") + ) + } + } + } +} diff --git a/raphtory/src/db/graph/views/filter/model/graph_filter.rs b/raphtory/src/db/graph/views/filter/model/graph_filter.rs index 878df902b6..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,9 +3,11 @@ use crate::{ api::state::ops::{filter::NodeExistsOp, GraphView}, graph::views::filter::{ model::{ - edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, - CompositeExplodedEdgeFilter, CompositeNodeFilter, FilterTree, InternalViewWrapOps, - TryAsCompositeFilter, Wrap, + latest_filter::Latest, + layered_filter::Layered, + snapshot_filter::{SnapshotAt, SnapshotLatest}, + windowed_filter::Windowed, + CombinedFilter, InternalViewWrapOps, Wrap, }, CreateFilter, }, @@ -74,23 +76,34 @@ 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())) - } +// ── expr-layer view ops ── - fn try_as_composite_node_filter(&self) -> Result { - Err(GraphError::NotSupported) - } +pub trait GraphFilterOps: + InternalViewWrapOps + CombinedFilter + Send + Sync + 'static +{ + type GraphWindow: GraphFilterOps + CombinedFilter; +} - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) - } +impl GraphFilterOps for GraphFilter { + type GraphWindow = Self::Window; +} - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) - } +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/is_active_edge_filter.rs b/raphtory/src/db/graph/views/filter/model/is_active_edge_filter.rs index a91da1c91e..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, TryAsCompositeFilter, - }, - CreateFilter, + edge_filtered_graph::EdgeFilteredGraph, model::ComposableFilter, CreateFilter, }, is_active_graph::IsActiveGraph, }, @@ -73,18 +68,37 @@ impl CreateFilter for IsActiveEdge { impl ComposableFilter for IsActiveEdge {} -impl TryAsCompositeFilter for IsActiveEdge { - fn try_as_composite_node_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::{ + 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 try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsActiveEdge(IsActiveEdge)) + fn prop_type(&self) -> PropType { + PropType::Bool } - fn try_as_composite_exploded_edge_filter( + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsActiveEdge { + fn create_edge_op<'g, G: GraphView + 'g>( &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsActiveEdge(IsActiveEdge)) + 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..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, TryAsCompositeFilter, - }, - node_filtered_graph::NodeFilteredGraph, - CreateFilter, + model::ComposableFilter, node_filtered_graph::NodeFilteredGraph, CreateFilter, }, }, errors::GraphError, @@ -72,18 +67,38 @@ impl CreateFilter for IsActiveNode { impl ComposableFilter for IsActiveNode {} -impl TryAsCompositeFilter for IsActiveNode { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::IsActiveNode(IsActiveNode)) +// ── 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 try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) + fn prop_type(&self) -> PropType { + PropType::Bool } - fn try_as_composite_exploded_edge_filter( + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsActiveNode { + fn create_node_op<'g, G: GraphView + 'g>( &self, - ) -> Result { - Err(GraphError::NotSupported) + 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..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, TryAsCompositeFilter, - }, - CreateFilter, + edge_filtered_graph::EdgeFilteredGraph, model::ComposableFilter, CreateFilter, }, is_deleted_graph::IsDeletedGraph, }, @@ -73,18 +68,37 @@ impl CreateFilter for IsDeletedEdge { impl ComposableFilter for IsDeletedEdge {} -impl TryAsCompositeFilter for IsDeletedEdge { - fn try_as_composite_node_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::{ + 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 try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsDeletedEdge(IsDeletedEdge)) + fn prop_type(&self) -> PropType { + PropType::Bool } - fn try_as_composite_exploded_edge_filter( + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsDeletedEdge { + fn create_edge_op<'g, G: GraphView + 'g>( &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsDeletedEdge(IsDeletedEdge)) + 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..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, TryAsCompositeFilter, - }, - CreateFilter, - }, + filter::{model::ComposableFilter, CreateFilter}, is_self_loop_graph::IsSelfLoopGraph, }, }, @@ -73,18 +67,37 @@ impl CreateFilter for IsSelfLoopEdge { impl ComposableFilter for IsSelfLoopEdge {} -impl TryAsCompositeFilter for IsSelfLoopEdge { - fn try_as_composite_node_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::{ + 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 try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) + fn prop_type(&self) -> PropType { + PropType::Bool } - fn try_as_composite_exploded_edge_filter( + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsSelfLoopEdge { + fn create_edge_op<'g, G: GraphView + 'g>( &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsSelfLoopEdge(IsSelfLoopEdge)) + 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..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, TryAsCompositeFilter, - }, - CreateFilter, + edge_filtered_graph::EdgeFilteredGraph, model::ComposableFilter, CreateFilter, }, valid_graph::ValidGraph, }, @@ -73,18 +68,37 @@ impl CreateFilter for IsValidEdge { impl ComposableFilter for IsValidEdge {} -impl TryAsCompositeFilter for IsValidEdge { - fn try_as_composite_node_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::{ + 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 try_as_composite_edge_filter(&self) -> Result { - Ok(CompositeEdgeFilter::IsValidEdge(IsValidEdge)) + fn prop_type(&self) -> PropType { + PropType::Bool } - fn try_as_composite_exploded_edge_filter( + fn nullable(&self) -> bool { + false + } +} + +impl CreateOp for IsValidEdge { + fn create_edge_op<'g, G: GraphView + 'g>( &self, - ) -> Result { - Ok(CompositeExplodedEdgeFilter::IsValidEdge(IsValidEdge)) + 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 0363e1843a..a8232f46b6 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -1,25 +1,15 @@ use crate::{ db::{ - api::view::internal::GraphView, + api::{state::NodeOp, 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, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, FilterTree, GraphViewOp, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, - InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, TryAsCompositeFilter, 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, }, @@ -29,8 +19,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 { @@ -58,89 +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 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> @@ -201,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>; @@ -248,3 +135,36 @@ impl EdgeViewFilterOps for Latest { self.wrap(self.inner.is_self_loop()) } } + +// ── expr-layer view construction ── + +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()) + } +} + +// ── 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 ef24f82d95..49d4c07c61 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -1,24 +1,14 @@ use crate::{ db::{ - api::view::internal::GraphView, + api::{state::NodeOp, 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, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, FilterTree, GraphViewOp, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, - InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, TryAsCompositeFilter, 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, }, @@ -28,8 +18,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 { @@ -39,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(", "), } } @@ -70,90 +79,6 @@ impl InternalViewWrapOps for Layered { } } -impl InternalNodeFilterBuilder for Layered { - type FilterType = T::FilterType; - - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalNodeIdFilterBuilder for Layered { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalPropertyFilterBuilder for Layered { - type Filter = Layered; - type ExprBuilder = Layered; - type Marker = T::Marker; - - fn property_ref(&self) -> PropertyRef { - self.inner.property_ref() - } - - fn ops(&self) -> &[Op] { - self.inner.ops() - } - - fn entity(&self) -> Self::Marker { - self.inner.entity() - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - -impl TryAsCompositeFilter for Layered { - fn try_as_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> @@ -217,26 +142,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>; @@ -264,3 +169,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 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/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index e8cf441231..7bc2a544be 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, @@ -30,12 +6,17 @@ 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, }, - filter_operator::FilterOperator, - node_filter::{NodeFilter, NodeNameFilter, NodeTypeFilter}, + node_expr::{ + AllExpr, AnyExpr, AvgExpr, BinaryCmpExpr, EntityAggOps, FirstExpr, + LastExpr, LenExpr, MaxExpr, MinExpr, PropValueSetExpr, StringExpr, SumExpr, + TemporalPropExpr, UnaryExpr, + }, + node_filter::{NodeFilter, NodeFilterFactory}, not_filter::NotFilter, or_filter::OrFilter, }, @@ -47,20 +28,51 @@ pub use crate::{ errors::GraphError, prelude::{GraphViewOps, TimeOps}, }; -pub use node_filter::CompositeNodeFilter; +use crate::{ + db::{ + api::{ + state::{ + ops::{filter::NO_FILTER, Const}, + 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, + }, + layer_graph::LayeredGraph, + }, + }, + prelude::LayerOps, +}; use raphtory_api::core::{ - entities::Layer, + entities::{ + properties::prop::{unify_types, Prop}, + Layer, + }, storage::timeindex::{AsTime, EventTime}, utils::time::IntoTime, }; 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; 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; @@ -69,12 +81,14 @@ 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; pub mod or_filter; pub mod property_filter; pub mod snapshot_filter; +pub mod tree; pub mod windowed_filter; #[derive(Debug, Copy, Clone)] @@ -123,22 +137,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; @@ -172,23 +170,7 @@ 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 { +pub trait DynCreateFilter: Send + Sync + 'static { fn create_dyn_filter<'graph>( &self, graph: DynGraphArc<'graph>, @@ -276,317 +258,239 @@ impl CreateFilter for Arc { } } -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; +#[derive(Copy, Clone)] +pub enum EntityMarker { + Node, + Edge, + ExplodedEdge, + Const, } -impl DynPropertyFilterBuilder for T { - fn dyn_property_ref(&self) -> PropertyRef { - self.property_ref() - } - - fn dyn_ops(&self) -> &[Op] { - self.ops() - } +// ───────────────────────────────────────────────────────────────────────────── +// 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, +} - fn dyn_entity(&self) -> EntityMarker { - self.entity().into() +impl Property { + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } } +} - fn dyn_filter(&self, filter: PropertyFilterInput) -> Arc { - Arc::new(self.filter(filter)) - } +/// Static metadata field — implements both `NodeExpr` and `EdgeExpr`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Metadata { + pub name: String, +} - fn dyn_into_expr_builder( - &self, - builder: PropertyExprBuilderInput, - ) -> Arc { - Arc::new(self.with_expr_builder(builder)) +impl Metadata { + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } } } -impl InternalPropertyFilterBuilder for Arc { - type Filter = Arc; - type ExprBuilder = Arc; - type Marker = EntityMarker; - - fn property_ref(&self) -> PropertyRef { - self.deref().dyn_property_ref() - } +#[derive(Clone)] +pub struct PropertyExpr { + pub(crate) view_expr: E, + pub(crate) name: String, +} - fn ops(&self) -> &[Op] { - self.deref().dyn_ops() - } +impl EntityExpr for PropertyExpr { + type Marker = E::Marker; 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) + self.view_expr.entity() } } -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() - } +#[derive(Clone)] +pub struct MetadataExpr { + view_expr: E, + name: String, +} +impl EntityExpr for MetadataExpr { + type Marker = E::Marker; fn entity(&self) -> Self::Marker { - self.deref().dyn_entity() + self.view_expr.entity() } +} - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.deref().dyn_filter(filter) +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 with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.deref().dyn_into_expr_builder(builder) + 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 InternalPropertyFilterBuilder for Arc { - type Filter = T::Filter; - type ExprBuilder = T::ExprBuilder; - type Marker = T::Marker; - - fn property_ref(&self) -> PropertyRef { - self.deref().property_ref() +impl CreateOp for MetadataExpr { + 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()))?; + let graph = self.view_expr.create_view(graph)?; + Ok(Arc::new(NodeMetaOp { graph, prop_id })) } - fn ops(&self) -> &[Op] { - self.deref().ops() + 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 })) } +} - fn entity(&self) -> Self::Marker { - self.deref().entity() - } +/// Entry point of the expr API: selects a property or metadata column on any view expression. +pub trait PropertyExprFactory: CreateView + EntityExpr + Sized { + fn property(&self, name: impl Into) -> PropertyExpr; + + fn metadata(&self, name: impl Into) -> MetadataExpr; +} - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.deref().filter(filter) +impl PropertyExprFactory for T { + fn property(&self, name: impl Into) -> PropertyExpr { + PropertyExpr { + view_expr: self.clone(), + name: name.into(), + } } - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.deref().with_expr_builder(builder) + fn metadata(&self, name: impl Into) -> MetadataExpr { + MetadataExpr { + view_expr: self.clone(), + name: name.into(), + } } } -#[derive(Copy, Clone)] -pub enum EntityMarker { - Node, - Edge, - ExplodedEdge, +pub trait DynPropertyExprFactory { + fn dyn_property(&self, name: String) -> 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; +impl DynPropertyExprFactory for T { + fn dyn_property(&self, name: String) -> Arc { + Arc::new(self.property(name)) + } } -pub trait DynPropertyFilterFactory: Send + Sync + 'static { - fn dyn_entity(&self) -> EntityMarker; - - fn dyn_property_builder(&self, property: String) -> Arc; - - fn dyn_metadata_builder(&self, property: String) -> Arc; +impl PropertyExpr { + pub fn temporal(&self) -> TemporalPropExpr { + TemporalPropExpr { + view_expr: self.view_expr.clone(), + name: self.name.clone(), + } + } } -impl DynPropertyFilterFactory for T { - fn dyn_entity(&self) -> EntityMarker { - self.entity().into() +/// 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 dyn_property_builder(&self, property: String) -> Arc { - Arc::new(self.property_builder(property)) + fn avg(self) -> AvgExpr { + AvgExpr(self) } - - fn dyn_metadata_builder(&self, property: String) -> Arc { - Arc::new(self.metadata_builder(property)) + fn min(self) -> MinExpr { + MinExpr(self) } -} - -impl InternalPropertyFilterFactory for Arc { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; - - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() + fn max(self) -> MaxExpr { + MaxExpr(self) } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) + fn first(self) -> FirstExpr { + FirstExpr(self) } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) + fn last(self) -> LastExpr { + LastExpr(self) } -} - -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()) + fn len(self) -> LenExpr { + LenExpr(self) } } -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. -#[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 -/// 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. -#[derive(Clone, Debug)] -pub enum FilterTree { - Node(CompositeNodeFilter), - Edge(CompositeEdgeFilter), - ExplodedEdge(CompositeExplodedEdgeFilter), - View(Vec), - And(Vec), - Or(Vec), - Not(Box), -} - -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(), - } +/// As for [`PropertyExpr`]: aggregation over a list-valued metadata field. +impl EntityAggOps for MetadataExpr { + fn sum(self) -> SumExpr { + SumExpr(self) } -} - -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()?, - )) + fn avg(self) -> AvgExpr { + AvgExpr(self) } -} - -impl TryAsCompositeFilter for Arc { - fn try_as_filter_tree(&self) -> Result { - self.deref().try_as_filter_tree() + fn min(self) -> MinExpr { + MinExpr(self) } - - fn try_as_composite_node_filter(&self) -> Result { - self.deref().try_as_composite_node_filter() + fn max(self) -> MaxExpr { + MaxExpr(self) } - - fn try_as_composite_edge_filter(&self) -> Result { - self.deref().try_as_composite_edge_filter() + fn first(self) -> FirstExpr { + FirstExpr(self) } - - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - self.deref().try_as_composite_exploded_edge_filter() + fn last(self) -> LastExpr { + LastExpr(self) + } + fn len(self) -> LenExpr { + LenExpr(self) } } -pub trait CombinedFilter: CreateFilter + TryAsCompositeFilter + Clone + 'static {} +// ───────────────────────────────────────────────────────────────────────────── +// EdgeFilterFactory — marker for edge-side filter factory types +// ───────────────────────────────────────────────────────────────────────────── + +/// 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. +pub trait EdgeFilterFactory: PropertyExprFactory + Clone {} -impl CombinedFilter for T {} +// ───────────────────────────────────────────────────────────────────────────── +// PropertyExpr / MetadataExpr — EdgeExpr impls +// ───────────────────────────────────────────────────────────────────────────── + +use crate::db::graph::views::filter::model::{ + edge_expr::ops::{EdgeMetaOp, EdgePropOp}, + graph_filter::GraphFilterOps, + node_expr::{CreateOp, DynTemporal, EntityExpr, PredicateLhs}, +}; +use edge_expr::EdgeOp; +use raphtory_api::core::entities::properties::prop::PropType; // This is implemented to avoid infinite recursive windowing. pub trait InternalViewWrapOps: Send + Sync + Clone + 'static { @@ -620,7 +524,7 @@ impl DynInternalViewWrapOps for T { } } -impl InternalViewWrapOps for Arc { +impl InternalViewWrapOps for Arc { type Window = Arc; fn bounds(&self) -> (EventTime, EventTime) { @@ -632,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 { @@ -643,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] @@ -684,83 +604,183 @@ pub trait ViewWrapOps: InternalViewWrapOps + Sized { impl ViewWrapOps for T {} -pub trait ViewWrapPropOps: InternalViewWrapOps + InternalPropertyFilterFactory + Sized {} - -impl ViewWrapPropOps for T where T: InternalViewWrapOps + InternalPropertyFilterFactory + Sized {} +pub trait CreateView: Clone + Send + Sync + 'static { + type View<'graph, G: GraphView + 'graph>: GraphView + 'graph; + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError>; +} -pub trait DynInternalViewWrapPropOps: DynInternalViewWrapOps + DynPropertyFilterFactory {} +pub trait DynCreateView: Send + Sync + 'static { + fn dyn_create_view<'graph>( + &self, + view: Arc, + ) -> Result, GraphError>; +} -impl DynInternalViewWrapPropOps for T where T: DynInternalViewWrapOps + DynPropertyFilterFactory {} +impl DynCreateView for T { + fn dyn_create_view<'graph>( + &self, + view: Arc, + ) -> Result, GraphError> { + Ok(Arc::new(self.create_view(view)?)) + } +} -impl InternalPropertyFilterFactory for Arc { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; +impl CreateView for Arc { + type View<'graph, G: GraphView + 'graph> = Arc; - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + self.deref().dyn_create_view(Arc::new(view)) } +} - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) +impl CreateView for NodeFilter { + type View<'graph, G: GraphView + 'graph> = G; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + Ok(view) } +} - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) +impl EntityExpr for NodeFilter { + type Marker = NodeFilter; + fn entity(&self) -> Self::Marker { + NodeFilter } } -impl InternalViewWrapOps for Arc { - type Window = Arc; +impl CreateView for EdgeFilter { + type View<'graph, G: GraphView + 'graph> = G; - fn bounds(&self) -> (EventTime, EventTime) { - self.deref().dyn_bounds() + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + Ok(view) } +} - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - Arc::new(Windowed::new(start, end, self)) +impl EntityExpr for EdgeFilter { + type Marker = EdgeFilter; + fn entity(&self) -> Self::Marker { + EdgeFilter } } -pub trait DynViewFilter: DynInternalViewWrapOps + DynCreateFilter + Send + Sync + 'static {} -impl DynViewFilter for T where T: DynInternalViewWrapOps + DynCreateFilter + Send + Sync + 'static -{} +impl CreateView for ExplodedEdgeFilter { + type View<'graph, G: GraphView + 'graph> = G; -pub type DynView = Arc; + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + Ok(view) + } +} -pub type DynFilter = Arc; +impl EntityExpr for ExplodedEdgeFilter { + type Marker = ExplodedEdgeFilter; + fn entity(&self) -> Self::Marker { + ExplodedEdgeFilter + } +} -impl ComposableFilter for DynFilter {} -impl ComposableFilter for DynView {} +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 InternalViewWrapOps for DynView { - type Window = DynView; +impl EntityExpr for Latest { + type Marker = T::Marker; + fn entity(&self) -> Self::Marker { + self.inner.entity() + } +} - fn bounds(&self) -> (EventTime, EventTime) { - self.deref().dyn_bounds() +impl EntityExpr for SnapshotAt { + type Marker = T::Marker; + fn entity(&self) -> Self::Marker { + self.inner.entity() } +} - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - Arc::new(Windowed::new(start, end, self)) +impl EntityExpr for SnapshotLatest { + type Marker = T::Marker; + fn entity(&self) -> Self::Marker { + self.inner.entity() } } -pub trait NodeViewFilterOps: ViewWrapOps { - type Output: CombinedFilter; +impl CreateView for Layered { + type View<'graph, G: GraphView + 'graph> = LayeredGraph>; - fn is_active(&self) -> Self::Output; + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result>, GraphError> { + let inner = self.inner.create_view(view)?; + inner.layers(self.layer.clone()) + } } -pub trait DynNodeViewFilterOps: DynInternalViewWrapPropOps { - fn dyn_is_active(&self) -> Arc; +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, +{ + fn dyn_bounds(&self) -> (EventTime, EventTime) { + self.bounds() + } -impl DynNodeViewFilterOps for T { - fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) + 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; + +pub type DynFilter = Arc; + +impl ComposableFilter for DynFilter {} +impl ComposableFilter for DynView {} + pub trait EdgeViewFilterOps: ViewWrapOps { type Output: CombinedFilter; @@ -773,122 +793,405 @@ pub trait EdgeViewFilterOps: ViewWrapOps { fn is_self_loop(&self) -> Self::Output; } -pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps { - fn dyn_is_active(&self) -> Arc; +// ───────────────────────────────────────────────────────────────────────────── +// EntityExprFilterOps — comparison and set operators on any EntityExpr +// ───────────────────────────────────────────────────────────────────────────── - fn dyn_is_valid(&self) -> Arc; +/// 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 +/// element is greater than ten. +/// +/// ```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 dyn_is_deleted(&self) -> Arc; + fn ge(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Ge, rhs, entity) + } - fn dyn_is_self_loop(&self) -> Arc; -} + fn lt(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Lt, rhs, entity) + } -impl DynEdgeViewFilterOps for T { - fn dyn_is_active(&self) -> Arc { - Arc::new(self.is_active()) + fn le(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Le, rhs, entity) } - fn dyn_is_valid(&self) -> Arc { - Arc::new(self.is_valid()) + fn eq(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Eq, rhs, entity) } - fn dyn_is_deleted(&self) -> Arc { - Arc::new(self.is_deleted()) + fn ne(self, rhs: R) -> BinaryCmpExpr { + let entity = self.entity(); + BinaryCmpExpr::new(self, BinaryOp::Ne, rhs, entity) } - fn dyn_is_self_loop(&self) -> Arc { - Arc::new(self.is_self_loop()) + fn starts_with(self, rhs: R) -> StringExpr { + let entity = self.entity(); + StringExpr::new(self, StringOp::StartsWith, rhs, entity) } -} -pub type DynNodeViewProps = Arc; + fn ends_with(self, rhs: R) -> StringExpr { + let entity = self.entity(); + StringExpr::new(self, StringOp::EndsWith, rhs, entity) + } -impl InternalViewWrapOps for DynNodeViewProps { - type Window = DynNodeViewProps; + fn contains(self, rhs: R) -> StringExpr { + let entity = self.entity(); + StringExpr::new(self, StringOp::Contains, rhs, entity) + } - fn bounds(&self) -> (EventTime, EventTime) { - self.deref().dyn_bounds() + fn not_contains(self, rhs: R) -> StringExpr { + let entity = self.entity(); + StringExpr::new(self, StringOp::NotContains, rhs, entity) } - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - Arc::new(Windowed::new(start, end, self)) + 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, + ) } -} -impl NodeViewFilterOps for DynNodeViewProps { - type Output = Arc; + fn is_some(self) -> UnaryExpr { + let entity = self.entity(); + UnaryExpr { + expr: self, + op: UnaryOp::IsSome, + entity, + } + } - fn is_active(&self) -> Self::Output { - self.deref().dyn_is_active() + fn is_none(self) -> UnaryExpr { + let entity = self.entity(); + UnaryExpr { + expr: self, + op: UnaryOp::IsNone, + entity, + } } -} -impl InternalPropertyFilterFactory for DynNodeViewProps { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; + 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 entity(&self) -> Self::Entity { - self.deref().dyn_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 property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) + fn any(self) -> AnyExpr { + AnyExpr(self) } - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) + fn all(self) -> AllExpr { + AllExpr(self) } } -pub type DynEdgeViewProps = Arc; +impl EntityExprFilterOps for E {} + +// Concrete LHS markers +impl PredicateLhs for NodeFilter {} +impl PredicateLhs for EdgeFilter {} +impl PredicateLhs for ExplodedEdgeFilter {} + +// Property / metadata accessors +impl PredicateLhs for PropertyExpr {} +impl PredicateLhs for MetadataExpr {} + +// 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. +pub fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), GraphError> { + if 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 + ))); + } + } + Ok(()) +} -impl InternalViewWrapOps for DynEdgeViewProps { - type Window = DynEdgeViewProps; +/// 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(()) +} - fn bounds(&self) -> (EventTime, EventTime) { - self.deref().dyn_bounds() +/// 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 } +} - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - Arc::new(Windowed::new(start, end, self)) - } +/// 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> { + value.map(|v| cast_prop_to(target, v)).transpose() } -impl EdgeViewFilterOps for DynEdgeViewProps { - type Output = Arc; +/// [`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 + )) + }) +} - fn is_active(&self) -> Self::Output { - self.deref().dyn_is_active() +/// 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 { + // 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 + ))); + } } + Ok(()) +} - fn is_valid(&self) -> Self::Output { - self.deref().dyn_is_valid() - } +/// 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!( + "type mismatch: lhs is {}, rhs is {}", + lhs_pt, rhs_pt + ))); + } + Ok(()) +} - fn is_deleted(&self) -> Self::Output { - self.deref().dyn_is_deleted() +/// 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) +} - fn is_self_loop(&self) -> Self::Output { - self.deref().dyn_is_self_loop() +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 + ))), } } -impl InternalPropertyFilterFactory for DynEdgeViewProps { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; +/// Narrow an `is_in`/`is_not_in` set to the members that could match 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 + .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()) +} - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() - } +pub trait CombinedFilter: CreateFilter + Clone + Send + Sync + 'static {} - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) - } +pub trait NodeViewFilterOps: ViewWrapOps { + type Output: CombinedFilter; - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) - } + fn is_active(&self) -> Self::Output; } + +impl CombinedFilter for T {} 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..9be87b7d23 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/dyn_expr.rs @@ -0,0 +1,220 @@ +//! 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::{ + api::{ + state::{ops::GraphView, NodeOp}, + view::BoxableGraphView, + }, + 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, + }, + CreateView, EntityMarker, PropertyExpr, + }, + }, + errors::GraphError, +}; +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use std::{ops::Deref, sync::Arc}; + +pub 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() + } +} + +pub trait DynTemporal: DynCreateOp { + fn temporal(&self) -> Arc; +} + +impl DynTemporal for PropertyExpr { + fn temporal(&self) -> Arc { + Arc::new(self.temporal()) + } +} + +impl DynTemporal for EdgeEndpointWrapper> +where + E: EntityExpr + CreateView + Clone + Send + Sync + 'static, + Self: DynCreateOp, +{ + fn temporal(&self) -> Arc { + Arc::new(self.temporal()) + } +} + +/// 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; + + fn dyn_selects_node_id(&self) -> bool; + + fn dyn_create_node_op<'g>( + &self, + graph: Arc, + ) -> Result> + 'g>, GraphError>; + + fn dyn_create_edge_op<'g>( + &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 { + fn dyn_const_cast_type(&self) -> Option { + self.const_cast_type() + } + + fn dyn_selects_node_id(&self) -> bool { + self.selects_node_id() + } + + 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) + } + + 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 { + 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() + } +} + +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() + } + + 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)) + } + + 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 { + 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/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..6b67dfd964 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/exprs.rs @@ -0,0 +1,744 @@ +//! 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 [`CreateOp::create_node_op`] to compile it against a specific graph +//! view, performing name→ID resolution once. +//! +//! # Field expressions +//! +//! All expressions produce `Option` — field values are mapped via `into_prop()`. +//! +//! ```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::() +//! ``` +//! +//! # Degree expressions +//! +//! ```rust,ignore +//! 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 — 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 +//! +//! Accessed via `.temporal()` on `PropertyExpr` (returned by `.property("name")`): +//! +//! ```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: +//! 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 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::{ + AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, SumNodeOp, + TemporalNodePropOp, WithPropType, + }, + AllEdgeOp, AllNodeOp, AnyEdgeOp, AnyNodeOp, AvgEdgeOp, CreateOp, EntityExpr, FirstEdgeOp, + LastEdgeOp, LenEdgeOp, MaxEdgeOp, MinEdgeOp, PredicateLhs, SumEdgeOp, +}; +use crate::{ + db::{ + api::{ + state::ops::{Const, Degree, Id, Name, NodeOp, Type}, + view::internal::GraphView, + }, + graph::views::filter::model::{ + edge_expr::{ops::TemporalEdgePropOp, EdgeOp}, + elem_prop_type, + filter_operator::{Comparable, ElemQual}, + node_filter::NodeFilter, + require_aggregable, resolved_prop_type, CreateView, EntityMarker, + }, + }, + errors::GraphError, +}; +use raphtory_api::core::{ + entities::{ + properties::prop::{IntoProp, Prop, PropType}, + GidType, 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. +// 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) +// ───────────────────────────────────────────────────────────────────────────── + +#[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; + + fn entity(&self) -> Self::Marker { + NodeFilter + } +} + +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, + ) -> 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, + })) + } +} + +impl EntityExpr for GID { + type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } +} + +impl CreateOp for GID { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.clone().into_prop())))) + } +} + +impl EntityExpr for Name { + type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } + + fn prop_type(&self) -> PropType { + PropType::Str + } +} + +impl PredicateLhs for Name {} + +impl CreateOp for Name { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(WithPropType { + inner: Name.map(|a| Some(a.into_prop())), + pt: PropType::Str, + })) + } +} + +impl EntityExpr for Type { + type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } + + fn prop_type(&self) -> PropType { + PropType::Str + } +} + +impl PredicateLhs for Type {} + +impl CreateOp for Type { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> 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, + })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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 EntityExpr for usize { + type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } + + fn prop_type(&self) -> PropType { + PropType::U64 + } +} + +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 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 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 { + type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } + + fn prop_type(&self) -> PropType { + PropType::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)))))) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Prop scalar — NodeExpr impl +// +// 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 { + type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } + + fn prop_type(&self) -> PropType { + self.dtype() + } +} + +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_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 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_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 +/// [`CreateOp`] 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 EntityExpr for ConstExpr { + type Marker = ConstFilter; + + fn entity(&self) -> Self::Marker { + ConstFilter + } +} + +impl + Clone + Send + Sync + 'static> CreateOp for ConstExpr { + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.0.clone().into())))) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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 `Degree { dir, view }.map(|a| Some(Prop::U64(a as u64)))`. +/// +/// ```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 EntityExpr for DegreeExpr { + type Marker = NodeFilter; + + fn entity(&self) -> Self::Marker { + NodeFilter + } + + fn prop_type(&self) -> PropType { + PropType::U64 + } + fn nullable(&self) -> bool { + false + } +} + +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, + ) -> Result> + 'g>, GraphError> { + 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, + })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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. +/// +/// Implements `NodeExpr` when `E: NodeFilterFactory` and `EdgeExpr` when `E: EdgeFilterFactory`. +/// 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 TemporalPropExpr { + pub(crate) view_expr: E, + pub(crate) name: String, +} + +impl EntityExpr for TemporalPropExpr { + type Marker = E::Marker; + fn entity(&self) -> Self::Marker { + self.view_expr.entity() + } +} + +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 CreateOp for TemporalPropExpr { + 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( + TemporalNodePropOp { graph, prop_id }.map(|a| Some(a)), + )) + } + + 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(TemporalEdgePropOp { graph, prop_id })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Aggregator Exprs — NodeExpr wrappers producing a single scalar +// +// Each wraps an inner NodeExpr (typically TemporalPropertyExpr) and reduces +// the Prop::List it produces. Not constructed directly — +// EntityAggOps methods on TemporalExpr return these exprs directly: +// +// .property("v").temporal().sum() → SumExpr> +// .property("v").temporal().len() → LenExpr> +// .property("v").temporal().any() → AnyExpr> +// +// Calling .gt() / .eq() etc. on any of these (via NodeExprFilterOps) produces: +// BinaryCmpExpr>, RHS> +// ───────────────────────────────────────────────────────────────────────────── + +// ───────────────────────────────────────────────────────────────────────────── +// EntityAggOps — secondary aggregate operators on filter expression types +// +// Scoped narrowly (not blanket-impl) to avoid name collisions with stdlib methods +// like `Ord::min` / `Ord::max` / `Iterator::sum` on primitive `EntityExpr` types +// (`u64`, `i64`, etc. all implement `EntityExpr` as constant values). +// ───────────────────────────────────────────────────────────────────────────── + +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, $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 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>( + &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(&elem_prop_type(&pt, quals.len())?, $name)?; + 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(&elem_prop_type(&pt, quals.len())?, $name)?; + 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); + + impl EntityExpr for $expr { + type Marker = E::Marker; + fn entity(&self) -> Self::Marker { + self.0.entity() + } + } + + 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) + } + } + + }; + (@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 })) + } + }; +} + +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 new file mode 100644 index 0000000000..cbb614a35c --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs @@ -0,0 +1,1042 @@ +//! 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::{Const, NodeOp}, + view::{internal::GraphView, BoxableGraphView}, + }, + graph::views::filter::{ + model::{ + cast_const_to, cast_prop_to, coerce_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_castable, + validate_string_op, validate_types_compatible, ComposableFilter, CreateFilter, + EntityMarker, ExplodedEdgeFilter, + }, + node_filtered_graph::NodeFilteredGraph, + }, + }, + errors::GraphError, + prelude::{EdgeFilter, NodeFilter}, +}; +use raphtory_api::core::entities::{ + properties::prop::{Prop, PropType}, + GID, +}; +use std::sync::Arc; +// ───────────────────────────────────────────────────────────────────────────── +// BinaryCmpExpr — binary expression filter +// ───────────────────────────────────────────────────────────────────────────── + +/// 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`. +/// +/// ```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 } +/// ``` +#[derive(Clone)] +pub struct BinaryCmpExpr { + pub left: L, + pub op: BinaryOp, + pub right: R, + 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 { + left, + op, + right, + entity, + } + } + + fn with_entity(self, entity: T) -> BinaryCmpExpr { + BinaryCmpExpr { + left: self.left, + op: self.op, + right: self.right, + entity, + } + } +} + +impl ComposableFilter for BinaryCmpExpr {} + +impl PredicateLhs 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, + })) + } + + 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 + 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)?; + 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)?, + } + 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 } +/// ``` +#[derive(Clone)] +pub struct UnaryExpr { + pub expr: E, + pub op: UnaryOp, + 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) + } +} + +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 } +/// ``` +#[derive(Clone)] +pub struct StringExpr { + pub left: L, + pub op: StringOp, + pub right: R, + pub entity: Entity, +} + +impl StringExpr { + pub fn new(left: L, op: StringOp, right: R, entity: Entity) -> Self { + Self { + left, + op, + right, + entity, + } + } + + fn with_entity(self, entity: T) -> StringExpr { + StringExpr { + left: self.left, + op: self.op, + right: self.right, + entity, + } + } +} + +impl ComposableFilter for StringExpr {} + +impl PredicateLhs for StringExpr {} + +impl 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_castable(&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) + } +} + +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`). +#[derive(Clone)] +pub struct PropValueSetExpr { + pub(crate) expr: E, + pub(crate) values: Vec, + pub(crate) op: SetOp, + 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())?; + // 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)?, + }; + 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) + } +} + +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 new file mode 100644 index 0000000000..0a98e2797a --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -0,0 +1,177 @@ +use crate::{ + db::{ + api::{state::ops::NodeOp, view::internal::GraphView}, + graph::views::filter::model::CreateView, + }, + errors::GraphError, +}; +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; + +#[cfg(test)] +mod tests; + +pub use super::{Metadata, Property}; +use crate::db::graph::views::filter::model::{ + edge_expr::EdgeOp, filter_operator::ElemQual, EntityMarker, +}; +pub use dyn_expr::*; +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. +/// +/// 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.property("score").temporal().sum().gt(100i64) +/// NodeFilter.property("score").temporal().gt(10i64).any() +/// ``` +/// +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. + fn selects_node_id(&self) -> bool { + false + } + + /// 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> { + Err(GraphError::NotNodeFilter) + } + + fn create_edge_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> 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 {} + +impl + Copy + Send + Sync + 'static> Marker for M {} + +pub trait EntityExpr: Clone + Send + Sync + 'static { + type Marker: Marker; + + fn entity(&self) -> Self::Marker; + + /// A priory known type (for early validation where possible) + 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 + } +} + +/// 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 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) +/// 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 new file mode 100644 index 0000000000..78ac69c3be --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -0,0 +1,870 @@ +//! 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) ← 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))) +//! +//! 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_list_values(Sum) +//! ``` +//! +//! # Quantified evaluation +//! +//! Filter types (`BinaryCmpExpr`, `StringExpr`, `PropValueSetExpr`) also +//! implement `NodeExpr`, producing list-aware ops for mid-chain use before `.any()`/`.all()`: +//! +//! ```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) +//! ``` + +use super::EdgeOp; +use crate::{ + core::entities::nodes::node_ref::AsNodeRef, + db::{ + api::{ + properties::PropertiesOps, + state::ops::NodeOp, + view::{ + internal::{GraphView, NodeList}, + 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, + }, + }, + }, + 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_storage::graph::graph::GraphStorage; +use std::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 { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.graph.node_list() + } + + 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 { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.graph.node_list() + } + + 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() + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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, storage: &GraphStorage) -> Option { + self.inner.const_value_in_domain(storage) + } + + fn apply(&self, storage: &GraphStorage, node: VID) -> Self::Output { + self.inner.apply(storage, node) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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 `Some(Prop::List([...]))`. +/// That list is then consumed by aggregator ops (`SumNodeOp`, `LenNodeOp`, …) or +/// by `ListAwareCmpNodeOp` for element-wise comparisons before `.any()`/`.all()` reduction. +#[derive(Clone)] +pub(crate) struct TemporalNodePropOp { + pub(crate) graph: G, + pub(crate) prop_id: usize, +} + +impl NodeOp for TemporalNodePropOp { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + 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 { + 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) +// ───────────────────────────────────────────────────────────────────────────── + +/// Aggregations collapse the innermost list level; outer levels survive so a +/// 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_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)] + pub struct $node_name<'g> { + pub inner: Arc> + 'g>, + } + + impl<'g> NodeOp for $node_name<'g> { + fn domain(&self, _storage: &GraphStorage) -> NodeList { + self.inner.domain(_storage) + } + + 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)) + } + } + + #[derive(Clone)] + pub struct $edge_name<'g> { + pub inner: Arc> + 'g>, + } + + 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)) + } + } + }; +} + +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 { + PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => { + let (promoted, s64, s128, _) = scan_u64_sum(vals)?; + Some(if promoted { + // 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) + }) + } + PropType::I32 | PropType::I64 => { + let (promoted, s64, s128, _) = scan_i64_sum(vals)?; + Some(if promoted { + match i64::try_from(s128) { + Ok(v) => Prop::I64(v), + Err(_) => Prop::Decimal(BigDecimal::from(s128)), + } + } 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, + |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, + } + }) + } +); +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| { + // 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, + |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, + |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 +// +// 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())), + _ => 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| broadcast_binary(l, Some(r.clone()), op)) + .flatten() + .collect(), + )), + (l, Prop::List(r)) => Some(Prop::List( + r.iter_all() + .map(|r| broadcast_binary(Some(l.clone()), r, op)) + .flatten() + .collect(), + )), + (l, r) => op(Some(l), Some(r)), + } +} + +#[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 +// ───────────────────────────────────────────────────────────────────────────── + +/// 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 { + 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, + } +} + +/// 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 +// ───────────────────────────────────────────────────────────────────────────── + +/// 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/node_expr/tests.rs b/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs new file mode 100644 index 0000000000..4af1cbb7a8 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs @@ -0,0 +1,748 @@ +use super::*; +use crate::{ + db::{ + api::{ + state::ops::{Id, Name, NodeOp}, + view::{ + filter_ops::Select, + internal::{CoreGraphOps, NodeList}, + }, + }, + graph::views::filter::{ + model::{ + filter_operator::BinaryOp, node_filter::NodeFilter, PropertyExprFactory, + ViewWrapOps, + }, + CreateFilter, + }, + }, + prelude::{AdditionOps, EntityExprFilterOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, +}; +use raphtory_api::core::{ + entities::{ + properties::prop::{IntoProp, Prop}, + GID, VID, + }, + 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, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, +{ + let fg = filter.filter_graph_view(g.clone()).unwrap(); + let mut names: Vec = filter + .create_filter(g, fg) + .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 = BinaryCmpExpr::new( + ConstExpr(2usize), + BinaryOp::Eq, + ConstExpr(2usize), + NodeFilter, + ); + 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, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, +{ + let fg = filter.filter_graph_view(g.clone()).unwrap(); + let mut names: Vec = filter + .create_filter(g, fg) + .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 = NodeFilter.property("score").temporal().eq(5i64).any(); + 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 = NodeFilter.property("score").temporal().gt(4i64).any(); + 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 = NodeFilter.property("score").temporal().gt(1i64).any(); + 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 = NodeFilter.property("score").temporal().gt(0i64).all(); + 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 = NodeFilter.property("score").temporal().gt(4i64).all(); + 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 = NodeFilter.property("score").temporal().ge(0i64).all(); + 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 = NodeFilter.property("score").temporal().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 = NodeFilter.property("score").temporal().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 = NodeFilter.property("score").temporal().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 = NodeFilter.property("score").temporal().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 = NodeFilter.property("score").temporal().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 = NodeFilter.property("score").temporal().len().ge(2usize); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); +} + +// ── NodeFilter entry point ──────────────────────────────────────────────── + +#[test] +fn node_filter_temporal_property_entry_point() { + let g = build_temporal_graph(); + let filter = NodeFilter.property("score").temporal().eq(5i64).any(); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); +} + +// ── TemporalExprOps blanket ─────────────────────────────────────────────── + +#[test] +fn temporal_expr_ops_blanket_any() { + // 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"]); +} + +// ── 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, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, +{ + let fg = filter.filter_graph_view(g.clone()).unwrap(); + let mut names: Vec = filter + .create_filter(g, 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) + .property("score") + .temporal() + .eq(5i64) + .any(); + // window [1,2) shows t=1 only → alice has score=1, not 5 + assert!(windowed_filtered_names(filter, g).is_empty()); +} + +#[test] +fn windowed_temporal_any_matches_in_window() { + // window [2, 3) → alice has score=5 (t=2), bob has score=3 (t=2) + let g = build_temporal_graph(); + let filter = NodeFilter + .window(2, 3) + .property("score") + .temporal() + .eq(5i64) + .any(); + 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, F::FilteredGraph<'graph, Graph>>: + GraphViewOps<'graph>, +{ + let fg = filter.filter_graph_view(g.clone()).unwrap(); + let mut names: Vec = filter + .create_filter(g, 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") + .property("score") + .temporal() + .eq(5i64) + .any(); + assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); +} + +#[test] +fn layered_temporal_any_restricts_to_layer_b_updates() { + // layer_b view: bob has scores [2, 3], alice has none, carol has none + // any > 2 → bob qualifies (score=3 > 2), alice and carol do not + let g = build_layered_temporal_graph(); + let filter = NodeFilter + .layer("layer_b") + .property("score") + .temporal() + .gt(2i64) + .any(); + assert_eq!(layered_filtered_names(filter, g), vec!["bob"]); +} + +#[test] +fn layered_temporal_sum_is_layer_scoped() { + // layer_a: alice sum = 1+5+10 = 16; layer_b: bob sum = 2+3 = 5 + // layer_a sum > 10 → alice (16 > 10); carol (no score) excluded + let g = build_layered_temporal_graph(); + let filter = NodeFilter + .layer("layer_a") + .property("score") + .temporal() + .sum() + .gt(10i64); + assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); +} + +// ── is_true() / is_false() ─────────────────────────────────────────────── + +/// Graph with bool "active" property: +/// "on" — active = true +/// "off" — active = false +/// "na" — no active property +fn build_bool_graph() -> Graph { + let g = Graph::new(); + g.add_node(0, "on", [("active", true.into_prop())], None, None) + .unwrap(); + g.add_node(0, "off", [("active", false.into_prop())], None, None) + .unwrap(); + g.add_node(0, "na", NO_PROPS, None, None).unwrap(); + g +} + +#[test] +fn is_true_keeps_only_true_nodes() { + let g = build_bool_graph(); + let filter = NodeFilter.property("active").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").eq(Prop::Bool(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").eq(Prop::Bool(true)); + let names = filtered_names(filter, g); + assert!(!names.contains(&"na".to_string())); +} + +// ── Runtime validation via prop_type() ─────────────────────────────────── + +#[test] +fn string_op_on_numeric_prop_returns_error() { + let g = build_temporal_graph(); + let filter = NodeFilter + .property("score") + .starts_with(Prop::Str("x".into())); + let result = filter.create_filter(g.clone(), g); + assert!( + result.is_err(), + "expected Err for string op on numeric property" + ); +} + +#[test] +fn ordering_op_on_bool_prop_orders_false_before_true() { + let g = Graph::new(); + 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 + 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"] + ); +} + +// ── 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"]); +} + +// ── 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 + ); +} 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 6f8f996367..0000000000 --- a/raphtory/src/db/graph/views/filter/model/node_filter/builders.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::db::graph::views::filter::model::{ - filter::Filter, - node_filter::{NodeNameFilter, NodeTypeFilter}, - Wrap, -}; -use std::{ops::Deref, sync::Arc}; - -pub trait InternalNodeIdFilterBuilder: Send + Sync + Wrap { - fn field_name(&self) -> &'static str; -} - -impl InternalNodeIdFilterBuilder for Arc { - fn field_name(&self) -> &'static str { - self.deref().field_name() - } -} - -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() - } -} - -#[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 f53467e43f..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 @@ -1,51 +1,24 @@ use crate::{ api::core::Direction, db::{ - api::{ - state::{ - ops::{ - filter::{ - AndOp, NodeIdFilterOp, NodeNameFilterOp, NodeTypeFilterOp, NotOp, OrOp, - }, - NodeOp, - }, - 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_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, - EntityMarker, InternalPropertyFilterFactory, InternalViewWrapOps, - NodeViewFilterOps, NotFilter, OrFilter, TryAsCompositeFilter, Wrap, - }, - node_filtered_graph::NodeFilteredGraph, - CreateFilter, + 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, }, }, 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; #[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] pub struct NodeFilter; @@ -56,43 +29,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; @@ -101,38 +37,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, self.entity()) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - MetadataFilterBuilder(property, self.entity()) - } -} - -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; @@ -141,413 +45,93 @@ impl NodeViewFilterOps for NodeFilter { } } -#[derive(Debug, Clone)] -pub struct NodeIdFilter(pub Filter); +// ── expr-layer factory ── -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) - } -} - -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) +pub trait NodeFilterFactory: + InternalViewWrapOps + CreateView + EntityExpr +{ + type NodeWindow: NodeFilterFactory + NodeViewFilterOps; + #[inline] + fn id(&self) -> Id { + Id } -} -#[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) + /// 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 } -} -impl From for NodeNameFilter { - fn from(filter: Filter) -> Self { - NodeNameFilter(filter) + /// Selects the node type field for filtering. + /// + /// Returns `Type` which implements `NodeExprFilterOps`. + #[inline] + fn node_type(&self) -> Type { + Type } -} - -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 + /// 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 - 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) + V: NodeStateValue + 'graph, + T: Clone + Send + Sync + 'graph, + Self: Sized, + { + state.bool_col_filter(col) } -} -impl TryAsCompositeFilter for NodeNameFilter { - fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Node(self.0.clone())) + /// Total degree expression — supports `.gt(n)`, `.lt(n)`, etc. + fn degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::BOTH, + view_expr: self.clone(), + } } - fn try_as_composite_edge_filter(&self) -> Result { - Err(GraphError::NotSupported) + /// In-degree expression. + fn in_degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::IN, + view_expr: self.clone(), + } } - fn try_as_composite_exploded_edge_filter( - &self, - ) -> Result { - Err(GraphError::NotSupported) + /// Out-degree expression. + #[inline] + fn out_degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::OUT, + view_expr: self.clone(), + } } } -#[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 NodeFilterFactory for NodeFilter { + type NodeWindow = Self::Window; } -impl From for NodeTypeFilter { - fn from(filter: Filter) -> Self { - NodeTypeFilter(filter) - } +impl NodeFilterFactory for Windowed { + type NodeWindow = T::NodeWindow; } -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_mask = graph - .node_meta() - .node_type_meta() - .keys() - .iter() - .map(|k| self.0.matches(Some(k))) // TODO: _default check - .collect::>(); - - let filter = NodeTypeFilterOp::from_mask(node_types_mask.into(), &graph); - - Ok(NodeFilteredGraph::new(graph, filter)) - } - - 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(NodeTypeFilterOp::from_mask(node_types_filter.into(), graph)) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } +impl NodeFilterFactory for Latest { + type NodeWindow = Self::Window; } -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)] -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 NodeFilterFactory for SnapshotAt { + type NodeWindow = Self::Window; } -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), - } - } +impl NodeFilterFactory for SnapshotLatest { + type NodeWindow = Self::Window; } -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) - } +impl NodeFilterFactory for Layered { + type NodeWindow = Self::Window; } 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 b567c2fd3c..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::FilterValue, - 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: &FilterValue) -> &'static str { - match fv { - FilterValue::ID(GID::U64(_)) => "U64", - FilterValue::ID(GID::Str(_)) => "Str", - FilterValue::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" - } - } - FilterValue::Single(_) => "Str", - FilterValue::Set(_) => "Str", - } - } - - let value_matches_kind = |fv: &FilterValue, expect: GidType| -> bool { - match (fv, expect) { - (FilterValue::ID(GID::U64(_)), U64) => true, - (FilterValue::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, - - _ => 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, - FilterValue::IDSet(_) | FilterValue::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, - FilterValue::ID(GID::Str(_)) | FilterValue::Single(_) - ) { - return Err(GraphError::InvalidGqlFilter( - "String operators on ID expect a single string ID".into(), - )); - } - } - Lt | Le | Gt | Ge => { - if !matches!(filter.field_value, FilterValue::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 c799eca492..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,13 +1,7 @@ use crate::{ - db::{ - api::{ - state::{ops::NodeOp, Index, NodeStateValue, TypedNodeState}, - view::internal::NodeList, - }, - graph::views::filter::model::{ - edge_filter::CompositeEdgeFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, - TryAsCompositeFilter, - }, + db::api::{ + state::{ops::NodeOp, Index, NodeStateValue, TypedNodeState}, + view::internal::NodeList, }, errors::GraphError, }; @@ -80,19 +74,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..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,14 +5,7 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, ComposableFilter, FilterTree, - TryAsCompositeFilter, - }, - not_filtered_graph::NotFilteredGraph, - CreateFilter, + model::ComposableFilter, not_filtered_graph::NotFilteredGraph, CreateFilter, }, }, errors::GraphError, @@ -79,41 +72,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..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,14 +5,7 @@ use crate::{ view::internal::GraphView, }, graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - exploded_edge_filter::CompositeExplodedEdgeFilter, - node_filter::CompositeNodeFilter, ComposableFilter, FilterTree, - TryAsCompositeFilter, - }, - or_filtered_graph::OrFilteredGraph, - CreateFilter, + model::ComposableFilter, or_filtered_graph::OrFilteredGraph, CreateFilter, }, }, errors::GraphError, @@ -90,47 +83,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/builders.rs b/raphtory/src/db/graph/views/filter/model/property_filter/builders.rs deleted file mode 100644 index 2cfda22420..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()) - } -} - -impl TemporalPropertyFilterFactory for PropertyFilterBuilder -where - T: Into + Send + Sync + Clone + 'static, - PropertyFilter: CombinedFilter, -{ -} - -#[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()) - } -} 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..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,5 +1,9 @@ -use crate::{db::graph::views::filter::model::Op, prelude::PropertyFilter}; -use raphtory_api::core::{entities::properties::prop::Prop, utils::generalised_reduce}; +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 std::borrow::Borrow; enum ValueType { Seq(Vec), @@ -329,3 +333,99 @@ impl PropertyFilter { self.apply_eval(r, s, q, is_t) } } + +// ── numeric scan helpers for expr aggregates ── + +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 d9a7542e09..9f48b82799 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, TryAsCompositeFilter, - }, - 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; -mod evaluate; -pub mod ops; +pub(crate) mod evaluate; mod validate; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -383,168 +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 {} - -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/property_filter/ops.rs b/raphtory/src/db/graph/views/filter/model/property_filter/ops.rs deleted file mode 100644 index 22f1bca14a..0000000000 --- a/raphtory/src/db/graph/views/filter/model/property_filter/ops.rs +++ /dev/null @@ -1,284 +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_hashable::HashableProp, 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().map(HashableProp::from).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().map(HashableProp::from).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 386e72c975..4e02597279 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -1,22 +1,18 @@ use crate::{ db::{ - api::view::{internal::GraphView, time::TimeOps}, + api::{ + state::NodeOp, + view::{internal::GraphView, time::TimeOps}, + }, graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, FilterTree, GraphViewOp, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, - InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, TryAsCompositeFilter, 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, }, @@ -25,8 +21,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 { @@ -58,81 +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 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> @@ -196,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>; @@ -270,76 +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 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> @@ -399,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>; @@ -446,3 +259,67 @@ impl EdgeViewFilterOps for SnapshotLatest { self.wrap(self.inner.is_self_loop()) } } + +// ── expr-layer view construction ── + +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()) + } +} + +// ── 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/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/db/graph/views/filter/model/windowed_filter.rs b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs index b8e87f7df3..bdaa72da4d 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -1,24 +1,14 @@ use crate::{ db::{ - api::view::internal::GraphView, + api::{state::NodeOp, 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, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, FilterTree, GraphViewOp, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, - InternalViewWrapOps, NodeViewFilterOps, Op, PropertyRef, - TemporalPropertyFilterFactory, TryAsCompositeFilter, 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, }, @@ -29,10 +19,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 { @@ -83,93 +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 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> @@ -234,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>; @@ -281,3 +165,38 @@ impl EdgeViewFilterOps for Windowed { self.wrap(self.inner.is_self_loop()) } } + +// ── expr-layer view construction ── + +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())) + } +} + +// ── 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.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/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index 21e2c18916..c6299f08a2 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 { @@ -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/errors.rs b/raphtory/src/errors.rs index 0d26a00992..8a9fb8805b 100644 --- a/raphtory/src/errors.rs +++ b/raphtory/src/errors.rs @@ -341,9 +341,12 @@ pub enum GraphError { #[error("Not supported")] NotSupported, - #[error("Node filter expected")] + #[error("Expression is not a node filter")] NotNodeFilter, + #[error("Expression is not an edge filter")] + 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 2506f4c795..8c3477b829 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, @@ -156,6 +154,10 @@ pub mod prelude { }, }; + pub use crate::db::graph::views::filter::model::{ + node_filter::NodeFilterFactory, EntityAggOps, EntityExprFilterOps, + }; + pub use storage::{ persist::{args::ArgsOps, config::ConfigOps}, Args, Config, diff --git a/raphtory/src/python/filter/edge_expr.rs b/raphtory/src/python/filter/edge_expr.rs new file mode 100644 index 0000000000..4ae60b387f --- /dev/null +++ b/raphtory/src/python/filter/edge_expr.rs @@ -0,0 +1,487 @@ +use crate::{ + db::graph::views::filter::model::{ + edge_filter::Endpoint, + tree::{Entity, Expr, Field, FilterExpr, Scope, Structural, Target, ViewOp}, + }, + python::{ + filter::{ + filter_expr::PyFilterExpr, + node_expr::{PyExpr, PyPropertyExpr}, + }, + types::iterable::FromIterable, + }, +}; +use pyo3::{pyclass, pymethods}; +use raphtory_api::core::storage::timeindex::EventTime; + +/// 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" +#[pyclass(frozen, name = "EdgeEndpoint", module = "raphtory.filter")] +pub struct PyEdgeEndpoint(pub(crate) Scope); + +impl PyEdgeEndpoint { + fn read(&self, target: Target) -> Expr { + Expr::Read { + scope: self.0.clone(), + target, + } + } +} + +#[pymethods] +impl PyEdgeEndpoint { + /// Selects the endpoint node ID field for filtering. + /// + /// Returns: + /// filter.Expr: + fn id(&self) -> PyExpr { + PyExpr(self.read(Target::Field(Field::Id))) + } + + /// Selects the endpoint node name field for filtering. + /// + /// Returns: + /// filter.Expr: + fn name(&self) -> PyExpr { + PyExpr(self.read(Target::Field(Field::Name))) + } + + /// Selects the endpoint node type field for filtering. + /// + /// Returns: + /// filter.Expr: + fn node_type(&self) -> PyExpr { + PyExpr(self.read(Target::Field(Field::NodeType))) + } + + /// Filters an endpoint node property by name. + /// + /// Arguments: + /// name (str): Property key. + /// + /// Returns: + /// filter.PropertyExpr: + fn property(&self, name: String) -> PyPropertyExpr { + PyPropertyExpr(self.read(Target::Property(name))) + } + + /// Filters an endpoint node metadata field by name. + /// + /// Arguments: + /// name (str): Metadata key. + /// + /// Returns: + /// filter.Expr: + fn metadata(&self, name: String) -> PyExpr { + PyExpr(self.read(Target::Metadata(name))) + } +} + +impl PyEdgeFilter { + pub(crate) fn root() -> Self { + PyEdgeFilter(Scope::new(Entity::Edge)) + } + + fn with_view(&self, view: ViewOp) -> Self { + PyEdgeFilter(self.0.clone().with_view(view)) + } + + fn read(&self, target: Target) -> Expr { + Expr::Read { + scope: self.0.clone(), + target, + } + } +} + +/// 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. +#[pyclass(frozen, name = "EdgeFilter", module = "raphtory.filter")] +pub struct PyEdgeFilter(pub(crate) Scope); + +#[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)) + } + + /// Selects the edge **destination endpoint** for filtering. + /// + /// Returns: + /// filter.EdgeEndpoint: + fn dst(&self) -> PyEdgeEndpoint { + PyEdgeEndpoint(self.0.clone().through(Endpoint::Dst)) + } + + /// Filters an edge property by name. + /// + /// Arguments: + /// name (str): Property key. + /// + /// Returns: + /// filter.PropertyExpr: + fn property(&self, name: String) -> PyPropertyExpr { + PyPropertyExpr(self.read(Target::Property(name))) + } + + /// Filters an edge metadata field by name. + /// + /// Arguments: + /// name (str): Metadata key. + /// + /// Returns: + /// filter.Expr: + fn metadata(&self, name: String) -> PyExpr { + PyExpr(self.read(Target::Metadata(name))) + } + + /// 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.with_view(ViewOp::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.with_view(ViewOp::At(time)) + } + + /// 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.with_view(ViewOp::After(time)) + } + + /// 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.with_view(ViewOp::Before(time)) + } + + /// Evaluates edge predicates against the latest available edge state. + /// + /// Returns: + /// filter.EdgeFilter: + fn latest(&self) -> PyEdgeFilter { + self.with_view(ViewOp::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.with_view(ViewOp::SnapshotAt(time)) + } + + /// Evaluates edge predicates against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.EdgeFilter: + fn snapshot_latest(&self) -> PyEdgeFilter { + self.with_view(ViewOp::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.with_view(ViewOp::Layers(vec![layer])) + } + + /// 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 { + self.with_view(ViewOp::Layers(layers.into())) + } + + /// 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, + }) + } + + /// 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, + }) + } + + /// Matches edges that have been deleted. + /// + /// Returns: + /// filter.FilterExpr: + fn is_deleted(&self) -> PyFilterExpr { + PyFilterExpr(FilterExpr::Structural { + scope: self.0.clone(), + pred: Structural::IsDeleted, + }) + } + + /// 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, + }) + } +} + +/// 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/edge_filter_builders.rs b/raphtory/src/python/filter/edge_filter_builders.rs deleted file mode 100644 index 8887dddbe9..0000000000 --- a/raphtory/src/python/filter/edge_filter_builders.rs +++ /dev/null @@ -1,526 +0,0 @@ -use crate::{ - db::graph::views::filter::model::{ - edge_filter::{EdgeEndpointWrapper, EdgeFilter}, - node_filter::{ - builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, - ops::{NodeFilterOps, NodeIdFilterOps}, - NodeFilter, - }, - property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, - EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, - }, - impl_node_text_filter_builder, - python::{ - filter::{ - filter_expr::PyFilterExpr, - property_filter_builders::{ - PyEdgeViewPropsFilterBuilder, PyPropertyExprBuilder, PyPropertyFilterBuilder, - }, - }, - types::iterable::FromIterable, - }, -}; -use pyo3::{pyclass, pymethods, types::PyAnyMethods, Bound, IntoPyObject, PyAny, PyResult, Python}; -use raphtory_api::core::{entities::GID, 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: &Bound<'_, PyAny>) -> PyResult { - // Extract explicitly: with a concrete parameter type pyo3 turns a - // conversion failure into `NotImplemented`, so Python falls back to - // its default `==` and hands back a plain `bool` instead of a filter - // expression — the comparison silently stops being a filter. - let value: GID = value.extract()?; - Ok(PyFilterExpr(Arc::new(self.0.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: &Bound<'_, PyAny>) -> PyResult { - // Extract explicitly: with a concrete parameter type pyo3 turns a - // conversion failure into `NotImplemented`, so Python falls back to - // its default `==` and hands back a plain `bool` instead of a filter - // expression — the comparison silently stops being a filter. - let value: GID = value.extract()?; - Ok(PyFilterExpr(Arc::new(self.0.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.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.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.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.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.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.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.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.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.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.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.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); - -impl_node_text_filter_builder!(PyEdgeEndpointNameFilterBuilder); -impl_node_text_filter_builder!(PyEdgeEndpointTypeFilterBuilder); - -/// 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" -#[pyclass(frozen, name = "EdgeEndpoint", module = "raphtory.filter")] -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()) - } - - /// Selects the endpoint node name field for filtering. - /// - /// Returns: - /// filter.EdgeEndpointNameFilter: - fn name(&self) -> PyEdgeEndpointNameFilterBuilder { - PyEdgeEndpointNameFilterBuilder(self.0.name()) - } - - /// Selects the endpoint node type field for filtering. - /// - /// Returns: - /// filter.EdgeEndpointTypeFilter: - fn node_type(&self) -> PyEdgeEndpointTypeFilterBuilder { - PyEdgeEndpointTypeFilterBuilder(self.0.node_type()) - } - - /// 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) - } - - /// 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) - } -} - -/// 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() -#[pyclass(frozen, name = "Edge", module = "raphtory.filter")] -pub struct PyEdgeFilter; - -#[pymethods] -impl PyEdgeFilter { - /// Selects the edge **source endpoint** for filtering. - /// - /// Returns: - /// filter.EdgeEndpoint: - #[staticmethod] - fn src() -> PyEdgeEndpoint { - PyEdgeEndpoint(EdgeFilter::src()) - } - - /// Selects the edge **destination endpoint** for filtering. - /// - /// Returns: - /// filter.EdgeEndpoint: - #[staticmethod] - fn dst() -> 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) - } - - /// 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) - } - - /// 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))) - } - - /// 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))) - } - - /// 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))) - } - - /// 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))) - } - - /// Evaluates edge predicates against the latest available edge state. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn latest() -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(EdgeFilter.latest())) - } - - /// 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))) - } - - /// 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())) - } - - /// 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))) - } - - /// 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))) - } - - /// 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())) - } - - /// Matches edges that are structurally valid in the current view. - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_valid() -> PyFilterExpr { - PyFilterExpr(Arc::new(EdgeFilter.is_valid())) - } - - /// Matches edges that have been deleted. - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_deleted() -> PyFilterExpr { - PyFilterExpr(Arc::new(EdgeFilter.is_deleted())) - } - - /// Matches edges that are self-loops (source == destination). - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_self_loop() -> PyFilterExpr { - PyFilterExpr(Arc::new(EdgeFilter.is_self_loop())) - } -} diff --git a/raphtory/src/python/filter/exploded_edge_expr.rs b/raphtory/src/python/filter/exploded_edge_expr.rs new file mode 100644 index 0000000000..8c82eaa746 --- /dev/null +++ b/raphtory/src/python/filter/exploded_edge_expr.rs @@ -0,0 +1,393 @@ +use crate::{ + db::graph::views::filter::model::tree::{ + Entity, Expr, FilterExpr, Scope, Structural, Target, ViewOp, + }, + python::{ + filter::{ + filter_expr::PyFilterExpr, + node_expr::{PyExpr, PyPropertyExpr}, + }, + types::iterable::FromIterable, + }, +}; +use pyo3::{pyclass, pymethods}; +use raphtory_api::core::storage::timeindex::EventTime; + +/// 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. +#[pyclass(frozen, name = "ExplodedEdgeFilter", module = "raphtory.filter")] +pub struct PyExplodedEdgeFilter(pub(crate) Scope); + +impl PyExplodedEdgeFilter { + pub(crate) fn root() -> Self { + PyExplodedEdgeFilter(Scope::new(Entity::ExplodedEdge)) + } + + fn with_view(&self, view: ViewOp) -> Self { + PyExplodedEdgeFilter(self.0.clone().with_view(view)) + } + + fn read(&self, target: Target) -> Expr { + Expr::Read { + scope: self.0.clone(), + target, + } + } +} + +#[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. + /// + /// Arguments: + /// name (str): Property key. + /// + /// Returns: + /// filter.PropertyExpr: + fn property(&self, name: String) -> PyPropertyExpr { + PyPropertyExpr(self.read(Target::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: + fn metadata(&self, name: String) -> PyExpr { + PyExpr(self.read(Target::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: + fn window(&self, start: EventTime, end: EventTime) -> PyExplodedEdgeFilter { + self.with_view(ViewOp::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.with_view(ViewOp::At(time)) + } + + /// 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.with_view(ViewOp::After(time)) + } + + /// 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.with_view(ViewOp::Before(time)) + } + + /// Evaluates exploded edge predicates against the latest available state. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + fn latest(&self) -> PyExplodedEdgeFilter { + self.with_view(ViewOp::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.with_view(ViewOp::SnapshotAt(time)) + } + + /// Evaluates exploded edge predicates against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.ExplodedEdgeFilter: + fn snapshot_latest(&self) -> PyExplodedEdgeFilter { + self.with_view(ViewOp::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.with_view(ViewOp::Layers(vec![layer])) + } + + /// 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 { + self.with_view(ViewOp::Layers(layers.into())) + } + + /// 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, + }) + } + + /// 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, + }) + } + + /// 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, + }) + } + + /// 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, + }) + } +} + +/// 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/exploded_edge_filter_builder.rs b/raphtory/src/python/filter/exploded_edge_filter_builder.rs deleted file mode 100644 index 3766a9c044..0000000000 --- a/raphtory/src/python/filter/exploded_edge_filter_builder.rs +++ /dev/null @@ -1,216 +0,0 @@ -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_filter_builders::{ - PyEdgeViewPropsFilterBuilder, PyPropertyExprBuilder, PyPropertyFilterBuilder, - }, - }, - types::iterable::FromIterable, - }, -}; -use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyResult, Python}; -use raphtory_api::core::storage::timeindex::EventTime; -use std::sync::Arc; - -/// 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. -/// -/// 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")] -pub struct PyExplodedEdgeFilter; - -#[pymethods] -impl PyExplodedEdgeFilter { - /// 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) - } - - /// 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.FilterOps: - #[staticmethod] - fn metadata<'py>(py: Python<'py>, name: String) -> PyResult> { - let b: MetadataFilterBuilder = - PropertyFilterFactory::metadata(&ExplodedEdgeFilter, name); - b.into_pyobject(py) - } - - /// 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))) - } - - /// 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))) - } - - /// 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))) - } - - /// 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))) - } - - /// Evaluates exploded edge predicates against the latest available state. - /// - /// Returns: - /// filter.EdgeViewPropsFilterBuilder: - #[staticmethod] - fn latest() -> PyEdgeViewPropsFilterBuilder { - PyEdgeViewPropsFilterBuilder(Arc::new(ExplodedEdgeFilter.latest())) - } - - /// 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))) - } - - /// 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())) - } - - /// 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))) - } - - /// 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))) - } - - /// 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())) - } - - /// 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())) - } - - /// Matches exploded edges that have been deleted. - /// - /// Returns: - /// filter.FilterExpr: - #[staticmethod] - fn is_deleted() -> PyFilterExpr { - PyFilterExpr(Arc::new(ExplodedEdgeFilter.is_deleted())) - } - - /// 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())) - } -} diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 0a6973afb0..29d1e0eab8 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -1,23 +1,18 @@ use crate::{ db::{ api::{ - state::ops::NodeOp, - view::{internal::GraphView, BoxableGraphView}, - }, - graph::views::filter::{ - model::{ - edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, - not_filter::NotFilter, or_filter::OrFilter, AndFilter, DynCreateFilter, FilterTree, - TryAsCompositeFilter, - }, - 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", @@ -26,44 +21,57 @@ use std::sync::Arc; from_py_object )] #[derive(Clone)] -pub struct PyFilterExpr(pub Arc); +pub struct PyFilterExpr(pub FilterExpr); impl PyFilterExpr { - pub fn try_as_filter_tree(&self) -> Result { - self.0.try_as_filter_tree() - } - - 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() + 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(); - PyFilterExpr(Arc::new(AndFilter { left, right })) + PyFilterExpr(FilterExpr::And(vec![self.0.clone(), other.0.clone()])) + } + + 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) -> PyResult { + no_view(&self.0)?; + Ok(PyFilterExpr(FilterExpr::Not(Box::new(self.0.clone())))) } - pub fn __or__(&self, other: &Self) -> Self { - let left = self.0.clone(); - let right = other.0.clone(); - PyFilterExpr(Arc::new(OrFilter { left, right })) + /// Shows the filter tree: what runs locally and what a server receives. + fn __repr__(&self) -> String { + format!("FilterExpr({})", self.0) } +} - fn __invert__(&self) -> Self { - PyFilterExpr(Arc::new(NotFilter(self.0.clone()))) +/// 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> @@ -71,7 +79,7 @@ impl CreateFilter for PyFilterExpr { Self: 'graph; type FilteredGraph<'graph, G> - = Arc + = DynGraphArc<'graph> where Self: 'graph, G: GraphView + 'graph; diff --git a/raphtory/src/python/filter/graph_filter.rs b/raphtory/src/python/filter/graph_filter.rs index f3e47810ec..5706f65c8d 100644 --- a/raphtory/src/python/filter/graph_filter.rs +++ b/raphtory/src/python/filter/graph_filter.rs @@ -1,31 +1,35 @@ 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::tree::{FilterExpr, ViewOp}, + 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; -/// 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 `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() -#[pyclass(frozen, name = "Graph", module = "raphtory.filter", from_py_object)] -#[derive(Clone, Copy)] -pub struct PyGraphFilter; +/// 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 = "GraphFilter", + module = "raphtory.filter", + extends = PyFilterExpr, + frozen +)] +pub struct PyGraphFilter(pub(crate) Vec); + +impl PyGraphFilter { + pub(crate) fn root() -> Self { + PyGraphFilter(Vec::new()) + } + + fn with_view(&self, view: ViewOp) -> Self { + let mut ops = self.0.clone(); + ops.push(view); + PyGraphFilter(ops) + } +} #[pymethods] impl PyGraphFilter { @@ -38,10 +42,116 @@ impl PyGraphFilter { /// end (int): End time. /// /// Returns: - /// filter.ViewFilterBuilder: + /// filter.GraphFilter: + fn window(&self, start: EventTime, end: EventTime) -> PyGraphFilter { + self.with_view(ViewOp::Window { start, end }) + } + + /// Restricts evaluation to a single point in time. + /// + /// Arguments: + /// time (int): Event time. + /// + /// Returns: + /// filter.GraphFilter: + fn at(&self, time: EventTime) -> PyGraphFilter { + self.with_view(ViewOp::At(time)) + } + + /// Restricts evaluation to times strictly after the given time. + /// + /// Arguments: + /// time (int): Lower time bound. + /// + /// Returns: + /// filter.GraphFilter: + fn after(&self, time: EventTime) -> PyGraphFilter { + self.with_view(ViewOp::After(time)) + } + + /// Restricts evaluation to times strictly before the given time. + /// + /// Arguments: + /// time (int): Upper time bound. + /// + /// Returns: + /// filter.GraphFilter: + fn before(&self, time: EventTime) -> PyGraphFilter { + self.with_view(ViewOp::Before(time)) + } + + /// Evaluates filters against the latest available state of the graph. + /// + /// Returns: + /// filter.GraphFilter: + fn latest(&self) -> PyGraphFilter { + self.with_view(ViewOp::Latest) + } + + /// Evaluates filters against a snapshot of the graph at a given time. + /// + /// Arguments: + /// time (int): Snapshot time. + /// + /// Returns: + /// filter.GraphFilter: + fn snapshot_at(&self, time: EventTime) -> PyGraphFilter { + self.with_view(ViewOp::SnapshotAt(time)) + } + + /// Evaluates filters against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.GraphFilter: + fn snapshot_latest(&self) -> PyGraphFilter { + self.with_view(ViewOp::SnapshotLatest) + } + + /// Restricts evaluation to a single layer. + /// + /// Arguments: + /// layer (str): Layer name. + /// + /// Returns: + /// filter.GraphFilter: + fn layer(&self, layer: String) -> PyGraphFilter { + self.with_view(ViewOp::Layers(vec![layer])) + } + + /// Restricts evaluation to any of the given layers. + /// + /// Arguments: + /// layers (list[str]): Layer names. + /// + /// Returns: + /// filter.GraphFilter: + fn layers(&self, layers: FromIterable) -> PyGraphFilter { + self.with_view(ViewOp::Layers(layers.into())) + } +} + +/// 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) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.window(start, end))) + fn window(start: EventTime, end: EventTime) -> PyGraphFilter { + PyGraphFilter::root().window(start, end) } /// Restricts evaluation to a single point in time. @@ -50,10 +160,10 @@ impl PyGraphFilter { /// time (int): Event time. /// /// Returns: - /// filter.ViewFilterBuilder: + /// filter.GraphFilter: #[staticmethod] - fn at(time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.at(time))) + fn at(time: EventTime) -> PyGraphFilter { + PyGraphFilter::root().at(time) } /// Restricts evaluation to times strictly after the given time. @@ -62,10 +172,10 @@ impl PyGraphFilter { /// time (int): Lower time bound. /// /// Returns: - /// filter.ViewFilterBuilder: + /// filter.GraphFilter: #[staticmethod] - fn after(time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.after(time))) + fn after(time: EventTime) -> PyGraphFilter { + PyGraphFilter::root().after(time) } /// Restricts evaluation to times strictly before the given time. @@ -74,19 +184,19 @@ impl PyGraphFilter { /// time (int): Upper time bound. /// /// Returns: - /// filter.ViewFilterBuilder: + /// filter.GraphFilter: #[staticmethod] - fn before(time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.before(time))) + fn before(time: EventTime) -> PyGraphFilter { + PyGraphFilter::root().before(time) } /// Evaluates filters against the latest available state of the graph. /// /// Returns: - /// filter.ViewFilterBuilder: + /// filter.GraphFilter: #[staticmethod] - fn latest() -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.latest())) + fn latest() -> PyGraphFilter { + PyGraphFilter::root().latest() } /// Evaluates filters against a snapshot of the graph at a given time. @@ -95,19 +205,19 @@ impl PyGraphFilter { /// time (int): Snapshot time. /// /// Returns: - /// filter.ViewFilterBuilder: + /// filter.GraphFilter: #[staticmethod] - fn snapshot_at(time: EventTime) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.snapshot_at(time))) + fn snapshot_at(time: EventTime) -> PyGraphFilter { + PyGraphFilter::root().snapshot_at(time) } /// Evaluates filters against the most recent snapshot of the graph. /// /// Returns: - /// filter.ViewFilterBuilder: + /// filter.GraphFilter: #[staticmethod] - fn snapshot_latest() -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.snapshot_latest())) + fn snapshot_latest() -> PyGraphFilter { + PyGraphFilter::root().snapshot_latest() } /// Restricts evaluation to a single layer. @@ -116,10 +226,10 @@ impl PyGraphFilter { /// layer (str): Layer name. /// /// Returns: - /// filter.ViewFilterBuilder: + /// filter.GraphFilter: #[staticmethod] - fn layer(layer: String) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.layer(layer))) + fn layer(layer: String) -> PyGraphFilter { + PyGraphFilter::root().layer(layer) } /// Restricts evaluation to any of the given layers. @@ -128,9 +238,20 @@ impl PyGraphFilter { /// layers (list[str]): Layer names. /// /// Returns: - /// filter.ViewFilterBuilder: + /// filter.GraphFilter: #[staticmethod] - fn layers(layers: FromIterable) -> PyViewFilterBuilder { - PyViewFilterBuilder(Arc::new(GraphFilter.layer(layers))) + fn layers(layers: FromIterable) -> PyGraphFilter { + PyGraphFilter::root().layers(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(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 f585a12036..08d7b0e67e 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -1,16 +1,10 @@ use crate::python::{ filter::{ - edge_filter_builders::{ - PyEdgeEndpoint, PyEdgeEndpointIdFilterBuilder, PyEdgeEndpointNameFilterBuilder, - PyEdgeEndpointTypeFilterBuilder, PyEdgeFilter, - }, - exploded_edge_filter_builder::PyExplodedEdgeFilter, + edge_expr::{PyEdge, PyEdgeEndpoint, PyEdgeFilter}, + exploded_edge_expr::{PyExplodedEdge, PyExplodedEdgeFilter}, filter_expr::PyFilterExpr, - graph_filter::PyGraphFilter, - node_filter_builders::{ - PyNodeFilter, PyNodeIdFilterBuilder, PyNodeNameFilterBuilder, PyNodeTypeFilterBuilder, - }, - property_filter_builders::{PyPropertyExprBuilder, PyPropertyFilterBuilder}, + graph_filter::{PyGraph, PyGraphFilter}, + node_expr::{PyExpr, PyNode, PyNodeFilter, PyPropertyExpr}, }, types::iterable::FromIterable, }; @@ -20,12 +14,11 @@ 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; impl From> for Layer { fn from(iter: FromIterable) -> Self { @@ -37,21 +30,20 @@ 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::()?; - filter_module.add_class::()?; - filter_module.add_class::()?; + filter_module.add_class::()?; filter_module.add_class::()?; + + filter_module.add_class::()?; filter_module.add_class::()?; Ok(filter_module) diff --git a/raphtory/src/python/filter/node_expr.rs b/raphtory/src/python/filter/node_expr.rs new file mode 100644 index 0000000000..0ac14eda07 --- /dev/null +++ b/raphtory/src/python/filter/node_expr.rs @@ -0,0 +1,837 @@ +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, + }, + }, + python::{ + filter::filter_expr::PyFilterExpr, graph::node_state::PyOutputNodeState, + types::iterable::FromIterable, + }, +}; +use pyo3::{ + exceptions::{PyTypeError, PyValueError}, + pyclass, pymethods, Bound, FromPyObject, IntoPyObject, PyErr, PyResult, Python, +}; +use raphtory_api::core::{ + entities::properties::prop::{Prop, PropType}, + storage::timeindex::EventTime, + Direction, +}; +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`]. +#[pyclass( + frozen, + subclass, + name = "Expr", + module = "raphtory.filter", + from_py_object +)] +#[derive(Clone)] +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(pub(crate) Expr); + +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()); + Bound::new(py, (self, parent)) + } +} + +/// 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), + 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. +fn static_type(lhs: &Expr) -> PyResult { + Ok(lhs.compile()?.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}", + 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) +} + +/// 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(()) +} + +/// 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(()) +} + +/// 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) + } + }) +} + +/// 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 { + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Eq, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) + } + fn __ne__(&self, other: ExprOrValue) -> PyResult { + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Ne, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) + } + fn __lt__(&self, other: ExprOrValue) -> PyResult { + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Lt, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) + } + fn __le__(&self, other: ExprOrValue) -> PyResult { + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Le, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) + } + fn __gt__(&self, other: ExprOrValue) -> PyResult { + Ok(PyFilterExpr(FilterExpr::Cmp { + op: CmpOp::Gt, + lhs: self.0.clone(), + rhs: compared(&self.0, other)?, + })) + } + fn __ge__(&self, other: ExprOrValue) -> PyResult { + 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. + /// + /// Arguments: + /// other (Prop | filter.Expr): Prefix to check for. + /// + /// 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)?, + })) + } + /// 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 { + 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. + /// + /// Arguments: + /// other (Prop | filter.Expr): Substring that must appear within the value. + /// + /// 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)?, + })) + } + /// 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 { + 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. + /// + /// 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, + levenshtein_distance: usize, + prefix_match: bool, + ) -> PyResult { + 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. + /// + /// Arguments: + /// 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: checked_values(&self.0, values)?, + negated: 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: checked_values(&self.0, values)?, + negated: true, + })) + } + + /// Checks whether the value is present (not `None`). + /// + /// Returns: + /// filter.FilterExpr: + 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) -> 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). + /// + /// Returns: + /// filter.Expr: + fn any(&self) -> Self { + 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 { + PyExpr(Expr::Qual(Qual::All, Box::new(self.0.clone()))) + } + + /// 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()))) + } + /// 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()))) + } + /// 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()))) + } + /// 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()))) + } + /// 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()))) + } + /// 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()))) + } + /// 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()))) + } +} + +#[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(Expr::Temporal(Box::new(self.0.clone()))) + } +} + +/// 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. +#[pyclass(frozen, name = "NodeFilter", module = "raphtory.filter")] +pub struct PyNodeFilter(pub(crate) Scope); + +impl PyNodeFilter { + pub(crate) fn root() -> Self { + PyNodeFilter(Scope::new(Entity::Node)) + } + + fn with_view(&self, view: ViewOp) -> Self { + PyNodeFilter(self.0.clone().with_view(view)) + } + + fn read(&self, target: Target) -> Expr { + Expr::Read { + scope: self.0.clone(), + target, + } + } +} + +#[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))) + } + + /// Selects the node name field for filtering. + /// + /// Returns: + /// filter.Expr: + fn name(&self) -> PyExpr { + PyExpr(self.read(Target::Field(Field::Name))) + } + + /// Selects the node type field for filtering. + /// + /// Returns: + /// filter.Expr: + fn node_type(&self) -> PyExpr { + PyExpr(self.read(Target::Field(Field::NodeType))) + } + + /// Selects incoming node degree for filtering. + /// + /// Returns: + /// filter.Expr: + fn in_degree(&self) -> PyExpr { + PyExpr(self.read(Target::Degree(Direction::IN))) + } + + /// Selects total node degree for filtering. + /// + /// Returns: + /// filter.Expr: + fn degree(&self) -> PyExpr { + PyExpr(self.read(Target::Degree(Direction::BOTH))) + } + + /// Selects outgoing node degree for filtering. + /// + /// Returns: + /// filter.Expr: + fn out_degree(&self) -> PyExpr { + PyExpr(self.read(Target::Degree(Direction::OUT))) + } + + /// 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: + fn property(&self, name: String) -> PyPropertyExpr { + PyPropertyExpr(self.read(Target::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: + fn metadata(&self, name: String) -> PyExpr { + PyExpr(self.read(Target::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: + fn window(&self, start: EventTime, end: EventTime) -> PyNodeFilter { + self.with_view(ViewOp::Window { start, end }) + } + + /// Restricts node evaluation to a single point in time. + /// + /// Arguments: + /// time (int): Event time. + /// + /// Returns: + /// filter.NodeFilter: + fn at(&self, time: EventTime) -> PyNodeFilter { + self.with_view(ViewOp::At(time)) + } + + /// Restricts node evaluation to times strictly after the given time. + /// + /// Arguments: + /// time (int): Lower time bound. + /// + /// Returns: + /// filter.NodeFilter: + fn after(&self, time: EventTime) -> PyNodeFilter { + self.with_view(ViewOp::After(time)) + } + + /// Restricts node evaluation to times strictly before the given time. + /// + /// Arguments: + /// time (int): Upper time bound. + /// + /// Returns: + /// filter.NodeFilter: + fn before(&self, time: EventTime) -> PyNodeFilter { + self.with_view(ViewOp::Before(time)) + } + + /// Evaluates filters against the latest available state of each node. + /// + /// Returns: + /// filter.NodeFilter: + fn latest(&self) -> PyNodeFilter { + self.with_view(ViewOp::Latest) + } + + /// Evaluates filters against a snapshot of the graph at a given time. + /// + /// Arguments: + /// time (int): Snapshot time. + /// + /// Returns: + /// filter.NodeFilter: + fn snapshot_at(&self, time: EventTime) -> PyNodeFilter { + self.with_view(ViewOp::SnapshotAt(time)) + } + + /// Evaluates filters against the most recent snapshot of the graph. + /// + /// Returns: + /// filter.NodeFilter: + fn snapshot_latest(&self) -> PyNodeFilter { + self.with_view(ViewOp::SnapshotLatest) + } + + /// Restricts evaluation to nodes belonging to the given layer. + /// + /// Arguments: + /// layer (str): Layer name. + /// + /// Returns: + /// filter.NodeFilter: + fn layer(&self, layer: String) -> PyNodeFilter { + self.with_view(ViewOp::Layers(vec![layer])) + } + + /// Restricts evaluation to nodes belonging to any of the given layers. + /// + /// Arguments: + /// layers (list[str]): Layer names. + /// + /// Returns: + /// filter.NodeFilter: + fn layers(&self, layers: FromIterable) -> PyNodeFilter { + self.with_view(ViewOp::Layers(layers.into())) + } + + /// 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, + }) + } + + /// 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: + fn by_state_column(&self, state: &PyOutputNodeState, col: String) -> PyResult { + let op = NodeStateBoolColOp::new(&state.inner, &col) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(PyFilterExpr(FilterExpr::Opaque(OpaqueFilter(Arc::new(op))))) + } +} + +/// 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/node_filter_builders.rs b/raphtory/src/python/filter/node_filter_builders.rs deleted file mode 100644 index 069e60ee08..0000000000 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ /dev/null @@ -1,613 +0,0 @@ -use crate::{ - db::graph::views::filter::model::{ - degree_filter::DegreeFilterFactory, - node_filter::{ - builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, - ops::{NodeFilterOps, NodeIdFilterOps}, - NodeFilter, - }, - node_state_filter::NodeStateBoolColOp, - property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, - NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, - }, - python::{ - filter::{ - filter_expr::PyFilterExpr, - property_filter_builders::{ - PyNodeViewPropsFilterBuilder, PyPropertyExprBuilder, PyPropertyFilterBuilder, - }, - }, - graph::node_state::PyOutputNodeState, - types::iterable::FromIterable, - }, -}; -use pyo3::{pyclass, pymethods, types::PyAnyMethods, Bound, IntoPyObject, PyAny, PyResult, Python}; -use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; -use std::sync::Arc; - -/// 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(Arc); - -#[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: &Bound<'_, PyAny>) -> PyResult { - // Extract explicitly: with a concrete parameter type pyo3 turns a - // conversion failure into `NotImplemented`, so Python falls back to - // its default `==` and hands back a plain `bool` instead of a filter - // expression — the comparison silently stops being a filter. - let value: GID = value.extract()?; - Ok(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: &Bound<'_, PyAny>) -> PyResult { - // Extract explicitly: with a concrete parameter type pyo3 turns a - // conversion failure into `NotImplemented`, so Python falls back to - // its default `==` and hands back a plain `bool` instead of a filter - // expression — the comparison silently stops being a filter. - let value: GID = value.extract()?; - Ok(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))) - } - - /// 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, - ))) - } -} - -/// 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(Arc); - -/// 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(Arc); - -#[macro_export] -macro_rules! impl_node_text_filter_builder { - ($py_ty:ident) => { - #[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: &Bound<'_, PyAny>) -> PyResult { - // Extract explicitly: with a concrete parameter type pyo3 turns a - // conversion failure into `NotImplemented`, so Python falls back to - // its default `==` and hands back a plain `bool` instead of a filter - // expression — the comparison silently stops being a filter. - let value: String = value.extract()?; - Ok(PyFilterExpr(Arc::new(self.0.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: &Bound<'_, PyAny>) -> PyResult { - // Extract explicitly: with a concrete parameter type pyo3 turns a - // conversion failure into `NotImplemented`, so Python falls back to - // its default `==` and hands back a plain `bool` instead of a filter - // expression — the comparison silently stops being a filter. - let value: String = value.extract()?; - Ok(PyFilterExpr(Arc::new(self.0.ne(value)))) - } - - /// Returns a filter expression that checks whether the entity's - /// string value is contained within the given iterable of strings. - /// - /// Arguments: - /// values (list[str]): Iterable of allowed string values. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating membership. - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new(self.0.is_in(vals))) - } - - /// Returns a filter expression that checks whether the entity's - /// string value is **not** contained within the given iterable of strings. - /// - /// Arguments: - /// values (list[str]): Iterable of string values to exclude. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating non-membership. - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new(self.0.is_not_in(vals))) - } - - /// 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))) - } - - /// 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))) - } - - /// 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))) - } - - /// 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))) - } - - /// 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( - value, - levenshtein_distance, - prefix_match, - ))) - } - } - }; -} - -impl_node_text_filter_builder!(PyNodeNameFilterBuilder); -impl_node_text_filter_builder!(PyNodeTypeFilterBuilder); - -/// 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. -#[pyclass(frozen, name = "Node", module = "raphtory.filter")] -pub struct PyNodeFilter; - -#[pymethods] -impl PyNodeFilter { - /// Selects the node ID field for filtering. - /// - /// Returns: - /// filter.NodeIdFilterBuilder: - #[staticmethod] - fn id() -> PyNodeIdFilterBuilder { - PyNodeIdFilterBuilder(Arc::new(NodeFilter::id())) - } - - /// Selects the node name field for filtering. - /// - /// Returns: - /// filter.NodeNameFilterBuilder: - #[staticmethod] - fn name() -> PyNodeNameFilterBuilder { - PyNodeNameFilterBuilder(Arc::new(NodeFilter::name())) - } - - /// Selects the node type field for filtering. - /// - /// Returns: - /// filter.NodeTypeFilterBuilder: - #[staticmethod] - fn node_type() -> PyNodeTypeFilterBuilder { - PyNodeTypeFilterBuilder(Arc::new(NodeFilter::node_type())) - } - - /// Selects incoming node degree for filtering. - /// - /// Returns: - /// filter.FilterOps: a builder that selects the node degree for filtering. - #[staticmethod] - fn in_degree<'py>(_py: Python<'py>) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(Arc::new(NodeFilter.in_degree())) - } - - /// Selects total node degree for filtering. - /// - /// Returns: - /// filter.FilterOps: a builder that selects the node degree for filtering. - #[staticmethod] - fn degree<'py>(_py: Python<'py>) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(Arc::new(NodeFilter.degree())) - } - - /// Selects outgoing node degree for filtering. - /// - /// Returns: - /// filter.FilterOps: a builder that selects the node degree for filtering. - #[staticmethod] - fn out_degree<'py>(_py: Python<'py>) -> PyPropertyExprBuilder { - PyPropertyExprBuilder(Arc::new(NodeFilter.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.PropertyFilterOps: - #[staticmethod] - fn property<'py>( - py: Python<'py>, - name: String, - ) -> PyResult> { - let b: PropertyFilterBuilder = - PropertyFilterFactory::property(&NodeFilter, name); - b.into_pyobject(py) - } - - /// 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] - fn metadata<'py>(py: Python<'py>, name: String) -> PyResult> { - let b: MetadataFilterBuilder = - PropertyFilterFactory::metadata(&NodeFilter, name); - b.into_pyobject(py) - } - - /// 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: - #[staticmethod] - fn window(start: EventTime, end: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.window(start, end))) - } - - /// Restricts node evaluation to a single point in time. - /// - /// Arguments: - /// time (int): Event time. - /// - /// Returns: - /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn at(time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.at(time))) - } - - /// Restricts node evaluation to times strictly after the given time. - /// - /// Arguments: - /// time (int): Lower time bound. - /// - /// Returns: - /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn after(time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.after(time))) - } - - /// Restricts node evaluation to times strictly before the given time. - /// - /// Arguments: - /// time (int): Upper time bound. - /// - /// Returns: - /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn before(time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.before(time))) - } - - /// Evaluates filters against the latest available state of each node. - /// - /// Returns: - /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn latest() -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.latest())) - } - - /// Evaluates filters against a snapshot of the graph at a given time. - /// - /// Arguments: - /// time (int): Snapshot time. - /// - /// Returns: - /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn snapshot_at(time: EventTime) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.snapshot_at(time))) - } - - /// Evaluates filters against the most recent snapshot of the graph. - /// - /// Returns: - /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn snapshot_latest() -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.snapshot_latest())) - } - - /// Restricts evaluation to nodes belonging to the given layer. - /// - /// Arguments: - /// layer (str): Layer name. - /// - /// Returns: - /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn layer(layer: String) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.layer(layer))) - } - - /// Restricts evaluation to nodes belonging to any of the given layers. - /// - /// Arguments: - /// layers (list[str]): Layer names. - /// - /// Returns: - /// filter.NodeViewPropsFilterBuilder: - #[staticmethod] - fn layers(layers: FromIterable) -> PyNodeViewPropsFilterBuilder { - PyNodeViewPropsFilterBuilder(Arc::new(NodeFilter.layer(layers))) - } - - /// 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())) - } - - /// 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 { - let op = NodeStateBoolColOp::new(&state.inner, &col) - .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; - Ok(PyFilterExpr(Arc::new(op))) - } -} 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 d676f9e4ea..0000000000 --- a/raphtory/src/python/filter/property_filter_builders.rs +++ /dev/null @@ -1,683 +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, DynPropertyFilterBuilder, - DynTemporalPropertyFilterBuilder, DynView, EntityMarker, InternalPropertyFilterBuilder, - PropertyFilterFactory, TemporalPropertyFilterFactory, TryAsCompositeFilter, - ViewWrapOps, - }, - CreateFilter, - }, - prelude::PropertyFilter, - python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, -}; -use pyo3::{ - pyclass, pymethods, types::PyAnyMethods, Bound, IntoPyObject, PyAny, PyErr, PyResult, 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: &Bound<'_, PyAny>) -> PyResult { - // Extract explicitly: with a concrete parameter type pyo3 turns a - // conversion failure into `NotImplemented`, so Python falls back to - // its default `==` and hands back a plain `bool` instead of a filter - // expression — the comparison silently stops being a filter. - let value: Prop = value.extract()?; - Ok(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: &Bound<'_, PyAny>) -> PyResult { - // Extract explicitly: with a concrete parameter type pyo3 turns a - // conversion failure into `NotImplemented`, so Python falls back to - // its default `==` and hands back a plain `bool` instead of a filter - // expression — the comparison silently stops being a filter. - let value: Prop = value.extract()?; - Ok(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))) - } -} 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; 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: