This is an automated email from the ASF dual-hosted git repository.
Cole-Greer pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/master by this push:
new 90d8547f5b TINKERPOP-3248: Added Character support in gremlin-go
(#3673)
90d8547f5b is described below
commit 90d8547f5bf31821b3548ef87a1a69f94b29d5e9
Author: Iryna-Kahamlyk <[email protected]>
AuthorDate: Mon Oct 5 14:24:43 2026 -0700
TINKERPOP-3248: Added Character support in gremlin-go (#3673)
Adds support for the Gremlin Character type to gremlin-go, in both
GremlinLang text generation and GraphBinary serialization.
Go's rune is only an alias for int32, so it can't be distinguished from an
int in a type switch. This introduces a new exported type, Char, that callers
use to explicitly mark a value as a Character (e.g.
g.Inject(gremlingo.Char('a'))).
---------
Co-authored-by: Guian Gumpac <[email protected]>
---
CHANGELOG.asciidoc | 1 +
docs/src/reference/gremlin-variants.asciidoc | 13 +++--
.../translator/DotNetTranslateVisitor.java | 10 +++-
.../language/translator/GoTranslateVisitor.java | 23 ++++++++-
.../translator/GroovyTranslateVisitor.java | 2 +-
.../language/translator/JavaTranslateVisitor.java | 2 +-
.../translator/PythonTranslateVisitor.java | 2 +-
.../language/translator/GremlinTranslatorTest.java | 56 +++++++++++++++++++++-
gremlin-go/driver/cucumber/cucumberSteps_test.go | 12 ++++-
gremlin-go/driver/cucumber/gremlin.go | 10 ++--
gremlin-go/driver/graphBinaryDeserializer.go | 36 ++++++++++++++
gremlin-go/driver/graphBinarySerializer.go | 9 ++++
gremlin-go/driver/graphBinarySerializer_test.go | 16 +++++++
gremlin-go/driver/gremlinlang.go | 2 +
gremlin-go/driver/gremlinlang_test.go | 42 ++++++++++++++++
gremlin-go/driver/serializer.go | 1 +
gremlin-go/driver/traversal.go | 3 ++
.../language/translator/DotNetTranslateVisitor.ts | 10 +++-
.../lib/language/translator/GoTranslateVisitor.ts | 26 +++++++++-
.../language/translator/GroovyTranslateVisitor.ts | 2 +-
.../language/translator/JavaTranslateVisitor.ts | 2 +-
.../language/translator/PythonTranslateVisitor.ts | 2 +-
.../unit/translator/gremlin-translator-test.js | 39 ++++++++++++---
23 files changed, 292 insertions(+), 29 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index bb0e323c94..68add2b68b 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -25,6 +25,7 @@
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
[[release-4-0-0]]
=== TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET)
+* Added a `Char` type to `gremlin-go` to represent the Gremlin `Character`
type in `GremlinLang`.
* Fixed `gremlin-go` to report a malformed or truncated GraphBinary response
as a deserialization error rather than a bare decoder message.
* Made `TinkerGraph` an interface and renamed the in-memory implementation to
`TinkerMemoryGraph`; `TinkerGraph.open()` and `gremlin.graph=...TinkerGraph`
behave as before. *(breaking)*
* Renamed `TinkerTransactionGraph` to `TinkerStorageGraph`. *(breaking)*
diff --git a/docs/src/reference/gremlin-variants.asciidoc
b/docs/src/reference/gremlin-variants.asciidoc
index 940d08be07..9da66c381d 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -682,6 +682,15 @@ Because Go does not support method overloading, the
batched form of `next(n)` av
distinct method `NextN(n)` on `Traversal`. `Next()` returns a single
`*Result`, while `NextN(n)` returns up to `n`
results as `[]*Result`.
+Go rune literals are interpreted as `Integer` values rather than `Character`
literals by default.
+To use the Gremlin `Character` type, wrap the rune in `gremlingo.Char()`. For
example:
+
+[source,go]
+----
+g.Inject('a') // interpreted as integer 97 (the Unicode
code point)
+g.Inject(gremlingo.Char('a')) // properly translated to a Gremlin
character literal
+----
+
[[gremlin-go-aliases]]
=== Aliases
To make the code more readable and close to the Gremlin query language), you
can use aliases. These aliases can be named with capital letters to be
consistent with non-aliased steps but will result in exported variables which
could be problematic if not being used in a top-level program (i.e. not a
redistributable package).
@@ -737,8 +746,6 @@ each is supplied as a property value:
[[gremlin-go-limitations]]
=== Limitations
-* The Gremlin `Character` type is not supported by Gremlin-Go.
-
* There is no default `set` type in Go. A set type code from the server is
deserialized into a
`*gremlingo.SimpleSet`, which implements the `gremlingo.Set` interface, rather
than into a slice. To read its
elements, the result should be type-asserted to `gremlingo.Set` and its
`ToSlice()` method called. To input a set
@@ -749,8 +756,6 @@ fulfill the `gremlingo.Set` interface if desired.
* Go does not support ordered maps natively as the built-in `map` type does
not guarantee iteration order. Traversal
results which contain maps may not preserve original ordering when
deserialized into Go's native map types.
-* GraphBinary `Char` values are not supported.
-
* GraphBinary permits map keys of any supported type, including composite
values such as lists. Go's native `map`
type only supports comparable keys, so GraphBinary maps with non-comparable
keys are deserialized lossily rather than
rejected. A list key is converted to its string form through `fmt.Sprint`, so
a key such as `[marko 29]` is stored as
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 a22cf48bc5..27e196fbda 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
@@ -1425,7 +1425,15 @@ public class DotNetTranslateVisitor extends
AbstractTranslateVisitor {
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("'");
+ final String csChar;
+ if (inner.equals("'")) {
+ csChar = "\\'";
+ } else if (inner.matches("\\\\[0-7]{1,3}")) {
+ csChar = String.format("\\u%04X",
Integer.parseInt(inner.substring(1), 8));
+ } else {
+ csChar = inner;
+ }
+ sb.append("'").append(csChar).append("'");
return null;
}
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 830c7c63d4..2383f361f1 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
@@ -36,10 +36,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class GoTranslateVisitor extends AbstractTranslateVisitor {
private final static String GO_PACKAGE_NAME = "gremlingo.";
+ private final static Pattern OCTAL_ESCAPE =
Pattern.compile("\\\\([0-7]{1,3})");
private final static List<String> STRATEGY_WITH_MAP_OPTS =
Collections.unmodifiableList(Arrays.asList(
"OptionsStrategy",
"ReferenceElementStrategy", "ComputerFinalizationStrategy",
"ProfileStrategy",
@@ -362,7 +365,25 @@ public class GoTranslateVisitor extends
AbstractTranslateVisitor {
@Override
public Void visitCharacterLiteral(final
GremlinParser.CharacterLiteralContext ctx) {
- throw new TranslatorException("Character literals are not supported in
Go");
+ final String text = ctx.getText();
+ final String withoutSuffix = text.substring(0, text.length() - 1);
+ final String inner = removeFirstAndLastCharacters(withoutSuffix);
+ final Matcher octal = OCTAL_ESCAPE.matcher(inner);
+ if (octal.matches()) {
+ sb.append(GO_PACKAGE_NAME).append("Char(0o")
+
.append(Integer.toOctalString(Integer.parseInt(octal.group(1), 8))).append(")");
+ return null;
+ }
+
+ final String goRune;
+ if (inner.equals("'"))
+ goRune = "\\'";
+ else if (inner.equals("\\\""))
+ goRune = "\"";
+ else
+ goRune = inner;
+
sb.append(GO_PACKAGE_NAME).append("Char('").append(goRune).append("')");
+ return null;
}
@Override
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 76b369d2f1..decbd4be7a 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
@@ -136,7 +136,7 @@ public class GroovyTranslateVisitor extends
TranslateVisitor {
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");
+ sb.append("'").append(inner.equals("'") ? "\\'" : inner).append("' as
char");
return null;
}
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 2376155f45..20542105e0 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
@@ -254,7 +254,7 @@ public class JavaTranslateVisitor extends
AbstractTranslateVisitor {
final String text = ctx.getText();
final String withoutSuffix = text.substring(0, text.length() - 1);
final String inner = removeFirstAndLastCharacters(withoutSuffix);
- sb.append("'").append(inner).append("'");
+ sb.append("'").append(inner.equals("'") ? "\\'" : inner).append("'");
return null;
}
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 ab71f99e7e..d8b9e8e76b 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
@@ -294,7 +294,7 @@ public class PythonTranslateVisitor extends
AbstractTranslateVisitor {
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("')");
+ sb.append("SingleChar('").append(inner.equals("'") ? "\\'" :
inner).append("')");
return null;
}
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 7ddc147fe3..0a26ce1c01 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
@@ -1437,11 +1437,65 @@ public class GremlinTranslatorTest {
null,
"g.inject(character0)",
"g.Inject<object>('a')",
- "Character literals are not supported in Go",
+ "g.Inject(gremlingo.Char('a'))",
"g.inject('a' as char)",
"g.inject('a')",
"Character literals are not supported in
JavaScript",
"g.inject(SingleChar('a'))"},
+ {"g.inject(\"\\\"\"c)",
+ null,
+ "g.inject(character0)",
+ "g.Inject<object>('\\\"')",
+ "g.Inject(gremlingo.Char('\"'))",
+ "g.inject('\\\"' as char)",
+ "g.inject('\\\"')",
+ "Character literals are not supported in
JavaScript",
+ "g.inject(SingleChar('\\\"'))"},
+ {"g.inject(\"\\\\\"c)",
+ null,
+ "g.inject(character0)",
+ "g.Inject<object>('\\\\')",
+ "g.Inject(gremlingo.Char('\\\\'))",
+ "g.inject('\\\\' as char)",
+ "g.inject('\\\\')",
+ "Character literals are not supported in
JavaScript",
+ "g.inject(SingleChar('\\\\'))"},
+ {"g.inject(\"'\"c)",
+ null,
+ "g.inject(character0)",
+ "g.Inject<object>('\\'')",
+ "g.Inject(gremlingo.Char('\\''))",
+ "g.inject('\\'' as char)",
+ "g.inject('\\'')",
+ "Character literals are not supported in
JavaScript",
+ "g.inject(SingleChar('\\''))"},
+ {"g.inject('\\''c)",
+ null,
+ "g.inject(character0)",
+ "g.Inject<object>('\\'')",
+ "g.Inject(gremlingo.Char('\\''))",
+ "g.inject('\\'' as char)",
+ "g.inject('\\'')",
+ "Character literals are not supported in
JavaScript",
+ "g.inject(SingleChar('\\''))"},
+ {"g.inject(\"\\7\"c)",
+ null,
+ "g.inject(character0)",
+ "g.Inject<object>('\\u0007')",
+ "g.Inject(gremlingo.Char(0o7))",
+ "g.inject('\\7' as char)",
+ "g.inject('\\7')",
+ "Character literals are not supported in
JavaScript",
+ "g.inject(SingleChar('\\7'))"},
+ {"g.inject(\"\\07\"c)",
+ null,
+ "g.inject(character0)",
+ "g.Inject<object>('\\u0007')",
+ "g.Inject(gremlingo.Char(0o7))",
+ "g.inject('\\07' as char)",
+ "g.inject('\\07')",
+ "Character literals are not supported in
JavaScript",
+ "g.inject(SingleChar('\\07'))"},
{"g.inject(Duration(9000,0))",
null,
"g.inject(duration0)",
diff --git a/gremlin-go/driver/cucumber/cucumberSteps_test.go
b/gremlin-go/driver/cucumber/cucumberSteps_test.go
index 6089a47523..6b15d238f4 100644
--- a/gremlin-go/driver/cucumber/cucumberSteps_test.go
+++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go
@@ -55,6 +55,7 @@ func init() {
regexp.MustCompile(`^uuid\[(.*)]$`): toUuid,
regexp.MustCompile(`^dur\[(.*)]$`): toDuration,
regexp.MustCompile(`^bin\[(.*)]$`): toBinary,
+ regexp.MustCompile(`^char\[(.*)]$`): toChar,
regexp.MustCompile(`^d\[(.*)]\.[bslfd]$`): toNumeric,
regexp.MustCompile(`^d\[(.*)]\.[m]$`): toBigDecimal,
regexp.MustCompile(`^d\[(.*)]\.[n]$`): toBigInt,
@@ -169,6 +170,15 @@ func toBinary(stringVal, graphName string) interface{} {
return &gremlingo.ByteBuffer{Data: data}
}
+// Parse a Character value.
+func toChar(stringVal, graphName string) interface{} {
+ r := []rune(stringVal)
+ if len(r) == 0 {
+ return nil
+ }
+ return gremlingo.Char(r[0])
+}
+
// Parse numeric.
func toNumeric(stringVal, graphName string) interface{} {
if strings.Contains(stringVal, ".") {
@@ -1282,7 +1292,7 @@ func TestCucumberFeatures(t *testing.T) {
TestSuiteInitializer: InitializeTestSuite,
ScenarioInitializer: InitializeScenario,
Options: &godog.Options{
- Tags: "~@GraphComputerOnly &&
~@AllowNullPropertyValues && ~@StepWrite && ~@DataChar && ~@MultiLabelDefault",
+ Tags: "~@GraphComputerOnly &&
~@AllowNullPropertyValues && ~@StepWrite && ~@MultiLabelDefault",
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 e95627b537..4e85c0d399 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -214,11 +214,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_injectXaX": {func(g *gremlingo.GraphTraversalSource, p
map[string]interface{}) *gremlingo.GraphTraversal {return
g.Inject(gremlingo.Char('a'))}},
+ "g_injectXescaped_quoteX": {func(g *gremlingo.GraphTraversalSource, p
map[string]interface{}) *gremlingo.GraphTraversal {return
g.Inject(gremlingo.Char('"'))}},
+ "g_injectXunicodeX": {func(g *gremlingo.GraphTraversalSource, p
map[string]interface{}) *gremlingo.GraphTraversal {return
g.Inject(gremlingo.Char('é'))}},
+ "g_valuesXinitialX_isXtypeOfXGType_CHARXX": {func(g
*gremlingo.GraphTraversalSource, p map[string]interface{})
*gremlingo.GraphTraversal {return g.AddV("data").Property("initial",
gremlingo.Char('a'))}, func(g *gremlingo.GraphTraversalSource, p
map[string]interface{}) *gremlingo.GraphTraversal {return
g.V().Values("initial").Is(gremlingo.P.TypeOf(gremlingo.GType.Char))}},
+ "g_injectXaX_isXeqXaXX": {func(g *gremlingo.GraphTraversalSource, p
map[string]interface{}) *gremlingo.GraphTraversal {return
g.Inject(gremlingo.Char('a')).Is(gremlingo.P.Eq(gremlingo.Char('a')))}},
"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,
[...]
diff --git a/gremlin-go/driver/graphBinaryDeserializer.go
b/gremlin-go/driver/graphBinaryDeserializer.go
index b086cdfed4..1b23facc5a 100644
--- a/gremlin-go/driver/graphBinaryDeserializer.go
+++ b/gremlin-go/driver/graphBinaryDeserializer.go
@@ -28,6 +28,7 @@ import (
"math/big"
"reflect"
"time"
+ "unicode/utf8"
"github.com/google/uuid"
)
@@ -217,6 +218,8 @@ func (d *GraphBinaryDeserializer) readValue(dt dataType,
flag byte) (interface{}
return b != 0, err
case byteType:
return d.readByte()
+ case charType:
+ return d.readChar()
case shortType:
if d.err != nil {
return nil, d.err
@@ -302,6 +305,39 @@ func (d *GraphBinaryDeserializer) readString() (string,
error) {
return string(buf), nil
}
+// readChar reads a Character value, encoded as one to four UTF-8 bytes with
no length prefix.
+// The number of bytes making up the encoded rune is determined by the leading
bits of the first byte.
+func (d *GraphBinaryDeserializer) readChar() (Char, error) {
+ firstByte, err := d.readByte()
+ if err != nil {
+ return 0, err
+ }
+ byteLength := 1
+ if firstByte&0x80 > 0 {
+ switch {
+ case firstByte&0xf0 == 0xf0:
+ byteLength = 4
+ case firstByte&0xe0 == 0xe0:
+ byteLength = 3
+ case firstByte&0xc0 == 0xc0:
+ byteLength = 2
+ }
+ }
+
+ buf := make([]byte, byteLength)
+ buf[0] = firstByte
+ if byteLength > 1 {
+ rest, err := d.readBytes(byteLength - 1)
+ if err != nil {
+ return 0, err
+ }
+ copy(buf[1:], rest)
+ }
+
+ r, _ := utf8.DecodeRune(buf)
+ return Char(r), nil
+}
+
func (d *GraphBinaryDeserializer) readList(bulked bool) (interface{}, error) {
length, err := d.readInt32()
if err != nil {
diff --git a/gremlin-go/driver/graphBinarySerializer.go
b/gremlin-go/driver/graphBinarySerializer.go
index 4cf377a787..958d17e541 100644
--- a/gremlin-go/driver/graphBinarySerializer.go
+++ b/gremlin-go/driver/graphBinarySerializer.go
@@ -63,6 +63,7 @@ const (
booleanType dataType = 0x27
treeType dataType = 0x2b
mergeType dataType = 0x2e
+ charType dataType = 0x80
durationType dataType = 0x81
compositePDTType dataType = 0xf0
primitivePDTType dataType = 0xf1
@@ -163,6 +164,12 @@ func mapWriter(value interface{}, w io.Writer,
typeSerializer *graphBinaryTypeSe
return nil
}
+// Format: one to four bytes representing a single UTF-8 encoded char, with no
length prefix.
+func charWriter(value interface{}, w io.Writer, _ *graphBinaryTypeSerializer)
error {
+ _, err := w.Write([]byte(string(rune(value.(Char)))))
+ return err
+}
+
func stringWriter(value interface{}, w io.Writer, _
*graphBinaryTypeSerializer) error {
str := value.(string)
if err := binary.Write(w, binary.BigEndian, int32(len(str))); err !=
nil {
@@ -645,6 +652,8 @@ func (serializer *graphBinaryTypeSerializer) getType(val
interface{}) (dataType,
return shortType, nil
case uint8:
return byteType, nil
+ case Char:
+ return charType, nil
case bool:
return booleanType, nil
case uuid.UUID:
diff --git a/gremlin-go/driver/graphBinarySerializer_test.go
b/gremlin-go/driver/graphBinarySerializer_test.go
index 766fca4973..e40bf0424b 100644
--- a/gremlin-go/driver/graphBinarySerializer_test.go
+++ b/gremlin-go/driver/graphBinarySerializer_test.go
@@ -70,6 +70,11 @@ func TestGraphBinaryV4(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, listType, res)
})
+ t.Run("getType should return charType for Char", func(t
*testing.T) {
+ res, err := serializer.getType(Char('a'))
+ assert.Nil(t, err)
+ assert.Equal(t, charType, res)
+ })
t.Run("getType should return error for missing type", func(t
*testing.T) {
_, err := serializer.getType(Error)
assert.NotNil(t, err)
@@ -292,6 +297,17 @@ func TestGraphBinaryV4(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, source, res)
})
+ t.Run("read-write char", func(t *testing.T) {
+ for _, source := range []Char{'a', '"', '\'', 'é', '€',
'😀'} {
+ var buffer bytes.Buffer
+ err := charWriter(source, &buffer, nil)
+ assert.Nil(t, err)
+ d :=
NewGraphBinaryDeserializer(bytes.NewReader(buffer.Bytes()))
+ res, err := d.readChar()
+ assert.Nil(t, err)
+ assert.Equal(t, source, res)
+ }
+ })
t.Run("read-write set", func(t *testing.T) {
var buffer bytes.Buffer
source := NewSimpleSet(int32(111), "str")
diff --git a/gremlin-go/driver/gremlinlang.go b/gremlin-go/driver/gremlinlang.go
index e5565c6837..351d541574 100644
--- a/gremlin-go/driver/gremlinlang.go
+++ b/gremlin-go/driver/gremlinlang.go
@@ -310,6 +310,8 @@ func (gl *GremlinLang) argAsString(arg interface{})
(string, error) {
return fmt.Sprintf("Binary(\"%s\")",
base64.StdEncoding.EncodeToString(v.Data)), nil
case []byte:
return fmt.Sprintf("Binary(\"%s\")",
base64.StdEncoding.EncodeToString(v)), nil
+ case Char:
+ return fmt.Sprintf("\"%s\"c", escapeString(string(rune(v)))),
nil
default:
// Registry-based dehydration: a registered adapter
intentionally takes precedence
// over any reflection/struct-based fallback, allowing explicit
adapters to override
diff --git a/gremlin-go/driver/gremlinlang_test.go
b/gremlin-go/driver/gremlinlang_test.go
index dbd7b19b77..6bc97dccd3 100644
--- a/gremlin-go/driver/gremlinlang_test.go
+++ b/gremlin-go/driver/gremlinlang_test.go
@@ -717,6 +717,48 @@ func Test_GremlinLang(t *testing.T) {
},
equals: `g.inject(Binary("AA=="))`,
},
+ {
+ name: "g_Inject_Char",
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(Char('a'))
+ },
+ equals: `g.inject("a"c)`,
+ },
+ {
+ name: "g_Inject_Char_EscapedQuote",
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(Char('"'))
+ },
+ equals: `g.inject("\""c)`,
+ },
+ {
+ name: "g_Inject_Char_Backslash",
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(Char('\\'))
+ },
+ equals: `g.inject("\\"c)`,
+ },
+ {
+ name: "g_Inject_Char_SingleQuote",
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(Char('\''))
+ },
+ equals: `g.inject("'"c)`,
+ },
+ {
+ name: "g_Inject_Char_Newline",
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject(Char('\n'))
+ },
+ equals: `g.inject("\n"c)`,
+ },
+ {
+ name: "g_Inject_Rune_AsInt",
+ assert: func(g *GraphTraversalSource) *GraphTraversal {
+ return g.Inject('a')
+ },
+ equals: `g.inject(97)`,
+ },
{
name: "GTS Match with query string spawn step",
assert: func(g *GraphTraversalSource) *GraphTraversal {
diff --git a/gremlin-go/driver/serializer.go b/gremlin-go/driver/serializer.go
index 73a9b6d705..a74fe06b34 100644
--- a/gremlin-go/driver/serializer.go
+++ b/gremlin-go/driver/serializer.go
@@ -179,6 +179,7 @@ func initSerializers() {
longType: longWriter,
intType: intWriter,
shortType: shortWriter,
+ charType: charWriter,
byteType: func(value interface{}, w io.Writer, typeSerializer
*graphBinaryTypeSerializer) error {
return binary.Write(w, binary.BigEndian, value.(uint8))
},
diff --git a/gremlin-go/driver/traversal.go b/gremlin-go/driver/traversal.go
index a78f6ee9c2..1a6baf1759 100644
--- a/gremlin-go/driver/traversal.go
+++ b/gremlin-go/driver/traversal.go
@@ -905,3 +905,6 @@ func ParseBigInt(strValue string) *big.Int {
type ByteBuffer struct {
Data []byte
}
+
+// Char represents the GraphBinary/Gremlin Character type.
+type Char rune
diff --git
a/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
b/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
index 9b1da650d7..fb42fb1e40 100644
---
a/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
+++
b/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
@@ -229,8 +229,16 @@ export default class DotNetTranslateVisitor extends
TranslateVisitor {
const text: string = ctx.getText();
const withoutSuffix = text.substring(0, text.length - 1);
const inner =
TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix);
+ let csChar: string;
+ if (inner === "'") {
+ csChar = "\\'";
+ } else if (/^\\[0-7]{1,3}$/.test(inner)) {
+ csChar = '\\u' + parseInt(inner.substring(1),
8).toString(16).toUpperCase().padStart(4, '0');
+ } else {
+ csChar = inner;
+ }
this.sb.push("'");
- this.sb.push(inner);
+ this.sb.push(csChar);
this.sb.push("'");
}
diff --git
a/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts
b/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts
index f69caf2948..cfaeaef182 100644
---
a/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts
+++
b/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts
@@ -257,8 +257,30 @@ export default class GoTranslateVisitor extends
TranslateVisitor {
this.sb.push(')');
}
- visitCharacterLiteral(_ctx: any): void {
- throw new TranslatorException('Character literals are not supported in
Go');
+ visitCharacterLiteral(ctx: any): void {
+ const text: string = ctx.getText();
+ const withoutSuffix = text.substring(0, text.length - 1);
+ let inner = withoutSuffix.substring(1, withoutSuffix.length - 1);
+
+ const octal = /^\\([0-7]{1,3})$/.exec(inner);
+ if (octal) {
+ this.sb.push(GO_PACKAGE_NAME);
+ this.sb.push('Char(0o');
+ this.sb.push(parseInt(octal[1], 8).toString(8));
+ this.sb.push(')');
+ return;
+ }
+
+ inner = inner.replace(/\\([\s\S])|'/g, (m: string, esc?: string) => {
+ if (esc === undefined) return "\\'"; // bare ' -> \'
+ if (esc === '"') return '"'; // \" -> "
+ if (esc === "'") return "\\'"; // \' -> \'
+ return m; // \n, \\, \uXXXX, ...
unchanged
+ });
+ this.sb.push(GO_PACKAGE_NAME);
+ this.sb.push("Char('");
+ this.sb.push(inner);
+ this.sb.push("')");
}
visitDurationLiteral(ctx: any): void {
diff --git
a/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts
b/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts
index f061975e2b..9796909e94 100644
---
a/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts
+++
b/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts
@@ -119,7 +119,7 @@ export default class GroovyTranslateVisitor extends
TranslateVisitor {
const withoutSuffix = text.substring(0, text.length - 1);
const inner =
TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix);
this.sb.push("'");
- this.sb.push(inner);
+ this.sb.push(inner === "'" ? "\\'" : inner);
this.sb.push("' as char");
}
diff --git
a/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts
b/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts
index fbbe38d1b1..c8d526f0bd 100644
---
a/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts
+++
b/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts
@@ -205,7 +205,7 @@ export default class JavaTranslateVisitor extends
TranslateVisitor {
const withoutSuffix = text.substring(0, text.length - 1);
const inner =
TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix);
this.sb.push("'");
- this.sb.push(inner);
+ this.sb.push(inner === "'" ? "\\'" : inner);
this.sb.push("'");
}
diff --git
a/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts
b/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts
index 6dad9a8b02..4bc04afc47 100644
---
a/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts
+++
b/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts
@@ -225,7 +225,7 @@ export default class PythonTranslateVisitor extends
TranslateVisitor {
const withoutSuffix = text.substring(0, text.length - 1);
const inner =
TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix);
this.sb.push("SingleChar('");
- this.sb.push(inner);
+ this.sb.push(inner === "'" ? "\\'" : inner);
this.sb.push("')");
}
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 868e1bf07b..f7f6ae5ff3 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
@@ -198,6 +198,12 @@ describe('PythonTranslateVisitor', function () {
['g.inject({1,2,3})', 'g.inject({1, 2, 3})'],
// Character literal
['g.inject("a"c)', "g.inject(SingleChar('a'))"],
+ ['g.inject("\\""c)', "g.inject(SingleChar('\\\"'))"],
+ ['g.inject("\\\\"c)', "g.inject(SingleChar('\\\\'))"],
+ ["g.inject(\"'\"c)", "g.inject(SingleChar('\\''))"],
+ ["g.inject('\\''c)", "g.inject(SingleChar('\\''))"],
+ ['g.inject("\\7"c)', "g.inject(SingleChar('\\7'))"],
+ ['g.inject("\\07"c)', "g.inject(SingleChar('\\07'))"],
// Duration literal
['g.inject(Duration(9000,0))', 'g.inject(timedelta(seconds=9000))'],
// Binary literal
@@ -285,6 +291,14 @@ describe('GoTranslateVisitor', function () {
['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}})'],
+ // Character literals
+ ['g.inject("a"c)', "g.Inject(gremlingo.Char('a'))"],
+ ['g.inject("\\""c)', "g.Inject(gremlingo.Char('\"'))"],
+ ['g.inject("\\\\"c)', "g.Inject(gremlingo.Char('\\\\'))"],
+ ["g.inject(\"'\"c)", "g.Inject(gremlingo.Char('\\''))"],
+ ["g.inject('\\''c)", "g.Inject(gremlingo.Char('\\''))"],
+ ['g.inject("\\7"c)', 'g.Inject(gremlingo.Char(0o7))'],
+ ['g.inject("\\07"c)', 'g.Inject(gremlingo.Char(0o7))'],
// null → nil
['g.V().has("name", null)', 'g.V().Has("name", nil)'],
// terminal steps — PascalCase
@@ -329,13 +343,6 @@ describe('GoTranslateVisitor', function () {
TranslatorException,
);
});
-
- it('throws TranslatorException for character literal', function () {
- assert.throws(
- () => GremlinTranslator.translate('g.inject("a"c)', 'g', 'GO'),
- TranslatorException,
- );
- });
});
});
@@ -372,6 +379,12 @@ describe('DotNetTranslateVisitor', function () {
['g.inject({1,2,3})', 'g.Inject<object>(new HashSet<object> { 1, 2, 3
})'],
// Character literal
['g.inject("a"c)', "g.Inject<object>('a')"],
+ ['g.inject("\\""c)', "g.Inject<object>('\\\"')"],
+ ['g.inject("\\\\"c)', "g.Inject<object>('\\\\')"],
+ ["g.inject(\"'\"c)", "g.Inject<object>('\\'')"],
+ ["g.inject('\\''c)", "g.Inject<object>('\\'')"],
+ ['g.inject("\\7"c)', "g.Inject<object>('\\u0007')"],
+ ['g.inject("\\07"c)', "g.Inject<object>('\\u0007')"],
// Duration literal
['g.inject(Duration(9000,0))',
'g.Inject<object>(TimeSpan.FromTicks(90000000000L))'],
// Binary literal
@@ -463,6 +476,12 @@ describe('JavaTranslateVisitor', function () {
['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')"],
+ ['g.inject("\\""c)', "g.inject('\\\"')"],
+ ['g.inject("\\\\"c)', "g.inject('\\\\')"],
+ ["g.inject(\"'\"c)", "g.inject('\\'')"],
+ ["g.inject('\\''c)", "g.inject('\\'')"],
+ ['g.inject("\\7"c)', "g.inject('\\7')"],
+ ['g.inject("\\07"c)', "g.inject('\\07')"],
// Duration literal
['g.inject(Duration(9000,0))', 'g.inject(Duration.ofSeconds(9000, 0))'],
// Binary literal
@@ -547,6 +566,12 @@ describe('GroovyTranslateVisitor', function () {
['g.inject({1,2,3})', 'g.inject([1, 2, 3] as Set)'],
// Character literal
['g.inject("a"c)', "g.inject('a' as char)"],
+ ['g.inject("\\""c)', "g.inject('\\\"' as char)"],
+ ['g.inject("\\\\"c)', "g.inject('\\\\' as char)"],
+ ["g.inject(\"'\"c)", "g.inject('\\'' as char)"],
+ ["g.inject('\\''c)", "g.inject('\\'' as char)"],
+ ['g.inject("\\7"c)', "g.inject('\\7' as char)"],
+ ['g.inject("\\07"c)', "g.inject('\\07' as char)"],
// Duration literal
['g.inject(Duration(9000,0))', 'g.inject(Duration.ofSeconds(9000, 0))'],
// Binary literal