Skip to content

Implement PrimitivePDT#3485

Open
Cole-Greer wants to merge 17 commits into
masterfrom
simplePDT
Open

Implement PrimitivePDT#3485
Cole-Greer wants to merge 17 commits into
masterfrom
simplePDT

Conversation

@Cole-Greer

Copy link
Copy Markdown
Contributor

Summary

Implements PrimitivePDT, the provider-defined primitive type reserved in the
GraphBinary (0xF1) and GraphSON (g:PrimitivePdt) V4 specs, completing the
Provider Defined Types feature begun with CompositePDT in #3433.

A PrimitivePDT represents a value as a single opaque stringified value plus a
provider type name — for types that have no native TinkerPop representation but can
be expressed as one string (e.g. an unsigned 32-bit integer, a WKT geometry, a
provider-specific identifier). It complements CompositePDT (a type name + a map of
fields) for structured types.

Wire/text forms:

  • GraphBinary 0xF1: {type}{value} — two fully-qualified Strings; the value is
    opaque and never parsed by TinkerPop.
  • GraphSON g:PrimitivePdt: {"type": "...", "value": "..."} with an untyped
    string value (response-only in V4).
  • gremlin-lang: PDT("name", "value") — an overload of the composite
    PDT("name", [map]) literal (the second argument's type selects the variant; this
    is unambiguous to the parser).

Provider integration is adapter-based: a provider registers a primitive adapter
(toValue/fromValue) so the driver auto-dehydrates a native object on send and
auto-hydrates it on receive, with no per-call configuration. A registered adapter
takes precedence over the @ProviderDefined annotation / [ProviderDefined]
attribute on dehydration, consistent across GLVs.

What's included

gremlin-core / gremlin-util (Java)

  • PrimitiveProviderDefinedType value type and PrimitiveProviderDefinedTypeSerializer
    (DataType.PRIMITIVE_PDT 0xF1).
  • New PrimitivePDTAdapter; extracted a common ProviderDefinedTypeAdapter supertype
    with CompositePDTAdapter / PrimitivePDTAdapter implementations and explicit
    composite/primitive registry accessors.
  • Registry hydration (incl. primitive-nested-in-composite), GraphBinary reader/writer
    dispatch, and GraphSON g:PrimitivePdt ser/deser.
  • gremlin-lang grammar overload + all translator visitors + GremlinLang text emission.
  • Prerequisite fix: made the GraphBinary/GraphSON write path registry-aware so a
    type registered via an adapter (but not annotated) can be serialized on the
    server→client response path — a pre-existing CompositePDT gap.

GLVs — full support in Python, JavaScript, Go, .NET (and the Java driver):
value type, 0xF1 serializer, registry primitive path, gremlin-lang PDT("name","value")
emission + adapter auto-dehydration, and client wiring (reusing the existing PDT
registry plumbing).

Server — test fixtures (Uint32, TinkerId, and a composite Measurement
containing a primitive) and end-to-end integration coverage.

Docs — provider guide section, gremlin-lang literal reference, upgrade note, and
CHANGELOG entry. The GraphBinary/GraphSON spec sections already matched the
implementation.

Key design decisions

  • Adapter-only provider integration for primitives (no annotation path); a common
    adapter supertype unifies discovery while keeping composite/primitive distinct.
  • Grammar overload of PDT(...) rather than a new keyword, following the existing
    literal-alternation convention.
  • Opaque value: TinkerPop never parses the value; round-trip fidelity is preserved
    (leading zeros, large/non-numeric values), with the provider adapter owning
    parse/format.
  • Adapter precedence over annotation/attribute on dehydration, matching the request
    and response paths.

Testing

  • Unit tests across all languages (serializer round-trip incl. opaque-value
    fidelity, registry hydration with graceful degradation, dual-registration rejection,
    gremlin-lang text emission, adapter-over-annotation/attribute precedence).
  • Per-GLV integration tests against the Docker test server, covering the
    unregistered/raw round-trip, registered auto de/hydration, and
    primitive-nested-in-composite: Java, Python, JavaScript, Go, .NET.

VOTE +1

Fixes the CompositePDT (0xF0) response-path gap where a provider type
registered via a ProviderDefinedTypeAdapter but not annotated with
@ProviderDefined could not be serialized when returned to the client.

GraphBinaryWriter now accepts an optional ProviderDefinedTypeRegistry
(parallel to GraphBinaryReader). When a class has no direct serializer
and is not @ProviderDefined-annotated, the writer consults the registry
by class and dehydrates via the adapter (adapter.toFields) into a
ProviderDefinedType. Annotation-based auto-conversion is unchanged;
adapter lookup is an additional resolution path.

The GraphSON write path gains the same capability via
PdtGraphSONSerializerProviderV4, which returns an adapter-based
serializer for registry-registered classes. The registry is threaded
through GraphBinaryMessageSerializerV4 and AbstractGraphSONMessageSerializerV4.

The prior gap-validation test (which asserted the failure) is replaced by
GraphBinaryWriterPdtTest#shouldDehydrateRegisteredButUnannotatedTypeViaAdapterOnWritePath,
asserting a successful adapter round-trip.

tinkerpop-lka

Assisted-by: Kiro:claude-opus-4.8
…r to CompositePDTAdapter

Behavior-preserving refactor that prepares the PDT adapter SPI for the
upcoming PrimitivePDT (0xF1) work by introducing a common supertype.

- ProviderDefinedTypeAdapter<T> is now a thin common supertype exposing
  only typeName() and targetClass().
- New CompositePDTAdapter<T> extends it with the composite-specific
  toFields(T)/fromFields(Map) methods.
- ProviderDefinedTypeRegistry stores composite adapters as
  CompositePDTAdapter; register(...) accepts the supertype and routes
  composite adapters via instanceof; create() discovers adapters via
  ServiceLoader on the supertype so a single service file can list any
  adapter kind. AnnotatedTypeAdapter now implements CompositePDTAdapter.
- Updated composite toFields call sites in GremlinLang and the lka
  write-path code (GraphBinaryWriter, PdtGraphSONSerializersV4) to use
  CompositePDTAdapter.
- Updated test fixtures (TestPointAdapter and others) to implement
  CompositePDTAdapter.

No primitive (0xF1) logic is introduced here. Composite behavior is
unchanged; all composite PDT tests pass.

tinkerpop-2gy.1

Assisted-by: Kiro:claude-opus-4.8
Introduces the core value type for PrimitivePDT and its GraphBinary
serialization, mirroring the composite ProviderDefinedType.

- PrimitiveProviderDefinedType: immutable (String name, String value).
  name non-null/non-empty; value non-null with empty string allowed and
  null rejected; equals/hashCode on (name, value); toString pdt[name](value);
  transient withHydrated/getHydrated for symmetry. The value is opaque and
  is never parsed or normalized.
- PrimitiveProviderDefinedTypeSerializer: SimpleTypeSerializer over
  DataType.PRIMITIVE_PDT writing/reading two fully-qualified Strings
  ({name}{value}).
- DataType: add PRIMITIVE_PDT(0xF1).
- TypeSerializerRegistry: register PrimitiveProviderDefinedType ->
  PrimitiveProviderDefinedTypeSerializer.

Adapter/registry hydration, reader/writer dehydration dispatch, GraphSON,
grammar, and GLVs are deferred to later beads.

Tests: PrimitiveProviderDefinedTypeTest (validation, equals/hashCode/toString)
and PrimitiveProviderDefinedTypeSerializerTest (round-trip incl. opaque-value
fidelity: leading zeros, large and non-numeric values).

tinkerpop-2gy.2

Assisted-by: Kiro:claude-opus-4.8
… dispatch

Wires PrimitivePDT into the registry and the GraphBinary read/write paths.

- New PrimitivePDTAdapter<T> extends ProviderDefinedTypeAdapter<T> with
  toValue(T)/fromValue(String).
- ProviderDefinedTypeRegistry: parallel primitiveAdaptersByName/ByClass;
  register(...) routes PrimitivePDTAdapter into the primitive maps;
  getPrimitiveAdapterByName/ByClass added. Registering a class already
  registered under the other kind (composite vs primitive) throws
  (fail-fast, bidirectional). ServiceLoader discovery on the supertype
  picks up primitive adapters automatically.
- hydratePrimitive(PrimitiveProviderDefinedType): adapter lookup by name +
  fromValue, with graceful degradation (log + return raw) on missing or
  throwing adapter. The composite hydrate() recursion now also hydrates a
  PrimitiveProviderDefinedType nested inside a composite's fields.
- GraphBinaryReader.read(): hydrate a deserialized PrimitiveProviderDefinedType
  via the registry.
- GraphBinaryWriter: dehydrate a raw object whose class has a registered
  PrimitivePDTAdapter into a PrimitiveProviderDefinedType (parallel to the
  composite adapter path), resolving the PRIMITIVE_PDT serializer.

GraphSON, grammar, server fixtures, and GLVs remain for later beads.

Tests: registry primitive register/lookup, hydratePrimitive success and
graceful degradation, dual-registration throws, primitive-nested-in-composite
hydration; writer adapter round-trip for an unannotated primitive type.

tinkerpop-2gy.3

Assisted-by: Kiro:claude-opus-4.8
Adds GraphSON V4 support for PrimitiveProviderDefinedType under the
g:PrimitivePdt type tag.

- PrimitiveProviderDefinedTypeJacksonSerializer emits
  {"type": <name>, "value": <value>} with value as an untyped JSON string
  (per the GraphSON spec; the value is the opaque stringified primitive).
- PrimitiveProviderDefinedTypeJacksonDeserializer parses type/value and
  hydrates via the ProviderDefinedTypeRegistry when set.
- GraphSONModule (V4) maps PrimitiveProviderDefinedType -> "PrimitivePdt",
  registers the ser/deser, and threads the registry to the primitive
  deserializer via setPdtRegistry.
- Write-side adapter fallback (PdtGraphSONSerializerProviderV4 /
  GraphSONTypeIdResolver) extended so a raw object with a registered
  PrimitivePDTAdapter serializes as g:PrimitivePdt.

Response-only in T4; both directions implemented for round-trip tests.

Tests: PdtGraphSONSerializersV4Test extended with g:PrimitivePdt
serialize/deserialize, registry hydration, and primitive-nested-in-composite.

tinkerpop-2gy.4

Assisted-by: Kiro:claude-opus-4.8
…ng support

Adds the gremlin-lang text form for PrimitivePDT as an overload of the
existing PDT(...) literal: PDT("name", "value").

- Gremlin.g4: pdtLiteral gains a second unlabeled alternative
  (K_PDT LPAREN stringLiteral COMMA stringLiteral RPAREN). Unambiguous with
  the composite map form ('[' vs quote on the 2nd arg). Reuses K_PDT.
- GenericLiteralVisitor.visitPdtLiteral branches on genericMapLiteral != null;
  the primitive form builds PrimitiveProviderDefinedType(name, value).
- GremlinLang.argAsString emits PDT(<name>,<value>) for
  PrimitiveProviderDefinedType and auto-dehydrates classes with a registered
  PrimitivePDTAdapter.
- All translator visitors (Java, Groovy, Python, Javascript, Go, DotNet,
  Anonymized) emit the language-native primitive construction; the base
  TranslateVisitor passthrough covers both forms. Composite branches updated
  from stringLiteral() to stringLiteral(0) for the new list accessor.

GLV runtime libraries are handled in later beads.

Tests: GeneralLiteralVisitorTest (parse primitive + composite still works),
GremlinLangTest (round-trip + auto-dehydration), GremlinTranslatorTest
(per-language primitive emission).

tinkerpop-2gy.5

Assisted-by: Kiro:claude-opus-4.8
PrimitivePDT reuses the same ProviderDefinedTypeRegistry that composite
already threads through the Java serialization stack, so no production
wiring changes were required. Adds a driver-level test proving end-to-end
behavior at the serializer/registry level (no live server):

- raw adapter-registered object round-trips through the GraphBinary message
  serializer (request dehydration + response hydration);
- a raw PrimitiveProviderDefinedType round-trips over GraphBinary;
- GraphSON response path hydrates an adapter-registered primitive;
- full GraphBinary request/response cycle hydrates back to the typed object.

tinkerpop-2gy.6

Assisted-by: Kiro:claude-opus-4.8
Adds gremlin-server test fixtures and end-to-end integration coverage for
PrimitivePDT, mirroring the composite fixtures.

- Uint32 (long-backed unsigned 32-bit) + Uint32Adapter (PrimitivePDTAdapter;
  toValue=Long.toUnsignedString, fromValue parses with 0..4294967295 range
  validation).
- TinkerId (String-backed, non-numeric) + TinkerIdAdapter, proving the
  adapter generalizes beyond numbers.
- Measurement: a @ProviderDefined composite fixture containing a Uint32
  field, exercising primitive-nested-in-composite.
- Primitive adapters registered via the unified
  META-INF/services/...ProviderDefinedTypeAdapter file so the server/test
  registry discovers them through ServiceLoader.
- GremlinServerPrimitivePdtIntegrateTest: injects PDT("Uint32","..."),
  PDT("TinkerId","..."), a Measurement containing a nested Uint32, and a
  collection, asserting correct round-trip/hydration.

tinkerpop-2gy.7

Assisted-by: Kiro:claude-opus-4.8
Implements PrimitivePDT in the Python GLV, mirroring the composite support.

- structure/graph.py: PrimitiveProviderDefinedType(name, value) and registry
  support for primitive adapters (register + hydrate_primitive with graceful
  degradation); reuses the existing pdt_registry threading.
- structure/io/graphbinaryV4.py: DataType.primitive_pdt=0xf1 and
  PrimitiveProviderDefinedTypeIO (writes/reads two fully-qualified Strings);
  reader hydration dispatch for PrimitiveProviderDefinedType, including
  primitive-nested-in-composite.
- driver/serializer.py: primitive registry threaded through the same
  pdt_registry path as composite.

GraphSON read support is intentionally omitted: the gremlin-python driver is
GraphBinary-only for V4 (no GraphSON V4 deserializer exists), so there is no
g:PrimitivePdt read path to add. Clients send PrimitivePDT as the gremlin-lang
PDT("name","value") literal and receive it via GraphBinary.

Tests: 40 passing (GraphBinary round-trip incl. opaque-value fidelity,
registry hydration, primitive-nested-in-composite), 3 pre-existing
entry_points skips.

tinkerpop-2gy.8

Assisted-by: Kiro:claude-opus-4.8
…upport, registry naming, and integration coverage

Outcome of the post-implementation review pass over the PrimitivePDT work.
Touches several already-merged beads; details and departures below.

Production fixes:
- GraphBinaryWriter.dehydrateToPdt now prefers a registered adapter over the
  @ProviderDefined annotation, matching the documented precedence in
  GremlinLang.argAsString ("a registered adapter takes priority"). Previously
  the binary write path preferred the annotation, diverging from the request
  path. (refs tinkerpop-lka, tinkerpop-2gy.3)
- gremlin-python: GremlinLang._arg_as_string did not handle
  PrimitiveProviderDefinedType, raising TypeError when a primitive PDT was used
  as a traversal argument. Added the PDT("name","value") text emission and
  primitive-adapter auto-dehydration (primitive checked before composite),
  mirroring the Java side. This gap shipped in the Python GLV bead and was
  caught by the new integration tests. (refs tinkerpop-2gy.8)
- Renamed the composite adapter accessors/maps to be explicit about "composite"
  in both Java (getCompositeAdapterByName/ByClass, compositeAdaptersBy*) and
  Python (get_composite_adapter_by_class, _composite_adapters_by_*), now that a
  parallel primitive set exists. (refs tinkerpop-2gy.1, tinkerpop-2gy.3)

Test coverage added (these gaps allowed the above bugs to ship):
- GraphBinaryWriterPdtTest: precedence regression test asserting a registered
  adapter wins over @ProviderDefined on the binary write path.
- GremlinDriverIntegrateTest: PrimitivePDT traversal-API integration tests
  covering the unregistered base case, registered auto de/hydration, and the
  registered nested (composite-containing-primitive) case. Reuses the
  server-side Uint32/Uint32Adapter/Measurement fixtures rather than
  duplicating them. (refs tinkerpop-2gy.6, tinkerpop-2gy.7)
- gremlin-python: traversal-API integration tests (raw/unregistered, registered
  hydration, in-collection, nested-in-composite) mirroring the composite suite.
  (refs tinkerpop-2gy.8)

Departure note: the gremlin-python GLV (tinkerpop-2gy.8) does not add a GraphSON
g:PrimitivePdt read path because the Python driver is GraphBinary-only for V4;
an orphaned GraphSON reader added during implementation was removed.

Assisted-by: Kiro:claude-opus-4.8
Implements PrimitivePDT in the JavaScript GLV, mirroring composite support
and applying the review lessons from the Python GLV.

- PrimitiveProviderDefinedType (name, value) in structure/graph.ts.
- PrimitivePDTSerializer replaces the prior StubSerializer for
  DataType.PRIMITIVEPDT (0xf1): writes/reads two fully-qualified Strings.
- ProviderDefinedTypeRegistry gains an explicit primitive adapter path
  (registerPrimitive / getPrimitiveAdapterByClass / hydratePrimitive),
  mirroring the composite/primitive split used in Java/Python.
- gremlin-lang text serialization emits PDT("name","value") for a
  PrimitiveProviderDefinedType and auto-dehydrates classes with a registered
  primitive adapter (primitive checked before composite). This is the
  client-side text path that was the Python gap; covered by unit tests here.
- Client/connection reuse the existing pdtRegistry option.

No GraphSON g:PrimitivePdt read path is added (consistent with the JS driver's
GraphBinary-based V4 response handling; nothing fabricated).

Tests: unit tests (serializer round-trip incl. opaque-value fidelity, registry
hydration, gremlin-lang PDT text emission) — full unit suite passing
(21082 tests). Integration tests (raw/unregistered, registered de/hydration,
nested-in-composite) pass against the test server: 4/4.

tinkerpop-2gy.9

Assisted-by: Kiro:claude-opus-4.8
Implements PrimitivePDT in the Go GLV, mirroring composite support and
applying the review lessons from the Python GLV.

- PrimitiveProviderDefinedType struct {Name, Value} in providerDefinedType.go.
- primitivePDTType (0xf1): type resolution for *PrimitiveProviderDefinedType,
  primitivePdtWriter (two fully-qualified Strings), writer-map entry, and
  readPrimitivePDT + deserializer switch case.
- PDTRegistry gains an explicit primitive adapter path
  (RegisterPrimitiveFuncs[WithType], HydratePrimitive) alongside composite.
- gremlin-lang text translation (gremlinlang.go) emits PDT("name","value") for
  *PrimitiveProviderDefinedType and auto-dehydrates values whose type has a
  registered primitive adapter (primitive checked before composite) — the
  client-side text path that was the Python gap, unit-tested here.
- Client wiring reuses the existing PDTRegistry path.

No GraphSON g:PrimitivePdt read path added (consistent with the Go driver's
GraphBinary-based V4 response handling).

Tests: unit tests for serializer/deserializer round-trip (incl. opaque-value
fidelity: leading zeros, large/non-numeric/empty), gremlin-lang text emission,
adapter dehydration with primitive-over-composite precedence, and registry
hydration (no-adapter raw, error->raw, nil, nested-in-composite) — all passing.
Integration tests (unregistered, registered de/hydration, nested, in-collection)
pass against the test server: PASS.

tinkerpop-2gy.10

Assisted-by: Kiro:claude-opus-4.8
Implements PrimitivePDT in the .NET GLV, mirroring composite support and
applying the review lessons from the Python GLV.

- PrimitiveProviderDefinedType (Name, Value) + IPrimitivePdtAdapter<T>
  (TypeName/FromString/ToString) in Structure/.
- DataType.PrimitivePDT (0xF1) enabled; PrimitivePDTSerializer writes/reads
  two fully-qualified Strings; registered in TypeSerializerRegistry.
- ProviderDefinedTypeRegistry gains an explicit primitive adapter path
  (register + GetPrimitiveAdapterByType + HydratePrimitive), mirroring the
  composite/primitive naming split used across the other GLVs.
- GraphBinaryReader hydrates PrimitiveProviderDefinedType via the registry.
- GremlinLang text translation emits PDT("name","value") for a
  PrimitiveProviderDefinedType and auto-dehydrates registered types — the
  client-side text path that was the Python gap.
- ADAPTER-OVER-ATTRIBUTE precedence: a registered adapter takes priority over
  the [ProviderDefined] attribute on dehydration (matching the Java/Python fix
  in ef194e3), applied in the text path.
- Client wiring reuses the existing SetPdtRegistry / GremlinClient /
  DriverRemoteConnection path.

No GraphSON g:PrimitivePdt read path added (consistent with the .NET driver's
GraphBinary-based V4 response handling).

Tests: 57 unit tests pass (serializer round-trip incl. opaque-value fidelity,
registry hydration, gremlin-lang text emission, adapter-over-attribute
precedence). Integration tests (raw, opaque value, in-collection,
nested-in-composite, registered) pass against the test server: 6/6.

tinkerpop-2gy.11

Assisted-by: Kiro:claude-opus-4.8
…note, CHANGELOG

Documentation for the PrimitivePDT (0xF1 / g:PrimitivePdt) feature.

- docs/src/dev/provider/index.asciidoc: new "Primitive Provider Defined Types"
  section (anchor primitive-provider-defined-types) — when to use a primitive
  PDT (a single opaque stringified value for types with no native TinkerPop
  representation, e.g. unsigned integers), per-GLV adapter registration
  (Java PrimitivePDTAdapter, Python register_primitive, JS registerPrimitive,
  Go RegisterPrimitiveFuncs, .NET IPrimitivePdtAdapter), and the
  adapter-over-annotation/attribute precedence.
- docs/src/reference/gremlin-variants.asciidoc: document both PDT literal forms
  — composite PDT("name",[map]) and primitive PDT("name","value").
- docs/src/upgrade/release-4.x.x.asciidoc: PrimitivePDT upgrade note.
- CHANGELOG.asciidoc: entry in the current unreleased section.

The GraphBinary (0xf1, {type}{value} two fully-qualified Strings) and GraphSON
(g:PrimitivePdt, untyped string value) spec sections already matched the
implementation and required no changes.

tinkerpop-2gy.13

Assisted-by: Kiro:claude-opus-4.8
…nsient, clarify JS PDT quoting

Minor cleanups from the tinkerpop-2gy code review (no behavioral change):

- PrimitiveProviderDefinedType: document why hydrated is excluded from
  equals/hashCode (mirroring ProviderDefinedType) and drop the no-op
  `transient` modifier (the type is not Serializable), aligning with the
  composite POJO's field declaration.
- gremlin-javascript gremlin-lang: comment that PDT literals deliberately use
  double-quoted strings (consistent with the composite PDT form), with
  JSON.stringify handling escaping.

Verified: gremlin-core PrimitiveProviderDefinedTypeTest 10/0/0; gremlin-javascript
unit (gremlin-lang + pdt-registry) 228 passing.

tinkerpop-2gy

Assisted-by: Kiro:claude-opus-4.8
Go natively serializes the built-in uint32 (emitted as a Gremlin Long), and
that native type-switch in argAsString runs before the PDT registry lookup, so
a bare uint32 never reaches a primitive adapter. The nested integration test
therefore defines a distinct named type (type myUint32 uint32) to opt into PDT
handling. Added a comment explaining this and contrasting with .NET (where
System.UInt32 is not natively serialized and can be registered directly).

Filed tinkerpop-kof to decide whether a registered adapter should take
precedence over native type serialization across all GLVs.

tinkerpop-2gy

Assisted-by: Kiro:claude-opus-4.8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant