Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima

* Added configurable CORS `allowedOrigins` setting to Gremlin Server; warns when wildcard origin is used alongside authentication.
* Fixed `ByteBuf` leak in `GraphBinaryMessageSerializerV4` when serialization throws an `IOException`.
* Changed `Tree` to no longer extend `HashMap`; it is now a final class with a tree-shaped API (`childAt`, `hasChild`, `contains`, `findSubtree`, `getOrCreateChild`, `getNodesAtDepth`, `getLeafNodes`, `nodeCount`) and is no longer a `Map`.
* Changed `count(local)` on a `Tree` to return the total node count (`Tree.nodeCount()`) instead of the root-entry count.
* Changed Tree class in Java to not extend from HashMap and offered a new tree-shaped API for navigation.
* Added typed numeric wrappers and `preciseNumbers` connection option to `gremlin-javascript` for explicit control over numeric type serialization and deserialization.
* Added `NextN(n)` to `Traversal` in `gremlin-go` for batched result iteration, providing API parity with `next(n)` in the Java, Python, and .NET GLVs, and updated the Go translators in `gremlin-core` and `gremlin-javascript` to emit `NextN(n)` for the batched form.
* Added Provider Defined Types (PDT) support — graph providers can define custom types via `@ProviderDefined` annotation that serialize/deserialize seamlessly across all GLVs without driver-side configuration. Replaces TP3 custom type mechanism.
Expand Down
17 changes: 13 additions & 4 deletions docs/src/reference/the-traversal.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1481,8 +1481,9 @@ g.V().hasLabel('person').outE('created').count().map {it.get() * 10}.path() <2>
<1> `count()`-step is a <<a-note-on-barrier-steps,reducing barrier step>> meaning that all of the previous traversers are folded into a new traverser.
<2> The path of the traverser emanating from `count()` starts at `count()`.

IMPORTANT: `count(local)` counts the current, local object (not the objects in the traversal stream). This works for
`Collection`- and `Map`-type objects. For any other object, a count of 1 is returned.
IMPORTANT: `count(local)` counts the current, local object (not the objects in the traversal stream). For a
`Collection` it returns the number of elements, for a `Map` the number of entries, for a `Path` the number of
objects in the path, and for a `Tree` the total number of nodes in the tree. For any other object, a count of 1 is returned.

*Additional References*

Expand Down Expand Up @@ -5156,14 +5157,17 @@ It is important to see how the paths of all the emanating traversers are united

image::tree-step2.png[width=500]

The resultant tree data structure can then be manipulated (see `Tree` JavaDoc).
The resultant tree data structure can then be navigated with its tree-shaped API such as `childAt()`, `findSubtree()`, `getNodesAtDepth()`, and `getLeafNodes()` (see `Tree` JavaDoc).

[gremlin-groovy,modern]
----
tree = g.V().out().out().tree().by('name').next()
tree.childAt('marko')
tree.childAt('marko').childAt('josh')
tree.getNodesAtDepth(2)
// The sugar plugin offer convenience accessors
tree['marko']
tree['marko']['josh']
tree.getObjectsAtDepth(3)
----

Note that when using `by()`-modulation, tree nodes are combined based on projection uniqueness, not on the
Expand All @@ -5179,6 +5183,11 @@ g.V().has('name','josh').out('created').values('name').
<1> When the `tree()` is created, vertex 3 and 5 are unique and thus, form unique branches in the tree structure.
<2> When the `tree()` is `by()`-modulated by `label`, then vertex 3 and 5 are both "software" and thus are merged to a single node in the tree.

NOTE: A `Tree` keys its children by node value, so sibling branches that resolve to the same value (whether the
original objects or a `by()`-projection) collapse into a single node. This means structure can be lost when distinct
paths share a value at the same depth. If every path must be preserved, use `path()`; if a queryable result that
retains element identity, cycles, and parallel edges is required, use `subgraph()`.

The `tree()` step can also take a side-effect key as an argument. When using this form, the `Tree` is is built up in a
side-effect as each traverser passes through. The `Tree` can later be accessed by either `select()` or `cap()`.

Expand Down
54 changes: 54 additions & 0 deletions docs/src/upgrade/release-4.x.x.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,41 @@ effort to the old custom serializer approach but is entirely optional for basic
* <<gremlin-go-pdt,Gremlin-Go>>


==== Tree No Longer Extends HashMap

`Tree` no longer extends `HashMap`. It is now a `final` class that holds a `Map` internally and exposes a
tree-shaped API instead of `Map` methods. This is a breaking change for code that treated a `tree()` result as a
`Map`.

Replacements for the common `Map`-based access patterns:

[options="header"]
|=======================
|3.x (`Tree` as `Map`) |4.x (`Tree` API)
|`tree.get(key)` |`tree.childAt(key)` (throws if absent) or `tree.findSubtree(key)` (recursive, returns `Optional`)
|`tree.containsKey(key)` |`tree.hasChild(key)`
|`tree.keySet()` |`tree.rootNodes()`
|`tree.size()` |`tree.rootNodes().size()` for root entries, or `tree.nodeCount()` for total nodes
|`getObjectsAtDepth(d)` |`getNodesAtDepth(d)` (now 0-based: depth 0 returns the roots)
Comment thread
GumpacG marked this conversation as resolved.
|`getLeafObjects()` |`getLeafNodes()`
|=======================

A few Gremlin patterns that worked only because `Tree` was a `Map` (for example `select(keys)` and `unfold()`
applied to a `Tree`) no longer compose. Process the `Tree` result client-side after `next()`, or reshape the
upstream traversal.

`count(local)` on a `Tree` has changed semantics. Previously, because `Tree` was a `Map`, it returned the
number of root entries; it now returns the total number of nodes in the tree (`Tree.nodeCount()`), consistent
with how `count(local)` counts the contents of other local objects (for example the objects in a `Path`). Use
`rootNodes().size()` on the materialized `Tree` if the root-entry count is required.

`isLeaf()` no longer throws on an empty tree (it returns `true`), and a `Tree` keeps the long-standing limitation
that sibling branches resolving to the same value collapse into one node; use `path()` or `subgraph()` when full
path structure must be preserved.

See: link:https://lists.apache.org/thread/o0nqh6kmrkdht531655p351ldjll045d[[DISCUSS] Make Tree no longer extend HashMap]


Comment thread
GumpacG marked this conversation as resolved.
=== Upgrading for Providers

==== Graph System Providers
Expand All @@ -654,6 +689,25 @@ benefiting all driver users transparently.
See <<provider-defined-types>> for full details on annotation usage, field filtering, nested types, and ServiceLoader
registration.

`Tree` no longer extends `HashMap`. Provider code that inspected or rebuilt a `Tree` via `Map` methods
(`get`, `put`, `keySet`, `entrySet`, `size`) must use the tree-shaped API instead: read with `rootNodes()` +
`childAt(key)` and build with `getOrCreateChild(key)` + `addTree(subtree)`. The GraphSON (`g:Tree`) and
GraphBinary (`0x2b`) wire formats are unchanged, so no GraphSON- or GraphBinary-serialized data or cross-version
===== Tree Serialization and API Changes

`Tree` no longer extends `HashMap`.

Providers that use the reference (de)serializers receive a `Tree` instance from TinkerPop. Code that inspected or
rebuilt that `Tree` via `Map` methods (`get`, `put`, `keySet`, `entrySet`, `size`) must use the tree-shaped API
instead: read with `rootNodes()` + `childAt(key)` and build with `getOrCreateChild(key)` + `addTree(subtree)`.

The GraphSON (`g:Tree`) and GraphBinary (`0x2b`) wire formats are unchanged, so no GraphSON- or GraphBinary-serialized data or cross-version
compatibility is affected. The Gryo `Tree` wire format, however, did change: in 3.x a `Tree` was serialized via
Kryo's default `Map` serializer because it extended `HashMap`, whereas in 4.x it has an explicit `Tree`
serializer. As a result, Gryo-serialized `Tree` data written by 3.x cannot be read by 4.x and vice versa. The
Gryo type registration id (`61`) is unchanged and 4.x-to-4.x Gryo round-trips correctly; this Gryo break is
expected for the 4.0.0 major release.

==== Graph Driver Providers

===== Request Interceptors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.tinkerpop.gremlin.process.traversal.Path;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;

Expand All @@ -42,7 +43,8 @@ public CountLocalStep(final Traversal.Admin traversal) {
@Override
protected Long map(final Traverser.Admin<S> traverser) {
final S item = traverser.get();
return (item instanceof Collection) ? ((Collection) item).size()
return (item instanceof Tree) ? ((Tree) item).nodeCount()
: (item instanceof Collection) ? ((Collection) item).size()
: (item instanceof Map) ? ((Map) item).size()
: (item instanceof Path) ? ((Path) item).size()
: IteratorUtils.count(IteratorUtils.asIterator(item));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,7 @@ public Tree projectTraverser(final Traverser.Admin<S> traverser) {
final TraversalProduct product = TraversalUtil.produce(path.<Object>get(i), this.traversalRing.next());
if (product.isProductive()) {
final Object object = product.get();
if (!depth.containsKey(object))
depth.put(object, new Tree<>());
depth = (Tree) depth.get(object);
depth = depth.getOrCreateChild(object);
}
}
this.traversalRing.reset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ protected void sideEffect(final Traverser.Admin<S> traverser) {
final Path path = traverser.path();
for (int i = 0; i < path.size(); i++) {
final Object object = TraversalUtil.applyNullable(path.<Object>get(i), this.traversalRing.next());
if (!depth.containsKey(object))
depth.put(object, new Tree<>());
depth = (Tree) depth.get(object);
depth = depth.getOrCreateChild(object);
}
this.traversalRing.reset();
this.getTraversal().getSideEffects().add(this.sideEffectKey, root);
Expand Down
Loading
Loading