Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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 .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
**/*.log
**/target
!gremlin-server/target/apache-tinkerpop-gremlin-server-*
!gremlin-server/target/gremlin-server-*-tests.jar
!gremlin-console/target/apache-tinkerpop-gremlin-console-*
!gremlin-tools/gremlin-socket-server/target/gremlin-socket-server-*
!gremlin-tools/gremlin-socket-server/target/libs
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Fixed `ByteBuf` leak in `GraphBinaryMessageSerializerV4` when serialization throws an `IOException`.
* 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 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
1 change: 1 addition & 0 deletions docker/gremlin-test-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ USER root
RUN mkdir -p /opt
WORKDIR /opt
COPY gremlin-server/src/test /opt/test/
COPY gremlin-server/target/gremlin-server-*-tests.jar /opt/gremlin-server/lib/
COPY docker/gremlin-server/docker-entrypoint.sh docker/gremlin-server/*.yaml docker/gremlin-server/*.conf /opt/
RUN chmod 755 /opt/docker-entrypoint.sh

Expand Down
219 changes: 219 additions & 0 deletions docs/src/dev/provider/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,225 @@ can be used as a reference on how these files can be used and its
link:https://github.com/apache/tinkerpop/blob/x.y.z/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/Model.java[model]
shows the Java representation of those files.

[[provider-defined-types]]
=== Provider Defined Types (PDT)

Provider Defined Types allow graph providers to expose custom types that drivers can serialize and deserialize without
manual configuration on the client side. A provider annotates a class (or registers an adapter for a class it doesn't
own), and the type flows through the wire protocol automatically. Clients receive PDT values as structured objects they
can use directly or hydrate into language-native types.

==== Basic Usage

Annotate a class with `@ProviderDefined` from the `org.apache.tinkerpop.gremlin.structure.io.pdt` package:

[source,java]
----
import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefined;

@ProviderDefined(name = "mygraph:Point")
public class Point {
public double x;
public double y;

public Point(double x, double y) {
this.x = x;
this.y = y;
}
}
----

The `name` attribute is a unique identifier for the type. It must not be null or empty — all GLVs reject an
empty name when a type is defined or a `ProviderDefinedType` is constructed. It is strongly recommended to namespace
type names using your graph's identifier as a prefix (e.g. `"mygraph:Point"`). This avoids collisions when clients
interact with multiple providers and makes the origin of a type immediately clear. By default, all fields are included.
Use `includedFields` or `excludedFields` to control which fields are serialized:

[source,java]
----
@ProviderDefined(name = "mygraph:Point", includedFields = {"x", "y"})
public class Point { ... }

// or exclude specific fields
@ProviderDefined(name = "mygraph:Person", excludedFields = {"internalId"})
public class Person { ... }
----

NOTE: For annotation-based round-trip hydration (see <<round-trip-support>>), an annotated class must expose a no-arg
constructor and the mapped fields must be directly settable (e.g. public fields). Classes that cannot meet these
requirements — for example those with immutable `final` fields or no default constructor — should instead use a
`ProviderDefinedTypeAdapter` (see <<adapter-for-types-you-don-t-own>>), which gives full control over construction.

The serialized field set becomes a map whose keys are always strings. In statically typed GLVs (Java, .NET,
Go, and TypeScript) this is enforced by the type system; in Python the keys are validated at runtime and a
`TypeError` is raised for any non-string key.

==== Nested Types

PDT supports nested custom types. Each nested type must also be annotated:

[source,java]
----
@ProviderDefined(name = "mygraph:Address")
public class Address {
public String street;
public String city;
}

@ProviderDefined(name = "mygraph:Person")
public class Person {
public String name;
public Address address;
}
----

When serialized, the `address` field is itself encoded as a PDT value.

[[adapter-for-types-you-don-t-own]]
==== Adapter for Types You Don't Own

For classes you cannot annotate (e.g. `java.awt.Color`), implement `ProviderDefinedTypeAdapter<T>`:

[source,java]
----
import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeAdapter;

public class ColorAdapter implements ProviderDefinedTypeAdapter<java.awt.Color> {

@Override
public String typeName() { return "mygraph:Color"; }

@Override
public Class<java.awt.Color> targetClass() { return java.awt.Color.class; }

@Override
public Map<String, Object> toFields(java.awt.Color color) {
return Map.of("r", color.getRed(), "g", color.getGreen(),
"b", color.getBlue(), "a", color.getAlpha());
}

@Override
public java.awt.Color fromFields(Map<String, Object> fields) {
return new java.awt.Color((int) fields.get("r"), (int) fields.get("g"),
(int) fields.get("b"), (int) fields.get("a"));
}
}
----

[[round-trip-support]]
==== Round-Trip Support (Dehydration and Hydration)

There is an important distinction between *dehydration* (serializing a type for sending) and *hydration* (deserializing
a received PDT back into a language-native type).

*Dehydration* is handled automatically for `@ProviderDefined`-annotated classes and adapter-registered types. When a
user passes an annotated object into a Gremlin traversal or script, TinkerPop converts it to a PDT on the wire
without any extra configuration.

*Hydration* — reconstructing an incoming PDT back into the original typed object — requires the driver to know which
class corresponds to a given PDT name. Without this mapping, the driver will return a generic `ProviderDefinedType`
object. To enable automatic round-trip hydration, providers must expose a pre-configured `ProviderDefinedTypeRegistry`
to users. How that registry is populated differs by language:

===== Java

Register annotated classes explicitly with the registry. `register(Class<?>...)` inspects the `@ProviderDefined`
annotation to derive the type name and field mapping automatically:

[source,java]
----
ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.create();
registry.register(Point.class, Address.class, Person.class);
----

Adapter types (for classes you don't own) are discovered automatically via `ServiceLoader` when using
`ProviderDefinedTypeRegistry.create()`. Register them by adding a file at:

----
META-INF/services/org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeAdapter
----

with the fully qualified class name of each adapter:

----
com.example.graph.ColorAdapter
----

NOTE: Annotation-based hydration reflectively constructs and populates the annotated class
(`Constructor.setAccessible(true)` and `Field.set()` in `AnnotatedTypeAdapter`). Under JPMS, the provider's module
must therefore `opens` its PDT package to `org.apache.tinkerpop.gremlin.core` (e.g.
`opens com.example.graph.types to org.apache.tinkerpop.gremlin.core;` in `module-info.java`, or the equivalent
`--add-opens` JVM flag), otherwise the JVM throws `InaccessibleObjectException` at hydration time. Providers using a
`ProviderDefinedTypeAdapter` with explicit `fromFields` logic do not rely on reflection and are unaffected.

===== Python

Hydration is fully automatic for `@provider_defined`-decorated classes. The decorator registers the class at
definition time (import time), so any annotated type round-trips without any additional setup.

===== .NET

`[ProviderDefined]`-annotated types are discovered automatically. Calling `ProviderDefinedTypeRegistry.Create()`
scans all loaded assemblies for `[ProviderDefined]`-annotated types and registers them for hydration. No extra
configuration is needed — providers simply annotate their types and users call `Create()` to create the registry.

===== JavaScript

Register hydration adapters explicitly on a `ProviderDefinedTypeRegistry` instance, then pass it to the connection:

[source,javascript]
----
const registry = new ProviderDefinedTypeRegistry();
registry.register('mygraph:Point', {
serialize: (obj) => ({ x: obj.x, y: obj.y }),
deserialize: (fields) => new Point(fields.x, fields.y)
}, Point);
----

===== Go

Register types on a `PDTRegistry` instance. Go supports either reflection-based registration (using `pdt` struct
tags) or explicit function registration:

[source,go]
----
registry := NewPDTRegistry()
registry.RegisterType("mygraph:Point", reflect.TypeOf(Point{}))
----

===== Provider Factory Pattern

Regardless of language, the recommended pattern is for providers to expose a factory method that returns a
pre-configured `ProviderDefinedTypeRegistry`. This shields end users from needing to know which types exist or how
the registry is populated:

[source,java]
----
// In the provider's client library
public class MyGraphTypeRegistry {
public static ProviderDefinedTypeRegistry create() {
ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.create(); // discovers ServiceLoader adapters
registry.register(Point.class, Address.class, Person.class); // registers annotated types
return registry;
}
}
----

End users configure their connection in one line:

[source,java]
----
DriverRemoteConnection conn = DriverRemoteConnection.using(cluster);
conn.setPdtRegistry(MyGraphTypeRegistry.create());
GraphTraversalSource g = traversal().with(conn);
----

With this in place, `Point` objects round-trip transparently in both directions — the annotation handles outbound
serialization and the registry handles inbound reconstruction.

For driver users consuming PDTs, see the <<gremlin-variants,Gremlin Variants>> reference documentation for
each language driver.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add contraints and error behaviour section of some sort? I think something for property map keys must be String and the type name must be non-empty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've folded details about the String keys and non-empty field names into the basic usage section.

[[gremlin-plugins]]
== Gremlin Plugins

Expand Down
Loading
Loading