This is an automated email from the ASF dual-hosted git repository.
xiazcy 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 cf0118a45c Add typed numeric wrappers and precise number mode to
gremlin-javascript (#3427)
cf0118a45c is described below
commit cf0118a45c44947cc6f2613d7247ec3246aebb74
Author: kirill-stepanishin <[email protected]>
AuthorDate: Tue May 26 13:04:55 2026 -0700
Add typed numeric wrappers and precise number mode to gremlin-javascript
(#3427)
Assisted-by: Claude Code:claude-opus-4-6
---
CHANGELOG.asciidoc | 1 +
docs/src/reference/gremlin-variants.asciidoc | 51 +-
docs/src/upgrade/release-4.x.x.asciidoc | 47 ++
.../gremlin-javascript/lib/driver/connection.ts | 5 +-
gremlin-js/gremlin-javascript/lib/index.ts | 12 +
.../gremlin-javascript/lib/process/gremlin-lang.ts | 26 +-
.../lib/structure/io/binary/GraphBinary.js | 115 +++--
.../structure/io/binary/internals/AnySerializer.js | 11 +-
.../internals/NumberSerializationStrategy.js | 34 +-
gremlin-js/gremlin-javascript/lib/utils.ts | 156 +++++-
gremlin-js/gremlin-javascript/test/helper.js | 2 +
.../test/integration/client-tests.js | 33 +-
.../gremlin-javascript/test/unit/exports-test.js | 13 +
.../test/unit/graphbinary/precise-mode-test.js | 521 +++++++++++++++++++++
.../test/unit/graphbinary/typed-number-test.js | 212 +++++++++
.../test/unit/gremlin-lang-test.js | 134 +++++-
16 files changed, 1315 insertions(+), 58 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 284bbcca26..e312807286 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 typed numeric wrappers and `preciseNumbers` connection option to
`gremlin-javascript` for explicit control over numeric type serialization and
deserialization.
* Added `NextN(n)` to `Traversal` in `gremlin-go` for batched result
iteration, providing API parity with `next(n)` in the Java, Python, and .NET
GLVs, and updated the Go translators in `gremlin-core` and `gremlin-javascript`
to emit `NextN(n)` for the batched form.
* 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()`.
diff --git a/docs/src/reference/gremlin-variants.asciidoc
b/docs/src/reference/gremlin-variants.asciidoc
index 2ad34f1d9e..521a7017bf 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -1677,6 +1677,7 @@ can be passed in the constructor of a new `Client` or
`DriverRemoteConnection` :
|options.traversalSource |String |The traversal source. |'g'
|options.headers |Object |Additional HTTP header key/values included with each
request. |undefined
|options.interceptors |RequestInterceptor/RequestInterceptor[] |One or more
functions that can modify the HTTP request before it is sent. |undefined
+|options.preciseNumbers |Boolean |When `true`, wraps deserialized numbers in
typed wrappers that preserve the server's original type. |undefined
|options.reader |GraphBinaryReader |The reader to use for deserializing
responses. |GraphBinaryReader
|options.writer |GraphBinaryWriter |The writer to use for serializing
requests. |GraphBinaryWriter
|options.enableUserAgentOnConnect |Boolean |Determines if a user agent header
will be sent with requests. |true
@@ -1975,15 +1976,61 @@ g.V().hasLabel('person').groupCount().by('age')
Either of the above two options accomplishes the desired goal as both prevent
`groupCount()` from having to process
the possibility of `null`.
+[[gremlin-javascript-numeric-types]]
+=== Numeric Types
+
+JavaScript has a single `Number` type (IEEE 754 double) which cannot
distinguish between Gremlin numeric types. The driver
+provides typed wrapper classes and factory functions that give explicit
control over serialization and deserialization.
+
+Wrapping a value selects the GremlinLang type suffix and GraphBinary type code
sent to the server. Without wrappers the
+driver infers types automatically, so existing code is unaffected.
+
+[source,javascript]
+----
+const { toLong, toInt, toFloat, toDouble, toShort, toByte } =
gremlin.structure;
+
+g.V().has('age', toInt(29)).next();
+g.V().has('score', toFloat(3.14)).next();
+g.V().has('id', toLong('9007199254740993')).next();
+----
+
+`toLong()` accepts `number`, `string`, or `bigint`. String and bigint inputs
support the full signed 64-bit range;
+number inputs must be within the safe integer range (`RangeError` is thrown
otherwise).
+
+By default, the driver deserializes all numeric values as plain `Number` (or
`BigInt` for large longs). To preserve the
+server's original type, set `preciseNumbers` to `true` on the connection:
+
+[source,javascript]
+----
+const g = traversal().with_(new
DriverRemoteConnection('http://localhost:8182/gremlin', {
+ preciseNumbers: true
+}));
+
+const v = await g.V(1).elementMap().next();
+const age = v.value.get('age'); // Int { value: 29, type: 'int' }
+age + 1; // 30 — wrappers support arithmetic via
valueOf()
+----
+
+The `unwrap()` helper extracts the raw value from any wrapper, passing
non-wrapper values through unchanged:
+
+[source,javascript]
+----
+const { unwrap } = gremlin.structure;
+unwrap(toInt(29)); // 29
+unwrap('hello'); // 'hello'
+----
+
[[gremlin-javascript-limitations]]
=== Limitations
* JavaScript's `Number` type is an IEEE 754 double-precision float. `Float`,
`Byte`, and `Short` values from the server
-are deserialized as `Number` and lose their original type information.
+are deserialized as `Number` and lose their original type information. Use
`preciseNumbers: true` to preserve the
+original types — see <<gremlin-javascript-numeric-types>>.
* `Long` values outside the safe integer range (|n| > 2^53 - 1) are
deserialized as `BigInt` to preserve precision.
Values within the safe range are deserialized as `Number`. The same server
type may produce different JavaScript types.
* `Number.isInteger(1.0)` is `true` in JavaScript, so the driver cannot
distinguish integer values from whole-number
-doubles. `BigDecimal` is not implemented.
+doubles. `BigDecimal` is not implemented. Typed wrappers (e.g. `toInt()`,
`toDouble()`) can be used to control the
+exact type sent to the server — see <<gremlin-javascript-numeric-types>>.
* The driver applies GremlinLang type suffixes automatically based on value
characteristics: integers within the 32-bit
signed range are unsuffixed (Int), integers beyond that up to
`Number.MAX_SAFE_INTEGER` use the `L` suffix (Long),
non-integer numbers and integers beyond the safe range use the `D` suffix
(Double), and `BigInt` values use the `N`
diff --git a/docs/src/upgrade/release-4.x.x.asciidoc
b/docs/src/upgrade/release-4.x.x.asciidoc
index 960c090f5d..aa14264ca0 100644
--- a/docs/src/upgrade/release-4.x.x.asciidoc
+++ b/docs/src/upgrade/release-4.x.x.asciidoc
@@ -442,6 +442,53 @@ beyond this limit will be rejected with an error.
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3247[TINKERPOP-3247]
+==== JavaScript Typed Numeric Wrappers
+
+JavaScript has a single `Number` type (IEEE 754 double) which loses the
distinction between Gremlin numeric types like
+`int`, `float`, `long`, and `double`. The `gremlin-javascript` driver now
provides typed wrapper classes and factory
+functions that give explicit control over how numbers are serialized and
deserialized.
+
+On the *serialization* side, wrapping a value controls the GremlinLang type
suffix and GraphBinary type code sent to
+the server:
+
+[source,javascript]
+----
+const { toInt, toLong, toFloat, toDouble } = gremlin.structure;
+
+g.V().has('age', toInt(29)).next(); // age sent as Int
+g.V().has('score', toFloat(3.14)).next(); // score sent as Float
+g.V().has('id', toLong('9007199254740993')).next(); // id sent as Long
+----
+
+`toLong()` accepts `number`, `string`, or `bigint`. Number inputs must be
within the safe integer range (throws
+`RangeError` otherwise); string and bigint inputs support the full signed
64-bit range.
+
+Without wrappers, the driver continues to infer types automatically from the
JavaScript value, so existing code is
+unaffected.
+
+On the *deserialization* side, a new `preciseNumbers: true` connection option
wraps incoming numeric values in the
+same typed wrappers, preserving the server's original type information:
+
+[source,javascript]
+----
+const g = traversal().with_(new
DriverRemoteConnection('http://localhost:8182/gremlin', {
+ preciseNumbers: true
+}));
+
+const v = await g.V(1).elementMap().next();
+const age = v.value.get('age'); // Int { value: 29, type: 'int' }
+age + 1; // 30 — wrappers support arithmetic via
valueOf()
+----
+
+The `unwrap()` helper extracts the raw value from any wrapper, passing
non-wrapper values through unchanged:
+
+[source,javascript]
+----
+const { unwrap } = gremlin.structure;
+unwrap(toInt(29)); // 29
+unwrap('hello'); // 'hello'
+----
+
=== Upgrading for Providers
==== Graph System Providers
diff --git a/gremlin-js/gremlin-javascript/lib/driver/connection.ts
b/gremlin-js/gremlin-javascript/lib/driver/connection.ts
index ea1b231d59..12f7da0467 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/connection.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/connection.ts
@@ -24,7 +24,7 @@
import { Buffer } from 'buffer';
import { EventEmitter } from 'eventemitter3';
import type { Agent } from 'node:http';
-import ioc from '../structure/io/binary/GraphBinary.js';
+import ioc, { createPreciseReader } from
'../structure/io/binary/GraphBinary.js';
import StreamReader from '../structure/io/binary/internals/StreamReader.js';
import * as utils from '../utils.js';
import ResultSet from './result-set.js';
@@ -53,6 +53,7 @@ export type ConnectionOptions = {
ca?: string[];
cert?: string | string[] | Buffer;
pfx?: string | Buffer;
+ preciseNumbers?: boolean;
reader?: any;
rejectUnauthorized?: boolean;
traversalSource?: string;
@@ -87,7 +88,7 @@ export default class Connection extends EventEmitter {
) {
super();
- this._reader = options.reader || graphBinaryReader;
+ this._reader = options.reader || (options.preciseNumbers === true ?
createPreciseReader() : graphBinaryReader);
this._writer = 'writer' in options ? options.writer : graphBinaryWriter;
this.traversalSource = options.traversalSource || 'g';
this._enableUserAgentOnConnect = options.enableUserAgentOnConnect !==
false;
diff --git a/gremlin-js/gremlin-javascript/lib/index.ts
b/gremlin-js/gremlin-javascript/lib/index.ts
index fd40186eb6..f79c31e948 100644
--- a/gremlin-js/gremlin-javascript/lib/index.ts
+++ b/gremlin-js/gremlin-javascript/lib/index.ts
@@ -87,6 +87,18 @@ export const structure = {
Vertex: graph.Vertex,
VertexProperty: graph.VertexProperty,
toLong: utils.toLong,
+ toInt: utils.toInt,
+ toFloat: utils.toFloat,
+ toDouble: utils.toDouble,
+ toShort: utils.toShort,
+ toByte: utils.toByte,
+ Int: utils.Int,
+ Float: utils.Float,
+ Double: utils.Double,
+ Short: utils.Short,
+ Byte: utils.Byte,
+ Long: utils.Long,
+ unwrap: utils.unwrap,
};
export default { driver, process, structure };
diff --git a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
index 6d8c09287b..2a07ad8dec 100644
--- a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
@@ -19,7 +19,7 @@
import { P, TextP, EnumValue } from './traversal.js';
import { OptionsStrategy, TraversalStrategy } from './traversal-strategy.js';
-import { Long } from '../utils.js';
+import { Long, Int, Float, Double, Short, Byte, INT32_MIN, INT32_MAX } from
'../utils.js';
import { Vertex } from '../structure/graph.js';
import { Buffer } from 'buffer';
@@ -72,6 +72,21 @@ export default class GremlinLang {
if (arg instanceof Long) {
return String(arg.value) + 'L';
}
+ if (arg instanceof Float) {
+ return GremlinLang._fpAsString(arg.value, 'F');
+ }
+ if (arg instanceof Double) {
+ return GremlinLang._fpAsString(arg.value, 'D');
+ }
+ if (arg instanceof Short) {
+ return String(arg.value) + 'S';
+ }
+ if (arg instanceof Byte) {
+ return String(arg.value) + 'B';
+ }
+ if (arg instanceof Int) {
+ return String(arg.value);
+ }
if (typeof arg === 'bigint') {
return String(arg) + 'N';
}
@@ -80,7 +95,7 @@ export default class GremlinLang {
if (arg === Infinity) return '+Infinity';
if (arg === -Infinity) return '-Infinity';
if (!Number.isInteger(arg)) return String(arg) + 'D';
- if (arg >= -2147483648 && arg <= 2147483647) return String(arg);
+ if (arg >= INT32_MIN && arg <= INT32_MAX) return String(arg);
// Outside safe integer range, values have lost precision and may exceed
Java Long — emit as Double.
if (arg > Number.MAX_SAFE_INTEGER || arg < -Number.MAX_SAFE_INTEGER)
return String(arg) + 'D';
return String(arg) + 'L';
@@ -187,6 +202,13 @@ export default class GremlinLang {
return this;
}
+ private static _fpAsString(v: number, suffix: 'F' | 'D'): string {
+ if (v === Infinity) return '+Infinity';
+ if (v === -Infinity) return '-Infinity';
+ if (Number.isNaN(v)) return 'NaN';
+ return Number.isInteger(v) ? `${v}.0${suffix}` : `${v}${suffix}`;
+ }
+
getGremlin(prefix: string = 'g'): string {
if (this.gremlin.length > 0 && this.gremlin[0] !== '.') {
return this.gremlin;
diff --git
a/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js
b/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js
index b8930f91c4..4bd402b596 100644
--- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js
+++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js
@@ -70,48 +70,79 @@ import AnySerializer from './internals/AnySerializer.js';
import GraphBinaryReader from './internals/GraphBinaryReader.js';
import GraphBinaryWriter from './internals/GraphBinaryWriter.js';
-const ioc = {};
-
-ioc.DataType = DataType;
-ioc.utils = utils;
-
-ioc.serializers = {};
-
-ioc.intSerializer = new IntSerializer(ioc);
-ioc.longSerializer = new LongSerializer(ioc);
-ioc.stringSerializer = new StringSerializer(ioc, ioc.DataType.STRING);
-ioc.dateTimeSerializer = new DateTimeSerializer(ioc);
-ioc.doubleSerializer = new DoubleSerializer(ioc);
-ioc.floatSerializer = new FloatSerializer(ioc);
-ioc.listSerializer = new ArraySerializer(ioc, ioc.DataType.LIST);
-ioc.mapSerializer = new MapSerializer(ioc);
-ioc.setSerializer = new SetSerializer(ioc, ioc.DataType.SET);
-ioc.uuidSerializer = new UuidSerializer(ioc);
-ioc.edgeSerializer = new EdgeSerializer(ioc);
-ioc.pathSerializer = new PathSerializer(ioc);
-ioc.propertySerializer = new PropertySerializer(ioc);
-ioc.vertexSerializer = new VertexSerializer(ioc);
-ioc.vertexPropertySerializer = new VertexPropertySerializer(ioc);
-ioc.bigIntegerSerializer = new BigIntegerSerializer(ioc);
-ioc.byteSerializer = new ByteSerializer(ioc);
-ioc.binarySerializer = new BinarySerializer(ioc);
-ioc.shortSerializer = new ShortSerializer(ioc);
-ioc.booleanSerializer = new BooleanSerializer(ioc);
-ioc.markerSerializer = new MarkerSerializer(ioc);
-ioc.unspecifiedNullSerializer = new UnspecifiedNullSerializer(ioc);
-ioc.enumSerializer = new EnumSerializer(ioc);
-
-// Register stub serializers for unimplemented v4 types
-new StubSerializer(ioc, ioc.DataType.TREE, 'Tree');
-new StubSerializer(ioc, ioc.DataType.GRAPH, 'Graph');
-new StubSerializer(ioc, ioc.DataType.COMPOSITEPDT, 'CompositePDT');
-new StubSerializer(ioc, ioc.DataType.PRIMITIVEPDT, 'PrimitivePDT');
-
-ioc.numberSerializationStrategy = new NumberSerializationStrategy(ioc);
-ioc.anySerializer = new AnySerializer(ioc);
-
-ioc.graphBinaryReader = new GraphBinaryReader(ioc);
-ioc.graphBinaryWriter = new GraphBinaryWriter(ioc);
+import { Float, Double, Int, Long, Short, Byte } from '../../../utils.js';
+
+function createIoc(anySerializerOptions) {
+ const ioc = {};
+
+ ioc.DataType = DataType;
+ ioc.utils = utils;
+
+ ioc.serializers = {};
+
+ ioc.intSerializer = new IntSerializer(ioc);
+ ioc.longSerializer = new LongSerializer(ioc);
+ ioc.stringSerializer = new StringSerializer(ioc, ioc.DataType.STRING);
+ ioc.dateTimeSerializer = new DateTimeSerializer(ioc);
+ ioc.doubleSerializer = new DoubleSerializer(ioc);
+ ioc.floatSerializer = new FloatSerializer(ioc);
+ ioc.listSerializer = new ArraySerializer(ioc, ioc.DataType.LIST);
+ ioc.mapSerializer = new MapSerializer(ioc);
+ ioc.setSerializer = new SetSerializer(ioc, ioc.DataType.SET);
+ ioc.uuidSerializer = new UuidSerializer(ioc);
+ ioc.edgeSerializer = new EdgeSerializer(ioc);
+ ioc.pathSerializer = new PathSerializer(ioc);
+ ioc.propertySerializer = new PropertySerializer(ioc);
+ ioc.vertexSerializer = new VertexSerializer(ioc);
+ ioc.vertexPropertySerializer = new VertexPropertySerializer(ioc);
+ ioc.bigIntegerSerializer = new BigIntegerSerializer(ioc);
+ ioc.byteSerializer = new ByteSerializer(ioc);
+ ioc.binarySerializer = new BinarySerializer(ioc);
+ ioc.shortSerializer = new ShortSerializer(ioc);
+ ioc.booleanSerializer = new BooleanSerializer(ioc);
+ ioc.markerSerializer = new MarkerSerializer(ioc);
+ ioc.unspecifiedNullSerializer = new UnspecifiedNullSerializer(ioc);
+ ioc.enumSerializer = new EnumSerializer(ioc);
+
+ // Register stub serializers for unimplemented v4 types
+ new StubSerializer(ioc, ioc.DataType.TREE, 'Tree');
+ new StubSerializer(ioc, ioc.DataType.GRAPH, 'Graph');
+ new StubSerializer(ioc, ioc.DataType.COMPOSITEPDT, 'CompositePDT');
+ new StubSerializer(ioc, ioc.DataType.PRIMITIVEPDT, 'PrimitivePDT');
+
+ ioc.numberSerializationStrategy = new NumberSerializationStrategy(ioc);
+ ioc.anySerializer = new AnySerializer(ioc, anySerializerOptions);
+
+ ioc.graphBinaryReader = new GraphBinaryReader(ioc);
+ ioc.graphBinaryWriter = new GraphBinaryWriter(ioc);
+
+ return ioc;
+}
+
+export function createPreciseReader() {
+ const wrapperMap = new Map([
+ [DataType.INT, Int],
+ [DataType.LONG, Long],
+ [DataType.FLOAT, Float],
+ [DataType.DOUBLE, Double],
+ [DataType.SHORT, Short],
+ [DataType.BYTE, Byte],
+ ]);
+
+ const preciseIoc = createIoc({
+ postDeserialize(result, typeCode) {
+ const Wrapper = wrapperMap.get(typeCode);
+ if (Wrapper && result !== null && result !== undefined) {
+ return new Wrapper(result);
+ }
+ return result;
+ },
+ });
+
+ return preciseIoc.graphBinaryReader;
+}
+
+const ioc = createIoc();
export { default as DataType } from './internals/DataType.js';
diff --git
a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js
b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js
index c3bd93bf1d..bfc20c0016 100644
---
a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js
+++
b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js
@@ -22,8 +22,9 @@
*/
export default class AnySerializer {
- constructor(ioc) {
+ constructor(ioc, { postDeserialize } = {}) {
this.ioc = ioc;
+ this._postDeserialize = postDeserialize || null;
// specifically ordered, the first canBeUsedFor=true wins
this.serializers = [
@@ -84,11 +85,17 @@ export default class AnySerializer {
throw new Error(`AnySerializer: unexpected
{value_flag}=0x${value_flag.toString(16)} at position ${pos}`);
}
+ let result;
try {
- return await serializer.deserializeValue(reader, value_flag, type_code);
+ result = await serializer.deserializeValue(reader, value_flag,
type_code);
} catch (err) {
err.message = `${serializer.constructor.name}.deserializeValue() at
position ${pos}: ${err.message}`;
throw err;
}
+
+ if (this._postDeserialize) {
+ return this._postDeserialize(result, type_code);
+ }
+ return result;
}
}
diff --git
a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/NumberSerializationStrategy.js
b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/NumberSerializationStrategy.js
index c457935e45..61c74fd551 100644
---
a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/NumberSerializationStrategy.js
+++
b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/NumberSerializationStrategy.js
@@ -21,6 +21,8 @@
* @author Igor Ostapenko
*/
+import { Long, Int, Float, Double, Short, Byte, INT32_MIN, INT32_MAX } from
'../../../../utils.js';
+
// Based on GraphSON NumberSerializer.serialize().
// It's tested by AnySerializer.serialize() tests.
export default class NumberSerializationStrategy {
@@ -29,6 +31,16 @@ export default class NumberSerializationStrategy {
}
canBeUsedFor(value) {
+ if (
+ value instanceof Long ||
+ value instanceof Int ||
+ value instanceof Float ||
+ value instanceof Double ||
+ value instanceof Short ||
+ value instanceof Byte
+ ) {
+ return true;
+ }
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY || value ===
Number.NEGATIVE_INFINITY) {
return true;
}
@@ -43,6 +55,25 @@ export default class NumberSerializationStrategy {
}
serialize(item, fullyQualifiedFormat = true) {
+ if (item instanceof Float) {
+ return this.ioc.floatSerializer.serialize(item.value,
fullyQualifiedFormat);
+ }
+ if (item instanceof Double) {
+ return this.ioc.doubleSerializer.serialize(item.value,
fullyQualifiedFormat);
+ }
+ if (item instanceof Int) {
+ return this.ioc.intSerializer.serialize(item.value,
fullyQualifiedFormat);
+ }
+ if (item instanceof Long) {
+ return this.ioc.longSerializer.serialize(item.value,
fullyQualifiedFormat);
+ }
+ if (item instanceof Short) {
+ return this.ioc.shortSerializer.serialize(item.value,
fullyQualifiedFormat);
+ }
+ if (item instanceof Byte) {
+ return this.ioc.byteSerializer.serialize(item.value,
fullyQualifiedFormat);
+ }
+
if (typeof item === 'number') {
if (
Number.isNaN(item) ||
@@ -54,8 +85,7 @@ export default class NumberSerializationStrategy {
return this.ioc.doubleSerializer.serialize(item, fullyQualifiedFormat);
}
- if (item >= -2147483648 && item <= 2147483647) {
- // INT32_MIN/MAX
+ if (item >= INT32_MIN && item <= INT32_MAX) {
return this.ioc.intSerializer.serialize(item, fullyQualifiedFormat);
}
if (item >= Number.MIN_SAFE_INTEGER && item <= Number.MAX_SAFE_INTEGER) {
diff --git a/gremlin-js/gremlin-javascript/lib/utils.ts
b/gremlin-js/gremlin-javascript/lib/utils.ts
index 35ba5a45ae..be81755872 100644
--- a/gremlin-js/gremlin-javascript/lib/utils.ts
+++ b/gremlin-js/gremlin-javascript/lib/utils.ts
@@ -24,16 +24,164 @@
const gremlinVersion = '4.0.0-SNAPSHOT'; // DO NOT MODIFY - Configured
automatically by Maven Replacer Plugin
-export function toLong(value: number | string) {
+const INT64_MIN = -9223372036854775808n;
+const INT64_MAX = 9223372036854775807n;
+
+export const INT32_MIN = -2147483648;
+export const INT32_MAX = 2147483647;
+
+export function toLong(value: number | string | bigint) {
return new Long(value);
}
export class Long {
- constructor(public value: number | string) {
- if (typeof value !== 'string' && typeof value !== 'number') {
- throw new TypeError('The value must be a string or a number');
+ readonly type = 'long';
+
+ constructor(public readonly value: number | string | bigint) {
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value
!== 'bigint') {
+ throw new TypeError('The value must be a string, a number, or a bigint');
+ }
+ if (typeof value === 'string') {
+ if (!/^(?:0|-?[1-9]\d*)$/.test(value)) {
+ throw new TypeError('Long value must be a valid integer');
+ }
+ const n = BigInt(value);
+ if (n < INT64_MIN || n > INT64_MAX) {
+ throw new RangeError('Long value is outside int64 range');
+ }
+ }
+ if (typeof value === 'number') {
+ if (!Number.isInteger(value)) {
+ throw new TypeError('Long value must be an integer');
+ }
+ if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) {
+ throw new RangeError('Long number values outside safe integer range
lose precision; use string or bigint');
+ }
+ }
+ if (typeof value === 'bigint') {
+ if (value < INT64_MIN || value > INT64_MAX) {
+ throw new RangeError('Long value is outside int64 range');
+ }
+ }
+ }
+
+ valueOf(): number {
+ if (typeof this.value === 'number') return this.value;
+ const big = typeof this.value === 'bigint' ? this.value :
BigInt(this.value);
+ if (big > BigInt(Number.MAX_SAFE_INTEGER) || big <
BigInt(Number.MIN_SAFE_INTEGER)) {
+ throw new RangeError('Long value is outside safe integer range');
+ }
+ return Number(big);
+ }
+
+ [Symbol.toPrimitive](hint: string) {
+ if (hint === 'string') return String(this.value);
+ return this.valueOf();
+ }
+
+ toJSON() {
+ if (typeof this.value === 'bigint') return this.value.toString();
+ return this.value;
+ }
+}
+
+export class Int {
+ readonly type = 'int';
+ constructor(public readonly value: number) {
+ if (typeof value !== 'number') {
+ throw new TypeError('Int value must be a number');
}
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
+ throw new TypeError('Int value must be a finite integer');
+ }
+ if (value < INT32_MIN || value > INT32_MAX) {
+ throw new RangeError('Int value must be within int32 range');
+ }
+ }
+ valueOf() { return this.value; }
+ [Symbol.toPrimitive](hint: string) { return hint === 'string' ?
String(this.value) : this.value; }
+ toJSON() { return this.value; }
+}
+
+export class Float {
+ readonly type = 'float';
+ constructor(public readonly value: number) {
+ if (typeof value !== 'number') {
+ throw new TypeError('Float value must be a number');
+ }
+ }
+ valueOf() { return this.value; }
+ [Symbol.toPrimitive](hint: string) { return hint === 'string' ?
String(this.value) : this.value; }
+ toJSON() { return this.value; }
+}
+
+export class Double {
+ readonly type = 'double';
+ constructor(public readonly value: number) {
+ if (typeof value !== 'number') {
+ throw new TypeError('Double value must be a number');
+ }
+ }
+ valueOf() { return this.value; }
+ [Symbol.toPrimitive](hint: string) { return hint === 'string' ?
String(this.value) : this.value; }
+ toJSON() { return this.value; }
+}
+
+export class Short {
+ readonly type = 'short';
+ constructor(public readonly value: number) {
+ if (typeof value !== 'number') {
+ throw new TypeError('Short value must be a number');
+ }
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
+ throw new TypeError('Short value must be a finite integer');
+ }
+ if (value < -32768 || value > 32767) {
+ throw new RangeError('Short value must be within int16 range');
+ }
+ }
+ valueOf() { return this.value; }
+ [Symbol.toPrimitive](hint: string) { return hint === 'string' ?
String(this.value) : this.value; }
+ toJSON() { return this.value; }
+}
+
+export class Byte {
+ readonly type = 'byte';
+ constructor(public readonly value: number) {
+ if (typeof value !== 'number') {
+ throw new TypeError('Byte value must be a number');
+ }
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
+ throw new TypeError('Byte value must be a finite integer');
+ }
+ if (value < -128 || value > 127) {
+ throw new RangeError('Byte value must be within int8 range');
+ }
+ }
+ valueOf() { return this.value; }
+ [Symbol.toPrimitive](hint: string) { return hint === 'string' ?
String(this.value) : this.value; }
+ toJSON() { return this.value; }
+}
+
+export function toInt(value: number) { return new Int(value); }
+export function toFloat(value: number) { return new Float(value); }
+export function toDouble(value: number) { return new Double(value); }
+export function toShort(value: number) { return new Short(value); }
+export function toByte(value: number) { return new Byte(value); }
+
+export function unwrap(value: Float): number;
+export function unwrap(value: Double): number;
+export function unwrap(value: Int): number;
+export function unwrap(value: Long): number | string | bigint;
+export function unwrap(value: Short): number;
+export function unwrap(value: Byte): number;
+export function unwrap<T>(value: T): T;
+export function unwrap(value: any): any {
+ if (value instanceof Long || value instanceof Int || value instanceof Float
||
+ value instanceof Double || value instanceof Short || value instanceof
Byte) {
+ return value.value;
}
+ return value;
}
export function getUuid() {
diff --git a/gremlin-js/gremlin-javascript/test/helper.js
b/gremlin-js/gremlin-javascript/test/helper.js
index 42926b323d..fa7f0d65b2 100644
--- a/gremlin-js/gremlin-javascript/test/helper.js
+++ b/gremlin-js/gremlin-javascript/test/helper.js
@@ -34,6 +34,8 @@ if (process.env.DOCKER_ENVIRONMENT === 'true') {
serverAuthUrl = 'https://localhost:45941/gremlin';
}
+export { serverUrl };
+
/** @returns {DriverRemoteConnection} */
export function getConnection(traversalSource) {
return new DriverRemoteConnection(serverUrl, { traversalSource });
diff --git a/gremlin-js/gremlin-javascript/test/integration/client-tests.js
b/gremlin-js/gremlin-javascript/test/integration/client-tests.js
index 09a4cd8230..aca3548d40 100644
--- a/gremlin-js/gremlin-javascript/test/integration/client-tests.js
+++ b/gremlin-js/gremlin-javascript/test/integration/client-tests.js
@@ -19,8 +19,10 @@
import assert from 'assert';
import { Vertex, Edge, VertexProperty } from '../../lib/structure/graph.js';
-import { getClient } from '../helper.js';
+import { getClient, serverUrl } from '../helper.js';
import { cardinality } from '../../lib/process/traversal.js';
+import Client from '../../lib/driver/client.js';
+import { Int, Double } from '../../lib/utils.js';
let client, clientCrew;
@@ -157,6 +159,35 @@ describe('Client', function () {
// assert.ok(!closingClient.isOpen());
// });
});
+
+ describe('#submit() with preciseNumbers', function () {
+ let preciseClient;
+
+ before(async function () {
+ preciseClient = new Client(serverUrl, { traversalSource: 'gmodern',
preciseNumbers: true });
+ await preciseClient.open();
+ });
+
+ after(async function () {
+ await preciseClient.close();
+ });
+
+ it('should return Int wrapper for integer vertex property', async function
() {
+ const result = await preciseClient.submit('g.V().has("name",
"marko").values("age")');
+ assert.ok(result);
+ assert.ok(result.first() instanceof Int);
+ assert.strictEqual(result.first().value, 29);
+ });
+
+ it('should return Double wrapper for edge weight property', async function
() {
+ const result = await preciseClient.submit('g.E().has("weight",
0.5).limit(1)');
+ assert.ok(result);
+ const edge = result.first();
+ assert.ok(edge instanceof Edge);
+ assert.ok(edge.properties[0].value instanceof Double);
+ assert.strictEqual(edge.properties[0].value.value, 0.5);
+ });
+ });
});
function assertVertexProperties(vertex) {
diff --git a/gremlin-js/gremlin-javascript/test/unit/exports-test.js
b/gremlin-js/gremlin-javascript/test/unit/exports-test.js
index 3f5adb88bf..f009c7074d 100644
--- a/gremlin-js/gremlin-javascript/test/unit/exports-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/exports-test.js
@@ -61,6 +61,19 @@ describe('API', function () {
assert.strictEqual(typeof glvModule.structure.Property, 'function');
assert.strictEqual(typeof glvModule.structure.Vertex, 'function');
assert.strictEqual(typeof glvModule.structure.VertexProperty, 'function');
+ assert.strictEqual(typeof glvModule.structure.toLong, 'function');
+ assert.strictEqual(typeof glvModule.structure.toInt, 'function');
+ assert.strictEqual(typeof glvModule.structure.toFloat, 'function');
+ assert.strictEqual(typeof glvModule.structure.toDouble, 'function');
+ assert.strictEqual(typeof glvModule.structure.toShort, 'function');
+ assert.strictEqual(typeof glvModule.structure.toByte, 'function');
+ assert.strictEqual(typeof glvModule.structure.Int, 'function');
+ assert.strictEqual(typeof glvModule.structure.Float, 'function');
+ assert.strictEqual(typeof glvModule.structure.Double, 'function');
+ assert.strictEqual(typeof glvModule.structure.Short, 'function');
+ assert.strictEqual(typeof glvModule.structure.Byte, 'function');
+ assert.strictEqual(typeof glvModule.structure.Long, 'function');
+ assert.strictEqual(typeof glvModule.structure.unwrap, 'function');
});
it('should expose fields under driver', function () {
assert.ok(glvModule.driver);
diff --git
a/gremlin-js/gremlin-javascript/test/unit/graphbinary/precise-mode-test.js
b/gremlin-js/gremlin-javascript/test/unit/graphbinary/precise-mode-test.js
new file mode 100644
index 0000000000..c2b9246713
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/precise-mode-test.js
@@ -0,0 +1,521 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'assert';
+import ioc, { createPreciseReader, DataType } from
'../../../lib/structure/io/binary/GraphBinary.js';
+import StreamReader from
'../../../lib/structure/io/binary/internals/StreamReader.js';
+import { Float, Double, Int, Long, Short, Byte, toFloat, toDouble, toInt,
toLong, toShort, toByte, unwrap } from '../../../lib/utils.js';
+import Connection from '../../../lib/driver/connection.js';
+import { Path } from '../../../lib/structure/graph.js';
+
+const { anySerializer, graphBinaryReader } = ioc;
+
+describe('Precise Mode Tests', () => {
+ let preciseReader;
+
+ before(() => {
+ preciseReader = createPreciseReader();
+ });
+
+ async function deserializeWithPrecise(buf) {
+ return
preciseReader.ioc.anySerializer.deserialize(StreamReader.fromBuffer(buf));
+ }
+
+ async function deserializeWithDefault(buf) {
+ return anySerializer.deserialize(StreamReader.fromBuffer(buf));
+ }
+
+ describe('Basic deserialization', () => {
+ it('FLOAT bytes → Float instance', async () => {
+ const buf = anySerializer.serialize(toFloat(1.5));
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result instanceof Float);
+ assert.strictEqual(result.value, 1.5);
+ assert.strictEqual(result.type, 'float');
+ });
+
+ it('DOUBLE bytes → Double instance', async () => {
+ const buf = anySerializer.serialize(toDouble(3.14));
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result instanceof Double);
+ assert.strictEqual(result.value, 3.14);
+ assert.strictEqual(result.type, 'double');
+ });
+
+ it('INT bytes → Int instance', async () => {
+ const buf = anySerializer.serialize(toInt(42));
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result instanceof Int);
+ assert.strictEqual(result.value, 42);
+ assert.strictEqual(result.type, 'int');
+ });
+
+ it('LONG bytes (safe range) → Long instance with number value', async ()
=> {
+ const buf = anySerializer.serialize(toLong(42));
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result instanceof Long);
+ assert.strictEqual(result.value, 42);
+ assert.strictEqual(result.type, 'long');
+ });
+
+ it('LONG bytes (unsafe range) → Long instance with bigint value', async ()
=> {
+ const buf = anySerializer.serialize(toLong(9007199254740993n));
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result instanceof Long);
+ assert.strictEqual(result.value, 9007199254740993n);
+ });
+
+ it('SHORT bytes → Short instance', async () => {
+ const buf = anySerializer.serialize(toShort(5));
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result instanceof Short);
+ assert.strictEqual(result.value, 5);
+ assert.strictEqual(result.type, 'short');
+ });
+
+ it('BYTE bytes → Byte instance', async () => {
+ const buf = anySerializer.serialize(toByte(127));
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result instanceof Byte);
+ assert.strictEqual(result.value, 127);
+ assert.strictEqual(result.type, 'byte');
+ });
+ });
+
+ describe('Nested structures', () => {
+ it('MAP containing Float values → Float wrappers inside Map', async () => {
+ const map = new Map([['x', toFloat(1.5)]]);
+ const buf = anySerializer.serialize(map);
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result.get('x') instanceof Float);
+ assert.strictEqual(result.get('x').value, 1.5);
+ });
+
+ it('LIST with mixed numeric types → each wrapped correctly', async () => {
+ const list = [toFloat(1.5), toInt(2), toDouble(3.14)];
+ const buf = anySerializer.serialize(list);
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result[0] instanceof Float);
+ assert.ok(result[1] instanceof Int);
+ assert.ok(result[2] instanceof Double);
+ });
+
+ it('PATH with numeric vertex IDs → wrapped correctly', async () => {
+ const path = new Path([['a'], ['b']], [toInt(1), toInt(2)]);
+ const buf = anySerializer.serialize(path);
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result instanceof Path);
+ assert.ok(result.objects[0] instanceof Int);
+ assert.strictEqual(result.objects[0].value, 1);
+ assert.ok(result.objects[1] instanceof Int);
+ assert.strictEqual(result.objects[1].value, 2);
+ });
+
+ it('LIST with Short, Byte, Long → each wrapped correctly', async () => {
+ const list = [toShort(5), toByte(127), toLong(99)];
+ const buf = anySerializer.serialize(list);
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result[0] instanceof Short);
+ assert.strictEqual(result[0].value, 5);
+ assert.ok(result[1] instanceof Byte);
+ assert.strictEqual(result[1].value, 127);
+ assert.ok(result[2] instanceof Long);
+ assert.strictEqual(result[2].value, 99);
+ });
+
+ it('Non-numeric types still deserialize correctly', async () => {
+ const list = ['hello', true, 'world'];
+ const buf = anySerializer.serialize(list);
+ const result = await deserializeWithPrecise(buf);
+ assert.deepStrictEqual(result, ['hello', true, 'world']);
+ });
+ });
+
+ describe('Precise reader error paths', () => {
+ it('returns null for a null-flagged numeric value', async () => {
+ const buf = Buffer.from([DataType.INT, 0x01]);
+ const result = await deserializeWithPrecise(buf);
+ assert.strictEqual(result, null);
+ });
+
+ it('returns null for a null-flagged LONG value', async () => {
+ const buf = Buffer.from([DataType.LONG, 0x01]);
+ assert.strictEqual(await deserializeWithPrecise(buf), null);
+ });
+
+ it('returns null for a null-flagged FLOAT value', async () => {
+ const buf = Buffer.from([DataType.FLOAT, 0x01]);
+ assert.strictEqual(await deserializeWithPrecise(buf), null);
+ });
+
+ it('returns null for a null-flagged DOUBLE value', async () => {
+ const buf = Buffer.from([DataType.DOUBLE, 0x01]);
+ assert.strictEqual(await deserializeWithPrecise(buf), null);
+ });
+
+ it('returns null for a null-flagged SHORT value', async () => {
+ const buf = Buffer.from([DataType.SHORT, 0x01]);
+ assert.strictEqual(await deserializeWithPrecise(buf), null);
+ });
+
+ it('returns null for a null-flagged BYTE value', async () => {
+ const buf = Buffer.from([DataType.BYTE, 0x01]);
+ assert.strictEqual(await deserializeWithPrecise(buf), null);
+ });
+
+ it('throws on invalid value_flag', async () => {
+ const buf = Buffer.from([DataType.INT, 0xFF, 0, 0, 0, 0]);
+ await assert.rejects(() => deserializeWithPrecise(buf), /AnySerializer:
unexpected \{value_flag}=0x/);
+ });
+
+ it('throws on unknown type_code', async () => {
+ const buf = Buffer.from([0xEE, 0x00]);
+ await assert.rejects(() => deserializeWithPrecise(buf), /AnySerializer:
unknown \{type_code}=0xee/);
+ });
+
+ it('deserializes value_flag 0x02 as non-null', async () => {
+ const buf = Buffer.from([DataType.INT, 0x02, 0x00, 0x00, 0x00, 0x2A]);
+ const result = await deserializeWithPrecise(buf);
+ assert.ok(result instanceof Int);
+ assert.strictEqual(result.value, 42);
+ });
+
+ it('wraps deserializeValue errors with position info', async () => {
+ const buf = Buffer.from([DataType.INT, 0x00]);
+ await assert.rejects(() => deserializeWithPrecise(buf),
/IntSerializer\.deserializeValue\(\) at position \d+/);
+ });
+ });
+
+ describe('Wrapper behavior', () => {
+ it('Float valueOf works in arithmetic', () => {
+ assert.strictEqual(new Float(1.5) + 1, 2.5);
+ });
+
+ it('Int valueOf works in arithmetic', () => {
+ assert.strictEqual(new Int(42) + 0, 42);
+ });
+
+ it('Double valueOf works in arithmetic', () => {
+ assert.strictEqual(new Double(3.14) + 0, 3.14);
+ });
+
+ it('Short valueOf works in arithmetic', () => {
+ assert.strictEqual(new Short(5) + 1, 6);
+ });
+
+ it('Byte valueOf works in arithmetic', () => {
+ assert.strictEqual(new Byte(127) + 0, 127);
+ });
+
+ it('Long with unsafe bigint throws RangeError on arithmetic', () => {
+ assert.throws(() => new Long(9007199254740993n) + 1, RangeError);
+ });
+
+ it('Long with unsafe string throws RangeError on arithmetic', () => {
+ assert.throws(() => new Long('9007199254740993') + 1, RangeError);
+ });
+
+ it('Long with safe bigint works in arithmetic', () => {
+ assert.strictEqual(new Long(42n) + 1, 43);
+ });
+
+ it('Long toPrimitive string hint works for unsafe values', () => {
+ assert.strictEqual(`${new Long(9007199254740993n)}`, '9007199254740993');
+ });
+
+ it('Float toPrimitive string hint', () => {
+ assert.strictEqual(`${new Float(1.5)}`, '1.5');
+ });
+
+ it('Double toPrimitive string hint', () => {
+ assert.strictEqual(`${new Double(3.14)}`, '3.14');
+ });
+
+ it('Int toPrimitive string hint', () => {
+ assert.strictEqual(`${new Int(67)}`, '67');
+ });
+
+ it('Short toPrimitive string hint', () => {
+ assert.strictEqual(`${new Short(5)}`, '5');
+ });
+
+ it('Byte toPrimitive string hint', () => {
+ assert.strictEqual(`${new Byte(127)}`, '127');
+ });
+
+ it('JSON.stringify Float', () => {
+ assert.strictEqual(JSON.stringify(new Float(1.5)), '1.5');
+ });
+
+ it('JSON.stringify Long with bigint', () => {
+ assert.strictEqual(JSON.stringify(new Long(9007199254740993n)),
'"9007199254740993"');
+ });
+
+ it('Long valueOf with safe string returns number', () => {
+ assert.strictEqual(new Long('42').valueOf(), 42);
+ });
+
+ it('Long valueOf with unsafe string throws RangeError', () => {
+ assert.throws(() => new Long('9007199254740993').valueOf(), RangeError);
+ });
+
+ it('JSON.stringify Long with string value preserves string', () => {
+ assert.strictEqual(JSON.stringify(new Long('9007199254740993')),
'"9007199254740993"');
+ });
+
+ it('JSON.stringify Long with number value is a number', () => {
+ assert.strictEqual(JSON.stringify(new Long(42)), '42');
+ });
+
+ it('JSON.stringify Double', () => {
+ assert.strictEqual(JSON.stringify(new Double(3.14)), '3.14');
+ });
+
+ it('JSON.stringify Int', () => {
+ assert.strictEqual(JSON.stringify(new Int(67)), '67');
+ });
+
+ it('JSON.stringify Short', () => {
+ assert.strictEqual(JSON.stringify(new Short(5)), '5');
+ });
+
+ it('JSON.stringify Byte', () => {
+ assert.strictEqual(JSON.stringify(new Byte(127)), '127');
+ });
+
+ it('unwrap Float', () => {
+ assert.strictEqual(unwrap(new Float(1.5)), 1.5);
+ });
+
+ it('unwrap Long with bigint', () => {
+ assert.strictEqual(unwrap(new Long(42n)), 42n);
+ });
+
+ it('unwrap Long with string', () => {
+ assert.strictEqual(unwrap(new Long('123')), '123');
+ });
+
+ it('unwrap Long with number', () => {
+ assert.strictEqual(unwrap(new Long(42)), 42);
+ });
+
+ it('unwrap Int', () => {
+ assert.strictEqual(unwrap(new Int(42)), 42);
+ });
+
+ it('unwrap Double', () => {
+ assert.strictEqual(unwrap(new Double(3.14)), 3.14);
+ });
+
+ it('unwrap Short', () => {
+ assert.strictEqual(unwrap(new Short(5)), 5);
+ });
+
+ it('unwrap Byte', () => {
+ assert.strictEqual(unwrap(new Byte(127)), 127);
+ });
+
+ it('unwrap plain number passthrough', () => {
+ assert.strictEqual(unwrap(42), 42);
+ });
+
+ it('unwrap null passthrough', () => {
+ assert.strictEqual(unwrap(null), null);
+ });
+
+ it('unwrap undefined passthrough', () => {
+ assert.strictEqual(unwrap(undefined), undefined);
+ });
+ });
+
+ describe('Long constructor validation', () => {
+ it('rejects non-numeric string', () => {
+ assert.throws(() => new Long('abc'), TypeError);
+ });
+
+ it('rejects injection attempt', () => {
+ assert.throws(() => new Long('1L).drop()'), TypeError);
+ });
+
+ it('rejects empty string', () => {
+ assert.throws(() => new Long(''), TypeError);
+ });
+
+ it('rejects non-integer number', () => {
+ assert.throws(() => new Long(1.5), TypeError);
+ });
+
+ it('accepts exact int64 max (bigint)', () => {
+ const l = new Long(9223372036854775807n);
+ assert.strictEqual(l.value, 9223372036854775807n);
+ });
+
+ it('accepts exact int64 min (bigint)', () => {
+ const l = new Long(-9223372036854775808n);
+ assert.strictEqual(l.value, -9223372036854775808n);
+ });
+
+ it('rejects one above int64 max (bigint)', () => {
+ assert.throws(() => new Long(9223372036854775808n), RangeError);
+ });
+
+ it('rejects one below int64 min (bigint)', () => {
+ assert.throws(() => new Long(-9223372036854775809n), RangeError);
+ });
+
+ it('accepts exact int64 max (string)', () => {
+ const l = new Long('9223372036854775807');
+ assert.strictEqual(l.value, '9223372036854775807');
+ });
+
+ it('rejects one above int64 max (string)', () => {
+ assert.throws(() => new Long('9223372036854775808'), RangeError);
+ });
+
+ it('rejects negative zero string', () => {
+ assert.throws(() => new Long('-0'), TypeError);
+ });
+
+ it('accepts negative zero number', () => {
+ const l = new Long(-0);
+ assert.strictEqual(l.valueOf(), -0);
+ });
+
+ it('rejects leading zeros in string', () => {
+ assert.throws(() => new Long('0042'), TypeError);
+ });
+ });
+
+ describe('Float constructor validation', () => {
+ it('rejects non-number argument', () => {
+ assert.throws(() => new Float('1.5'), TypeError);
+ });
+ });
+
+ describe('Double constructor validation', () => {
+ it('rejects non-number argument', () => {
+ assert.throws(() => new Double('3.14'), TypeError);
+ });
+ });
+
+ describe('Int constructor validation', () => {
+ it('accepts exact int32 max', () => {
+ assert.strictEqual(new Int(2147483647).value, 2147483647);
+ });
+
+ it('accepts exact int32 min', () => {
+ assert.strictEqual(new Int(-2147483648).value, -2147483648);
+ });
+
+ it('rejects above int32 max', () => {
+ assert.throws(() => new Int(2147483648), RangeError);
+ });
+
+ it('rejects below int32 min', () => {
+ assert.throws(() => new Int(-2147483649), RangeError);
+ });
+
+ it('rejects non-integer', () => {
+ assert.throws(() => new Int(1.5), TypeError);
+ });
+
+ it('rejects non-number argument', () => {
+ assert.throws(() => new Int('5'), TypeError);
+ });
+ });
+
+ describe('Short constructor validation', () => {
+ it('accepts exact int16 max', () => {
+ assert.strictEqual(new Short(32767).value, 32767);
+ });
+
+ it('accepts exact int16 min', () => {
+ assert.strictEqual(new Short(-32768).value, -32768);
+ });
+
+ it('rejects above int16 max', () => {
+ assert.throws(() => new Short(32768), RangeError);
+ });
+
+ it('rejects below int16 min', () => {
+ assert.throws(() => new Short(-32769), RangeError);
+ });
+
+ it('rejects non-integer', () => {
+ assert.throws(() => new Short(1.5), TypeError);
+ });
+
+ it('rejects non-number argument', () => {
+ assert.throws(() => new Short('5'), TypeError);
+ });
+ });
+
+ describe('Byte constructor validation', () => {
+ it('accepts exact int8 max', () => {
+ assert.strictEqual(new Byte(127).value, 127);
+ });
+
+ it('accepts exact int8 min', () => {
+ assert.strictEqual(new Byte(-128).value, -128);
+ });
+
+ it('rejects above int8 max', () => {
+ assert.throws(() => new Byte(128), RangeError);
+ });
+
+ it('rejects below int8 min', () => {
+ assert.throws(() => new Byte(-129), RangeError);
+ });
+
+ it('rejects non-integer', () => {
+ assert.throws(() => new Byte(1.5), TypeError);
+ });
+
+ it('rejects non-number argument', () => {
+ assert.throws(() => new Byte('127'), TypeError);
+ });
+ });
+
+ describe('Backward compatibility', () => {
+ it('default graphBinaryReader still returns plain numbers after
createPreciseReader()', async () => {
+ const buf = anySerializer.serialize(toFloat(1.5));
+ const result = await deserializeWithDefault(buf);
+ assert.strictEqual(result, 1.5);
+ assert.ok(!(result instanceof Float));
+ });
+ });
+
+ describe('Connection option wiring', () => {
+ it('preciseNumbers: true uses a precise reader', () => {
+ const conn = new Connection('http://localhost:8182', { preciseNumbers:
true });
+ assert.ok(conn._reader !== graphBinaryReader);
+ });
+
+ it('explicit reader takes precedence over preciseNumbers', () => {
+ const customReader = { custom: true };
+ const conn = new Connection('http://localhost:8182', { reader:
customReader, preciseNumbers: true });
+ assert.strictEqual(conn._reader, customReader);
+ });
+
+ it('default uses the default reader', () => {
+ const conn = new Connection('http://localhost:8182', {});
+ assert.strictEqual(conn._reader, graphBinaryReader);
+ });
+ });
+});
diff --git
a/gremlin-js/gremlin-javascript/test/unit/graphbinary/typed-number-test.js
b/gremlin-js/gremlin-javascript/test/unit/graphbinary/typed-number-test.js
new file mode 100644
index 0000000000..9e611e419f
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/typed-number-test.js
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'assert';
+import { toFloat, toDouble, toInt, toLong, toShort, toByte } from
'../../../lib/utils.js';
+import ioc, { DataType } from
'../../../lib/structure/io/binary/GraphBinary.js';
+import StreamReader from
'../../../lib/structure/io/binary/internals/StreamReader.js';
+import { P } from '../../../lib/process/traversal.js';
+
+const { anySerializer, numberSerializationStrategy } = ioc;
+
+describe('Typed Number Tests', () => {
+ describe('Type-code routing via anySerializer', () => {
+ it('toFloat → FLOAT', () => {
+ assert.strictEqual(anySerializer.serialize(toFloat(1.0))[0],
DataType.FLOAT);
+ });
+
+ it('toDouble → DOUBLE', () => {
+ assert.strictEqual(anySerializer.serialize(toDouble(1.0))[0],
DataType.DOUBLE);
+ });
+
+ it('toInt → INT', () => {
+ assert.strictEqual(anySerializer.serialize(toInt(67))[0], DataType.INT);
+ });
+
+ it('toLong → LONG', () => {
+ assert.strictEqual(anySerializer.serialize(toLong(67))[0],
DataType.LONG);
+ });
+
+ it('toShort → SHORT', () => {
+ assert.strictEqual(anySerializer.serialize(toShort(5))[0],
DataType.SHORT);
+ });
+
+ it('toByte → BYTE', () => {
+ assert.strictEqual(anySerializer.serialize(toByte(127))[0],
DataType.BYTE);
+ });
+ });
+
+ describe('canBeUsedFor routing', () => {
+ it('accepts typed wrappers', () => {
+ assert.ok(numberSerializationStrategy.canBeUsedFor(toFloat(1.0)));
+ assert.ok(numberSerializationStrategy.canBeUsedFor(toInt(67)));
+ assert.ok(numberSerializationStrategy.canBeUsedFor(toLong(67n)));
+ assert.ok(numberSerializationStrategy.canBeUsedFor(toDouble(1.0)));
+ assert.ok(numberSerializationStrategy.canBeUsedFor(toShort(5)));
+ assert.ok(numberSerializationStrategy.canBeUsedFor(toByte(127)));
+ });
+
+ it('rejects plain objects', () => {
+ assert.ok(!numberSerializationStrategy.canBeUsedFor({ value: 1 }));
+ });
+ });
+
+ describe('Byte-level verification', () => {
+ it('toFloat(1.0) produces different bytes than toInt(1)', () => {
+ const floatBytes = anySerializer.serialize(toFloat(1.0));
+ const intBytes = anySerializer.serialize(toInt(1));
+ assert.ok(!Buffer.from(floatBytes).equals(Buffer.from(intBytes)));
+ });
+
+ it('toFloat(1.0) produces different bytes than toDouble(1.0) (4 vs 8 value
bytes)', () => {
+ const floatBytes = anySerializer.serialize(toFloat(1.0));
+ const doubleBytes = anySerializer.serialize(toDouble(1.0));
+ assert.ok(!Buffer.from(floatBytes).equals(Buffer.from(doubleBytes)));
+ });
+
+ it('toShort(1) produces different bytes than toInt(1)', () => {
+ const shortBytes = anySerializer.serialize(toShort(1));
+ const intBytes = anySerializer.serialize(toInt(1));
+ assert.ok(!Buffer.from(shortBytes).equals(Buffer.from(intBytes)));
+ });
+
+ it('toByte(1) produces different bytes than toShort(1)', () => {
+ const byteBytes = anySerializer.serialize(toByte(1));
+ const shortBytes = anySerializer.serialize(toShort(1));
+ assert.ok(!Buffer.from(byteBytes).equals(Buffer.from(shortBytes)));
+ });
+
+ it('toLong(1) produces different bytes than toInt(1)', () => {
+ const longBytes = anySerializer.serialize(toLong(1));
+ const intBytes = anySerializer.serialize(toInt(1));
+ assert.ok(!Buffer.from(longBytes).equals(Buffer.from(intBytes)));
+ });
+ });
+
+ describe('Backward compatibility', () => {
+ it('plain 67 still routes to INT', () => {
+ assert.strictEqual(anySerializer.serialize(67)[0], DataType.INT);
+ });
+
+ it('plain 3.14 still routes to DOUBLE', () => {
+ assert.strictEqual(anySerializer.serialize(3.14)[0], DataType.DOUBLE);
+ });
+
+ it('plain 2147483648 still routes to LONG', () => {
+ assert.strictEqual(anySerializer.serialize(2147483648)[0],
DataType.LONG);
+ });
+ });
+
+ describe('Edge cases', () => {
+ it('toLong(67n) serializes correctly (bigint input)', () => {
+ const bytes = anySerializer.serialize(toLong(67n));
+ assert.strictEqual(bytes[0], DataType.LONG);
+ });
+
+ it('toLong string input serializes without precision loss', () => {
+ const bytes = anySerializer.serialize(toLong('9007199254740993'));
+ assert.strictEqual(bytes[0], DataType.LONG);
+ });
+
+ it('toShort(-1) — negative value', () => {
+ const bytes = anySerializer.serialize(toShort(-1));
+ assert.strictEqual(bytes[0], DataType.SHORT);
+ });
+
+ it('toByte(-128) — negative value', () => {
+ const bytes = anySerializer.serialize(toByte(-128));
+ assert.strictEqual(bytes[0], DataType.BYTE);
+ });
+
+ it('toFloat(NaN)', () => {
+ const bytes = anySerializer.serialize(toFloat(NaN));
+ assert.strictEqual(bytes[0], DataType.FLOAT);
+ });
+
+ it('toFloat(Infinity)', () => {
+ const bytes = anySerializer.serialize(toFloat(Infinity));
+ assert.strictEqual(bytes[0], DataType.FLOAT);
+ });
+
+ it('toFloat(-0)', () => {
+ const bytes = anySerializer.serialize(toFloat(-0));
+ assert.strictEqual(bytes[0], DataType.FLOAT);
+ });
+
+ it('toDouble(NaN)', () => {
+ const bytes = anySerializer.serialize(toDouble(NaN));
+ assert.strictEqual(bytes[0], DataType.DOUBLE);
+ });
+
+ it('toDouble(Infinity)', () => {
+ const bytes = anySerializer.serialize(toDouble(Infinity));
+ assert.strictEqual(bytes[0], DataType.DOUBLE);
+ });
+
+ it('toDouble(-Infinity)', () => {
+ const bytes = anySerializer.serialize(toDouble(-Infinity));
+ assert.strictEqual(bytes[0], DataType.DOUBLE);
+ });
+
+ it('toDouble(-0)', () => {
+ const bytes = anySerializer.serialize(toDouble(-0));
+ assert.strictEqual(bytes[0], DataType.DOUBLE);
+ });
+ });
+
+ describe('Round-trip through default reader', () => {
+ it('toFloat(1.5) round-trips to plain number', async () => {
+ const bytes = anySerializer.serialize(toFloat(1.5));
+ const result = await
anySerializer.deserialize(StreamReader.fromBuffer(bytes));
+ assert.strictEqual(result, 1.5);
+ });
+
+ it('toInt(67) round-trips to plain number', async () => {
+ const bytes = anySerializer.serialize(toInt(67));
+ const result = await
anySerializer.deserialize(StreamReader.fromBuffer(bytes));
+ assert.strictEqual(result, 67);
+ });
+
+ it('toLong string value round-trips without precision loss', async () => {
+ const bytes = anySerializer.serialize(toLong('9007199254740993'));
+ const result = await
anySerializer.deserialize(StreamReader.fromBuffer(bytes));
+ assert.strictEqual(result, 9007199254740993n);
+ });
+ });
+
+ describe('Integration with traversal structures', () => {
+ it('wrappers work inside P predicates', () => {
+ const predicate = P.gt(toFloat(1.0));
+ const bytes = anySerializer.serialize(predicate);
+ // P serializes successfully — the inner Float gets correct type code
+ assert.ok(bytes.length > 0);
+ });
+
+ it('wrappers inside collections get correct type codes', async () => {
+ const bytes = anySerializer.serialize([toFloat(1.0), toInt(2)]);
+ const reader = StreamReader.fromBuffer(bytes);
+ // Skip list type code (1 byte) and length (4 bytes)
+ const result = await anySerializer.deserialize(reader);
+ // The list deserializes — each element was serialized with its own type
code
+ assert.strictEqual(result.length, 2);
+ assert.strictEqual(result[0], 1.0);
+ assert.strictEqual(result[1], 2);
+ });
+ });
+});
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 40472c99f5..a8c4e9add0 100644
--- a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
@@ -27,7 +27,7 @@ import { ReadOnlyStrategy, SubgraphStrategy, OptionsStrategy,
PartitionStrategy, SeedStrategy } from
'../../lib/process/traversal-strategy.js';
import { Graph, Vertex } from '../../lib/structure/graph.js';
import { TraversalStrategies } from '../../lib/process/traversal-strategy.js';
-import { Long } from '../../lib/utils.js';
+import { Long, toFloat, toDouble, toShort, toByte, toInt, toLong } from
'../../lib/utils.js';
import GremlinLang from '../../lib/process/gremlin-lang.js';
const g = new GraphTraversalSource(new Graph(), new TraversalStrategies());
@@ -434,6 +434,138 @@ describe('GremlinLang', function () {
const view = new Uint8Array(ab, 1, 3); // bytes [1, 2, 3]
assert.strictEqual(g.inject(view).getGremlinLang().getGremlin(),
'g.inject(Binary("AQID"))');
});
+
+ it('should handle toFloat with round number', function () {
+ assert.strictEqual(g.V(toFloat(1.0)).getGremlinLang().getGremlin(),
'g.V(1.0F)');
+ });
+
+ it('should handle toFloat with fractional number', function () {
+ assert.strictEqual(g.V(toFloat(1.5)).getGremlinLang().getGremlin(),
'g.V(1.5F)');
+ });
+
+ it('should handle toDouble with round number', function () {
+ assert.strictEqual(g.V(toDouble(1.0)).getGremlinLang().getGremlin(),
'g.V(1.0D)');
+ });
+
+ it('should handle toDouble with fractional number', function () {
+ assert.strictEqual(g.V(toDouble(1.5)).getGremlinLang().getGremlin(),
'g.V(1.5D)');
+ });
+
+ it('should handle toShort', function () {
+ assert.strictEqual(g.V(toShort(5)).getGremlinLang().getGremlin(),
'g.V(5S)');
+ });
+
+ it('should handle toShort with negative value', function () {
+ assert.strictEqual(g.V(toShort(-32768)).getGremlinLang().getGremlin(),
'g.V(-32768S)');
+ });
+
+ it('should handle toByte', function () {
+ assert.strictEqual(g.V(toByte(127)).getGremlinLang().getGremlin(),
'g.V(127B)');
+ });
+
+ it('should handle toByte with negative value', function () {
+ assert.strictEqual(g.V(toByte(-128)).getGremlinLang().getGremlin(),
'g.V(-128B)');
+ });
+
+ it('should handle toInt', function () {
+ assert.strictEqual(g.V(toInt(42)).getGremlinLang().getGremlin(),
'g.V(42)');
+ });
+
+ it('should handle toLong with bigint input', function () {
+ assert.strictEqual(g.V(toLong(42n)).getGremlinLang().getGremlin(),
'g.V(42L)');
+ });
+
+ it('should handle toLong with number input', function () {
+ assert.strictEqual(g.V(toLong(42)).getGremlinLang().getGremlin(),
'g.V(42L)');
+ });
+
+ it('should handle toLong with string input', function () {
+
assert.strictEqual(g.V(toLong('9007199254740993')).getGremlinLang().getGremlin(),
'g.V(9007199254740993L)');
+ });
+
+ it('should handle toFloat with zero', function () {
+ assert.strictEqual(g.V(toFloat(0)).getGremlinLang().getGremlin(),
'g.V(0.0F)');
+ });
+
+ it('should handle toDouble with zero', function () {
+ assert.strictEqual(g.V(toDouble(0)).getGremlinLang().getGremlin(),
'g.V(0.0D)');
+ });
+
+ it('should handle toFloat with NaN', function () {
+ assert.strictEqual(g.V(toFloat(NaN)).getGremlinLang().getGremlin(),
'g.V(NaN)');
+ });
+
+ it('should handle toFloat with Infinity', function () {
+ assert.strictEqual(g.V(toFloat(Infinity)).getGremlinLang().getGremlin(),
'g.V(+Infinity)');
+ });
+
+ it('should handle toFloat with -Infinity', function () {
+
assert.strictEqual(g.V(toFloat(-Infinity)).getGremlinLang().getGremlin(),
'g.V(-Infinity)');
+ });
+
+ it('should handle toDouble with NaN', function () {
+ assert.strictEqual(g.V(toDouble(NaN)).getGremlinLang().getGremlin(),
'g.V(NaN)');
+ });
+
+ it('should handle toDouble with Infinity', function () {
+
assert.strictEqual(g.V(toDouble(Infinity)).getGremlinLang().getGremlin(),
'g.V(+Infinity)');
+ });
+
+ it('should handle toDouble with -Infinity', function () {
+
assert.strictEqual(g.V(toDouble(-Infinity)).getGremlinLang().getGremlin(),
'g.V(-Infinity)');
+ });
+
+ it('should handle toLong with negative number input', function () {
+ assert.strictEqual(g.V(toLong(-1)).getGremlinLang().getGremlin(),
'g.V(-1L)');
+ });
+
+ it('should handle toLong with int64 min bigint', function () {
+
assert.strictEqual(g.V(toLong(-9223372036854775808n)).getGremlinLang().getGremlin(),
'g.V(-9223372036854775808L)');
+ });
+
+ it('should handle toInt with negative value', function () {
+ assert.strictEqual(g.V(toInt(-1)).getGremlinLang().getGremlin(),
'g.V(-1)');
+ });
+
+ it('should handle toInt at int32 max', function () {
+ assert.strictEqual(g.V(toInt(2147483647)).getGremlinLang().getGremlin(),
'g.V(2147483647)');
+ });
+
+ it('should handle toInt at int32 min', function () {
+
assert.strictEqual(g.V(toInt(-2147483648)).getGremlinLang().getGremlin(),
'g.V(-2147483648)');
+ });
+
+ it('should handle toShort at boundary values', function () {
+ assert.strictEqual(g.V(toShort(32767)).getGremlinLang().getGremlin(),
'g.V(32767S)');
+ });
+
+ it('should handle toByte at zero', function () {
+ assert.strictEqual(g.V(toByte(0)).getGremlinLang().getGremlin(),
'g.V(0B)');
+ });
+
+ it('should handle toFloat inside P.gt', function () {
+ assert.strictEqual(g.V().has('x',
P.gt(toFloat(1.5))).getGremlinLang().getGremlin(), "g.V().has('x',gt(1.5F))");
+ });
+
+ it('should handle toShort inside P.between', function () {
+ assert.strictEqual(g.V().has('x', P.between(toShort(1),
toShort(10))).getGremlinLang().getGremlin(), "g.V().has('x',between(1S,10S))");
+ });
+
+ it('should handle toInt inside P.within', function () {
+ assert.strictEqual(g.V().has('x', P.within([toInt(1),
toInt(2)])).getGremlinLang().getGremlin(), "g.V().has('x',within([1,2]))");
+ });
+
+ it('should handle toDouble inside P.gt', function () {
+ assert.strictEqual(g.V().has('x',
P.gt(toDouble(3.14))).getGremlinLang().getGremlin(),
"g.V().has('x',gt(3.14D))");
+ });
+
+ it('should handle typed wrappers in inject array', function () {
+ assert.strictEqual(g.inject([toFloat(1.5),
toFloat(2.5)]).getGremlinLang().getGremlin(), 'g.inject([1.5F,2.5F])');
+ });
+
+ it('should handle typed wrapper in inject Map', function () {
+ assert.strictEqual(g.inject(new Map([['x',
toDouble(3.14)]])).getGremlinLang().getGremlin(), "g.inject(['x':3.14D])");
+ });
});
describe('Unsupported type tests', function () {