Set Side Effects
withSideEffect("a", Set.of(..)) declares a side effect as a set: aggregate("a") and store("a")
keep each value once, and cap("a")/select("a") return it as a list without duplicates. It
differs from Apache TinkerPop 3.8.2 in these details.
A set is a property of the side effect, not a value type
- TinkerPop: a set is a first-class value (
{"alice"}in Groovy,toSet(),GType.SET). - Graphersal: there is no set value. The set belongs to the side-effect bucket.
cap("a")returns a plain array (insertion order, no duplicates);toSet(),GType.SETand set literals anywhere else are not implemented. - DSL spelling:
Set.of("a", "b")(0 to 10 arguments),Set.ofList(["a", "b"])and theSet::of(..)/Set::of_list([..])forms. The Groovy literal{..}does not exist in Rhai. In Rust:GraphTraversalSource::with_side_effect_set. Set.of(..)accepts only plain values (strings, numbers, booleans, UUIDs, null); a vertex, a traversal or a map is a script error.
Order
- TinkerPop: the iteration order of a set is the one of its implementation (
HashSet: unspecified). - Graphersal: first-seen order, seed values first.
Equality of members
- Members are compared by value.
1and1.0are different members. - A property value collected from
values()is compared by its value, not by the element it came from, so two vertices named "x" contribute one member. A property element collected fromproperties()is compared by its element and key (TinkerPop's property id), so those two vertices contribute two members (an edge property is also kept by identity, unlike TinkerPop; see Property Elements). Elements (vertices, edges) keep their identity. - Bulk is invisible: a traverser of bulk
ninserts its value once, with merging on or off.
g.withSideEffect("a", Set.of()).V().both().values("name").aggregate("a").cap("a").next()
// ["josh", "lop", "vadas", "marko", "peter", "ripple"]
g.withSideEffect("a", []).V().both().values("name").aggregate("a").cap("a").next()
// a list seed keeps duplicates
Empty side effects
aggregate("a"), store("a") and subgraph("a") declare their key when they run, even if they
collect nothing (an empty upstream, or a by() that resolves no value). cap("a") then returns
the empty collection, as in TinkerPop, and where(P.within("a")) is false and
where(P.without("a")) true for it. Only a key that no step declares (a typo) is an error.
cap("sg")of asubgraph("sg")that collected nothing is an empty graph (see Subgraphs).
Subgraphs
subgraph("sg") collects the edges that pass through it, and cap("sg") returns them as a graph
value, as in TinkerPop:
let sub = g.e(0).subgraph("sg").cap("sg").next();
sub.v().to_json() // the two endpoint vertices of edge 0, with all their properties
sub.e().count().next() // 1
The graph value is a traversal source: sub.v(), sub.e(), sub.addV(..) work as on g.
What the graph holds:
- the selected edges and both endpoint vertices of each (a vertex shared by several edges is copied once; a vertex without a selected edge is not included),
- ids, the whole label set of every vertex (not only the first label) and all properties at full depth (nested objects and arrays),
- the schema the source graph stores (mode and declarations, copied; clear it with
set_schemaon the copy if you do not want it). A source with no stored schema gives a copy that keeps inferring from its own data.
subgraph() needs the label: subgraph() alone is an error that names subgraph("sg") + cap("sg") and the
short to_graph() terminal. The stream must be made of edges: a vertex (or any other value) reaching
subgraph("sg") is an error whose help points at outE()/inE()/bothE()
(g.v(1).bothE().subgraph("sg").cap("sg")). The kind is checked when each traverser arrives, so this
also holds inside a nested traversal (local(__.out().subgraph("sg")) fails, local(__.outE().subgraph("sg"))
works); an empty stream is fine and gives the empty graph.
It is a snapshot: a copy, never a view. Changing the subgraph does not touch the source, and
later changes of the source do not reach the subgraph. Ids are preserved (so the same vertex has
the same id in both graphs) and stay unique. Bulk is invisible: an edge that reaches subgraph()
once or a thousand times is collected once. While the stream runs only edge handles are
collected; the copy is built when cap() runs.
to_graph() is the short form (a Graphersal extension, TinkerPop has no such step): a terminal
that builds the same graph from a stream of edges, without a label.
g.e(0).to_graph() // one edge and its two vertices
g.v(1).bothE().to_graph() // all edges of vertex 1, with their vertices
g.v(1).to_graph() // error: to_graph() needs edges
A stream of anything else than edges (vertices, values) is an error that names the edge forms; a
stream with no results is an empty graph. In the Rust API to_graph() returns the shared graph
handle (Arc<Graph>), and cap("sg").next() lists the graph as
{"vertices": [..], "edges": [..]}. A script that ends on a graph value (g.e(0).to_graph(),
cap("sg").next()) shows that same listing in graphersal and the playground, cut to the display
limits (see Graph values); in the script the value stays a
graph you traverse.
Writing into your own graph
By default cap("sg") builds a new in-memory TraversalGraph. To write into a graph you own,
declare it as the side effect, as in TinkerPop:
let target = g.e(0).to_graph(); // or GraphSource::empty()
g.withSideEffect("sg", target).v(1).outE("created").subgraph("sg").toList();
target.e().count().next() // 2: the edge copied before, plus one
The step writes through the graph mutation API when it has run, and cap("sg") returns the target
itself. A vertex id the target already holds is reused and an edge id it already holds is skipped,
so writing the same edges twice changes nothing. The target's own stored schema is kept (the
source schema is copied only into a target that stores none). The target must not be the graph
being traversed (g itself): that fails with an error instead of deadlocking.
The target must also be a TraversalGraph. Every graph a script builds is one (GraphSource::*,
to_graph(), cap("sg"), _g), but a host may serve its own g from another GraphStorage.
withSideEffect("sg", g) with such a graph fails when the step is built, with
GraphError::Unsupported naming the target's storage type. To get a subgraph of a foreign graph,
let it land in a graph of the script (g.E().hasLabel("knows").to_graph()) and copy what you
need from there.
- Deviations from TinkerPop: the default graph is a Graphersal
TraversalGraph, not a TinkerGraph, and an explicit target must be a graph value of this kind.to_graph()and the schema copy are extensions. In the Rust API a graph value is shown bynext()/to_list()as its{"vertices", "edges"}listing; useto_graph()for the graph itself. See TinkerPop deviations.
Reduced values
withSideEffect("a", 0, Operator.sum) declares a side effect that aggregate("a") and store("a") combine with
an Operator instead of collecting a list: g.withSideEffect("a", 1, Operator.sum).V().aggregate("a").by("age").cap("a")
is 124. See Sack and Operators for the rule and the deviations.