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.
-| direction |
-DegreeDirection! |
+lhs |
+Expr! |
|
-| where |
-PropCondition! |
+rhs |
+Expr! |
|
@@ -11883,36 +11885,7 @@ Metadata.
-### 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
-| src |
-NodeFilter |
+reverse |
+Boolean |
-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.
|
-| dst |
-NodeFilter |
+src |
+NodeSortBy |
-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.
|
-| property |
-PropertyFilterNew |
+dst |
+NodeSortBy |
-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.
|
-| metadata |
-PropertyFilterNew |
+neighbour |
+NodeSortBy |
-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).
|
-| temporalProperty |
-PropertyFilterNew |
+time |
+SortByTime |
-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!] |
+property |
+String |
-Logical **AND** over multiple edge filters.
+Property
-All nested filters must evaluate to `true`.
+ |
+
+
+
-Example:
-`{ And: [ { IsActive: true }, { IsValid: true } ] }`
+### EdgeViewCollection
-
+
+
+
+| Field |
+Type |
+Description |
+
+
-| or |
-[EdgeFilter!] |
+defaultLayer |
+Boolean |
-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.
|
-| not |
-EdgeFilter |
+latest |
+Boolean |
-Logical **NOT** over a nested edge filter.
-
-Negates the result of the wrapped filter.
-
-Example:
-`{ Not: { IsDeleted: true } }`
+Latest time.
|
-| window |
-EdgeWindowExpr |
+snapshotLatest |
+Boolean |
-Restricts edge evaluation to a **time window**.
-
-The window is inclusive of `start` and exclusive of `end`.
+Snapshot at latest time.
|
-| at |
-EdgeTimeExpr |
+snapshotAt |
+TimeInput |
-Restricts edge evaluation to a **single point in time**.
+Snapshot at specified time.
|
-| before |
-EdgeTimeExpr |
+layers |
+[String!] |
-Restricts edge evaluation to times **strictly before** a given time.
+List of included layers.
|
-| after |
-EdgeTimeExpr |
+excludeLayers |
+[String!] |
-Restricts edge evaluation to times **strictly after** a given time.
+List of excluded layers.
|
-| latest |
-EdgeUnaryExpr |
+excludeLayer |
+String |
-Evaluates edge predicates against the **latest available state**.
+Single excluded layer.
|
-| snapshotAt |
-EdgeTimeExpr |
+window |
+Window |
-Evaluates edge predicates against a **snapshot** of the graph
-at a specific time.
+Window between a start and end time.
|
-| snapshotLatest |
-EdgeUnaryExpr |
+at |
+TimeInput |
-Evaluates edge predicates against the **most recent snapshot**
-of the graph.
+View at a specified time.
|
-| layers |
-EdgeLayersExpr |
+before |
+TimeInput |
-Restricts evaluation to edges belonging to one or more **layers**.
-
-Example:
-`{ Layers: { values: ["fire_nation", "air_nomads"] } }`
+View before a specified time (end exclusive).
|
-| isActive |
-Boolean |
+after |
+TimeInput |
-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).
|
-| isValid |
-Boolean |
+shrinkStart |
+TimeInput |
-Matches edges that are structurally valid (i.e. not deleted)
-in the current view/window.
+Set the window start to a specified time.
|
-| isDeleted |
-Boolean |
+shrinkEnd |
+TimeInput |
-Matches edges that have been deleted in the current view/window.
+Set the window end to a specified time.
|
-| isSelfLoop |
-Boolean |
+filter |
+FilterExpr |
-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!]! |
+defaultLayer |
+Boolean |
-Layer names to include.
+Contains only the default layer.
|
-| expr |
-EdgeFilter! |
+latest |
+Boolean |
-Filter evaluated within the layer-restricted view.
+Latest time.
|
-
-
+
+| snapshotLatest |
+Boolean |
+
-### EdgeSortBy
+Snapshot at latest time.
-
-
-
-| Field |
-Type |
-Description |
+
-
-
-| reverse |
-Boolean |
+snapshotAt |
+TimeInput |
-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.
|
-| src |
-NodeSortBy |
+layers |
+[String!] |
-Sort by the source node.
+List of included layers.
|
-| dst |
-NodeSortBy |
+excludeLayers |
+[String!] |
-Sort by the destination node.
+List of excluded layers.
|
-| neighbour |
-NodeSortBy |
+excludeLayer |
+String |
-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.
|
-| time |
-SortByTime |
+window |
+Window |
-Time
+Window between a start and end time.
|
-| property |
-String |
+at |
+TimeInput |
-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).
-
-
+
+
-| Field |
-Type |
-Description |
+shrinkStart |
+TimeInput |
+
+
+Set the window start to a specified time.
+
+ |
-
-
-| time |
-TimeInput! |
+shrinkEnd |
+TimeInput |
-Reference time for the operation.
+Set the window end to a specified time.
|
-| expr |
-EdgeFilter! |
+filter |
+FilterExpr |
-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.
-| expr |
-EdgeFilter! |
+openAI |
+OpenAIConfig |
-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.
-| defaultLayer |
-Boolean |
+const |
+Value |
-Contains only the default layer.
+A literal.
|
-| latest |
-Boolean |
+read |
+Read |
-Latest time.
+A field, degree, property or metadata read from an entity.
|
-| snapshotLatest |
-Boolean |
+temporal |
+Expr |
-Snapshot at latest time.
+The full history of a property instead of its latest value.
|
-| snapshotAt |
-TimeInput |
-
-
-Snapshot at specified time.
-
- |
+sum |
+Expr |
+ |
-| layers |
-[String!] |
+avg |
+Expr |
+ |
+
+
+| min |
+Expr |
+ |
+
+
+| max |
+Expr |
+ |
+
+
+| first |
+Expr |
+ |
+
+
+| last |
+Expr |
+ |
+
+
+| len |
+Expr |
+ |
+
+
+| any |
+Expr |
-List of included layers.
+The predicate holds if it holds for any element.
|
-| excludeLayers |
-[String!] |
+all |
+Expr |
-List of excluded layers.
-
- |
-
-
-| excludeLayer |
-String |
-
-
-Single excluded layer.
-
- |
-
-
-| window |
-Window |
-
-
-Window between a start and end time.
-
- |
-
-
-| at |
-TimeInput |
-
-
-View at a specified time.
-
- |
-
-
-| before |
-TimeInput |
-
-
-View before a specified time (end exclusive).
-
- |
-
-
-| after |
-TimeInput |
-
-
-View after a specified time (start exclusive).
-
- |
-
-
-| shrinkStart |
-TimeInput |
-
-
-Set the window start to a specified time.
-
- |
-
-
-| shrinkEnd |
-TimeInput |
-
-
-Set the window end to a specified time.
-
- |
-
-
-| edgeFilter |
-EdgeFilter |
-
-
-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`.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| start |
-TimeInput! |
-
-
-Window start time (inclusive).
-
- |
-
-
-| end |
-TimeInput! |
-
-
-Window end time (exclusive).
-
- |
-
-
-| expr |
-EdgeFilter! |
-
-
-Filter evaluated within the restricted window.
-
- |
-
-
-
-
-### EdgesViewCollection
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| defaultLayer |
-Boolean |
-
-
-Contains only the default layer.
-
- |
-
-
-| latest |
-Boolean |
-
-
-Latest time.
-
- |
-
-
-| snapshotLatest |
-Boolean |
-
-
-Snapshot at latest time.
-
- |
-
-
-| snapshotAt |
-TimeInput |
-
-
-Snapshot at specified time.
-
- |
-
-
-| layers |
-[String!] |
-
-
-List of included layers.
-
- |
-
-
-| excludeLayers |
-[String!] |
-
-
-List of excluded layers.
-
- |
-
-
-| excludeLayer |
-String |
-
-
-Single excluded layer.
-
- |
-
-
-| window |
-Window |
-
-
-Window between a start and end time.
-
- |
-
-
-| at |
-TimeInput |
-
-
-View at a specified time.
-
- |
-
-
-| before |
-TimeInput |
-
-
-View before a specified time (end exclusive).
-
- |
-
-
-| after |
-TimeInput |
-
-
-View after a specified time (start exclusive).
-
- |
-
-
-| shrinkStart |
-TimeInput |
-
-
-Set the window start to a specified time.
-
- |
-
-
-| shrinkEnd |
-TimeInput |
-
-
-Set the window end to a specified time.
-
- |
-
-
-| edgeFilter |
-EdgeFilter |
-
-
-Edge filter
-
- |
-
-
-
-
-### EmbeddingModel
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| openAI |
-OpenAIConfig |
-
-
-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`.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| src |
-NodeFilter |
-
-
-Applies a filter to the **source node** of the exploded edge.
-
- |
-
-
-| dst |
-NodeFilter |
-
-
-Applies a filter to the **destination node** of the exploded edge.
-
- |
-
-
-| property |
-PropertyFilterNew |
-
-
-Filters an exploded-edge **property** by name and value, evaluated
-per event.
-
-Example:
-`{ Property: { name: "weight", gt: 0.5 } }`
-
- |
-
-
-| metadata |
-PropertyFilterNew |
-
-
-Filters an exploded-edge **metadata field**.
-
-Metadata is shared across all temporal versions of an edge.
-
- |
-
-
-| temporalProperty |
-PropertyFilterNew |
-
-
-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.
-
- |
-
-
-| not |
-ExplodedEdgeFilter |
-
-
-Logical **NOT** over a nested exploded-edge filter.
-
- |
-
-
-| window |
-ExplodedEdgeWindowExpr |
-
-
-Restricts exploded-edge evaluation to a **time window**
-(inclusive start, exclusive end).
-
- |
-
-
-| at |
-ExplodedEdgeTimeExpr |
-
-
-Restricts exploded-edge evaluation to a **single point in time**.
-
- |
-
-
-| before |
-ExplodedEdgeTimeExpr |
-
-
-Restricts exploded-edge evaluation to times **strictly before** a
-given time.
-
- |
-
-
-| after |
-ExplodedEdgeTimeExpr |
-
-
-Restricts exploded-edge evaluation to times **strictly after** a
-given time.
-
- |
-
-
-| latest |
-ExplodedEdgeUnaryExpr |
-
-
-Evaluates exploded-edge predicates against the **latest available
-state**.
-
- |
-
-
-| snapshotAt |
-ExplodedEdgeTimeExpr |
-
-
-Evaluates exploded-edge predicates against a **snapshot** of the graph
-at a specific time.
-
- |
-
-
-| snapshotLatest |
-ExplodedEdgeUnaryExpr |
-
-
-Evaluates exploded-edge predicates against the **most recent
-snapshot** of the graph.
-
- |
-
-
-| layers |
-ExplodedEdgeLayersExpr |
-
-
-Restricts evaluation to exploded edges belonging to one or more
-**layers**.
-
- |
-
-
-| isActive |
-Boolean |
-
-
-Matches exploded edges that have at least one event in the current
-view/window.
-
- |
-
-
-| isValid |
-Boolean |
-
-
-Matches exploded edges that are structurally valid (i.e. not deleted)
-in the current view/window.
-
- |
-
-
-| isDeleted |
-Boolean |
-
-
-Matches exploded edges that have been deleted in the current
-view/window.
-
- |
-
-
-| isSelfLoop |
-Boolean |
-
-
-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`.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| names |
-[String!]! |
-
-
-Layer names to include.
-
- |
-
-
-| expr |
-ExplodedEdgeFilter! |
-
-
-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.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| time |
-TimeInput! |
-
-
-Reference time for the operation.
-
- |
-
-
-| expr |
-ExplodedEdgeFilter! |
-
-
-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.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| expr |
-ExplodedEdgeFilter! |
-
-
-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`.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| start |
-TimeInput! |
-
-
-Window start time (inclusive).
-
- |
-
-
-| end |
-TimeInput! |
-
-
-Window end time (exclusive).
-
- |
-
-
-| expr |
-ExplodedEdgeFilter! |
-
-
-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.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| value |
-String! |
-
-
-The string to match against.
-
- |
-
-
-| levenshteinDistance |
-Int! |
-
-
-Maximum Levenshtein edit distance for a match.
-
- |
-
-
-| prefixMatch |
-Boolean! |
-
-
-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.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| node |
-NodeFilter |
-
-
-Filter by node properties, fields, or temporal state.
-
- |
-
-
-| edge |
-EdgeFilter |
-
-
-Filter by edge properties, source/destination, or temporal state.
-(Persisted filters may use the legacy `edge` key.)
-
- |
-
-
-| explodedEdge |
-ExplodedEdgeFilter |
-
-
-Filter exploded edges — per-event edge instances — by properties,
-endpoints, or temporal state, evaluated per event.
-
- |
-
-
-| graph |
-GraphFilter |
-
-
-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.
-
- |
-
-
-| not |
-GqlFilter |
-
-
-Inverts the nested filter.
-
- |
-
-
-| window |
-GraphWindowExpr |
-
-
-Restrict evaluation to a time window (inclusive start, exclusive end).
-
- |
-
-
-| at |
-GraphTimeExpr |
-
-
-Restrict evaluation to a single point in time.
-
- |
-
-
-| before |
-GraphTimeExpr |
-
-
-Restrict evaluation to times strictly before the given time.
-
- |
-
-
-| after |
-GraphTimeExpr |
-
-
-Restrict evaluation to times strictly after the given time.
-
- |
-
-
-| latest |
-GraphUnaryExpr |
-
-
-Evaluate against the latest available state.
-
- |
-
-
-| snapshotAt |
-GraphTimeExpr |
-
-
-Evaluate against a snapshot of the graph at a given time.
-
- |
-
-
-| snapshotLatest |
-GraphUnaryExpr |
-
-
-Evaluate against the most recent snapshot of the graph.
-
- |
-
-
-| layers |
-GraphLayersExpr |
-
-
-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.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| window |
-GraphWindowExpr |
-
-
-Restrict evaluation to a time window (inclusive start, exclusive end).
-
- |
-
-
-| at |
-GraphTimeExpr |
-
-
-Restrict evaluation to a single point in time.
-
- |
-
-
-| before |
-GraphTimeExpr |
-
-
-Restrict evaluation to times strictly before the given time.
-
- |
-
-
-| after |
-GraphTimeExpr |
-
-
-Restrict evaluation to times strictly after the given time.
-
- |
-
-
-| latest |
-GraphUnaryExpr |
-
-
-Evaluate against the latest available state.
-
- |
-
-
-| snapshotAt |
-GraphTimeExpr |
-
-
-Evaluate against a snapshot of the graph at a given time.
-
- |
-
-
-| snapshotLatest |
-GraphUnaryExpr |
-
-
-Evaluate against the most recent snapshot of the graph.
-
- |
-
-
-| layers |
-GraphLayersExpr |
-
-
-Restrict evaluation to one or more layers.
-
- |
-
-
-
-
-### GraphLayersExpr
-
-Graph view restriction by layer membership, optionally chaining another `GraphFilter`.
-
-Used by `GqlGraphFilter::Layers`.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| names |
-[String!]! |
-
-
-Layer names to include.
-
- |
-
-
-| expr |
-GraphFilter |
-
-
-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"] } } } }`
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| time |
-TimeInput! |
-
-
-Reference time for the operation.
-
- |
-
-
-| expr |
-GraphFilter |
-
-
-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`.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| expr |
-GraphFilter |
-
-
-Optional nested filter applied after the unary operation.
-
- |
-
-
-
-
-### GraphViewCollection
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| defaultLayer |
-Boolean |
-
-
-Contains only the default layer.
-
- |
-
-
-| layers |
-[String!] |
-
-
-List of included layers.
-
- |
-
-
-| excludeLayers |
-[String!] |
-
-
-List of excluded layers.
-
- |
-
-
-| excludeLayer |
-String |
-
-
-Single excluded layer.
-
- |
-
-
-| subgraph |
-[NodeId!] |
-
-
-Subgraph nodes.
-
- |
-
-
-| subgraphNodeTypes |
-[String!] |
-
-
-Subgraph node types.
-
- |
-
-
-| excludeNodes |
-[NodeId!] |
-
-
-List of excluded nodes.
-
- |
-
-
-| valid |
-Boolean |
-
-
-Valid state.
-
- |
-
-
-| window |
-Window |
-
-
-Window between a start and end time.
-
- |
-
-
-| at |
-TimeInput |
-
-
-View at a specified time.
-
- |
-
-
-| latest |
-Boolean |
-
-
-View at the latest time.
-
- |
-
-
-| snapshotAt |
-TimeInput |
-
-
-Snapshot at specified time.
-
- |
-
-
-| snapshotLatest |
-Boolean |
-
-
-Snapshot at latest time.
-
- |
-
-
-| before |
-TimeInput |
-
-
-View before a specified time (end exclusive).
-
- |
-
-
-| after |
-TimeInput |
-
-
-View after a specified time (start exclusive).
-
- |
-
-
-| shrinkStart |
-TimeInput |
-
-
-Set the window start to a specified time.
-
- |
-
-
-| shrinkEnd |
-TimeInput |
-
-
-Set the window end to a specified time.
-
- |
-
-
-| nodeFilter |
-NodeFilter |
-
-
-Node filter.
-
- |
-
-
-| edgeFilter |
-EdgeFilter |
-
-
-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"] } } } }
-```
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| start |
-TimeInput! |
-
-
-Window start time (inclusive).
-
- |
-
-
-| end |
-TimeInput! |
-
-
-Window end time (exclusive).
-
- |
-
-
-| expr |
-GraphFilter |
-
-
-Optional nested filter applied after the window restriction.
-
- |
-
-
-
-
-### InputEdge
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| src |
-NodeId! |
-
-
-Source node id (string or non-negative integer).
-
- |
-
-
-| dst |
-NodeId! |
-
-
-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`.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| field |
-MetaGraphField |
-
-
-Built-in attribute to test.
-
- |
-
-
-| metadataKey |
-String |
-
-
-Metadata key to test.
-
- |
-
-
-| where |
-PropCondition! |
-
-
-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.
-
- |
-
-
-| matchesIfAbsent |
-Boolean |
-
-
-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`).
-
- |
-
-
-| caseSensitive |
-Boolean |
-
-
-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.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| condition |
-MetaGraphCondition |
-
-
-Condition on a built-in attribute or a metadata key.
-
- |
-
-
-| and |
-[MetaGraphFilter!] |
-
-
-Logical AND over nested filters.
-
- |
-
-
-| or |
-[MetaGraphFilter!] |
-
-
-Logical OR over nested filters.
-
- |
-
-
-| not |
-MetaGraphFilter |
-
-
-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.
-| field |
-MetaGraphField |
-
-
-Sort on a built-in attribute.
-
- |
+eq |
+Cmp |
+ |
-| metadataKey |
-String |
-
-
-Sort on the value of this metadata key.
-
- |
+ne |
+Cmp |
+ |
-| 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.
-
- |
+lt |
+Cmp |
+ |
-| reverse |
-Boolean |
+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 |
+ |
+
+
+| 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!] |
+ |
+
+
+| not |
+FilterExpr |
+ |
+
-### 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.
-| pathContains |
-String |
-
-
-Substring match against the namespace's path.
-
- |
+lhs |
+Expr! |
+ |
-| caseSensitive |
-Boolean |
-
-
-Match `pathContains` case-sensitively (defaults to false).
-
- |
+rhs |
+Expr! |
+ |
+
+
+| levenshteinDistance |
+Int! |
+ |
+
+
+| prefixMatch |
+Boolean! |
+ |
-### 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.
-| reverse |
-Boolean |
+value |
+String! |
-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.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-| graphs |
-MetaGraphFilter |
+levenshteinDistance |
+Int! |
-Applied to the graphs in the collection.
+Maximum Levenshtein edit distance for a match.
|
-| namespaces |
-NamespaceFilter |
+prefixMatch |
+Boolean! |
-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.
-| name |
-NodeId! |
+defaultLayer |
+Boolean |
-Node id (string or non-negative integer).
+Contains only the default layer.
|
-| nodeType |
-String |
+layers |
+[String!] |
-Node type.
+List of included layers.
|
-| metadata |
-[PropertyInput!] |
+excludeLayers |
+[String!] |
-Metadata.
+List of excluded layers.
|
-| updates |
-[TemporalPropertyInput!] |
+excludeLayer |
+String |
-Updates.
+Single excluded layer.
|
-| layer |
-String |
+subgraph |
+[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.)
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-| eq |
-Value |
+subgraphNodeTypes |
+[String!] |
-Equality.
+Subgraph node types.
|
-| ne |
-Value |
+excludeNodes |
+[NodeId!] |
-Inequality.
+List of excluded nodes.
|
-| gt |
-Value |
+valid |
+Boolean |
-Greater-than.
+Valid state.
|
-| ge |
-Value |
+window |
+Window |
-Greater-than-or-equal.
+Window between a start and end time.
|
-| lt |
-Value |
+at |
+TimeInput |
-Less-than.
+View at a specified time.
|
-| le |
-Value |
+latest |
+Boolean |
-Less-than-or-equal.
+View at the latest time.
|
-| startsWith |
-Value |
+snapshotAt |
+TimeInput |
-String prefix match.
+Snapshot at specified time.
|
-| endsWith |
-Value |
+snapshotLatest |
+Boolean |
-String suffix match.
+Snapshot at latest time.
|
-| contains |
-Value |
+before |
+TimeInput |
-Substring match.
+View before a specified time (end exclusive).
|
-| notContains |
-Value |
+after |
+TimeInput |
-Negated substring match.
+View after a specified time (start exclusive).
|
-| fuzzySearch |
-FuzzySearchExpr |
+shrinkStart |
+TimeInput |
-Fuzzy string match (Levenshtein distance, optional prefix matching).
+Set the window start to a specified time.
|
-| isIn |
-Value |
+shrinkEnd |
+TimeInput |
-Set membership.
+Set the window end to a specified time.
|
-| isNotIn |
-Value |
+filter |
+FilterExpr |
-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: ... } }`,
-| where |
-NodeFieldCondition! |
+src |
+NodeId! |
-Condition applied to the field.
+Source node id (string or non-negative integer).
+
+ |
+
+
+| dst |
+NodeId! |
+
-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:
+
+
+
+| Field |
+Type |
+Description |
+
+
+
+
+| expr |
+Expr! |
+ |
+
+
+| values |
+Value! |
+ |
+
+
+
-- 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
-| id |
-NodeFieldWhere |
+field |
+MetaGraphField |
-Filters the node id: `{ id: { where: ... } }`.
+Built-in attribute to test.
|
-| name |
-NodeFieldWhere |
+metadataKey |
+String |
-Filters the node name: `{ name: { where: ... } }`.
+Metadata key to test.
|
-| nodeType |
-NodeFieldWhere |
+where |
+PropCondition! |
-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.
|
-| property |
-PropertyFilterNew |
+matchesIfAbsent |
+Boolean |
-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`).
|
-| degree |
-DegreeFilterNew |
+caseSensitive |
+Boolean |
-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.
|
-
-| metadata |
-PropertyFilterNew |
-
+ |
+
+
+### 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.
- |
+
+
+
+| Field |
+Type |
+Description |
+
+
-| temporalProperty |
-PropertyFilterNew |
+condition |
+MetaGraphCondition |
-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.
|
-| not |
-NodeFilter |
+not |
+MetaGraphFilter |
-Logical NOT over a nested node filter.
+Logical NOT over a nested filter.
|
-
-| window |
-NodeWindowExpr |
-
+ |
+
-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.
+
+
+
+
+| Field |
+Type |
+Description |
+
+
-| at |
-NodeTimeExpr |
+field |
+MetaGraphField |
-Restricts evaluation to a single point in time.
+Sort on a built-in attribute.
|
-| before |
-NodeTimeExpr |
+metadataKey |
+String |
-Restricts evaluation to times strictly before the given time.
+Sort on the value of this metadata key.
|
-| after |
-NodeTimeExpr |
+valueOrder |
+[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.
|
-| latest |
-NodeUnaryExpr |
+reverse |
+Boolean |
-Evaluates predicates against the latest available node state.
+Reverse this key's direction (default ascending).
|
-
-| snapshotAt |
-NodeTimeExpr |
-
+ |
+
-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.
+
+
+
+
+| Field |
+Type |
+Description |
+
+
-| snapshotLatest |
-NodeUnaryExpr |
+pathContains |
+String |
-Evaluates predicates against the most recent snapshot of the graph.
+Substring match against the namespace's path.
|
-| layers |
-NodeLayersExpr |
+caseSensitive |
+Boolean |
-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.
+
+
+
+
+| Field |
+Type |
+Description |
+
+
+
-| isActive |
+reverse |
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!]! |
+graphs |
+MetaGraphFilter |
-Layer names to include.
+Applied to the graphs in the collection.
|
-| expr |
-NodeFilter! |
+namespaces |
+NamespaceFilter |
-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.
-| reverse |
-Boolean |
-
-
-Reverse order
-
- |
-
-
-| id |
-Boolean |
+name |
+NodeId! |
-Unique Id
+Node id (string or non-negative integer).
|
-| name |
-Boolean |
+nodeType |
+String |
-Node name
+Node type.
|
-| type |
-Boolean |
+metadata |
+[PropertyInput!] |
-Node type. Untyped nodes sort first (before any named type).
+Metadata.
|
-| time |
-SortByTime |
+updates |
+[TemporalPropertyInput!] |
-Time
+Updates.
|
-| property |
+layer |
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.
-| time |
-TimeInput! |
+reverse |
+Boolean |
-Reference time for the operation.
+Reverse order
|
-| expr |
-NodeFilter! |
+id |
+Boolean |
-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).
-
-
+
+
-| Field |
-Type |
-Description |
+time |
+SortByTime |
+
+
+Time
+
+ |
-
-
-| expr |
-NodeFilter! |
+property |
+String |
-Filter evaluated after applying the unary operation.
+Property
|
@@ -14741,58 +13346,11 @@ Set the window end to a specified time.
-| nodeFilter |
-NodeFilter |
-
-
-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`.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| start |
-TimeInput! |
-
-
-Window start time (inclusive).
-
- |
-
-
-| end |
-TimeInput! |
-
-
-Window end time (exclusive).
-
- |
-
-
-| expr |
-NodeFilter! |
+filter |
+FilterExpr |
-Filter evaluated within the restricted window.
+A filter tree; the entity it tests is written in the tree.
|
@@ -14928,11 +13486,11 @@ Set the window end to a specified time.
-| nodeFilter |
-NodeFilter |
+filter |
+FilterExpr |
-Node filter.
+A filter tree; the entity it tests is written in the tree.
|
@@ -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.
-### 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):
-| name |
+key |
String! |
-Property (or metadata) key.
+Key.
|
-| where |
-PropCondition! |
+value |
+Value! |
-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.
-| key |
-String! |
-
-
-Key.
-
- |
+entity |
+Entity! |
+ |
-| value |
-Value! |
-
+ | views |
+[ViewOp!] |
+ |
+
+
+| endpoint |
+Endpoint |
+ |
+
+
+| target |
+Target! |
+ |
+
+
+
-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.
+
+
+
+
+| Field |
+Type |
+Description |
+
+
+
+
+| entity |
+Entity! |
+ |
+
+
+| views |
+[ViewOp!] |
+ |
+
+
+| endpoint |
+Endpoint |
+ |
@@ -15543,6 +14121,58 @@ Infect this fraction of the nodes, chosen at random.
+### Target
+
+What a read selects on its entity.
+
+
+
+
+| Field |
+Type |
+Description |
+
+
+
+
+| field |
+NodeFieldName |
+
+
+A built-in node field.
+
+ |
+
+
+| degree |
+DegreeDirection |
+
+
+A node degree in a direction.
+
+ |
+
+
+| property |
+String |
+
+
+A property, by name.
+
+ |
+
+
+| metadata |
+String |
+
+
+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.
+
+
+
### 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.
+
+
+
+
+| Value |
+Description |
+
+
+
+
+| SRC |
+ |
+
+
+| DST |
+ |
+
+
+
+
+### Entity
+
+The kind of thing a filter tests: a node, an edge, or one edge update.
+
+
+
+
+| Value |
+Description |
+
+
+
+
+| NODE |
+ |
+
+
+| EDGE |
+ |
+
+
+| EXPLODED_EDGE |
+ |
+
+
+
+
### GraphType
@@ -16112,6 +14836,33 @@ Number of edges.
+### NodeFieldName
+
+A built-in node field.
+
+
+
+
+| Value |
+Description |
+
+
+
+
+| 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