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 <[email protected]> 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
