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/20 16:21:16 UTC
(tinkerpop) branch stringify-params updated (4ea102c33f -> cdf59f1e62)
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 4ea102c33f update test to new structure
discard 9dc4903637 Convert request bindings to gremlin-lang string format
discard eabfcbaf93 Fix null map key parsing in GenericLiteralVisitor
discard a4e433e30e Add Char, Duration, Binary support to gremlin-lang. Squash with GLV implemenetations later
add c29ce6e154 CTR Fix generate-modern-readonly.groovy
add 7e9615ef3b TINKERPOP-3225 conjoin has incorrect null handling (#3372)
add fae1dcc362 Merge branch '3.7-dev' into 3.8-dev
add 9509d4f92d Fix GHA exclusions for GLVs (#3378)
add e3be426d50 Merge branch '3.8-dev'
add 72d6881362 GO support for escaped characters (#3381)
add aa383872be CTR: Pin more-itertools for 3.9 compatibility in GHA
add f4516e4370 CTR Bump commons-beanutils to 1.11.0 (CVE-2025-48734)
add 0f02480e94 Merge branch '3.7-dev' into 3.8-dev
add b08a6f0f71 Merge branch '3.8-dev'
new cdf59f1e62 Add Char, Duration, Binary support to gremlin-lang.
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 (4ea102c33f)
\
N -- N -- N refs/heads/stringify-params (cdf59f1e62)
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 1 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:
.github/workflows/build-test.yml | 2 +-
CHANGELOG.asciidoc | 4 +-
docs/src/upgrade/release-3.7.x.asciidoc | 29 ++
docs/src/upgrade/release-4.x.x.asciidoc | 9 -
.../language/grammar/GenericLiteralVisitor.java | 19 +-
.../language/grammar/GremlinQueryParser.java | 105 +----
.../language/grammar/ParameterMapVisitor.java | 102 -----
.../gremlin/process/traversal/GremlinLang.java | 91 +----
.../process/traversal/step/map/ConjoinStep.java | 2 +-
.../grammar/GeneralLiteralVisitorTest.java | 68 +++-
.../language/grammar/ParameterMapVisitorTest.java | 220 -----------
.../gremlin/process/traversal/GremlinLangTest.java | 424 +--------------------
.../traversal/step/map/ConjoinStepTest.java | 3 +-
.../Gremlin.Net/Process/Traversal/GremlinLang.cs | 17 +-
.../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 18 +
.../Process/Traversal/GremlinLangTests.cs | 106 ++++++
.../tinkerpop/gremlin/driver/RequestOptions.java | 49 +--
.../driver/remote/DriverRemoteConnection.java | 6 -
.../driver/remote/TransactionRemoteConnection.java | 6 -
gremlin-go/driver/cucumber/gremlin.go | 2 +
gremlin-go/driver/gremlinlang.go | 93 ++++-
gremlin-go/driver/gremlinlang_test.go | 84 ++++
.../gremlin-javascript/lib/process/gremlin-lang.ts | 4 +
.../gremlin-javascript/test/cucumber/gremlin.js | 2 +
.../test/unit/gremlin-lang-test.js | 32 ++
gremlin-language/src/main/antlr4/Gremlin.g4 | 3 +-
.../python/gremlin_python/process/traversal.py | 52 ++-
.../src/main/python/tests/feature/gremlin.py | 2 +
.../python/tests/unit/process/test_gremlin_lang.py | 64 +++-
.../scripts/generate-modern-readonly.groovy | 2 +-
.../apache/tinkerpop/gremlin/server/Context.java | 10 -
.../server/handler/HttpGremlinEndpointHandler.java | 47 +--
.../server/handler/HttpRequestMessageDecoder.java | 8 +-
.../gremlin/server/util/GremlinError.java | 7 -
.../GremlinDriverTransactionIntegrateTest.java | 33 --
.../server/GremlinServerHttpIntegrateTest.java | 22 +-
.../gremlin/server/GremlinServerIntegrateTest.java | 50 +--
.../gremlin/server/HttpDriverIntegrateTest.java | 39 +-
.../handler/HttpRequestMessageDecoderTest.java | 9 +-
.../gremlin/language/translator/translations.json | 34 ++
.../gremlin/test/features/map/Conjoin.feature | 24 ++
.../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 --
47 files changed, 792 insertions(+), 1212 deletions(-)
delete mode 100644 gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/ParameterMapVisitor.java
delete mode 100644 gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/ParameterMapVisitorTest.java
(tinkerpop) 01/01: 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 cdf59f1e62c9eedcd8041396798cf947d3bbeacb
Author: Ken Hu <10...@users.noreply.github.com>
AuthorDate: Wed Apr 8 19:35:41 2026 -0700
Add Char, Duration, Binary support to gremlin-lang.
---
.../grammar/DefaultGremlinBaseVisitor.java | 14 +-
.../language/grammar/GenericLiteralVisitor.java | 67 +++++++-
.../gremlin/process/traversal/GremlinLang.java | 24 +++
.../grammar/GeneralLiteralVisitorTest.java | 191 +++++++++++++++++++++
.../gremlin/process/traversal/GremlinLangTest.java | 38 ++++
.../Gremlin.Net/Process/Traversal/GremlinLang.cs | 17 +-
.../Process/Traversal/GremlinLangTests.cs | 106 ++++++++++++
gremlin-go/driver/gremlinlang.go | 63 +++++++
gremlin-go/driver/gremlinlang_test.go | 78 +++++++++
.../gremlin-javascript/lib/process/gremlin-lang.ts | 4 +
.../test/unit/gremlin-lang-test.js | 32 ++++
gremlin-language/src/main/antlr4/Gremlin.g4 | 44 ++++-
.../test/resources/incorrect-gremlin-values.txt | 4 +-
.../python/gremlin_python/process/traversal.py | 52 +++++-
.../python/tests/unit/process/test_gremlin_lang.py | 64 ++++++-
15 files changed, 787 insertions(+), 11 deletions(-)
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..d6f76e2738 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,12 @@ 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.time.format.DateTimeParseException;
import java.util.ArrayList;
+import java.util.Base64;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -518,6 +522,59 @@ 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 String iso8601 = (String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral());
+
+ // reject year/month components - check for Y or M before the T separator
+ final String upper = iso8601.toUpperCase();
+ final int tIndex = upper.indexOf('T');
+ final String datePart = tIndex >= 0 ? upper.substring(0, tIndex) : upper;
+ if (datePart.contains("Y") || datePart.contains("M")) {
+ throw new GremlinParserException(String.format(
+ "Duration literal does not support year or month components: '%s'. Use days, hours, minutes, and seconds only.", iso8601));
+ }
+
+ try {
+ return Duration.parse(iso8601);
+ } catch (DateTimeParseException e) {
+ throw new GremlinParserException(
+ String.format("Invalid Duration literal: '%s' is not a valid ISO-8601 duration", iso8601));
+ }
+ }
+
+ /**
+ * {@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 +590,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 +739,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/process/traversal/GremlinLang.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
index e174b5aeca..d895f6ec5b 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,27 @@ 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)
+ return String.format("Duration(\"%s\")", arg);
+
+ 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..7dd40a61fb 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,177 @@ 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(\"PT2H30M\")", Duration.ofHours(2).plusMinutes(30)},
+ {"Duration(\"PT0S\")", Duration.ZERO},
+ {"Duration(\"P1DT12H\")", Duration.ofDays(1).plusHours(12)},
+ {"Duration(\"PT-30S\")", Duration.ofSeconds(-30)},
+ {"Duration(\"PT0.5S\")", Duration.ofMillis(500)},
+ {"Duration(\"-PT30S\")", Duration.ofSeconds(-30)},
+ {"Duration(\"-P1DT12H\")", Duration.ofHours(-36)},
+ {"Duration(\"P2D\")", Duration.ofDays(2)},
+ {"Duration(\"PT1H30M15S\")", Duration.ofHours(1).plusMinutes(30).plusSeconds(15)},
+ {"Duration(\"P0D\")", Duration.ZERO},
+ {"Duration(\"PT0.000000001S\")", Duration.ofNanos(1)},
+ {"Duration(\"PT36H\")", Duration.ofHours(36)},
+ {"Duration(\"PT0.0000001S\")", Duration.ofNanos(100)},
+ {"Duration(\"PT0.000001S\")", Duration.ofNanos(1000)},
+ {"Duration(\"PT-0.5S\")", Duration.ofMillis(-500)},
+ {"Duration(\"-PT0.5S\")", Duration.ofMillis(-500)},
+ });
+ }
+
+ @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 shouldFailOnInvalidDuration() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"not-a-duration\")"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
+ try {
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
+ fail("Invalid Duration value should have thrown exception");
+ } catch (GremlinParserException gpe) {
+ assertThat(gpe.getMessage().contains("Invalid Duration literal:"), Matchers.is(true));
+ }
+ }
+
+ @Test(expected = GremlinParserException.class)
+ public void shouldFailOnEmptyDuration() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"\")"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
+ }
+
+ @Test
+ public void shouldFailOnYearComponent() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"P1Y\")"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
+ try {
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
+ fail("Year component should have thrown exception");
+ } catch (GremlinParserException gpe) {
+ assertThat(gpe.getMessage().contains("does not support year or month components"), Matchers.is(true));
+ }
+ }
+
+ @Test
+ public void shouldFailOnMonthComponent() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"P6M\")"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
+ try {
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
+ fail("Month component should have thrown exception");
+ } catch (GremlinParserException gpe) {
+ assertThat(gpe.getMessage().contains("does not support year or month components"), Matchers.is(true));
+ }
+ }
+
+ @Test
+ public void shouldFailOnYearMonthDayComponent() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"P1Y6M3DT4H\")"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
+ try {
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
+ fail("Year/month component should have thrown exception");
+ } catch (GremlinParserException gpe) {
+ assertThat(gpe.getMessage().contains("does not support year or month components"), 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/process/traversal/GremlinLangTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
index cf29daa0f1..9dd7cf3c47 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,8 @@ 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;
@@ -128,6 +130,27 @@ 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 literals
+ {g.inject('a'), "g.inject(\"a\"c)"},
+ {g.inject('"'), "g.inject(\"\\\"\"c)"},
+ {g.inject('\''), "g.inject(\"'\"c)"},
+ {g.inject('\\'), "g.inject(\"\\\\\"c)"},
+ {g.inject('\u00e9'), "g.inject(\"\\u00E9\"c)"},
+ // Duration literals
+ {g.inject(Duration.ofHours(2).plusMinutes(30)), "g.inject(Duration(\"PT2H30M\"))"},
+ {g.inject(Duration.ZERO), "g.inject(Duration(\"PT0S\"))"},
+ {g.inject(Duration.ofSeconds(-30)), "g.inject(Duration(\"PT-30S\"))"},
+ {g.inject(Duration.ofMillis(500)), "g.inject(Duration(\"PT0.5S\"))"},
+ {g.inject(Duration.ofDays(1).plusHours(12)), "g.inject(Duration(\"PT36H\"))"},
+ {g.inject(Duration.ofNanos(1)), "g.inject(Duration(\"PT0.000000001S\"))"},
+ {g.inject(Duration.ofMillis(-500)), "g.inject(Duration(\"PT-0.5S\"))"},
+ // Binary literals
+ {g.inject(ByteBuffer.wrap(new byte[]{1, 2, 3})), "g.inject(Binary(\"AQID\"))"},
+ {g.inject(ByteBuffer.wrap(new byte[]{})), "g.inject(Binary(\"\"))"},
+ {g.inject(ByteBuffer.wrap(new byte[]{0})), "g.inject(Binary(\"AA==\"))"},
+ {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 +192,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..4de5cdcc12 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
@@ -28,6 +28,7 @@ using System.Globalization;
using System.Numerics;
using System.Text;
using System.Threading;
+using System.Xml;
using Gremlin.Net.Process.Traversal.Strategy;
using Gremlin.Net.Process.Traversal.Strategy.Decoration;
using Gremlin.Net.Structure;
@@ -238,6 +239,15 @@ 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)
+ return $"Duration(\"{XmlConvert.ToString(timeSpan)}\")";
+
+ if (arg is byte[] byteArray)
+ return $"Binary(\"{Convert.ToBase64String(byteArray)}\")";
+
if (arg is EnumWrapper enumWrapper)
return $"{enumWrapper.EnumName}.{enumWrapper.EnumValue}";
@@ -663,7 +673,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.UnitTest/Process/Traversal/GremlinLangTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
index 3167499c45..884f666ff3 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,111 @@ 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());
}
+
+ [Fact]
+ public void g_Inject_Char_a()
+ {
+ Assert.Equal("g.inject(\"a\"c)",
+ _g.Inject((object)'a').GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Char_DoubleQuote()
+ {
+ Assert.Equal("g.inject(\"\\\"\"c)",
+ _g.Inject((object)'"').GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Char_SingleQuote()
+ {
+ Assert.Equal("g.inject(\"'\"c)",
+ _g.Inject((object)'\'').GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Char_Backslash()
+ {
+ Assert.Equal("g.inject(\"\\\\\"c)",
+ _g.Inject((object)'\\').GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Char_Unicode()
+ {
+ Assert.Equal("g.inject(\"\\u00E9\"c)",
+ _g.Inject((object)'\u00e9').GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Duration_2H30M()
+ {
+ Assert.Equal("g.inject(Duration(\"PT2H30M\"))",
+ _g.Inject((object)new TimeSpan(2, 30, 0)).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Duration_Zero()
+ {
+ Assert.Equal("g.inject(Duration(\"PT0S\"))",
+ _g.Inject((object)TimeSpan.Zero).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Duration_Negative30S()
+ {
+ Assert.Equal("g.inject(Duration(\"-PT30S\"))",
+ _g.Inject((object)TimeSpan.FromSeconds(-30)).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Duration_FractionalSeconds()
+ {
+ Assert.Equal("g.inject(Duration(\"PT0.5S\"))",
+ _g.Inject((object)TimeSpan.FromMilliseconds(500)).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Duration_DaysAndHours()
+ {
+ Assert.Equal("g.inject(Duration(\"P1DT12H\"))",
+ _g.Inject((object)(TimeSpan.FromDays(1) + TimeSpan.FromHours(12))).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Duration_TickPrecision()
+ {
+ // TimeSpan tick = 100 nanoseconds
+ Assert.Equal("g.inject(Duration(\"PT0.0000001S\"))",
+ _g.Inject((object)TimeSpan.FromTicks(1)).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Duration_NegativeSubSecond()
+ {
+ Assert.Equal("g.inject(Duration(\"-PT0.5S\"))",
+ _g.Inject((object)TimeSpan.FromMilliseconds(-500)).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Binary()
+ {
+ Assert.Equal("g.inject(Binary(\"AQID\"))",
+ _g.Inject((object)new byte[] { 1, 2, 3 }).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Binary_Empty()
+ {
+ Assert.Equal("g.inject(Binary(\"\"))",
+ _g.Inject((object)Array.Empty<byte>()).GremlinLang.GetGremlin());
+ }
+
+ [Fact]
+ public void g_Inject_Binary_Padding()
+ {
+ Assert.Equal("g.inject(Binary(\"AA==\"))",
+ _g.Inject((object)new byte[] { 0 }).GremlinLang.GetGremlin());
+ }
}
}
diff --git a/gremlin-go/driver/gremlinlang.go b/gremlin-go/driver/gremlinlang.go
index 2983fbc559..b063765f47 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,55 @@ func escapeString(s string) string {
return sb.String()
}
+// formatISO8601Duration formats a time.Duration as an ISO-8601 duration string
+// matching Java's Duration.toString() output (e.g., "PT2H30M", "PT-30S", "PT0.5S").
+func formatISO8601Duration(d time.Duration) string {
+ if d == 0 {
+ return "PT0S"
+ }
+
+ totalSeconds := int64(d / time.Second)
+ nanos := int64(d % time.Second)
+
+ hours := totalSeconds / 3600
+ minutes := (totalSeconds % 3600) / 60
+ seconds := totalSeconds % 60
+
+ var sb strings.Builder
+ sb.WriteString("PT")
+
+ if hours != 0 {
+ fmt.Fprintf(&sb, "%dH", hours)
+ }
+ if minutes != 0 {
+ fmt.Fprintf(&sb, "%dM", minutes)
+ }
+ if seconds != 0 || nanos != 0 {
+ if nanos == 0 {
+ fmt.Fprintf(&sb, "%dS", seconds)
+ } else {
+ absNanos := nanos
+ if absNanos < 0 {
+ absNanos = -absNanos
+ }
+ frac := fmt.Sprintf(".%09d", absNanos)
+ frac = strings.TrimRight(frac, "0")
+ // for negative sub-second durations where seconds truncated to 0
+ if seconds == 0 && d < 0 {
+ fmt.Fprintf(&sb, "-0%sS", frac)
+ } else {
+ fmt.Fprintf(&sb, "%d%sS", seconds, frac)
+ }
+ }
+ }
+
+ if hours == 0 && minutes == 0 && seconds == 0 && nanos == 0 {
+ sb.WriteString("0S")
+ }
+
+ return sb.String()
+}
+
func (gl *GremlinLang) argAsString(arg interface{}) (string, error) {
if arg == nil {
return "null", nil
@@ -269,6 +319,14 @@ 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:
+ return fmt.Sprintf("Duration(\"%s\")", formatISO8601Duration(v)), 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 +637,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..9f31595113 100644
--- a/gremlin-go/driver/gremlinlang_test.go
+++ b/gremlin-go/driver/gremlinlang_test.go
@@ -680,6 +680,84 @@ func Test_GremlinLang(t *testing.T) {
},
equals: "g.V().has(\"name\",\"\\\"marko\\n\\r\\t\\b\\f\\\"\")",
},
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(2*time.Hour + 30*time.Minute)
+ },
+ equals: `g.inject(Duration("PT2H30M"))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(time.Duration(0))
+ },
+ equals: `g.inject(Duration("PT0S"))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(-30 * time.Second)
+ },
+ equals: `g.inject(Duration("PT-30S"))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(500 * time.Millisecond)
+ },
+ equals: `g.inject(Duration("PT0.5S"))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(time.Nanosecond)
+ },
+ equals: `g.inject(Duration("PT0.000000001S"))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(-500 * time.Millisecond)
+ },
+ equals: `g.inject(Duration("PT-0.5S"))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(36 * time.Hour)
+ },
+ equals: `g.inject(Duration("PT36H"))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(ByteBuffer{Data: []byte{1, 2, 3}})
+ },
+ equals: `g.inject(Binary("AQID"))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(ByteBuffer{Data: []byte{}})
+ },
+ equals: `g.inject(Binary(""))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(ByteBuffer{Data: []byte{0}})
+ },
+ equals: `g.inject(Binary("AA=="))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject([]byte{1, 2, 3})
+ },
+ equals: `g.inject(Binary("AQID"))`,
+ },
+ {
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject([]byte{})
+ },
+ equals: `g.inject(Binary(""))`,
+ },
+ {
+ 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/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/test/unit/gremlin-lang-test.js b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
index 9d359a163c..59197e14c3 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,36 @@ describe('GremlinLang', function () {
"g.inject(['age':Cardinality.single(33)])"
);
});
+
+ it('should handle Buffer', function () {
+ assert.strictEqual(g.inject(Buffer.from([1, 2, 3])).getGremlinLang().getGremlin(), 'g.inject(Binary("AQID"))');
+ });
+
+ it('should handle empty Buffer', function () {
+ assert.strictEqual(g.inject(Buffer.from([])).getGremlinLang().getGremlin(), 'g.inject(Binary(""))');
+ });
+
+ it('should handle Buffer with base64 padding', function () {
+ assert.strictEqual(g.inject(Buffer.from([0])).getGremlinLang().getGremlin(), 'g.inject(Binary("AA=="))');
+ });
+
+ 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-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4
index 5272aeeb1c..68b1fc58c8 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 stringLiteral 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/src/main/python/gremlin_python/process/traversal.py b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
index edc6deefa6..41ec218a36 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):
@@ -804,6 +805,44 @@ GREMLIN LANGUAGE
'''
+def _format_iso8601_duration(td):
+ """Formats a timedelta as an ISO-8601 duration string matching Java's Duration.toString()."""
+ if td == timedelta(0):
+ return "PT0S"
+
+ total_seconds = int(td.total_seconds())
+ microseconds = td.microseconds if td.total_seconds() >= 0 else -td.microseconds
+
+ # detect negative sub-second durations where int() truncates to 0
+ is_negative = td.total_seconds() < 0
+
+ # use int() for truncation toward zero (not // which is floor division in Python)
+ hours = int(total_seconds / 3600)
+ minutes = int((total_seconds - hours * 3600) / 60)
+ seconds = total_seconds - hours * 3600 - minutes * 60
+
+ parts = ["PT"]
+ if hours != 0:
+ parts.append(f"{hours}H")
+ if minutes != 0:
+ parts.append(f"{minutes}M")
+ if seconds != 0 or microseconds != 0:
+ if microseconds == 0:
+ parts.append(f"{seconds}S")
+ else:
+ frac = abs(microseconds) / 1_000_000
+ frac_str = f"{frac:.6f}".lstrip("0").rstrip("0")
+ # for negative sub-second durations where seconds truncated to 0
+ if seconds == 0 and is_negative:
+ parts.append(f"-0{frac_str}S")
+ else:
+ parts.append(f"{seconds}{frac_str}S")
+ if len(parts) == 1:
+ parts.append("0S")
+
+ return "".join(parts)
+
+
class GremlinLang(object):
conn_p = ['and', 'or']
@@ -844,6 +883,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 +921,12 @@ class GremlinLang(object):
if isinstance(arg, uuid.UUID):
return f'UUID("{arg}")'
+ if isinstance(arg, timedelta):
+ return f'Duration("{_format_iso8601_duration(arg)}")'
+
+ 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/unit/process/test_gremlin_lang.py b/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
index b4c738a7ac..40f0ecd33b 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,75 @@ 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\"))"])
+ # 122
+ tests.append([g.inject(SingleChar('a')),
+ "g.inject('a'c)"])
+
+ # 123 - double quote char: repr gives '"', so output is '"'c
+ tests.append([g.inject(SingleChar('"')),
+ "g.inject('\"'c)"])
+
+ # 124 - single quote char: repr gives "'" (double-quoted), so output is "'"c
+ tests.append([g.inject(SingleChar("'")),
+ "g.inject(\"'\"c)"])
+
+ # 125 - backslash char: repr gives '\\', so output is '\\'c
+ tests.append([g.inject(SingleChar('\\')),
+ "g.inject('\\\\'c)"])
+
+ # 126 - unicode char: repr gives 'é', so output is 'é'c
+ tests.append([g.inject(SingleChar('\u00e9')),
+ "g.inject('\u00e9'c)"])
+
+ # 127
+ tests.append([g.inject(timedelta(hours=2, minutes=30)),
+ 'g.inject(Duration("PT2H30M"))'])
+
+ # 128
+ tests.append([g.inject(timedelta(0)),
+ 'g.inject(Duration("PT0S"))'])
+
+ # 129
+ tests.append([g.inject(timedelta(seconds=-30)),
+ 'g.inject(Duration("PT-30S"))'])
+
+ # 130
+ tests.append([g.inject(timedelta(milliseconds=500)),
+ 'g.inject(Duration("PT0.5S"))'])
+
+ # 131
+ tests.append([g.inject(timedelta(microseconds=1)),
+ 'g.inject(Duration("PT0.000001S"))'])
+
+ # 132
+ tests.append([g.inject(timedelta(milliseconds=-500)),
+ 'g.inject(Duration("PT-0.5S"))'])
+
+ # 133
+ tests.append([g.inject(timedelta(days=1, hours=12)),
+ 'g.inject(Duration("PT36H"))'])
+
+ # 134
+ tests.append([g.inject(bytes([1, 2, 3])),
+ 'g.inject(Binary("AQID"))'])
+
+ # 135
+ tests.append([g.inject(bytes()),
+ 'g.inject(Binary(""))'])
+
+ # 136
+ tests.append([g.inject(bytes([0])),
+ 'g.inject(Binary("AA=="))'])
+
for t in range(len(tests)):
gremlin_lang = tests[t][0].gremlin_lang.get_gremlin()
assert gremlin_lang == tests[t][1]