You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2026/06/30 19:16:12 UTC
(tinkerpop) 02/06: Add by() modulation to path() in gremlin-javascript Tiny Gremlin
This is an automated email from the ASF dual-hosted git repository.
spmallette pushed a commit to branch jsprocess
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit c7392acd0ff0c87de250bb40f97895f71c2cac81
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Fri Jun 26 15:38:43 2026 -0400
Add by() modulation to path() in gremlin-javascript Tiny Gremlin
path() now projects each path element via by(). Supported forms: by(String),
by(T), and by(Traversal) restricted to a single value-extraction step (id,
label, key, value, values, valueMap, elementMap; values takes the first item).
Multiple by() modulators apply round-robin across path elements. A non-productive
by() filters the path traverser, matching default Gremlin semantics
(ProductiveByStrategy is not supported).
- LocalExecutor: fold consecutive by() into path(); applyPath runs the single
value-extraction sub-step per element; reject by(Traversal) on order()
- steps.ts: round-robin projection in stepPath with NON_PRODUCTIVE filtering
- ExecuteVisitor: parse the by(Traversal) sub-pipeline (was a discarded placeholder)
- local-graph-connection: drop the erroneous override on commit()/rollback(), which
are not part of the RemoteConnection abstract contract, so the test suite compiles
Tests:
- Path.feature: tag three existing path().by() scenarios @TinyGremlin and add
by(T.label) and by(__.values("name")) scenarios
- new unit test for by(T), by(Traversal) and the single-step restriction error
- new integration test exercising the primary use case: subgraph() from a remote
graph then local Tiny Gremlin traversal, plus JavaScript closure post-processing
(map/filter/flat/reduce/forEach) for steps outside the subset
Regenerates the cross-language feature test code from the corpus, which also catches
up generated code left stale by the prior Tiny Gremlin commit (AddVertex scenarios).
Assisted-by: Claude Code:claude-opus-4-8
---
docs/src/dev/provider/tiny-gremlin.asciidoc | 16 +-
.../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 4 +
gremlin-go/driver/cucumber/gremlin.go | 4 +
.../lib/driver/local-graph-connection.ts | 8 +-
.../lib/language/executor/ExecuteVisitor.ts | 12 +-
.../lib/process/local/LocalExecutor.ts | 58 +++++-
.../gremlin-javascript/lib/process/local/steps.ts | 44 ++++-
.../gremlin-javascript/test/cucumber/gremlin.js | 6 +-
.../integration/tiny-gremlin-subgraph-tests.js | 218 +++++++++++++++++++++
.../test/unit/tiny-gremlin-path-by-test.js | 75 +++++++
.../src/main/python/tests/feature/gremlin.py | 4 +
.../gremlin/language/translator/translations.json | 128 ++++++++++++
.../gremlin/test/features/map/Path.feature | 36 +++-
13 files changed, 598 insertions(+), 15 deletions(-)
diff --git a/docs/src/dev/provider/tiny-gremlin.asciidoc b/docs/src/dev/provider/tiny-gremlin.asciidoc
index 2dd151598a..bffd26056e 100644
--- a/docs/src/dev/provider/tiny-gremlin.asciidoc
+++ b/docs/src/dev/provider/tiny-gremlin.asciidoc
@@ -304,9 +304,23 @@ Maps an edge to its outgoing vertex (the tail of the arrow).
Maps the current traverser to the `Path` of elements it has visited.
+`by()` modulation is supported to project each element in the path. Multiple `by()` modulators are applied
+round-robin across the path elements (the first element uses the first `by()`, the second the second, and so on,
+cycling back to the first). The following forms are supported:
+
+* `by(String)` — project the named property value (the first value for multi-properties).
+* `by(T)` — project `T.id` or `T.label`.
+* `by()` — natural identity (the raw element), equivalent to omitting `by()` for that position.
+* `by(Traversal)` — restricted to a *single value-extraction step*: `id()`, `label()`, `key()`, `value()`,
+ `values()`, `valueMap()`, or `elementMap()`. `values()` projects the first value. Any other traversal shape
+ raises an error.
+
+A non-productive `by()` (for example, `by("age")` on an element without an `age` property) filters the whole
+path traverser, matching default Gremlin semantics. `ProductiveByStrategy` is not supported, so there is no
+null-producing variant.
+
*Tiny Gremlin limitations:*
-* `by()` modulation is not supported. Path objects always contain the raw elements visited.
* `from()` and `to()` selection within a path is not supported because `as()` step labels are excluded from Tiny
Gremlin.
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index 0d610f47d9..8424c6aba5 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -1033,6 +1033,8 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_addV_propertyXset_emptyX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "foo").Property(Cardinality.Set, new Dictionary<object, object> {}), (g,p) =>g.V().HasLabel("person").Values<object>()}},
{"g_addVXpersonX_propertyXname_joshX_propertyXage_nullX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "josh").Property("age", null), (g,p) =>g.V().Has("person", "age", (object) null)}},
{"g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("friendWeight", null, "acl", null), (g,p) =>g.V().Has("person", "name", "marko").Has("friendWeight", (object) null), (g,p) =>g.V().Has("person", "name", "marko").Properties<object>("friendWeight").Has("acl", (object) null), (g,p) =>g.V().Has("person", [...]
+ {"g_V_hasXperson_name_aliceX_propertyXage_51X", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "alice").Property("age", 50), (g,p) =>g.V().Has("person", "name", "alice").Property("age", 51), (g,p) =>g.V().Has("person", "age", 50), (g,p) =>g.V().Has("person", "age", 51)}},
+ {"g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "alice").Property("age", 30), (g,p) =>g.V().Has("person", "name", "alice"), (g,p) =>g.V().Has("person", "age", 30)}},
{"g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "alice").Property(Cardinality.Single, "age", 50), (g,p) =>g.V().Has("person", "name", "alice").Property("age", __.Union<object>(__.Values<object>("age"), __.Constant<object>(1)).Sum<object>()), (g,p) =>g.V().Has("person", "age", 50), (g,p) =>g.V().Has("person", "age", [...]
{"g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).As("vadas").AddV((string) "software").Property("name", "lop").Property("lang", "java").As("lop").AddV((string) "pers [...]
{"g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV().Property("name", "marko").With("key", "value").Values<object>("name", "key")}},
@@ -1666,6 +1668,8 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_VX1X_outEXcreatedX_inV_inE_outV_path", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).OutE("created").InV().InE().OutV().Path()}},
{"g_V_asXaX_out_asXbX_out_asXcX_path_fromXbX_toXcX_byXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().As("a").Out().As("b").Out().As("c").Path().From("b").To("c").By("name")}},
{"g_VX1X_out_path_byXageX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out().Path().By("age")}},
+ {"g_VX1X_out_path_byXlabelX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out().Path().By(T.Label)}},
+ {"g_VX1X_out_path_byXvaluesXnameXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out().Path().By(__.Values<object>("name"))}},
{"g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new ProductiveByStrategy()).V(p["vid1"]).Out().Path().By("age")}},
{"g_injectX1_null_nullX_path", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(1, null, null).Path()}},
{"g_injectX1_null_nullX_path_dedup", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(1, null, null).Path().Dedup()}},
diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go
index 70822b8eba..253337d7c5 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -1003,6 +1003,8 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
"g_addV_propertyXset_emptyX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("foo").Property(gremlingo.Cardinality.Set, map[interface{}]interface{}{ })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").Values()}},
"g_addVXpersonX_propertyXname_joshX_propertyXage_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "josh").Property("age", nil)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "age", nil)}},
"g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko").Property("friendWeight", nil, "acl", nil)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "marko").Has("friendWeight", nil)}, func(g *gremlingo.GraphTraversalSource, p map[string]interfa [...]
+ "g_V_hasXperson_name_aliceX_propertyXage_51X": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "alice").Property("age", 50)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "alice").Property("age", 51)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("perso [...]
+ "g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "alice").Property("age", 30)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "alice")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", " [...]
"g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "alice").Property(gremlingo.Cardinality.Single, "age", 50)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "alice").Property("age", gremlingo.T__.Union(gremlingo.T__.Values("age"), gremlingo.T_ [...]
"g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko").Property("age", 29).As("marko").AddV("person").Property("name", "vadas").Property("age", 27).As("vadas").AddV("software").Property("name", "lop").Property("lang", "java").As("lop").AddV("person").Property("name", "josh").Prop [...]
"g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV().Property("name", "marko").With("key", "value").Values("name", "key")}},
@@ -1636,6 +1638,8 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
"g_VX1X_outEXcreatedX_inV_inE_outV_path": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).OutE("created").InV().InE().OutV().Path()}},
"g_V_asXaX_out_asXbX_out_asXcX_path_fromXbX_toXcX_byXnameX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().As("a").Out().As("b").Out().As("c").Path().From("b").To("c").By("name")}},
"g_VX1X_out_path_byXageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Out().Path().By("age")}},
+ "g_VX1X_out_path_byXlabelX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Out().Path().By(gremlingo.T.Label)}},
+ "g_VX1X_out_path_byXvaluesXnameXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Out().Path().By(gremlingo.T__.Values("name"))}},
"g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.ProductiveByStrategy()).V(p["vid1"]).Out().Path().By("age")}},
"g_injectX1_null_nullX_path": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(1, nil, nil).Path()}},
"g_injectX1_null_nullX_path_dedup": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(1, nil, nil).Path().Dedup()}},
diff --git a/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts b/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts
index 0828caf8f9..2601e4eff6 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts
@@ -78,11 +78,13 @@ export class LocalGraphConnection extends RemoteConnection {
return new RemoteTraversal(generator);
}
- override async commit(): Promise<void> {
- // no-op: local graph mutations are in-memory only
+ // commit()/rollback() are not part of the RemoteConnection abstract contract, so they
+ // are plain methods (no 'override'); local graph mutations are in-memory only.
+ async commit(): Promise<void> {
+ // no-op
}
- override async rollback(): Promise<void> {
+ async rollback(): Promise<void> {
// no-op
}
diff --git a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
index 99957284f2..37c5808196 100644
--- a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
@@ -449,8 +449,16 @@ export class ExecuteVisitor {
this.push('by', [null, dir]);
}
- visitTraversalMethod_by_Traversal(_ctx: any): void { this.push('by(Traversal)', []); }
- visitTraversalMethod_by_Traversal_Comparator(_ctx: any): void { this.push('by(Traversal)', []); }
+ // by(Traversal) — emit the parsed sub-pipeline; the local executor restricts it to a
+ // single value-extraction step (e.g. elementMap()) when used as a path() projection.
+ visitTraversalMethod_by_Traversal(ctx: any): void {
+ this.push('by', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+ }
+
+ // by(Traversal, Comparator) — Order is irrelevant for path() projection and is dropped.
+ visitTraversalMethod_by_Traversal_Comparator(ctx: any): void {
+ this.push('by', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+ }
/** by(T, Order) — T.id, T.label etc. with a direction, routed through traversalFunction */
visitTraversalMethod_by_Function(ctx: any): void {
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
index ee4c04dc63..059b1ddc17 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
@@ -22,7 +22,7 @@ import { defaultPasses, type OptimizationPass } from './passes.js';
import { LocalExecutionError } from './LocalExecutionError.js';
import { Graph, Vertex, Edge } from '../../structure/graph.js';
import {
- wrap, getValue, type StreamItem,
+ wrap, getValue, NON_PRODUCTIVE, type StreamItem,
stepOut, stepIn, stepBoth, stepOutE, stepInE, stepBothE, stepOutV, stepInV, stepOtherV,
stepHas, stepHasId, stepHasLabel, stepHasNot,
stepId, stepLabel, stepValue, stepKey, stepValues, stepValueMap, stepElementMap,
@@ -33,6 +33,11 @@ import {
type StepFn = (source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean) => Generator<StreamItem>;
+/** Steps allowed inside a by(Traversal) projection on path() — single value extraction only. */
+const VALUE_EXTRACTION_STEPS = new Set<string>([
+ 'id', 'label', 'key', 'value', 'values', 'valueMap', 'elementMap',
+]);
+
/**
* Executes a Tiny Gremlin Pipeline against a local in-memory Graph, returning
* an AsyncGenerator that yields one result element at a time.
@@ -76,12 +81,37 @@ export class LocalExecutor {
// Fold a single following by() modulator into the order step args
const next = pipeline[i + 1];
if (next?.name === 'by') {
+ if (Array.isArray(next.args[0])) {
+ throw new LocalExecutionError('by(Traversal) is not supported for order() in Tiny Gremlin');
+ }
result.push({ name: 'order', args: next.args }); // args = [key, direction]
i += 2;
} else {
result.push(step);
i++;
}
+ } else if (step.name === 'path') {
+ // Fold all following by() modulators into the path step as a list of projections.
+ // Each projection is a key (String/T/null) or a single-step value-extraction
+ // sub-pipeline (by(Traversal)); they are applied round-robin across path elements.
+ const projections: Arg[] = [];
+ let j = i + 1;
+ while (j < pipeline.length && pipeline[j].name === 'by') {
+ const proj = pipeline[j].args[0];
+ if (Array.isArray(proj)) {
+ const sub = proj as unknown as Pipeline;
+ if (sub.length !== 1 || !VALUE_EXTRACTION_STEPS.has(sub[0].name)) {
+ throw new LocalExecutionError(
+ 'Tiny Gremlin only supports by(Traversal) on path() with a single value-extraction ' +
+ 'step (id, label, key, value, values, valueMap, elementMap)',
+ );
+ }
+ }
+ projections.push(proj);
+ j++;
+ }
+ result.push({ name: 'path', args: projections });
+ i = j;
} else {
result.push(step);
i++;
@@ -115,11 +145,35 @@ export class LocalExecutor {
private applyStep(step: StepDescriptor, source: Iterable<StreamItem>, graph: Graph, trackPaths: boolean): Iterable<StreamItem> {
if (step.name === 'addE') return this.applyAddE(source, step, graph, trackPaths);
+ if (step.name === 'path') return this.applyPath(source, step, graph, trackPaths);
const fn = STEP_REGISTRY.get(step.name);
if (!fn) throw new LocalExecutionError(`No implementation for step '${step.name}'`);
return fn(source, step.args, graph, trackPaths);
}
+ /**
+ * Executes path() with its folded by() projections. Provides stepPath a callback that
+ * runs a single value-extraction sub-step against one path element, taking its first
+ * result (e.g. values('name') yields the first value).
+ */
+ private applyPath(
+ source: Iterable<StreamItem>,
+ step: StepDescriptor,
+ graph: Graph,
+ trackPaths: boolean,
+ ): Iterable<StreamItem> {
+ const runTraversal = (sub: Pipeline, object: any): any => {
+ const sub0 = sub[0];
+ const fn = STEP_REGISTRY.get(sub0.name);
+ if (!fn) throw new LocalExecutionError(`No implementation for step '${sub0.name}'`);
+ for (const out of fn([wrap(object, [], false)], sub0.args, graph, false)) {
+ return getValue(out, false);
+ }
+ return NON_PRODUCTIVE; // non-productive by(Traversal) filters the path traverser
+ };
+ return stepPath(source, step.args, trackPaths, runTraversal);
+ }
+
private applyAddE(
source: Iterable<StreamItem>,
step: StepDescriptor,
@@ -237,7 +291,7 @@ const STEP_REGISTRY = new Map<string, StepFn>([
['values', stepValues],
['valueMap', stepValueMap],
['elementMap', stepElementMap],
- ['path', stepPath],
+ // 'path' is dispatched via applyPath (needs the registry for by(Traversal) projections)
['limit', stepLimit],
['range', stepRange],
['skip', stepSkip],
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
index 7ae0700683..ba2b04a5a0 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
@@ -19,7 +19,7 @@
import { P, TextP, t, direction } from '../traversal.js';
import { Graph, Vertex, Edge, VertexProperty, Property, Path } from '../../structure/graph.js';
-import type { Arg } from './types.js';
+import type { Arg, Pipeline } from './types.js';
// ── Traverser helpers ─────────────────────────────────────────────────────────
@@ -487,13 +487,51 @@ export function* stepElementMap(
// ── Path step ─────────────────────────────────────────────────────────────────
+/**
+ * Sentinel signalling a non-productive by() projection (e.g. by('age') on an element
+ * lacking the 'age' property). Without ProductiveByStrategy — which Tiny Gremlin does not
+ * support — a non-productive by() filters the whole path traverser.
+ */
+export const NON_PRODUCTIVE = Symbol('nonProductive');
+
+/** by(String)/by(T)/natural projection. Returns NON_PRODUCTIVE for an absent property. */
+function projectByKey(el: any, key: string | null | undefined): any {
+ if (key == null) return el; // natural / by()
+ if (key === 'T.id' || key === 'id') return el?.id;
+ if (key === 'T.label' || key === 'label') return el?.label;
+ if (el instanceof Vertex || el instanceof Edge) {
+ const vals = getValues(el, key);
+ return vals.length === 0 ? NON_PRODUCTIVE : vals[0]; // first value, multi-valued
+ }
+ return el;
+}
+
+// projections carry one entry per by() modulator, applied round-robin across path
+// elements. An entry is either a key (String/T/null for by(String)/by(T)/natural) or a
+// single-step sub-Pipeline (by(Traversal)), run via runTraversal. Empty when no by()
+// modulators are present, in which case the raw path objects are used.
export function* stepPath(
- source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+ source: Iterable<StreamItem>,
+ projections: Arg[],
+ trackPaths: boolean,
+ runTraversal: (sub: Pipeline, object: any) => any,
): Generator<StreamItem> {
for (const item of source) {
const pathEntries: PathEntry[] = getPath(item, trackPaths);
const labels: string[][] = pathEntries.map(e => e.labels);
- const objects: any[] = pathEntries.map(e => e.object);
+ const objects: any[] = [];
+ let productive = true;
+ for (let idx = 0; idx < pathEntries.length; idx++) {
+ const object = pathEntries[idx].object;
+ if (projections.length === 0) { objects.push(object); continue; }
+ const proj = projections[idx % projections.length];
+ const value = Array.isArray(proj)
+ ? runTraversal(proj as unknown as Pipeline, object)
+ : projectByKey(object, proj as string | null | undefined);
+ if (value === NON_PRODUCTIVE) { productive = false; break; }
+ objects.push(value);
+ }
+ if (!productive) continue; // default (non-ProductiveBy) semantics: filter the path
const pathObj = new Path(labels, objects);
// Yield as a traverser so toAsync's trackPaths unwrap produces the Path value correctly.
yield trackPaths ? { value: pathObj, path: [] } : pathObj;
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
index 73ef8f48f3..4b1e157bbb 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
@@ -1034,7 +1034,9 @@ const gremlins = {
g_addV_propertyXset_emptyX: [function({g}) { return g.addV("foo").property(Cardinality.set, new Map([])) }, function({g}) { return g.V().hasLabel("person").values() }],
g_addVXpersonX_propertyXname_joshX_propertyXage_nullX: [function({g}) { return g.addV("person").property("name", "josh").property("age", null) }, function({g}) { return g.V().has("person", "age", null) }],
g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX: [function({g}) { return g.addV("person").property("name", "marko").property("friendWeight", null, "acl", null) }, function({g}) { return g.V().has("person", "name", "marko").has("friendWeight", null) }, function({g}) { return g.V().has("person", "name", "marko").properties("friendWeight").has("acl", null) }, function({g}) { return g.V().has("person", "name", "marko").properties("friendWeight").count() }],
- g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX: [function({g}) { return g.addV("person").property("name", "alice").property("age", 50) }, function({g}) { return g.V().has("person", "name", "alice").property("age", 51) }, function({g}) { return g.V().has("person", "age", 50) }, function({g}) { return g.V().has("person", "age", 51) }],
+ g_V_hasXperson_name_aliceX_propertyXage_51X: [function({g}) { return g.addV("person").property("name", "alice").property("age", 50) }, function({g}) { return g.V().has("person", "name", "alice").property("age", 51) }, function({g}) { return g.V().has("person", "age", 50) }, function({g}) { return g.V().has("person", "age", 51) }],
+ g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query: [function({g}) { return g.addV("person").property("name", "alice").property("age", 30) }, function({g}) { return g.V().has("person", "name", "alice") }, function({g}) { return g.V().has("person", "age", 30) }],
+ g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX: [function({g}) { return g.addV("person").property("name", "alice").property(Cardinality.single, "age", 50) }, function({g}) { return g.V().has("person", "name", "alice").property("age", __.union(__.values("age"), __.constant(1)).sum()) }, function({g}) { return g.V().has("person", "age", 50) }, function({g}) { return g.V().has("person", "age", 51) }],
g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX: [function({g}) { return g.addV("person").property("name", "marko").property("age", 29).as("marko").addV("person").property("name", "vadas").property("age", 27).as("vadas").addV("software").property("name", "lop").property("lang", "java").as("lop").addV("person").property("name", "josh").property("age", 32).as("josh").addV("software").property("name", "ripple").property [...]
g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX: [function({g}) { return g.addV().property("name", "marko").with_("key", "value").values("name", "key") }],
g_addV_propertyXname_marko_since_2010X_withXkey_valueX_propertiesXnameX_valuesXsince_keyX: [function({g}) { return g.addV().property("name", "marko", "since", 2010).with_("key", "value").properties("name").values("since", "key") }],
@@ -1667,6 +1669,8 @@ const gremlins = {
g_VX1X_outEXcreatedX_inV_inE_outV_path: [function({g, vid1}) { return g.V(vid1).outE("created").inV().inE().outV().path() }],
g_V_asXaX_out_asXbX_out_asXcX_path_fromXbX_toXcX_byXnameX: [function({g}) { return g.V().as("a").out().as("b").out().as("c").path().from_("b").to("c").by("name") }],
g_VX1X_out_path_byXageX: [function({g, vid1}) { return g.V(vid1).out().path().by("age") }],
+ g_VX1X_out_path_byXlabelX: [function({g, vid1}) { return g.V(vid1).out().path().by(T.label) }],
+ g_VX1X_out_path_byXvaluesXnameXX: [function({g, vid1}) { return g.V(vid1).out().path().by(__.values("name")) }],
g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX: [function({g, vid1}) { return g.withStrategies(new ProductiveByStrategy()).V(vid1).out().path().by("age") }],
g_injectX1_null_nullX_path: [function({g}) { return g.inject(1, null, null).path() }],
g_injectX1_null_nullX_path_dedup: [function({g}) { return g.inject(1, null, null).path().dedup() }],
diff --git a/gremlin-js/gremlin-javascript/test/integration/tiny-gremlin-subgraph-tests.js b/gremlin-js/gremlin-javascript/test/integration/tiny-gremlin-subgraph-tests.js
new file mode 100644
index 0000000000..7ff34e194c
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/integration/tiny-gremlin-subgraph-tests.js
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'assert';
+import anon from '../../lib/process/anonymous-traversal.js';
+import { getConnection } from '../helper.js';
+import { Graph, Vertex, Edge } from '../../lib/structure/graph.js';
+import { t } from '../../lib/process/traversal.js';
+
+/**
+ * Exercises a core Tiny Gremlin use case end-to-end: pull a subgraph() from a remote
+ * graph, then run local in-process traversals over the detached Graph returned to the client.
+ *
+ * The "created" subgraph of the modern graph contains 5 vertices
+ * (marko, josh, peter — person; lop, ripple — software; vadas is excluded) and 4 created
+ * edges (marko->lop, josh->ripple, josh->lop, peter->lop).
+ */
+describe('Tiny Gremlin over subgraph()', function () {
+ let connection;
+ let sg; // detached Graph returned by subgraph()
+ let lg; // local Tiny Gremlin traversal source over sg
+ let markoId; // marko's id from the remote graph, for id-preservation checks
+
+ const sorted = (arr) => [...arr].sort();
+ const pathObjects = (paths) => paths.map((p) => p.objects);
+
+ before(async function () {
+ connection = getConnection('gmodern');
+ await connection.open();
+ const g = anon.traversal().with_(connection);
+ sg = (await g.E().hasLabel('created').subgraph('sg').cap('sg').next()).value;
+ lg = anon.traversal().with_(sg);
+ markoId = (await g.V().has('name', 'marko').id().next()).value;
+ });
+
+ after(function () {
+ return connection.close();
+ });
+
+ it('returns a detached Graph from subgraph()', function () {
+ assert.ok(sg instanceof Graph);
+ });
+
+ it('captures the created-subgraph vertex set', async function () {
+ const vertices = await lg.V().toList();
+ assert.strictEqual(vertices.length, 5);
+ vertices.forEach((v) => assert.ok(v instanceof Vertex));
+ const names = sorted(await lg.V().values('name').toList());
+ assert.deepStrictEqual(names, ['josh', 'lop', 'marko', 'peter', 'ripple']);
+ assert.ok(!names.includes('vadas'), 'vadas has no created edges and must be excluded');
+ });
+
+ it('captures the created edges', async function () {
+ const edges = await lg.E().toList();
+ assert.strictEqual(edges.length, 4);
+ edges.forEach((e) => assert.ok(e instanceof Edge));
+ const labels = await lg.E().label().toList();
+ assert.deepStrictEqual(sorted(labels), ['created', 'created', 'created', 'created']);
+ });
+
+ it('filters by label locally', async function () {
+ const software = sorted(await lg.V().hasLabel('software').values('name').toList());
+ assert.deepStrictEqual(software, ['lop', 'ripple']);
+ const people = sorted(await lg.V().hasLabel('person').values('name').toList());
+ assert.deepStrictEqual(people, ['josh', 'marko', 'peter']);
+ });
+
+ it('preserves vertex properties through the round-trip', async function () {
+ assert.deepStrictEqual(await lg.V().has('name', 'marko').values('age').toList(), [29]);
+ assert.deepStrictEqual(await lg.V().has('name', 'josh').values('age').toList(), [32]);
+ const langs = await lg.V().hasLabel('software').values('lang').toList();
+ assert.deepStrictEqual(sorted(langs), ['java', 'java']);
+ });
+
+ it('navigates out-edges locally', async function () {
+ const joshOut = sorted(await lg.V().has('name', 'josh').out().values('name').toList());
+ assert.deepStrictEqual(joshOut, ['lop', 'ripple']);
+ const markoOut = await lg.V().has('name', 'marko').out().values('name').toList();
+ assert.deepStrictEqual(markoOut, ['lop']);
+ });
+
+ it('navigates in-edges locally', async function () {
+ const lopIn = sorted(await lg.V().has('name', 'lop').in_().values('name').toList());
+ assert.deepStrictEqual(lopIn, ['josh', 'marko', 'peter']);
+ const rippleIn = await lg.V().has('name', 'ripple').in_().values('name').toList();
+ assert.deepStrictEqual(rippleIn, ['josh']);
+ });
+
+ it('preserves edge properties', async function () {
+ // weight may serialize as float; compare within tolerance rather than exact equality
+ const near = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-6, `${actual} ~ ${expected}`);
+ const markoWeight = await lg.V().has('name', 'marko').outE().values('weight').toList();
+ assert.strictEqual(markoWeight.length, 1);
+ near(markoWeight[0], 0.4);
+ const weights = (await lg.E().values('weight').toList()).map(Number).sort((a, b) => a - b);
+ assert.strictEqual(weights.length, 4);
+ [0.2, 0.4, 0.4, 1.0].forEach((expected, i) => near(weights[i], expected));
+ });
+
+ it('orders results locally', async function () {
+ const ordered = await lg.V().hasLabel('person').values('name').order().toList();
+ assert.deepStrictEqual(ordered, ['josh', 'marko', 'peter']);
+ });
+
+ it('projects paths with by() over the subgraph', async function () {
+ const byName = pathObjects(await lg.V().has('name', 'josh').out().path().by('name').toList());
+ assert.strictEqual(byName.length, 2);
+ byName.forEach((p) => assert.strictEqual(p[0], 'josh'));
+ assert.deepStrictEqual(sorted(byName.map((p) => p[1])), ['lop', 'ripple']);
+ const byLabel = pathObjects(await lg.V().has('name', 'marko').out().path().by(t.label).toList());
+ assert.deepStrictEqual(byLabel, [['person', 'software']]);
+ });
+
+ it('preserves element ids for direct lookup', async function () {
+ const localMarkoId = (await lg.V().has('name', 'marko').id().next()).value;
+ assert.deepStrictEqual(localMarkoId, markoId);
+ const byId = await lg.V(markoId).values('name').toList();
+ assert.deepStrictEqual(byId, ['marko']);
+ });
+
+ it('returns elementMap with tokens and properties', async function () {
+ const m = (await lg.V().has('name', 'marko').elementMap().next()).value;
+ assert.strictEqual(m.get('name'), 'marko');
+ assert.strictEqual(m.get('age'), 29);
+ assert.strictEqual(m.get(t.label), 'person');
+ assert.deepStrictEqual(m.get(t.id), markoId);
+ });
+
+ // Tiny Gremlin is a subset — steps like project(), group(), groupCount(), sum() and fold()
+ // are unavailable. The pattern is to do everything the subset DOES support inside the
+ // traversal (here, sort with order().by(...)), terminate with toList(), then finish in
+ // ordinary JavaScript with the array methods the subset lacks. forEach()/reduce() are
+ // native Tiny Gremlin terminals; map()/filter()/flat() run on the materialized array.
+ describe('post-processing with JavaScript closures', function () {
+ it('projects with Array.map() in place of project()', async function () {
+ // order() does the sorting; only the projection has no Tiny Gremlin equivalent
+ const projected = (await lg.V().hasLabel('person').order().by('name').elementMap().toList())
+ .map((m) => ({ name: m.get('name'), age: m.get('age') }));
+ assert.deepStrictEqual(projected, [
+ { name: 'josh', age: 32 },
+ { name: 'marko', age: 29 },
+ { name: 'peter', age: 35 },
+ ]);
+ });
+
+ it('filters with Array.filter() on a JS-only predicate', async function () {
+ // name length has no Tiny Gremlin predicate, so the filter must live in JS
+ const longNames = (await lg.V().hasLabel('person').order().by('name').elementMap().toList())
+ .filter((m) => m.get('name').length > 4)
+ .map((m) => m.get('name'));
+ assert.deepStrictEqual(longNames, ['marko', 'peter']);
+ });
+
+ it('fans out and flattens with Array.flat() in place of flatMap()', async function () {
+ const people = await lg.V().hasLabel('person').order().by('name').values('name').toList();
+ const created = (
+ await Promise.all(
+ people.map(async (name) => {
+ const items = await lg.V().has('name', name).out().order().by('name').values('name').toList();
+ return items.map((item) => `${name}->${item}`);
+ }),
+ )
+ ).flat();
+ assert.deepStrictEqual(created, ['josh->lop', 'josh->ripple', 'marko->lop', 'peter->lop']);
+ });
+
+ it('aggregates with the reduce() terminal in place of sum()', async function () {
+ const totalAge = await lg.V().hasLabel('person').values('age').reduce((acc, age) => acc + age, 0);
+ assert.strictEqual(totalAge, 29 + 32 + 35);
+ });
+
+ it('group-counts with Array.reduce() in place of groupCount()', async function () {
+ const labels = await lg.V().label().toList();
+ const counts = labels.reduce((acc, label) => {
+ acc[label] = (acc[label] || 0) + 1;
+ return acc;
+ }, {});
+ assert.deepStrictEqual(counts, { person: 3, software: 2 });
+ });
+
+ it('groups into buckets with Array.reduce() in place of group()', async function () {
+ // order().by('name') means each bucket comes out name-sorted with no JS sort
+ const maps = await lg.V().order().by('name').elementMap().toList();
+ const byLabel = maps.reduce((acc, m) => {
+ const label = m.get(t.label);
+ (acc[label] = acc[label] || []).push(m.get('name'));
+ return acc;
+ }, {});
+ assert.deepStrictEqual(byLabel, {
+ person: ['josh', 'marko', 'peter'],
+ software: ['lop', 'ripple'],
+ });
+ });
+
+ it('collects via the forEach() terminal for side-effecting consumption', async function () {
+ const collected = [];
+ await lg.V().hasLabel('software').order().by('name').values('name')
+ .forEach((name) => collected.push(name.toUpperCase()));
+ assert.deepStrictEqual(collected, ['LOP', 'RIPPLE']);
+ });
+ });
+});
diff --git a/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-path-by-test.js b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-path-by-test.js
new file mode 100644
index 0000000000..b8e9b750ed
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-path-by-test.js
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { assert } from 'chai';
+import anon from '../../lib/process/anonymous-traversal.js';
+import { statics as __ } from '../../lib/process/graph-traversal.js';
+import { t } from '../../lib/process/traversal.js';
+import { buildModernGraph } from '../cucumber/local-fixtures.js';
+
+/**
+ * Unit coverage for path().by() projection forms that the @TinyGremlin Gherkin
+ * scenarios do not exercise: by(T), the limited by(Traversal), and the rejection
+ * of unsupported by(Traversal) shapes. by(String) round-robin and non-productive
+ * filtering are covered by Path.feature.
+ */
+describe('Tiny Gremlin path().by()', () => {
+ let g;
+ const markoId = 1; // modern graph: makeVertex(1, 'person', ...)
+
+ beforeEach(() => {
+ g = anon.traversal().with_(buildModernGraph().graph);
+ });
+
+ const objectsOf = (paths) => paths.map((p) => p.objects);
+
+ it('by(String) filters non-productive elements (lop has no age)', async () => {
+ const result = await g.V(markoId).out().path().by('age').toList();
+ // marko(29) -> {vadas(27), josh(32), lop(no age -> path filtered)}
+ assert.sameDeepMembers(objectsOf(result), [[29, 27], [29, 32]]);
+ });
+
+ it('by(T) projects element id/label', async () => {
+ const result = await g.V(markoId).out().path().by(t.label).toList();
+ // marko=person, out-neighbours: vadas/josh=person, lop=software
+ assert.sameDeepMembers(objectsOf(result), [
+ ['person', 'person'],
+ ['person', 'person'],
+ ['person', 'software'],
+ ]);
+ });
+
+ it('by(Traversal) with a single value-extraction step (values) takes the first value', async () => {
+ const result = await g.V(markoId).out().path().by(__.values('name')).toList();
+ assert.sameDeepMembers(objectsOf(result), [
+ ['marko', 'vadas'],
+ ['marko', 'josh'],
+ ['marko', 'lop'],
+ ]);
+ });
+
+ it('rejects by(Traversal) that is not a single value-extraction step', async () => {
+ try {
+ await g.V(markoId).out().path().by(__.out().values('name')).toList();
+ assert.fail('expected a LocalExecutionError for a multi-step by(Traversal)');
+ } catch (err) {
+ assert.match(err.message, /single value-extraction step/);
+ }
+ });
+});
diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py
index 6ce019b0c0..52c2651eda 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -1008,6 +1008,8 @@ world.gremlins = {
'g_addV_propertyXset_emptyX': [(lambda g:g.add_v('foo').property(Cardinality.set_, { })), (lambda g:g.V().has_label('person').values())],
'g_addVXpersonX_propertyXname_joshX_propertyXage_nullX': [(lambda g:g.add_v('person').property('name', 'josh').property('age', None)), (lambda g:g.V().has('person', 'age', None))],
'g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX': [(lambda g:g.add_v('person').property('name', 'marko').property('friendWeight', None, 'acl', None)), (lambda g:g.V().has('person', 'name', 'marko').has('friendWeight', None)), (lambda g:g.V().has('person', 'name', 'marko').properties('friendWeight').has('acl', None)), (lambda g:g.V().has('person', 'name', 'marko').properties('friendWeight').count())],
+ 'g_V_hasXperson_name_aliceX_propertyXage_51X': [(lambda g:g.add_v('person').property('name', 'alice').property('age', 50)), (lambda g:g.V().has('person', 'name', 'alice').property('age', 51)), (lambda g:g.V().has('person', 'age', 50)), (lambda g:g.V().has('person', 'age', 51))],
+ 'g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query': [(lambda g:g.add_v('person').property('name', 'alice').property('age', 30)), (lambda g:g.V().has('person', 'name', 'alice')), (lambda g:g.V().has('person', 'age', 30))],
'g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX': [(lambda g:g.add_v('person').property('name', 'alice').property(Cardinality.single, 'age', 50)), (lambda g:g.V().has('person', 'name', 'alice').property('age', __.union(__.values('age'), __.constant(1)).sum_())), (lambda g:g.V().has('person', 'age', 50)), (lambda g:g.V().has('person', 'age', 51))],
'g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX': [(lambda g:g.add_v('person').property('name', 'marko').property('age', 29).as_('marko').add_v('person').property('name', 'vadas').property('age', 27).as_('vadas').add_v('software').property('name', 'lop').property('lang', 'java').as_('lop').add_v('person').property('name', 'josh').property('age', 32).as_('josh').add_v('software').property('name', 'ripple').property(' [...]
'g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX': [(lambda g:g.add_v().property('name', 'marko').with_('key', 'value').values('name', 'key'))],
@@ -1641,6 +1643,8 @@ world.gremlins = {
'g_VX1X_outEXcreatedX_inV_inE_outV_path': [(lambda g, vid1=None:g.V(vid1).out_e('created').in_v().in_e().out_v().path())],
'g_V_asXaX_out_asXbX_out_asXcX_path_fromXbX_toXcX_byXnameX': [(lambda g:g.V().as_('a').out().as_('b').out().as_('c').path().from_('b').to('c').by('name'))],
'g_VX1X_out_path_byXageX': [(lambda g, vid1=None:g.V(vid1).out().path().by('age'))],
+ 'g_VX1X_out_path_byXlabelX': [(lambda g, vid1=None:g.V(vid1).out().path().by(T.label))],
+ 'g_VX1X_out_path_byXvaluesXnameXX': [(lambda g, vid1=None:g.V(vid1).out().path().by(__.values('name')))],
'g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX': [(lambda g, vid1=None:g.with_strategies(ProductiveByStrategy()).V(vid1).out().path().by('age'))],
'g_injectX1_null_nullX_path': [(lambda g:g.inject(1, None, None).path())],
'g_injectX1_null_nullX_path_dedup': [(lambda g:g.inject(1, None, None).path().dedup())],
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
index d7e90db6b3..e701f0f03d 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
@@ -20551,6 +20551,100 @@
}
]
},
+ {
+ "scenario": "g_V_hasXperson_name_aliceX_propertyXage_51X",
+ "traversals": [
+ {
+ "original": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+ "language": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+ "canonical": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+ "anonymized": "g.addV(string0).property(string1, string2).property(string3, number0)",
+ "dotnet": "g.AddV((string) \"person\").Property(\"name\", \"alice\").Property(\"age\", 50)",
+ "go": "g.AddV(\"person\").Property(\"name\", \"alice\").Property(\"age\", 50)",
+ "groovy": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+ "java": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+ "javascript": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+ "python": "g.add_v('person').property('name', 'alice').property('age', 50)"
+ },
+ {
+ "original": "g.V().has(\"person\",\"name\",\"alice\").property(\"age\", 51)",
+ "language": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+ "canonical": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+ "anonymized": "g.V().has(string0, string1, string2).property(string3, number0)",
+ "dotnet": "g.V().Has(\"person\", \"name\", \"alice\").Property(\"age\", 51)",
+ "go": "g.V().Has(\"person\", \"name\", \"alice\").Property(\"age\", 51)",
+ "groovy": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+ "java": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+ "javascript": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+ "python": "g.V().has('person', 'name', 'alice').property('age', 51)"
+ },
+ {
+ "original": "g.V().has(\"person\",\"age\",50)",
+ "language": "g.V().has(\"person\", \"age\", 50)",
+ "canonical": "g.V().has(\"person\", \"age\", 50)",
+ "anonymized": "g.V().has(string0, string1, number0)",
+ "dotnet": "g.V().Has(\"person\", \"age\", 50)",
+ "go": "g.V().Has(\"person\", \"age\", 50)",
+ "groovy": "g.V().has(\"person\", \"age\", 50)",
+ "java": "g.V().has(\"person\", \"age\", 50)",
+ "javascript": "g.V().has(\"person\", \"age\", 50)",
+ "python": "g.V().has('person', 'age', 50)"
+ },
+ {
+ "original": "g.V().has(\"person\",\"age\",51)",
+ "language": "g.V().has(\"person\", \"age\", 51)",
+ "canonical": "g.V().has(\"person\", \"age\", 51)",
+ "anonymized": "g.V().has(string0, string1, number0)",
+ "dotnet": "g.V().Has(\"person\", \"age\", 51)",
+ "go": "g.V().Has(\"person\", \"age\", 51)",
+ "groovy": "g.V().has(\"person\", \"age\", 51)",
+ "java": "g.V().has(\"person\", \"age\", 51)",
+ "javascript": "g.V().has(\"person\", \"age\", 51)",
+ "python": "g.V().has('person', 'age', 51)"
+ }
+ ]
+ },
+ {
+ "scenario": "g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query",
+ "traversals": [
+ {
+ "original": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+ "language": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+ "canonical": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+ "anonymized": "g.addV(string0).property(string1, string2).property(string3, number0)",
+ "dotnet": "g.AddV((string) \"person\").Property(\"name\", \"alice\").Property(\"age\", 30)",
+ "go": "g.AddV(\"person\").Property(\"name\", \"alice\").Property(\"age\", 30)",
+ "groovy": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+ "java": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+ "javascript": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+ "python": "g.add_v('person').property('name', 'alice').property('age', 30)"
+ },
+ {
+ "original": "g.V().has(\"person\",\"name\",\"alice\")",
+ "language": "g.V().has(\"person\", \"name\", \"alice\")",
+ "canonical": "g.V().has(\"person\", \"name\", \"alice\")",
+ "anonymized": "g.V().has(string0, string1, string2)",
+ "dotnet": "g.V().Has(\"person\", \"name\", \"alice\")",
+ "go": "g.V().Has(\"person\", \"name\", \"alice\")",
+ "groovy": "g.V().has(\"person\", \"name\", \"alice\")",
+ "java": "g.V().has(\"person\", \"name\", \"alice\")",
+ "javascript": "g.V().has(\"person\", \"name\", \"alice\")",
+ "python": "g.V().has('person', 'name', 'alice')"
+ },
+ {
+ "original": "g.V().has(\"person\",\"age\",30)",
+ "language": "g.V().has(\"person\", \"age\", 30)",
+ "canonical": "g.V().has(\"person\", \"age\", 30)",
+ "anonymized": "g.V().has(string0, string1, number0)",
+ "dotnet": "g.V().Has(\"person\", \"age\", 30)",
+ "go": "g.V().Has(\"person\", \"age\", 30)",
+ "groovy": "g.V().has(\"person\", \"age\", 30)",
+ "java": "g.V().has(\"person\", \"age\", 30)",
+ "javascript": "g.V().has(\"person\", \"age\", 30)",
+ "python": "g.V().has('person', 'age', 30)"
+ }
+ ]
+ },
{
"scenario": "g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX",
"traversals": [
@@ -35224,6 +35318,40 @@
}
]
},
+ {
+ "scenario": "g_VX1X_out_path_byXlabelX",
+ "traversals": [
+ {
+ "original": "g.V(vid1).out().path().by(T.label)",
+ "language": "g.V(vid1).out().path().by(T.label)",
+ "canonical": "g.V(vid1).out().path().by(T.label)",
+ "anonymized": "g.V(vid1).out().path().by(T.label)",
+ "dotnet": "g.V(vid1).Out().Path().By(T.Label)",
+ "go": "g.V(vid1).Out().Path().By(gremlingo.T.Label)",
+ "groovy": "g.V(vid1).out().path().by(T.label)",
+ "java": "g.V(vid1).out().path().by(T.label)",
+ "javascript": "g.V(vid1).out().path().by(T.label)",
+ "python": "g.V(vid1).out().path().by(T.label)"
+ }
+ ]
+ },
+ {
+ "scenario": "g_VX1X_out_path_byXvaluesXnameXX",
+ "traversals": [
+ {
+ "original": "g.V(vid1).out().path().by(__.values(\"name\"))",
+ "language": "g.V(vid1).out().path().by(__.values(\"name\"))",
+ "canonical": "g.V(vid1).out().path().by(__.values(\"name\"))",
+ "anonymized": "g.V(vid1).out().path().by(__.values(string0))",
+ "dotnet": "g.V(vid1).Out().Path().By(__.Values<object>(\"name\"))",
+ "go": "g.V(vid1).Out().Path().By(gremlingo.T__.Values(\"name\"))",
+ "groovy": "g.V(vid1).out().path().by(__.values(\"name\"))",
+ "java": "g.V(vid1).out().path().by(__.values(\"name\"))",
+ "javascript": "g.V(vid1).out().path().by(__.values(\"name\"))",
+ "python": "g.V(vid1).out().path().by(__.values('name'))"
+ }
+ ]
+ },
{
"scenario": "g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX",
"traversals": [
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature
index adfdd7ccb1..d7a62ad900 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature
@@ -31,7 +31,7 @@ Feature: Step - path()
| result |
| p[v[marko],marko] |
- @GraphComputerVerificationReferenceOnly
+ @GraphComputerVerificationReferenceOnly @TinyGremlin
Scenario: g_VX1X_out_path_byXageX_byXnameX
Given the modern graph
And using the parameter vid1 defined as "v[marko].id"
@@ -59,7 +59,7 @@ Feature: Step - path()
| p[v[marko],josh,java] |
| p[v[marko],josh,java] |
- @GraphComputerVerificationReferenceOnly
+ @GraphComputerVerificationReferenceOnly @TinyGremlin
Scenario: g_V_out_out_path_byXnameX_byXageX
Given the modern graph
And the traversal of
@@ -111,7 +111,7 @@ Feature: Step - path()
| p[josh,ripple] |
| p[josh,lop] |
- @GraphComputerVerificationReferenceOnly
+ @GraphComputerVerificationReferenceOnly @TinyGremlin
Scenario: g_VX1X_out_path_byXageX
Given the modern graph
And using the parameter vid1 defined as "v[marko].id"
@@ -125,6 +125,36 @@ Feature: Step - path()
| p[d[29].i,d[27].i] |
| p[d[29].i,d[32].i] |
+ @GraphComputerVerificationReferenceOnly @TinyGremlin
+ Scenario: g_VX1X_out_path_byXlabelX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And the traversal of
+ """
+ g.V(vid1).out().path().by(T.label)
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | p[person,person] |
+ | p[person,person] |
+ | p[person,software] |
+
+ @GraphComputerVerificationReferenceOnly @TinyGremlin
+ Scenario: g_VX1X_out_path_byXvaluesXnameXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And the traversal of
+ """
+ g.V(vid1).out().path().by(__.values("name"))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | p[marko,vadas] |
+ | p[marko,josh] |
+ | p[marko,lop] |
+
@GraphComputerVerificationReferenceOnly @WithProductiveByStrategy
Scenario: g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX
Given the modern graph