Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f6e60c5
Make GraphBinary/GraphSON write path registry-aware for PDT dehydration
Cole-Greer Jun 24, 2026
f6f65ae
Extract ProviderDefinedTypeAdapter supertype; rename composite adapte…
Cole-Greer Jun 24, 2026
3ec227c
Add core PrimitiveProviderDefinedType + GraphBinary serializer (0xF1)
Cole-Greer Jun 24, 2026
c77d1d6
Add PrimitivePDTAdapter, registry hydration, and binary reader/writer…
Cole-Greer Jun 24, 2026
38d3850
Add GraphSON V4 serialization for PrimitivePDT (g:PrimitivePdt)
Cole-Greer Jun 24, 2026
a23714f
Add PrimitivePDT grammar literal overload, translators, and GremlinLa…
Cole-Greer Jun 24, 2026
85f2d7b
Verify Java driver registry wiring for PrimitivePDT
Cole-Greer Jun 24, 2026
7aa8ead
Add server-side PrimitivePDT test fixtures and integration coverage
Cole-Greer Jun 24, 2026
bab3d4a
Add PrimitivePDT support to gremlin-python (first GLV)
Cole-Greer Jun 24, 2026
ef194e3
PDT review fixes: adapter precedence, Python gremlin-lang primitive s…
Cole-Greer Jun 25, 2026
16316eb
Add PrimitivePDT support to gremlin-javascript GLV
Cole-Greer Jun 25, 2026
002865a
Add PrimitivePDT support to gremlin-go GLV
Cole-Greer Jun 25, 2026
fdff7b2
Add PrimitivePDT support to gremlin-dotnet GLV
Cole-Greer Jun 25, 2026
435f10d
Document PrimitivePDT: provider guide, gremlin-lang literal, upgrade …
Cole-Greer Jun 25, 2026
45fe41b
PrimitivePDT review nits: document hydrated exclusion, drop no-op tra…
Cole-Greer Jun 25, 2026
ba9f3b4
Document why the Go PrimitivePDT test uses a named uint32 type
Cole-Greer Jun 25, 2026
66d9128
cleanup dotnet tests
Cole-Greer Jun 25, 2026
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 @@ -30,6 +30,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* 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 Provider Defined Types (PDT) support — graph providers can define custom types via `@ProviderDefined` annotation that serialize/deserialize seamlessly across all GLVs without driver-side configuration. Replaces TP3 custom type mechanism.
* Added Primitive Provider Defined Types (PrimitivePDT) — a single opaque stringified value variant of PDT for types with no native TinkerPop representation (e.g. unsigned integers). Supported across all GLVs via adapter registration.
* Added Gremlator, a single page web application, that translates Gremlin into various programming languages like Javascript and Python.
* Added explicit transaction support to all non-Java GLVs (gremlin-python, gremlin-go, gremlin-javascript, gremlin-dotnet).
* Changed default transaction close behavior from commit to rollback across all GLVs to align with embedded graph defaults.
Expand Down
111 changes: 111 additions & 0 deletions docs/src/dev/provider/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,117 @@ serialization and the registry handles inbound reconstruction.
For driver users consuming PDTs, see the <<gremlin-variants,Gremlin Variants>> reference documentation for
each language driver.

[[primitive-provider-defined-types]]
==== Primitive Provider Defined Types

When a custom type is best represented as a single opaque stringified value rather than a map of fields — for example,
an unsigned 32-bit integer, a WKT geometry string, or a provider-specific identifier — use a *Primitive PDT* instead of
a Composite PDT. A Primitive PDT carries only a type name and a string value; TinkerPop never parses or interprets the
value.

On the wire, a Primitive PDT is serialized as GraphBinary type `0xf1` (two fully-qualified Strings: type and value) or
GraphSON type `g:PrimitivePdt` (with an untyped JSON string value). In gremlin-lang, the literal form is
`PDT("name","value")` — an overload of the composite `PDT("name",[map])` literal where the second argument is a string
instead of a map.

A type may be registered as composite *or* primitive, not both — attempting to register a type for both forms throws an
error.

===== Adapter Precedence

A registered adapter always takes precedence over annotation/attribute-based handling on dehydration. This is consistent
across all GLVs: if an adapter is registered for a type, it controls serialization regardless of whether the class is
also annotated.

===== Java

Implement `PrimitivePDTAdapter<T>`:

[source,java]
----
public class Uint32Adapter implements PrimitivePDTAdapter<Uint32> {
@Override public String typeName() { return "mygraph:Uint32"; }
@Override public Class<Uint32> targetClass() { return Uint32.class; }
@Override public String toValue(Uint32 obj) { return Long.toUnsignedString(obj.getValue()); }
@Override public Uint32 fromValue(String value) { return new Uint32(Integer.parseUnsignedInt(value)); }
}
----

Register via ServiceLoader in the same way as composite adapters — add the fully qualified class name to
`META-INF/services/org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitivePDTAdapter`.

===== Python

Use `register_primitive` on the registry:

[source,python]
----
from gremlin_python.structure.graph import ProviderDefinedTypeRegistry

registry = ProviderDefinedTypeRegistry()
registry.register_primitive('mygraph:Uint32',
from_value=lambda s: Uint32(int(s)),
to_value=lambda obj: str(obj.value),
target_class=Uint32)
----

===== JavaScript

Use `registerPrimitive` with a `PrimitivePdtAdapter`:

[source,javascript]
----
const { ProviderDefinedTypeRegistry, PrimitivePdtAdapter } = require('gremlin');

const registry = new ProviderDefinedTypeRegistry();
registry.registerPrimitive('mygraph:Uint32', new PrimitivePdtAdapter(
(str) => new Uint32(parseInt(str)), // fromValue
(obj) => obj.value.toString() // toValue
), Uint32);
----

===== Go

Use `RegisterPrimitiveFuncs` or `RegisterPrimitiveFuncsWithType`:

[source,go]
----
registry := gremlingo.NewPDTRegistry()
registry.RegisterPrimitiveFuncsWithType("mygraph:Uint32", reflect.TypeOf(Uint32{}),
// hydrate: string -> Go type
func(value string) (interface{}, error) {
v, err := strconv.ParseUint(value, 10, 32)
return Uint32{Value: uint32(v)}, err
},
// dehydrate: Go type -> string
func(obj interface{}) (string, error) {
return strconv.FormatUint(uint64(obj.(Uint32).Value), 10), nil
},
)
----

===== .NET

Implement `IPrimitivePdtAdapter<T>`:

[source,csharp]
----
public class Uint32Adapter : IPrimitivePdtAdapter<Uint32>
{
public string TypeName => "mygraph:Uint32";
public Uint32 FromString(string value) => new Uint32(uint.Parse(value));
public string ToString(Uint32 obj) => obj.Value.ToString();
}
----

Register on the `ProviderDefinedTypeRegistry` in the same way as composite adapters.

===== Value Type

Across all GLVs, unhydrated primitive PDT values are represented as `PrimitiveProviderDefinedType` objects containing a
`name` (or `Name`) and a string `value` (or `Value`). When no adapter is registered for a given type name, the driver
returns this generic value object.

[[gremlin-plugins]]
== Gremlin Plugins

Expand Down
21 changes: 21 additions & 0 deletions docs/src/reference/gremlin-variants.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,27 @@ string is valid within the ANTLR grammar:

For example, a string value `say "hello"` would be serialized as `"say \"hello\""` in the canonical Gremlin string.

==== PDT Literals

The gremlin-lang grammar supports Provider Defined Type literals in two forms:

* Composite: `PDT("typeName",[key:"value",...])` — creates a `ProviderDefinedType` with a map of fields.
* Primitive: `PDT("typeName","value")` — creates a `PrimitiveProviderDefinedType` with an opaque string value.

Both forms share the `PDT(` prefix. The second argument determines the variant: a map literal produces a composite PDT,
a string literal produces a primitive PDT.

Examples:

[source,groovy]
----
// Composite PDT — a Point with x and y fields
g.inject(PDT("mygraph:Point",["x":1.0,"y":2.0]))

// Primitive PDT — an unsigned 32-bit integer as a stringified value
g.inject(PDT("mygraph:Uint32","4294967295"))
----

The following sections describe each language variant and driver that is officially TinkerPop a part of the project,
providing more detailed information about usage, configuration and known limitations.

Expand Down
14 changes: 14 additions & 0 deletions docs/src/upgrade/release-4.x.x.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,10 @@ effort to the old custom serializer approach but is entirely optional for basic
* <<gremlin-dotnet-pdt,Gremlin.Net>>
* <<gremlin-go-pdt,Gremlin-Go>>

PDTs come in two flavors: *Composite* (a type name plus a map of fields, for structured types) and *Primitive* (a type
name plus a single opaque string value, for types expressible as a single stringified value). The gremlin-lang grammar
supports both forms via the `PDT("name",[map])` and `PDT("name","value")` literals respectively.


=== Upgrading for Providers

Expand All @@ -565,6 +569,16 @@ benefiting all driver users transparently.
See <<provider-defined-types>> for full details on annotation usage, field filtering, nested types, and ServiceLoader
registration.

===== Primitive Provider Defined Types

In addition to composite PDTs (which carry a map of fields), providers can now expose *Primitive PDTs* — types
represented as a single opaque stringified value (GraphBinary type `0xf1`, GraphSON type `g:PrimitivePdt`). This is
ideal for types with no native TinkerPop representation that can be expressed as a single string, such as unsigned
integers or WKT geometry strings. Each GLV provides an adapter interface for primitive PDTs
(`PrimitivePDTAdapter` in Java, `register_primitive` in Python, `registerPrimitive` in JavaScript,
`RegisterPrimitiveFuncs` in Go, `IPrimitivePdtAdapter<T>` in .NET). A type may be registered as composite or
primitive, not both. See <<primitive-provider-defined-types>> for details.

==== Graph Driver Providers

== TinkerPop 4.0.0-beta.2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType;
import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
import org.apache.tinkerpop.gremlin.util.DatetimeHelper;

Expand Down Expand Up @@ -584,15 +585,20 @@ public Object visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
*/
@Override
public Object visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
final String name = (String) visitStringLiteral(ctx.stringLiteral());
final Map<String, Object> fields = new LinkedHashMap<>();
final Map<?, ?> rawMap = (Map<?, ?>) visitGenericMapLiteral(ctx.genericMapLiteral());
for (final Map.Entry<?, ?> entry : rawMap.entrySet()) {
if (!(entry.getKey() instanceof String))
throw new IllegalArgumentException("PDT fields map must have String keys, found: " + entry.getKey().getClass().getName());
fields.put((String) entry.getKey(), entry.getValue());
final String name = (String) visitStringLiteral(ctx.stringLiteral(0));
if (ctx.genericMapLiteral() != null) {
final Map<String, Object> fields = new LinkedHashMap<>();
final Map<?, ?> rawMap = (Map<?, ?>) visitGenericMapLiteral(ctx.genericMapLiteral());
for (final Map.Entry<?, ?> entry : rawMap.entrySet()) {
if (!(entry.getKey() instanceof String))
throw new IllegalArgumentException("PDT fields map must have String keys, found: " + entry.getKey().getClass().getName());
fields.put((String) entry.getKey(), entry.getValue());
}
return new ProviderDefinedType(name, fields);
} else {
final String value = (String) visitStringLiteral(ctx.stringLiteral(1));
return new PrimitiveProviderDefinedType(name, value);
}
return new ProviderDefinedType(name, fields);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import org.antlr.v4.runtime.ParserRuleContext;
import org.apache.tinkerpop.gremlin.language.grammar.GremlinParser;
import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType;
import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;

import java.math.BigDecimal;
Expand Down Expand Up @@ -208,6 +209,10 @@ public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {

@Override
public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
return anonymize(ctx, ProviderDefinedType.class);
if (ctx.genericMapLiteral() != null) {
return anonymize(ctx, ProviderDefinedType.class);
} else {
return anonymize(ctx, PrimitiveProviderDefinedType.class);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1209,11 +1209,19 @@ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx)

@Override
public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
sb.append("new ProviderDefinedType(");
sb.append(ctx.stringLiteral().getText());
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
if (ctx.genericMapLiteral() != null) {
sb.append("new ProviderDefinedType(");
sb.append(ctx.stringLiteral(0).getText());
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
} else {
sb.append("new PrimitiveProviderDefinedType(");
sb.append(ctx.stringLiteral(0).getText());
sb.append(", ");
sb.append(ctx.stringLiteral(1).getText());
sb.append(")");
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,19 @@ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx)

@Override
public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
sb.append("&gremlingo.ProviderDefinedType{Name: ");
visitStringLiteral(ctx.stringLiteral());
sb.append(", Fields: ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append("}");
if (ctx.genericMapLiteral() != null) {
sb.append("&gremlingo.ProviderDefinedType{Name: ");
visitStringLiteral(ctx.stringLiteral(0));
sb.append(", Fields: ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append("}");
} else {
sb.append("&gremlingo.PrimitiveProviderDefinedType{Name: ");
visitStringLiteral(ctx.stringLiteral(0));
sb.append(", Value: ");
visitStringLiteral(ctx.stringLiteral(1));
sb.append("}");
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,19 @@ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx)

@Override
public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
sb.append("new ProviderDefinedType(");
sb.append(ctx.stringLiteral().getText());
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
if (ctx.genericMapLiteral() != null) {
sb.append("new ProviderDefinedType(");
sb.append(ctx.stringLiteral(0).getText());
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
} else {
sb.append("new PrimitiveProviderDefinedType(");
sb.append(ctx.stringLiteral(0).getText());
sb.append(", ");
sb.append(ctx.stringLiteral(1).getText());
sb.append(")");
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,19 @@ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx)

@Override
public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
sb.append("new ProviderDefinedType(");
sb.append(ctx.stringLiteral().getText());
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
if (ctx.genericMapLiteral() != null) {
sb.append("new ProviderDefinedType(");
sb.append(ctx.stringLiteral(0).getText());
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
} else {
sb.append("new PrimitiveProviderDefinedType(");
sb.append(ctx.stringLiteral(0).getText());
sb.append(", ");
sb.append(ctx.stringLiteral(1).getText());
sb.append(")");
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,19 @@ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx)

@Override
public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
sb.append("new ProviderDefinedType(");
visitStringLiteral(ctx.stringLiteral());
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
if (ctx.genericMapLiteral() != null) {
sb.append("new ProviderDefinedType(");
visitStringLiteral(ctx.stringLiteral(0));
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
} else {
sb.append("new PrimitiveProviderDefinedType(");
visitStringLiteral(ctx.stringLiteral(0));
sb.append(", ");
visitStringLiteral(ctx.stringLiteral(1));
sb.append(")");
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,19 @@ public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx)

@Override
public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
sb.append("ProviderDefinedType(");
visitStringLiteral(ctx.stringLiteral());
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
if (ctx.genericMapLiteral() != null) {
sb.append("ProviderDefinedType(");
visitStringLiteral(ctx.stringLiteral(0));
sb.append(", ");
visitGenericMapLiteral(ctx.genericMapLiteral());
sb.append(")");
} else {
sb.append("PrimitiveProviderDefinedType(");
visitStringLiteral(ctx.stringLiteral(0));
sb.append(", ");
visitStringLiteral(ctx.stringLiteral(1));
sb.append(")");
}
return null;
}

Expand Down
Loading