You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by ke...@apache.org on 2026/04/27 21:15:39 UTC
(tinkerpop) branch stringify-params updated (bffbe77c3c -> 6b3e2fc388)
This is an automated email from the ASF dual-hosted git repository.
kenhuuu pushed a change to branch stringify-params
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
discard bffbe77c3c test fixes
discard bda9e02d56 rebase error
discard 31ea7d60c9 missing import
discard d1ef05ed8c Convert request bindings to gremlin-lang string format
discard 7002ed271c Fix null map key parsing in GenericLiteralVisitor
discard 6259d898ae Add Char, Duration, Binary support to gremlin-lang.
new 316e283505 TINKERPOP-3153 Add Char, Duration, Binary support to gremlin-lang.
new 4c246f6cf5 Fix null map key parsing in GenericLiteralVisitor
new 6b3e2fc388 TINKERPOP-3247 Convert request bindings to gremlin-lang string format
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (bffbe77c3c)
\
N -- N -- N refs/heads/stringify-params (6b3e2fc388)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
CHANGELOG.asciidoc | 3 +++
docs/src/dev/provider/index.asciidoc | 4 ++--
docs/src/upgrade/release-4.x.x.asciidoc | 11 +++++++----
3 files changed, 12 insertions(+), 6 deletions(-)
(tinkerpop) 01/03: TINKERPOP-3153 Add Char, Duration, Binary support to gremlin-lang.
Posted by ke...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
kenhuuu pushed a commit to branch stringify-params
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 316e2835054d5e17ce963cf6d967e7a5c92c2299
Author: Ken Hu <10...@users.noreply.github.com>
AuthorDate: Wed Apr 8 19:35:41 2026 -0700
TINKERPOP-3153 Add Char, Duration, Binary support to gremlin-lang.
Duration is based on GraphBinary's seconds and nanos representation
instead of Java Duration's ISO-8601 string. Despite ISO-8601 being more
human-readable, the lack of support for it from the languages meant that
lots of extra parsing had to be added to make it work. Also, this
implementation doesn't preclude a future overload to Duration() from
being added to the grammar.
A 's' suffix was added in addition to the 'c' suffix for users to
explicitly choose a string over a character but string is already the
default so it shouldn't cause any changes.
Support was only added to GLVs that already had support for Char,
Duration, and Binary in GraphBinary.
---
CHANGELOG.asciidoc | 1 +
docs/src/dev/developer/for-committers.asciidoc | 5 +
docs/src/upgrade/release-4.x.x.asciidoc | 59 ++++
.../tinkerpop/gremlin/jsr223/CoreImports.java | 3 +
.../grammar/DefaultGremlinBaseVisitor.java | 14 +-
.../language/grammar/GenericLiteralVisitor.java | 69 ++++-
.../translator/AnonymizedTranslatorVisitor.java | 17 ++
.../translator/DotNetTranslateVisitor.java | 34 +++
.../language/translator/GoTranslateVisitor.java | 30 ++
.../translator/GroovyTranslateVisitor.java | 29 ++
.../language/translator/JavaTranslateVisitor.java | 30 ++
.../translator/JavascriptTranslateVisitor.java | 18 ++
.../translator/PythonTranslateVisitor.java | 36 +++
.../language/translator/TranslateVisitor.java | 18 ++
.../gremlin/process/traversal/GremlinLang.java | 33 +++
.../grammar/GeneralLiteralVisitorTest.java | 165 +++++++++++
.../language/translator/GremlinTranslatorTest.java | 54 ++++
.../gremlin/process/traversal/GremlinLangTest.java | 24 ++
.../Gremlin.Net/Process/Traversal/GremlinLang.cs | 23 +-
.../Gherkin/CommonSteps.cs | 24 ++
.../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 18 ++
.../Process/Traversal/GremlinLangTests.cs | 16 +
gremlin-go/build/generate.groovy | 66 +++--
gremlin-go/driver/cucumber/cucumberSteps_test.go | 39 ++-
gremlin-go/driver/cucumber/gremlin.go | 18 ++
gremlin-go/driver/gremlinlang.go | 25 ++
gremlin-go/driver/gremlinlang_test.go | 22 ++
.../translator/AnonymizedTranslateVisitor.ts | 12 +
.../language/translator/DotNetTranslateVisitor.ts | 28 ++
.../lib/language/translator/GoTranslateVisitor.ts | 39 +++
.../language/translator/GroovyTranslateVisitor.ts | 26 ++
.../language/translator/JavaTranslateVisitor.ts | 26 ++
.../translator/JavascriptTranslateVisitor.ts | 14 +
.../language/translator/PythonTranslateVisitor.ts | 33 +++
.../lib/language/translator/TranslateVisitor.ts | 12 +
.../gremlin-javascript/lib/process/gremlin-lang.ts | 4 +
.../scripts/groovy/generate.groovy | 43 +--
.../test/cucumber/feature-steps.js | 6 +
.../gremlin-javascript/test/cucumber/gremlin.js | 19 ++
.../gremlin-javascript/test/cucumber/world.js | 8 +
.../test/unit/gremlin-lang-test.js | 21 ++
.../unit/translator/gremlin-translator-test.js | 57 ++++
gremlin-language/src/main/antlr4/Gremlin.g4 | 44 ++-
.../test/resources/incorrect-gremlin-values.txt | 4 +-
gremlin-python/build/generate.groovy | 4 +-
gremlin-python/docker-compose.yml | 8 +-
.../python/gremlin_python/process/traversal.py | 22 +-
.../src/main/python/tests/feature/feature_steps.py | 22 +-
.../src/main/python/tests/feature/gremlin.py | 22 +-
.../python/tests/unit/process/test_gremlin_lang.py | 14 +-
.../tinkerpop/gremlin/features/StepDefinition.java | 24 ++
.../gremlin/language/translator/translations.json | 324 +++++++++++++++++++++
.../gremlin/test/features/data/Binary.feature | 82 ++++++
.../gremlin/test/features/data/Char.feature | 82 ++++++
.../gremlin/test/features/data/Duration.feature | 118 ++++++++
55 files changed, 1940 insertions(+), 68 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 2fec4921cd..374bb7f073 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -27,6 +27,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Added Gremlator, a single page web application, that translates Gremlin into various programming languages like Javascript and Python.
* Removed `uuid` dependency from `gremlin-javascript` in favor of the built-in `globalThis.crypto.randomUUID()`.
+* Added `Character`, `Binary`, `Duration` to the `gremlin-lang` grammar, GLVs, and Translators.
[[release-4-0-0-beta-2]]
=== TinkerPop 4.0.0-beta.2 (April 1, 2026)
diff --git a/docs/src/dev/developer/for-committers.asciidoc b/docs/src/dev/developer/for-committers.asciidoc
index e88b79cc2f..e2dff0722e 100644
--- a/docs/src/dev/developer/for-committers.asciidoc
+++ b/docs/src/dev/developer/for-committers.asciidoc
@@ -385,7 +385,12 @@ appropriate types expected by the tests.
The syntax of the type notation involves a prefix character to help denote the type, a value between two square
brackets, optionally suffixed with some additional notation depending on the primary type.
+* Binary - *bin[_xxx_]* - The "xxx" should be replaced with a base64-encoded string.
+* Character - *char[_xxx_]* - The "xxx" should be replaced with a single UTF-8 character.
* Date - *dt[_xxx_]* - The "xxx" should be ISO 8601 string.
+* Duration - *dur[_xxx_,_yyy_,_zzz_]* - The "xxx" should be replaced a Long representing seconds, the "yyy" should be
+replaced an Int representing nanoseconds, and the optional "zzz" should be replaced by a boolean which is true if the
+value is positive and false if its negative.
* Edge - *e[_xxx_]* - The "xxx" should be replaced with a representation of an edge in the form of the
`vertex_name-edgelabel->vertex_name`. This syntax may also include the `.id` suffix which would indicate getting the
edge identifier or the `.sid` suffix which gets a string representation of the edge identifier.
diff --git a/docs/src/upgrade/release-4.x.x.asciidoc b/docs/src/upgrade/release-4.x.x.asciidoc
index 08e019de33..0d54666422 100644
--- a/docs/src/upgrade/release-4.x.x.asciidoc
+++ b/docs/src/upgrade/release-4.x.x.asciidoc
@@ -51,6 +51,65 @@ deserialization in GraphBinary is unchanged. Applications that depend on the `uu
`gremlin-javascript` brought it in as a transitive dependency should add it directly to their own `package.json`
if they still need it.
+==== New Gremlin-Lang Literals
+
+The canonical Gremlin grammar now includes literal syntax for `Character`, `Duration`, and `Binary` types. These
+literals can be used directly in traversals without falling back to parameter serialization.
+
+===== Character
+
+A character literal is a single-character string with a `c` suffix:
+
+[source,text]
+----
+g.V().has("initial","M"c)
+----
+
+NOTE: Character is not supported in gremlin-go or gremlin-javascript.
+
+===== Duration
+
+A duration literal uses seconds and nanoseconds with an optional sign flag:
+
+[source,text]
+----
+Duration(seconds,nanos)
+Duration(seconds,nanos,false)
+----
+
+The first argument is seconds (non-negative integer), the second is nanoseconds (integer, 0 to 999999999). The
+optional third argument is an `isPositive` boolean that defaults to `true` when omitted. Set it to `false` for
+negative durations.
+
+[source,text]
+----
+g.V().has("length",P.gt(Duration(3600,0)))
+----
+
+NOTE: Python's `timedelta` has microsecond resolution, so sub-microsecond nanosecond values are truncated.
+.NET's `TimeSpan` has 100-nanosecond tick resolution, so sub-100ns values are truncated.
+
+NOTE: Duration is not supported in gremlin-javascript.
+
+===== Binary
+
+A binary literal wraps a base64-encoded string:
+
+[source,text]
+----
+Binary("AQID")
+Binary("")
+----
+
+Each GLV maps this to its native byte array type:
+
+[source,text]
+----
+g.V().has("blob",P.eq(Binary("AQID")))
+----
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3153[TINKERPOP-3153]
+
== TinkerPop 4.0.0-beta.2
*Release Date: April 1, 2026*
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreImports.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreImports.java
index 398652a889..4fe50ed633 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreImports.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreImports.java
@@ -326,8 +326,11 @@ public final class CoreImports {
CLASS_IMPORTS.add(Lambda.class);
CLASS_IMPORTS.add(java.util.Date.class);
CLASS_IMPORTS.add(java.time.OffsetDateTime.class);
+ CLASS_IMPORTS.add(java.time.Duration.class);
+ CLASS_IMPORTS.add(java.nio.ByteBuffer.class);
CLASS_IMPORTS.add(java.sql.Timestamp.class);
CLASS_IMPORTS.add(java.util.UUID.class);
+ CLASS_IMPORTS.add(java.util.Base64.class);
CLASS_IMPORTS.add(GremlinTranslator.class);
CLASS_IMPORTS.add(Translator.class);
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
index 894eb3d75e..550774cd27 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
@@ -1343,7 +1343,19 @@ public class DefaultGremlinBaseVisitor<T> extends AbstractParseTreeVisitor<T> im
* {@inheritDoc}
*/
@Override public T visitUuidLiteral(final GremlinParser.UuidLiteralContext ctx) { notImplemented(ctx); return null; }
- /**
+ /**
+ * {@inheritDoc}
+ */
+ @Override public T visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) { notImplemented(ctx); return null; }
+ /**
+ * {@inheritDoc}
+ */
+ @Override public T visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) { notImplemented(ctx); return null; }
+ /**
+ * {@inheritDoc}
+ */
+ @Override public T visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) { notImplemented(ctx); return null; }
+ /**
* {@inheritDoc}
*/
@Override public T visitPageRankStringConstant(final GremlinParser.PageRankStringConstantContext ctx) { notImplemented(ctx); return null; }
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
index 983cfc0ad9..6a7f621c10 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
@@ -31,8 +31,11 @@ import org.apache.tinkerpop.gremlin.util.DatetimeHelper;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
+import java.util.Base64;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -518,6 +521,62 @@ public class GenericLiteralVisitor extends DefaultGremlinBaseVisitor<Object> {
return UUID.fromString((String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral()));
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
+ final String text = ctx.getText();
+ // strip the 'c' suffix, then strip quotes, then unescape
+ final String withoutSuffix = text.substring(0, text.length() - 1);
+ final String inner = StringEscapeUtils.unescapeJava(stripQuotes(withoutSuffix));
+ if (inner.length() != 1) {
+ throw new GremlinParserException("Character literal must contain exactly one character");
+ }
+ return inner.charAt(0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
+ final Number secondsNum = (Number) antlr.genericVisitor.visitIntegerLiteral(ctx.integerLiteral(0));
+ final Number nanosNum = (Number) antlr.genericVisitor.visitIntegerLiteral(ctx.integerLiteral(1));
+
+ final long seconds = secondsNum.longValue();
+ final long nanos = nanosNum.longValue();
+
+ if (seconds < 0) {
+ throw new GremlinParserException(
+ String.format("Duration seconds must be non-negative, got: %d", seconds));
+ }
+ if (nanos < 0 || nanos > 999999999) {
+ throw new GremlinParserException(
+ String.format("Duration nanoseconds must be between 0 and 999999999, got: %d", nanos));
+ }
+
+ final boolean isPositive = ctx.booleanLiteral() == null ||
+ Boolean.parseBoolean(ctx.booleanLiteral().getText());
+
+ final Duration d = Duration.ofSeconds(seconds, nanos);
+ return isPositive ? d : d.negated();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
+ final String base64 = (String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral());
+ try {
+ return ByteBuffer.wrap(Base64.getDecoder().decode(base64));
+ } catch (IllegalArgumentException e) {
+ throw new GremlinParserException(
+ String.format("Invalid Binary literal: '%s' is not valid base64", base64));
+ }
+ }
+
/**
* {@inheritDoc}
*/
@@ -533,7 +592,12 @@ public class GenericLiteralVisitor extends DefaultGremlinBaseVisitor<Object> {
*/
@Override
public Object visitStringLiteral(final GremlinParser.StringLiteralContext ctx) {
- return StringEscapeUtils.unescapeJava(stripQuotes(ctx.getText()));
+ final String text = ctx.getText();
+ // handle explicit 's' suffix for string literals
+ if (ctx.StringSuffixLiteral() != null || ctx.EmptyStringSuffixLiteral() != null) {
+ return StringEscapeUtils.unescapeJava(stripQuotes(text.substring(0, text.length() - 1)));
+ }
+ return StringEscapeUtils.unescapeJava(stripQuotes(text));
}
/**
@@ -677,8 +741,7 @@ public class GenericLiteralVisitor extends DefaultGremlinBaseVisitor<Object> {
public Object visitStringNullableLiteral(final GremlinParser.StringNullableLiteralContext ctx) {
if (ctx.K_NULL() != null)
return null;
- else
- return StringEscapeUtils.unescapeJava(stripQuotes(ctx.getText()));
+ return antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral());
}
@Override
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/AnonymizedTranslatorVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/AnonymizedTranslatorVisitor.java
index 98c8be8ef7..23b70526bc 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/AnonymizedTranslatorVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/AnonymizedTranslatorVisitor.java
@@ -23,6 +23,8 @@ import org.apache.tinkerpop.gremlin.language.grammar.GremlinParser;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.List;
@@ -187,4 +189,19 @@ public class AnonymizedTranslatorVisitor extends TranslateVisitor {
@Override
public Void visitUuidLiteral(final GremlinParser.UuidLiteralContext ctx) { return anonymize(ctx, String.class); }
+
+ @Override
+ public Void visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
+ return anonymize(ctx, Character.class);
+ }
+
+ @Override
+ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
+ return anonymize(ctx, Duration.class);
+ }
+
+ @Override
+ public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
+ return anonymize(ctx, ByteBuffer.class);
+ }
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java
index 41fcaa4feb..11546c6ca2 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java
@@ -1181,6 +1181,40 @@ public class DotNetTranslateVisitor extends AbstractTranslateVisitor {
return null;
}
+ @Override
+ public Void visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
+ // "a"c or 'a'c -> 'a' in C#
+ final String text = ctx.getText();
+ // strip the 'c' suffix to get the quoted character
+ final String withoutSuffix = text.substring(0, text.length() - 1);
+ // C# char literals use single quotes
+ final String inner = removeFirstAndLastCharacters(withoutSuffix);
+ sb.append("'").append(inner).append("'");
+ return null;
+ }
+
+ @Override
+ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
+ final long seconds = Long.parseLong(ctx.integerLiteral(0).getText());
+ final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText());
+ final boolean isPositive = ctx.booleanLiteral() == null ||
+ Boolean.parseBoolean(ctx.booleanLiteral().getText());
+ // Convert to ticks: 1 tick = 100 nanoseconds, 1 second = 10,000,000 ticks
+ final long ticks = seconds * 10_000_000L + nanos / 100;
+ sb.append(String.format("TimeSpan.FromTicks(%dL)", ticks));
+ if (!isPositive)
+ sb.append(".Negate()");
+ return null;
+ }
+
+ @Override
+ public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
+ sb.append("Convert.FromBase64String(");
+ sb.append(ctx.stringLiteral().getText());
+ sb.append(")");
+ return null;
+ }
+
@Override
public Void visitTraversalGType(GremlinParser.TraversalGTypeContext ctx) {
final String[] split = ctx.getText().split("\\.");
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java
index 18cdfa1660..f9c30a017f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java
@@ -30,6 +30,7 @@ import org.apache.tinkerpop.gremlin.util.DatetimeHelper;
import java.time.OffsetDateTime;
import java.util.Arrays;
+import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -346,6 +347,35 @@ public class GoTranslateVisitor extends AbstractTranslateVisitor {
return null;
}
+ @Override
+ public Void visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
+ throw new TranslatorException("Character literals are not supported in Go");
+ }
+
+ @Override
+ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
+ final long seconds = Long.parseLong(ctx.integerLiteral(0).getText());
+ final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText());
+ final boolean isPositive = ctx.booleanLiteral() == null ||
+ Boolean.parseBoolean(ctx.booleanLiteral().getText());
+ final long totalNanos = seconds * 1_000_000_000L + nanos;
+ sb.append("time.Duration(").append(isPositive ? totalNanos : -totalNanos).append(")");
+ return null;
+ }
+
+ @Override
+ public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
+ final String base64Str = removeFirstAndLastCharacters(ctx.stringLiteral().getText());
+ final byte[] bytes = Base64.getDecoder().decode(base64Str);
+ sb.append("gremlingo.ByteBuffer{Data: []byte{");
+ for (int i = 0; i < bytes.length; i++) {
+ if (i > 0) sb.append(",");
+ sb.append(bytes[i] & 0xFF);
+ }
+ sb.append("}}");
+ return null;
+ }
+
@Override
protected String getCardinalityFunctionClass() {
return "CardinalityValue";
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java
index 33139d0919..009233c9eb 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java
@@ -131,6 +131,35 @@ public class GroovyTranslateVisitor extends TranslateVisitor {
return null;
}
+ @Override
+ public Void visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
+ final String text = ctx.getText();
+ final String withoutSuffix = text.substring(0, text.length() - 1);
+ final String inner = removeFirstAndLastCharacters(withoutSuffix);
+ sb.append("'").append(inner).append("' as char");
+ return null;
+ }
+
+ @Override
+ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
+ final long seconds = Long.parseLong(ctx.integerLiteral(0).getText());
+ final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText());
+ final boolean isPositive = ctx.booleanLiteral() == null ||
+ Boolean.parseBoolean(ctx.booleanLiteral().getText());
+ sb.append(String.format("Duration.ofSeconds(%d, %d)", seconds, nanos));
+ if (!isPositive)
+ sb.append(".negated()");
+ return null;
+ }
+
+ @Override
+ public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
+ sb.append("ByteBuffer.wrap(Base64.getDecoder().decode(");
+ sb.append(ctx.stringLiteral().getText());
+ sb.append("))");
+ return null;
+ }
+
@Override
public Void visitNullLiteral(final GremlinParser.NullLiteralContext ctx) {
if (ctx.getParent() instanceof GremlinParser.GenericMapNullableArgumentContext) {
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java
index aecf7c2015..ee03430118 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java
@@ -248,6 +248,36 @@ public class JavaTranslateVisitor extends AbstractTranslateVisitor {
return null;
}
+ @Override
+ public Void visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
+ // "a"c or 'a'c -> 'a' in Java
+ final String text = ctx.getText();
+ final String withoutSuffix = text.substring(0, text.length() - 1);
+ final String inner = removeFirstAndLastCharacters(withoutSuffix);
+ sb.append("'").append(inner).append("'");
+ return null;
+ }
+
+ @Override
+ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
+ final long seconds = Long.parseLong(ctx.integerLiteral(0).getText());
+ final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText());
+ final boolean isPositive = ctx.booleanLiteral() == null ||
+ Boolean.parseBoolean(ctx.booleanLiteral().getText());
+ sb.append(String.format("Duration.ofSeconds(%d, %d)", seconds, nanos));
+ if (!isPositive)
+ sb.append(".negated()");
+ return null;
+ }
+
+ @Override
+ public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
+ sb.append("ByteBuffer.wrap(Base64.getDecoder().decode(");
+ sb.append(ctx.stringLiteral().getText());
+ sb.append("))");
+ return null;
+ }
+
@Override
public Void visitGenericRangeLiteral(final GremlinParser.GenericRangeLiteralContext ctx) {
throw new TranslatorException("Java does not support range literals");
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavascriptTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavascriptTranslateVisitor.java
index 08f097d6f7..2a8e0ed07f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavascriptTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavascriptTranslateVisitor.java
@@ -230,6 +230,24 @@ public class JavascriptTranslateVisitor extends AbstractTranslateVisitor {
return null;
}
+ @Override
+ public Void visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
+ throw new TranslatorException("Character literals are not supported in JavaScript");
+ }
+
+ @Override
+ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
+ throw new TranslatorException("Duration literals are not supported in JavaScript");
+ }
+
+ @Override
+ public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
+ sb.append("Buffer.from(");
+ visitStringLiteral(ctx.stringLiteral());
+ sb.append(",'base64')");
+ return null;
+ }
+
@Override
protected String getCardinalityFunctionClass() {
return "CardinalityValue";
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java
index 4473a38dbf..e6297dc539 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java
@@ -289,6 +289,42 @@ public class PythonTranslateVisitor extends AbstractTranslateVisitor {
return null;
}
+ @Override
+ public Void visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
+ final String text = ctx.getText();
+ final String withoutSuffix = text.substring(0, text.length() - 1);
+ final String inner = removeFirstAndLastCharacters(withoutSuffix);
+ sb.append("SingleChar('").append(inner).append("')");
+ return null;
+ }
+
+ @Override
+ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
+ final long seconds = Long.parseLong(ctx.integerLiteral(0).getText());
+ final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText());
+ final boolean isPositive = ctx.booleanLiteral() == null ||
+ Boolean.parseBoolean(ctx.booleanLiteral().getText());
+ final long totalSeconds = isPositive ? seconds : -seconds;
+ // Python's timedelta has microsecond resolution; sub-microsecond nanos are truncated
+ final long micros = nanos / 1000;
+ if (micros == 0) {
+ sb.append("timedelta(seconds=").append(totalSeconds).append(")");
+ } else {
+ // for negative durations, microseconds must also be negated
+ final long totalMicros = isPositive ? micros : -micros;
+ sb.append("timedelta(seconds=").append(totalSeconds).append(",microseconds=").append(totalMicros).append(")");
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
+ sb.append("base64.b64decode(");
+ visitStringLiteral(ctx.stringLiteral());
+ sb.append(")");
+ return null;
+ }
+
@Override
protected String getCardinalityFunctionClass() {
return "CardinalityValue";
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/TranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/TranslateVisitor.java
index 5eccd5f400..a8d51f6b28 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/TranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/TranslateVisitor.java
@@ -452,6 +452,24 @@ public class TranslateVisitor extends GremlinBaseVisitor<Void> {
return null;
}
+ @Override
+ public Void visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
+ sb.append(ctx.getText());
+ return null;
+ }
+
+ @Override
+ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
+ sb.append(ctx.getText());
+ return null;
+ }
+
+ @Override
+ public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
+ sb.append(ctx.getText());
+ return null;
+ }
+
@Override
public Void visitVariable(final GremlinParser.VariableContext ctx) {
final String var = ctx.getText();
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
index e174b5aeca..aa00fccb8c 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
@@ -37,6 +37,8 @@ import java.io.Serializable;
import java.util.Date;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashMap;
@@ -48,6 +50,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
+import java.util.Base64;
import java.util.concurrent.atomic.AtomicInteger;
import static org.apache.tinkerpop.gremlin.util.DatetimeHelper.format;
@@ -147,6 +150,36 @@ public class GremlinLang implements Cloneable, Serializable {
return String.format("UUID(\"%s\")", arg);
}
+ if (arg instanceof Character)
+ // escapeJava converts non-ASCII to \\uXXXX form, ensuring the gremlin-lang string
+ // is pure ASCII and avoids encoding issues across different platforms
+ return String.format("\"%s\"c", StringEscapeUtils.escapeJava(arg.toString()));
+
+ if (arg instanceof Duration) {
+ final Duration d = (Duration) arg;
+ final boolean isNegative = d.isNegative();
+ // negate to get magnitude components - Java's Duration stores negative values
+ // with adjusted seconds (e.g. -1.5s is seconds=-2, nanos=500000000)
+ final Duration abs = isNegative ? d.negated() : d;
+ if (isNegative)
+ return String.format("Duration(%d,%d,false)", abs.getSeconds(), abs.getNano());
+ else
+ return String.format("Duration(%d,%d)", abs.getSeconds(), abs.getNano());
+ }
+
+ if (arg instanceof ByteBuffer) {
+ // duplicate() shares the underlying data but gives an independent position cursor,
+ // so reading bytes for base64 encoding doesn't mutate the caller's buffer state
+ final ByteBuffer buf = ((ByteBuffer) arg).duplicate();
+ final byte[] bytes = new byte[buf.remaining()];
+ buf.get(bytes);
+ return String.format("Binary(\"%s\")", Base64.getEncoder().encodeToString(bytes));
+ }
+
+ if (arg instanceof byte[]) {
+ return String.format("Binary(\"%s\")", Base64.getEncoder().encodeToString((byte[]) arg));
+ }
+
if (arg instanceof Enum) {
// special handling for enums with additional interfaces
if (arg instanceof T)
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java
index b10b31cc9e..ed519b6b31 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java
@@ -33,6 +33,8 @@ import org.junit.runners.Parameterized;
import java.lang.reflect.Constructor;
import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.time.Duration;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@@ -51,6 +53,7 @@ import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo;
import static org.hamcrest.number.OrderingComparison.lessThan;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
/**
* Generic Literal visitor test
@@ -238,6 +241,11 @@ public class GeneralLiteralVisitorTest {
{"'abc\\u2300def'", "abc\u2300def"},
{"'\u2300'", "\u2300"},
{"'abc\u2300def'", "abc\u2300def"},
+ // explicit 's' suffix for string literals
+ {"\"hello\"s", "hello"},
+ {"'hello's", "hello"},
+ {"\"\"s", "Empty"},
+ {"\"a\"s", "a"},
});
}
@@ -255,6 +263,16 @@ public class GeneralLiteralVisitorTest {
}
}
+ public static class ValidStringNullableLiteralTest {
+ @Test
+ public void shouldParseStringSuffixAsNullableLiteral() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("\"person\"s"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.StringNullableLiteralContext ctx = parser.stringNullableLiteral();
+ assertEquals("person", new GenericLiteralVisitor(new GremlinAntlrToJava()).visitStringNullableLiteral(ctx));
+ }
+ }
+
@RunWith(Parameterized.class)
public static class AllPrintableAsciiCharactersTest {
@Parameterized.Parameter(value = 0)
@@ -880,4 +898,151 @@ public class GeneralLiteralVisitorTest {
assertEquals(expected, new GenericLiteralVisitor(new GremlinAntlrToJava()).visitTraversalCardinality(ctx));
}
}
+
+ @RunWith(Parameterized.class)
+ public static class ValidCharacterLiteralTest {
+ @Parameterized.Parameter(value = 0)
+ public String script;
+
+ @Parameterized.Parameter(value = 1)
+ public Character expected;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Iterable<Object[]> generateTestParameters() {
+ return Arrays.asList(new Object[][]{
+ {"\"a\"c", 'a'},
+ {"'a'c", 'a'},
+ {"\"\\\"\"c", '"'},
+ {"'\\''c", '\''},
+ {"\"\\\\\"c", '\\'},
+ {"\"'\"c", '\''},
+ {"\"\\u00E9\"c", '\u00E9'},
+ {"'\u00E9'c", '\u00E9'},
+ });
+ }
+
+ @Test
+ public void shouldParse() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(script));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.CharacterLiteralContext ctx = parser.characterLiteral();
+ assertEquals(expected, new GenericLiteralVisitor(new GremlinAntlrToJava()).visitCharacterLiteral(ctx));
+ }
+ }
+
+ @RunWith(Parameterized.class)
+ public static class ValidDurationLiteralTest {
+ @Parameterized.Parameter(value = 0)
+ public String script;
+
+ @Parameterized.Parameter(value = 1)
+ public Duration expected;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Iterable<Object[]> generateTestParameters() {
+ return Arrays.asList(new Object[][]{
+ {"Duration(9000, 0)", Duration.ofHours(2).plusMinutes(30)},
+ {"Duration(0, 0)", Duration.ZERO},
+ {"Duration(30, 0)", Duration.ofSeconds(30)},
+ {"Duration(0, 500000000)", Duration.ofMillis(500)},
+ {"Duration(5415, 0)", Duration.ofHours(1).plusMinutes(30).plusSeconds(15)},
+ {"Duration(0, 1)", Duration.ofNanos(1)},
+ {"Duration(1, 500000000)", Duration.ofSeconds(1).plusMillis(500)},
+ {"Duration(30, 0, false)", Duration.ofSeconds(-30)},
+ {"Duration(0, 500000000, false)", Duration.ofMillis(-500)},
+ {"Duration(0, 0, false)", Duration.ZERO},
+ {"Duration(0, 0, true)", Duration.ZERO},
+ });
+ }
+
+ @Test
+ public void shouldParse() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(script));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
+ assertEquals(expected, new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx));
+ }
+ }
+
+ public static class InvalidDurationLiteralTest {
+ @Test
+ public void shouldFailOnNegativeNanos() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(0, -1)"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
+ try {
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
+ fail("Negative nanos should have thrown exception");
+ } catch (GremlinParserException gpe) {
+ assertThat(gpe.getMessage().contains("nanoseconds must be between 0 and 999999999"), Matchers.is(true));
+ }
+ }
+
+ @Test
+ public void shouldFailOnNanosOverflow() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(0, 1000000000)"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
+ try {
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
+ fail("Nanos overflow should have thrown exception");
+ } catch (GremlinParserException gpe) {
+ assertThat(gpe.getMessage().contains("nanoseconds must be between 0 and 999999999"), Matchers.is(true));
+ }
+ }
+
+ @Test
+ public void shouldFailOnNegativeSeconds() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(-1, 0)"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
+ try {
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
+ fail("Negative seconds should have thrown exception");
+ } catch (GremlinParserException gpe) {
+ assertThat(gpe.getMessage().contains("seconds must be non-negative"), Matchers.is(true));
+ }
+ }
+ }
+
+ @RunWith(Parameterized.class)
+ public static class ValidBinaryLiteralTest {
+ @Parameterized.Parameter(value = 0)
+ public String script;
+
+ @Parameterized.Parameter(value = 1)
+ public ByteBuffer expected;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Iterable<Object[]> generateTestParameters() {
+ return Arrays.asList(new Object[][]{
+ {"Binary(\"AQID\")", ByteBuffer.wrap(new byte[]{1, 2, 3})},
+ {"Binary(\"\")", ByteBuffer.wrap(new byte[]{})},
+ {"Binary(\"AA==\")", ByteBuffer.wrap(new byte[]{0})},
+ });
+ }
+
+ @Test
+ public void shouldParse() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(script));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.BinaryLiteralContext ctx = parser.binaryLiteral();
+ assertEquals(expected, new GenericLiteralVisitor(new GremlinAntlrToJava()).visitBinaryLiteral(ctx));
+ }
+ }
+
+ public static class InvalidBinaryLiteralTest {
+ @Test
+ public void shouldFailOnInvalidBase64() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Binary(\"!!!not-base64\")"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.BinaryLiteralContext ctx = parser.binaryLiteral();
+ try {
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitBinaryLiteral(ctx);
+ fail("Invalid Binary/base64 value should have thrown exception");
+ } catch (GremlinParserException gpe) {
+ assertThat(gpe.getMessage().contains("Invalid Binary literal:"), Matchers.is(true));
+ }
+ }
+ }
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java
index 525a3dd1e9..050787e8f8 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java
@@ -1426,6 +1426,60 @@ public class GremlinTranslatorTest {
"g.call(\"--list\").with(\"service\", __.constant(\"tinker.search\"))",
"g.call(\"--list\").with_(\"service\", __.constant(\"tinker.search\"))",
"g.call('--list').with_('service', __.constant('tinker.search'))"},
+ {"g.inject(\"a\"c)",
+ null,
+ "g.inject(character0)",
+ "g.Inject<object>('a')",
+ "Character literals are not supported in Go",
+ "g.inject('a' as char)",
+ "g.inject('a')",
+ "Character literals are not supported in JavaScript",
+ "g.inject(SingleChar('a'))"},
+ {"g.inject(Duration(9000,0))",
+ null,
+ "g.inject(duration0)",
+ "g.Inject<object>(TimeSpan.FromTicks(90000000000L))",
+ "g.Inject(time.Duration(9000000000000))",
+ "g.inject(Duration.ofSeconds(9000, 0))",
+ "g.inject(Duration.ofSeconds(9000, 0))",
+ "Duration literals are not supported in JavaScript",
+ "g.inject(timedelta(seconds=9000))"},
+ {"g.inject(Duration(30,0,false))",
+ null,
+ "g.inject(duration0)",
+ "g.Inject<object>(TimeSpan.FromTicks(300000000L).Negate())",
+ "g.Inject(time.Duration(-30000000000))",
+ "g.inject(Duration.ofSeconds(30, 0).negated())",
+ "g.inject(Duration.ofSeconds(30, 0).negated())",
+ "Duration literals are not supported in JavaScript",
+ "g.inject(timedelta(seconds=-30))"},
+ {"g.inject(Duration(1,500000000))",
+ null,
+ "g.inject(duration0)",
+ "g.Inject<object>(TimeSpan.FromTicks(15000000L))",
+ "g.Inject(time.Duration(1500000000))",
+ "g.inject(Duration.ofSeconds(1, 500000000))",
+ "g.inject(Duration.ofSeconds(1, 500000000))",
+ "Duration literals are not supported in JavaScript",
+ "g.inject(timedelta(seconds=1,microseconds=500000))"},
+ {"g.inject(Duration(0,500000000,false))",
+ null,
+ "g.inject(duration0)",
+ "g.Inject<object>(TimeSpan.FromTicks(5000000L).Negate())",
+ "g.Inject(time.Duration(-500000000))",
+ "g.inject(Duration.ofSeconds(0, 500000000).negated())",
+ "g.inject(Duration.ofSeconds(0, 500000000).negated())",
+ "Duration literals are not supported in JavaScript",
+ "g.inject(timedelta(seconds=0,microseconds=-500000))"},
+ {"g.inject(Binary(\"AQID\"))",
+ null,
+ "g.inject(bytebuffer0)",
+ "g.Inject<object>(Convert.FromBase64String(\"AQID\"))",
+ "g.Inject(gremlingo.ByteBuffer{Data: []byte{1,2,3}})",
+ "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\")))",
+ "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\")))",
+ "g.inject(Buffer.from(\"AQID\",'base64'))",
+ "g.inject(base64.b64decode('AQID'))"},
});
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
index cf29daa0f1..b7f9da3290 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
@@ -37,6 +37,7 @@ import org.junit.runners.Parameterized;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@@ -128,6 +129,14 @@ public class GremlinLangTest {
{g.inject(GValue.of("x", "x")).V(GValue.of("ids", new int[]{1, 2, 3})), "g.inject(x).V(ids)"},
{newG().inject(GValue.of(null, "test1"), GValue.of("xx2", "test2")), "g.inject(\"test1\",xx2)"},
{newG().inject(new HashSet<>(Arrays.asList(1, 2))), "g.inject({1,2})"},
+ // Character - single quote (no feature equivalent since grammar uses double quotes in feature files)
+ {g.inject('\''), "g.inject(\"'\"c)"},
+ // Character - backslash (not in feature file due to Gherkin escaping issues)
+ {g.inject('\\'), "g.inject(\"\\\\\"c)"},
+ // Binary - byte[] type (distinct from ByteBuffer)
+ {g.inject(new byte[]{1, 2, 3}), "g.inject(Binary(\"AQID\"))"},
+ {g.inject(new byte[]{}), "g.inject(Binary(\"\"))"},
+ {g.inject(new byte[]{0}), "g.inject(Binary(\"AA==\"))"},
});
}
@@ -169,4 +178,19 @@ public class GremlinLangTest {
assertEquals(value, gremlin.getParameters().get("ids"));
}
}
+
+ public static class BinaryTests {
+ @Test
+ public void shouldEncodeByteBufferWithPositionOffset() {
+ final ByteBuffer buf = ByteBuffer.allocate(4);
+ buf.put((byte) 0);
+ buf.put((byte) 1);
+ buf.put((byte) 2);
+ buf.put((byte) 3);
+ buf.flip();
+ buf.get(); // advance position by 1, remaining is [1, 2, 3]
+ final String gremlin = g.inject(buf).asAdmin().getGremlinLang().getGremlin();
+ assertEquals("g.inject(Binary(\"AQID\"))", gremlin);
+ }
+ }
}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
index aa5a975879..2dfdc02fd5 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
@@ -238,6 +238,22 @@ namespace Gremlin.Net.Process.Traversal
if (arg is Guid guid)
return $"UUID(\"{guid}\")";
+ if (arg is char charVal)
+ return $"\"{EscapeJava(charVal.ToString())}\"c";
+
+ if (arg is TimeSpan timeSpan)
+ {
+ // Use integer tick arithmetic to avoid float precision loss
+ var absTicks = Math.Abs(timeSpan.Ticks);
+ var totalSeconds = absTicks / TimeSpan.TicksPerSecond;
+ var nanos = (int)((absTicks % TimeSpan.TicksPerSecond) * 100);
+ return timeSpan < TimeSpan.Zero
+ ? $"Duration({totalSeconds},{nanos},false)"
+ : $"Duration({totalSeconds},{nanos})";
+ }
+ if (arg is byte[] byteArray)
+ return $"Binary(\"{Convert.ToBase64String(byteArray)}\")";
+
if (arg is EnumWrapper enumWrapper)
return $"{enumWrapper.EnumName}.{enumWrapper.EnumValue}";
@@ -663,7 +679,12 @@ namespace Gremlin.Net.Process.Traversal
sb.Append("\\f");
break;
default:
- sb.Append(c);
+ // escape non-ASCII to \\uXXXX form, ensuring the gremlin-lang string
+ // is pure ASCII and avoids encoding issues across different platforms
+ if (c > 0x7E)
+ sb.Append($"\\u{(int)c:X4}");
+ else
+ sb.Append(c);
break;
}
}
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
index bb3cf3e671..4921813393 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
@@ -63,6 +63,9 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{@"prop\[(.+)\]", ToProperty},
{@"dt\[(.+)\]", ToDateTime},
{@"uuid\[(.+)\]", ToUuid},
+ {@"char\[(.)\]", ToChar},
+ {@"dur\[(.+)\]", ToDuration},
+ {@"bin\[(.*)\]", ToBinary},
{@"d\[(.*)\]\.([bsilfdmn])", ToNumber},
{@"D\[(.+)\]", ToDirection},
{@"M\[(.+)\]", ToMerge},
@@ -541,6 +544,27 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
return null;
}
+ private static object ToChar(string value, string graphName)
+ {
+ return value[0];
+ }
+
+ private static object ToDuration(string value, string graphName)
+ {
+ var parts = value.Split(',');
+ var seconds = long.Parse(parts[0].Trim());
+ var nanos = int.Parse(parts[1].Trim());
+ var isPositive = parts.Length < 3 || bool.Parse(parts[2].Trim());
+ // TimeSpan has 100-nanosecond tick resolution; sub-100ns values are truncated
+ var ts = TimeSpan.FromSeconds(seconds) + TimeSpan.FromTicks(nanos / 100);
+ return isPositive ? ts : ts.Negate();
+ }
+
+ private static object ToBinary(string base64, string graphName)
+ {
+ return Convert.FromBase64String(base64);
+ }
+
private static Vertex ToVertex(string name, string graphName)
{
if (ScenarioData.GetByGraphName(graphName).Vertices.ContainsKey(name))
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index 9e7003065c..bf88cfccfa 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -226,6 +226,11 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_V_valuesXintX_asNumberXGType_BIGINTX_isXtypeOfXGType_BIGINTXX_project_byXidentityX_byXmathXaddX999XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("int", 50), (g,p) =>g.V().Values<object>("int").AsNumber(GType.BigInt).Is(P.TypeOf(GType.BigInt)).Project<object>("original", "added").By(__.Identity()).By(__.Math("_ + 999"))}},
{"g_injectX777X_asNumberXGType_BIGINTX_isXtypeOfXGType_BIGINTXX_groupCount", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(777).AsNumber(GType.BigInt).Is(P.TypeOf(GType.BigInt)).GroupCount<object>()}},
{"g_V_valuesXageX_isXtypeOfXGType_BIGINTXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Values<object>("age").Is(P.TypeOf(GType.BigInt))}},
+ {"g_injectXBinaryXAQIDXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(Convert.FromBase64String("AQID"))}},
+ {"g_injectXBinaryXemptyXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(Convert.FromBase64String(""))}},
+ {"g_injectXBinaryXAA_eqeqXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(Convert.FromBase64String("AA=="))}},
+ {"g_valuesXblobX_isXtypeOfXGType_BINARYXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("blob", Convert.FromBase64String("AQID")), (g,p) =>g.V().Values<object>("blob").Is(P.TypeOf(GType.Binary))}},
+ {"g_injectXBinaryXAQIDXX_isXeqXBinaryXAQIDXXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(Convert.FromBase64String("AQID")).Is(P.Eq(Convert.FromBase64String("AQID")))}},
{"g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("int", 5), (g,p) =>g.V().Values<object>("int").AsNumber(GType.Byte).Is(P.TypeOf(GType.Byte))}},
{"g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_mathXaddX20XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("int", 10), (g,p) =>g.V().Values<object>("int").AsNumber(GType.Byte).Is(P.TypeOf(GType.Byte)).Math("_ + 20")}},
{"g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_isXltX10XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("int", 7), (g,p) =>g.V().Values<object>("int").AsNumber(GType.Byte).Is(P.TypeOf(GType.Byte)).Is(P.Lt(10))}},
@@ -234,6 +239,11 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_chooseXisXeqX12XX_constantXtwelveX_constantXotherXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("int", 12), (g,p) =>g.V().Values<object>("int").AsNumber(GType.Byte).Is(P.TypeOf(GType.Byte)).Choose<object>(__.Is(P.Eq(12)), __.Constant<object>("twelve"), __.Constant<object>("other"))}},
{"g_injectX15X_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_groupCount", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(15).AsNumber(GType.Byte).Is(P.TypeOf(GType.Byte)).GroupCount<object>()}},
{"g_V_valuesXageX_isXtypeOfXGType_BYTEXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Values<object>("age").Is(P.TypeOf(GType.Byte))}},
+ {"g_injectXaX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>('a')}},
+ {"g_injectXescaped_quoteX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>('\"')}},
+ {"g_injectXunicodeX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>('\u00E9')}},
+ {"g_valuesXinitialX_isXtypeOfXGType_CHARXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("initial", 'a'), (g,p) =>g.V().Values<object>("initial").Is(P.TypeOf(GType.Char))}},
+ {"g_injectXaX_isXeqXaXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>('a').Is(P.Eq('a'))}},
{"g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "event").Property("datetime", DateTimeOffset.Parse("2023-08-08T00:00Z")), (g,p) =>g.V().Values<object>("datetime").Is(P.TypeOf(GType.DateTime))}},
{"g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX_project_byXidentityX_byXdateAddXDT_dayX1XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "event").Property("datetime", DateTimeOffset.Parse("2023-08-08T00:00Z")), (g,p) =>g.V().Values<object>("datetime").Is(P.TypeOf(GType.DateTime)).Project<object>("original", "nextDay").By(__.Identity()).By(__.DateAdd(DT.Day, 1))}},
{"g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX_dateDiffXdatetimeX2023_08_10XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "event").Property("datetime", DateTimeOffset.Parse("2023-08-08T00:00Z")), (g,p) =>g.V().Values<object>("datetime").Is(P.TypeOf(GType.DateTime)).DateDiff(DateTimeOffset.Parse("2023-08-08T00:00:30Z"))}},
@@ -253,6 +263,14 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_V_valuesXdoubleX_isXtypeOfXGType_DOUBLEXX_order_byXascX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("double", 3.2d).AddV((string) "data").Property("double", 1.8d).AddV((string) "data").Property("double", 2.5d), (g,p) =>g.V().Values<object>("double").Is(P.TypeOf(GType.Double)).Order().By(Order.Asc)}},
{"g_injectX5_5dX_isXtypeOfXGType_DOUBLEXX_groupCount", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(5.5d).Is(P.TypeOf(GType.Double)).GroupCount<object>()}},
{"g_V_valuesXageX_isXtypeOfXGType_DOUBLEXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Values<object>("age").Is(P.TypeOf(GType.Double))}},
+ {"g_injectXDurationX9000_0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(90000000000L))}},
+ {"g_injectXDurationX0_0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(0L))}},
+ {"g_injectXDurationX0_500000000XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(5000000L))}},
+ {"g_injectXDurationX30_0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(300000000L))}},
+ {"g_injectXDurationX30_0_falseXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(300000000L).Negate())}},
+ {"g_injectXDurationX1_500000000_falseXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(15000000L).Negate())}},
+ {"g_valuesXlengthX_isXtypeOfXGType_DURATIONXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("length", TimeSpan.FromTicks(90000000000L)), (g,p) =>g.V().Values<object>("length").Is(P.TypeOf(GType.Duration))}},
+ {"g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(90000000000L)).Is(P.Gt(TimeSpan.FromTicks(36000000000L)))}},
{"g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("float", 2.5), (g,p) =>g.V().Values<object>("float").AsNumber(GType.Float).Is(P.TypeOf(GType.Float))}},
{"g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_mathXmulX2XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("float", 3.0), (g,p) =>g.V().Values<object>("float").AsNumber(GType.Float).Is(P.TypeOf(GType.Float)).Math("_ * 2")}},
{"g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_isXeqX1_5XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("float", 1.5), (g,p) =>g.V().Values<object>("float").AsNumber(GType.Float).Is(P.TypeOf(GType.Float)).Is(P.Eq(1.5))}},
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
index 3167499c45..f54f4e359b 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
@@ -1041,5 +1041,21 @@ namespace Gremlin.Net.UnitTest.Process.Traversal
"g.V(\"1\").property(Cardinality.set,\"city\",\"wilmington\").property(Cardinality.set,\"state\",\"delaware\")",
_g.V("1").Property(Cardinality.Set, map).GremlinLang.GetGremlin());
}
+
+ // Character - backslash (not in feature file due to Gherkin escaping issues)
+ [Fact]
+ public void g_Inject_Char_Backslash()
+ {
+ Assert.Equal("g.inject(\"\\\\\"c)",
+ _g.Inject((object)'\\').GremlinLang.GetGremlin());
+ }
+
+ // Character - single quote (no feature equivalent)
+ [Fact]
+ public void g_Inject_Char_SingleQuote()
+ {
+ Assert.Equal("g.inject(\"'\"c)",
+ _g.Inject((object)'\'').GremlinLang.GetGremlin());
+ }
}
}
diff --git a/gremlin-go/build/generate.groovy b/gremlin-go/build/generate.groovy
index 0ed3937d5f..3069766aad 100644
--- a/gremlin-go/build/generate.groovy
+++ b/gremlin-go/build/generate.groovy
@@ -79,37 +79,45 @@ radishGremlinFile.withWriter('UTF-8') { Writer writer ->
if (staticTranslate.containsKey(k)) {
writer.writeLine(staticTranslate[k])
} else {
- writer.write(" ")
- writer.write("\"")
- writer.write(k)
- writer.write("\": {")
- def collected = v.collect { GremlinTranslator.translate(it, Translator.GO) }
- def gremlinItty = collected.iterator()
- while (gremlinItty.hasNext()) {
- def t = gremlinItty.next()
- writer.write("func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return ")
- try {
- writer.write(t.getTranslated().
- replaceAll("xx([0-9]+)", "p[\"xx\$1\"]").
- replaceAll("^-v([0-9]+)^-", "p[\"v\$1\"]"). // using ^- to avoid matching on g.io() tests - unexpected side-effect
- replaceAll("vid([0-9]+)", "p[\"vid\$1\"]").
- replaceAll("e([0-9]+)", "p[\"e\$1\"]").
- replaceAll("eid([0-9]+)", "p[\"eid\$1\"]").
- replace("l1", "p[\"l1\"]").
- replace("l2", "p[\"l2\"]").
- replace("pred1", "p[\"pred1\"]").
- replace("c1", "p[\"c1\"]").
- replace("c2", "p[\"c2\"]"))
- } catch (ignored) {
- // Putting these in place of not implemented functions
- // TODO make sure all is supported
- writer.write("nil")
+ try {
+ def collected = v.collect { GremlinTranslator.translate(it, Translator.GO) }
+ writer.write(" ")
+ writer.write("\"")
+ writer.write(k)
+ writer.write("\": {")
+ def gremlinItty = collected.iterator()
+ while (gremlinItty.hasNext()) {
+ def t = gremlinItty.next()
+ writer.write("func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return ")
+ try {
+ writer.write(t.getTranslated().
+ replaceAll("xx([0-9]+)", "p[\"xx\$1\"]").
+ replaceAll("^-v([0-9]+)^-", "p[\"v\$1\"]"). // using ^- to avoid matching on g.io() tests - unexpected side-effect
+ replaceAll("vid([0-9]+)", "p[\"vid\$1\"]").
+ replaceAll("e([0-9]+)", "p[\"e\$1\"]").
+ replaceAll("eid([0-9]+)", "p[\"eid\$1\"]").
+ replace("l1", "p[\"l1\"]").
+ replace("l2", "p[\"l2\"]").
+ replace("pred1", "p[\"pred1\"]").
+ replace("c1", "p[\"c1\"]").
+ replace("c2", "p[\"c2\"]"))
+ } catch (ignored) {
+ // Putting these in place of not implemented functions
+ // TODO make sure all is supported
+ writer.write("nil")
+ }
+ writer.write("}")
+ if (gremlinItty.hasNext())
+ writer.write(', ')
}
- writer.write("}")
- if (gremlinItty.hasNext())
- writer.write(', ')
+ writer.writeLine('}, ')
+ } catch (ignored) {
+ // emit a placeholder for scenarios with unsupported literals to keep the file
+ // syntactically valid; the scenario will be skipped at test runtime via tag exclusions
+ writer.write(" \"")
+ writer.write(k)
+ writer.writeLine("\": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return nil}}, ")
}
- writer.writeLine('}, ')
}
}
writer.writeLine('}\n')
diff --git a/gremlin-go/driver/cucumber/cucumberSteps_test.go b/gremlin-go/driver/cucumber/cucumberSteps_test.go
index 87d1ecdae0..2d72befcb7 100644
--- a/gremlin-go/driver/cucumber/cucumberSteps_test.go
+++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go
@@ -21,6 +21,7 @@ package gremlingo
import (
"context"
+ "encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -51,6 +52,8 @@ func init() {
regexp.MustCompile(`^str\[(.*)]$`): func(stringVal, graphName string) interface{} { return stringVal }, //returns the string value as is
regexp.MustCompile(`^dt\[(.*)]$`): toDateTime,
regexp.MustCompile(`^uuid\[(.*)]$`): toUuid,
+ regexp.MustCompile(`^dur\[(.*)]$`): toDuration,
+ regexp.MustCompile(`^bin\[(.*)]$`): toBinary,
regexp.MustCompile(`^d\[(.*)]\.[bslfd]$`): toNumeric,
regexp.MustCompile(`^d\[(.*)]\.[m]$`): toBigDecimal,
regexp.MustCompile(`^d\[(.*)]\.[n]$`): toBigInt,
@@ -131,6 +134,40 @@ func toUuid(stringVal, graphName string) interface{} {
return val
}
+// Parse duration from seconds,nanos[,isPositive] format.
+func toDuration(stringVal, graphName string) interface{} {
+ parts := strings.Split(stringVal, ",")
+ if len(parts) < 2 {
+ return nil
+ }
+ seconds, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64)
+ if err != nil {
+ return nil
+ }
+ nanos, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
+ if err != nil {
+ return nil
+ }
+ isPositive := true
+ if len(parts) >= 3 {
+ isPositive = strings.TrimSpace(parts[2]) == "true"
+ }
+ d := time.Duration(seconds)*time.Second + time.Duration(nanos)
+ if !isPositive {
+ d = -d
+ }
+ return d
+}
+
+// Parse binary from base64 string.
+func toBinary(stringVal, graphName string) interface{} {
+ data, err := base64.StdEncoding.DecodeString(stringVal)
+ if err != nil {
+ return nil
+ }
+ return &gremlingo.ByteBuffer{Data: data}
+}
+
// Parse numeric.
func toNumeric(stringVal, graphName string) interface{} {
if strings.Contains(stringVal, ".") {
@@ -1014,7 +1051,7 @@ func TestCucumberFeatures(t *testing.T) {
TestSuiteInitializer: InitializeTestSuite,
ScenarioInitializer: InitializeScenario,
Options: &godog.Options{
- Tags: "~@GraphComputerOnly && ~@AllowNullPropertyValues && ~@StepSubgraph && ~@StepTree && ~@StepWrite",
+ Tags: "~@GraphComputerOnly && ~@AllowNullPropertyValues && ~@StepSubgraph && ~@StepTree && ~@StepWrite && ~@DataChar",
Format: "pretty",
Paths: []string{getEnvOrDefaultString("CUCUMBER_FEATURE_FOLDER", "../../../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features")},
TestingT: t, // Testing instance that will run subtests.
diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go
index d42c088856..2c821d785d 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -196,6 +196,11 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
"g_V_valuesXintX_asNumberXGType_BIGINTX_isXtypeOfXGType_BIGINTXX_project_byXidentityX_byXmathXaddX999XXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("int", 50)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("int").AsNumber(gremlingo.GType.BigInt).Is(gremlingo.P.TypeOf(gremlingo.GType.BigInt)).Project("original", "added").By(gremlingo.T [...]
"g_injectX777X_asNumberXGType_BIGINTX_isXtypeOfXGType_BIGINTXX_groupCount": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(777).AsNumber(gremlingo.GType.BigInt).Is(gremlingo.P.TypeOf(gremlingo.GType.BigInt)).GroupCount()}},
"g_V_valuesXageX_isXtypeOfXGType_BIGINTXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("age").Is(gremlingo.P.TypeOf(gremlingo.GType.BigInt))}},
+ "g_injectXBinaryXAQIDXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(gremlingo.ByteBuffer{Data: []byte{1,2,3}})}},
+ "g_injectXBinaryXemptyXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(gremlingo.ByteBuffer{Data: []byte{}})}},
+ "g_injectXBinaryXAA_eqeqXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(gremlingo.ByteBuffer{Data: []byte{0}})}},
+ "g_valuesXblobX_isXtypeOfXGType_BINARYXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("blob", gremlingo.ByteBuffer{Data: []byte{1,2,3}})}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("blob").Is(gremlingo.P.TypeOf(gremlingo.GType.Binary))}},
+ "g_injectXBinaryXAQIDXX_isXeqXBinaryXAQIDXXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(gremlingo.ByteBuffer{Data: []byte{1,2,3}}).Is(gremlingo.P.Eq(gremlingo.ByteBuffer{Data: []byte{1,2,3}}))}},
"g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("int", 5)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("int").AsNumber(gremlingo.GType.Byte).Is(gremlingo.P.TypeOf(gremlingo.GType.Byte))}},
"g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_mathXaddX20XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("int", 10)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("int").AsNumber(gremlingo.GType.Byte).Is(gremlingo.P.TypeOf(gremlingo.GType.Byte)).Math("_ + 20")}},
"g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_isXltX10XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("int", 7)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("int").AsNumber(gremlingo.GType.Byte).Is(gremlingo.P.TypeOf(gremlingo.GType.Byte)).Is(gremlingo.P.Lt(10))}},
@@ -204,6 +209,11 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
"g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_chooseXisXeqX12XX_constantXtwelveX_constantXotherXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("int", 12)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("int").AsNumber(gremlingo.GType.Byte).Is(gremlingo.P.TypeOf(gremlingo.GType.Byte)).Choose(gremlingo.T__.Is(gremlingo.P.Eq( [...]
"g_injectX15X_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_groupCount": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(15).AsNumber(gremlingo.GType.Byte).Is(gremlingo.P.TypeOf(gremlingo.GType.Byte)).GroupCount()}},
"g_V_valuesXageX_isXtypeOfXGType_BYTEXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("age").Is(gremlingo.P.TypeOf(gremlingo.GType.Byte))}},
+ "g_injectXaX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return nil}},
+ "g_injectXescaped_quoteX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return nil}},
+ "g_injectXunicodeX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return nil}},
+ "g_valuesXinitialX_isXtypeOfXGType_CHARXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return nil}},
+ "g_injectXaX_isXeqXaXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return nil}},
"g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("event").Property("datetime", time.Date(2023, 8, 8, 0, 0, 0, 0, time.FixedZone("UTC+00:00", 0)))}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("datetime").Is(gremlingo.P.TypeOf(gremlingo.GType.DateTime))}},
"g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX_project_byXidentityX_byXdateAddXDT_dayX1XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("event").Property("datetime", time.Date(2023, 8, 8, 0, 0, 0, 0, time.FixedZone("UTC+00:00", 0)))}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("datetime").Is(gremlingo.P.TypeOf(gremlingo.GType.DateTime)).Project("orig [...]
"g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX_dateDiffXdatetimeX2023_08_10XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("event").Property("datetime", time.Date(2023, 8, 8, 0, 0, 0, 0, time.FixedZone("UTC+00:00", 0)))}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("datetime").Is(gremlingo.P.TypeOf(gremlingo.GType.DateTime)).DateDiff(time.Date(2023, [...]
@@ -223,6 +233,14 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
"g_V_valuesXdoubleX_isXtypeOfXGType_DOUBLEXX_order_byXascX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("double", 3.2).AddV("data").Property("double", 1.8).AddV("data").Property("double", 2.5)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("double").Is(gremlingo.P.TypeOf(gremlingo.GType.Double)).Order().By(gremlingo.Order.Asc)}},
"g_injectX5_5dX_isXtypeOfXGType_DOUBLEXX_groupCount": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(5.5).Is(gremlingo.P.TypeOf(gremlingo.GType.Double)).GroupCount()}},
"g_V_valuesXageX_isXtypeOfXGType_DOUBLEXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("age").Is(gremlingo.P.TypeOf(gremlingo.GType.Double))}},
+ "g_injectXDurationX9000_0XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(9000000000000))}},
+ "g_injectXDurationX0_0XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(0))}},
+ "g_injectXDurationX0_500000000XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(500000000))}},
+ "g_injectXDurationX30_0XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(30000000000))}},
+ "g_injectXDurationX30_0_falseXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(-30000000000))}},
+ "g_injectXDurationX1_500000000_falseXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(-1500000000))}},
+ "g_valuesXlengthX_isXtypeOfXGType_DURATIONXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("length", time.Duration(9000000000000))}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("length").Is(gremlingo.P.TypeOf(gremlingo.GType.Duration))}},
+ "g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(9000000000000)).Is(gremlingo.P.Gt(time.Duration(3600000000000)))}},
"g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("float", 2.5)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("float").AsNumber(gremlingo.GType.Float).Is(gremlingo.P.TypeOf(gremlingo.GType.Float))}},
"g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_mathXmulX2XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("float", 3.0)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("float").AsNumber(gremlingo.GType.Float).Is(gremlingo.P.TypeOf(gremlingo.GType.Float)).Math("_ * 2")}},
"g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_isXeqX1_5XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("float", 1.5)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("float").AsNumber(gremlingo.GType.Float).Is(gremlingo.P.TypeOf(gremlingo.GType.Float)).Is(gremlingo.P.Eq(1.5))}},
diff --git a/gremlin-go/driver/gremlinlang.go b/gremlin-go/driver/gremlinlang.go
index 2983fbc559..2362515e5f 100644
--- a/gremlin-go/driver/gremlinlang.go
+++ b/gremlin-go/driver/gremlinlang.go
@@ -20,6 +20,7 @@ under the License.
package gremlingo
import (
+ "encoding/base64"
"fmt"
"go/token"
"math"
@@ -136,6 +137,7 @@ func escapeString(s string) string {
return sb.String()
}
+
func (gl *GremlinLang) argAsString(arg interface{}) (string, error) {
if arg == nil {
return "null", nil
@@ -269,6 +271,24 @@ func (gl *GremlinLang) argAsString(arg interface{}) (string, error) {
return key, nil
case uuid.UUID:
return fmt.Sprintf("UUID(\"%v\")", v.String()), nil
+ case time.Duration:
+ isNegative := v < 0
+ abs := v
+ if isNegative {
+ abs = -v
+ }
+ totalSeconds := int64(abs / time.Second)
+ nanos := int64(abs % time.Second)
+ if isNegative {
+ return fmt.Sprintf("Duration(%d,%d,false)", totalSeconds, nanos), nil
+ }
+ return fmt.Sprintf("Duration(%d,%d)", totalSeconds, nanos), nil
+ case ByteBuffer:
+ return fmt.Sprintf("Binary(\"%s\")", base64.StdEncoding.EncodeToString(v.Data)), nil
+ case *ByteBuffer:
+ return fmt.Sprintf("Binary(\"%s\")", base64.StdEncoding.EncodeToString(v.Data)), nil
+ case []byte:
+ return fmt.Sprintf("Binary(\"%s\")", base64.StdEncoding.EncodeToString(v)), nil
default:
switch reflect.TypeOf(arg).Kind() {
case reflect.Map:
@@ -579,6 +599,11 @@ func (gl *GremlinLang) convertArgument(arg interface{}) (interface{}, error) {
if isUUID {
return arg, nil
}
+ // preserve []byte as-is for Binary literal support
+ _, isByteSlice := arg.([]byte)
+ if isByteSlice {
+ return arg, nil
+ }
argList := reflect.ValueOf(arg)
newList := make([]interface{}, argList.Len())
for i := 0; i < argList.Len(); i++ {
diff --git a/gremlin-go/driver/gremlinlang_test.go b/gremlin-go/driver/gremlinlang_test.go
index e9cb5e6d1c..9d3ee5b93c 100644
--- a/gremlin-go/driver/gremlinlang_test.go
+++ b/gremlin-go/driver/gremlinlang_test.go
@@ -680,6 +680,28 @@ func Test_GremlinLang(t *testing.T) {
},
equals: "g.V().has(\"name\",\"\\\"marko\\n\\r\\t\\b\\f\\\"\")",
},
+ // Binary - []byte type (distinct from ByteBuffer)
+ {
+ name: "g_Inject_ByteSlice",
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject([]byte{1, 2, 3})
+ },
+ equals: `g.inject(Binary("AQID"))`,
+ },
+ {
+ name: "g_Inject_ByteSlice_Empty",
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject([]byte{})
+ },
+ equals: `g.inject(Binary(""))`,
+ },
+ {
+ name: "g_Inject_ByteSlice_Padding",
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject([]byte{0})
+ },
+ equals: `g.inject(Binary("AA=="))`,
+ },
}
var testsToRun []test
diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/AnonymizedTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/AnonymizedTranslateVisitor.ts
index 4f90e986fa..3ff98f0288 100644
--- a/gremlin-js/gremlin-javascript/lib/language/translator/AnonymizedTranslateVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/translator/AnonymizedTranslateVisitor.ts
@@ -141,6 +141,18 @@ export default class AnonymizedTranslateVisitor extends TranslateVisitor {
this.anonymize(ctx, 'String');
}
+ visitCharacterLiteral(ctx: any): void {
+ this.anonymize(ctx, 'Character');
+ }
+
+ visitDurationLiteral(ctx: any): void {
+ this.anonymize(ctx, 'Duration');
+ }
+
+ visitBinaryLiteral(ctx: any): void {
+ this.anonymize(ctx, 'ByteBuffer');
+ }
+
// ─── Keys — output unquoted (only values are anonymized) ─────────────────
visitNakedKey(ctx: any): void {
diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
index 11e303144f..8cb11b125b 100644
--- a/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
@@ -225,6 +225,34 @@ export default class DotNetTranslateVisitor extends TranslateVisitor {
this.sb.push(')');
}
+ visitCharacterLiteral(ctx: any): void {
+ const text: string = ctx.getText();
+ const withoutSuffix = text.substring(0, text.length - 1);
+ const inner = TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix);
+ this.sb.push("'");
+ this.sb.push(inner);
+ this.sb.push("'");
+ }
+
+ visitDurationLiteral(ctx: any): void {
+ const seconds = parseInt(ctx.integerLiteral(0).getText(), 10);
+ const nanos = parseInt(ctx.integerLiteral(1).getText(), 10);
+ const isPositive = ctx.booleanLiteral() === null ||
+ ctx.booleanLiteral().getText() === 'true';
+ // Convert to ticks: 1 tick = 100 nanoseconds, 1 second = 10,000,000 ticks
+ const ticks = seconds * 10_000_000 + Math.floor(nanos / 100);
+ this.sb.push(`TimeSpan.FromTicks(${ticks}L)`);
+ if (!isPositive) {
+ this.sb.push('.Negate()');
+ }
+ }
+
+ visitBinaryLiteral(ctx: any): void {
+ this.sb.push('Convert.FromBase64String(');
+ this.sb.push(ctx.stringLiteral().getText());
+ this.sb.push(')');
+ }
+
visitClassType(ctx: any): void {
this.sb.push('typeof(');
this.sb.push(ctx.getText());
diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts
index a3806bca85..c95cc6de32 100644
--- a/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts
@@ -22,6 +22,7 @@
import TranslateVisitor from './TranslateVisitor.js';
import { TranslatorException } from './TranslatorException.js';
+import { Buffer } from 'buffer';
const GO_PACKAGE_NAME = 'gremlingo.';
@@ -256,6 +257,31 @@ export default class GoTranslateVisitor extends TranslateVisitor {
this.sb.push(')');
}
+ visitCharacterLiteral(_ctx: any): void {
+ throw new TranslatorException('Character literals are not supported in Go');
+ }
+
+ visitDurationLiteral(ctx: any): void {
+ const seconds = parseInt(ctx.integerLiteral(0).getText(), 10);
+ const nanos = parseInt(ctx.integerLiteral(1).getText(), 10);
+ const isPositive = ctx.booleanLiteral() === null ||
+ ctx.booleanLiteral().getText() === 'true';
+ // Use BigInt to avoid precision loss for durations over ~104 days
+ const totalNanos = BigInt(seconds) * 1_000_000_000n + BigInt(nanos);
+ this.sb.push(`time.Duration(${isPositive ? totalNanos : -totalNanos})`);
+ }
+
+ visitBinaryLiteral(ctx: any): void {
+ const base64Str = TranslateVisitor.removeFirstAndLastCharacters(ctx.stringLiteral().getText());
+ const bytes = base64ToBytes(base64Str);
+ this.sb.push(GO_PACKAGE_NAME + 'ByteBuffer{Data: []byte{');
+ for (let i = 0; i < bytes.length; i++) {
+ if (i > 0) this.sb.push(',');
+ this.sb.push(String(bytes[i]));
+ }
+ this.sb.push('}}');
+ }
+
visitTraversalStrategy(ctx: any): void {
if (ctx.getChildCount() === 1) {
this.sb.push(GO_PACKAGE_NAME);
@@ -349,6 +375,19 @@ export default class GoTranslateVisitor extends TranslateVisitor {
}
}
+/**
+ * Decodes a base64 string into an array of unsigned byte values.
+ */
+function base64ToBytes(base64: string): number[] {
+ if (base64 === '') return [];
+ const buf = Buffer.from(base64, 'base64');
+ const bytes: number[] = new Array(buf.length);
+ for (let i = 0; i < buf.length; i++) {
+ bytes[i] = buf[i];
+ }
+ return bytes;
+}
+
class SymbolHelper {
private static readonly TO_GO_MAP: Record<string, string> = {
'OUT': 'Out',
diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts
index 4058377cd2..f061975e2b 100644
--- a/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts
@@ -114,6 +114,32 @@ export default class GroovyTranslateVisitor extends TranslateVisitor {
this.sb.push(')');
}
+ visitCharacterLiteral(ctx: any): void {
+ const text: string = ctx.getText();
+ const withoutSuffix = text.substring(0, text.length - 1);
+ const inner = TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix);
+ this.sb.push("'");
+ this.sb.push(inner);
+ this.sb.push("' as char");
+ }
+
+ visitDurationLiteral(ctx: any): void {
+ const seconds = parseInt(ctx.integerLiteral(0).getText(), 10);
+ const nanos = parseInt(ctx.integerLiteral(1).getText(), 10);
+ const isPositive = ctx.booleanLiteral() === null ||
+ ctx.booleanLiteral().getText() === 'true';
+ this.sb.push(`Duration.ofSeconds(${seconds}, ${nanos})`);
+ if (!isPositive) {
+ this.sb.push('.negated()');
+ }
+ }
+
+ visitBinaryLiteral(ctx: any): void {
+ this.sb.push('ByteBuffer.wrap(Base64.getDecoder().decode(');
+ this.sb.push(ctx.stringLiteral().getText());
+ this.sb.push('))');
+ }
+
visitNullLiteral(ctx: any): void {
if (ctx.parent?.constructor?.name === 'GenericMapNullableArgumentContext') {
this.sb.push('null as Map');
diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts
index b7cf670f3d..fbbe38d1b1 100644
--- a/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts
@@ -200,6 +200,32 @@ export default class JavaTranslateVisitor extends TranslateVisitor {
this.sb.push(')');
}
+ visitCharacterLiteral(ctx: any): void {
+ const text: string = ctx.getText();
+ const withoutSuffix = text.substring(0, text.length - 1);
+ const inner = TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix);
+ this.sb.push("'");
+ this.sb.push(inner);
+ this.sb.push("'");
+ }
+
+ visitDurationLiteral(ctx: any): void {
+ const seconds = parseInt(ctx.integerLiteral(0).getText(), 10);
+ const nanos = parseInt(ctx.integerLiteral(1).getText(), 10);
+ const isPositive = ctx.booleanLiteral() === null ||
+ ctx.booleanLiteral().getText() === 'true';
+ this.sb.push(`Duration.ofSeconds(${seconds}, ${nanos})`);
+ if (!isPositive) {
+ this.sb.push('.negated()');
+ }
+ }
+
+ visitBinaryLiteral(ctx: any): void {
+ this.sb.push('ByteBuffer.wrap(Base64.getDecoder().decode(');
+ this.sb.push(ctx.stringLiteral().getText());
+ this.sb.push('))');
+ }
+
visitGenericRangeLiteral(_ctx: any): void {
throw new TranslatorException('Java does not support range literals');
}
diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/JavascriptTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/JavascriptTranslateVisitor.ts
index e01fb5568b..4ae5020ba1 100644
--- a/gremlin-js/gremlin-javascript/lib/language/translator/JavascriptTranslateVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/translator/JavascriptTranslateVisitor.ts
@@ -185,6 +185,20 @@ export default class JavascriptTranslateVisitor extends TranslateVisitor {
this.visitStringLiteral(ctx.stringLiteral());
this.sb.push(')');
}
+
+ visitCharacterLiteral(_ctx: any): void {
+ throw new TranslatorException('Character literals are not supported in JavaScript');
+ }
+
+ visitDurationLiteral(_ctx: any): void {
+ throw new TranslatorException('Duration literals are not supported in JavaScript');
+ }
+
+ visitBinaryLiteral(ctx: any): void {
+ this.sb.push("Buffer.from(");
+ this.visitStringLiteral(ctx.stringLiteral());
+ this.sb.push(",'base64')");
+ }
}
/**
diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts
index e74eb549c8..e9e4de9967 100644
--- a/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts
@@ -214,6 +214,37 @@ export default class PythonTranslateVisitor extends TranslateVisitor {
this.sb.push(')');
}
+ visitCharacterLiteral(ctx: any): void {
+ const text: string = ctx.getText();
+ const withoutSuffix = text.substring(0, text.length - 1);
+ const inner = TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix);
+ this.sb.push("SingleChar('");
+ this.sb.push(inner);
+ this.sb.push("')");
+ }
+
+ visitDurationLiteral(ctx: any): void {
+ const seconds = parseInt(ctx.integerLiteral(0).getText(), 10);
+ const nanos = parseInt(ctx.integerLiteral(1).getText(), 10);
+ const isPositive = ctx.booleanLiteral() === null ||
+ ctx.booleanLiteral().getText() === 'true';
+ const totalSeconds = isPositive ? seconds : -seconds;
+ // Python's timedelta has microsecond resolution; sub-microsecond nanos are truncated
+ const micros = Math.floor(nanos / 1000);
+ if (micros === 0) {
+ this.sb.push(`timedelta(seconds=${totalSeconds})`);
+ } else {
+ const totalMicros = isPositive ? micros : -micros;
+ this.sb.push(`timedelta(seconds=${totalSeconds},microseconds=${totalMicros})`);
+ }
+ }
+
+ visitBinaryLiteral(ctx: any): void {
+ this.sb.push('base64.b64decode(');
+ this.visitStringLiteral(ctx.stringLiteral());
+ this.sb.push(')');
+ }
+
visitTraversalStrategy(ctx: any): void {
if (ctx.getChildCount() === 1) {
this.sb.push(ctx.getText());
@@ -319,3 +350,5 @@ function convertCamelCaseToSnakeCase(camelCase: string): string {
}
return result;
}
+
+
diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/TranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/TranslateVisitor.ts
index dcf2b77aa1..207a53c40a 100644
--- a/gremlin-js/gremlin-javascript/lib/language/translator/TranslateVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/translator/TranslateVisitor.ts
@@ -394,6 +394,18 @@ export default class TranslateVisitor {
this.sb.push(ctx.getText());
}
+ visitCharacterLiteral(ctx: any): void {
+ this.sb.push(ctx.getText());
+ }
+
+ visitDurationLiteral(ctx: any): void {
+ this.sb.push(ctx.getText());
+ }
+
+ visitBinaryLiteral(ctx: any): void {
+ this.sb.push(ctx.getText());
+ }
+
visitStringLiteral(ctx: any): void {
const text = TranslateVisitor.removeFirstAndLastCharacters(ctx.getText());
this.handleStringLiteralText(text);
diff --git a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
index c25d8c595c..1830a7bb7a 100644
--- a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
@@ -21,6 +21,7 @@ import { P, TextP, EnumValue } from './traversal.js';
import { OptionsStrategy, TraversalStrategy } from './traversal-strategy.js';
import { Long } from '../utils.js';
import { Vertex } from '../structure/graph.js';
+import { Buffer } from 'buffer';
export default class GremlinLang {
private gremlin: string = '';
@@ -125,6 +126,9 @@ export default class GremlinLang {
}
return arg.getGremlinLang().getGremlin('__');
}
+ if (arg instanceof Uint8Array) {
+ return `Binary("${Buffer.from(arg.buffer, arg.byteOffset, arg.byteLength).toString('base64')}")`;
+ }
if (arg instanceof Set) {
const items = [...arg];
if (items.length === 0) return '{}';
diff --git a/gremlin-js/gremlin-javascript/scripts/groovy/generate.groovy b/gremlin-js/gremlin-javascript/scripts/groovy/generate.groovy
index 089706aac7..74fb127730 100644
--- a/gremlin-js/gremlin-javascript/scripts/groovy/generate.groovy
+++ b/gremlin-js/gremlin-javascript/scripts/groovy/generate.groovy
@@ -56,6 +56,7 @@ radishGremlinFile.withWriter('UTF-8') { Writer writer ->
writer.writeLine(
'import * as graphTraversalModule from \'../../lib/process/graph-traversal.js\';\n' +
'import * as traversalModule from \'../../lib/process/traversal.js\';\n' +
+ 'import { Buffer } from \'buffer\';\n' +
'import { TraversalStrategies, VertexProgramStrategy, OptionsStrategy, PartitionStrategy, \n' +
' ReadOnlyStrategy, GraphFilterStrategy, SeedStrategy, SubgraphStrategy, ProductiveByStrategy, \n' +
' LambdaRestrictionStrategy, StandardVerificationStrategy, VertexProgramRestrictionStrategy, \n' +
@@ -102,25 +103,33 @@ radishGremlinFile.withWriter('UTF-8') { Writer writer ->
if (staticTranslate.containsKey(k)) {
writer.writeLine(staticTranslate[k])
} else {
- writer.write(" ")
- writer.write(k)
- writer.write(": [")
- def collected = v.collect { GremlinTranslator.translate(it, Translator.JAVASCRIPT) }
- def uniqueBindings = collected.collect { it.getParameters() }.flatten().unique()
- def gremlinItty = collected.iterator()
- while (gremlinItty.hasNext()) {
- def t = gremlinItty.next()
- writer.write("function({g")
- if (!uniqueBindings.isEmpty()) {
- writer.write(", ")
- writer.write(uniqueBindings.join(", "))
+ try {
+ def collected = v.collect { GremlinTranslator.translate(it, Translator.JAVASCRIPT) }
+ writer.write(" ")
+ writer.write(k)
+ writer.write(": [")
+ def uniqueBindings = collected.collect { it.getParameters() }.flatten().unique()
+ def gremlinItty = collected.iterator()
+ while (gremlinItty.hasNext()) {
+ def t = gremlinItty.next()
+ writer.write("function({g")
+ if (!uniqueBindings.isEmpty()) {
+ writer.write(", ")
+ writer.write(uniqueBindings.join(", "))
+ }
+ writer.write("}) { return ")
+ writer.write(t.getTranslated())
+ writer.write(" }")
+ if (gremlinItty.hasNext()) writer.write(', ')
}
- writer.write("}) { return ")
- writer.write(t.getTranslated())
- writer.write(" }")
- if (gremlinItty.hasNext()) writer.write(', ')
+ writer.writeLine('], ')
+ } catch (ignored) {
+ // emit a placeholder for scenarios with unsupported literals to keep the file
+ // syntactically valid; the scenario will be skipped at test runtime via tag exclusions
+ writer.write(" ")
+ writer.write(k)
+ writer.writeLine(": [function({g}) { return null }], ")
}
- writer.writeLine('], ')
}
}
writer.writeLine('}\n')
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js
index e8ac2d1c1c..08830a407e 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js
@@ -36,6 +36,7 @@ import { toLong } from '../../lib/utils.js';
import anon from '../../lib/process/anonymous-traversal.js';
const __ = statics;
import { deepMembersById } from './element-comparison.js';
+import { Buffer } from 'buffer';
import GremlinLang from "../../lib/process/gremlin-lang.js";
const parsers = [
[ 'str\\[(.*)\\]', (stringValue) => stringValue ], //returns the string value as is
@@ -44,6 +45,7 @@ const parsers = [
[ 'dt\\[(.+)\\]', toDateTime ],
[ 'uuid\\[(.+)\\]', toUuid ],
[ 'd\\[(.*)\\]\\.[bsilfdmn]', toNumeric ],
+ [ 'bin\\[(.*)\\]', toBinary ],
[ 'v\\[(.+)\\]', toVertex ],
[ 'v\\[(.+)\\]\\.id', toVertexId ],
[ 'v\\[(.+)\\]\\.sid', toVertexIdString ],
@@ -448,6 +450,10 @@ function toMerge(value) {
return merge[value];
}
+function toBinary(value) {
+ return Buffer.from(value, 'base64');
+}
+
function splitByElement(s) {
let depth = 0;
let current = '';
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
index 60e3697146..dccd1a3cf3 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
@@ -26,6 +26,7 @@
import * as graphTraversalModule from '../../lib/process/graph-traversal.js';
import * as traversalModule from '../../lib/process/traversal.js';
+import { Buffer } from 'buffer';
import { TraversalStrategies, VertexProgramStrategy, OptionsStrategy, PartitionStrategy,
ReadOnlyStrategy, GraphFilterStrategy, SeedStrategy, SubgraphStrategy, ProductiveByStrategy,
LambdaRestrictionStrategy, StandardVerificationStrategy, VertexProgramRestrictionStrategy,
@@ -226,6 +227,11 @@ const gremlins = {
g_V_valuesXintX_asNumberXGType_BIGINTX_isXtypeOfXGType_BIGINTXX_project_byXidentityX_byXmathXaddX999XXX: [function({g}) { return g.addV("data").property("int", 50) }, function({g}) { return g.V().values("int").asNumber(GType.bigInt).is(P.typeOf(GType.bigInt)).project("original", "added").by(__.identity()).by(__.math("_ + 999")) }],
g_injectX777X_asNumberXGType_BIGINTX_isXtypeOfXGType_BIGINTXX_groupCount: [function({g}) { return g.inject(777).asNumber(GType.bigInt).is(P.typeOf(GType.bigInt)).groupCount() }],
g_V_valuesXageX_isXtypeOfXGType_BIGINTXX: [function({g}) { return g.V().values("age").is(P.typeOf(GType.bigInt)) }],
+ g_injectXBinaryXAQIDXX: [function({g}) { return g.inject(Buffer.from("AQID",'base64')) }],
+ g_injectXBinaryXemptyXX: [function({g}) { return g.inject(Buffer.from("",'base64')) }],
+ g_injectXBinaryXAA_eqeqXX: [function({g}) { return g.inject(Buffer.from("AA==",'base64')) }],
+ g_valuesXblobX_isXtypeOfXGType_BINARYXX: [function({g}) { return g.addV("data").property("blob", Buffer.from("AQID",'base64')) }, function({g}) { return g.V().values("blob").is(P.typeOf(GType.binary)) }],
+ g_injectXBinaryXAQIDXX_isXeqXBinaryXAQIDXXX: [function({g}) { return g.inject(Buffer.from("AQID",'base64')).is(P.eq(Buffer.from("AQID",'base64'))) }],
g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX: [function({g}) { return g.addV("data").property("int", 5) }, function({g}) { return g.V().values("int").asNumber(GType.byte).is(P.typeOf(GType.byte)) }],
g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_mathXaddX20XX: [function({g}) { return g.addV("data").property("int", 10) }, function({g}) { return g.V().values("int").asNumber(GType.byte).is(P.typeOf(GType.byte)).math("_ + 20") }],
g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_isXltX10XX: [function({g}) { return g.addV("data").property("int", 7) }, function({g}) { return g.V().values("int").asNumber(GType.byte).is(P.typeOf(GType.byte)).is(P.lt(10)) }],
@@ -234,6 +240,11 @@ const gremlins = {
g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_chooseXisXeqX12XX_constantXtwelveX_constantXotherXX: [function({g}) { return g.addV("data").property("int", 12) }, function({g}) { return g.V().values("int").asNumber(GType.byte).is(P.typeOf(GType.byte)).choose(__.is(P.eq(12)), __.constant("twelve"), __.constant("other")) }],
g_injectX15X_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_groupCount: [function({g}) { return g.inject(15).asNumber(GType.byte).is(P.typeOf(GType.byte)).groupCount() }],
g_V_valuesXageX_isXtypeOfXGType_BYTEXX: [function({g}) { return g.V().values("age").is(P.typeOf(GType.byte)) }],
+ g_injectXaX: [function({g}) { return null }],
+ g_injectXescaped_quoteX: [function({g}) { return null }],
+ g_injectXunicodeX: [function({g}) { return null }],
+ g_valuesXinitialX_isXtypeOfXGType_CHARXX: [function({g}) { return null }],
+ g_injectXaX_isXeqXaXX: [function({g}) { return null }],
g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX: [function({g}) { return g.addV("event").property("datetime", new Date('2023-08-08T00:00Z')) }, function({g}) { return g.V().values("datetime").is(P.typeOf(GType.datetime)) }],
g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX_project_byXidentityX_byXdateAddXDT_dayX1XX: [function({g}) { return g.addV("event").property("datetime", new Date('2023-08-08T00:00Z')) }, function({g}) { return g.V().values("datetime").is(P.typeOf(GType.datetime)).project("original", "nextDay").by(__.identity()).by(__.dateAdd(DT.day, 1)) }],
g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX_dateDiffXdatetimeX2023_08_10XX: [function({g}) { return g.addV("event").property("datetime", new Date('2023-08-08T00:00Z')) }, function({g}) { return g.V().values("datetime").is(P.typeOf(GType.datetime)).dateDiff(new Date('2023-08-08T00:00:30Z')) }],
@@ -253,6 +264,14 @@ const gremlins = {
g_V_valuesXdoubleX_isXtypeOfXGType_DOUBLEXX_order_byXascX: [function({g}) { return g.addV("data").property("double", 3.2).addV("data").property("double", 1.8).addV("data").property("double", 2.5) }, function({g}) { return g.V().values("double").is(P.typeOf(GType.double)).order().by(Order.asc) }],
g_injectX5_5dX_isXtypeOfXGType_DOUBLEXX_groupCount: [function({g}) { return g.inject(5.5).is(P.typeOf(GType.double)).groupCount() }],
g_V_valuesXageX_isXtypeOfXGType_DOUBLEXX: [function({g}) { return g.V().values("age").is(P.typeOf(GType.double)) }],
+ g_injectXDurationX9000_0XX: [function({g}) { return null }],
+ g_injectXDurationX0_0XX: [function({g}) { return null }],
+ g_injectXDurationX0_500000000XX: [function({g}) { return null }],
+ g_injectXDurationX30_0XX: [function({g}) { return null }],
+ g_injectXDurationX30_0_falseXX: [function({g}) { return null }],
+ g_injectXDurationX1_500000000_falseXX: [function({g}) { return null }],
+ g_valuesXlengthX_isXtypeOfXGType_DURATIONXX: [function({g}) { return null }],
+ g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX: [function({g}) { return null }],
g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX: [function({g}) { return g.addV("data").property("float", 2.5) }, function({g}) { return g.V().values("float").asNumber(GType.float).is(P.typeOf(GType.float)) }],
g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_mathXmulX2XX: [function({g}) { return g.addV("data").property("float", 3.0) }, function({g}) { return g.V().values("float").asNumber(GType.float).is(P.typeOf(GType.float)).math("_ * 2") }],
g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_isXeqX1_5XX: [function({g}) { return g.addV("data").property("float", 1.5) }, function({g}) { return g.V().values("float").asNumber(GType.float).is(P.typeOf(GType.float)).is(P.eq(1.5)) }],
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/world.js b/gremlin-js/gremlin-javascript/test/cucumber/world.js
index 1e66c8e73b..563abb8ce8 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/world.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/world.js
@@ -110,6 +110,14 @@ Before({tags: "@StepWrite"}, function() {
return 'skipped'
})
+Before({tags: "@DataChar"}, function() {
+ return 'skipped'
+})
+
+Before({tags: "@DataDuration"}, function() {
+ return 'skipped'
+})
+
function getVertices(connection) {
const g = anon.traversal().withRemote(connection);
return g.V().group().by('name').by(__.tail()).next().then(it => {
diff --git a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
index 9d359a163c..be3a39c460 100644
--- a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
@@ -18,6 +18,7 @@
*/
import assert from 'assert';
+import { Buffer } from 'buffer';
import { GraphTraversalSource, statics as __, CardinalityValue } from '../../lib/process/graph-traversal.js';
import { P, TextP, t as T, order as Order, scope as Scope, column as Column,
operator as Operator, pop as Pop, cardinality as Cardinality,
@@ -413,5 +414,25 @@ describe('GremlinLang', function () {
"g.inject(['age':Cardinality.single(33)])"
);
});
+
+ // Binary - Uint8Array type (JS-specific, distinct from Buffer)
+ it('should handle Uint8Array', function () {
+ assert.strictEqual(g.inject(new Uint8Array([1, 2, 3])).getGremlinLang().getGremlin(), 'g.inject(Binary("AQID"))');
+ });
+
+ it('should handle empty Uint8Array', function () {
+ assert.strictEqual(g.inject(new Uint8Array([])).getGremlinLang().getGremlin(), 'g.inject(Binary(""))');
+ });
+
+ it('should handle Uint8Array with base64 padding', function () {
+ assert.strictEqual(g.inject(new Uint8Array([0])).getGremlinLang().getGremlin(), 'g.inject(Binary("AA=="))');
+ });
+
+ it('should handle Uint8Array view with offset', function () {
+ const ab = new ArrayBuffer(5);
+ new Uint8Array(ab).set([0, 1, 2, 3, 4]);
+ const view = new Uint8Array(ab, 1, 3); // bytes [1, 2, 3]
+ assert.strictEqual(g.inject(view).getGremlinLang().getGremlin(), 'g.inject(Binary("AQID"))');
+ });
});
});
\ No newline at end of file
diff --git a/gremlin-js/gremlin-javascript/test/unit/translator/gremlin-translator-test.js b/gremlin-js/gremlin-javascript/test/unit/translator/gremlin-translator-test.js
index 02866c3b37..264aa319c6 100644
--- a/gremlin-js/gremlin-javascript/test/unit/translator/gremlin-translator-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/translator/gremlin-translator-test.js
@@ -64,6 +64,8 @@ describe('GremlinTranslator', function () {
['g.inject(NaN)', 'g.inject(Number.NaN)'],
['g.inject(Infinity)', 'g.inject(Number.POSITIVE_INFINITY)'],
['g.inject(-Infinity)', 'g.inject(Number.NEGATIVE_INFINITY)'],
+ // Binary literal
+ ['g.inject(Binary("AQID"))', 'g.inject(Buffer.from("AQID",\'base64\'))'],
// terminal steps — pass through as-is in JavaScript
['g.V().toList()', 'g.V().toList()'],
['g.V().toSet()', 'g.V().toSet()'],
@@ -134,6 +136,20 @@ describe('GremlinTranslator', function () {
TranslatorException,
);
});
+
+ it('throws TranslatorException for character literal', function () {
+ assert.throws(
+ () => GremlinTranslator.translate('g.inject("a"c)'),
+ TranslatorException,
+ );
+ });
+
+ it('throws TranslatorException for duration literal', function () {
+ assert.throws(
+ () => GremlinTranslator.translate('g.inject(Duration(9000,0))'),
+ TranslatorException,
+ );
+ });
});
});
@@ -180,6 +196,12 @@ describe('PythonTranslateVisitor', function () {
['g.inject([1,2,3])', 'g.inject([1, 2, 3])'],
// Set literal (non-empty)
['g.inject({1,2,3})', 'g.inject({1, 2, 3})'],
+ // Character literal
+ ['g.inject("a"c)', "g.inject(SingleChar('a'))"],
+ // Duration literal
+ ['g.inject(Duration(9000,0))', 'g.inject(timedelta(seconds=9000))'],
+ // Binary literal
+ ['g.inject(Binary("AQID"))', "g.inject(base64.b64decode('AQID'))"],
// Map literal — T.id → T.id_ in Python
['g.addV("person").property(T.id, [(T.label): "person"])', "g.add_v('person').property(T.id_, { T.label: 'person' })"],
// terminal steps — camelCase → snake_case
@@ -259,6 +281,10 @@ describe('GoTranslateVisitor', function () {
['g.inject(NaN)', 'g.Inject(math.NaN())'],
['g.inject(Infinity)', 'g.Inject(math.Inf(1))'],
['g.inject(-Infinity)', 'g.Inject(math.Inf(-1))'],
+ // Duration literal
+ ['g.inject(Duration(9000,0))', 'g.Inject(time.Duration(9000000000000))'],
+ // Binary literal
+ ['g.inject(Binary("AQID"))', 'g.Inject(gremlingo.ByteBuffer{Data: []byte{1,2,3}})'],
// null → nil
['g.V().has("name", null)', 'g.V().Has("name", nil)'],
// terminal steps — PascalCase
@@ -302,6 +328,13 @@ describe('GoTranslateVisitor', function () {
TranslatorException,
);
});
+
+ it('throws TranslatorException for character literal', function () {
+ assert.throws(
+ () => GremlinTranslator.translate('g.inject("a"c)', 'g', 'GO'),
+ TranslatorException,
+ );
+ });
});
});
@@ -336,6 +369,12 @@ describe('DotNetTranslateVisitor', function () {
// Collections
['g.inject([1,2,3])', 'g.Inject<object>(new List<object> { 1, 2, 3 })'],
['g.inject({1,2,3})', 'g.Inject<object>(new HashSet<object> { 1, 2, 3 })'],
+ // Character literal
+ ['g.inject("a"c)', "g.Inject<object>('a')"],
+ // Duration literal
+ ['g.inject(Duration(9000,0))', 'g.Inject<object>(TimeSpan.FromTicks(90000000000L))'],
+ // Binary literal
+ ['g.inject(Binary("AQID"))', 'g.Inject<object>(Convert.FromBase64String("AQID"))'],
// Map literal
['g.addV("person").property(T.id, [(T.label): "person"])', 'g.AddV((string) "person").Property(T.Id, new Dictionary<object, object> {{ T.Label, "person" }})'],
// values gets <object>
@@ -421,6 +460,12 @@ describe('JavaTranslateVisitor', function () {
['g.inject([1,2,3])', 'g.inject(new ArrayList<Object>() {{ add(1); add(2); add(3); }})'],
// Set literal
['g.inject({1,2,3})', 'g.inject(new HashSet<Object>() {{ add(1); add(2); add(3); }})'],
+ // Character literal
+ ['g.inject("a"c)', "g.inject('a')"],
+ // Duration literal
+ ['g.inject(Duration(9000,0))', 'g.inject(Duration.ofSeconds(9000, 0))'],
+ // Binary literal
+ ['g.inject(Binary("AQID"))', 'g.inject(ByteBuffer.wrap(Base64.getDecoder().decode("AQID")))'],
// Map literal
['g.addV("person").property(T.id, [(T.label): "person"])', 'g.addV("person").property(T.id, new LinkedHashMap<Object, Object>() {{ put(T.label, "person"); }})'],
// terminal steps — pass through as-is in Java
@@ -499,6 +544,12 @@ describe('GroovyTranslateVisitor', function () {
['g.inject([1,2,3])', 'g.inject([1, 2, 3])'],
// Set literal — Groovy: [items] as Set
['g.inject({1,2,3})', 'g.inject([1, 2, 3] as Set)'],
+ // Character literal
+ ['g.inject("a"c)', "g.inject('a' as char)"],
+ // Duration literal
+ ['g.inject(Duration(9000,0))', 'g.inject(Duration.ofSeconds(9000, 0))'],
+ // Binary literal
+ ['g.inject(Binary("AQID"))', 'g.inject(ByteBuffer.wrap(Base64.getDecoder().decode("AQID")))'],
// Map literal — Groovy preserves parens on expression keys
['g.addV("person").property(T.id, [(T.label): "person"])', 'g.addV("person").property(T.id, [(T.label):"person"])'],
// null as Map in mergeV
@@ -584,6 +635,12 @@ describe('AnonymizedTranslateVisitor', function () {
['g.inject([1,2,3])', 'g.inject(list0)'],
// set → set
['g.inject({1,2,3})', 'g.inject(set0)'],
+ // character → character
+ ['g.inject("a"c)', 'g.inject(character0)'],
+ // duration -> duration
+ ['g.inject(Duration(9000,0))', 'g.inject(duration0)'],
+ // binary → bytebuffer
+ ['g.inject(Binary("AQID"))', 'g.inject(bytebuffer0)'],
// map → map
['g.addV("person").property(T.id, [(T.label): "person"])', 'g.addV(string0).property(T.id, map0)'],
// enums/traversal steps pass through unchanged
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4
index 5272aeeb1c..6798ebde55 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -1633,6 +1633,9 @@ genericLiteral
| nestedTraversal
| terminatedTraversal
| uuidLiteral
+ | characterLiteral
+ | durationLiteral
+ | binaryLiteral
| genericMapLiteral
;
@@ -1659,11 +1662,12 @@ mapEntry
stringLiteral
: EmptyStringLiteral
| NonEmptyStringLiteral
+ | EmptyStringSuffixLiteral
+ | StringSuffixLiteral
;
stringNullableLiteral
- : EmptyStringLiteral
- | NonEmptyStringLiteral
+ : stringLiteral
| K_NULL
;
@@ -1712,6 +1716,18 @@ uuidLiteral
| K_UUID LPAREN stringLiteral RPAREN
;
+characterLiteral
+ : CharacterLiteral
+ ;
+
+durationLiteral
+ : K_DURATIONC LPAREN integerLiteral COMMA integerLiteral (COMMA booleanLiteral)? RPAREN
+ ;
+
+binaryLiteral
+ : K_BINARYC LPAREN stringLiteral RPAREN
+ ;
+
nakedKey
: Identifier
@@ -1753,6 +1769,7 @@ keyword
| K_BIGINT
| K_BIGINTU
| K_BINARY
+ | K_BINARYC
| K_BINARYU
| K_BOTH
| K_BOTHU
@@ -1804,6 +1821,7 @@ keyword
| K_DROP
| K_DT
| K_DURATION
+ | K_DURATIONC
| K_DURATIONU
| K_E
| K_EDGE
@@ -2063,6 +2081,7 @@ K_BIGDECIMALU: 'BIGDECIMAL';
K_BIGINT: 'bigInt';
K_BIGINTU: 'BIGINT';
K_BINARY: 'binary';
+K_BINARYC: 'Binary';
K_BINARYU: 'BINARY';
K_BOOLEAN: 'boolean';
K_BOOLEANU: 'BOOLEAN';
@@ -2114,6 +2133,7 @@ K_DOUBLEU: 'DOUBLE';
K_DROP: 'drop';
K_DT: 'DT';
K_DURATION: 'duration';
+K_DURATIONC: 'Duration';
K_DURATIONU: 'DURATION';
K_E: 'E';
K_EDGE: 'edge';
@@ -2500,6 +2520,26 @@ SignedInfLiteral
// String Literals
+// Character literal is a single character string with a 'c' suffix, modeled after printf's %c.
+// Must appear before NonEmptyStringLiteral for longest-match priority.
+CharacterLiteral
+ : '"' DoubleQuotedStringCharacter '"' 'c'
+ | '\'' SingleQuotedStringCharacter '\'' 'c'
+ ;
+
+// String literal with explicit 's' suffix. The suffix is optional (no suffix defaults to string),
+// but can be used to force string interpretation for single-character values.
+// Must appear before NonEmptyStringLiteral for longest-match priority.
+StringSuffixLiteral
+ : '"' DoubleQuotedStringCharacters '"' 's'
+ | '\'' SingleQuotedStringCharacters '\'' 's'
+ ;
+
+EmptyStringSuffixLiteral
+ : '""' 's'
+ | '\'\'' 's'
+ ;
+
// String literal is customized since Java only allows double quoted strings where Groovy supports single quoted
// literals also. A side effect of this is ANTLR will not be able to parse single character string literals with
// single quoted so we instead remove char literal altogether and only have string literal in lexer tokens.
diff --git a/gremlin-language/src/test/resources/incorrect-gremlin-values.txt b/gremlin-language/src/test/resources/incorrect-gremlin-values.txt
index 06cc6dce8f..daa3c8b90c 100644
--- a/gremlin-language/src/test/resources/incorrect-gremlin-values.txt
+++ b/gremlin-language/src/test/resources/incorrect-gremlin-values.txt
@@ -16,4 +16,6 @@ test"
[1, {a: 123}]
{{a: 123}}
[Scope.local: 100]
-[Order.asc: 100]
\ No newline at end of file
+[Order.asc: 100]
+""c
+"ab"c
\ No newline at end of file
diff --git a/gremlin-python/build/generate.groovy b/gremlin-python/build/generate.groovy
index aaa66090d5..e96964dbf6 100644
--- a/gremlin-python/build/generate.groovy
+++ b/gremlin-python/build/generate.groovy
@@ -55,9 +55,11 @@ radishGremlinFile.withWriter('UTF-8') { Writer writer ->
writer.writeLine(
'from radish import world\n' +
+ 'import base64\n' +
'import datetime\n' +
'import uuid\n' +
- 'from gremlin_python.statics import long, bigint, bigdecimal, GremlinType\n' +
+ 'from datetime import timedelta\n' +
+ 'from gremlin_python.statics import long, bigint, bigdecimal, SingleChar, GremlinType\n' +
'from gremlin_python.process.anonymous_traversal import traversal\n' +
'from gremlin_python.process.strategies import *\n' +
'from gremlin_python.process.traversal import TraversalStrategy\n' +
diff --git a/gremlin-python/docker-compose.yml b/gremlin-python/docker-compose.yml
index 0c78152e81..617c1ae15a 100644
--- a/gremlin-python/docker-compose.yml
+++ b/gremlin-python/docker-compose.yml
@@ -65,17 +65,13 @@ services:
pytest &&
radish --junit-xml python-reports/feature-graphbinary-bulked-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.graphbinary-v4.0' --user-data='bulked=true' &&
radish --junit-xml python-reports/feature-graphbinary-params-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.graphbinary-v4.0' --user-data='parameterize=true' &&
- radish --junit-xml python-reports/feature-graphbinary-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.graphbinary-v4.0' &&
- radish --junit-xml python-reports/feature-graphson-params-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.gremlin-v4.0+json' --user-data='parameterize=true' &&
- radish --junit-xml python-reports/feature-graphson-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.gremlin-v4.0+json' ;
+ radish --junit-xml python-reports/feature-graphbinary-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.graphbinary-v4.0' ;
elif [ -n \"$PYTEST_ARGS\" ]; then
pytest $PYTEST_ARGS ;
elif [ -n \"$RADISH_ARGS\" ]; then
radish --junit-xml python-reports/feature-graphbinary-bulked-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.graphbinary-v4.0' --user-data='bulked=true' $RADISH_ARGS &&
radish --junit-xml python-reports/feature-graphbinary-params-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.graphbinary-v4.0' --user-data='parameterize=true' $RADISH_ARGS &&
- radish --junit-xml python-reports/feature-graphbinary-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.graphbinary-v4.0' $RADISH_ARGS &&
- radish --junit-xml python-reports/feature-graphson-params-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.gremlin-v4.0+json' --user-data='parameterize=true' $RADISH_ARGS &&
- radish --junit-xml python-reports/feature-graphson-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.gremlin-v4.0+json' $RADISH_ARGS ;
+ radish --junit-xml python-reports/feature-graphbinary-result.xml -f dots -e -t -b ./tests/feature ./gremlin-test --user-data='serializer=application/vnd.graphbinary-v4.0' $RADISH_ARGS ;
fi
&& pip install .
&& echo 'Running examples...'
diff --git a/gremlin-python/src/main/python/gremlin_python/process/traversal.py b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
index edc6deefa6..4a531d5845 100644
--- a/gremlin-python/src/main/python/gremlin_python/process/traversal.py
+++ b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
@@ -27,8 +27,9 @@ from aenum import Enum
from gremlin_python.structure.graph import Vertex, Edge, Path, Property
from .. import statics
-from ..statics import long, SingleByte, short, bigint, BigDecimal
-from datetime import datetime
+from ..statics import long, SingleByte, SingleChar, short, bigint, BigDecimal
+from datetime import datetime, timedelta
+import base64
class Traversal(object):
@@ -844,6 +845,9 @@ class GremlinLang(object):
if arg is None:
return 'null'
+ if isinstance(arg, SingleChar):
+ return f'{arg!r}c'
+
if isinstance(arg, str):
return f'{arg!r}' # use repr() format for canonical string rep
# return f'"{arg}"'
@@ -879,6 +883,20 @@ class GremlinLang(object):
if isinstance(arg, uuid.UUID):
return f'UUID("{arg}")'
+ if isinstance(arg, timedelta):
+ is_negative = arg.total_seconds() < 0
+ abs_td = -arg if is_negative else arg
+ # Use integer components directly to avoid float precision loss
+ seconds = abs_td.days * 86400 + abs_td.seconds
+ nanos = abs_td.microseconds * 1000
+ if is_negative:
+ return f'Duration({seconds},{nanos},false)'
+ else:
+ return f'Duration({seconds},{nanos})'
+
+ if isinstance(arg, bytes):
+ return f'Binary("{base64.b64encode(arg).decode("ascii")}")'
+
if isinstance(arg, Enum):
tmp = str(arg)
if tmp.endswith('_'):
diff --git a/gremlin-python/src/main/python/tests/feature/feature_steps.py b/gremlin-python/src/main/python/tests/feature/feature_steps.py
index f82b65ab55..c5365d009f 100644
--- a/gremlin-python/src/main/python/tests/feature/feature_steps.py
+++ b/gremlin-python/src/main/python/tests/feature/feature_steps.py
@@ -18,11 +18,12 @@
#
from collections.abc import Iterable
-from datetime import datetime
+from datetime import datetime, timedelta
+import base64
import json
import re
import uuid
-from gremlin_python.statics import long, bigdecimal
+from gremlin_python.statics import long, bigdecimal, SingleChar
from gremlin_python.structure.graph import Path, Vertex, Graph, Edge, VertexProperty, Property
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.process.graph_traversal import __
@@ -345,6 +346,17 @@ def _split_by_element(s):
return results
+def _parse_duration(dur_str):
+ """Parses a duration from seconds,nanos[,isPositive] format into a timedelta."""
+ parts = dur_str.split(",")
+ seconds = int(parts[0].strip())
+ nanos = int(parts[1].strip())
+ is_positive = len(parts) < 3 or parts[2].strip() == "true"
+ # timedelta has microsecond resolution; sub-microsecond nanos are truncated
+ td = timedelta(seconds=seconds, microseconds=nanos // 1000)
+ return td if is_positive else -td
+
+
def _convert(val, ctx):
graph_name = ctx.graph_name
if isinstance(val, dict): # convert dictionary keys/values
@@ -366,6 +378,12 @@ def _convert(val, ctx):
elif isinstance(val, str) and re.match(r"^uuid\[.*\]$", val): # parse uuid
name = val[5:-1] # strip 'uuid[...]' or similar format
return uuid.UUID(name)
+ elif isinstance(val, str) and re.match(r"^char\[.\]$", val): # parse char
+ return SingleChar(val[5:-1])
+ elif isinstance(val, str) and re.match(r"^dur\[.*\]$", val): # parse duration
+ return _parse_duration(val[4:-1])
+ elif isinstance(val, str) and re.match(r"^bin\[.*\]$", val): # parse binary
+ return base64.b64decode(val[4:-1])
elif isinstance(val, str) and re.match(r"^d\[NaN\]$", val): # parse nan
return float("nan")
elif isinstance(val, str) and re.match(r"^d\[Infinity\]$", val): # parse +inf
diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py
index 235b5f9384..9cd84e34ff 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -25,9 +25,11 @@
from radish import world
+import base64
import datetime
import uuid
-from gremlin_python.statics import long, bigint, bigdecimal, GremlinType
+from datetime import timedelta
+from gremlin_python.statics import long, bigint, bigdecimal, SingleChar, GremlinType
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.process.strategies import *
from gremlin_python.process.traversal import TraversalStrategy
@@ -199,6 +201,11 @@ world.gremlins = {
'g_V_valuesXintX_asNumberXGType_BIGINTX_isXtypeOfXGType_BIGINTXX_project_byXidentityX_byXmathXaddX999XXX': [(lambda g:g.add_v('data').property('int', 50)), (lambda g:g.V().values('int').as_number(GType.BIGINT).is_(P.type_of(GType.BIGINT)).project('original', 'added').by(__.identity()).by(__.math('_ + 999')))],
'g_injectX777X_asNumberXGType_BIGINTX_isXtypeOfXGType_BIGINTXX_groupCount': [(lambda g:g.inject(777).as_number(GType.BIGINT).is_(P.type_of(GType.BIGINT)).group_count())],
'g_V_valuesXageX_isXtypeOfXGType_BIGINTXX': [(lambda g:g.V().values('age').is_(P.type_of(GType.BIGINT)))],
+ 'g_injectXBinaryXAQIDXX': [(lambda g:g.inject(base64.b64decode('AQID')))],
+ 'g_injectXBinaryXemptyXX': [(lambda g:g.inject(base64.b64decode('')))],
+ 'g_injectXBinaryXAA_eqeqXX': [(lambda g:g.inject(base64.b64decode('AA==')))],
+ 'g_valuesXblobX_isXtypeOfXGType_BINARYXX': [(lambda g:g.add_v('data').property('blob', base64.b64decode('AQID'))), (lambda g:g.V().values('blob').is_(P.type_of(GType.BINARY)))],
+ 'g_injectXBinaryXAQIDXX_isXeqXBinaryXAQIDXXX': [(lambda g:g.inject(base64.b64decode('AQID')).is_(P.eq(base64.b64decode('AQID'))))],
'g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX': [(lambda g:g.add_v('data').property('int', 5)), (lambda g:g.V().values('int').as_number(GType.BYTE).is_(P.type_of(GType.BYTE)))],
'g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_mathXaddX20XX': [(lambda g:g.add_v('data').property('int', 10)), (lambda g:g.V().values('int').as_number(GType.BYTE).is_(P.type_of(GType.BYTE)).math('_ + 20'))],
'g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_isXltX10XX': [(lambda g:g.add_v('data').property('int', 7)), (lambda g:g.V().values('int').as_number(GType.BYTE).is_(P.type_of(GType.BYTE)).is_(P.lt(10)))],
@@ -207,6 +214,11 @@ world.gremlins = {
'g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_chooseXisXeqX12XX_constantXtwelveX_constantXotherXX': [(lambda g:g.add_v('data').property('int', 12)), (lambda g:g.V().values('int').as_number(GType.BYTE).is_(P.type_of(GType.BYTE)).choose(__.is_(P.eq(12)), __.constant('twelve'), __.constant('other')))],
'g_injectX15X_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX_groupCount': [(lambda g:g.inject(15).as_number(GType.BYTE).is_(P.type_of(GType.BYTE)).group_count())],
'g_V_valuesXageX_isXtypeOfXGType_BYTEXX': [(lambda g:g.V().values('age').is_(P.type_of(GType.BYTE)))],
+ 'g_injectXaX': [(lambda g:g.inject(SingleChar('a')))],
+ 'g_injectXescaped_quoteX': [(lambda g:g.inject(SingleChar('\"')))],
+ 'g_injectXunicodeX': [(lambda g:g.inject(SingleChar('\u00E9')))],
+ 'g_valuesXinitialX_isXtypeOfXGType_CHARXX': [(lambda g:g.add_v('data').property('initial', SingleChar('a'))), (lambda g:g.V().values('initial').is_(P.type_of(GType.CHAR)))],
+ 'g_injectXaX_isXeqXaXX': [(lambda g:g.inject(SingleChar('a')).is_(P.eq(SingleChar('a'))))],
'g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX': [(lambda g:g.add_v('event').property('datetime', datetime.datetime.fromisoformat('2023-08-08T00:00+00:00'))), (lambda g:g.V().values('datetime').is_(P.type_of(GType.DATETIME)))],
'g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX_project_byXidentityX_byXdateAddXDT_dayX1XX': [(lambda g:g.add_v('event').property('datetime', datetime.datetime.fromisoformat('2023-08-08T00:00+00:00'))), (lambda g:g.V().values('datetime').is_(P.type_of(GType.DATETIME)).project('original', 'nextDay').by(__.identity()).by(__.date_add(DT.day, 1)))],
'g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX_dateDiffXdatetimeX2023_08_10XX': [(lambda g:g.add_v('event').property('datetime', datetime.datetime.fromisoformat('2023-08-08T00:00+00:00'))), (lambda g:g.V().values('datetime').is_(P.type_of(GType.DATETIME)).date_diff(datetime.datetime.fromisoformat('2023-08-08T00:00:30+00:00')))],
@@ -226,6 +238,14 @@ world.gremlins = {
'g_V_valuesXdoubleX_isXtypeOfXGType_DOUBLEXX_order_byXascX': [(lambda g:g.add_v('data').property('double', 3.2).add_v('data').property('double', 1.8).add_v('data').property('double', 2.5)), (lambda g:g.V().values('double').is_(P.type_of(GType.DOUBLE)).order().by(Order.asc))],
'g_injectX5_5dX_isXtypeOfXGType_DOUBLEXX_groupCount': [(lambda g:g.inject(5.5).is_(P.type_of(GType.DOUBLE)).group_count())],
'g_V_valuesXageX_isXtypeOfXGType_DOUBLEXX': [(lambda g:g.V().values('age').is_(P.type_of(GType.DOUBLE)))],
+ 'g_injectXDurationX9000_0XX': [(lambda g:g.inject(timedelta(seconds=9000)))],
+ 'g_injectXDurationX0_0XX': [(lambda g:g.inject(timedelta(seconds=0)))],
+ 'g_injectXDurationX0_500000000XX': [(lambda g:g.inject(timedelta(seconds=0,microseconds=500000)))],
+ 'g_injectXDurationX30_0XX': [(lambda g:g.inject(timedelta(seconds=30)))],
+ 'g_injectXDurationX30_0_falseXX': [(lambda g:g.inject(timedelta(seconds=-30)))],
+ 'g_injectXDurationX1_500000000_falseXX': [(lambda g:g.inject(timedelta(seconds=-1,microseconds=-500000)))],
+ 'g_valuesXlengthX_isXtypeOfXGType_DURATIONXX': [(lambda g:g.add_v('data').property('length', timedelta(seconds=9000))), (lambda g:g.V().values('length').is_(P.type_of(GType.DURATION)))],
+ 'g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX': [(lambda g:g.inject(timedelta(seconds=9000)).is_(P.gt(timedelta(seconds=3600))))],
'g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX': [(lambda g:g.add_v('data').property('float', 2.5)), (lambda g:g.V().values('float').as_number(GType.FLOAT).is_(P.type_of(GType.FLOAT)))],
'g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_mathXmulX2XX': [(lambda g:g.add_v('data').property('float', 3.0)), (lambda g:g.V().values('float').as_number(GType.FLOAT).is_(P.type_of(GType.FLOAT)).math('_ * 2'))],
'g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_isXeqX1_5XX': [(lambda g:g.add_v('data').property('float', 1.5)), (lambda g:g.V().values('float').as_number(GType.FLOAT).is_(P.type_of(GType.FLOAT)).is_(P.eq(1.5)))],
diff --git a/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py b/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
index b4c738a7ac..0f1351e8c1 100644
--- a/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
+++ b/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
@@ -24,7 +24,7 @@ import uuid
from gremlin_python.process.strategies import ReadOnlyStrategy, SubgraphStrategy, OptionsStrategy, PartitionStrategy
from gremlin_python.process.traversal import within, eq, T, Order, Scope, Column, Operator, P, Pop, Cardinality, \
between, inside, WithOptions, ShortestPath, starting_with, ending_with, containing, gt, lte, GValue
-from gremlin_python.statics import SingleByte, short, long, bigint, BigDecimal
+from gremlin_python.statics import SingleByte, short, long, bigint, BigDecimal, SingleChar
from gremlin_python.structure.graph import Graph, Vertex, Edge, VertexProperty
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.process.graph_traversal import __
@@ -473,13 +473,25 @@ class TestGremlinLang(object):
# 119
tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30, tzinfo=timezone.utc)),
"g.addV('test').property('date',datetime(\"2021-02-01T09:30:00+00:00\"))"])
+
# 120
tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30, tzinfo=timezone(timedelta(hours=7)))),
"g.addV('test').property('date',datetime(\"2021-02-01T09:30:00+07:00\"))"])
+
# 121
tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30, tzinfo=timezone(timedelta(hours=-5)))),
"g.addV('test').property('date',datetime(\"2021-02-01T09:30:00-05:00\"))"])
+ # Character - backslash (not in feature file due to Gherkin escaping issues)
+ # 122
+ tests.append([g.inject(SingleChar('\\')),
+ "g.inject('\\\\'c)"])
+
+ # Character - single quote (no feature equivalent)
+ # 123
+ tests.append([g.inject(SingleChar("'")),
+ "g.inject(\"'\"c)"])
+
for t in range(len(tests)):
gremlin_lang = tests[t][0].gremlin_lang.get_gremlin()
assert gremlin_lang == tests[t][1]
diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
index 09987991ac..45f29c17b6 100644
--- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
+++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
@@ -85,6 +85,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.Base64;
import java.util.UUID;
import static org.apache.commons.text.StringEscapeUtils.escapeJava;
@@ -165,6 +168,16 @@ public final class StepDefinition {
add(Pair.with(Pattern.compile("dt\\[(.*)\\]"), s -> String.format("datetime('%s')", s)));
+ add(Pair.with(Pattern.compile("char\\[(.)\\]"), s -> String.format("\"%s\"c", s)));
+ add(Pair.with(Pattern.compile("dur\\[(.*)\\]"), s -> {
+ final String[] parts = s.split(",");
+ if (parts.length == 2)
+ return String.format("Duration(%s, %s)", parts[0].trim(), parts[1].trim());
+ else
+ return String.format("Duration(%s, %s, %s)", parts[0].trim(), parts[1].trim(), parts[2].trim());
+ }));
+ add(Pair.with(Pattern.compile("bin\\[(.*)\\]"), s -> String.format("Binary(\"%s\")", s)));
+
add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.id"), s -> world.convertIdToScript(g.V().has("name", s).id().next(), Vertex.class)));
add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.sid"), s -> world.convertIdToScript(g.V().has("name", s).id().next(), Vertex.class)));
add(Pair.with(Pattern.compile("e\\[(.+)\\]\\.id"), s -> world.convertIdToScript(getEdgeId(g, s), Edge.class)));
@@ -237,6 +250,17 @@ public final class StepDefinition {
add(Pair.with(Pattern.compile("dt\\[(.*)\\]"), DatetimeHelper::parse));
add(Pair.with(Pattern.compile("uuid\\[(.*)\\]"), UUID::fromString));
+ add(Pair.with(Pattern.compile("char\\[(.)\\]"), s -> s.charAt(0)));
+ add(Pair.with(Pattern.compile("dur\\[(.*)\\]"), s -> {
+ final String[] parts = s.split(",");
+ final long seconds = Long.parseLong(parts[0].trim());
+ final int nanos = Integer.parseInt(parts[1].trim());
+ final boolean isPositive = parts.length < 3 || Boolean.parseBoolean(parts[2].trim());
+ final Duration d = Duration.ofSeconds(seconds, nanos);
+ return isPositive ? d : d.negated();
+ }));
+ add(Pair.with(Pattern.compile("bin\\[(.*)\\]"), s -> ByteBuffer.wrap(Base64.getDecoder().decode(s))));
+
add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.id"), s -> g.V().has("name", s).id().next()));
add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.sid"), s -> g.V().has("name", s).id().next().toString()));
add(Pair.with(Pattern.compile("v\\[(.+)\\]"), s -> detachVertex(g.V().has("name", s).next())));
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 35df2cff6a..44163ea0f7 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
@@ -3010,6 +3010,103 @@
}
]
},
+ {
+ "scenario": "g_injectXBinaryXAQIDXX",
+ "traversals": [
+ {
+ "original": "g.inject(Binary(\"AQID\"))",
+ "language": "g.inject(Binary(\"AQID\"))",
+ "canonical": "g.inject(Binary(\"AQID\"))",
+ "anonymized": "g.inject(bytebuffer0)",
+ "dotnet": "g.Inject<object>(Convert.FromBase64String(\"AQID\"))",
+ "go": "g.Inject(gremlingo.ByteBuffer{Data: []byte{1,2,3}})",
+ "groovy": "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\")))",
+ "java": "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\")))",
+ "javascript": "g.inject(Buffer.from(\"AQID\",'base64'))",
+ "python": "g.inject(base64.b64decode('AQID'))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXBinaryXemptyXX",
+ "traversals": [
+ {
+ "original": "g.inject(Binary(\"\"))",
+ "language": "g.inject(Binary(\"\"))",
+ "canonical": "g.inject(Binary(\"\"))",
+ "anonymized": "g.inject(bytebuffer0)",
+ "dotnet": "g.Inject<object>(Convert.FromBase64String(\"\"))",
+ "go": "g.Inject(gremlingo.ByteBuffer{Data: []byte{}})",
+ "groovy": "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"\")))",
+ "java": "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"\")))",
+ "javascript": "g.inject(Buffer.from(\"\",'base64'))",
+ "python": "g.inject(base64.b64decode(''))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXBinaryXAA_eqeqXX",
+ "traversals": [
+ {
+ "original": "g.inject(Binary(\"AA==\"))",
+ "language": "g.inject(Binary(\"AA==\"))",
+ "canonical": "g.inject(Binary(\"AA==\"))",
+ "anonymized": "g.inject(bytebuffer0)",
+ "dotnet": "g.Inject<object>(Convert.FromBase64String(\"AA==\"))",
+ "go": "g.Inject(gremlingo.ByteBuffer{Data: []byte{0}})",
+ "groovy": "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"AA==\")))",
+ "java": "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"AA==\")))",
+ "javascript": "g.inject(Buffer.from(\"AA==\",'base64'))",
+ "python": "g.inject(base64.b64decode('AA=='))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_valuesXblobX_isXtypeOfXGType_BINARYXX",
+ "traversals": [
+ {
+ "original": "g.addV(\"data\").property(\"blob\", Binary(\"AQID\"))",
+ "language": "g.addV(\"data\").property(\"blob\", Binary(\"AQID\"))",
+ "canonical": "g.addV(\"data\").property(\"blob\", Binary(\"AQID\"))",
+ "anonymized": "g.addV(string0).property(string1, bytebuffer0)",
+ "dotnet": "g.AddV((string) \"data\").Property(\"blob\", Convert.FromBase64String(\"AQID\"))",
+ "go": "g.AddV(\"data\").Property(\"blob\", gremlingo.ByteBuffer{Data: []byte{1,2,3}})",
+ "groovy": "g.addV(\"data\").property(\"blob\", ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\")))",
+ "java": "g.addV(\"data\").property(\"blob\", ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\")))",
+ "javascript": "g.addV(\"data\").property(\"blob\", Buffer.from(\"AQID\",'base64'))",
+ "python": "g.add_v('data').property('blob', base64.b64decode('AQID'))"
+ },
+ {
+ "original": "g.V().values(\"blob\").is(P.typeOf(GType.BINARY))",
+ "language": "g.V().values(\"blob\").is(P.typeOf(GType.BINARY))",
+ "canonical": "g.V().values(\"blob\").is(P.typeOf(GType.BINARY))",
+ "anonymized": "g.V().values(string0).is(P.typeOf(GType.BINARY))",
+ "dotnet": "g.V().Values<object>(\"blob\").Is(P.TypeOf(GType.Binary))",
+ "go": "g.V().Values(\"blob\").Is(gremlingo.P.TypeOf(gremlingo.GType.Binary))",
+ "groovy": "g.V().values(\"blob\").is(P.typeOf(GType.BINARY))",
+ "java": "g.V().values(\"blob\").is(P.typeOf(GType.BINARY))",
+ "javascript": "g.V().values(\"blob\").is(P.typeOf(GType.binary))",
+ "python": "g.V().values('blob').is_(P.type_of(GType.BINARY))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXBinaryXAQIDXX_isXeqXBinaryXAQIDXXX",
+ "traversals": [
+ {
+ "original": "g.inject(Binary(\"AQID\")).is(P.eq(Binary(\"AQID\")))",
+ "language": "g.inject(Binary(\"AQID\")).is(P.eq(Binary(\"AQID\")))",
+ "canonical": "g.inject(Binary(\"AQID\")).is(P.eq(Binary(\"AQID\")))",
+ "anonymized": "g.inject(bytebuffer0).is(P.eq(bytebuffer0))",
+ "dotnet": "g.Inject<object>(Convert.FromBase64String(\"AQID\")).Is(P.Eq(Convert.FromBase64String(\"AQID\")))",
+ "go": "g.Inject(gremlingo.ByteBuffer{Data: []byte{1,2,3}}).Is(gremlingo.P.Eq(gremlingo.ByteBuffer{Data: []byte{1,2,3}}))",
+ "groovy": "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\"))).is(P.eq(ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\"))))",
+ "java": "g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\"))).is(P.eq(ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\"))))",
+ "javascript": "g.inject(Buffer.from(\"AQID\",'base64')).is(P.eq(Buffer.from(\"AQID\",'base64')))",
+ "python": "g.inject(base64.b64decode('AQID')).is_(P.eq(base64.b64decode('AQID')))"
+ }
+ ]
+ },
{
"scenario": "g_V_valuesXintX_asNumberXGType_BYTEX_isXtypeOfXGType_BYTEXX",
"traversals": [
@@ -3218,6 +3315,93 @@
}
]
},
+ {
+ "scenario": "g_injectXaX",
+ "traversals": [
+ {
+ "original": "g.inject(\"a\"c)",
+ "language": "g.inject(\"a\"c)",
+ "canonical": "g.inject(\"a\"c)",
+ "anonymized": "g.inject(character0)",
+ "dotnet": "g.Inject<object>('a')",
+ "groovy": "g.inject('a' as char)",
+ "java": "g.inject('a')",
+ "python": "g.inject(SingleChar('a'))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXescaped_quoteX",
+ "traversals": [
+ {
+ "original": "g.inject(\"\\\"\"c)",
+ "language": "g.inject(\"\\\"\"c)",
+ "canonical": "g.inject(\"\\\"\"c)",
+ "anonymized": "g.inject(character0)",
+ "dotnet": "g.Inject<object>('\\\"')",
+ "groovy": "g.inject('\\\"' as char)",
+ "java": "g.inject('\\\"')",
+ "python": "g.inject(SingleChar('\\\"'))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXunicodeX",
+ "traversals": [
+ {
+ "original": "g.inject(\"\\u00E9\"c)",
+ "language": "g.inject(\"\\u00E9\"c)",
+ "canonical": "g.inject(\"\\u00E9\"c)",
+ "anonymized": "g.inject(character0)",
+ "dotnet": "g.Inject<object>('\\u00E9')",
+ "groovy": "g.inject('\\u00E9' as char)",
+ "java": "g.inject('\\u00E9')",
+ "python": "g.inject(SingleChar('\\u00E9'))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_valuesXinitialX_isXtypeOfXGType_CHARXX",
+ "traversals": [
+ {
+ "original": "g.addV(\"data\").property(\"initial\", \"a\"c)",
+ "language": "g.addV(\"data\").property(\"initial\", \"a\"c)",
+ "canonical": "g.addV(\"data\").property(\"initial\", \"a\"c)",
+ "anonymized": "g.addV(string0).property(string1, character0)",
+ "dotnet": "g.AddV((string) \"data\").Property(\"initial\", 'a')",
+ "groovy": "g.addV(\"data\").property(\"initial\", 'a' as char)",
+ "java": "g.addV(\"data\").property(\"initial\", 'a')",
+ "python": "g.add_v('data').property('initial', SingleChar('a'))"
+ },
+ {
+ "original": "g.V().values(\"initial\").is(P.typeOf(GType.CHAR))",
+ "language": "g.V().values(\"initial\").is(P.typeOf(GType.CHAR))",
+ "canonical": "g.V().values(\"initial\").is(P.typeOf(GType.CHAR))",
+ "anonymized": "g.V().values(string0).is(P.typeOf(GType.CHAR))",
+ "dotnet": "g.V().Values<object>(\"initial\").Is(P.TypeOf(GType.Char))",
+ "go": "g.V().Values(\"initial\").Is(gremlingo.P.TypeOf(gremlingo.GType.Char))",
+ "groovy": "g.V().values(\"initial\").is(P.typeOf(GType.CHAR))",
+ "java": "g.V().values(\"initial\").is(P.typeOf(GType.CHAR))",
+ "javascript": "g.V().values(\"initial\").is(P.typeOf(GType.char))",
+ "python": "g.V().values('initial').is_(P.type_of(GType.CHAR))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXaX_isXeqXaXX",
+ "traversals": [
+ {
+ "original": "g.inject(\"a\"c).is(P.eq(\"a\"c))",
+ "language": "g.inject(\"a\"c).is(P.eq(\"a\"c))",
+ "canonical": "g.inject(\"a\"c).is(P.eq(\"a\"c))",
+ "anonymized": "g.inject(character0).is(P.eq(character0))",
+ "dotnet": "g.Inject<object>('a').Is(P.Eq('a'))",
+ "groovy": "g.inject('a' as char).is(P.eq('a' as char))",
+ "java": "g.inject('a').is(P.eq('a'))",
+ "python": "g.inject(SingleChar('a')).is_(P.eq(SingleChar('a')))"
+ }
+ ]
+ },
{
"scenario": "g_V_valuesXdatetimeX_isXtypeOfXGType_DATETIMEXX",
"traversals": [
@@ -3709,6 +3893,146 @@
}
]
},
+ {
+ "scenario": "g_injectXDurationX9000_0XX",
+ "traversals": [
+ {
+ "original": "g.inject(Duration(9000, 0))",
+ "language": "g.inject(Duration(9000,0))",
+ "canonical": "g.inject(Duration(9000,0))",
+ "anonymized": "g.inject(duration0)",
+ "dotnet": "g.Inject<object>(TimeSpan.FromTicks(90000000000L))",
+ "go": "g.Inject(time.Duration(9000000000000))",
+ "groovy": "g.inject(Duration.ofSeconds(9000, 0))",
+ "java": "g.inject(Duration.ofSeconds(9000, 0))",
+ "python": "g.inject(timedelta(seconds=9000))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXDurationX0_0XX",
+ "traversals": [
+ {
+ "original": "g.inject(Duration(0, 0))",
+ "language": "g.inject(Duration(0,0))",
+ "canonical": "g.inject(Duration(0,0))",
+ "anonymized": "g.inject(duration0)",
+ "dotnet": "g.Inject<object>(TimeSpan.FromTicks(0L))",
+ "go": "g.Inject(time.Duration(0))",
+ "groovy": "g.inject(Duration.ofSeconds(0, 0))",
+ "java": "g.inject(Duration.ofSeconds(0, 0))",
+ "python": "g.inject(timedelta(seconds=0))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXDurationX0_500000000XX",
+ "traversals": [
+ {
+ "original": "g.inject(Duration(0, 500000000))",
+ "language": "g.inject(Duration(0,500000000))",
+ "canonical": "g.inject(Duration(0,500000000))",
+ "anonymized": "g.inject(duration0)",
+ "dotnet": "g.Inject<object>(TimeSpan.FromTicks(5000000L))",
+ "go": "g.Inject(time.Duration(500000000))",
+ "groovy": "g.inject(Duration.ofSeconds(0, 500000000))",
+ "java": "g.inject(Duration.ofSeconds(0, 500000000))",
+ "python": "g.inject(timedelta(seconds=0,microseconds=500000))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXDurationX30_0XX",
+ "traversals": [
+ {
+ "original": "g.inject(Duration(30, 0))",
+ "language": "g.inject(Duration(30,0))",
+ "canonical": "g.inject(Duration(30,0))",
+ "anonymized": "g.inject(duration0)",
+ "dotnet": "g.Inject<object>(TimeSpan.FromTicks(300000000L))",
+ "go": "g.Inject(time.Duration(30000000000))",
+ "groovy": "g.inject(Duration.ofSeconds(30, 0))",
+ "java": "g.inject(Duration.ofSeconds(30, 0))",
+ "python": "g.inject(timedelta(seconds=30))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXDurationX30_0_falseXX",
+ "traversals": [
+ {
+ "original": "g.inject(Duration(30, 0, false))",
+ "language": "g.inject(Duration(30,0,false))",
+ "canonical": "g.inject(Duration(30,0,false))",
+ "anonymized": "g.inject(duration0)",
+ "dotnet": "g.Inject<object>(TimeSpan.FromTicks(300000000L).Negate())",
+ "go": "g.Inject(time.Duration(-30000000000))",
+ "groovy": "g.inject(Duration.ofSeconds(30, 0).negated())",
+ "java": "g.inject(Duration.ofSeconds(30, 0).negated())",
+ "python": "g.inject(timedelta(seconds=-30))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXDurationX1_500000000_falseXX",
+ "traversals": [
+ {
+ "original": "g.inject(Duration(1, 500000000, false))",
+ "language": "g.inject(Duration(1,500000000,false))",
+ "canonical": "g.inject(Duration(1,500000000,false))",
+ "anonymized": "g.inject(duration0)",
+ "dotnet": "g.Inject<object>(TimeSpan.FromTicks(15000000L).Negate())",
+ "go": "g.Inject(time.Duration(-1500000000))",
+ "groovy": "g.inject(Duration.ofSeconds(1, 500000000).negated())",
+ "java": "g.inject(Duration.ofSeconds(1, 500000000).negated())",
+ "python": "g.inject(timedelta(seconds=-1,microseconds=-500000))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_valuesXlengthX_isXtypeOfXGType_DURATIONXX",
+ "traversals": [
+ {
+ "original": "g.addV(\"data\").property(\"length\", Duration(9000, 0))",
+ "language": "g.addV(\"data\").property(\"length\", Duration(9000,0))",
+ "canonical": "g.addV(\"data\").property(\"length\", Duration(9000,0))",
+ "anonymized": "g.addV(string0).property(string1, duration0)",
+ "dotnet": "g.AddV((string) \"data\").Property(\"length\", TimeSpan.FromTicks(90000000000L))",
+ "go": "g.AddV(\"data\").Property(\"length\", time.Duration(9000000000000))",
+ "groovy": "g.addV(\"data\").property(\"length\", Duration.ofSeconds(9000, 0))",
+ "java": "g.addV(\"data\").property(\"length\", Duration.ofSeconds(9000, 0))",
+ "python": "g.add_v('data').property('length', timedelta(seconds=9000))"
+ },
+ {
+ "original": "g.V().values(\"length\").is(P.typeOf(GType.DURATION))",
+ "language": "g.V().values(\"length\").is(P.typeOf(GType.DURATION))",
+ "canonical": "g.V().values(\"length\").is(P.typeOf(GType.DURATION))",
+ "anonymized": "g.V().values(string0).is(P.typeOf(GType.DURATION))",
+ "dotnet": "g.V().Values<object>(\"length\").Is(P.TypeOf(GType.Duration))",
+ "go": "g.V().Values(\"length\").Is(gremlingo.P.TypeOf(gremlingo.GType.Duration))",
+ "groovy": "g.V().values(\"length\").is(P.typeOf(GType.DURATION))",
+ "java": "g.V().values(\"length\").is(P.typeOf(GType.DURATION))",
+ "javascript": "g.V().values(\"length\").is(P.typeOf(GType.duration))",
+ "python": "g.V().values('length').is_(P.type_of(GType.DURATION))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX",
+ "traversals": [
+ {
+ "original": "g.inject(Duration(9000, 0)).is(P.gt(Duration(3600, 0)))",
+ "language": "g.inject(Duration(9000,0)).is(P.gt(Duration(3600,0)))",
+ "canonical": "g.inject(Duration(9000,0)).is(P.gt(Duration(3600,0)))",
+ "anonymized": "g.inject(duration0).is(P.gt(duration1))",
+ "dotnet": "g.Inject<object>(TimeSpan.FromTicks(90000000000L)).Is(P.Gt(TimeSpan.FromTicks(36000000000L)))",
+ "go": "g.Inject(time.Duration(9000000000000)).Is(gremlingo.P.Gt(time.Duration(3600000000000)))",
+ "groovy": "g.inject(Duration.ofSeconds(9000, 0)).is(P.gt(Duration.ofSeconds(3600, 0)))",
+ "java": "g.inject(Duration.ofSeconds(9000, 0)).is(P.gt(Duration.ofSeconds(3600, 0)))",
+ "python": "g.inject(timedelta(seconds=9000)).is_(P.gt(timedelta(seconds=3600)))"
+ }
+ ]
+ },
{
"scenario": "g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX",
"traversals": [
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Binary.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Binary.feature
new file mode 100644
index 0000000000..c8f365c268
--- /dev/null
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Binary.feature
@@ -0,0 +1,82 @@
+# 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.
+
+@StepClassData @DataBinary
+Feature: Data - BINARY
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXBinaryXAQIDXX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Binary("AQID"))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | bin[AQID] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXBinaryXemptyXX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Binary(""))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | bin[] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXBinaryXAA_eqeqXX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Binary("AA=="))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | bin[AA==] |
+
+ Scenario: g_valuesXblobX_isXtypeOfXGType_BINARYXX
+ Given the empty graph
+ And the graph initializer of
+ """
+ g.addV("data").property("blob", Binary("AQID"))
+ """
+ And the traversal of
+ """
+ g.V().values("blob").is(P.typeOf(GType.BINARY))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | bin[AQID] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXBinaryXAQIDXX_isXeqXBinaryXAQIDXXX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Binary("AQID")).is(P.eq(Binary("AQID")))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | bin[AQID] |
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Char.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Char.feature
new file mode 100644
index 0000000000..94606864c0
--- /dev/null
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Char.feature
@@ -0,0 +1,82 @@
+# 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.
+
+@StepClassData @DataChar
+Feature: Data - CHAR
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXaX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject("a"c)
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | char[a] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXescaped_quoteX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject("\""c)
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | char["] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXunicodeX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject("\u00E9"c)
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | char[é] |
+
+ Scenario: g_valuesXinitialX_isXtypeOfXGType_CHARXX
+ Given the empty graph
+ And the graph initializer of
+ """
+ g.addV("data").property("initial", "a"c)
+ """
+ And the traversal of
+ """
+ g.V().values("initial").is(P.typeOf(GType.CHAR))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | char[a] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXaX_isXeqXaXX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject("a"c).is(P.eq("a"c))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | char[a] |
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Duration.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Duration.feature
new file mode 100644
index 0000000000..5f2da18e77
--- /dev/null
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Duration.feature
@@ -0,0 +1,118 @@
+# 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.
+
+@StepClassData @DataDuration
+Feature: Data - DURATION
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXDurationX9000_0XX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Duration(9000, 0))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | dur[9000,0] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXDurationX0_0XX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Duration(0, 0))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | dur[0,0] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXDurationX0_500000000XX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Duration(0, 500000000))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | dur[0,500000000] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXDurationX30_0XX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Duration(30, 0))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | dur[30,0] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXDurationX30_0_falseXX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Duration(30, 0, false))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | dur[30,0,false] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXDurationX1_500000000_falseXX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Duration(1, 500000000, false))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | dur[1,500000000,false] |
+
+ Scenario: g_valuesXlengthX_isXtypeOfXGType_DURATIONXX
+ Given the empty graph
+ And the graph initializer of
+ """
+ g.addV("data").property("length", Duration(9000, 0))
+ """
+ And the traversal of
+ """
+ g.V().values("length").is(P.typeOf(GType.DURATION))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | dur[9000,0] |
+
+ @GraphComputerVerificationInjectionNotSupported
+ Scenario: g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX
+ Given the empty graph
+ And the traversal of
+ """
+ g.inject(Duration(9000, 0)).is(P.gt(Duration(3600, 0)))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | dur[9000,0] |
(tinkerpop) 02/03: Fix null map key parsing in GenericLiteralVisitor
Posted by ke...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
kenhuuu pushed a commit to branch stringify-params
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 4c246f6cf5e3399b3e7b853a1432ca3589b129a1
Author: Ken Hu <10...@users.noreply.github.com>
AuthorDate: Tue Apr 14 16:14:43 2026 -0700
Fix null map key parsing in GenericLiteralVisitor
The visitor treated all keyword map keys as their text representation,
so a null key in [null:"value"] was parsed as the String "null" instead
of Java null. This broke round-tripping maps with null keys through
GremlinLang serialization and ANTLR parsing.
---
.../tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java | 3 ++-
.../tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java | 4 ++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
index 6a7f621c10..d133fb6bb1 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
@@ -349,7 +349,8 @@ public class GenericLiteralVisitor extends DefaultGremlinBaseVisitor<Object> {
} else if (kctx instanceof GremlinParser.GenericMapLiteralContext) {
key = visitGenericMapLiteral((GremlinParser.GenericMapLiteralContext) kctx);
} else if (kctx instanceof GremlinParser.KeywordContext) {
- key = ((GremlinParser.KeywordContext) kctx).getText();
+ final String keywordText = ((GremlinParser.KeywordContext) kctx).getText();
+ key = keywordText.equals("null") ? null : keywordText;
} else if (kctx instanceof GremlinParser.NakedKeyContext) {
key = ((GremlinParser.NakedKeyContext) kctx).getText();
} else if (kctx instanceof TerminalNode) {
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java
index ed519b6b31..71f4acce19 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java
@@ -642,6 +642,10 @@ public class GeneralLiteralVisitorTest {
{"[{'x'}: 'x']", createMap(new Object[]{new HashSet<>(Arrays.asList("x")), "x"})},
{"[['x']: ['x',{'y'}]]", createMap(new Object[]{Arrays.asList("x"), Arrays.asList("x", new HashSet<>(Arrays.asList("y")))})},
{"[['x']: ['x',['y']]]", createMap(new Object[]{Arrays.asList("x"), Arrays.asList("x", Arrays.asList("y"))})},
+ {"[null:'x']", createMap(new Object[]{null, "x"})},
+ {"[null:null]", createMap(new Object[]{null, null})},
+ {"[null:'x','y':'z']", createMap(new Object[]{null, "x", "y", "z"})},
+ {"['null':'x']", createMap(new Object[]{"null", "x"})},
});
}
(tinkerpop) 03/03: TINKERPOP-3247 Convert request bindings to gremlin-lang string format
Posted by ke...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
kenhuuu pushed a commit to branch stringify-params
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 6b3e2fc3883320a1feed86277fe5acf902db57d3
Author: Ken Hu <10...@users.noreply.github.com>
AuthorDate: Tue Apr 14 19:02:44 2026 -0700
TINKERPOP-3247 Convert request bindings to gremlin-lang string format
Moving parameters from binary-serialized maps to string representations
makes the request side pure text, decoupling Gremlin language evolution
from GraphBinary versioning. New types can be introduced in minor/patch
versions without touching GraphBinary, eliminating the need for a major
version bump across the ecosystem for every new request-side type.
The asParameter() fallback is replaced with an unsupportedType flag that
records the class name and falls back to toString(). A flag is used
rather than throwing because embedded Traversals build GremlinLang as a
side effect but never send it, so unknown types must not break
execution. All other GLVs throw immediately since they have no embedded
mode and the early throw gives better errors.
Client APIs accept both map and string bindings (but not both at the
same time) because users who use the Client directly with raw Gremlin
strings shouldn't need to hand-craft gremlin-lang map literals. Mixing
both throws immediately to prevent silent loss where one set of bindings
would be discarded.
Edge and VertexProperty tests that relied on the old asParameter
fallback were removed because they aren't supported in gremlin-lang.
---
CHANGELOG.asciidoc | 4 +
docs/src/dev/provider/index.asciidoc | 4 +-
docs/src/upgrade/release-4.x.x.asciidoc | 10 +
.../language/grammar/GremlinQueryParser.java | 105 +++++-
.../language/grammar/ParameterMapVisitor.java | 102 +++++
.../gremlin/process/traversal/GremlinLang.java | 89 ++++-
.../language/grammar/ParameterMapVisitorTest.java | 220 +++++++++++
.../gremlin/process/traversal/GremlinLangTest.java | 417 ++++++++++++++++++++-
.../Gremlin.Net/Driver/Messages/RequestMessage.cs | 34 +-
.../Driver/Remote/DriverRemoteConnection.cs | 9 +-
.../Gremlin.Net/Process/Traversal/GremlinLang.cs | 46 ++-
.../Driver/DriverRemoteConnectionTests.cs | 6 +-
.../Driver/Messages/RequestMessageTests.cs | 33 +-
.../Process/Traversal/GraphTraversalSourceTests.cs | 1 -
.../Process/Traversal/GremlinLangTests.cs | 71 +++-
.../Process/Traversal/TraversalTests.cs | 1 -
.../tinkerpop/gremlin/driver/RequestOptions.java | 49 ++-
.../driver/remote/DriverRemoteConnection.java | 6 +
.../driver/remote/TransactionRemoteConnection.java | 6 +
gremlin-go/driver/client.go | 13 +-
gremlin-go/driver/gremlinlang.go | 60 ++-
gremlin-go/driver/gremlinlang_test.go | 84 +++++
gremlin-go/driver/request.go | 4 +-
gremlin-go/driver/requestOptions.go | 25 +-
gremlin-go/driver/requestOptions_test.go | 37 +-
gremlin-go/driver/request_test.go | 29 ++
gremlin-js/gremlin-javascript/lib/driver/client.ts | 12 +-
.../lib/driver/driver-remote-connection.ts | 6 +-
.../lib/driver/request-message.ts | 21 +-
.../gremlin-javascript/lib/process/gremlin-lang.ts | 17 +-
.../test/unit/gremlin-lang-test.js | 59 +++
.../main/python/gremlin_python/driver/client.py | 20 +-
.../driver/driver_remote_connection.py | 6 +-
.../python/gremlin_python/process/traversal.py | 42 ++-
.../python/tests/integration/driver/test_client.py | 8 +-
.../python/tests/unit/process/test_gremlin_lang.py | 96 +++--
.../python/tests/unit/process/test_strategies.py | 7 +-
.../apache/tinkerpop/gremlin/server/Context.java | 10 +
.../server/handler/HttpGremlinEndpointHandler.java | 50 ++-
.../server/handler/HttpRequestMessageDecoder.java | 41 +-
.../gremlin/server/util/GremlinError.java | 7 +
.../GremlinDriverTransactionIntegrateTest.java | 33 ++
.../server/GremlinServerHttpIntegrateTest.java | 22 +-
.../gremlin/server/GremlinServerIntegrateTest.java | 81 ++--
.../gremlin/server/HttpDriverIntegrateTest.java | 39 +-
.../handler/HttpRequestMessageDecoderTest.java | 9 +-
.../gremlin/util/message/RequestMessage.java | 25 +-
.../ser/AbstractGraphSONMessageSerializerV4.java | 2 +-
.../util/ser/binary/RequestMessageSerializer.java | 2 +-
.../gremlin/util/message/RequestMessageTest.java | 44 ++-
.../util/ser/binary/MessageSerializerTest.java | 2 +-
.../tinkergraph/structure/TinkerGraphTest.java | 26 ++
52 files changed, 1797 insertions(+), 355 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 374bb7f073..0215d20a97 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -28,6 +28,10 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Added Gremlator, a single page web application, that translates Gremlin into various programming languages like Javascript and Python.
* Removed `uuid` dependency from `gremlin-javascript` in favor of the built-in `globalThis.crypto.randomUUID()`.
* Added `Character`, `Binary`, `Duration` to the `gremlin-lang` grammar, GLVs, and Translators.
+* Modified request parameters from `Map<String, Object>` to gremlin-lang compatible `String`.
+* Modified HTTP API to expect gremlin-lang strings for parameters and update all GLVs to send requests in new format.
+* Added string parameter parsing to `GremlinServer` to prevent traversal injection and excessive nesting depths.
+* Modified all GLVs to detect unsupported types in `GremlinLang` and throw consistent error for that case.
[[release-4-0-0-beta-2]]
=== TinkerPop 4.0.0-beta.2 (April 1, 2026)
diff --git a/docs/src/dev/provider/index.asciidoc b/docs/src/dev/provider/index.asciidoc
index 87fc8c4a99..7654807a90 100644
--- a/docs/src/dev/provider/index.asciidoc
+++ b/docs/src/dev/provider/index.asciidoc
@@ -1004,7 +1004,7 @@ Gremlin-Hints: <hints>
{
"gremlin": string,
"timeoutMs": number,
- "bindings": object,
+ "bindings": string,
"g": string,
"language" : string,
"materializeProperties": string,
@@ -1064,7 +1064,7 @@ the serializer specified by the `Content-Type` header. The following are the key
|Key |Description |Value |Required
|gremlin |The Gremlin query to execute. |String containing script |Yes
|timeoutMs |The maximum time a query is allowed to execute in milliseconds. |Number between 0 and 2^31-1 |No
-|bindings |A map used during query execution. Its usage depends on "language". For "gremlin-groovy", these are the variable bindings. For "gremlin-lang", these are the parameter bindings. |Object (Map) |No
+|bindings |A gremlin-lang string that contains a map used during query execution. Its usage depends on "language". For "gremlin-groovy", these are the variable bindings. For "gremlin-lang", these are the parameter bindings. |Object (Map) |No
|g |The name of the graph traversal source to which the query applies. Default: "g" |String containing traversal source name |No
|language |The name of the ScriptEngine to use to parse the gremlin query. Default: "gremlin-lang" |String containing ScriptEngine name |No
|materializeProperties |Whether to include all properties for results. One of "tokens" or "all". |String |No
diff --git a/docs/src/upgrade/release-4.x.x.asciidoc b/docs/src/upgrade/release-4.x.x.asciidoc
index 0d54666422..0b9279ea26 100644
--- a/docs/src/upgrade/release-4.x.x.asciidoc
+++ b/docs/src/upgrade/release-4.x.x.asciidoc
@@ -110,6 +110,16 @@ g.V().has("blob",P.eq(Binary("AQID")))
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3153[TINKERPOP-3153]
+==== `gremlin-lang` based Parameters
+
+Bindings/parameters that are sent as part of the request are now `gremlin-lang` string maps rather than an actual Map
+that would have been serialized based on the serializer used for the request. A side effect of this is that the map key
+must be a valid Java identifier. This means that certain items can no longer be sent like `#jsr223` control flags that
+were used for the `gremlin-groovy` ScriptEngine. Ensure that you are using valid Java identifiers (e.g. start with a
+letter, no spaces, can't use reserved keywords, etc.) for keys.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3247[TINKERPOP-3247]
+
== TinkerPop 4.0.0-beta.2
*Release Date: April 1, 2026*
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinQueryParser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinQueryParser.java
index d17fa5a972..871233fa39 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinQueryParser.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinQueryParser.java
@@ -18,7 +18,6 @@
*/
package org.apache.tinkerpop.gremlin.language.grammar;
-import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.atn.PredictionMode;
@@ -26,6 +25,10 @@ import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.Collection;
+import java.util.Map;
+import javax.lang.model.SourceVersion;
+
/**
* Parses Gremlin strings to an {@code Object}, typically to a {@link Traversal}.
*/
@@ -44,21 +47,15 @@ public class GremlinQueryParser {
* Parse Gremlin string using a specified {@link GremlinAntlrToJava} object.
*/
public static Object parse(final String query, final GremlinVisitor<Object> visitor) {
- final CharStream in = CharStreams.fromString(query);
- final GremlinLexer lexer = new GremlinLexer(in);
- lexer.removeErrorListeners();
- lexer.addErrorListener(errorListener);
-
+ final GremlinLexer lexer = createLexer(query);
final CommonTokenStream tokens = new CommonTokenStream(lexer);
// Setup error handler on parser
- final GremlinParser parser = new GremlinParser(tokens);
+ final GremlinParser parser = createParser(tokens);
// SLL prediction mode is faster than the LL prediction mode when parsing the grammar,
// but it does not cover parsing all types of input. We use the SLL by default, and fallback
// to LL mode if fails to parse the query.
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
- parser.removeErrorListeners();
- parser.addErrorListener(errorListener);
GremlinParser.QueryListContext queryContext;
try {
@@ -91,4 +88,94 @@ public class GremlinQueryParser {
throw new GremlinParserException("Failed to interpret Gremlin query: " + ex.getMessage(), ex);
}
}
+
+ /**
+ * Parses a gremlin-lang map literal string into a {@code Map<String, Object>} for use as parameters.
+ * <p>
+ * Uses {@link ParameterMapVisitor} to prevent traversal injection and validates that all keys are strings
+ * and no values contain traversals.
+ *
+ * @param parameterMapString the gremlin-lang map literal string (e.g. {@code [x:1,y:"marko"]}) or {@code null}/empty
+ * @return the parsed and validated parameter map
+ * @throws GremlinParserException if parsing fails or validation detects invalid content
+ */
+ public static Map<String, Object> parseParameters(final String parameterMapString) {
+ if (parameterMapString == null || parameterMapString.isEmpty()) {
+ return Map.of();
+ }
+
+ final GremlinParser parser = createParser(parameterMapString);
+ final GremlinParser.GenericMapLiteralContext mapCtx = parser.genericMapLiteral();
+
+ final ParameterMapVisitor visitor = new ParameterMapVisitor(new GremlinAntlrToJava());
+ final Map<Object, Object> rawMap = (Map<Object, Object>) visitor.visitGenericMapLiteral(mapCtx);
+
+ if (rawMap == null) {
+ return Map.of();
+ }
+
+ for (final Map.Entry<?, ?> entry : rawMap.entrySet()) {
+ if (!(entry.getKey() instanceof String)) {
+ throw new GremlinParserException(
+ String.format("Parameter map keys must be String, found: %s",
+ entry.getKey() == null ? "null" : entry.getKey().getClass().getSimpleName()));
+ }
+ final String key = (String) entry.getKey();
+ if (!SourceVersion.isIdentifier(key)) {
+ throw new GremlinParserException(
+ String.format("Parameter map key must be a valid identifier: %s", key));
+ }
+ validateParameterValue(entry.getValue());
+ }
+
+ return (Map<String, Object>) (Map<?, ?>) rawMap;
+ }
+
+ /**
+ * Recursively validates that a parameter value does not contain a {@link Traversal}. Nested validation is needed
+ * because steps like mergeV iterate map values, so a Traversal hiding inside a nested map or collection would still
+ * be dangerous.
+ */
+ private static void validateParameterValue(final Object value) {
+ if (value instanceof Traversal) {
+ throw new GremlinParserException("Traversals are not allowed as parameter values");
+ }
+ if (value instanceof Map) {
+ for (final Object v : ((Map<?, ?>) value).values()) {
+ validateParameterValue(v);
+ }
+ }
+ if (value instanceof Collection) {
+ for (final Object v : (Collection<?>) value) {
+ validateParameterValue(v);
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link GremlinParser} from the given input string.
+ */
+ private static GremlinParser createParser(final String input) {
+ return createParser(new CommonTokenStream(createLexer(input)));
+ }
+
+ /**
+ * Creates a {@link GremlinParser} from the given {@link GremlinLexer}.
+ */
+ private static GremlinParser createParser(final CommonTokenStream tokens) {
+ final GremlinParser parser = new GremlinParser(tokens);
+ parser.removeErrorListeners();
+ parser.addErrorListener(errorListener);
+ return parser;
+ }
+
+ /**
+ * Creates a {@link GremlinLexer} from the given input string with error listeners configured.
+ */
+ private static GremlinLexer createLexer(final String input) {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(input));
+ lexer.removeErrorListeners();
+ lexer.addErrorListener(errorListener);
+ return lexer;
+ }
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/ParameterMapVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/ParameterMapVisitor.java
new file mode 100644
index 0000000000..b2566934b1
--- /dev/null
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/ParameterMapVisitor.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.language.grammar;
+
+/**
+ * A visitor for parsing parameter map strings that prevents traversal injection.
+ * <p>
+ * Extends {@link GenericLiteralVisitor} and overrides traversal-related visit methods
+ * to throw, ensuring that no traversal can be constructed or executed during the
+ * parameter map parsing walk. This is critical for security because
+ * {@code visitTerminatedTraversal} in the base class would execute the traversal
+ * immediately via {@link TraversalTerminalMethodVisitor}.
+ */
+public class ParameterMapVisitor extends GenericLiteralVisitor {
+
+ private static final int DEFAULT_MAX_NESTING_DEPTH = 32;
+
+ private final int maxNestingDepth;
+ private int currentNestingDepth = 0;
+
+ public ParameterMapVisitor(final GremlinAntlrToJava antlr) {
+ this(antlr, DEFAULT_MAX_NESTING_DEPTH);
+ }
+
+ public ParameterMapVisitor(final GremlinAntlrToJava antlr, final int maxNestingDepth) {
+ super(antlr);
+ this.maxNestingDepth = maxNestingDepth;
+ }
+
+ /**
+ * Overridden to prevent nested traversal construction in parameter maps.
+ */
+ @Override
+ public Object visitNestedTraversal(final GremlinParser.NestedTraversalContext ctx) {
+ throw new GremlinParserException("Traversals are not allowed in parameter maps");
+ }
+
+ /**
+ * Overridden to prevent terminated traversal execution in parameter maps.
+ * This is the critical override because the base class would execute the traversal
+ * immediately via {@link TraversalTerminalMethodVisitor}.
+ */
+ @Override
+ public Object visitTerminatedTraversal(final GremlinParser.TerminatedTraversalContext ctx) {
+ throw new GremlinParserException("Traversals are not allowed in parameter maps");
+ }
+
+ @Override
+ public Object visitGenericMapLiteral(final GremlinParser.GenericMapLiteralContext ctx) {
+ currentNestingDepth++;
+ if (currentNestingDepth > maxNestingDepth) {
+ throw new GremlinParserException("Parameter map nesting depth exceeds maximum of " + maxNestingDepth);
+ }
+ try {
+ return super.visitGenericMapLiteral(ctx);
+ } finally {
+ currentNestingDepth--;
+ }
+ }
+
+ @Override
+ public Object visitGenericCollectionLiteral(final GremlinParser.GenericCollectionLiteralContext ctx) {
+ currentNestingDepth++;
+ if (currentNestingDepth > maxNestingDepth) {
+ throw new GremlinParserException("Parameter map nesting depth exceeds maximum of " + maxNestingDepth);
+ }
+ try {
+ return super.visitGenericCollectionLiteral(ctx);
+ } finally {
+ currentNestingDepth--;
+ }
+ }
+
+ @Override
+ public Object visitGenericSetLiteral(final GremlinParser.GenericSetLiteralContext ctx) {
+ currentNestingDepth++;
+ if (currentNestingDepth > maxNestingDepth) {
+ throw new GremlinParserException("Parameter map nesting depth exceeds maximum of " + maxNestingDepth);
+ }
+ try {
+ return super.visitGenericSetLiteral(ctx);
+ } finally {
+ currentNestingDepth--;
+ }
+ }
+}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
index aa00fccb8c..08f124015c 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
@@ -51,7 +51,6 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.Base64;
-import java.util.concurrent.atomic.AtomicInteger;
import static org.apache.tinkerpop.gremlin.util.DatetimeHelper.format;
import static org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils.asIterator;
@@ -66,7 +65,7 @@ public class GremlinLang implements Cloneable, Serializable {
private StringBuilder gremlin = new StringBuilder();
private Map<String, Object> parameters = new HashMap<>();
- private static final AtomicInteger paramCount = new AtomicInteger(0);
+ private String unsupportedType = "";
private List<OptionsStrategy> optionsStrategies = new ArrayList<>();
public GremlinLang() {
@@ -208,6 +207,9 @@ public class GremlinLang implements Cloneable, Serializable {
if (arg instanceof GremlinLang || arg instanceof DefaultTraversal) {
final GremlinLang gremlinLang = arg instanceof GremlinLang ? (GremlinLang) arg : ((DefaultTraversal) arg).getGremlinLang();
parameters.putAll(gremlinLang.getParameters());
+ if (gremlinLang.containsUnsupportedTypes()) {
+ unsupportedType = gremlinLang.getUnsupportedType();
+ }
return gremlinLang.getGremlin("__");
}
@@ -250,14 +252,8 @@ public class GremlinLang implements Cloneable, Serializable {
return ((Class) arg).getSimpleName();
}
- return asParameter(arg);
- }
-
- private String asParameter(final Object arg) {
- final String paramName = String.format("_%d", paramCount.getAndIncrement());
- // todo: consider resetting paramCount when it's larger then 1_000_000
- parameters.put(paramName, arg);
- return paramName;
+ unsupportedType = arg.getClass().getSimpleName();
+ return arg.toString();
}
private String asString(final Iterator itty) {
@@ -378,17 +374,75 @@ public class GremlinLang implements Cloneable, Serializable {
}
/**
- * The alias to set.
+ * Returns the simple class name of the last type encountered by {@code argAsString()} that could not be
+ * represented as a gremlin-lang literal. An empty string means all types were supported.
+ *
+ * @return the simple class name of the unsupported type, or empty string if none
*/
- public void addG(final String g) {
- parameters.put("g", g);
+ public String getUnsupportedType() {
+ return unsupportedType;
+ }
+
+ /**
+ * Returns {@code true} if {@code argAsString()} encountered at least one type that could not be represented
+ * as a gremlin-lang literal. In remote mode, the driver should check this before sending the request.
+ * <p>
+ * Note: this only covers types encountered while building the gremlin string. Named {@code GValue} parameters
+ * store their inner value directly in the parameter map without type-checking via {@code argAsString()}, so
+ * an unsupported type wrapped in a named {@code GValue} will not be detected by this method. Such cases are
+ * caught separately by {@link #convertParametersToString(Map)} when the parameter map is serialized.
+ * Unnamed {@code GValue} instances (null name) recurse into {@code argAsString()} and will set this flag.
+ *
+ * @return true if unsupported types were encountered in the gremlin string
+ */
+ public boolean containsUnsupportedTypes() {
+ return !unsupportedType.isEmpty();
}
/**
- * Reset parameter naming counter. Mainly intended to make testing easier
- */
- public void reset() {
- paramCount.set(0);
+ * Serializes this instance's parameter map to a gremlin-lang map literal string. Keys are parameter names
+ * (identifiers), values are formatted using {@code argAsString()}.
+ *
+ * @return a gremlin-lang map literal string, e.g. {@code ["x":1,"y":"stephen"]} or {@code [:]} for empty
+ */
+ public String getParametersAsString() {
+ return convertParametersToString(parameters);
+ }
+
+ /**
+ * Converts an arbitrary parameter map to a gremlin-lang map literal string. Keys must be valid identifier strings.
+ * Values are formatted as gremlin-lang literals.
+ *
+ * @param params the parameter map to convert
+ * @return a gremlin-lang map literal string, e.g. {@code ["x":1,"y":"stephen"]} or {@code [:]} for empty
+ */
+ public static String convertParametersToString(final Map<String, Object> params) {
+ if (params == null || params.isEmpty()) return "[:]";
+
+ final GremlinLang helper = new GremlinLang();
+ final StringBuilder sb = new StringBuilder("[");
+ final Iterator<Map.Entry<String, Object>> itty = params.entrySet().iterator();
+ while (itty.hasNext()) {
+ final Map.Entry<String, Object> entry = itty.next();
+ sb.append(helper.argAsString(entry.getKey())).append(":").append(helper.argAsString(entry.getValue()));
+ if (itty.hasNext()) sb.append(",");
+ }
+ sb.append("]");
+
+ if (helper.containsUnsupportedTypes()) {
+ throw new IllegalArgumentException(String.format(
+ "Parameter map contains at least one type [%s] that cannot be represented as text.",
+ helper.getUnsupportedType()));
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * The alias to set.
+ */
+ public void addG(final String g) {
+ parameters.put("g", g);
}
/**
@@ -510,6 +564,7 @@ public class GremlinLang implements Cloneable, Serializable {
clone.gremlin = new StringBuilder(gremlin.length());
clone.gremlin.append(gremlin);
clone.optionsStrategies = new ArrayList<>(this.optionsStrategies);
+ clone.unsupportedType = this.unsupportedType;
return clone;
} catch (final CloneNotSupportedException e) {
throw new IllegalStateException(e.getMessage(), e);
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/ParameterMapVisitorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/ParameterMapVisitorTest.java
new file mode 100644
index 0000000000..68c8d9b82f
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/ParameterMapVisitorTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.language.grammar;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for parameter map parsing, validation, and security.
+ */
+public class ParameterMapVisitorTest {
+
+ @Test
+ public void shouldParseEmptyMap() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[:]");
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void shouldParseNullInput() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters(null);
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void shouldParseEmptyStringInput() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("");
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void shouldParseSingleIntegerParameter() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"x\":1]");
+ assertEquals(1, result.size());
+ assertEquals(1, result.get("x"));
+ }
+
+ @Test
+ public void shouldParseSingleStringParameter() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"name\":\"marko\"]");
+ assertEquals(1, result.size());
+ assertEquals("marko", result.get("name"));
+ }
+
+ @Test
+ public void shouldParseSingleLongParameter() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"x\":1L]");
+ assertEquals(1, result.size());
+ assertEquals(1L, result.get("x"));
+ }
+
+ @Test
+ public void shouldParseMultipleMixedParameters() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"x\":1,\"name\":\"marko\",\"flag\":true]");
+ assertEquals(3, result.size());
+ assertEquals(1, result.get("x"));
+ assertEquals("marko", result.get("name"));
+ assertEquals(true, result.get("flag"));
+ }
+
+ @Test
+ public void shouldParseNullValue() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"x\":null]");
+ assertEquals(1, result.size());
+ assertNull(result.get("x"));
+ }
+
+ @Test
+ public void shouldParseUuidValue() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"id\":UUID(\"bfa9bbe8-c3a3-4017-acc3-cd02dda55e3e\")]");
+ assertEquals(1, result.size());
+ assertEquals(UUID.fromString("bfa9bbe8-c3a3-4017-acc3-cd02dda55e3e"), result.get("id"));
+ }
+
+ @Test
+ public void shouldParseNestedMapValue() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"m\":[\"name\":\"marko\"]]");
+ assertEquals(1, result.size());
+ assertTrue(result.get("m") instanceof Map);
+ assertEquals("marko", ((Map<?, ?>) result.get("m")).get("name"));
+ }
+
+ @Test
+ public void shouldParseListValue() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"x\":[1,2,3]]");
+ assertEquals(1, result.size());
+ assertTrue(result.get("x") instanceof List);
+ assertEquals(3, ((List<?>) result.get("x")).size());
+ }
+
+ @Test
+ public void shouldParseUnicodeKey() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"caf\\u00E9\":1]");
+ assertEquals(1, result.size());
+ assertEquals(1, result.get("caf\u00e9"));
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectMalformedInput() {
+ GremlinQueryParser.parseParameters("[\"x\":");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectNumericKey() {
+ GremlinQueryParser.parseParameters("[1:\"value\"]");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectEnumKey() {
+ GremlinQueryParser.parseParameters("[(T.id):\"value\"]");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectNonIdentifierStringKey() {
+ GremlinQueryParser.parseParameters("[\"~id\":1]");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectKeyWithSpaces() {
+ GremlinQueryParser.parseParameters("[\"my key\":1]");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectEmptyStringKey() {
+ GremlinQueryParser.parseParameters("[\"\":1]");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectExcessiveNestingDepth() {
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 35; i++) {
+ sb.append("[\"a\":");
+ }
+ sb.append("1");
+ for (int i = 0; i < 35; i++) {
+ sb.append("]");
+ }
+ GremlinQueryParser.parseParameters(sb.toString());
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectNestedTraversalInValue() {
+ GremlinQueryParser.parseParameters("[\"x\":__.out()]");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectTerminatedTraversalInValue() {
+ GremlinQueryParser.parseParameters("[\"x\":g.V().drop().iterate()]");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectTraversalInNestedList() {
+ GremlinQueryParser.parseParameters("[\"x\":[__.out()]]");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectTraversalInNestedSet() {
+ GremlinQueryParser.parseParameters("[\"x\":{__.out()}]");
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldRejectTraversalInNestedMapValue() {
+ GremlinQueryParser.parseParameters("[\"x\":[\"a\":__.out()]]");
+ }
+
+ @Test
+ public void shouldAcceptStringContainingGremlinLikeContent() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"x\":\"g.V().drop()\"]");
+ assertEquals(1, result.size());
+ assertEquals("g.V().drop()", result.get("x"));
+ }
+
+ @Test
+ public void shouldAcceptStringContainingTraversalSyntax() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[\"x\":\"__.out().count()\"]");
+ assertEquals(1, result.size());
+ assertEquals("__.out().count()", result.get("x"));
+ }
+
+ @Test
+ public void shouldParseUnquotedIdentifierKey() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[x:1]");
+ assertEquals(1, result.size());
+ assertEquals(1, result.get("x"));
+ }
+
+ @Test
+ public void shouldParseKeywordAsMapKey() {
+ final Map<String, Object> result = GremlinQueryParser.parseParameters("[label:\"person\"]");
+ assertEquals(1, result.size());
+ assertEquals("person", result.get("label"));
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
index b7f9da3290..57649b697d 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
@@ -38,11 +38,14 @@ import org.junit.runners.Parameterized;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
+import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.UUID;
import static org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal;
@@ -52,6 +55,8 @@ import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.hasLab
import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.out;
import static org.apache.tinkerpop.gremlin.util.CollectionUtil.asMap;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
@RunWith(Parameterized.class)
public class GremlinLangTest {
@@ -66,14 +71,14 @@ public class GremlinLangTest {
@Test
public void doTest() {
- final String gremlin = traversal.asAdmin().getGremlinLang().getGremlin();
- assertEquals(expected, gremlin);
+ final GremlinLang gremlinLang = traversal.asAdmin().getGremlinLang();
+ assertEquals(expected, gremlinLang.getGremlin());
+ assertFalse(gremlinLang.containsUnsupportedTypes());
+ assertEquals("", gremlinLang.getUnsupportedType());
}
private static GraphTraversalSource newG() {
- final GraphTraversalSource g = traversal().with(EmptyGraph.instance());
- g.getGremlinLang().reset();
- return g;
+ return traversal().with(EmptyGraph.instance());
}
@Parameterized.Parameters(name = "{0}")
@@ -98,8 +103,6 @@ public class GremlinLangTest {
{g.V().has(T.label, "person"), "g.V().has(T.label,\"person\")"},
{g.addE("knows").from(new DetachedVertex(1, "test1", Collections.emptyList())).to(new DetachedVertex(6, "test2", Collections.emptyList())),
"g.addE(\"knows\").from(__.V(1)).to(__.V(6))"},
- {newG().E(new ReferenceEdge(1, "test label", new ReferenceVertex(1, "v1"), new ReferenceVertex(1, "v1"))),
- "g.E(_0)"},
{g.V().hasId(P.within(Collections.emptyList())).count(), "g.V().hasId(P.within([])).count()"},
{g.V(1).outE().has("weight", P.inside(0.0, 0.6)), "g.V(1).outE().has(\"weight\",P.gt(0.0D).and(P.lt(0.6D)))"},
{g.withSack(1.0, Operator.sum).V(1).local(__.out("knows").barrier(SackFunctions.Barrier.normSack)).in("knows").barrier().sack(),
@@ -193,4 +196,404 @@ public class GremlinLangTest {
assertEquals("g.inject(Binary(\"AQID\"))", gremlin);
}
}
+
+ public static class ParameterStringTests {
+
+ @Test
+ public void shouldSerializeEmptyParameterMap() {
+ assertEquals("[:]", GremlinLang.convertParametersToString(new HashMap<>()));
+ assertEquals("[:]", GremlinLang.convertParametersToString(null));
+ }
+
+ @Test
+ public void shouldSerializeSingleStringParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("name", "marko");
+ assertEquals("[\"name\":\"marko\"]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeSingleIntegerParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", 1);
+ assertEquals("[\"x\":1]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeLongParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", 1L);
+ assertEquals("[\"x\":1L]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeDoubleParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", 1.5D);
+ assertEquals("[\"x\":1.5D]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeBooleanParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("flag", true);
+ assertEquals("[\"flag\":true]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeNullParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", null);
+ assertEquals("[\"x\":null]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeUuidParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ final UUID uuid = UUID.fromString("bfa9bbe8-c3a3-4017-acc3-cd02dda55e3e");
+ params.put("id", uuid);
+ assertEquals("[\"id\":UUID(\"bfa9bbe8-c3a3-4017-acc3-cd02dda55e3e\")]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeStringWithSpecialCharacters() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", "hello \"world\"");
+ assertEquals("[\"x\":\"hello \\\"world\\\"\"]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeByteParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", (byte) 1);
+ assertEquals("[\"x\":1B]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeShortParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", (short) 1);
+ assertEquals("[\"x\":1S]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeBigIntegerParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", BigInteger.valueOf(123));
+ assertEquals("[\"x\":123N]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeBigDecimalParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", BigDecimal.valueOf(1.5));
+ assertEquals("[\"x\":1.5M]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeFloatParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", 1.5F);
+ assertEquals("[\"x\":1.5F]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeCharacterParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", 'a');
+ assertEquals("[\"x\":\"a\"c]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeDurationParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", Duration.ofHours(2).plusMinutes(30));
+ assertEquals("[\"x\":Duration(9000,0)]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeBinaryParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", ByteBuffer.wrap(new byte[]{1, 2, 3}));
+ assertEquals("[\"x\":Binary(\"AQID\")]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeEnumParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", T.id);
+ assertEquals("[\"x\":T.id]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeGetParametersAsString() {
+ final GraphTraversalSource g2 = newG();
+ final String result = g2.V(GValue.of("x", 1)).
+ has("name", GValue.of("name", "marko")).
+ asAdmin().
+ getGremlinLang().
+ getParametersAsString();
+ // parameters are in a HashMap so order may vary, just check structure
+ assertTrue(result.startsWith("["));
+ assertTrue(result.endsWith("]"));
+ assertTrue(result, result.contains("\"x\":1"));
+ assertTrue(result, result.contains("\"name\":\"marko\""));
+ }
+
+ @Test
+ public void shouldSerializeOffsetDateTimeParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", DatetimeHelper.parse("2018-03-21T08:35:44.741Z"));
+ assertEquals("[\"x\":datetime(\"2018-03-21T08:35:44.741Z\")]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeDateParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", Date.from(DatetimeHelper.parse("2018-03-21T08:35:44.741Z").toInstant()));
+ assertEquals("[\"x\":datetime(\"2018-03-21T08:35:44.741Z\")]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeMultipleParameters() {
+ // use a LinkedHashMap to guarantee order
+ final Map<String, Object> params = new LinkedHashMap<>();
+ params.put("x", 1);
+ params.put("name", "marko");
+ params.put("flag", true);
+ assertEquals("[\"x\":1,\"name\":\"marko\",\"flag\":true]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeMapValuedParameter() {
+ final Map<String, Object> params = new LinkedHashMap<>();
+ final Map<Object, Object> nested = new LinkedHashMap<>();
+ nested.put(T.label, "person");
+ nested.put("name", "marko");
+ params.put("m", nested);
+ assertEquals("[\"m\":[(T.label):\"person\",\"name\":\"marko\"]]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeListValuedParameter() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", Arrays.asList(1, 2, 3));
+ assertEquals("[\"x\":[1,2,3]]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeSetValuedParameter() {
+ // use a LinkedHashSet to guarantee order
+ final java.util.Set<Integer> set = new java.util.LinkedHashSet<>(Arrays.asList(1, 2));
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", set);
+ assertEquals("[\"x\":{1,2}]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeStringWithBackslash() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", "path\\to\\file");
+ assertEquals("[\"x\":\"path\\\\to\\\\file\"]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldSerializeStringWithUnicode() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", "caf\u00E9");
+ assertEquals("[\"x\":\"caf\\u00E9\"]", GremlinLang.convertParametersToString(params));
+ }
+
+ @Test
+ public void shouldHandleEmptyStringKey() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("", 1);
+ final String result = GremlinLang.convertParametersToString(params);
+ assertEquals("[\"\":1]", result);
+ }
+
+ @Test
+ public void shouldHandleKeyWithSpaces() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("my key", 1);
+ final String result = GremlinLang.convertParametersToString(params);
+ assertEquals("[\"my key\":1]", result);
+ }
+
+ @Test
+ public void shouldHandleNullKey() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put(null, 1);
+ final String result = GremlinLang.convertParametersToString(params);
+ assertEquals("[null:1]", result);
+ }
+
+ @Test
+ public void shouldSerializeUnicodeKey() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("caf\u00e9", 1);
+ assertEquals("[\"caf\\u00E9\":1]", GremlinLang.convertParametersToString(params));
+ }
+ }
+
+ public static class UnsupportedTypeTests {
+
+ /**
+ * A test-only type that will never have gremlin-lang literal support.
+ */
+ private static class UnsupportedType {
+ @Override
+ public String toString() {
+ return "custom-unsupported";
+ }
+ }
+
+ @Test
+ public void shouldMarkUnsupportedTypeAndUseToString() {
+ final UnsupportedType custom = new UnsupportedType();
+ final GremlinLang gremlinLang = g.inject(custom).asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", gremlinLang.getUnsupportedType());
+ assertTrue(gremlinLang.getGremlin().contains(custom.toString()));
+ }
+
+ @Test
+ public void shouldNotAddUnsupportedTypeToParameterMap() {
+ final GremlinLang gremlinLang = g.inject(new UnsupportedType()).asAdmin().getGremlinLang();
+ // unsupported type is flagged but not stored in the parameter map
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertTrue(gremlinLang.getParameters().isEmpty());
+ }
+
+ @Test
+ public void shouldRecordLastUnsupportedType() {
+ final GremlinLang gremlinLang = g.inject(new UnsupportedType(), new Thread()).asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ // last one wins
+ assertEquals("Thread", gremlinLang.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldPropagateFlagFromChildTraversal() {
+ final GremlinLang gremlinLang = g.V().where(__.has("x", P.eq(new UnsupportedType()))).asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", gremlinLang.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldNotPropagateFlagFromCleanChildTraversal() {
+ final GremlinLang gremlinLang = g.V().where(__.has("name", "marko")).asAdmin().getGremlinLang();
+ assertFalse(gremlinLang.containsUnsupportedTypes());
+ assertEquals("", gremlinLang.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldDetectUnsupportedTypeInList() {
+ final GremlinLang gremlinLang = g.inject(Arrays.asList(1, new UnsupportedType())).asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", gremlinLang.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldDetectUnsupportedTypeInMap() {
+ final Map<String, Object> map = new HashMap<>();
+ map.put("key", new UnsupportedType());
+ final GremlinLang gremlinLang = g.inject(map).asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", gremlinLang.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldDetectUnsupportedTypeInSet() {
+ final java.util.Set<Object> set = new java.util.LinkedHashSet<>();
+ set.add(1);
+ set.add(new UnsupportedType());
+ final GremlinLang gremlinLang = g.inject(set).asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", gremlinLang.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldDetectUnsupportedTypeInPredicate() {
+ final GremlinLang gremlinLang = g.V().has("x", P.eq(new UnsupportedType())).asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", gremlinLang.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldPreserveUnsupportedTypeOnClone() {
+ final GremlinLang original = g.inject(new UnsupportedType()).asAdmin().getGremlinLang();
+ final GremlinLang cloned = original.clone();
+ assertTrue(cloned.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", cloned.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldPreserveCleanStateOnClone() {
+ final GremlinLang original = g.V(1).asAdmin().getGremlinLang();
+ final GremlinLang cloned = original.clone();
+ assertFalse(cloned.containsUnsupportedTypes());
+ assertEquals("", cloned.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldStillAddGValueToParameterMap() {
+ final GremlinLang gremlinLang = g.V(GValue.of("myId", 1)).asAdmin().getGremlinLang();
+ assertFalse(gremlinLang.containsUnsupportedTypes());
+ assertEquals(1, gremlinLang.getParameters().size());
+ assertEquals(1, gremlinLang.getParameters().get("myId"));
+ }
+
+ @Test
+ public void shouldDetectUnsupportedTypeForReferenceEdge() {
+ final GremlinLang gremlinLang = newG().E(
+ new ReferenceEdge(1, "test label", new ReferenceVertex(1, "v1"), new ReferenceVertex(1, "v1")))
+ .asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("ReferenceEdge", gremlinLang.getUnsupportedType());
+ assertEquals("g.E(e[1][1-test label->1])", gremlinLang.getGremlin());
+ }
+
+ @Test
+ public void shouldDetectUnsupportedTypeInMapKey() {
+ final Map<Object, Object> map = new LinkedHashMap<>();
+ map.put(new UnsupportedType(), "value");
+ final GremlinLang gremlinLang = g.inject(map).asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", gremlinLang.getUnsupportedType());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldRejectUnsupportedTypeInConvertParametersToString() {
+ final Map<String, Object> params = new HashMap<>();
+ params.put("x", new UnsupportedType());
+ GremlinLang.convertParametersToString(params);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldRejectUnsupportedTypeInGValueViaGetParametersAsString() {
+ final GremlinLang gremlinLang = g.V(GValue.of("x", new UnsupportedType())).asAdmin().getGremlinLang();
+ // named GValue stores the raw value in the parameter map without type-checking,
+ // so containsUnsupportedTypes() does not detect it
+ assertFalse(gremlinLang.containsUnsupportedTypes());
+ // the error is caught when the parameter map is serialized
+ gremlinLang.getParametersAsString();
+ }
+
+ @Test
+ public void shouldDetectUnsupportedTypeInUnnamedGValue() {
+ // unnamed GValue (null name) recurses into argAsString(gValue.get()),
+ // which hits the unsupported-type fallback and sets the flag
+ final GremlinLang gremlinLang = g.inject(GValue.of(null, new UnsupportedType())).asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", gremlinLang.getUnsupportedType());
+ }
+
+ @Test
+ public void shouldPersistFlagAfterSupportedTypeFollows() {
+ final GremlinLang gremlinLang = g.inject(new UnsupportedType(), "hello").asAdmin().getGremlinLang();
+ assertTrue(gremlinLang.containsUnsupportedTypes());
+ assertEquals("UnsupportedType", gremlinLang.getUnsupportedType());
+ }
+ }
}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/RequestMessage.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/RequestMessage.cs
index f599a532d6..2ebd02cc29 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/RequestMessage.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/RequestMessage.cs
@@ -23,6 +23,7 @@
using System;
using System.Collections.Generic;
+using Gremlin.Net.Process.Traversal;
namespace Gremlin.Net.Driver.Messages
{
@@ -69,6 +70,7 @@ namespace Gremlin.Net.Driver.Messages
private readonly string _gremlin;
private readonly Dictionary<string, object> _fields = new Dictionary<string, object>();
private readonly Dictionary<string, object> _bindings = new Dictionary<string, object>();
+ private string? _bindingsString;
internal Builder(string gremlin)
{
@@ -94,17 +96,23 @@ namespace Gremlin.Net.Driver.Messages
/// <returns>The <see cref="Builder" />.</returns>
public Builder AddBinding(string key, object val)
{
+ if (_bindingsString != null)
+ throw new InvalidOperationException("Cannot mix AddBinding() with AddBindingsString().");
_bindings[key] = val;
return this;
}
/// <summary>
- /// Adds multiple binding parameters.
+ /// Adds multiple binding parameters from a dictionary. The values will be
+ /// converted to a gremlin-lang string when the message is created.
+ /// Cannot be mixed with <see cref="AddBindingsString"/>.
/// </summary>
/// <param name="bindings">The bindings to add.</param>
/// <returns>The <see cref="Builder" />.</returns>
public Builder AddBindings(Dictionary<string, object> bindings)
{
+ if (_bindingsString != null)
+ throw new InvalidOperationException("Cannot mix AddBindings() with AddBindingsString().");
foreach (var kvp in bindings)
{
_bindings[kvp.Key] = kvp.Value;
@@ -112,6 +120,20 @@ namespace Gremlin.Net.Driver.Messages
return this;
}
+ /// <summary>
+ /// Sets the bindings as a pre-serialized gremlin-lang map literal string.
+ /// Cannot be mixed with <see cref="AddBinding"/> or <see cref="AddBindings"/>.
+ /// </summary>
+ /// <param name="bindingsString">The gremlin-lang bindings string.</param>
+ /// <returns>The <see cref="Builder" />.</returns>
+ public Builder AddBindingsString(string bindingsString)
+ {
+ if (_bindings.Count > 0)
+ throw new InvalidOperationException("Cannot mix AddBindingsString() with AddBinding()/AddBindings().");
+ _bindingsString = bindingsString;
+ return this;
+ }
+
/// <summary>
/// Adds a field to the request message.
/// </summary>
@@ -203,7 +225,15 @@ namespace Gremlin.Net.Driver.Messages
/// <returns>The built <see cref="RequestMessage" />.</returns>
public RequestMessage Create()
{
- _fields[Tokens.ArgsBindings] = _bindings;
+ // prefer pre-serialized bindings string over raw map
+ if (_bindingsString != null)
+ {
+ _fields[Tokens.ArgsBindings] = _bindingsString;
+ }
+ else if (_bindings.Count > 0)
+ {
+ _fields[Tokens.ArgsBindings] = GremlinLang.ConvertParametersToString(_bindings);
+ }
return new RequestMessage(_gremlin, _fields);
}
}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
index 81dd3433e8..3340730c1b 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
@@ -112,8 +112,13 @@ namespace Gremlin.Net.Driver.Remote
gremlinLang.AddG(_traversalSource);
var requestMsg = RequestMessage.Build(gremlinLang.GetGremlin())
- .AddG(_traversalSource)
- .AddBindings(gremlinLang.Parameters);
+ .AddG(_traversalSource);
+
+ var parametersString = gremlinLang.GetParametersAsString();
+ if (parametersString != "[:]")
+ {
+ requestMsg.AddBindingsString(parametersString);
+ }
foreach (var optionsStrategy in gremlinLang.OptionsStrategies)
{
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
index 2dfdc02fd5..21115a8f8f 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
@@ -27,7 +27,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text;
-using System.Threading;
using Gremlin.Net.Process.Traversal.Strategy;
using Gremlin.Net.Process.Traversal.Strategy.Decoration;
using Gremlin.Net.Structure;
@@ -44,7 +43,6 @@ namespace Gremlin.Net.Process.Traversal
private StringBuilder _gremlin = new();
private Dictionary<string, object> _parameters = new();
- private static int _paramCount;
private List<OptionsStrategy> _optionsStrategies = new();
/// <summary>
@@ -123,6 +121,32 @@ namespace Gremlin.Net.Process.Traversal
/// </summary>
public Dictionary<string, object> Parameters => _parameters;
+ /// <summary>
+ /// Serializes this instance's parameter map to a gremlin-lang map literal string.
+ /// </summary>
+ public string GetParametersAsString() => ConvertParametersToString(_parameters);
+
+ /// <summary>
+ /// Converts a parameter map to a gremlin-lang map literal string.
+ /// Throws ArgumentException if any value is an unsupported type.
+ /// </summary>
+ public static string ConvertParametersToString(Dictionary<string, object>? parameters)
+ {
+ if (parameters == null || parameters.Count == 0) return "[:]";
+
+ var helper = new GremlinLang();
+ var sb = new StringBuilder("[");
+ var first = true;
+ foreach (var kvp in parameters)
+ {
+ if (!first) sb.Append(',');
+ sb.Append(helper.ArgAsString(kvp.Key)).Append(':').Append(helper.ArgAsString(kvp.Value));
+ first = false;
+ }
+ sb.Append(']');
+ return sb.ToString();
+ }
+
/// <summary>
/// Gets the list of extracted OptionsStrategy instances.
/// </summary>
@@ -148,14 +172,6 @@ namespace Gremlin.Net.Process.Traversal
}
}
- /// <summary>
- /// Resets the static parameter counter. Intended for test determinism.
- /// </summary>
- public static void ResetCounter()
- {
- Interlocked.Exchange(ref _paramCount, 0);
- }
-
private void AddToGremlin(string name, object?[] arguments)
{
var flattenedArguments = FlattenArguments(arguments);
@@ -332,14 +348,8 @@ namespace Gremlin.Net.Process.Traversal
if (arg is Type type)
return type.Name;
- return AsParameter(arg);
- }
-
- private string AsParameter(object arg)
- {
- var paramName = $"_{Interlocked.Increment(ref _paramCount) - 1}";
- _parameters[paramName] = arg;
- return paramName;
+ throw new ArgumentException(
+ $"GremlinLang contains at least one type [{arg.GetType().Name}] that cannot be represented as text.");
}
private string AsString(P p)
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs
index 7a67ce668f..2e83c1120b 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs
@@ -110,14 +110,14 @@ namespace Gremlin.Net.UnitTest.Driver
var connection = new DriverRemoteConnection(client, "g");
var gl = new GremlinLang();
- gl.Parameters["_0"] = 42;
+ gl.Parameters["x"] = 42;
gl.AddStep("V", Array.Empty<object>());
await connection.SubmitAsync<object, object>(gl);
Assert.NotNull(capturedRequest);
- var bindings = (Dictionary<string, object>)capturedRequest!.Fields[Tokens.ArgsBindings];
- Assert.Equal(42, bindings["_0"]);
+ var bindingsString = (string)capturedRequest!.Fields[Tokens.ArgsBindings];
+ Assert.Contains("\"x\":42", bindingsString);
}
[Fact]
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/Messages/RequestMessageTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/Messages/RequestMessageTests.cs
index a1c734d2b4..4d669841b5 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/Messages/RequestMessageTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/Messages/RequestMessageTests.cs
@@ -57,25 +57,25 @@ namespace Gremlin.Net.UnitTest.Driver.Messages
[Fact]
public void ShouldSetBindings()
{
- var msg = RequestMessage.Build("g.V(_0)")
- .AddBinding("_0", 1)
+ var msg = RequestMessage.Build("g.V(x)")
+ .AddBinding("x", 1)
.Create();
- var bindings = (Dictionary<string, object>)msg.Fields[Tokens.ArgsBindings];
- Assert.Equal(1, bindings["_0"]);
+ var bindingsString = (string)msg.Fields[Tokens.ArgsBindings];
+ Assert.Contains("\"x\":1", bindingsString);
}
[Fact]
public void ShouldSetMultipleBindings()
{
- var bindings = new Dictionary<string, object> { { "_0", 1 }, { "_1", "test" } };
- var msg = RequestMessage.Build("g.V(_0).has(_1)")
+ var bindings = new Dictionary<string, object> { { "x", 1 }, { "name", "test" } };
+ var msg = RequestMessage.Build("g.V(x).has(name)")
.AddBindings(bindings)
.Create();
- var resultBindings = (Dictionary<string, object>)msg.Fields[Tokens.ArgsBindings];
- Assert.Equal(1, resultBindings["_0"]);
- Assert.Equal("test", resultBindings["_1"]);
+ var bindingsString = (string)msg.Fields[Tokens.ArgsBindings];
+ Assert.Contains("\"x\":1", bindingsString);
+ Assert.Contains("\"name\":\"test\"", bindingsString);
}
[Fact]
@@ -99,12 +99,21 @@ namespace Gremlin.Net.UnitTest.Driver.Messages
}
[Fact]
- public void ShouldIncludeEmptyBindingsWhenNoneAdded()
+ public void ShouldNotContainBindingsWhenNoneAdded()
{
var msg = RequestMessage.Build("g.V()").Create();
- var bindings = (Dictionary<string, object>)msg.Fields[Tokens.ArgsBindings];
- Assert.Empty(bindings);
+ Assert.False(msg.Fields.ContainsKey(Tokens.ArgsBindings));
+ }
+
+ [Fact]
+ public void ShouldSetBindingsString()
+ {
+ var msg = RequestMessage.Build("g.V(x)")
+ .AddBindingsString("[\"x\":1]")
+ .Create();
+
+ Assert.Equal("[\"x\":1]", msg.Fields[Tokens.ArgsBindings]);
}
}
}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GraphTraversalSourceTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GraphTraversalSourceTests.cs
index 91370c69fb..51bb345766 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GraphTraversalSourceTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GraphTraversalSourceTests.cs
@@ -55,7 +55,6 @@ namespace Gremlin.Net.UnitTest.Process.Traversal
[Fact]
public void CloneShouldCreateIndependentGraphTraversal()
{
- GremlinLang.ResetCounter();
var g = AnonymousTraversalSource.Traversal().With(null);
var original = g.V().Out("created");
var clone = original.Clone().Out("knows");
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
index f54f4e359b..b4eeade168 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
@@ -38,7 +38,6 @@ namespace Gremlin.Net.UnitTest.Process.Traversal
public GremlinLangTests()
{
- GremlinLang.ResetCounter();
_g = new GraphTraversalSource();
}
@@ -816,28 +815,9 @@ namespace Gremlin.Net.UnitTest.Process.Traversal
// --- Python-Only Tests 106-116 ---
- [Fact]
- public void g_E_Edge_renders_as_parameter()
- {
- GremlinLang.ResetCounter();
- var edge = new Edge(2, new Vertex(1), "knows", new Vertex(3));
- var result = _g.E(edge).GremlinLang.GetGremlin();
- Assert.Equal("g.E(_0)", result);
- }
-
- [Fact]
- public void g_Inject_VertexProperty_renders_as_parameter()
- {
- GremlinLang.ResetCounter();
- var vp = new VertexProperty(3, "time", "18:00", null);
- var result = _g.Inject((object)vp).GremlinLang.GetGremlin();
- Assert.Equal("g.inject(_0)", result);
- }
-
[Fact]
public void g_Inject_dicts_with_enum_keys()
{
- GremlinLang.ResetCounter();
var dict1 = new Dictionary<object, object> { { "name", "java" } };
var dict2 = new Dictionary<object, object> { { T.Id, 0 } };
var dict3 = new Dictionary<object, object>();
@@ -1057,5 +1037,56 @@ namespace Gremlin.Net.UnitTest.Process.Traversal
Assert.Equal("g.inject(\"'\"c)",
_g.Inject((object)'\'').GremlinLang.GetGremlin());
}
+
+ [Fact]
+ public void Unsupported_type_throws_ArgumentException()
+ {
+ Assert.Throws<ArgumentException>(() => _g.Inject(new object()).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void Unsupported_type_in_list_throws_ArgumentException()
+ {
+ Assert.Throws<ArgumentException>(() =>
+ _g.Inject((object)new object[] { 1, new object() }).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void ConvertParametersToString_empty_returns_empty_map()
+ {
+ Assert.Equal("[:]", GremlinLang.ConvertParametersToString(null));
+ Assert.Equal("[:]", GremlinLang.ConvertParametersToString(new Dictionary<string, object>()));
+ }
+
+ [Fact]
+ public void ConvertParametersToString_basic_parameters()
+ {
+ var parameters = new Dictionary<string, object> { { "x", 1 } };
+ var result = GremlinLang.ConvertParametersToString(parameters);
+ Assert.Contains("\"x\":1", result);
+ }
+
+ [Fact]
+ public void ConvertParametersToString_unsupported_type_throws()
+ {
+ var parameters = new Dictionary<string, object> { { "x", new object() } };
+ Assert.Throws<ArgumentException>(() => GremlinLang.ConvertParametersToString(parameters));
+ }
+
+ [Fact]
+ public void ConvertParametersToString_nested_list()
+ {
+ var parameters = new Dictionary<string, object> { { "ids", new object[] { 1, 2, 3 } } };
+ var result = GremlinLang.ConvertParametersToString(parameters);
+ Assert.Contains("\"ids\":[1,2,3]", result);
+ }
+
+ [Fact]
+ public void ConvertParametersToString_escaped_string_value()
+ {
+ var parameters = new Dictionary<string, object> { { "name", "it's a \"test\"" } };
+ var result = GremlinLang.ConvertParametersToString(parameters);
+ Assert.Contains("\"name\":", result);
+ }
}
}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/TraversalTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/TraversalTests.cs
index a158ed8634..bf7c6c16a5 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/TraversalTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/TraversalTests.cs
@@ -184,7 +184,6 @@ namespace Gremlin.Net.UnitTest.Process.Traversal
[Fact]
public void ShouldExtractIdFromVertex()
{
- GremlinLang.ResetCounter();
var g = AnonymousTraversalSource.Traversal().With(null);
// Test basic V() step with mixed ID types
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/RequestOptions.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/RequestOptions.java
index c51f2b8516..d51657f0ae 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/RequestOptions.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/RequestOptions.java
@@ -44,7 +44,7 @@ public final class RequestOptions {
public static final RequestOptions EMPTY = RequestOptions.build().create();
private final String graphOrTraversalSource;
- private final Map<String, Object> parameters;
+ private final String parameters;
private final Integer batchSize;
private final Long timeout;
private final String language;
@@ -54,7 +54,7 @@ public final class RequestOptions {
private RequestOptions(final Builder builder) {
this.graphOrTraversalSource = builder.graphOrTraversalSource;
- this.parameters = builder.parameters;
+ this.parameters = builder.parametersString;
this.batchSize = builder.batchSize;
this.timeout = builder.timeout;
this.language = builder.language;
@@ -67,7 +67,7 @@ public final class RequestOptions {
return Optional.ofNullable(graphOrTraversalSource);
}
- public Optional<Map<String, Object>> getParameters() {
+ public Optional<String> getParameters() {
return Optional.ofNullable(parameters);
}
@@ -114,16 +114,17 @@ public final class RequestOptions {
if (builder.bulkResults == null)
builder.bulkResults(true);
- final Map<String, Object> parameters = gremlinLang.getParameters();
- if (parameters != null && !parameters.isEmpty()) {
- parameters.forEach(builder::addParameter);
+ final String parametersString = gremlinLang.getParametersAsString();
+ if (!"[:]".equals(parametersString)) {
+ builder.addParametersString(parametersString);
}
return builder.create();
}
public static final class Builder {
private String graphOrTraversalSource = null;
- private Map<String, Object> parameters = null;
+ private Map<String, Object> internalParameters = null;
+ private String parametersString = null;
private Integer batchSize = null;
private Long timeout = null;
private String materializeProperties = null;
@@ -139,7 +140,7 @@ public final class RequestOptions {
public static Builder from(final RequestOptions options) {
final Builder builder = build();
builder.graphOrTraversalSource = options.graphOrTraversalSource;
- builder.parameters = options.parameters;
+ builder.parametersString = options.parameters;
builder.batchSize = options.batchSize;
builder.timeout = options.timeout;
builder.materializeProperties = options.materializeProperties;
@@ -158,11 +159,17 @@ public final class RequestOptions {
}
/**
- * The parameters to pass on the request.
+ * Adds a parameter to the request. The parameter map is converted to a gremlin-lang map literal string when the
+ * request is built. This method accumulates parameters into an internal map which is converted on
+ * {@link #create()}.
+ * <p>
+ * Cannot be mixed with {@link #addParametersString(String)}.
*/
public Builder addParameter(final String name, final Object value) {
- if (null == parameters)
- parameters = new HashMap<>();
+ if (parametersString != null)
+ throw new IllegalStateException("Cannot mix addParameter() with addParametersString()");
+
+ if (internalParameters == null) internalParameters = new HashMap<>();
if (ARGS_G.equals(name)) {
this.graphOrTraversalSource = (String) value;
@@ -172,7 +179,21 @@ public final class RequestOptions {
this.language = (String) value;
}
- parameters.put(name, value);
+ internalParameters.put(name, value);
+ return this;
+ }
+
+ /**
+ * Sets the parameters as a pre-formatted gremlin-lang map literal string. This allows users to pass parameters
+ * as a raw gremlin-lang string when using the Client API with a gremlin string.
+ * <p>
+ * Cannot be mixed with {@link #addParameter(String, Object)}.
+ */
+ public Builder addParametersString(final String parametersString) {
+ if (internalParameters != null)
+ throw new IllegalStateException("Cannot mix addParametersString() with addParameter()");
+
+ this.parametersString = parametersString;
return this;
}
@@ -228,6 +249,10 @@ public final class RequestOptions {
}
public RequestOptions create() {
+ // if parameters were added via addParameter(), convert the map to a string
+ if (internalParameters != null) {
+ parametersString = GremlinLang.convertParametersToString(internalParameters);
+ }
return new RequestOptions(this);
}
}
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java
index c909e48604..baae303ce9 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java
@@ -216,6 +216,12 @@ public class DriverRemoteConnection implements RemoteConnection {
@Override
public <E> CompletableFuture<RemoteTraversal<?, E>> submitAsync(final GremlinLang gremlinLang) throws RemoteConnectionException {
+ if (gremlinLang.containsUnsupportedTypes()) {
+ throw new IllegalArgumentException(String.format(
+ "GremlinLang contains at least one type [%s] that cannot be represented as text.",
+ gremlinLang.getUnsupportedType()));
+ }
+
try {
gremlinLang.addG(remoteTraversalSourceName);
return client.submitAsync(gremlinLang.getGremlin(), getRequestOptions(gremlinLang))
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/TransactionRemoteConnection.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/TransactionRemoteConnection.java
index 6ea9ce5e28..050ca00740 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/TransactionRemoteConnection.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/TransactionRemoteConnection.java
@@ -68,6 +68,12 @@ class TransactionRemoteConnection implements RemoteConnection {
throw new RemoteConnectionException("Transaction is not open");
}
+ if (gremlinLang.containsUnsupportedTypes()) {
+ throw new IllegalArgumentException(String.format(
+ "GremlinLang contains at least one type [%s] that cannot be represented as text.",
+ gremlinLang.getUnsupportedType()));
+ }
+
try {
// Synchronous submission through transaction
final ResultSet rs = transaction.submit(gremlinLang.getGremlin(), getRequestOptions(gremlinLang));
diff --git a/gremlin-go/driver/client.go b/gremlin-go/driver/client.go
index 4d799b878d..3c98ecdd40 100644
--- a/gremlin-go/driver/client.go
+++ b/gremlin-go/driver/client.go
@@ -163,9 +163,12 @@ func (client *Client) Submit(traversalString string, bindings ...map[string]inte
func (client *Client) submitGremlinLang(gremlinLang *GremlinLang) (ResultSet, error) {
client.logHandler.logf(Debug, submitStartedString, *gremlinLang)
requestOptionsBuilder := new(RequestOptionsBuilder)
- if len(gremlinLang.GetParameters()) > 0 {
- requestOptionsBuilder.SetBindings(gremlinLang.GetParameters())
+
+ parametersString := gremlinLang.GetParametersAsString()
+ if parametersString != "[:]" {
+ requestOptionsBuilder.SetBindingsString(parametersString)
}
+
if len(gremlinLang.optionsStrategies) > 0 {
requestOptionsBuilder = applyOptionsConfig(requestOptionsBuilder, gremlinLang.optionsStrategies[0].configuration)
}
@@ -177,13 +180,15 @@ func (client *Client) submitGremlinLang(gremlinLang *GremlinLang) (ResultSet, er
func applyOptionsConfig(builder *RequestOptionsBuilder, config map[string]interface{}) *RequestOptionsBuilder {
builderValue := reflect.ValueOf(builder)
- // Map configuration keys to setter method names
+ // Map configuration keys to setter method names.
+ // "bindings" is intentionally excluded because bindings are handled separately
+ // via SetBindingsString in submitGremlinLang, and including it here would
+ // trigger the mutual exclusion panic between SetBindings and SetBindingsString.
setterMap := map[string]string{
"requestId": "SetRequestId",
"evaluationTimeout": "SetEvaluationTimeout",
"batchSize": "SetBatchSize",
"userAgent": "SetUserAgent",
- "bindings": "SetBindings",
"materializeProperties": "SetMaterializeProperties",
}
diff --git a/gremlin-go/driver/gremlinlang.go b/gremlin-go/driver/gremlinlang.go
index 2362515e5f..2625cb10e6 100644
--- a/gremlin-go/driver/gremlinlang.go
+++ b/gremlin-go/driver/gremlinlang.go
@@ -28,7 +28,6 @@ import (
"reflect"
"strconv"
"strings"
- "sync/atomic"
"time"
"github.com/google/uuid"
@@ -39,7 +38,6 @@ type GremlinLang struct {
gremlin []string
parameters map[string]interface{}
optionsStrategies []*traversalStrategy
- paramCount *atomic.Uint64
}
// NewGremlinLang creates a new GremlinLang to be used in traversals.
@@ -47,7 +45,6 @@ func NewGremlinLang(gl *GremlinLang) *GremlinLang {
gremlin := make([]string, 0)
parameters := make(map[string]interface{})
optionsStrategies := make([]*traversalStrategy, 0)
- paramCount := atomic.Uint64{}
if gl != nil {
gremlin = make([]string, len(gl.gremlin))
copy(gremlin, gl.gremlin)
@@ -59,15 +56,12 @@ func NewGremlinLang(gl *GremlinLang) *GremlinLang {
optionsStrategies = make([]*traversalStrategy, len(gl.optionsStrategies))
copy(optionsStrategies, gl.optionsStrategies)
-
- paramCount.Store(gl.paramCount.Load())
}
return &GremlinLang{
gremlin: gremlin,
parameters: parameters,
optionsStrategies: optionsStrategies,
- paramCount: ¶mCount,
}
}
@@ -175,7 +169,7 @@ func (gl *GremlinLang) argAsString(arg interface{}) (string, error) {
return "NaN", nil
}
if math.IsInf(float64(v), 1) {
- return "Infinity", nil
+ return "+Infinity", nil
}
if math.IsInf(float64(v), -1) {
return "-Infinity", nil
@@ -186,7 +180,7 @@ func (gl *GremlinLang) argAsString(arg interface{}) (string, error) {
return "NaN", nil
}
if math.IsInf(v, 1) {
- return "Infinity", nil
+ return "+Infinity", nil
}
if math.IsInf(v, -1) {
return "-Infinity", nil
@@ -296,7 +290,13 @@ func (gl *GremlinLang) argAsString(arg interface{}) (string, error) {
case reflect.Slice:
return gl.translateSlice(arg)
default:
- return gl.asParameter(arg), nil
+ // Panic rather than returning an error because unsupported types are programmer
+ // mistakes (passing a type the library can't serialize), not recoverable runtime
+ // conditions. This matches the existing GValue validation pattern in this file.
+ // An error-return approach was considered but every caller in the fluent traversal
+ // API (AddSource, WithSack, etc.) would need to propagate and immediately panic
+ // anyway, adding complexity with no practical benefit.
+ panic(fmt.Sprintf("GremlinLang contains at least one type [%s] that cannot be represented as text", reflect.TypeOf(arg).Name()))
}
}
}
@@ -435,13 +435,6 @@ func (gl *GremlinLang) translatePValue(operator string, values []interface{}) (s
return sb.String(), nil
}
-func (gl *GremlinLang) asParameter(arg interface{}) string {
- paramName := fmt.Sprintf("_%d", gl.paramCount.Load())
- gl.paramCount.Add(1)
- gl.parameters[paramName] = arg
- return paramName
-}
-
func (gl *GremlinLang) GetGremlin(arg ...string) string {
var g string
gremlin := strings.Join(gl.gremlin, "")
@@ -461,12 +454,39 @@ func (gl *GremlinLang) GetParameters() map[string]interface{} {
return gl.parameters
}
-func (gl *GremlinLang) AddG(g string) {
- gl.parameters["g"] = g
+// GetParametersAsString serializes this instance's parameter map to a gremlin-lang map literal string.
+func (gl *GremlinLang) GetParametersAsString() string {
+ return ConvertParametersToString(gl.parameters)
}
-func (gl *GremlinLang) Reset() {
- gl.paramCount.Store(0)
+// ConvertParametersToString converts a parameter map to a gremlin-lang map literal string.
+// Panics if any value is an unsupported type.
+func ConvertParametersToString(params map[string]interface{}) string {
+ if params == nil || len(params) == 0 {
+ return "[:]"
+ }
+
+ helper := NewGremlinLang(nil)
+ sb := strings.Builder{}
+ sb.WriteString("[")
+ first := true
+ for k, v := range params {
+ if !first {
+ sb.WriteString(",")
+ }
+ ks, _ := helper.argAsString(k)
+ vs, _ := helper.argAsString(v)
+ sb.WriteString(ks)
+ sb.WriteString(":")
+ sb.WriteString(vs)
+ first = false
+ }
+ sb.WriteString("]")
+ return sb.String()
+}
+
+func (gl *GremlinLang) AddG(g string) {
+ gl.parameters["g"] = g
}
func (gl *GremlinLang) AddSource(name string, arguments ...interface{}) {
diff --git a/gremlin-go/driver/gremlinlang_test.go b/gremlin-go/driver/gremlinlang_test.go
index 9d3ee5b93c..719bd48f60 100644
--- a/gremlin-go/driver/gremlinlang_test.go
+++ b/gremlin-go/driver/gremlinlang_test.go
@@ -20,8 +20,10 @@ under the License.
package gremlingo
import (
+ "fmt"
"math"
"regexp"
+ "strings"
"testing"
"time"
@@ -774,3 +776,85 @@ func getArgs(result string) []string {
return params
}
+
+func Test_UnsupportedType(t *testing.T) {
+ t.Run("should panic on unsupported type", func(t *testing.T) {
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Error("expected panic for unsupported type")
+ }
+ msg := fmt.Sprintf("%v", r)
+ if !strings.Contains(msg, "cannot be represented as text") {
+ t.Errorf("unexpected panic message: %v", msg)
+ }
+ }()
+ gl := NewGremlinLang(nil)
+ gl.argAsString(struct{}{})
+ })
+
+ t.Run("should panic on unsupported type in slice", func(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil {
+ t.Error("expected panic for unsupported type in slice")
+ }
+ }()
+ gl := NewGremlinLang(nil)
+ gl.argAsString([]interface{}{1, struct{}{}})
+ })
+}
+
+func Test_ConvertParametersToString(t *testing.T) {
+ t.Run("should convert empty map", func(t *testing.T) {
+ result := ConvertParametersToString(nil)
+ if result != "[:]" {
+ t.Errorf("expected [:], got %v", result)
+ }
+ })
+
+ t.Run("should convert basic parameters", func(t *testing.T) {
+ params := map[string]interface{}{"x": int32(1)}
+ result := ConvertParametersToString(params)
+ if !strings.Contains(result, "\"x\":1") {
+ t.Errorf("expected parameter string to contain \"x\":1, got %v", result)
+ }
+ })
+
+ t.Run("should panic on unsupported type in parameters", func(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil {
+ t.Error("expected panic for unsupported type in parameters")
+ }
+ }()
+ params := map[string]interface{}{"x": struct{}{}}
+ ConvertParametersToString(params)
+ })
+
+ t.Run("should convert nested list parameter", func(t *testing.T) {
+ params := map[string]interface{}{"ids": []interface{}{int32(1), int32(2), int32(3)}}
+ result := ConvertParametersToString(params)
+ if !strings.Contains(result, "\"ids\":[1,2,3]") {
+ t.Errorf("expected nested list in result, got %v", result)
+ }
+ })
+
+ t.Run("should panic on unsupported type in map value", func(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil {
+ t.Error("expected panic for unsupported type in map value")
+ }
+ }()
+ gl := NewGremlinLang(nil)
+ m := map[interface{}]interface{}{"key": struct{}{}}
+ gl.argAsString(m)
+ })
+
+ t.Run("should panic on unsupported type via RequestOptionsBuilder.Create()", func(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil {
+ t.Error("expected panic for unsupported type in bindings")
+ }
+ }()
+ new(RequestOptionsBuilder).SetBindings(map[string]interface{}{"x": struct{}{}}).Create()
+ })
+}
diff --git a/gremlin-go/driver/request.go b/gremlin-go/driver/request.go
index eafc0a5071..4a2d11b0bb 100644
--- a/gremlin-go/driver/request.go
+++ b/gremlin-go/driver/request.go
@@ -51,8 +51,8 @@ func MakeStringRequest(stringGremlin string, traversalSource string, requestOpti
"g": traversalSource,
}
- if requestOptions.bindings != nil {
- newFields["bindings"] = requestOptions.bindings
+ if requestOptions.bindingsString != "" && requestOptions.bindingsString != "[:]" {
+ newFields["bindings"] = requestOptions.bindingsString
}
if requestOptions.evaluationTimeout != 0 {
diff --git a/gremlin-go/driver/requestOptions.go b/gremlin-go/driver/requestOptions.go
index abca4fbf0b..27e2fb8b0f 100644
--- a/gremlin-go/driver/requestOptions.go
+++ b/gremlin-go/driver/requestOptions.go
@@ -28,7 +28,7 @@ type RequestOptions struct {
evaluationTimeout int
batchSize int
userAgent string
- bindings map[string]interface{}
+ bindingsString string
materializeProperties string
}
@@ -38,6 +38,7 @@ type RequestOptionsBuilder struct {
batchSize int
userAgent string
bindings map[string]interface{}
+ bindingsString string
materializeProperties string
}
@@ -62,16 +63,30 @@ func (builder *RequestOptionsBuilder) SetUserAgent(userAgent string) *RequestOpt
}
func (builder *RequestOptionsBuilder) SetBindings(bindings map[string]interface{}) *RequestOptionsBuilder {
+ if builder.bindingsString != "" {
+ panic("cannot mix SetBindings() with SetBindingsString()")
+ }
builder.bindings = bindings
return builder
}
+func (builder *RequestOptionsBuilder) SetBindingsString(bindingsString string) *RequestOptionsBuilder {
+ if builder.bindings != nil {
+ panic("cannot mix SetBindingsString() with SetBindings()")
+ }
+ builder.bindingsString = bindingsString
+ return builder
+}
+
func (builder *RequestOptionsBuilder) SetMaterializeProperties(materializeProperties string) *RequestOptionsBuilder {
builder.materializeProperties = materializeProperties
return builder
}
func (builder *RequestOptionsBuilder) AddBinding(key string, binding interface{}) *RequestOptionsBuilder {
+ if builder.bindingsString != "" {
+ panic("cannot mix AddBinding() with SetBindingsString()")
+ }
if builder.bindings == nil {
builder.bindings = make(map[string]interface{})
}
@@ -86,8 +101,14 @@ func (builder *RequestOptionsBuilder) Create() RequestOptions {
requestOptions.evaluationTimeout = builder.evaluationTimeout
requestOptions.batchSize = builder.batchSize
requestOptions.userAgent = builder.userAgent
- requestOptions.bindings = builder.bindings
requestOptions.materializeProperties = builder.materializeProperties
+ // convert map bindings to string at creation time, matching Java's RequestOptions.Builder.create()
+ if builder.bindingsString != "" {
+ requestOptions.bindingsString = builder.bindingsString
+ } else if builder.bindings != nil {
+ requestOptions.bindingsString = ConvertParametersToString(builder.bindings)
+ }
+
return *requestOptions
}
diff --git a/gremlin-go/driver/requestOptions_test.go b/gremlin-go/driver/requestOptions_test.go
index 104a7e6ed2..05c15e43a2 100644
--- a/gremlin-go/driver/requestOptions_test.go
+++ b/gremlin-go/driver/requestOptions_test.go
@@ -49,31 +49,32 @@ func TestRequestOptions(t *testing.T) {
assert.Equal(t, "TestMaterializeProperties", r.materializeProperties)
})
t.Run("Test RequestOptionsBuilder with custom bindings", func(t *testing.T) {
- bindings := map[string]interface{}{"x": 2, "y": 5}
+ bindings := map[string]interface{}{"x": int32(2), "y": int32(5)}
r := new(RequestOptionsBuilder).SetBindings(bindings).Create()
- assert.Equal(t, bindings, r.bindings)
+ assert.Contains(t, r.bindingsString, "\"x\":2")
+ assert.Contains(t, r.bindingsString, "\"y\":5")
})
t.Run("Test RequestOptionsBuilder AddBinding() with no other bindings", func(t *testing.T) {
- r := new(RequestOptionsBuilder).AddBinding("x", 2).AddBinding("y", 5).Create()
- expectedBindings := map[string]interface{}{"x": 2, "y": 5}
- assert.Equal(t, expectedBindings, r.bindings)
+ r := new(RequestOptionsBuilder).AddBinding("x", int32(2)).AddBinding("y", int32(5)).Create()
+ assert.Contains(t, r.bindingsString, "\"x\":2")
+ assert.Contains(t, r.bindingsString, "\"y\":5")
})
t.Run("Test RequestOptionsBuilder AddBinding() overwriting existing key", func(t *testing.T) {
- r := new(RequestOptionsBuilder).AddBinding("x", 2).AddBinding("x", 5).Create()
- expectedBindings := map[string]interface{}{"x": 5}
- assert.Equal(t, expectedBindings, r.bindings)
+ r := new(RequestOptionsBuilder).AddBinding("x", int32(2)).AddBinding("x", int32(5)).Create()
+ assert.Contains(t, r.bindingsString, "\"x\":5")
})
t.Run("Test RequestOptionsBuilder AddBinding() with existing bindings", func(t *testing.T) {
- bindings := map[string]interface{}{"x": 2, "y": 5}
- r := new(RequestOptionsBuilder).SetBindings(bindings).AddBinding("z", 7).Create()
- expectedBindings := map[string]interface{}{"x": 2, "y": 5, "z": 7}
- assert.Equal(t, expectedBindings, r.bindings)
+ bindings := map[string]interface{}{"x": int32(2), "y": int32(5)}
+ r := new(RequestOptionsBuilder).SetBindings(bindings).AddBinding("z", int32(7)).Create()
+ assert.Contains(t, r.bindingsString, "\"x\":2")
+ assert.Contains(t, r.bindingsString, "\"y\":5")
+ assert.Contains(t, r.bindingsString, "\"z\":7")
})
- t.Run("Test RequestOptionsBuilder SetBinding(...), SetBinding(nil), AddBinding(...)", func(t *testing.T) {
- bindings := map[string]interface{}{"x": 2, "y": 5}
- r := new(RequestOptionsBuilder).SetBindings(bindings).
- SetBindings(nil).AddBinding("z", 7).Create()
- expectedBindings := map[string]interface{}{"z": 7}
- assert.Equal(t, expectedBindings, r.bindings)
+ t.Run("Test RequestOptionsBuilder SetBindings resets previous bindings", func(t *testing.T) {
+ bindings1 := map[string]interface{}{"x": int32(2)}
+ bindings2 := map[string]interface{}{"z": int32(7)}
+ r := new(RequestOptionsBuilder).SetBindings(bindings1).SetBindings(bindings2).Create()
+ assert.Contains(t, r.bindingsString, "\"z\":7")
+ assert.NotContains(t, r.bindingsString, "\"x\"")
})
}
diff --git a/gremlin-go/driver/request_test.go b/gremlin-go/driver/request_test.go
index 6bd91f5756..4b1db25aba 100644
--- a/gremlin-go/driver/request_test.go
+++ b/gremlin-go/driver/request_test.go
@@ -51,4 +51,33 @@ func TestRequest(t *testing.T) {
new(RequestOptionsBuilder).SetUserAgent("TestUserAgent").Create())
assert.Equal(t, "TestUserAgent", r.Fields["userAgent"])
})
+
+ t.Run("Test makeStringRequest() with string bindings", func(t *testing.T) {
+ r := MakeStringRequest("g.V(x)", "g",
+ new(RequestOptionsBuilder).SetBindingsString("[\"x\":1]").Create())
+ assert.Equal(t, "[\"x\":1]", r.Fields["bindings"])
+ })
+
+ t.Run("Test makeStringRequest() with map bindings converted to string", func(t *testing.T) {
+ r := MakeStringRequest("g.V(x)", "g",
+ new(RequestOptionsBuilder).SetBindings(map[string]interface{}{"x": int32(1)}).Create())
+ assert.Contains(t, r.Fields["bindings"], "\"x\":1")
+ })
+
+ t.Run("Test RequestOptionsBuilder.Create() converts map bindings to string", func(t *testing.T) {
+ opts := new(RequestOptionsBuilder).SetBindings(map[string]interface{}{"x": int32(1)}).Create()
+ assert.NotEmpty(t, opts.bindingsString)
+ assert.Contains(t, opts.bindingsString, "\"x\":1")
+ })
+
+ t.Run("Test RequestOptionsBuilder rejects mixing SetBindings and SetBindingsString", func(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil {
+ t.Error("expected panic when mixing SetBindings and SetBindingsString")
+ }
+ }()
+ new(RequestOptionsBuilder).
+ SetBindings(map[string]interface{}{"x": int32(1)}).
+ SetBindingsString("[\"y\":2]")
+ })
}
diff --git a/gremlin-js/gremlin-javascript/lib/driver/client.ts b/gremlin-js/gremlin-javascript/lib/driver/client.ts
index 35cffc28f6..8bece7bc6d 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/client.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/client.ts
@@ -98,10 +98,18 @@ export default class Client {
requestBuilder.addLanguage(requestOptions.language);
}
if (requestOptions?.bindings) {
- requestBuilder.addBindings(requestOptions.bindings);
+ if (typeof requestOptions.bindings === 'string') {
+ requestBuilder.addBindingsString(requestOptions.bindings);
+ } else {
+ requestBuilder.addBindings(requestOptions.bindings);
+ }
}
if (bindings) {
- requestBuilder.addBindings(bindings);
+ if (typeof bindings === 'string') {
+ requestBuilder.addBindingsString(bindings);
+ } else {
+ requestBuilder.addBindings(bindings);
+ }
}
if (requestOptions?.materializeProperties) {
requestBuilder.addMaterializeProperties(requestOptions.materializeProperties);
diff --git a/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts b/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts
index b6624b69c7..5099be50dd 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts
@@ -88,9 +88,9 @@ export default class DriverRemoteConnection extends RemoteConnection {
requestOptions.bulkResults = true;
}
- const params = gremlinLang.getParameters();
- if (params.size > 0) {
- requestOptions.params = Object.fromEntries(params);
+ const parametersString = gremlinLang.getParametersAsString();
+ if (parametersString !== '[:]') {
+ requestOptions.bindings = parametersString;
}
return this._client.submit(gremlinLang.getGremlin(), null, requestOptions)
diff --git a/gremlin-js/gremlin-javascript/lib/driver/request-message.ts b/gremlin-js/gremlin-javascript/lib/driver/request-message.ts
index 9360416def..baaa8a2b19 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/request-message.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/request-message.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import GremlinLang from '../process/gremlin-lang.js';
+
// Token constants
const Tokens = {
ARGS_LANGUAGE: 'language',
@@ -109,6 +111,7 @@ export class RequestMessage {
export class Builder {
private readonly gremlin: string;
private readonly bindings = {};
+ private bindingsString?: string;
public language: string;
public timeoutMs?: number;
public g?: string;
@@ -128,16 +131,24 @@ export class Builder {
}
addBinding(key: string, value: any): Builder {
+ if (this.bindingsString) throw new Error('Cannot mix addBinding() with addBindingsString().');
Object.assign(this.bindings, {[key]: value})
return this;
}
addBindings(otherBindings: object): Builder {
if (!otherBindings) throw new Error('bindings argument cannot be null.');
+ if (this.bindingsString) throw new Error('Cannot mix addBindings() with addBindingsString().');
Object.assign(this.bindings, otherBindings)
return this;
}
+ addBindingsString(bindingsString: string): Builder {
+ if (Object.keys(this.bindings).length > 0) throw new Error('Cannot mix addBindingsString() with addBinding()/addBindings().');
+ this.bindingsString = bindingsString;
+ return this;
+ }
+
addG(g: string): Builder {
if (!g) throw new Error('g argument cannot be null.');
this.g = g;
@@ -175,12 +186,20 @@ export class Builder {
* Create the request message given the settings provided to the Builder.
*/
create(): RequestMessage {
+ // mutual exclusion between addBindings/addBinding and addBindingsString is
+ // enforced at call time, so only one path can be set here
+ let bindings: string | undefined;
+ if (this.bindingsString) {
+ bindings = this.bindingsString;
+ } else if (Object.keys(this.bindings).length > 0) {
+ bindings = GremlinLang.convertParametersToString(new Map(Object.entries(this.bindings)));
+ }
// @ts-ignore - accessing private constructor from Builder
return new RequestMessage(
this.gremlin,
this.language || 'gremlin-lang',
this.timeoutMs,
- Object.keys(this.bindings).length > 0 ? this.bindings : undefined,
+ bindings,
this.g,
this.materializeProperties,
this.bulkResults,
diff --git a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
index 1830a7bb7a..6d8c09287b 100644
--- a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
@@ -152,7 +152,7 @@ export default class GremlinLang {
if (entries.length === 0) return '[:]';
return '[' + entries.map(([k, v]) => `${this._argAsString(k)}:${this._argAsString(v)}`).join(',') + ']';
}
- return String(arg);
+ throw new TypeError(`GremlinLang contains at least one type [${arg?.constructor?.name ?? typeof arg}] that cannot be represented as text.`);
}
addStep(name: string, args?: any[]): GremlinLang {
@@ -193,4 +193,19 @@ export default class GremlinLang {
}
return prefix + this.gremlin;
}
+
+ getParametersAsString(): string {
+ return GremlinLang.convertParametersToString(this.parameters);
+ }
+
+ static convertParametersToString(params: Map<string, any> | null): string {
+ if (params == null || params.size === 0) return '[:]';
+
+ const helper = new GremlinLang();
+ const parts: string[] = [];
+ params.forEach((v, k) => {
+ parts.push(`${helper._argAsString(k)}:${helper._argAsString(v)}`);
+ });
+ return '[' + parts.join(',') + ']';
+ }
}
\ No newline at end of file
diff --git a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
index be3a39c460..40472c99f5 100644
--- a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
@@ -435,4 +435,63 @@ describe('GremlinLang', function () {
assert.strictEqual(g.inject(view).getGremlinLang().getGremlin(), 'g.inject(Binary("AQID"))');
});
});
+
+ describe('Unsupported type tests', function () {
+ it('should throw TypeError for unsupported type', function () {
+ class UnsupportedType {}
+ assert.throws(() => {
+ g.inject(new UnsupportedType()).getGremlinLang().getGremlin();
+ }, TypeError);
+ });
+
+ it('should throw TypeError for unsupported type in array', function () {
+ class UnsupportedType {}
+ assert.throws(() => {
+ g.inject([1, new UnsupportedType()]).getGremlinLang().getGremlin();
+ }, TypeError);
+ });
+
+ it('should include type name in error message', function () {
+ class MyCustomType {}
+ assert.throws(() => {
+ g.inject(new MyCustomType()).getGremlinLang().getGremlin();
+ }, /cannot be represented as text/);
+ });
+ });
+
+ describe('convertParametersToString', function () {
+ it('should return [:] for empty map', function () {
+ assert.strictEqual(GremlinLang.convertParametersToString(null), '[:]');
+ assert.strictEqual(GremlinLang.convertParametersToString(new Map()), '[:]');
+ });
+
+ it('should convert basic parameters', function () {
+ const params = new Map([['x', 1]]);
+ const result = GremlinLang.convertParametersToString(params);
+ assert.ok(result.includes("'x':1"));
+ });
+
+ it('should throw TypeError for unsupported type in parameters', function () {
+ class UnsupportedType {}
+ const params = new Map([['x', new UnsupportedType()]]);
+ assert.throws(() => {
+ GremlinLang.convertParametersToString(params);
+ }, TypeError);
+ });
+
+ it('should convert nested list parameter', function () {
+ const params = new Map([['ids', [1, 2, 3]]]);
+ const result = GremlinLang.convertParametersToString(params);
+ assert.ok(result.includes("'ids':[1,2,3]"));
+ });
+
+ it('should convert multiple parameters', function () {
+ const params = new Map([['x', 1], ['name', 'marko']]);
+ const result = GremlinLang.convertParametersToString(params);
+ assert.ok(result.startsWith('['));
+ assert.ok(result.endsWith(']'));
+ assert.ok(result.includes("'x':1"));
+ assert.ok(result.includes("'name':'marko'"));
+ });
+ });
});
\ No newline at end of file
diff --git a/gremlin-python/src/main/python/gremlin_python/driver/client.py b/gremlin-python/src/main/python/gremlin_python/driver/client.py
index 53995616b0..1ad25d9b68 100644
--- a/gremlin-python/src/main/python/gremlin_python/driver/client.py
+++ b/gremlin-python/src/main/python/gremlin_python/driver/client.py
@@ -157,7 +157,11 @@ class Client:
# TODO: bindings is now part of request_options, evaluate the need to keep it separate in python.
# Note this bindings parameter only applies to string script submissions
if isinstance(message, str) and bindings:
- fields['bindings'] = bindings
+ from gremlin_python.process.traversal import GremlinLang
+ if isinstance(bindings, dict):
+ fields['bindings'] = GremlinLang.convert_parameters_to_string(bindings)
+ else:
+ fields['bindings'] = bindings
if isinstance(message, str):
log.debug("fields='%s', gremlin='%s'", str(fields), str(message))
@@ -168,14 +172,10 @@ class Client:
message.fields.update({token: request_options[token] for token in request.Tokens
if token in request_options and token != 'bindings'})
if 'bindings' in request_options:
- if 'bindings' in message.fields:
- message.fields['bindings'].update(request_options['bindings'])
- else:
- message.fields['bindings'] = request_options['bindings']
- if 'params' in request_options:
- if 'bindings' in message.fields:
- message.fields['bindings'].update(request_options['params'])
- else:
- message.fields['bindings'] = request_options['params']
+ bindings_val = request_options['bindings']
+ if isinstance(bindings_val, dict):
+ from gremlin_python.process.traversal import GremlinLang
+ bindings_val = GremlinLang.convert_parameters_to_string(bindings_val)
+ message.fields['bindings'] = bindings_val
return conn.write(message)
diff --git a/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py b/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py
index 0039a4621e..2393e72bd3 100644
--- a/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py
+++ b/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py
@@ -126,7 +126,9 @@ class DriverRemoteConnection(RemoteConnection):
# request the server to bulk results by default when using drc through request options
if 'bulkResults' not in request_options:
request_options['bulkResults'] = True
- if gremlin_lang.parameters is not None and len(gremlin_lang.parameters) > 0:
- request_options["params"] = gremlin_lang.parameters
+
+ parameters_string = gremlin_lang.get_parameters_as_string()
+ if parameters_string != '[:]':
+ request_options["bindings"] = parameters_string
return request_options
diff --git a/gremlin-python/src/main/python/gremlin_python/process/traversal.py b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
index 4a531d5845..76c91822e7 100644
--- a/gremlin-python/src/main/python/gremlin_python/process/traversal.py
+++ b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
@@ -814,13 +814,11 @@ class GremlinLang(object):
self.gremlin = []
self.parameters = {}
self.options_strategies = []
- self.param_count = AtomicInteger()
if gremlin_lang is not None:
self.gremlin = list(gremlin_lang.gremlin)
self.parameters = dict(gremlin_lang.parameters)
self.options_strategies = list(gremlin_lang.options_strategies)
- self.param_count = gremlin_lang.param_count
def _add_to_gremlin(self, string_name, *args):
@@ -939,18 +937,17 @@ class GremlinLang(object):
if isinstance(arg, list):
return self._process_list(arg)
- if hasattr(arg, '__class__'):
- try:
- return arg.__name__
- except AttributeError:
- pass
+ # Strategy instances render as their name (e.g. "ReadOnlyStrategy") for withoutStrategies.
+ # Class objects render as their __name__ (e.g. strategy classes passed directly).
+ # These replace the old hasattr(arg, '__class__') check which was too broad since every
+ # Python object has __class__, making it a silent escape hatch for anything with __name__.
+ if isinstance(arg, TraversalStrategy):
+ return arg.strategy_name
- return self._as_parameter(arg)
+ if isinstance(arg, type):
+ return arg.__name__
- def _as_parameter(self, arg):
- param_name = f'_{self.param_count.get_and_increment()}'
- self.parameters[param_name] = arg
- return param_name
+ raise TypeError(f'GremlinLang contains at least one type [{type(arg).__name__}] that cannot be represented as text.')
# Do special processing needed to format predicates that come in
# such as "gt(a)" correctly.
@@ -1029,12 +1026,27 @@ class GremlinLang(object):
def get_parameters(self):
return self.parameters
+ def get_parameters_as_string(self):
+ return GremlinLang.convert_parameters_to_string(self.parameters)
+
+ @staticmethod
+ def convert_parameters_to_string(params):
+ """Converts a parameter map to a gremlin-lang map literal string.
+
+ Raises TypeError if any value is an unsupported type.
+ """
+ if params is None or len(params) == 0:
+ return '[:]'
+
+ helper = GremlinLang()
+ parts = []
+ for k, v in params.items():
+ parts.append(f'{helper._arg_as_string(k)}:{helper._arg_as_string(v)}')
+ return '[' + ','.join(parts) + ']'
+
def add_g(self, g):
self.parameters['g'] = g
- def reset(self):
- self.param_count.set(0)
-
def add_source(self, source_name, *args):
if source_name == 'withStrategies' and len(args) != 0:
diff --git a/gremlin-python/src/main/python/tests/integration/driver/test_client.py b/gremlin-python/src/main/python/tests/integration/driver/test_client.py
index 2c8c04c4f2..a21590fb6c 100644
--- a/gremlin-python/src/main/python/tests/integration/driver/test_client.py
+++ b/gremlin-python/src/main/python/tests/integration/driver/test_client.py
@@ -261,7 +261,7 @@ def test_client_gremlin_lang_request_options_with_binding(client):
g = GraphTraversalSource(Graph(), TraversalStrategies())
# Note that bindings for constructed traversals is done via Parameter only
t = g.with_('language', 'gremlin-lang').V(GValue('x', [1, 2, 3])).count()
- request_opts = {'language': 'gremlin-lang', 'params': {'x': [1, 2, 3]}}
+ request_opts = {'language': 'gremlin-lang', 'bindings': {'x': [1, 2, 3]}}
message = create_basic_request_message(t)
result_set = client.submit(message, request_options=request_opts)
assert result_set.all().result()[0] == 3
@@ -269,10 +269,10 @@ def test_client_gremlin_lang_request_options_with_binding(client):
result_set = client.submit('g.V(x).values("name")', request_options=request_opts)
assert result_set.all().result()[0] == 'marko'
# For script submission only, we can also add bindings to request options and they will be applied
- request_opts['bindings'] = {'y': 4}
- result_set = client.submit('g.V(y).values("name")', request_options=request_opts)
+ request_opts2 = {'language': 'gremlin-lang', 'bindings': {'y': 4}}
+ result_set = client.submit('g.V(y).values("name")', request_options=request_opts2)
assert result_set.all().result()[0] == 'josh'
- result_set = client.submit('g.V(z).values("name")', bindings={'z': 5}, request_options=request_opts)
+ result_set = client.submit('g.V(z).values("name")', bindings={'z': 5})
assert result_set.all().result()[0] == 'ripple'
diff --git a/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py b/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
index 0f1351e8c1..edf0515317 100644
--- a/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
+++ b/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
@@ -25,7 +25,7 @@ from gremlin_python.process.strategies import ReadOnlyStrategy, SubgraphStrategy
from gremlin_python.process.traversal import within, eq, T, Order, Scope, Column, Operator, P, Pop, Cardinality, \
between, inside, WithOptions, ShortestPath, starting_with, ending_with, containing, gt, lte, GValue
from gremlin_python.statics import SingleByte, short, long, bigint, BigDecimal, SingleChar
-from gremlin_python.structure.graph import Graph, Vertex, Edge, VertexProperty
+from gremlin_python.structure.graph import Graph, Vertex
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.process.graph_traversal import __
from datetime import datetime, timezone, timedelta
@@ -415,80 +415,66 @@ class TestGremlinLang(object):
"g.V(0)"])
# 107
- outVertex = Vertex(0, "person")
- inVertex = Vertex(1, "person")
- edge = Edge(2, outVertex, "knows", inVertex)
- tests.append([g.E(edge),
- "g.E(_0)"])
-
- # 108
- vp = VertexProperty(3, "time", "18:00", None)
- tests.append([g.inject(vp),
- "g.inject(_1)"])
-
- # 109
tests.append([g.inject({'name': 'java'}, {T.id: 0}, {},
{'age': float(10), 'pos_inf': float("inf"), 'neg_inf': float("-inf"),
'nan': float("nan")}),
"g.inject(['name':'java'],[(T.id):0],[:],['age':10.0D,'pos_inf':+Infinity,'neg_inf':-Infinity,'nan':NaN])"])
- # 110
+ # 108
tests.append([g.inject(float(1)).is_(P.eq(1).or_(P.gt(2)).or_(P.lte(3)).or_(P.gte(4))),
"g.inject(1.0D).is(eq(1).or(gt(2)).or(lte(3)).or(gte(4)))"])
- # 111
+ # 109
tests.append([g.V().has_label('person').has('age', P.gt(10).or_(P.gte(11).and_(P.lt(20))).and_(
P.lt(29).or_(P.eq(35)))).name,
"g.V().hasLabel('person').has('age',gt(10).or(gte(11).and(lt(20))).and(lt(29).or(eq(35)))).values('name')"])
- # 112
+ # 110
tests.append([g.inject(set('a')),
"g.inject({'a'})"])
- # 113
+ # 111
tests.append([g.merge_v(None),
"g.mergeV(null)"])
- # 114
+ # 112
tests.append([g.merge_e(None),
"g.mergeE(null)"])
- # 115
+ # 113
tests.append([g.add_v('\"test\"'),
"g.addV('\"test\"')"])
- # 116
+ # 114
tests.append([g.add_v('t\'"est'),
"g.addV('t\\'\"est')"])
- # 117
+ # 115
tests.append([g.inject(True, SingleByte(1), short(2), 3, long(4), float(5),
float(6), bigint(7), BigDecimal(0, 8)),
"g.inject(true,1B,2S,3,4L,5.0D,6.0D,7N,8M)"])
- #118
+ # 116
tests.append([g.inject(uuid.UUID('9b8d8a9c-61c2-43e5-9cc8-c27b9261290e')),
'g.inject(UUID("9b8d8a9c-61c2-43e5-9cc8-c27b9261290e"))'])
- # 119
+ # 117
tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30, tzinfo=timezone.utc)),
"g.addV('test').property('date',datetime(\"2021-02-01T09:30:00+00:00\"))"])
-
- # 120
+
+ # 118
tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30, tzinfo=timezone(timedelta(hours=7)))),
"g.addV('test').property('date',datetime(\"2021-02-01T09:30:00+07:00\"))"])
-
- # 121
+
+ # 119
tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30, tzinfo=timezone(timedelta(hours=-5)))),
"g.addV('test').property('date',datetime(\"2021-02-01T09:30:00-05:00\"))"])
- # Character - backslash (not in feature file due to Gherkin escaping issues)
- # 122
+ # 120 - Character backslash (not in feature file due to Gherkin escaping issues)
tests.append([g.inject(SingleChar('\\')),
"g.inject('\\\\'c)"])
- # Character - single quote (no feature equivalent)
- # 123
+ # 121 - Character single quote (no feature equivalent)
tests.append([g.inject(SingleChar("'")),
"g.inject(\"'\"c)"])
@@ -545,3 +531,51 @@ class TestGremlinLang(object):
gremlin = g.inject(p).V(p).gremlin_lang
assert 'g.inject(ids).V(ids)' == gremlin.get_gremlin()
assert val == gremlin.get_parameters().get('ids')
+
+ def test_unsupported_type_throws(self):
+ g = traversal().with_(None)
+ import pytest
+ with pytest.raises(TypeError, match='cannot be represented as text'):
+ g.inject(object()).gremlin_lang.get_gremlin()
+
+ def test_unsupported_type_in_list_throws(self):
+ g = traversal().with_(None)
+ import pytest
+ with pytest.raises(TypeError, match='cannot be represented as text'):
+ g.inject([1, object()]).gremlin_lang.get_gremlin()
+
+ def test_unsupported_type_in_dict_throws(self):
+ g = traversal().with_(None)
+ import pytest
+ with pytest.raises(TypeError, match='cannot be represented as text'):
+ g.inject({'key': object()}).gremlin_lang.get_gremlin()
+
+ def test_unsupported_type_in_convert_parameters_to_string_throws(self):
+ from gremlin_python.process.traversal import GremlinLang
+ import pytest
+ with pytest.raises(TypeError, match='cannot be represented as text'):
+ GremlinLang.convert_parameters_to_string({'x': object()})
+
+ def test_convert_parameters_to_string_empty(self):
+ from gremlin_python.process.traversal import GremlinLang
+ assert '[:]' == GremlinLang.convert_parameters_to_string({})
+ assert '[:]' == GremlinLang.convert_parameters_to_string(None)
+
+ def test_convert_parameters_to_string_basic(self):
+ from gremlin_python.process.traversal import GremlinLang
+ result = GremlinLang.convert_parameters_to_string({'x': 1, 'name': 'marko'})
+ assert result.startswith('[')
+ assert result.endswith(']')
+ assert "'x':1" in result
+ assert "'name':'marko'" in result
+
+ def test_convert_parameters_to_string_nested_list(self):
+ from gremlin_python.process.traversal import GremlinLang
+ result = GremlinLang.convert_parameters_to_string({'ids': [1, 2, 3]})
+ assert "'ids':[1,2,3]" in result
+
+ def test_convert_parameters_to_string_escaped_string(self):
+ from gremlin_python.process.traversal import GremlinLang
+ result = GremlinLang.convert_parameters_to_string({'name': "it's a test"})
+ assert "'name'" in result
+ assert "it" in result
diff --git a/gremlin-python/src/main/python/tests/unit/process/test_strategies.py b/gremlin-python/src/main/python/tests/unit/process/test_strategies.py
index 0667f0d19b..324e125e35 100644
--- a/gremlin-python/src/main/python/tests/unit/process/test_strategies.py
+++ b/gremlin-python/src/main/python/tests/unit/process/test_strategies.py
@@ -52,24 +52,21 @@ class TestTraversalStrategies(object):
##
gremlin_script = g.without_strategies(ReadOnlyStrategy()).V().gremlin_lang
gremlin_instr = gremlin_script.gremlin
- gremlin_params = gremlin_script.parameters
assert "withStrategies" in str(gremlin_script)
assert "ReadOnlyStrategy" in str(gremlin_script)
assert "IncidentToAdjacentStrategy" in str(gremlin_script)
assert "withoutStrategies" in str(gremlin_script)
assert "V()" in str(gremlin_script)
assert "withStrategies(ReadOnlyStrategy,IncidentToAdjacentStrategy)" == gremlin_instr[1]
- assert ReadOnlyStrategy() == gremlin_params["_0"]
+ assert "withoutStrategies(ReadOnlyStrategy)" in str(gremlin_script)
##
gremlin_script = g.without_strategies(ReadOnlyStrategy(), LazyBarrierStrategy()).V().gremlin_lang
- gremlin_params = gremlin_script.parameters
assert "withStrategies" in str(gremlin_script)
assert "ReadOnlyStrategy" in str(gremlin_script)
assert "IncidentToAdjacentStrategy" in str(gremlin_script)
assert "withoutStrategies" in str(gremlin_script)
assert "V()" in str(gremlin_script)
- assert ReadOnlyStrategy() == gremlin_params["_1"]
- assert LazyBarrierStrategy() == gremlin_params["_2"]
+ assert "withoutStrategies(ReadOnlyStrategy,LazyBarrierStrategy)" in str(gremlin_script)
###
g = traversal().with_(None)
gremlin_script = g.with_("x", "test").with_("y").gremlin_lang
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java
index 06e2507e75..10148abe08 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java
@@ -30,7 +30,9 @@ import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceFactory;
import org.apache.tinkerpop.gremlin.util.Tokens;
import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -57,6 +59,7 @@ public class Context {
private boolean timeoutExecutorGrabbed = false;
private final Object timeoutExecutorLock = new Object();
private String transactionId; // initially null for non-transactional requests and begin() calls; set after transaction creation.
+ private Map<String, Object> parameters = new HashMap<>(); // only available after string parameters are parsed by grammar.
public Context(final RequestMessage requestMessage, final ChannelHandlerContext ctx,
final Settings settings, final GraphManager graphManager,
@@ -129,6 +132,13 @@ public class Context {
this.transactionId = transactionId;
}
+ public Map<String, Object> getParameters() {
+ return this.parameters;
+ }
+
+ public void setParameters(final Map<String, Object> parameters) {
+ this.parameters = parameters;
+ }
/**
* Gets the current request to Gremlin Server.
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
index fa34d4d762..560c335372 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
@@ -34,6 +34,7 @@ import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor;
import org.apache.tinkerpop.gremlin.groovy.jsr223.TimedInterruptTimeoutException;
import org.apache.tinkerpop.gremlin.jsr223.GremlinScriptEngine;
import org.apache.tinkerpop.gremlin.language.grammar.GremlinParserException;
+import org.apache.tinkerpop.gremlin.language.grammar.GremlinQueryParser;
import org.apache.tinkerpop.gremlin.process.traversal.Failure;
import org.apache.tinkerpop.gremlin.process.traversal.Operator;
import org.apache.tinkerpop.gremlin.process.traversal.Order;
@@ -76,7 +77,6 @@ import javax.script.SimpleBindings;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -200,7 +200,7 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
try {
logger.debug("Processing request containing script [{}] and bindings of [{}] on {}",
requestMessage.getFieldOrDefault(Tokens.ARGS_GREMLIN, ""),
- requestMessage.getFieldOrDefault(Tokens.ARGS_BINDINGS, Collections.emptyMap()),
+ requestMessage.getFieldOrDefault(Tokens.ARGS_BINDINGS, "[:]"),
Thread.currentThread().getName());
if (settings.enableAuditLog) {
AuthenticatedUser user = ctx.channel().attr(StateKey.AUTHENTICATED_USER).get();
@@ -238,6 +238,33 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
throw new ProcessingException(GremlinError.transactionalControlRequiresTransaction());
}
+ // Guard against bad parameters while trying to parse string-based bindings into a Map<String, Object>
+ if (requestMessage.optionalField(Tokens.ARGS_BINDINGS).isPresent()) {
+ Map<String, Object> bindings = null;
+ final String bindingsString = (String) requestMessage.getFields().get(Tokens.ARGS_BINDINGS);
+ try {
+ bindings = GremlinQueryParser.parseParameters(bindingsString);
+ } catch (GremlinParserException e) {
+ throw new ProcessingException(GremlinError.incorrectParameterFormat(bindingsString, e));
+ }
+
+ final String language = requestMessage.<String>optionalField(Tokens.ARGS_LANGUAGE).orElse("gremlin-lang");
+ if ("gremlin-groovy".equals(language)) {
+ final Set<String> badBindings = IteratorUtils.set(IteratorUtils.<String>filter(
+ bindings.keySet().iterator(),
+ INVALID_BINDINGS_KEYS::contains));
+ if (!badBindings.isEmpty()) {
+ throw new ProcessingException(GremlinError.binding(badBindings));
+ }
+ }
+
+ if (bindings.size() > settings.maxParameters) {
+ throw new ProcessingException(GremlinError.binding(bindings.size(), settings.maxParameters));
+ }
+
+ requestCtx.setParameters(bindings);
+ }
+
// Send back the 200 OK response header here since the response is always chunk transfer encoded. Any
// failures that follow this will show up in the response body instead.
sendHttpResponse(ctx, OK, createResponseHeaders(ctx, serializer, requestCtx).toArray(CharSequence[]::new));
@@ -371,23 +398,6 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
private void iterateScriptEvalResult(final Context context, MessageSerializer<?> serializer, final RequestMessage message)
throws ProcessingException, InterruptedException, ScriptException {
- if (message.optionalField(Tokens.ARGS_BINDINGS).isPresent()) {
- final Map bindings = (Map) message.getFields().get(Tokens.ARGS_BINDINGS);
- if (IteratorUtils.anyMatch(bindings.keySet().iterator(), k -> null == k || !(k instanceof String))) {
- throw new ProcessingException(GremlinError.binding());
- }
-
- final Set<String> badBindings = IteratorUtils.set(IteratorUtils.<String>filter(bindings.keySet().iterator(), INVALID_BINDINGS_KEYS::contains));
- if (!badBindings.isEmpty()) {
- throw new ProcessingException(GremlinError.binding(badBindings));
- }
-
- // ignore control bindings that get passed in with the "#jsr223" prefix - those aren't used in compilation
- if (IteratorUtils.count(IteratorUtils.filter(bindings.keySet().iterator(), k -> !k.toString().startsWith("#jsr223"))) > settings.maxParameters) {
- throw new ProcessingException(GremlinError.binding(bindings.size(), settings.maxParameters));
- }
- }
-
final Map<String, Object> args = message.getFields();
final String language = args.containsKey(Tokens.ARGS_LANGUAGE) ? (String) args.get(Tokens.ARGS_LANGUAGE) : "gremlin-lang";
final GremlinScriptEngine scriptEngine = gremlinExecutor.getScriptEngineManager().getEngineByName(language);
@@ -507,7 +517,7 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
final RequestMessage msg = ctx.getRequestMessage();
// add any bindings to override any other supplied
- Optional.ofNullable((Map<String, Object>) msg.getFields().get(Tokens.ARGS_BINDINGS)).ifPresent(bindings::putAll);
+ bindings.putAll(ctx.getParameters());
if (msg.getFields().containsKey(Tokens.ARGS_G)) {
final String aliased = msg.getField(Tokens.ARGS_G);
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestMessageDecoder.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestMessageDecoder.java
index 4501697475..dc5dfabad8 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestMessageDecoder.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestMessageDecoder.java
@@ -32,14 +32,10 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import org.apache.tinkerpop.shaded.jackson.databind.JsonNode;
import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
-import org.apache.tinkerpop.shaded.jackson.databind.node.ArrayNode;
-import org.apache.tinkerpop.shaded.jackson.databind.node.ObjectNode;
import org.javatuples.Pair;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -194,13 +190,7 @@ public class HttpRequestMessageDecoder extends MessageToMessageDecoder<FullHttpR
final RequestMessage.Builder builder = RequestMessage.build(scriptNode.asText());
final JsonNode bindingsNode = body.get(Tokens.ARGS_BINDINGS);
- if (bindingsNode != null && !bindingsNode.isObject())
- throw new IllegalArgumentException("bindings must be a Map");
-
- final Map<String, Object> bindings = new HashMap<>();
- if (bindingsNode != null)
- bindingsNode.fields().forEachRemaining(kv -> bindings.put(kv.getKey(), fromJsonNode(kv.getValue())));
- builder.addBindings(bindings);
+ if (bindingsNode != null) builder.addBindings(bindingsNode.asText());
final JsonNode gNode = body.get(Tokens.ARGS_G);
if (null != gNode) builder.addG(gNode.asText());
@@ -222,33 +212,4 @@ public class HttpRequestMessageDecoder extends MessageToMessageDecoder<FullHttpR
return builder.create();
}
-
- private Object fromJsonNode(final JsonNode node) {
- if (node.isNull())
- return null;
- else if (node.isObject()) {
- final Map<String, Object> map = new HashMap<>();
- final ObjectNode objectNode = (ObjectNode) node;
- final Iterator<String> iterator = objectNode.fieldNames();
- while (iterator.hasNext()) {
- String key = iterator.next();
- map.put(key, fromJsonNode(objectNode.get(key)));
- }
- return map;
- } else if (node.isArray()) {
- final ArrayNode arrayNode = (ArrayNode) node;
- final ArrayList<Object> array = new ArrayList<>();
- for (int i = 0; i < arrayNode.size(); i++) {
- array.add(fromJsonNode(arrayNode.get(i)));
- }
- return array;
- } else if (node.isFloatingPointNumber())
- return node.asDouble();
- else if (node.isIntegralNumber())
- return node.asLong();
- else if (node.isBoolean())
- return node.asBoolean();
- else
- return node.asText();
- }
}
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java
index 3014438abd..7a5b77faff 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java
@@ -69,6 +69,13 @@ public class GremlinError {
return new GremlinError(HttpResponseStatus.BAD_REQUEST, message, "InvalidRequestException");
}
+ public static GremlinError incorrectParameterFormat(final String parameters, final GremlinParserException gpe) {
+ final String message = String.format(
+ "The provided parameter string \"%s\" could not be converted into a Map. %s",
+ parameters, gpe.getMessage());
+ return new GremlinError(HttpResponseStatus.BAD_REQUEST, message, "InvalidRequestException");
+ }
+
public static GremlinError binding(final Set<String> badBindings) {
final String message = String.format("The message supplies one or more invalid parameters key of [%s] - these are reserved names.",
badBindings);
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverTransactionIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverTransactionIntegrateTest.java
index 9a88487ddf..3cb37d06a1 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverTransactionIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverTransactionIntegrateTest.java
@@ -25,6 +25,7 @@ import org.apache.tinkerpop.gremlin.driver.RequestOptions;
import org.apache.tinkerpop.gremlin.driver.Result;
import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
import org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer;
import org.apache.tinkerpop.gremlin.structure.Transaction;
import org.apache.tinkerpop.gremlin.structure.Vertex;
@@ -614,4 +615,36 @@ public class GremlinDriverTransactionIntegrateTest extends AbstractGremlinServer
assertEquals(1L, g.V().hasLabel("val").count().next().longValue());
}
+
+ @Test
+ public void shouldRejectTransactionWithUnsupportedType() throws Exception {
+ final GraphTraversalSource g = traversal().with(DriverRemoteConnection.using(cluster, GTX));
+ final GraphTraversalSource gtx = g.tx().begin();
+ try {
+ gtx.inject(new Thread()).iterate();
+ fail("Expected IllegalArgumentException for unsupported type");
+ } catch (IllegalArgumentException ex) {
+ assertThat(ex.getMessage(), containsString("at least one type"));
+ assertThat(ex.getMessage(), containsString("Thread"));
+ assertThat(ex.getMessage(), containsString("cannot be represented as text"));
+ } finally {
+ gtx.tx().rollback();
+ }
+ }
+
+ @Test
+ public void shouldRejectTransactionWithUnsupportedTypeInGValue() throws Exception {
+ final GraphTraversalSource g = traversal().with(DriverRemoteConnection.using(cluster, GTX));
+ final GraphTraversalSource gtx = g.tx().begin();
+ try {
+ gtx.V(GValue.of("x", new Thread())).iterate();
+ fail("Expected exception for unsupported type in GValue parameter");
+ } catch (Exception ex) {
+ final Throwable inner = ExceptionHelper.getRootCause(ex);
+ assertThat(inner.getMessage(), containsString("at least one type"));
+ assertThat(inner.getMessage(), containsString("cannot be represented as text"));
+ } finally {
+ gtx.tx().rollback();
+ }
+ }
}
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
index 7ce01eac70..895b09b75c 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
@@ -329,7 +329,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
public void should200OnPOSTWithGremlinQueryStringArgumentWithBindingsAndFunction() throws Exception {
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
- httppost.setEntity(new StringEntity("{\"gremlin\":\"addItUp(Integer.parseInt(x),Integer.parseInt(y))\",\"language\":\"gremlin-groovy\",\"bindings\":{\"x\":\"10\", \"y\":\"10\"}}", Consts.UTF_8));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"addItUp(Integer.parseInt(x),Integer.parseInt(y))\",\"language\":\"gremlin-groovy\",\"bindings\":\"[x:\\\"10\\\",y:\\\"10\\\"]\"}", Consts.UTF_8));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@@ -596,7 +596,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
httppost.addHeader("Content-Type", "application/json");
- httppost.setEntity(new StringEntity("{\"gremlin\":\"x+y\", \"bindings\":{\"x\":10, \"y\":10}, \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"x+y\", \"bindings\":\"[x:10,y:10]\", \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@@ -612,7 +612,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
httppost.addHeader("Content-Type", "application/json");
- httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":{\"x\":10}, \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":\"[x:10]\", \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@@ -628,7 +628,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
httppost.addHeader("Content-Type", "application/json");
- httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":{\"x\":10.5}, \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":\"[x:10.5]\", \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@@ -644,7 +644,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
httppost.addHeader("Content-Type", "application/json");
- httppost.setEntity(new StringEntity("{\"gremlin\":\"g.V(1).out(x).values('name')\", \"bindings\":{\"x\":\"knows\"}, \"g\":\"gmodern\"}", Consts.UTF_8));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"g.V(1).out(x).values('name')\", \"bindings\":\"[x:\\\"knows\\\"]\", \"g\":\"gmodern\"}", Consts.UTF_8));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@@ -660,7 +660,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
httppost.addHeader("Content-Type", "application/json");
- httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":{\"x\":true}, \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":\"[x:true]\", \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@@ -676,7 +676,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
httppost.addHeader("Content-Type", "application/json");
- httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":{\"x\":null}, \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":\"[x:null]\", \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@@ -692,7 +692,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
httppost.addHeader("Content-Type", "application/json");
- httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x).unfold()\", \"bindings\":{\"x\":[1,2,3]}, \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x).unfold()\", \"bindings\":\"[x:[1,2,3]]\", \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@@ -711,7 +711,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
httppost.addHeader("Content-Type", "application/json");
- httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":{\"x\":{\"y\":1}}, \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"g.inject(x)\", \"bindings\":\"[x:[\\\"y\\\":1]]\", \"language\":\"gremlin-groovy\"}", Consts.UTF_8));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@@ -1158,7 +1158,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
public void should400OnPOSTWithInvalidRequestArgsWhenInvalidBindingsSupplied() throws Exception {
final GraphSONMessageSerializerV4 serializer = new GraphSONMessageSerializerV4();
final ByteBuf serializedRequest = serializer.serializeRequestAsBinary(
- RequestMessage.build("g.V(id)").addBinding("id", "1").create(),
+ RequestMessage.build("g.V(id)").addBindings("[id:1]").addLanguage("gremlin-groovy").create(),
new UnpooledByteBufAllocator(false));
final CloseableHttpClient httpclient = HttpClients.createDefault();
@@ -1168,7 +1168,7 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
httppost.setEntity(new ByteArrayEntity(serializedRequest.array()));
try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
- assertEquals(200, response.getStatusLine().getStatusCode());
+ assertEquals(400, response.getStatusLine().getStatusCode());
final String json = EntityUtils.toString(response.getEntity());
final JsonNode node = mapper.readTree(json);
assertTrue(node.get("status").get("message").asText().contains("The message supplies one or more invalid parameters key"));
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java
index 9f56cb2f1c..9b4030e655 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java
@@ -532,9 +532,9 @@ public class GremlinServerIntegrateTest extends AbstractGremlinServerIntegration
try (SimpleClient client = TestClientFactory.createSimpleHttpClient()) {
final Map<String, Object> bindings = new HashMap<>();
- bindings.put("id", "123");
+ bindings.put("id", "123"); // "id" is invalid for gremlin-groovy, but not gremlin-lang
final RequestMessage request = RequestMessage.build("g.inject(1,2,3,4,5,6,7,8,9,0)")
- .addBindings(bindings).create();
+ .addBindings(bindings).addLanguage("gremlin-groovy").create();
final CountDownLatch latch = new CountDownLatch(1);
final AtomicBoolean pass = new AtomicBoolean(false);
client.submit(request, result -> {
@@ -717,31 +717,6 @@ public class GremlinServerIntegrateTest extends AbstractGremlinServerIntegration
}
}
- @Test
- public void shouldGarbageCollectPhantomButNotHard() throws Exception {
- final Cluster cluster = TestClientFactory.open();
- final Client client = cluster.connect();
-
- assertEquals(2, client.submit("addItUp(1,1)", groovyRequestOptions).all().join().get(0).getInt());
- assertEquals(0, client.submit("def subtract(x,y){x-y};subtract(1,1)", groovyRequestOptions).all().join().get(0).getInt());
- assertEquals(0, client.submit("subtract(1,1)", groovyRequestOptions).all().join().get(0).getInt());
-
- RequestOptions options = RequestOptions.build()
- .addParameter(GremlinGroovyScriptEngine.KEY_REFERENCE_TYPE, GremlinGroovyScriptEngine.REFERENCE_TYPE_PHANTOM)
- .language("gremlin-groovy")
- .create();
- assertEquals(4, client.submit("def multiply(x,y){x*y};multiply(2,2)", options).all().join().get(0).getInt());
-
- try {
- client.submit("multiply(2,2)", groovyRequestOptions).all().join().get(0).getInt();
- fail("Should throw an exception since reference is phantom.");
- } catch (RuntimeException ignored) {
-
- } finally {
- cluster.close();
- }
- }
-
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
@Test
public void shouldBlockRequestWhenTooBig() throws Exception {
@@ -1088,4 +1063,56 @@ public class GremlinServerIntegrateTest extends AbstractGremlinServerIntegration
assertEquals(new BigDecimal("1.0"), client.submit("g.inject(1.0)", gremlinGroovy).one().getObject());
assertEquals(new BigDecimal("-123.456"), client.submit("g.inject(-123.456)", gremlinGroovy).one().getObject());
}
+
+ @Test
+ public void shouldSubmitWithStringBindingsViaRequestMessage() throws Exception {
+ try (SimpleClient client = TestClientFactory.createSimpleHttpClient()) {
+ final RequestMessage request = RequestMessage.build("g.V(x).out(y).values('name')")
+ .addBindings("[\"x\":1,\"y\":\"knows\"]").addG("gmodern").create();
+ final List<ResponseMessage> responses = client.submit(request);
+ assertEquals(HttpResponseStatus.OK, responses.get(0).getStatus().getCode());
+ assertEquals("vadas", responses.get(0).getResult().getData().get(0));
+ }
+ }
+
+ @Test
+ public void shouldRejectTraversalInjectionInStringBindings() throws Exception {
+ try (SimpleClient client = TestClientFactory.createSimpleHttpClient()) {
+ final RequestMessage request = RequestMessage.build("g.V(x)")
+ .addBindings("[x:__.V().drop()]").addG("gmodern").create();
+ final List<ResponseMessage> responses = client.submit(request);
+ assertEquals(HttpResponseStatus.BAD_REQUEST, responses.get(0).getStatus().getCode());
+ }
+ }
+
+ @Test
+ public void shouldReturnUserFriendlyErrorMessageForMalformedParameterStrings() throws Exception {
+ final Cluster cluster = TestClientFactory.build().create();
+ final Client client = cluster.connect();
+
+ // each entry is [malformed input, expected substring in error message]
+ final String[][] cases = {
+ {"[\"x\":", "could not be converted into a Map. Query parsing failed at"},
+ {"not a map at all", "could not be converted into a Map. Query parsing failed at"},
+ {"[\"x\":\"unclosed]", "could not be converted into a Map. Query parsing failed at"},
+ {"[\"x\":,\"y\":1]", "could not be converted into a Map. Query parsing failed at"},
+ {"[\"x\":__.V().drop()]", "Traversals are not allowed"},
+ {"[\"~id\":1]", "must be a valid identifier"}
+ };
+
+ for (final String[] testCase : cases) {
+ final ResultSet result = client.submit(
+ "g.V(x)", RequestOptions.build().addParametersString(testCase[0]).create());
+ try {
+ result.one();
+ } catch (Exception e) {
+ assertTrue(ExceptionHelper.getRootCause(e) instanceof ResponseException);
+ final ResponseException re = (ResponseException) ExceptionHelper.getRootCause(e);
+ assertEquals(HttpResponseStatus.BAD_REQUEST, re.getResponseStatusCode());
+ assertTrue(re.getMessage().contains(testCase[1]));
+ }
+ }
+
+ cluster.close();
+ }
}
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java
index f6452abd09..b1f6423f7a 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java
@@ -30,6 +30,7 @@ import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection;
import org.apache.tinkerpop.gremlin.process.traversal.Merge;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
import org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.util.CollectionUtil;
@@ -238,9 +239,8 @@ public class HttpDriverIntegrateTest extends AbstractGremlinServerIntegrationTes
fail("Should have thrown exception over bad serialization");
} catch (Exception ex) {
final Throwable inner = ExceptionHelper.getRootCause(ex);
- assertThat(inner, instanceOf(ResponseException.class));
- assertEquals(HttpResponseStatus.BAD_REQUEST, ((ResponseException) inner).getResponseStatusCode());
- assertTrue(ex.getMessage().contains("An error occurred during serialization of this request"));
+ assertThat(inner, instanceOf(IllegalArgumentException.class));
+ assertTrue(ex.getMessage().contains("Parameter map contains at least one type [Color] that cannot be represented as text"));
}
// should not die completely just because we had a bad serialization error. that kind of stuff happens
@@ -555,6 +555,39 @@ public class HttpDriverIntegrateTest extends AbstractGremlinServerIntegrationTes
}
}
+ @Test
+ public void shouldRejectTraversalWithUnsupportedType() {
+ final Cluster cluster = TestClientFactory.build().create();
+ try {
+ final GraphTraversalSource g = traversal().with(DriverRemoteConnection.using(cluster));
+ g.inject(new Thread()).toList();
+ fail("Expected IllegalArgumentException for unsupported type");
+ } catch (IllegalArgumentException ex) {
+ assertThat(ex.getMessage(), containsString("at least one type"));
+ assertThat(ex.getMessage(), containsString("Thread"));
+ assertThat(ex.getMessage(), containsString("cannot be represented as text"));
+ } finally {
+ cluster.close();
+ }
+ }
+
+ @Test
+ public void shouldRejectTraversalWithUnsupportedTypeInGValue() {
+ final Cluster cluster = TestClientFactory.build().create();
+ try {
+ final GraphTraversalSource g = traversal().with(DriverRemoteConnection.using(cluster));
+ g.V(GValue.of("x", new Thread())).toList();
+ fail("Expected exception for unsupported type in GValue parameter");
+ } catch (Exception ex) {
+ final Throwable inner = ExceptionHelper.getRootCause(ex);
+ assertThat(inner, instanceOf(IllegalArgumentException.class));
+ assertThat(inner.getMessage(), containsString("at least one type"));
+ assertThat(inner.getMessage(), containsString("cannot be represented as text"));
+ } finally {
+ cluster.close();
+ }
+ }
+
@Test
public void shouldEvalInGremlinLang() {
final Cluster cluster = TestClientFactory.build().create();
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestMessageDecoderTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestMessageDecoderTest.java
index 7596698109..62045069bc 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestMessageDecoderTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestMessageDecoderTest.java
@@ -147,7 +147,7 @@ public class HttpRequestMessageDecoderTest {
final String gremlin = "g.V(x)";
final ByteBuf buffer = allocator.buffer();
buffer.writeCharSequence("{ \"gremlin\": \"" + gremlin +
- "\", \"bindings\":{\"x\":\"2\"}" +
+ "\", \"bindings\":\"[\\\"x\\\":\\\"2\\\"]\"" +
", \"language\": \"gremlin-groovy\"}",
CharsetUtil.UTF_8);
@@ -163,7 +163,7 @@ public class HttpRequestMessageDecoderTest {
final RequestMessage decodedRequestMessage = testChannel.readInbound();
assertEquals(gremlin, decodedRequestMessage.getGremlin());
assertEquals("gremlin-groovy", decodedRequestMessage.getField(Tokens.ARGS_LANGUAGE));
- assertEquals("2", ((Map)decodedRequestMessage.getField(Tokens.ARGS_BINDINGS)).get("x"));
+ assertEquals("[\"x\":\"2\"]", decodedRequestMessage.getField(Tokens.ARGS_BINDINGS));
}
@Test
@@ -314,7 +314,7 @@ public class HttpRequestMessageDecoderTest {
final UUID rid = UUID.randomUUID();
final ByteBuf buffer = allocator.buffer();
buffer.writeCharSequence("{\"gremlin\":\"g.V().limit(2)\",\"batchSize\":\"10\",\"language\":\"gremlin-lang\"," +
- "\"g\":\"gmodern\",\"bindings\":{\"x\":\"1\"},\"timeoutMs\":\"12\"," +
+ "\"g\":\"gmodern\",\"bindings\":\"[\\\"x\\\":1]\",\"timeoutMs\":\"12\"," +
"\"materializeProperties\":\"" + Tokens.MATERIALIZE_PROPERTIES_TOKENS + "\"}", CharsetUtil.UTF_8);
final HttpHeaders headers = new DefaultHttpHeaders();
@@ -331,8 +331,7 @@ public class HttpRequestMessageDecoderTest {
assertEquals(10, (int) decodedRequest.getField(Tokens.ARGS_BATCH_SIZE));
assertEquals("gremlin-lang", decodedRequest.getField(Tokens.ARGS_LANGUAGE));
assertEquals("gmodern", decodedRequest.getField(Tokens.ARGS_G));
- assertEquals("1", ((Map) decodedRequest.getField(Tokens.ARGS_BINDINGS)).get("x"));
- assertEquals(1, ((Map) decodedRequest.getField(Tokens.ARGS_BINDINGS)).size());
+ assertEquals("[\"x\":1]", decodedRequest.getField(Tokens.ARGS_BINDINGS));
assertEquals(12, (long) decodedRequest.getField(Tokens.TIMEOUT_MS));
assertEquals(Tokens.MATERIALIZE_PROPERTIES_TOKENS, decodedRequest.getField(Tokens.ARGS_MATERIALIZE_PROPERTIES));
}
diff --git a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/message/RequestMessage.java b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/message/RequestMessage.java
index f25aa3781c..bae392eed7 100644
--- a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/message/RequestMessage.java
+++ b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/message/RequestMessage.java
@@ -18,6 +18,7 @@
*/
package org.apache.tinkerpop.gremlin.util.message;
+import org.apache.tinkerpop.gremlin.process.traversal.GremlinLang;
import org.apache.tinkerpop.gremlin.util.Tokens;
import java.util.Collections;
@@ -76,18 +77,12 @@ public final class RequestMessage {
public static Builder from(final RequestMessage msg) {
final Builder builder = build(msg.gremlin);
builder.fields.putAll(msg.getFields());
- if (msg.getFields().containsKey(Tokens.ARGS_BINDINGS)) {
- builder.addBindings((Map<String, Object>) msg.getFields().get(Tokens.ARGS_BINDINGS));
- }
return builder;
}
public static Builder from(final RequestMessage msg, final String gremlin) {
final Builder builder = build(gremlin);
builder.fields.putAll(msg.getFields());
- if (msg.getFields().containsKey(Tokens.ARGS_BINDINGS)) {
- builder.addBindings((Map<String, Object>) msg.getFields().get(Tokens.ARGS_BINDINGS));
- }
return builder;
}
@@ -108,7 +103,6 @@ public final class RequestMessage {
*/
public static final class Builder {
private final String gremlin;
- private final Map<String, Object> bindings = new HashMap<>();
private final Map<String, Object> fields = new HashMap<>(); // Only allow certain items to be added to prevent breaking changes.
private Builder(final String gremlin) {
@@ -121,14 +115,24 @@ public final class RequestMessage {
return this;
}
- public Builder addBinding(final String key, final Object val) {
- bindings.put(key, val);
+ /**
+ * Sets the bindings as a gremlin-lang map literal string. Calling this method multiple
+ * times will replace the previous bindings (last-one-wins).
+ */
+ public Builder addBindings(final String bindingsString) {
+ Objects.requireNonNull(bindingsString, "bindings argument cannot be null.");
+ this.fields.put(Tokens.ARGS_BINDINGS, bindingsString);
return this;
}
+ /**
+ * Sets the bindings from a map by converting it to a gremlin-lang map literal string
+ * using {@link GremlinLang#convertParametersToString(Map)}. Calling this method multiple
+ * times will replace the previous bindings (last-one-wins).
+ */
public Builder addBindings(final Map<String, Object> otherBindings) {
Objects.requireNonNull(otherBindings, "bindings argument cannot be null.");
- this.bindings.putAll(otherBindings);
+ this.fields.put(Tokens.ARGS_BINDINGS, GremlinLang.convertParametersToString(otherBindings));
return this;
}
@@ -183,7 +187,6 @@ public final class RequestMessage {
* Create the request message given the settings provided to the {@link Builder}.
*/
public RequestMessage create() {
- this.fields.put(Tokens.ARGS_BINDINGS, bindings);
return new RequestMessage(gremlin, fields);
}
}
diff --git a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/AbstractGraphSONMessageSerializerV4.java b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/AbstractGraphSONMessageSerializerV4.java
index caa3def020..eae1ba50a2 100644
--- a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/AbstractGraphSONMessageSerializerV4.java
+++ b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/AbstractGraphSONMessageSerializerV4.java
@@ -323,7 +323,7 @@ public abstract class AbstractGraphSONMessageSerializerV4 extends AbstractMessag
builder.addG(data.get(SerTokens.TOKEN_G).toString());
}
if (data.containsKey(SerTokens.TOKEN_BINDINGS)) {
- builder.addBindings((Map<String, Object>) data.get(SerTokens.TOKEN_BINDINGS));
+ builder.addBindings(data.get(SerTokens.TOKEN_BINDINGS).toString());
}
if (data.containsKey(Tokens.TIMEOUT_MS)) {
// Can be int for untyped JSON and long for typed GraphSON.
diff --git a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/binary/RequestMessageSerializer.java b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/binary/RequestMessageSerializer.java
index 9397e5afde..fc75caad99 100644
--- a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/binary/RequestMessageSerializer.java
+++ b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/binary/RequestMessageSerializer.java
@@ -59,7 +59,7 @@ public class RequestMessageSerializer {
builder.addG(fields.get(SerTokens.TOKEN_G).toString());
}
if (fields.containsKey(SerTokens.TOKEN_BINDINGS)) {
- builder.addBindings((Map<String, Object>) fields.get(SerTokens.TOKEN_BINDINGS));
+ builder.addBindings(fields.get(SerTokens.TOKEN_BINDINGS).toString());
}
if (fields.containsKey(Tokens.TIMEOUT_MS)) {
builder.addTimeoutMillis((long) fields.get(Tokens.TIMEOUT_MS));
diff --git a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/message/RequestMessageTest.java b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/message/RequestMessageTest.java
index d5577a48f0..13376778a8 100644
--- a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/message/RequestMessageTest.java
+++ b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/message/RequestMessageTest.java
@@ -43,19 +43,42 @@ public class RequestMessageTest {
bindings.put("a", "b");
bindings.put("g", "gmodern");
final RequestMessage msg = RequestMessage.build("gremlin").addBindings(bindings).create();
- assertEquals(bindings, msg.getField(Tokens.ARGS_BINDINGS));
+ // bindings are now stored as a gremlin-lang map literal string
+ final String bindingsString = msg.getField(Tokens.ARGS_BINDINGS);
+ assertTrue(bindingsString.startsWith("["));
+ assertTrue(bindingsString.endsWith("]"));
+ assertTrue(bindingsString.contains("\"a\":\"b\""));
+ assertTrue(bindingsString.contains("\"g\":\"gmodern\""));
}
@Test
- public void shouldSetBindingsWithKeyValue() {
- final Map<String, Object> bindings = new HashMap<>();
- bindings.put("a", "b");
- bindings.put("g", "gmodern");
+ public void shouldSetBindingsWithString() {
final RequestMessage msg = RequestMessage.build("gremlin")
- .addBinding("a", "b")
- .addBinding("g", "gmodern")
+ .addBindings("[a:\"b\",g:\"gmodern\"]")
.create();
- assertEquals(bindings, msg.getField(Tokens.ARGS_BINDINGS));
+ assertEquals("[a:\"b\",g:\"gmodern\"]", msg.getField(Tokens.ARGS_BINDINGS));
+ }
+
+ @Test
+ public void shouldNotContainBindingsWhenNoneSet() {
+ final RequestMessage msg = RequestMessage.build("g.V()").create();
+ assertNull(msg.getField(Tokens.ARGS_BINDINGS));
+ }
+
+ @Test
+ public void shouldOverwriteBindingsOnMultipleCalls() {
+ final RequestMessage msg = RequestMessage.build("gremlin")
+ .addBindings("[x:1]")
+ .addBindings("[y:2]")
+ .create();
+ assertEquals("[y:2]", msg.getField(Tokens.ARGS_BINDINGS));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldRejectUnsupportedTypeInBindingsMap() {
+ final Map<String, Object> bindings = new HashMap<>();
+ bindings.put("x", new Thread());
+ RequestMessage.build("g.V()").addBindings(bindings);
}
@Test
@@ -111,7 +134,10 @@ public class RequestMessageTest {
final Map<String, Object> fields = msg.getFields();
assertEquals(g, fields.get(Tokens.ARGS_G));
assertEquals(lang, fields.get(Tokens.ARGS_LANGUAGE));
- assertEquals(bindings, fields.get(Tokens.ARGS_BINDINGS));
+ // bindings are now a gremlin-lang string, not the original map
+ final String bindingsString = (String) fields.get(Tokens.ARGS_BINDINGS);
+ assertTrue(bindingsString.contains("\"b\":\"c\""));
+ assertTrue(bindingsString.contains("\"g\":\"gmodern\""));
assertEquals(query, msg.getGremlin());
}
diff --git a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/MessageSerializerTest.java b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/MessageSerializerTest.java
index 6dfe98c0db..7f1e749222 100644
--- a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/MessageSerializerTest.java
+++ b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/MessageSerializerTest.java
@@ -76,7 +76,7 @@ public class MessageSerializerTest {
.addTimeoutMillis(500)
.addG("g1")
.addLanguage("some-lang")
- .addBinding("k", "v")
+ .addBindings("['k':'v']")
.create();
final ByteBuf buffer = serializer.serializeRequestAsBinary(request, allocator);
diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphTest.java
index a9ecce5600..1ef6bc4332 100644
--- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphTest.java
+++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphTest.java
@@ -28,6 +28,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.IdentityRemovalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReservedKeysVerificationStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.util.Metrics;
@@ -923,6 +924,31 @@ public class TinkerGraphTest {
assertEquals(3, g.V(100, "1000", uuid).count().next().intValue());
}
+ @Test
+ public void shouldExecuteTraversalWithUnsupportedTypeInEmbeddedMode() {
+ final TinkerGraph graph = TinkerGraph.open();
+ final GraphTraversalSource g = traversal().withEmbedded(graph);
+
+ // use a type that has no gremlin-lang literal representation
+ final Object customValue = new Object();
+ final List<Object> result = g.inject(customValue).toList();
+ assertEquals(1, result.size());
+ assertEquals(customValue, result.get(0));
+ }
+
+ @Test
+ public void shouldExecuteTraversalWithUnsupportedTypeInGValueInEmbeddedMode() {
+ final TinkerGraph graph = TinkerGraph.open();
+ final GraphTraversalSource g = traversal().withEmbedded(graph);
+ graph.addVertex(T.id, 1, T.label, "person");
+
+ // named GValue with an unsupported type as the value works in embedded mode
+ // because the raw object is passed through bindings to the script engine
+ final Object customValue = 1;
+ final long count = g.V(GValue.of("myId", customValue)).count().next();
+ assertEquals(1L, count);
+ }
+
/**
* Coerces a {@code Color} to a {@link TinkerGraph} during serialization. Demonstrates how custom serializers
* can be developed that can coerce one value to another during serialization.