Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
Expand Down
51 changes: 49 additions & 2 deletions docs/src/reference/gremlin-variants.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down
47 changes: 47 additions & 0 deletions docs/src/upgrade/release-4.x.x.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions gremlin-js/gremlin-javascript/lib/driver/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -53,6 +53,7 @@ export type ConnectionOptions = {
ca?: string[];
cert?: string | string[] | Buffer;
pfx?: string | Buffer;
preciseNumbers?: boolean;
reader?: any;
rejectUnauthorized?: boolean;
traversalSource?: string;
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions gremlin-js/gremlin-javascript/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
26 changes: 24 additions & 2 deletions gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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';
}
Expand All @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
115 changes: 73 additions & 42 deletions gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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;
}
}
Loading
Loading