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 .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-*
*.iml
.idea
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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 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()`.
* Added streaming HTTP response support to `gremlin-driver` for incremental result deserialization over GraphBinary.
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
207 changes: 207 additions & 0 deletions docs/src/dev/provider/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,213 @@ 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 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.

==== 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> toProperties(java.awt.Color color) {
return Map.of("r", color.getRed(), "g", color.getGreen(),
"b", color.getBlue(), "a", color.getAlpha());
}

@Override
public java.awt.Color fromProperties(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.build();
registry.register(Point.class, Address.class, Person.class);
----

Adapter types (for classes you don't own) are discovered automatically via `ServiceLoader` when using
`ProviderDefinedTypeRegistry.build()`. 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
----

===== 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.Build()`
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 `Build()` 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: (props) => new Point(props.x, props.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 build() {
ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.build(); // 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.build());
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.

[[gremlin-plugins]]
== Gremlin Plugins

Expand Down
Loading
Loading