Skip to content

Serve the v3 metadata API as v2's structure under v3 names - #504

Closed
em3s wants to merge 3 commits into
mainfrom
feat/v3-metadata-surface-as-v2-renamed
Closed

em3s wants to merge 3 commits into
mainfrom
feat/v3-metadata-surface-as-v2-renamed

Conversation

@em3s

@em3s em3s commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

/graph/v3/.../metadata is already a façade over the v2 DDL services — there is no v3 metastore behind it. But the façade did not only rename v2's vocabulary, it redesigned the payload: the table kind became a Jackson discriminator on schema, direction/indexes/groups/caches moved inside schema, a multi-edge id was hoisted out of _id and a vertex id out of src, type values became lowercase, and the v2 response envelopes were dropped.

The consequence is that a v2 payload cannot be carried to v3 by renaming keys. Everything downstream that speaks metadata — the Go CLI's hand-written structs, the ops scripts' raw JSON parsers, the console's table form, the DDL fixtures — has to know two shapes and translate between them. That cost buys nothing: the two describe the same stored entity.

This makes the v3 surface v2's structure with v3's names, so the dialects differ by key names alone.

desc          -> comment            dirType     -> direction
schema.src    -> schema.source      indices     -> indexes
schema.tgt    -> schema.target      .name       -> .index / .field
schema.fields -> schema.properties  mode IGNORE -> mode DROP
INDEXED -> EDGE, IMMUTABLE_INDEXED -> IMMUTABLE_EDGE
name "a.b" -> database + table
POST /graph/v3/databases/{database}/tables
{
  "table": "user_like_item_v1",
  "type": "EDGE",
  "schema": {
    "source": { "type": "LONG", "comment": "user id" },
    "target": { "type": "STRING", "comment": "item id" },
    "properties": [{ "name": "rating", "type": "INT", "nullable": true, "comment": "rating" }]
  },
  "direction": "BOTH",
  "storage": "datastore://ns/user_like_item_v1",
  "indexes": [{ "index": "created_at_desc", "fields": [{ "field": "created_at", "order": "DESC" }] }],
  "groups": [],
  "caches": [],
  "mode": "SYNC",
  "comment": "user likes item"
}

Type values stay uppercase because the surface now carries v2's own DataType and VertexType rather than mapping through PrimitiveType. That also stops DECIMAL collapsing into OBJECT on the way through, which the old converter did in one direction and could not undo in the other.

A multi-edge keeps its id as the _id property and a vertex keeps its id in source, exactly as v2 stores them. Hoisting them read better, but it is what made the two shapes non-mechanical to convert, which is the thing being fixed. Listings answer {count, content} and mutations {status, result, message} — the same DdlPage and DdlStatus v2 serves. The v2-only event and readOnly flags leave the surface; the server derives them, as it already did.

V3MetadataConverter is now a rename in both directions and nothing else — the schema reshaping, the _id lifting, the vertex target synthesis and the four-way ModelSchema dispatch are all gone.

The engine is untouched. ModelSchema, v2.engine.v3.V3TableDescriptor, V2BackedTableBinding, PrimitiveType and the /graph/v3/.../edges data plane are exactly as they were, and /graph/v2 keeps serving its own dialect for backward compatibility. tools/v3v2-boundary-check reports the same boundary as before: the v2 dependency stays confined to the three metadata controllers plus the datastore-references path.

Two things worth a reviewer's attention.

The immutable-edge invariants — at most one index, never BOTH — lived in ModelSchema.ImmutableEdge.init. The surface no longer builds a ModelSchema, and the v2 metastore does not know about immutable edges, so those checks move to TableCreateRequest. Without the move, creation would have succeeded and the table would have failed later, when the runtime projects it.

DELETE now answers {"status": "DELETED", ...} with 200 rather than 204, which is the envelope decision applied consistently. A table's type and direction are not updatable, matching what the previous surface allowed.

Changes

  • Surface (server/api/graph/v3/metadata/): new TableType, TableSchema, TableResponse, DatabaseResponse, AliasResponse and DdlEnvelopes; TableCreateRequest / TableUpdateRequest / Database*Request rewritten flat; the three controllers and V3CompatService rewired onto DdlPage / DdlStatus.
  • Converter: V3MetadataConverter reduced to renames — LabelEntity <-> TableResponse, ServiceEntity <-> DatabaseResponse, AliasEntity <-> AliasResponse, plus LabelType <-> TableType.
  • Queue: QueueMetadataService builds its backing table through the new request type.
  • Cleanup (second commit, structural): TableDescriptor, DatabaseDescriptor, AliasDescriptor, the Id hierarchy under them and metadata/payload/ are removed — they were only ever this API's response types and nothing constructs them now. Id itself stays, since core.v2.metadata identifiers still implement it. V2ServiceDescriptor.toV3 and V2AliasDescriptor.toV3 went with the types they converted to.
  • Docs (third commit): the metadata reference rewritten against the flat surface, with a field-by-field v2 mapping table replacing the three-row terminology list. Two rules in it were already wrong and are corrected: the name pattern is ^[a-z][a-z0-9_]{0,63}$, and the storage URI allows an empty namespace. The glossary and schema pages carried the mapping as "v2 (Current) -> v3 (Future)"; it is now what the API serves.
  • Test fixtures across the v3 E2E suites moved to the flat shape.

How to Test

  • ./gradlew :server:test --tests '*V2V3CompatibilityTest*' — the contract. Every case writes through one dialect and reads back through the other, across EDGE / IMMUTABLE_EDGE / MULTI_EDGE / VERTEX, in both directions, covering the whole rename table including indices/indexes, IGNORE/DROP and the derived readOnly.
  • ./gradlew :server:test --tests '*api.graph.v3.metadata.*' — CRUD, status filtering, name validation and cache round-trips on the new shape.
  • ./gradlew :server:test --tests '*ImmutableEdgeE2ETest*' --tests '*VertexIntegrationTest*' — the immutable-edge invariants still answer 400, and a vertex table still works with its id in source.
  • ./gradlew :server:test --tests '*v2*' --tests '*Ddl*' — v2 backward compatibility, unchanged.
  • ./gradlew :tools:v3v2-boundary-check:run — the v3-to-v2 boundary is the same as before.
  • ./gradlew spotlessCheck build — formatting and the full build (746 tests).

AI Assistance

  • This PR was written largely with AI assistance.
    • Tool / model: Claude Code (Opus 5)

em3s and others added 3 commits September 12, 2026 22:58
…3 names

The v3 metadata API did not just rename v2's vocabulary, it redesigned the
payload: the table type became a Jackson discriminator on `schema`, direction /
indexes / groups / caches moved inside `schema`, a multi-edge id was hoisted out
of `_id` and a vertex id out of `src`, type values became lowercase, and the v2
response envelopes were dropped. A v2 payload therefore could not be carried to
v3 by renaming keys, which left every ecosystem client (the Go CLI, the ops
scripts, the console form, the DDL fixtures) maintaining two shapes.

Make the surface v2's structure with v3's names, so the two dialects differ by
key names alone:

  desc        -> comment          dirType    -> direction
  schema.src  -> schema.source    indices    -> indexes
  schema.tgt  -> schema.target    .name      -> .index / .field
  schema.fields -> schema.properties
  INDEXED     -> EDGE             IMMUTABLE_INDEXED -> IMMUTABLE_EDGE
  mode IGNORE -> mode DROP        name "a.b" -> database + table

Type values stay uppercase because the surface now carries v2's own `DataType`
and `VertexType`; `DECIMAL` no longer collapses into `OBJECT` on the way through.
A multi-edge keeps its id as the `_id` property and a vertex keeps its id in
`source`, exactly as v2 stores them. Listings return `{count, content}` and
mutations `{status, result, message}`, the same envelopes v2 serves. The v2-only
`event` and `readOnly` flags leave the surface; the server derives them.

`V3MetadataConverter` is now a pure rename in both directions -- all the schema
reshaping is gone. The engine is untouched: `ModelSchema`, `V3TableDescriptor`,
`V2BackedTableBinding`, `PrimitiveType` and the `/graph/v3/.../edges` data plane
are exactly as they were, and `/graph/v2` keeps serving its own dialect.

The immutable-edge invariants (at most one index, never BOTH) lived in
`ModelSchema.ImmutableEdge.init`, which the surface no longer builds, so they
move to `TableCreateRequest` -- otherwise creation would succeed and the table
would fail later, when the runtime projects it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`TableDescriptor`, `DatabaseDescriptor` and `AliasDescriptor` were only ever the
v3 API's response types -- the engine reads `ModelSchema` and projects it through
`v2.engine.v3.V3TableDescriptor`, never these. With the surface serving its own
flat responses, nothing constructs them, and the `Id` hierarchy and
`payload/Database*Request` under them have no callers either.

`V2ServiceDescriptor.toV3` and `V2AliasDescriptor.toV3` converted to two of the
removed types. Both live in `core.v2.metadata`, a parallel v2 model whose only
callers are its own serialization tests, so they go with them.

`Id` stays: those same `core.v2.metadata` identifiers still implement it.

No behavior changes -- nothing reachable from an endpoint referenced any of this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrite the metadata API reference against the flat surface: the data model, the
request and response examples, the envelopes, and a field-by-field v2 mapping
table in place of the old three-row terminology list.

Two rules in the reference were already wrong and are corrected here: the name
pattern is `^[a-z][a-z0-9_]{0,63}$`, not the mixed-case form, and the storage URI
allows an empty namespace.

The glossary and schema pages carried the mapping as "v2 (Current) -> v3
(Future)". The mapping is now what the API actually serves, so say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@em3s

em3s commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Closing this for now to give it some more thought.

@em3s em3s closed this Sep 16, 2026
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