You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by ke...@apache.org on 2025/10/29 23:18:18 UTC
(tinkerpop) branch 3.8-documentation created (now b0d9aa60e1)
This is an automated email from the ASF dual-hosted git repository.
kenhuuu pushed a change to branch 3.8-documentation
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
at b0d9aa60e1 Add subgroupings for each section
This branch includes the following new commits:
new c28a1d0650 Re-organize upgrade documentation based on severity
new b0d9aa60e1 Add subgroupings for each section
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(tinkerpop) 02/02: Add subgroupings for each section
Posted by ke...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
kenhuuu pushed a commit to branch 3.8-documentation
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit b0d9aa60e1f0eab95f874274f55f88086799734e
Author: Ken Hu <10...@users.noreply.github.com>
AuthorDate: Wed Oct 29 16:07:37 2025 -0700
Add subgroupings for each section
---
docs/src/upgrade/release-3.8.x.asciidoc | 1228 +++++++++++++++----------------
1 file changed, 612 insertions(+), 616 deletions(-)
diff --git a/docs/src/upgrade/release-3.8.x.asciidoc b/docs/src/upgrade/release-3.8.x.asciidoc
index 58c053a741..0536372ffe 100644
--- a/docs/src/upgrade/release-3.8.x.asciidoc
+++ b/docs/src/upgrade/release-3.8.x.asciidoc
@@ -30,76 +30,11 @@ complete list of all the modifications that are part of this release.
=== Upgrading for Users
-==== Breaking Language Changes
+==== Breaking Changes
-===== none() and discard()
+===== Removed and Renamed Steps
-There is a complicated relationship with the `none()` and `discard()` steps that begs some discussion. Prior to this
-version, the `none()` step was used to "throw away" all traversers that passed into it. In 3.8.0, that step has been
-renamed to `discard()`. The `discard()` step with its verb tone arguably makes for a better name for that feature, but
-it also helped make room for `none()` to be repurposed as `none(P)` which is a complement to `any(P)` and `all(P) steps.
-
-===== Prevented using cap(), inject() inside repeat()
-
-`cap()` inside `repeat()` is now disallowed by the `StandardVerificationStrategy`. Using `cap()` inside `repeat()` would
-have led to unexpected results since `cap()` isn't "repeat-aware". Because `cap()` is a `SupplyingBarrier` that reduces
-the number of traversers to one, its use inside `repeat()` is limited.
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3195[TINKERPOP-3195]
-
-`inject()` inside `repeat()` is now also disallowed by the `StandardVerificationStrategy`. The usefulness of `inject()`
-inside `repeat()` is questionable as the injections are exhausted after one iteration. Consider the following examples,
-noting that the examples for version 3.7.4 demonstrate the effect of `RepeatUnrollStrategy` on `inject()` semantics,
-which is problematic as strategies should not affect results. 3.8.0 examples do not disable the `RepeatUnrollStrategy`
-as the strategy was modified to be more restrictive in this version.
-
-[source,text]
-----
-// 3.7.4 results in data injected for each repeat loop
-gremlin> g.inject('x').repeat(inject('a')).times(5)
-==>a
-==>a
-==>a
-==>a
-==>a
-==>x
-
-// 3.7.4 without RepeatUnrollStrategy injections occur only once
-gremlin> g.withoutStrategies(RepeatUnrollStrategy).inject('x').repeat(inject('a')).times(5)
-==>a
-==>x
-
-// 3.8.0 inject() inside repeat() now produces an error
-gremlin> g.inject('x').repeat(inject('a')).times(5)
-The parent of inject()-step can not be repeat()-step: InjectStep(java.util.ArrayList$Itr@543da15)
-----
-
-Before upgrading, users should look for usages of `inject()` inside `repeat()` and if it is determined that per-loop
-injections are desired, it is possible to use `union()` and `constant()` instead.
-
-[source,text]
-----
-// 3.8.0 can use union() and constant() inside repeat() instead of inject()
-gremlin> g.inject('x').repeat(union(constant('a').limit(1),identity())).times(5)
-==>a
-==>a
-==>a
-==>a
-==>a
-==>x
-
-// can also use union() and constant() inside repeat() with multiple values
-gremlin> g.inject('x').repeat(union(constant(['a','b']).limit(1).unfold(),identity())).times(3)
-==>a
-==>b
-==>a
-==>a
-==>b
-==>b
-==>x
-----
-
-===== `aggregate()` with `Scope` Removed
+====== Removal of `aggregate()` with `Scope` and `store()`
The meaning of `Scope` parameters in `aggregate()` have always been unique compared to all other "scopable" steps.
`aggregate(global)` is a `Barrier`, which blocks the traversal until all traversers have been aggregated into the side
@@ -113,8 +48,11 @@ control vs. per-element application. This change aligns all side effect steps (n
and reserves the `Scope` parameter exclusively for "traverser-local" application patterns, eliminating confusion about
its contextual meanings.
-This makes the `AggregateStep` globally scoped by default with eager aggregation. The Lazy evaluation with `aggregate()` is
-achieved by wrapping the step in `local()`.
+This makes the `AggregateStep` globally scoped by default with eager evaluation. Lazy evaluation with `aggregate()` is
+achieved by wrapping the step in `local()`.
+
+Similarly, `store()` is an eqivalent step to `aggregate(local)` and has been deprecated since 3.4.3. It is also removed
+along with `aggregate(local)`.
[source,text]
----
@@ -173,23 +111,7 @@ gremlin> g.V().out().barrier().local(aggregate("x")).select("x")
See: link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-scoping-5.asciidoc[Lazy vs. Eager Evaluation]
-===== Removal of `store()` Step
-
-The `store()` step was a legacy name for `aggregate(local)` that has been deprecated since 3.4.3, and is now removed along
-with `aggregate(local)`. To achieve lazy aggregation, use `aggregate()` within `local()`.
-
-[source,text]
-----
-// 3.7.x - store() is still allowed
-gremlin> g.V().store("x").by("age").cap("x")
-==>[29,27,32,35]
-
-// 3.8.0 - store() removed, use local(aggregate()) to achieve lazy aggregation
-gremlin> g.V().local(aggregate("x").by("age")).cap("x")
-==>[29,27,32,35]
-----
-
-===== Removal of has(key, traversal)
+====== Removal of has(key, traversal)
The has(key, traversal) API has been removed in version 3.8.0 due to its confusing behavior that differed from other
has() variants. As well, most has(key, traversal) usage indicates a misunderstanding of the API. Unlike has(key, value)
@@ -221,186 +143,16 @@ gremlin> g.V().has("age", P.gt(30))
See: link:https://issues.apache.org/jira/browse/TINKERPOP-1463[TINKERPOP-1463]
-===== Removal of P.getOriginalValue()
-
-`P.getOriginalValue()` has been removed as it was not offering much value and was often confused with `P.getValue()`.
-Usage of `P.getOriginalValue()` often leads to unexpected results if called on a predicate which has had its value reset
-after construction. All usages of `P.getOriginalValue()` should be replaced with `P.getValue()`.
-
-===== SeedStrategy Construction
-
-The `SeedStrategy` public constructor has been removed for Java and has been replaced by the builder pattern common
-to all strategies. This change was made to ensure that the `SeedStrategy` could be constructed consistently.
-
-===== OptionsStrategy in Python
-
-The `\\__init__()` syntax has been updated to be both more Pythonic and more aligned to the `gremlin-lang` syntax.
-Previously, `OptionsStrategy()` took a single argument `options` which was a `dict` of all options to be set.
-Now, all options should be set directly as keyword arguments.
-
-For example:
-
-[source,python]
-----
-# 3.7 and before:
-g.with_strategies(OptionsStrategy(options={'key1': 'value1', 'key2': True}))
-# 4.x and newer:
-g.with_strategies(OptionsStrategy(key1='value1', key2=True))
-
-myOptions = {'key1': 'value1', 'key2': True}
-# 3.7 and before:
-g.with_strategies(OptionsStrategy(options=myOptions))
-# 4.x and newer:
-g.with_strategies(OptionsStrategy(**myOptions))
-----
-
-===== Remove Undocumented `with()` modulation
-
-There has long been a connection between the `with()` modulator, and mutating steps due to the design of
-some of the interfaces in the gremlin traversal engine. This has led to several undocumented usages of the
-`with()` modulator which have never been officially supported but have previously been functional.
-
-As of 3.8.0 `with()` modulation of the following steps will no longer work: `addV()`, `addE()`, `property()`, `drop()`,
-`mergeV()`, and `mergeE()`.
-
-
-==== Breaking Behavioral Changes
-
-===== Serialization Changes
-
-*Properties on Element Serialization in Python & Javascript*
-
-Element properties handling has been inconsistent across GLVs. Previously,`gremlin-python` deserialized empty properties
-as None or array depending on the serializer, while `gremlin-javascript` returned properties as objects or arrays, with
-empty properties as empty lists or undefined depending on the serializer.
-
-This inconsistency is now resolved, aligning to how properties are handled in Gremlin core and in the Java GLV.
-Both GLVs will deserialize element properties into lists of property objects, returning empty lists instead of null values
-for missing properties.
-
-For python, the most notable difference is in graphSON when "tokens" is turned on for "materializeProperties". The
-properties returned are no longer `None`, but empty lists. Users should update their code accordingly.
-
-For javascript, the change is slightly more extensive, as user should no longer expect javascript objects to be returned.
-All properties are returned as lists of Property or VertexProperty objects.
-
-[source,javascript]
-----
-// 3.7 and before:
-g.with_("materializeProperties", "tokens").V(1).next() // skip properties with token
-// graphson will return properties as a javascript object, which becomes undefined
-Vertex { id: 1, label: 'person', properties: undefined }
-// graphbinary will return properties as empty lists
-Vertex { id: 1, label: 'person', properties: [] }
-
-g.V(1).next() // properties returned
-// graphson will return properties as a javascript object
-Vertex {
- id: 1,
- label: 'person',
- properties: { name: [Array], age: [Array] }
-}
-// graphbinary will return properties as lists of VertexProperty objects
-Vertex {
- id: 1,
- label: 'person',
- properties: [ [VertexProperty], [VertexProperty] ]
-}
-
-// 3.8.0 and newer - properties are always arrays, empty array [] for missing properties:
-g.with_("materializeProperties", "tokens").V(1).next() // skip properties with token
-// both graphson and graphbinary return
-Vertex { id: 1, label: 'person', properties: [] }
-g.V(1).next()
-// both graphson and graphbinary return
-Vertex {
- id: 1,
- label: 'person',
- properties: [ [VertexProperty], [VertexProperty] ]
-}
-
-----
-
-This change only affects how GLVs deserialize property data in client applications. The underlying graph serialization
-formats and server-side behavior remain unchanged.
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3186[TINKERPOP-3186]
-
-*Javascript Set Deserialization*
-
-Starting from this version, `gremlin-javascript` will deserialize `Set` data into a ECMAScript 2015 Set. Previously,
-these were deserialized into arrays.
-
-*.NET Byte Serialization Change*
-
-The Gremlin .NET serializers has been updated to correctly handle byte values as signed integers to align with the IO
-specification, whereas previously it incorrectly serialized and deserialized bytes as unsigned values.
-
-This is a breaking change for .NET applications that rely on byte values. Existing applications using byte values
-should consider switching to `sbyte` for signed byte operations or `short` for a wider range of values to maintain
-compatibility.
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3161[TINKERPOP-3161]
-
-===== Simplified Comparability Semantics
-
-The previous system of ternary boolean semantics has been replaced with simplified binary semantics. The triggers for
-"ERROR" states from illegal comparisons are unchanged (typically comparisons with NaN or between incomparable types
-such as String and int). The difference now is that instead of the ERROR being propagated according to ternary logic
-semantics until a reduction point is reached, the error now immediately returns a value of FALSE.
-
-This will be most visible in expressions which include negations. Prior to this change, `g.inject(NaN).not(is(1))` would
-produce no results as `!(NaN == 1)` -> `!(ERROR)` -> `ERROR` -> traverser is filtered out. After this change, the same
-traversal will return NaN as the same expression now evaluates as `!(NaN == 1)` -> `!(FALSE)` -> `TRUE` -> traverser is
-not filtered.
-
-See: link:https://tinkerpop.apache.org/docs/3.8.0/dev/provider/#gremlin-semantics-equality-comparability[Comparability semantics docs]
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3173[TINKERPOP-3173]
-
-===== Auto-promotion of Numbers
-
-Previously, operations like `sum` or `sack` that involved mathematical calculations did not automatically promote the
-result to a larger numeric type (e.g., from int to long) when needed. As a result, values could wrap around within their
-current type leading to unexpected behavior. This issue has now been resolved by enabling automatic type promotion for
-results.
-
-Now, any mathematical operations such as `Add`, `Sub`, `Mul`, and `Div` will now automatically promote to the next
-numeric type if an overflow is detected. For integers, the promotion sequence is: byte → short → int → long → overflow
-exception. For floating-point numbers, the sequence is: float → double → infinity.
-
-The following example showcases the change in overflow behavior between 3.7.3 and 3.8.0
-
-[source,text]
-----
-// 3.7.3
-gremlin> g.inject([Byte.MAX_VALUE, (byte) 1], [Short.MAX_VALUE, (short) 1], [Integer.MAX_VALUE,1], [Long.MAX_VALUE, 1l]).sum(local)
-==>-128 // byte
-==>-32768 // short
-==>-2147483648 // int
-==>-9223372036854775808 // long
-
-gremlin> g.inject([Float.MAX_VALUE, Float.MAX_VALUE], [Double.MAX_VALUE, Double.MAX_VALUE]).sum(local)
-==>Infinity // float
-==>Infinity // double
-
-// 3.8.0
-gremlin> g.inject([Byte.MAX_VALUE, (byte) 1], [Short.MAX_VALUE, (short) 1], [Integer.MAX_VALUE,1]).sum(local)
-==>128 // short
-==>32768 // int
-==>2147483648 // long
-
-gremlin> g.inject([Long.MAX_VALUE, 1l]).sum(local)
-// throws java.lang.ArithmeticException: long overflow
+====== none() and discard()
-gremlin> g.inject([Float.MAX_VALUE, Float.MAX_VALUE], [Double.MAX_VALUE, Double.MAX_VALUE]).sum(local)
-==>6.805646932770577E38 // double
-==>Infinity // double
-----
+There is a complicated relationship with the `none()` and `discard()` steps that begs some discussion. Prior to this
+version, the `none()` step was used to "throw away" all traversers that passed into it. In 3.8.0, that step has been
+renamed to `discard()`. The `discard()` step with its verb tone arguably makes for a better name for that feature, but
+it also helped make room for `none()` to be repurposed as `none(P)` which is a complement to `any(P)` and `all(P) steps.
-See link:https://issues.apache.org/jira/browse/TINKERPOP-3115[TINKERPOP-3115]
+===== Changes to `repeat()`
-===== repeat() Step Global Children Semantics Change
+====== repeat() Step Global Children Semantics Change
The `repeat()` step has been updated to treat the repeat traversal as a global child in all cases. Previously, the
repeat traversal behaved as a hybrid between local and global semantics, which could lead to unexpected results in
@@ -498,12 +250,167 @@ gremlin> g.V().local(repeat(both().simplePath().order().by("name")).times(2)).pa
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3200[TINKERPOP-3200]
-===== Prefer OffsetDateTime
-
-The default implementation for date type in Gremlin is now changed from the `java.util.Date` to the more encompassing
-`java.time.OffsetDateTime`. This means the reference implementation for all date manipulation steps, `asDate()`,
-`dateAdd()`, and `dateDiff()`, as well as helper methods `datetime()`, will return `OffsetDateTime`, whose string
-representation will be in ISO 8601 format.
+====== Modified limit() skip() range() Semantics in repeat()
+
+The semantics of `limit()`, `skip()`, and `range()` steps called with default `Scope` or explicit `Scope.global` inside
+`repeat()` have been modified to ensure consistent semantics across repeat iterations. Previously, these steps would
+track global state across iterations, leading to unexpected filtering behavior between loops.
+
+Consider the following examples which demonstrate the unexpected behavior. Note that the examples for version 3.7.4
+disable the `RepeatUnrollStrategy` so that strategy optimization does not replace the `repeat()` traversal with a
+non-looping equivalent. 3.8.0 examples do not disable the `RepeatUnrollStrategy` as the strategy was modified to be more
+restrictive in this version.
+
+[source,groovy]
+----
+// 3.7.4 - grateful dead graph examples producing no results due to global counters
+gremlin> g.withoutStrategies(RepeatUnrollStrategy).V().has('name','JAM').repeat(out('followedBy').limit(2)).times(2).values('name')
+gremlin>
+gremlin> g.withoutStrategies(RepeatUnrollStrategy).V().has('name','DRUMS').repeat(__.in('followedBy').range(1,3)).times(2).values('name')
+gremlin>
+// 3.7.4 - modern graph examples demonstrating too many results with skip in repeat due to global counters
+gremlin> g.withoutStrategies(RepeatUnrollStrategy).V(1).repeat(out().skip(1)).times(2).values('name')
+==>ripple
+==>lop
+gremlin> g.withoutStrategies(RepeatUnrollStrategy).V(1).out().skip(1).out().skip(1).values('name')
+==>lop
+
+// 3.8.0 - grateful dead graph examples producing results as limit counters tracked per iteration
+gremlin> g.V().has('name','JAM').repeat(out('followedBy').limit(2)).times(2).values('name')
+==>HURTS ME TOO
+==>BLACK THROATED WIND
+gremlin> g.V().has('name','DRUMS').repeat(__.in('followedBy').range(1,3)).times(2).values('name')
+==>DEAL
+==>WOMEN ARE SMARTER
+// 3.8.0 - modern graph examples demonstrating consistent skip semantics
+gremlin> g.V(1).repeat(out().skip(1)).times(2).values('name')
+==>lop
+gremlin> g.V(1).out().skip(1).out().skip(1).values('name')
+==>lop
+----
+
+This change ensures that `limit()`, `skip()`, and `range()` steps called with default `Scope` or explicit `Scope.global`
+inside `repeat()` are more consistent with manually unrolled traversals. Before upgrading, users should determine if any
+traversals use `limit()`, skip()`, or `range()` with default `Scope` or explicit `Scope.global` inside `repeat()`. If it
+is desired that the limit or range should apply across all loops then the `limit()`, `skip()`, or `range()` step should
+be moved out of the `repeat()` step.
+
+====== Prevented using cap(), inject() inside repeat()
+
+`cap()` inside `repeat()` is now disallowed by the `StandardVerificationStrategy`. Using `cap()` inside `repeat()` would
+have led to unexpected results since `cap()` isn't "repeat-aware". Because `cap()` is a `SupplyingBarrier` that reduces
+the number of traversers to one, its use inside `repeat()` is limited.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3195[TINKERPOP-3195]
+
+`inject()` inside `repeat()` is now also disallowed by the `StandardVerificationStrategy`. The usefulness of `inject()`
+inside `repeat()` is questionable as the injections are exhausted after one iteration. Consider the following examples,
+noting that the examples for version 3.7.4 demonstrate the effect of `RepeatUnrollStrategy` on `inject()` semantics,
+which is problematic as strategies should not affect results. 3.8.0 examples do not disable the `RepeatUnrollStrategy`
+as the strategy was modified to be more restrictive in this version.
+
+[source,text]
+----
+// 3.7.4 results in data injected for each repeat loop
+gremlin> g.inject('x').repeat(inject('a')).times(5)
+==>a
+==>a
+==>a
+==>a
+==>a
+==>x
+
+// 3.7.4 without RepeatUnrollStrategy injections occur only once
+gremlin> g.withoutStrategies(RepeatUnrollStrategy).inject('x').repeat(inject('a')).times(5)
+==>a
+==>x
+
+// 3.8.0 inject() inside repeat() now produces an error
+gremlin> g.inject('x').repeat(inject('a')).times(5)
+The parent of inject()-step can not be repeat()-step: InjectStep(java.util.ArrayList$Itr@543da15)
+----
+
+Before upgrading, users should look for usages of `inject()` inside `repeat()` and if it is determined that per-loop
+injections are desired, it is possible to use `union()` and `constant()` instead.
+
+[source,text]
+----
+// 3.8.0 can use union() and constant() inside repeat() instead of inject()
+gremlin> g.inject('x').repeat(union(constant('a').limit(1),identity())).times(5)
+==>a
+==>a
+==>a
+==>a
+==>a
+==>x
+
+// can also use union() and constant() inside repeat() with multiple values
+gremlin> g.inject('x').repeat(union(constant(['a','b']).limit(1).unfold(),identity())).times(3)
+==>a
+==>b
+==>a
+==>a
+==>b
+==>b
+==>x
+----
+
+====== Stricter RepeatUnrollStrategy
+
+The `RepeatUnrollStrategy` has been updated to use a more conservative approach for determining which repeat traversals
+are safe to unroll. Previously, the strategy would attempt to unroll most usages of `repeat()` used with `times()`
+without `emit()`. This caused unintentional traversal semantic changes when some steps were unrolled (especially barrier
+steps).
+
+As of 3.8.0, the strategy will still only be applied if `repeat()` is used with `times()` without `emit()` but now only
+applies to repeat traversals that contain exclusively safe, well-understood steps: `out()`, `in()`, `both()`, `inV()`,
+`outV()`, `otherV()`, `has(key, value)`.
+
+Repeat traversals containing other steps will no longer be unrolled. There may be some performance differences for
+traversals that previously benefited from automatic unrolling but the consistency of semantics outweighs the performance
+impact.
+
+Examples of affected traversals include (but are not limited to):
+
+[source,groovy]
+----
+g.V().repeat(both().aggregate('x')).times(2).limit(10)
+g.V().repeat(out().limit(10)).times(3)
+g.V().repeat(in().order().by("name")).times(2)
+g.V().repeat(both().simplePath()).times(4)
+g.V().repeat(both().sample(1)).times(2)
+----
+
+======= Migration Strategies
+
+Before upgrading, analyze existing traversals which use `repeat()` with any steps other than `out()`, `in()`, `both()`,
+`inV()`, `outV()`, `otherV()`, `has(key, value)` and determine if the semantics of these traversals are as expected when
+the `RepeatUnrollStrategy` is disabled using `withoutStrategies(RepeatUnrollStrategy)`. If the semantics are unexpected
+the traversal should be restructured to no longer use `repeat()` by manually unrolling the steps inside `repeat()` or by
+moving affected steps outside the `repeat()`.
+
+Example:
+
+[source,groovy]
+----
+// original traversal
+g.V().repeat(both().dedup()).times(2)
+// can be manually unrolled to
+g.V().both().dedup().both().dedup()
+// or dedup can be moved outside of repeat
+g.V().repeat(both()).times(2).dedup()
+----
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3192[TINKERPOP-3192]
+
+===== Type System Changes
+
+====== New Default DateTime Type
+
+The default implementation for date type in Gremlin is now changed from the `java.util.Date` to the more encompassing
+`java.time.OffsetDateTime`. This means the reference implementation for all date manipulation steps, `asDate()`,
+`dateAdd()`, and `dateDiff()`, as well as helper methods `datetime()`, will return `OffsetDateTime`, whose string
+representation will be in ISO 8601 format.
`Date` is still supported as incoming traverser results for these steps, as well as input into `dateDiff()` for
compatibility purposes. All dates are assumed to be in `UTC` (given epoch time).
@@ -523,7 +430,15 @@ For Java GLV, this change would impact users who are expecting the old `Date` ob
application, in this case the recommendation is to update code to expect `OffsetDateTime` as part of the version
upgrade.
-===== split() on Empty String
+====== Float Defaults to Double
+
+The `GremlinLangScriptEngine` has been modified to treat float literals without explicit type suffixes (like 'm', 'f',
+or 'd') as Double by default. Users who need `BigDecimal` precision can still use the 'm' suffix (e.g., 1.0m).
+`GremlinGroovyScriptEngine` will still default to `BigDecimal` for `float` literals.
+
+===== Modified Step Behavior
+
+====== split() on Empty String
The `split()` step will now split a string into a list of its characters if the given separator is an empty string.
@@ -540,14 +455,14 @@ g.inject("Hello").split("")
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3083[TINKERPOP-3083]
-===== asString() No Longer Allow Nulls
+====== asString() No Longer Allow Nulls
The `asString()` step will no longer allow `null` input. An `IllegalArgumentException` will be thrown for consistency
with all other parsing steps (i.e. `asDate()`, `asBool()`, `asNumber()`).
See: link:https://lists.apache.org/thread/q76pgrvhprosb4lty63bnsnbw2ljyl7m[DISCUSS] thread
-===== Split bulked traversers for `local()`
+====== Split bulked traversers for `local()`
Prior to 3.8.0, local() exhibited "traverser-local" semantics, where the local traversal would apply independently to
each individual bulkable `Traverser`. This often led to confusion, especially in the presence of reducing barrier steps, as
@@ -571,228 +486,11 @@ gremlin> g.V().out().barrier().local(count())
==>1
==>1
==>1
-----
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3196[TINKERPOP-3196]
-
-===== Gremlin Grammar Changes
-
-A number of changes have been introduced to the Gremlin grammar to help make it be more consistent and easier to use.
-
-*Removed StructureVertex from Grammar*
-
-The grammar allowed the construction of a `Vertex` by way of syntax like `new Vertex(1,'person')` (or with similar
-arguments to `ReferenceVertex`). This syntax has been removed as it served little purpose within the grammar as it
-merely adds more characters to wrap around the identifier, which could simply be used by itself.
-
-The `V()` step, as well as the `from()` and `to()` modulators used with `addE()`, previously accepted `Vertex` as
-arguments in the grammar. The `V()` step has always accepted vertex ids as arguments, and continues to do so. The
-`from()` and `to()` modulators for `addE()` continue to accept `String` arguments (which are a shorthand for
-`__.select(String)`), as well as `Traversal` arguments. As always, these `Traversal` arguments may produce `Vertex`
-objects (such as `__.V(1)`) to directly bind to from/to. Newly added in 3.8.0, the `Traversal` may also produce the id
-of a vertex present in the graph (such as `__.constant(1)`), which will then bind to from/to.
-
-When using these steps in `gremlin-lang` scripts, a `Traversal` or `String` argument must be used directly. This change
-has no effect on the `GraphTraversal` API, nor on `gremlin-groovy` scripts. Vertices can continue to be used directly in
-those contexts.
-
-[source,text]
-----
-// 3.7.4
-gremlin> v1 = g.V(1).next()
-==>v[1]
-gremlin> v6 = g.V(6).next()
-==>v[6]
-gremlin> script = String.format("g.addE('knows').from(new Vertex(%s)).to(new ReferenceVertex(%s))", v1.id(), v6.id())
-==>g.addE('knows').from(new Vertex(1)).to(new ReferenceVertex(6))
-gremlin> client.submit(script).all().get().get(0).getEdge()
-==>e[0][1-knows->6]
-
-// 3.8.0
-gremlin> v1 = g.V(1).next()
-==>v[1]
-gremlin> v6 = g.V(6).next()
-==>v[6]
-gremlin> script = String.format("g.addE('knows').from(__.V(%s)).to(__.constant(%s))", v1.id(), v6.id())
-==>g.addE('knows').from(__.V(1)).to(__.constant(6))
-gremlin> client.submit(script).all().get().get(0).getEdge()
-==>e[0][1-knows->6]
-----
-
-*`new` keyword is now optional*
-
-The `new` keyword is now optional in all cases where it was previously used. Both of the following examples are now
-valid syntax with the second being the preferred form going forward:
-
-[source,groovy]
-----
-g.V().withStrategies(new SubgraphStrategy(vertices: __.hasLabel('person')))
-
-g.V().withStrategies(SubgraphStrategy(vertices: __.hasLabel('person')))
-----
-
-In a future version, it is likely that the `new` keyword will be removed entirely from the grammar.
-
-*Supports withoutStrategies()*
-
-The `withoutStrategies()` configuration step is now supported syntax for the grammar. While this option is not commonly
-used it is still a part of the Gremlin language and there are times when it is helpful to have this fine-grained
-control over how a traversal works.
-
-[source,groovy]
-----
-g.V().withoutStrategies(CountStrategy)
-----
-
-*`Map` keys restrictions*
-
-Earlier versions of the grammar allowed a wide range of values for the keys. In many cases, these didn't really make
-sense for Gremlin and were just inherited from the Groovy language since Gremlin tends to follow that language in many
-ways. That said, Gremlin did take some liberties with that syntax and introduced its own shorthand for some cases. Those
-shorthands created unfortunate situations where certain words were being prevented as being able to be used as keys
-which could lead to confusion.
-
-A `Map` is still defined in the same way it always has been, where the following two lines produce an equivalent `Map`:
-
-[source,groovy]
-----
-[label: 100]
-["label": 100]
-----
-
-Note that when quotes are not used to denote a string, Gremlin will assume that the intention is to shorthand a string
-key and not reference a Gremlin keyword. To reference an allowable keyword as the key, either wrap it with parenthesis
-or use its longhand form as shown in the following examples which all produce the same `Map`:
-
-[source,groovy]
-----
-[T.id: 100]
-[(T.id): 100]
-[(id): 100]
-----
-
-Note that the first example is a Gremlin convenience that is not compatible in Groovy. This does produce a syntax error
-in Groovy's case. When upgrading to 3.8.0, it will be important to evaluate any code using scripts with `Map` keys that
-match keywords that are not wrapped in parentheses. On upgrade they will begin to be treated as `String` keys rather
-than their `Enum` value. This is particularly relevant for `property(Map)`, `mergeV` and `mergeE` which use a 'Map`
-for their arguments and commonly require that `T` and `Direction` be used as keys.
-
-The following examples show some `Map` usage from older versions that will work without a need for changes in 3.8.0:
-
-[source,groovy]
-----
-// the long forms are used and each are wrapped in parenthesis
-g.mergeE([(T.label):'Sibling',created:'2022-02-07',(Direction.from):1,(Direction.to):2])
-
-// the short forms are used and each are wrapped in parenthesis
-g.mergeE([(label):'Sibling',created:'2022-02-07',(Direction.from):1,(Direction.to):2])
-
-// the long forms are used and for Gremlin this is a syntax convenience to spare typing
-// the parenthesis
-g.mergeE([T.label:'Sibling',created:'2022-02-07',Direction.from:1,Direction.to:2])
-
-// while the following line mixes qualified enums with T and uses shorthand for Direction
-// with from and to all of the enums are wrapped in parenthesis
-g.mergeE([(T.label):'Sibling',created:'2022-02-07',(from):1,(to):2])
-----
-
-In this next example, the `Map` keys are defined in a way that changes will be necessary in 3.8.0:
-
-[source,groovy]
-----
-// none of the keys below are qualified with their enum long form nor are they wrapped in
-// parenthesis and as a result will be treated as String key values in 3.8.0 unless a
-// change is made
-g.mergeE([label:'Sibling',created:'2022-02-07',from:1,to:2])
-----
-
-*Restriction of Step Arguments*
-
-Prior to 3.7.0, the grammar did not allow for any parameters in gremlin scripts. In 3.7, the grammar rules
-were loosened to permit variable use almost anywhere in a traversal, in a similar fashion as groovy, however
-immediately resolved upon parsing the script, and did not bring the same performance benefits as
-parameterization in groovy scripts brings. Parameters in gremlin-lang scripts are restricted to a
-link:++https://tinkerpop.apache.org/docs/x.y.z/dev/reference/#traversal-parameterization++[subset of steps]
-in 3.8.0, and scripts which use variables elsewhere will result in parsing exceptions. The implementation
-has been updated to persist query parameters through traversal construction and strategy application.
-Parameter persistence opens the door certain optimizations for repeated query patterns. Consult your
-providers documentation for specific recommendations on using query parameters with gremlin-lang scripts in
-TinkerPop 3.8.
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-2862[TINKERPOP-2862],
-link:https://issues.apache.org/jira/browse/TINKERPOP-3046[TINKERPOP-3046],
-link:https://issues.apache.org/jira/browse/TINKERPOP-3047[TINKERPOP-3047],
-link:https://issues.apache.org/jira/browse/TINKERPOP-3023[TINKERPOP-3023]
-
-===== Improved Translators
-
-The various Java `Translator` implementations allowing conversion of Gremlin traversals to string forms in various
-languages have been modified considerably. First, they have been moved from to the
-`org.apache.tinkerpop.gremlin.language.translator` package, because they now depend on the ANTLR grammar in
-`gremlin-language` to handled the translation process. Making this change allowed for a more accurate translation of
-Gremlin that doesn't need to rely on reflection and positional arguments to determine which step was intended for use.
-
-Another important change was the introduction of specific translators for Groovy and Java. While Groovy translation
-tends to work for most Java cases, there is syntax specific to Groovy where it does not. With a specific Java
-translator, the translation process can be more accurate and less error-prone.
-
-The syntax for the translators has simplified as well. The translator function now takes a Gremlin string and a target
-language to translate to. Consider the following example:
-
-[source,text]
-----
-gremlin> GremlinTranslator.translate("g.V().out('knows')", Translator.GO)
-==>g.V().Out("knows")
-----
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3028[TINKERPOP-3028]
-
-===== Modified limit() skip() range() Semantics in repeat()
-
-The semantics of `limit()`, `skip()`, and `range()` steps called with default `Scope` or explicit `Scope.global` inside
-`repeat()` have been modified to ensure consistent semantics across repeat iterations. Previously, these steps would
-track global state across iterations, leading to unexpected filtering behavior between loops.
-
-Consider the following examples which demonstrate the unexpected behavior. Note that the examples for version 3.7.4
-disable the `RepeatUnrollStrategy` so that strategy optimization does not replace the `repeat()` traversal with a
-non-looping equivalent. 3.8.0 examples do not disable the `RepeatUnrollStrategy` as the strategy was modified to be more
-restrictive in this version.
-
-[source,groovy]
-----
-// 3.7.4 - grateful dead graph examples producing no results due to global counters
-gremlin> g.withoutStrategies(RepeatUnrollStrategy).V().has('name','JAM').repeat(out('followedBy').limit(2)).times(2).values('name')
-gremlin>
-gremlin> g.withoutStrategies(RepeatUnrollStrategy).V().has('name','DRUMS').repeat(__.in('followedBy').range(1,3)).times(2).values('name')
-gremlin>
-// 3.7.4 - modern graph examples demonstrating too many results with skip in repeat due to global counters
-gremlin> g.withoutStrategies(RepeatUnrollStrategy).V(1).repeat(out().skip(1)).times(2).values('name')
-==>ripple
-==>lop
-gremlin> g.withoutStrategies(RepeatUnrollStrategy).V(1).out().skip(1).out().skip(1).values('name')
-==>lop
-
-// 3.8.0 - grateful dead graph examples producing results as limit counters tracked per iteration
-gremlin> g.V().has('name','JAM').repeat(out('followedBy').limit(2)).times(2).values('name')
-==>HURTS ME TOO
-==>BLACK THROATED WIND
-gremlin> g.V().has('name','DRUMS').repeat(__.in('followedBy').range(1,3)).times(2).values('name')
-==>DEAL
-==>WOMEN ARE SMARTER
-// 3.8.0 - modern graph examples demonstrating consistent skip semantics
-gremlin> g.V(1).repeat(out().skip(1)).times(2).values('name')
-==>lop
-gremlin> g.V(1).out().skip(1).out().skip(1).values('name')
-==>lop
-----
-
-This change ensures that `limit()`, `skip()`, and `range()` steps called with default `Scope` or explicit `Scope.global`
-inside `repeat()` are more consistent with manually unrolled traversals. Before upgrading, users should determine if any
-traversals use `limit()`, skip()`, or `range()` with default `Scope` or explicit `Scope.global` inside `repeat()`. If it
-is desired that the limit or range should apply across all loops then the `limit()`, `skip()`, or `range()` step should
-be moved out of the `repeat()` step.
+----
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3196[TINKERPOP-3196]
-===== Add barrier to most SideEffect steps
+====== Add barrier to most SideEffect steps
Prior to 3.8.0, the `group(String)`, `groupCount(String)`, `tree(String)` and `subgraph(String)` steps were non-blocking,
in that they allowed traversers to pass through without fully iterating the traversal and fully computing the side
@@ -835,7 +533,7 @@ gremlin> g.V().local(groupCount("x")).select("x")
==>[v[1]:1,v[2]:1,v[3]:1,v[4]:1,v[5]:1,v[6]:1]
----
-===== choose() Semantics
+====== choose() Semantics
Several enhancements and clarifications have been made to the `choose()` step in TinkerPop 3.8.0 to improve its behavior
and make it more consistent:
@@ -967,165 +665,417 @@ gremlin> g.V().hasLabel("person").union(
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3178[TINKERPOP-3178],
link:https://tinkerpop.apache.org/docs/3.8.0/reference/#choose-step[Reference Documentation - choose()]
-===== Float Defaults to Double
+====== Consistent Output for range(), limit(), tail()
-The `GremlinLangScriptEngine` has been modified to treat float literals without explicit type suffixes (like 'm', 'f',
-or 'd') as Double by default. Users who need `BigDecimal` precision can still use the 'm' suffix (e.g., 1.0m).
-`GremlinGroovyScriptEngine` will still default to `BigDecimal` for `float` literals.
+The `range(local)`, `limit(local)`, and `tail(local)` steps now consistently return collections rather than automatically
+unfolding single-element results when operating on iterable collections (List, Set, etc.). Previously, when these steps
+operated on collections and the result contained only one element, the step would return the single element directly
+instead of a collection containing that element.
+
+This change ensures predictable return types based on the input type, making the behavior more consistent and intuitive.
+Note that this change only affects iterable collections - Map objects continue to behave as before.
+
+[WARNING]
+====
+This is a breaking change that may require modifications to existing queries. If your queries relied on the previous
+behavior of receiving single elements directly from `range(local)`, `limit(local)`, or `tail(local)` steps, you will
+need to add `.unfold()` after these steps to maintain the same functionality. Without this update, some existing queries
+may throw a `ClassCastException` while others may return unexpected results.
+====
+
+[source,text]
+----
+// 3.7.x and earlier - inconsistent output types for collections
+gremlin> g.inject([1, 2, 3]).limit(local, 1)
+==>1 // single element returned directly
+
+gremlin> g.inject([1, 2, 3]).limit(local, 2)
+==>[1,2] // collection returned
+
+// 3.8.0 - consistent collection output for collections
+gremlin> g.inject([1, 2, 3]).limit(local, 1)
+==>[1] // collection always returned
+
+gremlin> g.inject([1, 2, 3]).limit(local, 2)
+==>[1,2] // collection returned
+
+// Map behavior unchanged in both versions
+gremlin> g.inject([a: 1, b: 2, c: 3]).limit(local, 1)
+==>[a:1] // Map entry returned (behavior unchanged)
+----
+
+If you need the old behavior of extracting single elements from collections, you can add `.unfold()` after the local step:
+
+[source,text]
+----
+gremlin> g.inject([1, 2, 3]).limit(local, 1).unfold()
+==>1
+----
+
+This change affects all three local collection manipulation steps when operating on iterable collections:
+- `range(local, low, high)`
+- `limit(local, count)`
+- `tail(local, count)`
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-2491[TINKERPOP-2491]
+
+====== group() Value Traversal Semantics
+
+The `group()` step takes two `by()` modulators. The first defines the key for the grouping, and the second acts upon the
+values grouped to each key. The latter is referred to as the "value traversal". In earlier versions of TinkerPop,
+using `order()` in the value traversal could produce an unexpected result if combined with a step like `fold()`.
+
+[source,text]
+----
+gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__.out().fold())
+==>[v[2]:[],v[6]:[v[3]]]
+gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__.out().order().fold())
+==>[v[6]:[v[3]]]
+----
+
+The example above shows that `v[2]` gets filtered away when `order()` is included. This was not expected behavior. The
+problem can be more generally explained as an issue where a `Barrier` like `order()` can return an empty result. If this
+step is followed by another `Barrier` that always produces an output like `sum()`, `count()` or `fold()` then the empty
+result would not feed through to that following step. This issue has now been fixed and the two traversals from the
+previous example now return the same results.
+
+[source,text]
+----
+gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__.out().fold())
+==>[v[2]:[],v[6]:[v[3]]]
+gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__.out().order().fold())
+==>[v[2]:[],v[6]:[v[3]]]
+----
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-2971[TINKERPOP-2971]
+
+====== Remove Undocumented `with()` modulation
+
+There has long been a connection between the `with()` modulator, and mutating steps due to the design of
+some of the interfaces in the gremlin traversal engine. This has led to several undocumented usages of the
+`with()` modulator which have never been officially supported but have previously been functional.
+
+As of 3.8.0 `with()` modulation of the following steps will no longer work: `addV()`, `addE()`, `property()`, `drop()`,
+`mergeV()`, and `mergeE()`.
+
+====== By Modulation Semantics
+
+*valueMap() and propertyMap() Semantics*
+
+The `valueMap()` and `propertyMap()` steps have been changed to throw an error if multiple `by()` modulators are applied.
+The previous behavior attempted to round-robin the `by()` but this wasn't possible for all providers.
+
+**groupCount(), dedup(), sack(), sample(), aggregate() By Modulation Semantics**
+
+The `groupCount()`, `dedup()`, `sack()`, `sample()`, and `aggregate()` steps has been changed to throw an error if
+multiple `by()` modulators are applied. The previous behavior would ignore previous `by()` modulators and apply the
+last one, which was not intuitive.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3121[TINKERPOP-3121],
+link:https://issues.apache.org/jira/browse/TINKERPOP-2974[TINKERPOP-2974]
+
+===== Gremlin Grammar Changes
+
+A number of changes have been introduced to the Gremlin grammar to help make it be more consistent and easier to use.
+
+*Removed StructureVertex from Grammar*
+
+The grammar allowed the construction of a `Vertex` by way of syntax like `new Vertex(1,'person')` (or with similar
+arguments to `ReferenceVertex`). This syntax has been removed as it served little purpose within the grammar as it
+merely adds more characters to wrap around the identifier, which could simply be used by itself.
+
+The `V()` step, as well as the `from()` and `to()` modulators used with `addE()`, previously accepted `Vertex` as
+arguments in the grammar. The `V()` step has always accepted vertex ids as arguments, and continues to do so. The
+`from()` and `to()` modulators for `addE()` continue to accept `String` arguments (which are a shorthand for
+`__.select(String)`), as well as `Traversal` arguments. As always, these `Traversal` arguments may produce `Vertex`
+objects (such as `__.V(1)`) to directly bind to from/to. Newly added in 3.8.0, the `Traversal` may also produce the id
+of a vertex present in the graph (such as `__.constant(1)`), which will then bind to from/to.
+
+When using these steps in `gremlin-lang` scripts, a `Traversal` or `String` argument must be used directly. This change
+has no effect on the `GraphTraversal` API, nor on `gremlin-groovy` scripts. Vertices can continue to be used directly in
+those contexts.
+
+[source,text]
+----
+// 3.7.4
+gremlin> v1 = g.V(1).next()
+==>v[1]
+gremlin> v6 = g.V(6).next()
+==>v[6]
+gremlin> script = String.format("g.addE('knows').from(new Vertex(%s)).to(new ReferenceVertex(%s))", v1.id(), v6.id())
+==>g.addE('knows').from(new Vertex(1)).to(new ReferenceVertex(6))
+gremlin> client.submit(script).all().get().get(0).getEdge()
+==>e[0][1-knows->6]
+
+// 3.8.0
+gremlin> v1 = g.V(1).next()
+==>v[1]
+gremlin> v6 = g.V(6).next()
+==>v[6]
+gremlin> script = String.format("g.addE('knows').from(__.V(%s)).to(__.constant(%s))", v1.id(), v6.id())
+==>g.addE('knows').from(__.V(1)).to(__.constant(6))
+gremlin> client.submit(script).all().get().get(0).getEdge()
+==>e[0][1-knows->6]
+----
+
+*`new` keyword is now optional*
+
+The `new` keyword is now optional in all cases where it was previously used. Both of the following examples are now
+valid syntax with the second being the preferred form going forward:
+
+[source,groovy]
+----
+g.V().withStrategies(new SubgraphStrategy(vertices: __.hasLabel('person')))
+
+g.V().withStrategies(SubgraphStrategy(vertices: __.hasLabel('person')))
+----
+
+In a future version, it is likely that the `new` keyword will be removed entirely from the grammar.
+
+*Supports withoutStrategies()*
+
+The `withoutStrategies()` configuration step is now supported syntax for the grammar. While this option is not commonly
+used it is still a part of the Gremlin language and there are times when it is helpful to have this fine-grained
+control over how a traversal works.
+
+[source,groovy]
+----
+g.V().withoutStrategies(CountStrategy)
+----
+
+*`Map` keys restrictions*
+
+Earlier versions of the grammar allowed a wide range of values for the keys. In many cases, these didn't really make
+sense for Gremlin and were just inherited from the Groovy language since Gremlin tends to follow that language in many
+ways. That said, Gremlin did take some liberties with that syntax and introduced its own shorthand for some cases. Those
+shorthands created unfortunate situations where certain words were being prevented as being able to be used as keys
+which could lead to confusion.
+
+A `Map` is still defined in the same way it always has been, where the following two lines produce an equivalent `Map`:
+
+[source,groovy]
+----
+[label: 100]
+["label": 100]
+----
+
+Note that when quotes are not used to denote a string, Gremlin will assume that the intention is to shorthand a string
+key and not reference a Gremlin keyword. To reference an allowable keyword as the key, either wrap it with parenthesis
+or use its longhand form as shown in the following examples which all produce the same `Map`:
+
+[source,groovy]
+----
+[T.id: 100]
+[(T.id): 100]
+[(id): 100]
+----
+
+Note that the first example is a Gremlin convenience that is not compatible in Groovy. This does produce a syntax error
+in Groovy's case. When upgrading to 3.8.0, it will be important to evaluate any code using scripts with `Map` keys that
+match keywords that are not wrapped in parentheses. On upgrade they will begin to be treated as `String` keys rather
+than their `Enum` value. This is particularly relevant for `property(Map)`, `mergeV` and `mergeE` which use a 'Map`
+for their arguments and commonly require that `T` and `Direction` be used as keys.
+
+The following examples show some `Map` usage from older versions that will work without a need for changes in 3.8.0:
+
+[source,groovy]
+----
+// the long forms are used and each are wrapped in parenthesis
+g.mergeE([(T.label):'Sibling',created:'2022-02-07',(Direction.from):1,(Direction.to):2])
+
+// the short forms are used and each are wrapped in parenthesis
+g.mergeE([(label):'Sibling',created:'2022-02-07',(Direction.from):1,(Direction.to):2])
+
+// the long forms are used and for Gremlin this is a syntax convenience to spare typing
+// the parenthesis
+g.mergeE([T.label:'Sibling',created:'2022-02-07',Direction.from:1,Direction.to:2])
+
+// while the following line mixes qualified enums with T and uses shorthand for Direction
+// with from and to all of the enums are wrapped in parenthesis
+g.mergeE([(T.label):'Sibling',created:'2022-02-07',(from):1,(to):2])
+----
+
+In this next example, the `Map` keys are defined in a way that changes will be necessary in 3.8.0:
+
+[source,groovy]
+----
+// none of the keys below are qualified with their enum long form nor are they wrapped in
+// parenthesis and as a result will be treated as String key values in 3.8.0 unless a
+// change is made
+g.mergeE([label:'Sibling',created:'2022-02-07',from:1,to:2])
+----
+
+*Restriction of Step Arguments*
+
+Prior to 3.7.0, the grammar did not allow for any parameters in gremlin scripts. In 3.7, the grammar rules
+were loosened to permit variable use almost anywhere in a traversal, in a similar fashion as groovy, however
+immediately resolved upon parsing the script, and did not bring the same performance benefits as
+parameterization in groovy scripts brings. Parameters in gremlin-lang scripts are restricted to a
+link:++https://tinkerpop.apache.org/docs/x.y.z/dev/reference/#traversal-parameterization++[subset of steps]
+in 3.8.0, and scripts which use variables elsewhere will result in parsing exceptions. The implementation
+has been updated to persist query parameters through traversal construction and strategy application.
+Parameter persistence opens the door certain optimizations for repeated query patterns. Consult your
+providers documentation for specific recommendations on using query parameters with gremlin-lang scripts in
+TinkerPop 3.8.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-2862[TINKERPOP-2862],
+link:https://issues.apache.org/jira/browse/TINKERPOP-3046[TINKERPOP-3046],
+link:https://issues.apache.org/jira/browse/TINKERPOP-3047[TINKERPOP-3047],
+link:https://issues.apache.org/jira/browse/TINKERPOP-3023[TINKERPOP-3023]
-===== Consistent Output for range(), limit(), tail()
+===== Gremlin Language Variant Changes
-The `range(local)`, `limit(local)`, and `tail(local)` steps now consistently return collections rather than automatically
-unfolding single-element results when operating on iterable collections (List, Set, etc.). Previously, when these steps
-operated on collections and the result contained only one element, the step would return the single element directly
-instead of a collection containing that element.
+====== Set minimum Java version to 11
-This change ensures predictable return types based on the input type, making the behavior more consistent and intuitive.
-Note that this change only affects iterable collections - Map objects continue to behave as before.
+TinkerPop 3.8 requires a minimum of Java 11 for building and running. Support for Java 1.8 has been dropped.
-[WARNING]
-====
-This is a breaking change that may require modifications to existing queries. If your queries relied on the previous
-behavior of receiving single elements directly from `range(local)`, `limit(local)`, or `tail(local)` steps, you will
-need to add `.unfold()` after these steps to maintain the same functionality. Without this update, some existing queries
-may throw a `ClassCastException` while others may return unexpected results.
-====
+====== SeedStrategy Construction
-[source,text]
-----
-// 3.7.x and earlier - inconsistent output types for collections
-gremlin> g.inject([1, 2, 3]).limit(local, 1)
-==>1 // single element returned directly
+The `SeedStrategy` public constructor has been removed for Java and has been replaced by the builder pattern common
+to all strategies. This change was made to ensure that the `SeedStrategy` could be constructed consistently.
-gremlin> g.inject([1, 2, 3]).limit(local, 2)
-==>[1,2] // collection returned
+====== OptionsStrategy in Python
-// 3.8.0 - consistent collection output for collections
-gremlin> g.inject([1, 2, 3]).limit(local, 1)
-==>[1] // collection always returned
+The `\\__init__()` syntax has been updated to be both more Pythonic and more aligned to the `gremlin-lang` syntax.
+Previously, `OptionsStrategy()` took a single argument `options` which was a `dict` of all options to be set.
+Now, all options should be set directly as keyword arguments.
-gremlin> g.inject([1, 2, 3]).limit(local, 2)
-==>[1,2] // collection returned
+For example:
-// Map behavior unchanged in both versions
-gremlin> g.inject([a: 1, b: 2, c: 3]).limit(local, 1)
-==>[a:1] // Map entry returned (behavior unchanged)
+[source,python]
----
+# 3.7 and before:
+g.with_strategies(OptionsStrategy(options={'key1': 'value1', 'key2': True}))
+# 4.x and newer:
+g.with_strategies(OptionsStrategy(key1='value1', key2=True))
-If you need the old behavior of extracting single elements from collections, you can add `.unfold()` after the local step:
-
-[source,text]
-----
-gremlin> g.inject([1, 2, 3]).limit(local, 1).unfold()
-==>1
+myOptions = {'key1': 'value1', 'key2': True}
+# 3.7 and before:
+g.with_strategies(OptionsStrategy(options=myOptions))
+# 4.x and newer:
+g.with_strategies(OptionsStrategy(**myOptions))
----
-This change affects all three local collection manipulation steps when operating on iterable collections:
-- `range(local, low, high)`
-- `limit(local, count)`
-- `tail(local, count)`
+====== Serialization Changes
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-2491[TINKERPOP-2491]
+*Properties on Element Serialization in Python & Javascript*
-===== group() Value Traversal Semantics
+Element properties handling has been inconsistent across GLVs. Previously,`gremlin-python` deserialized empty properties
+as None or array depending on the serializer, while `gremlin-javascript` returned properties as objects or arrays, with
+empty properties as empty lists or undefined depending on the serializer.
-The `group()` step takes two `by()` modulators. The first defines the key for the grouping, and the second acts upon the
-values grouped to each key. The latter is referred to as the "value traversal". In earlier versions of TinkerPop,
-using `order()` in the value traversal could produce an unexpected result if combined with a step like `fold()`.
+This inconsistency is now resolved, aligning to how properties are handled in Gremlin core and in the Java GLV.
+Both GLVs will deserialize element properties into lists of property objects, returning empty lists instead of null values
+for missing properties.
-[source,text]
-----
-gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__.out().fold())
-==>[v[2]:[],v[6]:[v[3]]]
-gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__.out().order().fold())
-==>[v[6]:[v[3]]]
-----
+For python, the most notable difference is in graphSON when "tokens" is turned on for "materializeProperties". The
+properties returned are no longer `None`, but empty lists. Users should update their code accordingly.
-The example above shows that `v[2]` gets filtered away when `order()` is included. This was not expected behavior. The
-problem can be more generally explained as an issue where a `Barrier` like `order()` can return an empty result. If this
-step is followed by another `Barrier` that always produces an output like `sum()`, `count()` or `fold()` then the empty
-result would not feed through to that following step. This issue has now been fixed and the two traversals from the
-previous example now return the same results.
+For javascript, the change is slightly more extensive, as user should no longer expect javascript objects to be returned.
+All properties are returned as lists of Property or VertexProperty objects.
-[source,text]
-----
-gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__.out().fold())
-==>[v[2]:[],v[6]:[v[3]]]
-gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__.out().order().fold())
-==>[v[2]:[],v[6]:[v[3]]]
+[source,javascript]
----
+// 3.7 and before:
+g.with_("materializeProperties", "tokens").V(1).next() // skip properties with token
+// graphson will return properties as a javascript object, which becomes undefined
+Vertex { id: 1, label: 'person', properties: undefined }
+// graphbinary will return properties as empty lists
+Vertex { id: 1, label: 'person', properties: [] }
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-2971[TINKERPOP-2971]
+g.V(1).next() // properties returned
+// graphson will return properties as a javascript object
+Vertex {
+ id: 1,
+ label: 'person',
+ properties: { name: [Array], age: [Array] }
+}
+// graphbinary will return properties as lists of VertexProperty objects
+Vertex {
+ id: 1,
+ label: 'person',
+ properties: [ [VertexProperty], [VertexProperty] ]
+}
-===== By Modulation Semantics
+// 3.8.0 and newer - properties are always arrays, empty array [] for missing properties:
+g.with_("materializeProperties", "tokens").V(1).next() // skip properties with token
+// both graphson and graphbinary return
+Vertex { id: 1, label: 'person', properties: [] }
+g.V(1).next()
+// both graphson and graphbinary return
+Vertex {
+ id: 1,
+ label: 'person',
+ properties: [ [VertexProperty], [VertexProperty] ]
+}
-*valueMap() and propertyMap() Semantics*
+----
-The `valueMap()` and `propertyMap()` steps have been changed to throw an error if multiple `by()` modulators are applied.
-The previous behavior attempted to round-robin the `by()` but this wasn't possible for all providers.
+This change only affects how GLVs deserialize property data in client applications. The underlying graph serialization
+formats and server-side behavior remain unchanged.
-**groupCount(), dedup(), sack(), sample(), aggregate() By Modulation Semantics**
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3186[TINKERPOP-3186]
-The `groupCount()`, `dedup()`, `sack()`, `sample()`, and `aggregate()` steps has been changed to throw an error if
-multiple `by()` modulators are applied. The previous behavior would ignore previous `by()` modulators and apply the
-last one, which was not intuitive.
+*Javascript Set Deserialization*
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3121[TINKERPOP-3121],
-link:https://issues.apache.org/jira/browse/TINKERPOP-2974[TINKERPOP-2974]
+Starting from this version, `gremlin-javascript` will deserialize `Set` data into a ECMAScript 2015 Set. Previously,
+these were deserialized into arrays.
-===== Stricter RepeatUnrollStrategy
+*.NET Byte Serialization Change*
-The `RepeatUnrollStrategy` has been updated to use a more conservative approach for determining which repeat traversals
-are safe to unroll. Previously, the strategy would attempt to unroll most usages of `repeat()` used with `times()`
-without `emit()`. This caused unintentional traversal semantic changes when some steps were unrolled (especially barrier
-steps).
+The Gremlin .NET serializers has been updated to correctly handle byte values as signed integers to align with the IO
+specification, whereas previously it incorrectly serialized and deserialized bytes as unsigned values.
-As of 3.8.0, the strategy will still only be applied if `repeat()` is used with `times()` without `emit()` but now only
-applies to repeat traversals that contain exclusively safe, well-understood steps: `out()`, `in()`, `both()`, `inV()`,
-`outV()`, `otherV()`, `has(key, value)`.
+This is a breaking change for .NET applications that rely on byte values. Existing applications using byte values
+should consider switching to `sbyte` for signed byte operations or `short` for a wider range of values to maintain
+compatibility.
-Repeat traversals containing other steps will no longer be unrolled. There may be some performance differences for
-traversals that previously benefited from automatic unrolling but the consistency of semantics outweighs the performance
-impact.
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3161[TINKERPOP-3161]
-Examples of affected traversals include (but are not limited to):
+===== Removal of P.getOriginalValue()
-[source,groovy]
-----
-g.V().repeat(both().aggregate('x')).times(2).limit(10)
-g.V().repeat(out().limit(10)).times(3)
-g.V().repeat(in().order().by("name")).times(2)
-g.V().repeat(both().simplePath()).times(4)
-g.V().repeat(both().sample(1)).times(2)
-----
+`P.getOriginalValue()` has been removed as it was not offering much value and was often confused with `P.getValue()`.
+Usage of `P.getOriginalValue()` often leads to unexpected results if called on a predicate which has had its value reset
+after construction. All usages of `P.getOriginalValue()` should be replaced with `P.getValue()`.
-====== Migration Strategies
+===== Improved Translators
-Before upgrading, analyze existing traversals which use `repeat()` with any steps other than `out()`, `in()`, `both()`,
-`inV()`, `outV()`, `otherV()`, `has(key, value)` and determine if the semantics of these traversals are as expected when
-the `RepeatUnrollStrategy` is disabled using `withoutStrategies(RepeatUnrollStrategy)`. If the semantics are unexpected
-the traversal should be restructured to no longer use `repeat()` by manually unrolling the steps inside `repeat()` or by
-moving affected steps outside the `repeat()`.
+The various Java `Translator` implementations allowing conversion of Gremlin traversals to string forms in various
+languages have been modified considerably. First, they have been moved from to the
+`org.apache.tinkerpop.gremlin.language.translator` package, because they now depend on the ANTLR grammar in
+`gremlin-language` to handled the translation process. Making this change allowed for a more accurate translation of
+Gremlin that doesn't need to rely on reflection and positional arguments to determine which step was intended for use.
-Example:
+Another important change was the introduction of specific translators for Groovy and Java. While Groovy translation
+tends to work for most Java cases, there is syntax specific to Groovy where it does not. With a specific Java
+translator, the translation process can be more accurate and less error-prone.
-[source,groovy]
+The syntax for the translators has simplified as well. The translator function now takes a Gremlin string and a target
+language to translate to. Consider the following example:
+
+[source,text]
----
-// original traversal
-g.V().repeat(both().dedup()).times(2)
-// can be manually unrolled to
-g.V().both().dedup().both().dedup()
-// or dedup can be moved outside of repeat
-g.V().repeat(both()).times(2).dedup()
+gremlin> GremlinTranslator.translate("g.V().out('knows')", Translator.GO)
+==>g.V().Out("knows")
----
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3192[TINKERPOP-3192]
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3028[TINKERPOP-3028]
-==== Other Breaking Changes
+===== Simplified Comparability Semantics
-===== Set minimum Java version to 11
+The previous system of ternary boolean semantics has been replaced with simplified binary semantics. The triggers for
+"ERROR" states from illegal comparisons are unchanged (typically comparisons with NaN or between incomparable types
+such as String and int). The difference now is that instead of the ERROR being propagated according to ternary logic
+semantics until a reduction point is reached, the error now immediately returns a value of FALSE.
-TinkerPop 3.8 requires a minimum of Java 11 for building and running. Support for Java 1.8 has been dropped.
+This will be most visible in expressions which include negations. Prior to this change, `g.inject(NaN).not(is(1))` would
+produce no results as `!(NaN == 1)` -> `!(ERROR)` -> `ERROR` -> traverser is filtered out. After this change, the same
+traversal will return NaN as the same expression now evaluates as `!(NaN == 1)` -> `!(FALSE)` -> `TRUE` -> traverser is
+not filtered.
+
+See: link:https://tinkerpop.apache.org/docs/3.8.0/dev/provider/#gremlin-semantics-equality-comparability[Comparability semantics docs]
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3173[TINKERPOP-3173]
==== Additions and New Features
@@ -1161,7 +1111,9 @@ gremlin> g.V().has('airport','code','IAD').valueMap('code','desc','lon','lat')
TinkerPop distributes the 1.0 version of the dataset.
-===== Type Predicate
+===== Type Conversion And Comparison
+
+====== Type Predicate
The new `P.typeOf()` predicate allows filtering traversers based on their runtime type. It accepts either a `GType`
enum constant or a string representation of a simple class name. This predicate is particularly useful for type-safe
@@ -1193,9 +1145,10 @@ gremlin> g.union(V(), E()).values().is(P.typeOf(GType.NUMBER))
The predicate supports type inheritance where `GType.NUMBER` will match any numeric type. Invalid type names will
throw an exception at execution time.
-See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#a-note-on-predicates[Predicates], link:https://issues.apache.org/jira/browse/TINKERPOP-2234[TINKERPOP-2234]
+See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#a-note-on-predicates[Predicates],
+link:https://issues.apache.org/jira/browse/TINKERPOP-2234[TINKERPOP-2234]
-===== Number Conversion Step
+====== Number Conversion Step
The new `asNumber()` step provides type casting functionality to Gremlin. It serves as an umbrella step that parses
strings and casts numbers into desired types. For the convenience of remote traversals in GLVs, these available types
@@ -1257,9 +1210,10 @@ gremlin> g.inject([1, 2, 3, 4]).asNumber()
==> IllegalArgumentException
----
-See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asNumber-step[asNumber()-step], link:https://issues.apache.org/jira/browse/TINKERPOP-3166[TINKERPOP-3166]
+See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asNumber-step[asNumber()-step],
+link:https://issues.apache.org/jira/browse/TINKERPOP-3166[TINKERPOP-3166]
-===== Boolean Conversion Step
+====== Boolean Conversion Step
The `asBool()` step bridges another gap in Gremlin's casting functionalities. Users now have the ability to parse
strings and numbers into boolean values, both for normalization and to perform boolean logic with numerical values.
@@ -1287,6 +1241,48 @@ gremlin> g.V().sack(assign).by(__.hasLabel('person').count().asBool()).sack(and)
See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asBool-step[asBool()-step],
link:https://issues.apache.org/jira/browse/TINKERPOP-3175[TINKERPOP-3175]
+====== Auto-promotion of Numbers
+
+Previously, operations like `sum` or `sack` that involved mathematical calculations did not automatically promote the
+result to a larger numeric type (e.g., from int to long) when needed. As a result, values could wrap around within their
+current type leading to unexpected behavior. This issue has now been resolved by enabling automatic type promotion for
+results.
+
+Now, any mathematical operations such as `Add`, `Sub`, `Mul`, and `Div` will now automatically promote to the next
+numeric type if an overflow is detected. For integers, the promotion sequence is: byte → short → int → long → overflow
+exception. For floating-point numbers, the sequence is: float → double → infinity.
+
+The following example showcases the change in overflow behavior between 3.7.3 and 3.8.0
+
+[source,text]
+----
+// 3.7.3
+gremlin> g.inject([Byte.MAX_VALUE, (byte) 1], [Short.MAX_VALUE, (short) 1], [Integer.MAX_VALUE,1], [Long.MAX_VALUE, 1l]).sum(local)
+==>-128 // byte
+==>-32768 // short
+==>-2147483648 // int
+==>-9223372036854775808 // long
+
+gremlin> g.inject([Float.MAX_VALUE, Float.MAX_VALUE], [Double.MAX_VALUE, Double.MAX_VALUE]).sum(local)
+==>Infinity // float
+==>Infinity // double
+
+// 3.8.0
+gremlin> g.inject([Byte.MAX_VALUE, (byte) 1], [Short.MAX_VALUE, (short) 1], [Integer.MAX_VALUE,1]).sum(local)
+==>128 // short
+==>32768 // int
+==>2147483648 // long
+
+gremlin> g.inject([Long.MAX_VALUE, 1l]).sum(local)
+// throws java.lang.ArithmeticException: long overflow
+
+gremlin> g.inject([Float.MAX_VALUE, Float.MAX_VALUE], [Double.MAX_VALUE, Double.MAX_VALUE]).sum(local)
+==>6.805646932770577E38 // double
+==>Infinity // double
+----
+
+See link:https://issues.apache.org/jira/browse/TINKERPOP-3115[TINKERPOP-3115]
+
==== Deprecations
===== Deprecated UnifiedChannelizer
@@ -1587,4 +1583,4 @@ encompassing `java.time.OffsetDateTime`. This means the reference implementation
string representation will be in ISO 8601 format.
This means that drivers should use the extended `OffsetDateTime` type in the IO specs to serialize and deserialize
-native date objects.
\ No newline at end of file
+native date objects.
(tinkerpop) 01/02: Re-organize upgrade documentation based on severity
Posted by ke...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
kenhuuu pushed a commit to branch 3.8-documentation
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit c28a1d0650d8f24e1bb7e8a33003e4dd3740171b
Author: Ken Hu <10...@users.noreply.github.com>
AuthorDate: Wed Oct 29 14:47:01 2025 -0700
Re-organize upgrade documentation based on severity
---
docs/src/upgrade/release-3.8.x.asciidoc | 970 ++++++++++++++++----------------
1 file changed, 472 insertions(+), 498 deletions(-)
diff --git a/docs/src/upgrade/release-3.8.x.asciidoc b/docs/src/upgrade/release-3.8.x.asciidoc
index 9f254b581d..58c053a741 100644
--- a/docs/src/upgrade/release-3.8.x.asciidoc
+++ b/docs/src/upgrade/release-3.8.x.asciidoc
@@ -30,171 +30,16 @@ complete list of all the modifications that are part of this release.
=== Upgrading for Users
-==== Gremlin MCP Server
+==== Breaking Language Changes
-Gremlin MCP Server is an experimental application that implements the link:https://modelcontextprotocol.io/[Model Context Protocol]
-(MCP) to expose Gremlin Server-backed graph operations to MCP-capable clients such as Claude Desktop, Cursor, or
-Windsurf. Through this integration, graph structure can be discovered, and Gremlin traversals can be executed. Basic
-health checks are included to validate connectivity.
-
-A running Gremlin Server that fronts the target TinkerPop graph is required. An MCP client can be configured to connect
-to the Gremlin MCP Server endpoint.
-
-==== Air Routes Dataset
-
-The Air Routes sample dataset has long been used to help showcase and teach Gremlin. Popularized by the first edition
-of link:https://kelvinlawrence.net/book/PracticalGremlin.html[Practical Gremlin], this dataset offers a real-world graph
-structure that allows for practical demonstration of virtually every feature that Gremlin syntax has to offer. While it
-was easy to simply get the dataset from the Practical Gremlin link:https://github.com/krlawrence/graph[repository],
-including it with the TinkerPop distribution makes it much more convenient to use with Gremlin Server, Gremlin Console,
-or directly in code that depends on the `tinkergraph-gremlin` package.
-
-[source,text]
-----
-plugin activated: tinkerpop.tinkergraph
-gremlin> graph = TinkerFactory.createAirRoutes()
-==>tinkergraph[vertices:3619 edges:50148]
-gremlin> g = traversal().with(graph)
-==>graphtraversalsource[tinkergraph[vertices:3619 edges:50148], standard]
-gremlin> g.V().has('airport','code','IAD').valueMap('code','desc','lon','lat')
-==>[code:[IAD],lon:[-77.45580292],lat:[38.94449997],desc:[Washington Dulles International Airport]]
-----
-
-TinkerPop distributes the 1.0 version of the dataset.
-
-==== Type Predicate
-
-The new `P.typeOf()` predicate allows filtering traversers based on their runtime type. It accepts either a `GType`
-enum constant or a string representation of a simple class name. This predicate is particularly useful for type-safe
-filtering in heterogeneous data scenarios.
-
-[source,text]
-----
-// Filter vertices by property type
-gremlin> g.V().values("age","name").is(P.typeOf(GType.INT))
-==>29
-==>27
-==>32
-==>35
-
-// Type inheritance support - NUMBER matches all numeric types
-gremlin> g.union(V(), E()).values().is(P.typeOf(GType.NUMBER))
-==>29
-==>27
-==>32
-==>35
-==>0.5
-==>1.0
-==>0.4
-==>1.0
-==>0.4
-==>0.2
-----
-
-The predicate supports type inheritance where `GType.NUMBER` will match any numeric type. Invalid type names will
-throw an exception at execution time.
-
-See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#a-note-on-predicates[Predicates], link:https://issues.apache.org/jira/browse/TINKERPOP-2234[TINKERPOP-2234]
-
-==== Number Conversion Step
-
-The new `asNumber()` step provides type casting functionality to Gremlin. It serves as an umbrella step that parses
-strings and casts numbers into desired types. For the convenience of remote traversals in GLVs, these available types
-are denoted by a set of number tokens (`GType`).
-
-This new step will allow users to normalize their data by converting string numbers and mixed numeric types to a
-consistent format, making it easier to perform downstream mathematical operations. As an example:
-
-[source,text]
-----
-// sum() step can only take numbers
-gremlin> g.inject(1.0, 2l, 3, "4", "0x5").sum()
-class java.lang.String cannot be cast to class java.lang.Number
-
-// use asNumber() to avoid casting exceptions
-gremlin> g.inject(1.0, 2l, 3, "4", "0x5").asNumber().sum()
-==>15.0
-
-// given sum() step returned a double, one can use asNumber() to further cast the result into desired type
-gremlin> g.inject(1.0, 2l, 3, "4", "0x5").asNumber().sum().asNumber(GType.INT)
-==>15
-----
-
-Semantically, the `asNumber()` step will convert the incoming traverser to a logical parsable type if no argument is
-provided, or to the desired numerical type, based on the number token (`GType`) provided.
-
-Numerical input will pass through unless a type is specified by the number token. `ArithmeticException` will be thrown
-for any overflow as a result of narrowing of types:
-
-[source,text]
-----
-gremlin> g.inject(5.0).asNumber(GType.INT)
-==> 5 // casts double to int
-gremlin> g.inject(12).asNumber(GType.BYTE)
-==> 12
-gremlin> g.inject(128).asNumber(GType.BYTE)
-==> ArithmeticException
-----
-
-String input will be parsed. By default, the smalled unit of number to be parsed into is `int` if no type token is
-provided. `NumberFormatException` will be thrown for any unparsable strings:
-
-[source,text]
-----
-gremlin> g.inject("5").asNumber()
-==> 5
-gremlin> g.inject("5.7").asNumber(GType.INT)
-==> 5
-gremlin> g.inject("1,000").asNumber(GType.INT)
-==> NumberFormatException
-gremlin> g.inject("128").asNumber(GType.BYTE)
-==> ArithmeticException
-----
-
-All other input types will result in `IllegalArgumentException`:
-[source,text]
-----
-gremlin> g.inject([1, 2, 3, 4]).asNumber()
-==> IllegalArgumentException
-----
-
-See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asNumber-step[asNumber()-step], link:https://issues.apache.org/jira/browse/TINKERPOP-3166[TINKERPOP-3166]
-
-==== Boolean Conversion Step
-
-The `asBool()` step bridges another gap in Gremlin's casting functionalities. Users now have the ability to parse
-strings and numbers into boolean values, both for normalization and to perform boolean logic with numerical values.
-
-[source,text]
-----
-gremlin> g.inject(2, "true", 1, 0, false, "FALSE").asBool().fold()
-==>[true,true,true,false,false,false]
-
-// using the modern graph, we can turn count() results into boolean values
-gremlin> g.V().local(outE().count()).fold()
-==>[3,0,0,2,0,1]
-gremlin> g.V().local(outE().count()).asBool().fold()
-==>[true,false,false,true,false,true]
-// a slightly more complex one using sack for boolean operations for vertices with both 'person' label and has out edges
-gremlin> g.V().sack(assign).by(__.hasLabel('person').count().asBool()).sack(and).by(__.outE().count().asBool()).sack().path()
-==>[v[1],true]
-==>[v[2],false]
-==>[v[3],false]
-==>[v[4],true]
-==>[v[5],false]
-==>[v[6],true]
-----
-
-See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asBool-step[asBool()-step], link:https://issues.apache.org/jira/browse/TINKERPOP-3175[TINKERPOP-3175]
-
-==== none() and discard()
+===== none() and discard()
There is a complicated relationship with the `none()` and `discard()` steps that begs some discussion. Prior to this
version, the `none()` step was used to "throw away" all traversers that passed into it. In 3.8.0, that step has been
renamed to `discard()`. The `discard()` step with its verb tone arguably makes for a better name for that feature, but
it also helped make room for `none()` to be repurposed as `none(P)` which is a complement to `any(P)` and `all(P) steps.
-==== Prevented using cap(), inject() inside repeat()
+===== Prevented using cap(), inject() inside repeat()
`cap()` inside `repeat()` is now disallowed by the `StandardVerificationStrategy`. Using `cap()` inside `repeat()` would
have led to unexpected results since `cap()` isn't "repeat-aware". Because `cap()` is a `SupplyingBarrier` that reduces
@@ -254,7 +99,250 @@ gremlin> g.inject('x').repeat(union(constant(['a','b']).limit(1).unfold(),identi
==>x
----
-==== Simplified Comparability Semantics
+===== `aggregate()` with `Scope` Removed
+
+The meaning of `Scope` parameters in `aggregate()` have always been unique compared to all other "scopable" steps.
+`aggregate(global)` is a `Barrier`, which blocks the traversal until all traversers have been aggregated into the side
+effect, where `aggregate(local)` is non-blocking, and will allow traversers to pass before the side effect has been
+fully aggregated. This is inconsistent with the semantics of `Scope` in all other steps. For example `dedup(global)`
+filters duplicates across the entire traversal stream, while `dedup(local)` filters duplicates within individual `List`
+traversers.
+
+The `Scope` parameter is being removed from `aggregate()` to fix inconsistency between the two different use cases: flow
+control vs. per-element application. This change aligns all side effect steps (none of the others have scope arguments)
+and reserves the `Scope` parameter exclusively for "traverser-local" application patterns, eliminating confusion about
+its contextual meanings.
+
+This makes the `AggregateStep` globally scoped by default with eager aggregation. The Lazy evaluation with `aggregate()` is
+achieved by wrapping the step in `local()`.
+
+[source,text]
+----
+// 3.7.x - scope is still supported
+gremlin> g.V().aggregate(local, "x").by("age").select("x")
+==>[29]
+==>[29,27]
+==>[29,27]
+==>[29,27,32]
+==>[29,27,32]
+==>[29,27,32,35]
+
+// 3.8.0 - must use aggregate() within local() to achieve lazy aggregation
+gremlin> g.V().local(aggregate("x").by("age")).select("x")
+==>[29]
+==>[29,27]
+==>[29,27]
+==>[29,27,32]
+==>[29,27,32]
+==>[29,27,32,35]
+----
+
+An slight behavioral difference exists between the removed `aggregate(local)` and its replacement `local(aggregate())`
+with respect to handling of bulked traversers. In 3.8.0, `local()` changed from traverser-local to object-local processing,
+always debulking incoming traversers into individual objects. This causes `local(aggregate())` to show true lazy, 1 object
+at a time aggregation, differing from the original `aggregate(local)`, which always consumed bulked traversers atomically.
+There is no workaround to preserve the old "traverser-local" semantics.
+
+[source,text]
+----
+// 3.7.x - both local() and local scope will preserve bulked traversers
+gremlin> g.V().out().barrier().aggregate(local, "x").select("x")
+==>[v[3],v[3],v[3]]
+==>[v[3],v[3],v[3]]
+==>[v[3],v[3],v[3]]
+==>[v[3],v[3],v[3],v[2]]
+==>[v[3],v[3],v[3],v[2],v[4]]
+==>[v[3],v[3],v[3],v[2],v[4],v[5]]
+gremlin> g.V().out().barrier().local(aggregate("x")).select("x")
+==>[v[3],v[3],v[3]]
+==>[v[3],v[3],v[3]]
+==>[v[3],v[3],v[3]]
+==>[v[3],v[3],v[3],v[2]]
+==>[v[3],v[3],v[3],v[2],v[4]]
+==>[v[3],v[3],v[3],v[2],v[4],v[5]]
+
+// 3.8.0 - bulked traversers are now split to be processed per-object, this affects local aggregation
+gremlin> g.V().out().barrier().local(aggregate("x")).select("x")
+==>[v[3]]
+==>[v[3],v[3]]
+==>[v[3],v[3],v[3]]
+==>[v[3],v[3],v[3],v[2]]
+==>[v[3],v[3],v[3],v[2],v[4]]
+==>[v[3],v[3],v[3],v[2],v[4],v[5]]
+----
+
+See: link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-scoping-5.asciidoc[Lazy vs. Eager Evaluation]
+
+===== Removal of `store()` Step
+
+The `store()` step was a legacy name for `aggregate(local)` that has been deprecated since 3.4.3, and is now removed along
+with `aggregate(local)`. To achieve lazy aggregation, use `aggregate()` within `local()`.
+
+[source,text]
+----
+// 3.7.x - store() is still allowed
+gremlin> g.V().store("x").by("age").cap("x")
+==>[29,27,32,35]
+
+// 3.8.0 - store() removed, use local(aggregate()) to achieve lazy aggregation
+gremlin> g.V().local(aggregate("x").by("age")).cap("x")
+==>[29,27,32,35]
+----
+
+===== Removal of has(key, traversal)
+
+The has(key, traversal) API has been removed in version 3.8.0 due to its confusing behavior that differed from other
+has() variants. As well, most has(key, traversal) usage indicates a misunderstanding of the API. Unlike has(key, value)
+which performs equality comparison, has(key, traversal) only checked if the traversal produced any result, creating
+inconsistent semantics.
+
+[source,text]
+----
+// 3.7.x - this condition is meaningless but yields result because count() is productive
+gremlin> g.V().has("age", __.count())
+==>v[1]
+==>v[2]
+==>v[3]
+==>v[4]
+==>v[5]
+==>v[6]
+// simple example
+gremlin> g.V().has("age", __.is(P.gt(30)))
+==>v[4]
+==>v[6]
+
+// 3.8.0 - traversals no longer yield results, for proper use cases consider using predicate or where() for filtering
+gremlin> g.V().has("age", __.count())
+gremlin> g.V().has("age", __.is(P.gt(30)))
+gremlin> g.V().has("age", P.gt(30))
+==>v[4]
+==>v[6]
+----
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-1463[TINKERPOP-1463]
+
+===== Removal of P.getOriginalValue()
+
+`P.getOriginalValue()` has been removed as it was not offering much value and was often confused with `P.getValue()`.
+Usage of `P.getOriginalValue()` often leads to unexpected results if called on a predicate which has had its value reset
+after construction. All usages of `P.getOriginalValue()` should be replaced with `P.getValue()`.
+
+===== SeedStrategy Construction
+
+The `SeedStrategy` public constructor has been removed for Java and has been replaced by the builder pattern common
+to all strategies. This change was made to ensure that the `SeedStrategy` could be constructed consistently.
+
+===== OptionsStrategy in Python
+
+The `\\__init__()` syntax has been updated to be both more Pythonic and more aligned to the `gremlin-lang` syntax.
+Previously, `OptionsStrategy()` took a single argument `options` which was a `dict` of all options to be set.
+Now, all options should be set directly as keyword arguments.
+
+For example:
+
+[source,python]
+----
+# 3.7 and before:
+g.with_strategies(OptionsStrategy(options={'key1': 'value1', 'key2': True}))
+# 4.x and newer:
+g.with_strategies(OptionsStrategy(key1='value1', key2=True))
+
+myOptions = {'key1': 'value1', 'key2': True}
+# 3.7 and before:
+g.with_strategies(OptionsStrategy(options=myOptions))
+# 4.x and newer:
+g.with_strategies(OptionsStrategy(**myOptions))
+----
+
+===== Remove Undocumented `with()` modulation
+
+There has long been a connection between the `with()` modulator, and mutating steps due to the design of
+some of the interfaces in the gremlin traversal engine. This has led to several undocumented usages of the
+`with()` modulator which have never been officially supported but have previously been functional.
+
+As of 3.8.0 `with()` modulation of the following steps will no longer work: `addV()`, `addE()`, `property()`, `drop()`,
+`mergeV()`, and `mergeE()`.
+
+
+==== Breaking Behavioral Changes
+
+===== Serialization Changes
+
+*Properties on Element Serialization in Python & Javascript*
+
+Element properties handling has been inconsistent across GLVs. Previously,`gremlin-python` deserialized empty properties
+as None or array depending on the serializer, while `gremlin-javascript` returned properties as objects or arrays, with
+empty properties as empty lists or undefined depending on the serializer.
+
+This inconsistency is now resolved, aligning to how properties are handled in Gremlin core and in the Java GLV.
+Both GLVs will deserialize element properties into lists of property objects, returning empty lists instead of null values
+for missing properties.
+
+For python, the most notable difference is in graphSON when "tokens" is turned on for "materializeProperties". The
+properties returned are no longer `None`, but empty lists. Users should update their code accordingly.
+
+For javascript, the change is slightly more extensive, as user should no longer expect javascript objects to be returned.
+All properties are returned as lists of Property or VertexProperty objects.
+
+[source,javascript]
+----
+// 3.7 and before:
+g.with_("materializeProperties", "tokens").V(1).next() // skip properties with token
+// graphson will return properties as a javascript object, which becomes undefined
+Vertex { id: 1, label: 'person', properties: undefined }
+// graphbinary will return properties as empty lists
+Vertex { id: 1, label: 'person', properties: [] }
+
+g.V(1).next() // properties returned
+// graphson will return properties as a javascript object
+Vertex {
+ id: 1,
+ label: 'person',
+ properties: { name: [Array], age: [Array] }
+}
+// graphbinary will return properties as lists of VertexProperty objects
+Vertex {
+ id: 1,
+ label: 'person',
+ properties: [ [VertexProperty], [VertexProperty] ]
+}
+
+// 3.8.0 and newer - properties are always arrays, empty array [] for missing properties:
+g.with_("materializeProperties", "tokens").V(1).next() // skip properties with token
+// both graphson and graphbinary return
+Vertex { id: 1, label: 'person', properties: [] }
+g.V(1).next()
+// both graphson and graphbinary return
+Vertex {
+ id: 1,
+ label: 'person',
+ properties: [ [VertexProperty], [VertexProperty] ]
+}
+
+----
+
+This change only affects how GLVs deserialize property data in client applications. The underlying graph serialization
+formats and server-side behavior remain unchanged.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3186[TINKERPOP-3186]
+
+*Javascript Set Deserialization*
+
+Starting from this version, `gremlin-javascript` will deserialize `Set` data into a ECMAScript 2015 Set. Previously,
+these were deserialized into arrays.
+
+*.NET Byte Serialization Change*
+
+The Gremlin .NET serializers has been updated to correctly handle byte values as signed integers to align with the IO
+specification, whereas previously it incorrectly serialized and deserialized bytes as unsigned values.
+
+This is a breaking change for .NET applications that rely on byte values. Existing applications using byte values
+should consider switching to `sbyte` for signed byte operations or `short` for a wider range of values to maintain
+compatibility.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3161[TINKERPOP-3161]
+
+===== Simplified Comparability Semantics
The previous system of ternary boolean semantics has been replaced with simplified binary semantics. The triggers for
"ERROR" states from illegal comparisons are unchanged (typically comparisons with NaN or between incomparable types
@@ -270,11 +358,7 @@ See: link:https://tinkerpop.apache.org/docs/3.8.0/dev/provider/#gremlin-semantic
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3173[TINKERPOP-3173]
-==== Set minimum Java version to 11
-
-TinkerPop 3.8 requires a minimum of Java 11 for building and running. Support for Java 1.8 has been dropped.
-
-==== Auto-promotion of Numbers
+===== Auto-promotion of Numbers
Previously, operations like `sum` or `sack` that involved mathematical calculations did not automatically promote the
result to a larger numeric type (e.g., from int to long) when needed. As a result, values could wrap around within their
@@ -316,7 +400,7 @@ gremlin> g.inject([Float.MAX_VALUE, Float.MAX_VALUE], [Double.MAX_VALUE, Double.
See link:https://issues.apache.org/jira/browse/TINKERPOP-3115[TINKERPOP-3115]
-==== repeat() Step Global Children Semantics Change
+===== repeat() Step Global Children Semantics Change
The `repeat()` step has been updated to treat the repeat traversal as a global child in all cases. Previously, the
repeat traversal behaved as a hybrid between local and global semantics, which could lead to unexpected results in
@@ -414,7 +498,7 @@ gremlin> g.V().local(repeat(both().simplePath().order().by("name")).times(2)).pa
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3200[TINKERPOP-3200]
-==== Prefer OffsetDateTime
+===== Prefer OffsetDateTime
The default implementation for date type in Gremlin is now changed from the `java.util.Date` to the more encompassing
`java.time.OffsetDateTime`. This means the reference implementation for all date manipulation steps, `asDate()`,
@@ -439,135 +523,7 @@ For Java GLV, this change would impact users who are expecting the old `Date` ob
application, in this case the recommendation is to update code to expect `OffsetDateTime` as part of the version
upgrade.
-==== Simplify g Construction
-
-The creation of "g" is the start point to writing Gremlin. There are a number of ways to create it, but TinkerPop has
-long recommended the use of the anonymous `traversal()` function for this creation.
-
-[source,groovy]
-----
-// for embedded cases
-graph = TinkerGraph.open()
-g = traversal().withEmbedded(graph)
-// for remote cases
-g = traversal().withRemote(DriverRemoteConnection.using(...)))
-----
-
-As of this release, those two methods have been deprecated in favor of just `with()` which means you could simply write:
-
-[source,groovy]
-----
-// for embedded cases
-graph = TinkerGraph.open()
-g = traversal().with(graph)
-// for remote cases
-g = traversal().with(DriverRemoteConnection.using(...)))
-----
-
-That's a bit less to type, but also removes the need to programmatically decide which function to call, which hopefully
-strengthens the abstraction further. To demonstrate this further, consider this next example:
-
-[source,groovy]
-----
-g = traversal().with("config.properties")
-----
-
-The properties file in the above example can either point to a remote configuration or a embedded configuration allowing
-"g" to be switched as needed without code changes.
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3017[TINKERPOP-3017]
-
-==== `aggregate()` with `Scope` Removed
-
-The meaning of `Scope` parameters in `aggregate()` have always been unique compared to all other "scopable" steps.
-`aggregate(global)` is a `Barrier`, which blocks the traversal until all traversers have been aggregated into the side
-effect, where `aggregate(local)` is non-blocking, and will allow traversers to pass before the side effect has been
-fully aggregated. This is inconsistent with the semantics of `Scope` in all other steps. For example `dedup(global)`
-filters duplicates across the entire traversal stream, while `dedup(local)` filters duplicates within individual `List`
-traversers.
-
-The `Scope` parameter is being removed from `aggregate()` to fix inconsistency between the two different use cases: flow
-control vs. per-element application. This change aligns all side effect steps (none of the others have scope arguments)
-and reserves the `Scope` parameter exclusively for "traverser-local" application patterns, eliminating confusion about
-its contextual meanings.
-
-This makes the `AggregateStep` globally scoped by default with eager aggregation. The Lazy evaluation with `aggregate()` is
-achieved by wrapping the step in `local()`.
-
-[source,text]
-----
-// 3.7.x - scope is still supported
-gremlin> g.V().aggregate(local, "x").by("age").select("x")
-==>[29]
-==>[29,27]
-==>[29,27]
-==>[29,27,32]
-==>[29,27,32]
-==>[29,27,32,35]
-
-// 3.8.0 - must use aggregate() within local() to achieve lazy aggregation
-gremlin> g.V().local(aggregate("x").by("age")).select("x")
-==>[29]
-==>[29,27]
-==>[29,27]
-==>[29,27,32]
-==>[29,27,32]
-==>[29,27,32,35]
-----
-
-An slight behavioral difference exists between the removed `aggregate(local)` and its replacement `local(aggregate())`
-with respect to handling of bulked traversers. In 3.8.0, `local()` changed from traverser-local to object-local processing,
-always debulking incoming traversers into individual objects. This causes `local(aggregate())` to show true lazy, 1 object
-at a time aggregation, differing from the original `aggregate(local)`, which always consumed bulked traversers atomically.
-There is no workaround to preserve the old "traverser-local" semantics.
-
-[source,text]
-----
-// 3.7.x - both local() and local scope will preserve bulked traversers
-gremlin> g.V().out().barrier().aggregate(local, "x").select("x")
-==>[v[3],v[3],v[3]]
-==>[v[3],v[3],v[3]]
-==>[v[3],v[3],v[3]]
-==>[v[3],v[3],v[3],v[2]]
-==>[v[3],v[3],v[3],v[2],v[4]]
-==>[v[3],v[3],v[3],v[2],v[4],v[5]]
-gremlin> g.V().out().barrier().local(aggregate("x")).select("x")
-==>[v[3],v[3],v[3]]
-==>[v[3],v[3],v[3]]
-==>[v[3],v[3],v[3]]
-==>[v[3],v[3],v[3],v[2]]
-==>[v[3],v[3],v[3],v[2],v[4]]
-==>[v[3],v[3],v[3],v[2],v[4],v[5]]
-
-// 3.8.0 - bulked traversers are now split to be processed per-object, this affects local aggregation
-gremlin> g.V().out().barrier().local(aggregate("x")).select("x")
-==>[v[3]]
-==>[v[3],v[3]]
-==>[v[3],v[3],v[3]]
-==>[v[3],v[3],v[3],v[2]]
-==>[v[3],v[3],v[3],v[2],v[4]]
-==>[v[3],v[3],v[3],v[2],v[4],v[5]]
-----
-
-See: link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-scoping-5.asciidoc[Lazy vs. Eager Evaluation]
-
-==== Removal of `store()` Step
-
-The `store()` step was a legacy name for `aggregate(local)` that has been deprecated since 3.4.3, and is now removed along
-with `aggregate(local)`. To achieve lazy aggregation, use `aggregate()` within `local()`.
-
-[source,text]
-----
-// 3.7.x - store() is still allowed
-gremlin> g.V().store("x").by("age").cap("x")
-==>[29,27,32,35]
-
-// 3.8.0 - store() removed, use local(aggregate()) to achieve lazy aggregation
-gremlin> g.V().local(aggregate("x").by("age")).cap("x")
-==>[29,27,32,35]
-----
-
-==== split() on Empty String
+===== split() on Empty String
The `split()` step will now split a string into a list of its characters if the given separator is an empty string.
@@ -584,122 +540,14 @@ g.inject("Hello").split("")
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3083[TINKERPOP-3083]
-==== asString() No Longer Allow Nulls
+===== asString() No Longer Allow Nulls
The `asString()` step will no longer allow `null` input. An `IllegalArgumentException` will be thrown for consistency
with all other parsing steps (i.e. `asDate()`, `asBool()`, `asNumber()`).
See: link:https://lists.apache.org/thread/q76pgrvhprosb4lty63bnsnbw2ljyl7m[DISCUSS] thread
-==== Removal of has(key, traversal)
-
-The `has(key, traversal)` API has been removed in version 3.8.0 due to its confusing behavior that differed from other
-has() variants. As well, most `has(key, traversal)` usage indicates a misunderstanding of the API. Unlike `has(key, value)`
-which performs equality comparison, `has(key, traversal)` only checked if the traversal produced any result, creating
-inconsistent semantics.
-
-[source,text]
-----
-// 3.7.x - this condition is meaningless but yields result because count() is productive
-gremlin> g.V().has("age", __.count())
-==>v[1]
-==>v[2]
-==>v[3]
-==>v[4]
-==>v[5]
-==>v[6]
-// simple example
-gremlin> g.V().has("age", __.is(P.gt(30)))
-==>v[4]
-==>v[6]
-
-// 3.8.0 - traversals no longer yield results, for proper use cases consider using predicate or where() for filtering
-gremlin> g.V().has("age", __.count())
-gremlin> g.V().has("age", __.is(P.gt(30)))
-gremlin> g.V().has("age", P.gt(30))
-==>v[4]
-==>v[6]
-----
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-1463[TINKERPOP-1463]
-
-==== Serialization Changes
-
-*Properties on Element Serialization in Python & Javascript*
-
-Element properties handling has been inconsistent across GLVs. Previously,`gremlin-python` deserialized empty properties
-as None or array depending on the serializer, while `gremlin-javascript` returned properties as objects or arrays, with
-empty properties as empty lists or undefined depending on the serializer.
-
-This inconsistency is now resolved, aligning to how properties are handled in Gremlin core and in the Java GLV.
-Both GLVs will deserialize element properties into lists of property objects, returning empty lists instead of null values
-for missing properties.
-
-For python, the most notable difference is in graphSON when "tokens" is turned on for "materializeProperties". The
-properties returned are no longer `None`, but empty lists. Users should update their code accordingly.
-
-For javascript, the change is slightly more extensive, as user should no longer expect javascript objects to be returned.
-All properties are returned as lists of Property or VertexProperty objects.
-
-[source,javascript]
-----
-// 3.7 and before:
-g.with_("materializeProperties", "tokens").V(1).next() // skip properties with token
-// graphson will return properties as a javascript object, which becomes undefined
-Vertex { id: 1, label: 'person', properties: undefined }
-// graphbinary will return properties as empty lists
-Vertex { id: 1, label: 'person', properties: [] }
-
-g.V(1).next() // properties returned
-// graphson will return properties as a javascript object
-Vertex {
- id: 1,
- label: 'person',
- properties: { name: [Array], age: [Array] }
-}
-// graphbinary will return properties as lists of VertexProperty objects
-Vertex {
- id: 1,
- label: 'person',
- properties: [ [VertexProperty], [VertexProperty] ]
-}
-
-// 3.8.0 and newer - properties are always arrays, empty array [] for missing properties:
-g.with_("materializeProperties", "tokens").V(1).next() // skip properties with token
-// both graphson and graphbinary return
-Vertex { id: 1, label: 'person', properties: [] }
-g.V(1).next()
-// both graphson and graphbinary return
-Vertex {
- id: 1,
- label: 'person',
- properties: [ [VertexProperty], [VertexProperty] ]
-}
-
-----
-
-This change only affects how GLVs deserialize property data in client applications. The underlying graph serialization
-formats and server-side behavior remain unchanged.
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3186[TINKERPOP-3186]
-
-*Javascript Set Deserialization*
-
-Starting from this version, `gremlin-javascript` will deserialize `Set` data into a ECMAScript 2015 Set. Previously,
-these were deserialized into arrays.
-
-*.NET Byte Serialization Change*
-
-The Gremlin .NET serializers has been updated to correctly handle byte values as signed integers to align with the IO
-specification, whereas previously it incorrectly serialized and deserialized bytes as unsigned values.
-
-This is a breaking change for .NET applications that rely on byte values. Existing applications using byte values
-should consider switching to `sbyte` for signed byte operations or `short` for a wider range of values to maintain
-compatibility.
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3161[TINKERPOP-3161]
-
-==== Split bulked traversers for `local()`
+===== Split bulked traversers for `local()`
Prior to 3.8.0, local() exhibited "traverser-local" semantics, where the local traversal would apply independently to
each individual bulkable `Traverser`. This often led to confusion, especially in the presence of reducing barrier steps, as
@@ -727,13 +575,7 @@ gremlin> g.V().out().barrier().local(count())
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3196[TINKERPOP-3196]
-==== Removal of P.getOriginalValue()
-
-`P.getOriginalValue()` has been removed as it was not offering much value and was often confused with `P.getValue()`.
-Usage of `P.getOriginalValue()` often leads to unexpected results if called on a predicate which has had its value reset
-after construction. All usages of `P.getOriginalValue()` should be replaced with `P.getValue()`.
-
-==== Gremlin Grammar Changes
+===== Gremlin Grammar Changes
A number of changes have been introduced to the Gremlin grammar to help make it be more consistent and easier to use.
@@ -882,12 +724,7 @@ link:https://issues.apache.org/jira/browse/TINKERPOP-3046[TINKERPOP-3046],
link:https://issues.apache.org/jira/browse/TINKERPOP-3047[TINKERPOP-3047],
link:https://issues.apache.org/jira/browse/TINKERPOP-3023[TINKERPOP-3023]
-==== SeedStrategy Construction
-
-The `SeedStrategy` public constructor has been removed for Java and has been replaced by the builder pattern common
-to all strategies. This change was made to ensure that the `SeedStrategy` could be constructed consistently.
-
-==== Improved Translators
+===== Improved Translators
The various Java `Translator` implementations allowing conversion of Gremlin traversals to string forms in various
languages have been modified considerably. First, they have been moved from to the
@@ -908,41 +745,54 @@ gremlin> GremlinTranslator.translate("g.V().out('knows')", Translator.GO)
==>g.V().Out("knows")
----
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3028[TINKERPOP-3028]
-
-==== Deprecated UnifiedChannelizer
-
-The `UnifiedChannelizer` was added in 3.5.0 in any attempt to streamline Gremlin Server code paths and resource usage.
-It was offered as an experimental feature and as releases went on was not further developed, particularly because of the
-major changes to Gremlin Server expected in 4.0.0 when websockets are removed. The removal of websockets with a pure
-reliance on HTTP will help do what the `UnifiedChannelizer` tried to do with its changes. As a result, there is no need
-to continue to refine this `Channelizer` implementation and it can be deprecated.
-
-See: link:https://issues.apache.org/jira/browse/TINKERPOP-3168[TINKERPOP-3168]
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3028[TINKERPOP-3028]
-==== OptionsStrategy in Python
+===== Modified limit() skip() range() Semantics in repeat()
-The `\\__init__()` syntax has been updated to be both more Pythonic and more aligned to the `gremlin-lang` syntax.
-Previously, `OptionsStrategy()` took a single argument `options` which was a `dict` of all options to be set.
-Now, all options should be set directly as keyword arguments.
+The semantics of `limit()`, `skip()`, and `range()` steps called with default `Scope` or explicit `Scope.global` inside
+`repeat()` have been modified to ensure consistent semantics across repeat iterations. Previously, these steps would
+track global state across iterations, leading to unexpected filtering behavior between loops.
-For example:
+Consider the following examples which demonstrate the unexpected behavior. Note that the examples for version 3.7.4
+disable the `RepeatUnrollStrategy` so that strategy optimization does not replace the `repeat()` traversal with a
+non-looping equivalent. 3.8.0 examples do not disable the `RepeatUnrollStrategy` as the strategy was modified to be more
+restrictive in this version.
-[source,python]
+[source,groovy]
----
-# 3.7 and before:
-g.with_strategies(OptionsStrategy(options={'key1': 'value1', 'key2': True}))
-# 4.x and newer:
-g.with_strategies(OptionsStrategy(key1='value1', key2=True))
+// 3.7.4 - grateful dead graph examples producing no results due to global counters
+gremlin> g.withoutStrategies(RepeatUnrollStrategy).V().has('name','JAM').repeat(out('followedBy').limit(2)).times(2).values('name')
+gremlin>
+gremlin> g.withoutStrategies(RepeatUnrollStrategy).V().has('name','DRUMS').repeat(__.in('followedBy').range(1,3)).times(2).values('name')
+gremlin>
+// 3.7.4 - modern graph examples demonstrating too many results with skip in repeat due to global counters
+gremlin> g.withoutStrategies(RepeatUnrollStrategy).V(1).repeat(out().skip(1)).times(2).values('name')
+==>ripple
+==>lop
+gremlin> g.withoutStrategies(RepeatUnrollStrategy).V(1).out().skip(1).out().skip(1).values('name')
+==>lop
-myOptions = {'key1': 'value1', 'key2': True}
-# 3.7 and before:
-g.with_strategies(OptionsStrategy(options=myOptions))
-# 4.x and newer:
-g.with_strategies(OptionsStrategy(**myOptions))
+// 3.8.0 - grateful dead graph examples producing results as limit counters tracked per iteration
+gremlin> g.V().has('name','JAM').repeat(out('followedBy').limit(2)).times(2).values('name')
+==>HURTS ME TOO
+==>BLACK THROATED WIND
+gremlin> g.V().has('name','DRUMS').repeat(__.in('followedBy').range(1,3)).times(2).values('name')
+==>DEAL
+==>WOMEN ARE SMARTER
+// 3.8.0 - modern graph examples demonstrating consistent skip semantics
+gremlin> g.V(1).repeat(out().skip(1)).times(2).values('name')
+==>lop
+gremlin> g.V(1).out().skip(1).out().skip(1).values('name')
+==>lop
----
-==== Add barrier to most SideEffect steps
+This change ensures that `limit()`, `skip()`, and `range()` steps called with default `Scope` or explicit `Scope.global`
+inside `repeat()` are more consistent with manually unrolled traversals. Before upgrading, users should determine if any
+traversals use `limit()`, skip()`, or `range()` with default `Scope` or explicit `Scope.global` inside `repeat()`. If it
+is desired that the limit or range should apply across all loops then the `limit()`, `skip()`, or `range()` step should
+be moved out of the `repeat()` step.
+
+===== Add barrier to most SideEffect steps
Prior to 3.8.0, the `group(String)`, `groupCount(String)`, `tree(String)` and `subgraph(String)` steps were non-blocking,
in that they allowed traversers to pass through without fully iterating the traversal and fully computing the side
@@ -985,7 +835,7 @@ gremlin> g.V().local(groupCount("x")).select("x")
==>[v[1]:1,v[2]:1,v[3]:1,v[4]:1,v[5]:1,v[6]:1]
----
-==== choose() Semantics
+===== choose() Semantics
Several enhancements and clarifications have been made to the `choose()` step in TinkerPop 3.8.0 to improve its behavior
and make it more consistent:
@@ -1117,13 +967,13 @@ gremlin> g.V().hasLabel("person").union(
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3178[TINKERPOP-3178],
link:https://tinkerpop.apache.org/docs/3.8.0/reference/#choose-step[Reference Documentation - choose()]
-==== Float Defaults to Double
+===== Float Defaults to Double
The `GremlinLangScriptEngine` has been modified to treat float literals without explicit type suffixes (like 'm', 'f',
or 'd') as Double by default. Users who need `BigDecimal` precision can still use the 'm' suffix (e.g., 1.0m).
`GremlinGroovyScriptEngine` will still default to `BigDecimal` for `float` literals.
-==== Consistent Output for range(), limit(), tail()
+===== Consistent Output for range(), limit(), tail()
The `range(local)`, `limit(local)`, and `tail(local)` steps now consistently return collections rather than automatically
unfolding single-element results when operating on iterable collections (List, Set, etc.). Previously, when these steps
@@ -1177,7 +1027,7 @@ This change affects all three local collection manipulation steps when operating
See: link:https://issues.apache.org/jira/browse/TINKERPOP-2491[TINKERPOP-2491]
-==== group() Value Traversal Semantics
+===== group() Value Traversal Semantics
The `group()` step takes two `by()` modulators. The first defines the key for the grouping, and the second acts upon the
values grouped to each key. The latter is referred to as the "value traversal". In earlier versions of TinkerPop,
@@ -1207,7 +1057,7 @@ gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__
See: link:https://issues.apache.org/jira/browse/TINKERPOP-2971[TINKERPOP-2971]
-==== By Modulation Semantics
+===== By Modulation Semantics
*valueMap() and propertyMap() Semantics*
@@ -1223,16 +1073,7 @@ last one, which was not intuitive.
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3121[TINKERPOP-3121],
link:https://issues.apache.org/jira/browse/TINKERPOP-2974[TINKERPOP-2974]
-==== Remove Undocumented `with()` modulation
-
-There has long been a connection between the `with()` modulator, and mutating steps due to the design of
-some of the interfaces in the gremlin traversal engine. This has led to several undocumented usages of the
-`with()` modulator which have never been officially supported but have previously been functional.
-
-As of 3.8.0 `with()` modulation of the following steps will no longer work: `addV()`, `addE()`, `property()`, `drop()`,
-`mergeV()`, and `mergeE()`.
-
-==== Stricter RepeatUnrollStrategy
+===== Stricter RepeatUnrollStrategy
The `RepeatUnrollStrategy` has been updated to use a more conservative approach for determining which repeat traversals
are safe to unroll. Previously, the strategy would attempt to unroll most usages of `repeat()` used with `times()`
@@ -1258,7 +1099,7 @@ g.V().repeat(both().simplePath()).times(4)
g.V().repeat(both().sample(1)).times(2)
----
-===== Migration Strategies
+====== Migration Strategies
Before upgrading, analyze existing traversals which use `repeat()` with any steps other than `out()`, `in()`, `both()`,
`inV()`, `outV()`, `otherV()`, `has(key, value)` and determine if the semantics of these traversals are as expected when
@@ -1280,50 +1121,183 @@ g.V().repeat(both()).times(2).dedup()
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3192[TINKERPOP-3192]
-==== Modified limit() skip() range() Semantics in repeat()
+==== Other Breaking Changes
-The semantics of `limit()`, `skip()`, and `range()` steps called with default `Scope` or explicit `Scope.global` inside
-`repeat()` have been modified to ensure consistent semantics across repeat iterations. Previously, these steps would
-track global state across iterations, leading to unexpected filtering behavior between loops.
+===== Set minimum Java version to 11
-Consider the following examples which demonstrate the unexpected behavior. Note that the examples for version 3.7.4
-disable the `RepeatUnrollStrategy` so that strategy optimization does not replace the `repeat()` traversal with a
-non-looping equivalent. 3.8.0 examples do not disable the `RepeatUnrollStrategy` as the strategy was modified to be more
-restrictive in this version.
+TinkerPop 3.8 requires a minimum of Java 11 for building and running. Support for Java 1.8 has been dropped.
-[source,groovy]
+==== Additions and New Features
+
+===== Gremlin MCP Server
+
+Gremlin MCP Server is an experimental application that implements the link:https://modelcontextprotocol.io/[Model Context Protocol]
+(MCP) to expose Gremlin Server-backed graph operations to MCP-capable clients such as Claude Desktop, Cursor, or
+Windsurf. Through this integration, graph structure can be discovered, and Gremlin traversals can be executed. Basic
+health checks are included to validate connectivity.
+
+A running Gremlin Server that fronts the target TinkerPop graph is required. An MCP client can be configured to connect
+to the Gremlin MCP Server endpoint.
+
+===== Air Routes Dataset
+
+The Air Routes sample dataset has long been used to help showcase and teach Gremlin. Popularized by the first edition
+of link:https://kelvinlawrence.net/book/PracticalGremlin.html[Practical Gremlin], this dataset offers a real-world graph
+structure that allows for practical demonstration of virtually every feature that Gremlin syntax has to offer. While it
+was easy to simply get the dataset from the Practical Gremlin link:https://github.com/krlawrence/graph[repository],
+including it with the TinkerPop distribution makes it much more convenient to use with Gremlin Server, Gremlin Console,
+or directly in code that depends on the `tinkergraph-gremlin` package.
+
+[source,text]
+----
+plugin activated: tinkerpop.tinkergraph
+gremlin> graph = TinkerFactory.createAirRoutes()
+==>tinkergraph[vertices:3619 edges:50148]
+gremlin> g = traversal().with(graph)
+==>graphtraversalsource[tinkergraph[vertices:3619 edges:50148], standard]
+gremlin> g.V().has('airport','code','IAD').valueMap('code','desc','lon','lat')
+==>[code:[IAD],lon:[-77.45580292],lat:[38.94449997],desc:[Washington Dulles International Airport]]
----
-// 3.7.4 - grateful dead graph examples producing no results due to global counters
-gremlin> g.withoutStrategies(RepeatUnrollStrategy).V().has('name','JAM').repeat(out('followedBy').limit(2)).times(2).values('name')
-gremlin>
-gremlin> g.withoutStrategies(RepeatUnrollStrategy).V().has('name','DRUMS').repeat(__.in('followedBy').range(1,3)).times(2).values('name')
-gremlin>
-// 3.7.4 - modern graph examples demonstrating too many results with skip in repeat due to global counters
-gremlin> g.withoutStrategies(RepeatUnrollStrategy).V(1).repeat(out().skip(1)).times(2).values('name')
-==>ripple
-==>lop
-gremlin> g.withoutStrategies(RepeatUnrollStrategy).V(1).out().skip(1).out().skip(1).values('name')
-==>lop
-// 3.8.0 - grateful dead graph examples producing results as limit counters tracked per iteration
-gremlin> g.V().has('name','JAM').repeat(out('followedBy').limit(2)).times(2).values('name')
-==>HURTS ME TOO
-==>BLACK THROATED WIND
-gremlin> g.V().has('name','DRUMS').repeat(__.in('followedBy').range(1,3)).times(2).values('name')
-==>DEAL
-==>WOMEN ARE SMARTER
-// 3.8.0 - modern graph examples demonstrating consistent skip semantics
-gremlin> g.V(1).repeat(out().skip(1)).times(2).values('name')
-==>lop
-gremlin> g.V(1).out().skip(1).out().skip(1).values('name')
-==>lop
+TinkerPop distributes the 1.0 version of the dataset.
+
+===== Type Predicate
+
+The new `P.typeOf()` predicate allows filtering traversers based on their runtime type. It accepts either a `GType`
+enum constant or a string representation of a simple class name. This predicate is particularly useful for type-safe
+filtering in heterogeneous data scenarios.
+
+[source,text]
----
+// Filter vertices by property type
+gremlin> g.V().values("age","name").is(P.typeOf(GType.INT))
+==>29
+==>27
+==>32
+==>35
-This change ensures that `limit()`, `skip()`, and `range()` steps called with default `Scope` or explicit `Scope.global`
-inside `repeat()` are more consistent with manually unrolled traversals. Before upgrading, users should determine if any
-traversals use `limit()`, skip()`, or `range()` with default `Scope` or explicit `Scope.global` inside `repeat()`. If it
-is desired that the limit or range should apply across all loops then the `limit()`, `skip()`, or `range()` step should
-be moved out of the `repeat()` step.
+// Type inheritance support - NUMBER matches all numeric types
+gremlin> g.union(V(), E()).values().is(P.typeOf(GType.NUMBER))
+==>29
+==>27
+==>32
+==>35
+==>0.5
+==>1.0
+==>0.4
+==>1.0
+==>0.4
+==>0.2
+----
+
+The predicate supports type inheritance where `GType.NUMBER` will match any numeric type. Invalid type names will
+throw an exception at execution time.
+
+See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#a-note-on-predicates[Predicates], link:https://issues.apache.org/jira/browse/TINKERPOP-2234[TINKERPOP-2234]
+
+===== Number Conversion Step
+
+The new `asNumber()` step provides type casting functionality to Gremlin. It serves as an umbrella step that parses
+strings and casts numbers into desired types. For the convenience of remote traversals in GLVs, these available types
+are denoted by a set of number tokens (`GType`).
+
+This new step will allow users to normalize their data by converting string numbers and mixed numeric types to a
+consistent format, making it easier to perform downstream mathematical operations. As an example:
+
+[source,text]
+----
+// sum() step can only take numbers
+gremlin> g.inject(1.0, 2l, 3, "4", "0x5").sum()
+class java.lang.String cannot be cast to class java.lang.Number
+
+// use asNumber() to avoid casting exceptions
+gremlin> g.inject(1.0, 2l, 3, "4", "0x5").asNumber().sum()
+==>15.0
+
+// given sum() step returned a double, one can use asNumber() to further cast the result into desired type
+gremlin> g.inject(1.0, 2l, 3, "4", "0x5").asNumber().sum().asNumber(GType.INT)
+==>15
+----
+
+Semantically, the `asNumber()` step will convert the incoming traverser to a logical parsable type if no argument is
+provided, or to the desired numerical type, based on the number token (`GType`) provided.
+
+Numerical input will pass through unless a type is specified by the number token. `ArithmeticException` will be thrown
+for any overflow as a result of narrowing of types:
+
+[source,text]
+----
+gremlin> g.inject(5.0).asNumber(GType.INT)
+==> 5 // casts double to int
+gremlin> g.inject(12).asNumber(GType.BYTE)
+==> 12
+gremlin> g.inject(128).asNumber(GType.BYTE)
+==> ArithmeticException
+----
+
+String input will be parsed. By default, the smalled unit of number to be parsed into is `int` if no type token is
+provided. `NumberFormatException` will be thrown for any unparsable strings:
+
+[source,text]
+----
+gremlin> g.inject("5").asNumber()
+==> 5
+gremlin> g.inject("5.7").asNumber(GType.INT)
+==> 5
+gremlin> g.inject("1,000").asNumber(GType.INT)
+==> NumberFormatException
+gremlin> g.inject("128").asNumber(GType.BYTE)
+==> ArithmeticException
+----
+
+All other input types will result in `IllegalArgumentException`:
+[source,text]
+----
+gremlin> g.inject([1, 2, 3, 4]).asNumber()
+==> IllegalArgumentException
+----
+
+See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asNumber-step[asNumber()-step], link:https://issues.apache.org/jira/browse/TINKERPOP-3166[TINKERPOP-3166]
+
+===== Boolean Conversion Step
+
+The `asBool()` step bridges another gap in Gremlin's casting functionalities. Users now have the ability to parse
+strings and numbers into boolean values, both for normalization and to perform boolean logic with numerical values.
+
+[source,text]
+----
+gremlin> g.inject(2, "true", 1, 0, false, "FALSE").asBool().fold()
+==>[true,true,true,false,false,false]
+
+// using the modern graph, we can turn count() results into boolean values
+gremlin> g.V().local(outE().count()).fold()
+==>[3,0,0,2,0,1]
+gremlin> g.V().local(outE().count()).asBool().fold()
+==>[true,false,false,true,false,true]
+// a slightly more complex one using sack for boolean operations for vertices with both 'person' label and has out edges
+gremlin> g.V().sack(assign).by(__.hasLabel('person').count().asBool()).sack(and).by(__.outE().count().asBool()).sack().path()
+==>[v[1],true]
+==>[v[2],false]
+==>[v[3],false]
+==>[v[4],true]
+==>[v[5],false]
+==>[v[6],true]
+----
+
+See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asBool-step[asBool()-step],
+link:https://issues.apache.org/jira/browse/TINKERPOP-3175[TINKERPOP-3175]
+
+==== Deprecations
+
+===== Deprecated UnifiedChannelizer
+
+The `UnifiedChannelizer` was added in 3.5.0 in any attempt to streamline Gremlin Server code paths and resource usage.
+It was offered as an experimental feature and as releases went on was not further developed, particularly because of the
+major changes to Gremlin Server expected in 4.0.0 when websockets are removed. The removal of websockets with a pure
+reliance on HTTP will help do what the `UnifiedChannelizer` tried to do with its changes. As a result, there is no need
+to continue to refine this `Channelizer` implementation and it can be deprecated.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3168[TINKERPOP-3168]
=== Upgrading for Providers