You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tinkerpop.apache.org by Cole Greer via dev <de...@tinkerpop.apache.org> on 2026/04/03 02:16:24 UTC
[DISCUSS] Graph Schema Interfaces for TP
Hi Everyone,
The topic of Graph Schema has been discussed extensively in recent TInkerPop Gatherings, and the following proposal has emerged from these gatherings. I believe it is now ready for broad consideration and discussions. I’ve done my best to incorporate initial feedback from Josh, Pieter, Valentyn, Stephen, Kris and others into this proposal, however I won’t claim that it accurately represents the views of anyone other than myself at this time. This is a broad topic and I’m deliberately excluding critical topics to focus this thread on standardizing interfaces for gremlin users and providers to interact with schema (see assumptions for more details).
## Overview
This proposal introduces graph schema interfaces for TinkerPop: a way to define vertex types, edge types, and property types as a meta-graph that is itself traversable with Gremlin. The schema describes the structure of a data graph; what kinds of vertices and edges exist, what properties they carry, and how they connect..
## Assumptions
- Type keys are element labels: there is a 1-to-1 mapping between a label and a type definition. A vertex labeled "person" corresponds to exactly one VertexType, and an edge labeled "knows" corresponds to exactly one EdgeType.
- Java classes are used as a type system: This proposal uses Java classes to define property type constraints. This is intended as a placeholder to be replaced by a proper type system to be defined via a later discussion.
- This proposal makes very little consideration of if/when/where/how validation and enforcement of schema takes place. I believe it is important for us to ship something which is flexible and useful to providers out of the box as well as leaving space for providers to plugin existing implementations or build their own if they desire. I’ve left this out of scope for this proposal to focus first on interfaces which give providers the appropriate access to schema.
## Design Points
### 1. Schema-as-Graph
`GraphSchema extends Graph`. Providers implement a familiar interface, and users traverse the schema with schema.traversal(). This avoids inventing a parallel API surface. The schema is just another graph.
A data graph exposes its schema via Graph.schema(), which returns the GraphSchema instance. Providers that don't support schema return UnsupportedOperationException by default.
### 2. All type definitions are vertices
VertexType, EdgeType, and PropertyType are all vertices in the schema meta-graph.
- A VertexType vertex represents a vertex label definition (e.g. "person", "software").
- An EdgeType vertex represents an edge label definition (e.g. "knows", "created"). Even though it describes edges in the data graph, it is itself a vertex in the schema graph, connected to its endpoint VertexType vertices via from/to edges.
- A PropertyType vertex represents a property on a type, connected to its parent type vertex via a “hasProperty" edge.
Property definitions are independent per type, no sharing across types.
Schema graph example for the classic TinkerPop modern graph:
```
(person:vertexType) --hasProperty--> (name:propertyType)
(person:vertexType) --hasProperty--> (age:propertyType)
(software:vertexType) --hasProperty--> (name:propertyType)
(software:vertexType) --hasProperty--> (lang:propertyType)
(knows:edgeType) --from--> (person:vertexType)
(knows:edgeType) --to--> (person:vertexType)
(knows:edgeType) --hasProperty--> (weight:propertyType)
(created:edgeType) --from--> (person:vertexType)
(created:edgeType) --to--> (software:vertexType)
(created:edgeType) --hasProperty--> (weight:propertyType)
```
### 3. Constraints are properties on type vertices
Rather than a fixed constraint taxonomy, constraints are regular properties on type vertices, keyed by string via constraint(key, value). This keeps the model extensible such that providers can define their own constraints without changes to the core API.
Constraints can be added to VertexType, EdgeType, and PropertyType vertices directly. The most common constraints such as property types and required properties would apply to PropertyTypes, while edge multiplicity constraints (e.g. one-to-many, one-to-one) are naturally expressed as constraints on the EdgeType itself rather than on any property.
While constraint keys are arbitrary strings and providers are free to implement any constraints they like, TinkerPop should standardize a set of core constraint keys representing the most common constraints. Examples include “type", “required", “unique", “minValue", “maxValue", etc. Providers that support equivalent constraints are encouraged to follow these conventional names for interoperability.
Non-core constraints (custom to a provider) are encouraged to follow a namespaced key convention to avoid collisions, e.g. "tinkergraph:notNull". Core constraint keys are unnamespaced.
### 4. Schema traversal steps in core Gremlin
New steps for schema manipulation live directly in GraphTraversal/GraphTraversalSource, not in a separate DSL:
- addVType(label) — creates a VertexType vertex
- addEType(label) — creates an EdgeType vertex
- propertyType(name) — creates a PropertyType vertex and connects it via hasProperty
- constraint(key, value) — adds a constraint property to the current type vertex
Example: defining a vertex type with properties:
```
schema.traversal().addVType("person")
.propertyType("name").constraint("type", String.class).constraint("required", true).constraint("unique", true)
.propertyType("age").constraint("type", Integer.class)
```
Example: defining an edge type with endpoint types and a property:
```
schema.traversal().addEType("knows")
.from("person").to("person")
.propertyType("weight").constraint("type", Double.class)
```
This mirrors the addE().from().to() pattern from the data-graph. Here from() and to() take vertex type labels (strings) and create from/to edges in the schema graph connecting the EdgeType to the referenced VertexType vertices.
### 5. Convenience methods for direct access
The schema-as-graph model is the source of truth, but traversing it for simple lookups isn’t always convenient. Direct methods provide compact access:
GraphSchema methods:
- vertexTypes() → Collection<VertexType>
- vertexType(String label) → Optional<VertexType>
- edgeTypes() → Collection<EdgeType>
- edgeType(String label) → Optional<EdgeType>
- addVertexType(String label) → VertexType
- addEdgeType(String label) → EdgeType
- store(OutputStream): serialize the schema to a compact JSON representation
- load(InputStream): deserialize and merge a schema from JSON into this schema graph
EdgeType methods:
- fromVertexTypes() → Collection<VertexType>
- toVertexTypes() → Collection<VertexType>
Example:
```
GraphSchema schema = graph.schema();
// Look up a vertex type
VertexType person = schema.vertexType("person").orElseThrow();
// Inspect its properties
for (PropertyType pd : person.propertyTypes()) {
System.out.println(pd.name() + " : " + pd.constraint("type"));
}
// Look up an edge type and its connectivity
EdgeType knows = schema.edgeType("knows").orElseThrow();
Collection<VertexType> fromTypes = knows.fromVertexTypes();
Collection<VertexType> toTypes = knows.toVertexTypes();
```
### 6. Cross-graph jumps
Two steps bridge the data graph and schema graph:
- type(): from a data traversal, jump to the element's type definition in the schema graph.
- instances(): from a schema traversal, jump to all matching elements in the data graph.
These compose for round-trip traversals:
```
// Get the type definition for "person" vertices
g.V().hasLabel("person").type()
// Get all instances of a schema type
schema.traversal().vertexType("person").instances()
// Round-trip: find marko's type, then get all instances of that type
g.V().has("person", "name", "marko").type().instances()
```
### 7. Schema restriction strategy
There are some steps we will want to restrict in both the data graph and the schema-graph. addVType() wouldn’t make sense in the data-graph, nor would addV() be sensible in the schema-graph. A TraversalStrategy can restrict schema traversals to a safe subset of Gremlin steps (allowlist-based). This prevents accidentally running data element insertions, OLAP computations, complex control flow, or side-effect steps against the schema graph. The strategy should be auto-registered when traversing a GraphSchema instance.
The exact allowlist should be a topic for later discussion.
### 8. Instance counts on type vertices
VertexType.instanceCount() and EdgeType.instanceCount() return the count of data graph elements matching each type. This is a method rather than a property on the type vertex, keeping the schema graph definitional (not statistical) and giving providers full implementation flexibility.
Approximate counts are likely acceptable and preferable for performance in most cases. However, TinkerPop should not stand in the way of providers that prefer exact counts, and should ensure that appropriate hooks are in place in reference implementations so that providers can maintain exact counts if they so desire.
Transactional implications need additional consideration. Maintaining accurate counts across concurrent writes, rollbacks, and transaction isolation levels adds significant complexity. This interacts with the broader schema transactions question (see transactions below) and should be addressed alongside it.
### 9. GLV Support
Each GLV (Python, JavaScript, .NET, Go) needs:
- Schema data classes: Parallel classes to the 4 core Java interfaces, following the same pattern as existing Vertex and Edge classes. These are data containers representing schema objects returned from the server:
- GraphSchema: holds collections of VertexTypes and EdgeTypes
- VertexType: label, full constraints map, and collection of PropertyTypes
- EdgeType: label, full constraints map, from/to VertexType references (same pattern as Edge.outV/Edge.inV), and collection of PropertyTypes
- PropertyType: name and full constraints map (including data type as a constraint)
- All new gremlin steps are supported from each GLV
## Future Questions
### Schema validation
Providers will need lots of flexibility regarding validation modes. Some providers may choose to have write-time validation for all inserts, others may choose validate an entire graph against a schema as a batch job, while others may choose to validate on-commit. For our purposes, we need to provide a viable reference implementation, as well as ensuring sufficient extension points exist for providers to fulfill their needs.
### Dynamic schema updates from data writes
It would be useful to auto-update the schema graph when data writes introduce new labels or properties (e.g. addV("newLabel”) automatically creates a VertexType). Keeping the schema exactly in-sync with such operations may introduce too much overhead for many purposes. We should provide appropriate hooks for providers to implement such behaviour if desired, or to help providers aggregate changes and perform incremental batch updates to the schema.
### Transactions
The schema graph will need to be transactional if the data
### File IO
It is often useful to persist and load schemas to/from files. This capability should be build into the GraphSchema class via simple store() and load() methods, using a custom compact JSON representation of the schema. The specifics of this format are deferred to later discussion.
GraphSchema exposes file IO directly:
- store(OutputStream): serialize the schema to a compact JSON representation
- load(InputStream): deserialize a schema from JSON and merge it into the current schema graph
Schema file IO should be implemented across all GLVs.
## Reference Implementation
TinkerGraph serves as the reference implementation:
- TinkerGraphSchema extends TinkerGraph implements GraphSchema
- TinkerVertexType extends TinkerVertex implements VertexType
- TinkerPropertyType extends TinkerVertex implements PropertyType
- TinkerEdgeType extends TinkerVertex implements EdgeType
- Recursion guard prevents schema-of-schema (TinkerGraphSchema overrides initSchema())
Please let me know any thoughts you may have on the approach. I intend to move this into a proposal PR soon, unless there are any major disagreements over the design.
Thanks,
Cole
Re: [DISCUSS] Graph Schema Interfaces for TP
Posted by Cole Greer <co...@apache.org>.
Hi Josh,
I like the idea of parameterized types. That sounds great if providers are free to plug in a rich type system if they choose, or to drop in a simple enum if they don't want added complexity.
Thanks,
Cole
On 2026/04/11 17:04:07 Joshua Shinavier wrote:
> Hi Cole,
>
> I agree with keeping things as simple as possible. The schema types I
> shared are parameterized by a *T* type for ids and property values. This
> keeps the model flexible: providers can plug in whatever they need for T --
> whether something as complex as Hydra's Type type (which does include
> complex record and union types, among other things), or as simple as a
> provider specific enum with STRING, INT, LONG, etc. Now, in TinkerPop we
> could keep the types parameterized (which is what I recommend), or we could
> replace T with a TinkerPop-specific type system; like an enum for
> DatatypeFeatures
> <https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/ext/org/apache/tinkerpop/features/DataTypeFeatures.html>
> (which
> is based on Gremlin's Graph.Features
> <https://tinkerpop.apache.org/javadocs/current/core/org/apache/tinkerpop/gremlin/structure/Graph.Features.html>
> interface). Does that address the concern?
>
> Josh
>
>
>
> On Fri, Apr 10, 2026 at 2:52 PM Cole Greer <co...@apache.org> wrote:
>
> > Hi Josh,
> >
> > I agree that the property graph model from Hydra should adapt well for our
> > purposes in TinkerPop. My main concern regarding schema types is that
> > providers aren't unnecessarily burdened when trying to map types from an
> > existing proprietary schema into our new interfaces. I like the idea of our
> > schema types supporting complex composite and union types, however my
> > primary goal is to keep this as simple for providers to adopt as possible.
> >
> > Thanks,
> > Cole
> >
> > On 2026/04/03 03:31:12 Joshua Shinavier wrote:
> > > Hi Cole. This looks good to me. With respect to the schema types, would
> > you
> > > please review the property graph model I showed a couple of meetings ago
> > --
> > > here
> > > <
> > https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/package-summary.html
> > >
> > > --
> > > and let me know if you have any feedback. The types of interest start
> > with
> > > GraphSchema
> > > <
> > https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/GraphSchema.html
> > >.
> > > Ignore Hydra-specific details like PersistentMap/ConsList/Name, etc. If
> > the
> > > structure is agreeable, I will map the types into a format suitable for
> > use
> > > in your PR, in an org.apache.tinkerpop namespace. There is an associated
> > > JSON format for interchange of GraphSchema and any other type we define
> > in
> > > this way.
> > >
> > > Josh
> > >
> > >
> > >
> > > On Thu, Apr 2, 2026 at 7:17 PM Cole Greer via dev <
> > dev@tinkerpop.apache.org>
> > > wrote:
> > >
> > > > Hi Everyone,
> > > >
> > > > The topic of Graph Schema has been discussed extensively in recent
> > > > TInkerPop Gatherings, and the following proposal has emerged from these
> > > > gatherings. I believe it is now ready for broad consideration and
> > > > discussions. I’ve done my best to incorporate initial feedback from
> > Josh,
> > > > Pieter, Valentyn, Stephen, Kris and others into this proposal, however
> > I
> > > > won’t claim that it accurately represents the views of anyone other
> > than
> > > > myself at this time. This is a broad topic and I’m deliberately
> > excluding
> > > > critical topics to focus this thread on standardizing interfaces for
> > > > gremlin users and providers to interact with schema (see assumptions
> > for
> > > > more details).
> > > >
> > > > ## Overview
> > > >
> > > > This proposal introduces graph schema interfaces for TinkerPop: a way
> > to
> > > > define vertex types, edge types, and property types as a meta-graph
> > that is
> > > > itself traversable with Gremlin. The schema describes the structure of
> > a
> > > > data graph; what kinds of vertices and edges exist, what properties
> > they
> > > > carry, and how they connect..
> > > >
> > > > ## Assumptions
> > > >
> > > > - Type keys are element labels: there is a 1-to-1 mapping between a
> > label
> > > > and a type definition. A vertex labeled "person" corresponds to
> > exactly one
> > > > VertexType, and an edge labeled "knows" corresponds to exactly one
> > EdgeType.
> > > > - Java classes are used as a type system: This proposal uses Java
> > classes
> > > > to define property type constraints. This is intended as a placeholder
> > to
> > > > be replaced by a proper type system to be defined via a later
> > discussion.
> > > > - This proposal makes very little consideration of if/when/where/how
> > > > validation and enforcement of schema takes place. I believe it is
> > important
> > > > for us to ship something which is flexible and useful to providers out
> > of
> > > > the box as well as leaving space for providers to plugin existing
> > > > implementations or build their own if they desire. I’ve left this out
> > of
> > > > scope for this proposal to focus first on interfaces which give
> > providers
> > > > the appropriate access to schema.
> > > >
> > > > ## Design Points
> > > >
> > > > ### 1. Schema-as-Graph
> > > >
> > > > `GraphSchema extends Graph`. Providers implement a familiar interface,
> > and
> > > > users traverse the schema with schema.traversal(). This avoids
> > inventing a
> > > > parallel API surface. The schema is just another graph.
> > > >
> > > > A data graph exposes its schema via Graph.schema(), which returns the
> > > > GraphSchema instance. Providers that don't support schema return
> > > > UnsupportedOperationException by default.
> > > >
> > > > ### 2. All type definitions are vertices
> > > >
> > > > VertexType, EdgeType, and PropertyType are all vertices in the schema
> > > > meta-graph.
> > > >
> > > > - A VertexType vertex represents a vertex label definition (e.g.
> > "person",
> > > > "software").
> > > > - An EdgeType vertex represents an edge label definition (e.g. "knows",
> > > > "created"). Even though it describes edges in the data graph, it is
> > itself
> > > > a vertex in the schema graph, connected to its endpoint VertexType
> > vertices
> > > > via from/to edges.
> > > > - A PropertyType vertex represents a property on a type, connected to
> > its
> > > > parent type vertex via a “hasProperty" edge.
> > > >
> > > > Property definitions are independent per type, no sharing across types.
> > > >
> > > > Schema graph example for the classic TinkerPop modern graph:
> > > > ```
> > > > (person:vertexType) --hasProperty--> (name:propertyType)
> > > > (person:vertexType) --hasProperty--> (age:propertyType)
> > > > (software:vertexType) --hasProperty--> (name:propertyType)
> > > > (software:vertexType) --hasProperty--> (lang:propertyType)
> > > > (knows:edgeType) --from--> (person:vertexType)
> > > > (knows:edgeType) --to--> (person:vertexType)
> > > > (knows:edgeType) --hasProperty--> (weight:propertyType)
> > > > (created:edgeType) --from--> (person:vertexType)
> > > > (created:edgeType) --to--> (software:vertexType)
> > > > (created:edgeType) --hasProperty--> (weight:propertyType)
> > > > ```
> > > >
> > > > ### 3. Constraints are properties on type vertices
> > > >
> > > > Rather than a fixed constraint taxonomy, constraints are regular
> > > > properties on type vertices, keyed by string via constraint(key,
> > value).
> > > > This keeps the model extensible such that providers can define their
> > own
> > > > constraints without changes to the core API.
> > > >
> > > > Constraints can be added to VertexType, EdgeType, and PropertyType
> > > > vertices directly. The most common constraints such as property types
> > and
> > > > required properties would apply to PropertyTypes, while edge
> > multiplicity
> > > > constraints (e.g. one-to-many, one-to-one) are naturally expressed as
> > > > constraints on the EdgeType itself rather than on any property.
> > > >
> > > > While constraint keys are arbitrary strings and providers are free to
> > > > implement any constraints they like, TinkerPop should standardize a
> > set of
> > > > core constraint keys representing the most common constraints. Examples
> > > > include “type", “required", “unique", “minValue", “maxValue", etc.
> > > > Providers that support equivalent constraints are encouraged to follow
> > > > these conventional names for interoperability.
> > > >
> > > > Non-core constraints (custom to a provider) are encouraged to follow a
> > > > namespaced key convention to avoid collisions, e.g.
> > "tinkergraph:notNull".
> > > > Core constraint keys are unnamespaced.
> > > >
> > > > ### 4. Schema traversal steps in core Gremlin
> > > >
> > > > New steps for schema manipulation live directly in
> > > > GraphTraversal/GraphTraversalSource, not in a separate DSL:
> > > >
> > > > - addVType(label) — creates a VertexType vertex
> > > > - addEType(label) — creates an EdgeType vertex
> > > > - propertyType(name) — creates a PropertyType vertex and connects it
> > via
> > > > hasProperty
> > > > - constraint(key, value) — adds a constraint property to the current
> > type
> > > > vertex
> > > >
> > > > Example: defining a vertex type with properties:
> > > > ```
> > > > schema.traversal().addVType("person")
> > > > .propertyType("name").constraint("type",
> > > > String.class).constraint("required", true).constraint("unique", true)
> > > > .propertyType("age").constraint("type", Integer.class)
> > > > ```
> > > >
> > > > Example: defining an edge type with endpoint types and a property:
> > > > ```
> > > > schema.traversal().addEType("knows")
> > > > .from("person").to("person")
> > > > .propertyType("weight").constraint("type", Double.class)
> > > > ```
> > > >
> > > > This mirrors the addE().from().to() pattern from the data-graph. Here
> > > > from() and to() take vertex type labels (strings) and create from/to
> > edges
> > > > in the schema graph connecting the EdgeType to the referenced
> > VertexType
> > > > vertices.
> > > >
> > > > ### 5. Convenience methods for direct access
> > > >
> > > > The schema-as-graph model is the source of truth, but traversing it for
> > > > simple lookups isn’t always convenient. Direct methods provide compact
> > > > access:
> > > >
> > > > GraphSchema methods:
> > > > - vertexTypes() → Collection<VertexType>
> > > > - vertexType(String label) → Optional<VertexType>
> > > > - edgeTypes() → Collection<EdgeType>
> > > > - edgeType(String label) → Optional<EdgeType>
> > > > - addVertexType(String label) → VertexType
> > > > - addEdgeType(String label) → EdgeType
> > > > - store(OutputStream): serialize the schema to a compact JSON
> > > > representation
> > > > - load(InputStream): deserialize and merge a schema from JSON into this
> > > > schema graph
> > > >
> > > > EdgeType methods:
> > > > - fromVertexTypes() → Collection<VertexType>
> > > > - toVertexTypes() → Collection<VertexType>
> > > >
> > > > Example:
> > > > ```
> > > > GraphSchema schema = graph.schema();
> > > >
> > > > // Look up a vertex type
> > > > VertexType person = schema.vertexType("person").orElseThrow();
> > > >
> > > > // Inspect its properties
> > > > for (PropertyType pd : person.propertyTypes()) {
> > > > System.out.println(pd.name() + " : " + pd.constraint("type"));
> > > > }
> > > >
> > > > // Look up an edge type and its connectivity
> > > > EdgeType knows = schema.edgeType("knows").orElseThrow();
> > > > Collection<VertexType> fromTypes = knows.fromVertexTypes();
> > > > Collection<VertexType> toTypes = knows.toVertexTypes();
> > > > ```
> > > >
> > > > ### 6. Cross-graph jumps
> > > >
> > > > Two steps bridge the data graph and schema graph:
> > > >
> > > > - type(): from a data traversal, jump to the element's type definition
> > in
> > > > the schema graph.
> > > > - instances(): from a schema traversal, jump to all matching elements
> > in
> > > > the data graph.
> > > >
> > > > These compose for round-trip traversals:
> > > > ```
> > > > // Get the type definition for "person" vertices
> > > > g.V().hasLabel("person").type()
> > > >
> > > > // Get all instances of a schema type
> > > > schema.traversal().vertexType("person").instances()
> > > >
> > > > // Round-trip: find marko's type, then get all instances of that type
> > > > g.V().has("person", "name", "marko").type().instances()
> > > > ```
> > > >
> > > > ### 7. Schema restriction strategy
> > > >
> > > > There are some steps we will want to restrict in both the data graph
> > and
> > > > the schema-graph. addVType() wouldn’t make sense in the data-graph, nor
> > > > would addV() be sensible in the schema-graph. A TraversalStrategy can
> > > > restrict schema traversals to a safe subset of Gremlin steps
> > > > (allowlist-based). This prevents accidentally running data element
> > > > insertions, OLAP computations, complex control flow, or side-effect
> > steps
> > > > against the schema graph. The strategy should be auto-registered when
> > > > traversing a GraphSchema instance.
> > > >
> > > > The exact allowlist should be a topic for later discussion.
> > > >
> > > > ### 8. Instance counts on type vertices
> > > >
> > > > VertexType.instanceCount() and EdgeType.instanceCount() return the
> > count
> > > > of data graph elements matching each type. This is a method rather
> > than a
> > > > property on the type vertex, keeping the schema graph definitional (not
> > > > statistical) and giving providers full implementation flexibility.
> > > >
> > > > Approximate counts are likely acceptable and preferable for
> > performance in
> > > > most cases. However, TinkerPop should not stand in the way of providers
> > > > that prefer exact counts, and should ensure that appropriate hooks are
> > in
> > > > place in reference implementations so that providers can maintain exact
> > > > counts if they so desire.
> > > >
> > > > Transactional implications need additional consideration. Maintaining
> > > > accurate counts across concurrent writes, rollbacks, and transaction
> > > > isolation levels adds significant complexity. This interacts with the
> > > > broader schema transactions question (see transactions below) and
> > should be
> > > > addressed alongside it.
> > > >
> > > > ### 9. GLV Support
> > > >
> > > > Each GLV (Python, JavaScript, .NET, Go) needs:
> > > >
> > > > - Schema data classes: Parallel classes to the 4 core Java interfaces,
> > > > following the same pattern as existing Vertex and Edge classes. These
> > are
> > > > data containers representing schema objects returned from the server:
> > > > - GraphSchema: holds collections of VertexTypes and EdgeTypes
> > > > - VertexType: label, full constraints map, and collection of
> > > > PropertyTypes
> > > > - EdgeType: label, full constraints map, from/to VertexType
> > references
> > > > (same pattern as Edge.outV/Edge.inV), and collection of PropertyTypes
> > > > - PropertyType: name and full constraints map (including data type
> > as a
> > > > constraint)
> > > > - All new gremlin steps are supported from each GLV
> > > >
> > > > ## Future Questions
> > > >
> > > > ### Schema validation
> > > >
> > > > Providers will need lots of flexibility regarding validation modes.
> > Some
> > > > providers may choose to have write-time validation for all inserts,
> > others
> > > > may choose validate an entire graph against a schema as a batch job,
> > while
> > > > others may choose to validate on-commit. For our purposes, we need to
> > > > provide a viable reference implementation, as well as ensuring
> > sufficient
> > > > extension points exist for providers to fulfill their needs.
> > > >
> > > > ### Dynamic schema updates from data writes
> > > >
> > > > It would be useful to auto-update the schema graph when data writes
> > > > introduce new labels or properties (e.g. addV("newLabel”) automatically
> > > > creates a VertexType). Keeping the schema exactly in-sync with such
> > > > operations may introduce too much overhead for many purposes. We should
> > > > provide appropriate hooks for providers to implement such behaviour if
> > > > desired, or to help providers aggregate changes and perform incremental
> > > > batch updates to the schema.
> > > >
> > > > ### Transactions
> > > >
> > > > The schema graph will need to be transactional if the data
> > > >
> > > > ### File IO
> > > >
> > > > It is often useful to persist and load schemas to/from files. This
> > > > capability should be build into the GraphSchema class via simple
> > store()
> > > > and load() methods, using a custom compact JSON representation of the
> > > > schema. The specifics of this format are deferred to later discussion.
> > > >
> > > > GraphSchema exposes file IO directly:
> > > > - store(OutputStream): serialize the schema to a compact JSON
> > > > representation
> > > > - load(InputStream): deserialize a schema from JSON and merge it into
> > the
> > > > current schema graph
> > > >
> > > > Schema file IO should be implemented across all GLVs.
> > > >
> > > > ## Reference Implementation
> > > >
> > > > TinkerGraph serves as the reference implementation:
> > > >
> > > > - TinkerGraphSchema extends TinkerGraph implements GraphSchema
> > > > - TinkerVertexType extends TinkerVertex implements VertexType
> > > > - TinkerPropertyType extends TinkerVertex implements PropertyType
> > > > - TinkerEdgeType extends TinkerVertex implements EdgeType
> > > > - Recursion guard prevents schema-of-schema (TinkerGraphSchema
> > overrides
> > > > initSchema())
> > > >
> > > >
> > > > Please let me know any thoughts you may have on the approach. I intend
> > to
> > > > move this into a proposal PR soon, unless there are any major
> > disagreements
> > > > over the design.
> > > >
> > > > Thanks,
> > > > Cole
> > > >
> > >
> >
>
Re: [DISCUSS] Graph Schema Interfaces for TP
Posted by Joshua Shinavier <jo...@gmail.com>.
Hi Cole,
I agree with keeping things as simple as possible. The schema types I
shared are parameterized by a *T* type for ids and property values. This
keeps the model flexible: providers can plug in whatever they need for T --
whether something as complex as Hydra's Type type (which does include
complex record and union types, among other things), or as simple as a
provider specific enum with STRING, INT, LONG, etc. Now, in TinkerPop we
could keep the types parameterized (which is what I recommend), or we could
replace T with a TinkerPop-specific type system; like an enum for
DatatypeFeatures
<https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/ext/org/apache/tinkerpop/features/DataTypeFeatures.html>
(which
is based on Gremlin's Graph.Features
<https://tinkerpop.apache.org/javadocs/current/core/org/apache/tinkerpop/gremlin/structure/Graph.Features.html>
interface). Does that address the concern?
Josh
On Fri, Apr 10, 2026 at 2:52 PM Cole Greer <co...@apache.org> wrote:
> Hi Josh,
>
> I agree that the property graph model from Hydra should adapt well for our
> purposes in TinkerPop. My main concern regarding schema types is that
> providers aren't unnecessarily burdened when trying to map types from an
> existing proprietary schema into our new interfaces. I like the idea of our
> schema types supporting complex composite and union types, however my
> primary goal is to keep this as simple for providers to adopt as possible.
>
> Thanks,
> Cole
>
> On 2026/04/03 03:31:12 Joshua Shinavier wrote:
> > Hi Cole. This looks good to me. With respect to the schema types, would
> you
> > please review the property graph model I showed a couple of meetings ago
> --
> > here
> > <
> https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/package-summary.html
> >
> > --
> > and let me know if you have any feedback. The types of interest start
> with
> > GraphSchema
> > <
> https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/GraphSchema.html
> >.
> > Ignore Hydra-specific details like PersistentMap/ConsList/Name, etc. If
> the
> > structure is agreeable, I will map the types into a format suitable for
> use
> > in your PR, in an org.apache.tinkerpop namespace. There is an associated
> > JSON format for interchange of GraphSchema and any other type we define
> in
> > this way.
> >
> > Josh
> >
> >
> >
> > On Thu, Apr 2, 2026 at 7:17 PM Cole Greer via dev <
> dev@tinkerpop.apache.org>
> > wrote:
> >
> > > Hi Everyone,
> > >
> > > The topic of Graph Schema has been discussed extensively in recent
> > > TInkerPop Gatherings, and the following proposal has emerged from these
> > > gatherings. I believe it is now ready for broad consideration and
> > > discussions. I’ve done my best to incorporate initial feedback from
> Josh,
> > > Pieter, Valentyn, Stephen, Kris and others into this proposal, however
> I
> > > won’t claim that it accurately represents the views of anyone other
> than
> > > myself at this time. This is a broad topic and I’m deliberately
> excluding
> > > critical topics to focus this thread on standardizing interfaces for
> > > gremlin users and providers to interact with schema (see assumptions
> for
> > > more details).
> > >
> > > ## Overview
> > >
> > > This proposal introduces graph schema interfaces for TinkerPop: a way
> to
> > > define vertex types, edge types, and property types as a meta-graph
> that is
> > > itself traversable with Gremlin. The schema describes the structure of
> a
> > > data graph; what kinds of vertices and edges exist, what properties
> they
> > > carry, and how they connect..
> > >
> > > ## Assumptions
> > >
> > > - Type keys are element labels: there is a 1-to-1 mapping between a
> label
> > > and a type definition. A vertex labeled "person" corresponds to
> exactly one
> > > VertexType, and an edge labeled "knows" corresponds to exactly one
> EdgeType.
> > > - Java classes are used as a type system: This proposal uses Java
> classes
> > > to define property type constraints. This is intended as a placeholder
> to
> > > be replaced by a proper type system to be defined via a later
> discussion.
> > > - This proposal makes very little consideration of if/when/where/how
> > > validation and enforcement of schema takes place. I believe it is
> important
> > > for us to ship something which is flexible and useful to providers out
> of
> > > the box as well as leaving space for providers to plugin existing
> > > implementations or build their own if they desire. I’ve left this out
> of
> > > scope for this proposal to focus first on interfaces which give
> providers
> > > the appropriate access to schema.
> > >
> > > ## Design Points
> > >
> > > ### 1. Schema-as-Graph
> > >
> > > `GraphSchema extends Graph`. Providers implement a familiar interface,
> and
> > > users traverse the schema with schema.traversal(). This avoids
> inventing a
> > > parallel API surface. The schema is just another graph.
> > >
> > > A data graph exposes its schema via Graph.schema(), which returns the
> > > GraphSchema instance. Providers that don't support schema return
> > > UnsupportedOperationException by default.
> > >
> > > ### 2. All type definitions are vertices
> > >
> > > VertexType, EdgeType, and PropertyType are all vertices in the schema
> > > meta-graph.
> > >
> > > - A VertexType vertex represents a vertex label definition (e.g.
> "person",
> > > "software").
> > > - An EdgeType vertex represents an edge label definition (e.g. "knows",
> > > "created"). Even though it describes edges in the data graph, it is
> itself
> > > a vertex in the schema graph, connected to its endpoint VertexType
> vertices
> > > via from/to edges.
> > > - A PropertyType vertex represents a property on a type, connected to
> its
> > > parent type vertex via a “hasProperty" edge.
> > >
> > > Property definitions are independent per type, no sharing across types.
> > >
> > > Schema graph example for the classic TinkerPop modern graph:
> > > ```
> > > (person:vertexType) --hasProperty--> (name:propertyType)
> > > (person:vertexType) --hasProperty--> (age:propertyType)
> > > (software:vertexType) --hasProperty--> (name:propertyType)
> > > (software:vertexType) --hasProperty--> (lang:propertyType)
> > > (knows:edgeType) --from--> (person:vertexType)
> > > (knows:edgeType) --to--> (person:vertexType)
> > > (knows:edgeType) --hasProperty--> (weight:propertyType)
> > > (created:edgeType) --from--> (person:vertexType)
> > > (created:edgeType) --to--> (software:vertexType)
> > > (created:edgeType) --hasProperty--> (weight:propertyType)
> > > ```
> > >
> > > ### 3. Constraints are properties on type vertices
> > >
> > > Rather than a fixed constraint taxonomy, constraints are regular
> > > properties on type vertices, keyed by string via constraint(key,
> value).
> > > This keeps the model extensible such that providers can define their
> own
> > > constraints without changes to the core API.
> > >
> > > Constraints can be added to VertexType, EdgeType, and PropertyType
> > > vertices directly. The most common constraints such as property types
> and
> > > required properties would apply to PropertyTypes, while edge
> multiplicity
> > > constraints (e.g. one-to-many, one-to-one) are naturally expressed as
> > > constraints on the EdgeType itself rather than on any property.
> > >
> > > While constraint keys are arbitrary strings and providers are free to
> > > implement any constraints they like, TinkerPop should standardize a
> set of
> > > core constraint keys representing the most common constraints. Examples
> > > include “type", “required", “unique", “minValue", “maxValue", etc.
> > > Providers that support equivalent constraints are encouraged to follow
> > > these conventional names for interoperability.
> > >
> > > Non-core constraints (custom to a provider) are encouraged to follow a
> > > namespaced key convention to avoid collisions, e.g.
> "tinkergraph:notNull".
> > > Core constraint keys are unnamespaced.
> > >
> > > ### 4. Schema traversal steps in core Gremlin
> > >
> > > New steps for schema manipulation live directly in
> > > GraphTraversal/GraphTraversalSource, not in a separate DSL:
> > >
> > > - addVType(label) — creates a VertexType vertex
> > > - addEType(label) — creates an EdgeType vertex
> > > - propertyType(name) — creates a PropertyType vertex and connects it
> via
> > > hasProperty
> > > - constraint(key, value) — adds a constraint property to the current
> type
> > > vertex
> > >
> > > Example: defining a vertex type with properties:
> > > ```
> > > schema.traversal().addVType("person")
> > > .propertyType("name").constraint("type",
> > > String.class).constraint("required", true).constraint("unique", true)
> > > .propertyType("age").constraint("type", Integer.class)
> > > ```
> > >
> > > Example: defining an edge type with endpoint types and a property:
> > > ```
> > > schema.traversal().addEType("knows")
> > > .from("person").to("person")
> > > .propertyType("weight").constraint("type", Double.class)
> > > ```
> > >
> > > This mirrors the addE().from().to() pattern from the data-graph. Here
> > > from() and to() take vertex type labels (strings) and create from/to
> edges
> > > in the schema graph connecting the EdgeType to the referenced
> VertexType
> > > vertices.
> > >
> > > ### 5. Convenience methods for direct access
> > >
> > > The schema-as-graph model is the source of truth, but traversing it for
> > > simple lookups isn’t always convenient. Direct methods provide compact
> > > access:
> > >
> > > GraphSchema methods:
> > > - vertexTypes() → Collection<VertexType>
> > > - vertexType(String label) → Optional<VertexType>
> > > - edgeTypes() → Collection<EdgeType>
> > > - edgeType(String label) → Optional<EdgeType>
> > > - addVertexType(String label) → VertexType
> > > - addEdgeType(String label) → EdgeType
> > > - store(OutputStream): serialize the schema to a compact JSON
> > > representation
> > > - load(InputStream): deserialize and merge a schema from JSON into this
> > > schema graph
> > >
> > > EdgeType methods:
> > > - fromVertexTypes() → Collection<VertexType>
> > > - toVertexTypes() → Collection<VertexType>
> > >
> > > Example:
> > > ```
> > > GraphSchema schema = graph.schema();
> > >
> > > // Look up a vertex type
> > > VertexType person = schema.vertexType("person").orElseThrow();
> > >
> > > // Inspect its properties
> > > for (PropertyType pd : person.propertyTypes()) {
> > > System.out.println(pd.name() + " : " + pd.constraint("type"));
> > > }
> > >
> > > // Look up an edge type and its connectivity
> > > EdgeType knows = schema.edgeType("knows").orElseThrow();
> > > Collection<VertexType> fromTypes = knows.fromVertexTypes();
> > > Collection<VertexType> toTypes = knows.toVertexTypes();
> > > ```
> > >
> > > ### 6. Cross-graph jumps
> > >
> > > Two steps bridge the data graph and schema graph:
> > >
> > > - type(): from a data traversal, jump to the element's type definition
> in
> > > the schema graph.
> > > - instances(): from a schema traversal, jump to all matching elements
> in
> > > the data graph.
> > >
> > > These compose for round-trip traversals:
> > > ```
> > > // Get the type definition for "person" vertices
> > > g.V().hasLabel("person").type()
> > >
> > > // Get all instances of a schema type
> > > schema.traversal().vertexType("person").instances()
> > >
> > > // Round-trip: find marko's type, then get all instances of that type
> > > g.V().has("person", "name", "marko").type().instances()
> > > ```
> > >
> > > ### 7. Schema restriction strategy
> > >
> > > There are some steps we will want to restrict in both the data graph
> and
> > > the schema-graph. addVType() wouldn’t make sense in the data-graph, nor
> > > would addV() be sensible in the schema-graph. A TraversalStrategy can
> > > restrict schema traversals to a safe subset of Gremlin steps
> > > (allowlist-based). This prevents accidentally running data element
> > > insertions, OLAP computations, complex control flow, or side-effect
> steps
> > > against the schema graph. The strategy should be auto-registered when
> > > traversing a GraphSchema instance.
> > >
> > > The exact allowlist should be a topic for later discussion.
> > >
> > > ### 8. Instance counts on type vertices
> > >
> > > VertexType.instanceCount() and EdgeType.instanceCount() return the
> count
> > > of data graph elements matching each type. This is a method rather
> than a
> > > property on the type vertex, keeping the schema graph definitional (not
> > > statistical) and giving providers full implementation flexibility.
> > >
> > > Approximate counts are likely acceptable and preferable for
> performance in
> > > most cases. However, TinkerPop should not stand in the way of providers
> > > that prefer exact counts, and should ensure that appropriate hooks are
> in
> > > place in reference implementations so that providers can maintain exact
> > > counts if they so desire.
> > >
> > > Transactional implications need additional consideration. Maintaining
> > > accurate counts across concurrent writes, rollbacks, and transaction
> > > isolation levels adds significant complexity. This interacts with the
> > > broader schema transactions question (see transactions below) and
> should be
> > > addressed alongside it.
> > >
> > > ### 9. GLV Support
> > >
> > > Each GLV (Python, JavaScript, .NET, Go) needs:
> > >
> > > - Schema data classes: Parallel classes to the 4 core Java interfaces,
> > > following the same pattern as existing Vertex and Edge classes. These
> are
> > > data containers representing schema objects returned from the server:
> > > - GraphSchema: holds collections of VertexTypes and EdgeTypes
> > > - VertexType: label, full constraints map, and collection of
> > > PropertyTypes
> > > - EdgeType: label, full constraints map, from/to VertexType
> references
> > > (same pattern as Edge.outV/Edge.inV), and collection of PropertyTypes
> > > - PropertyType: name and full constraints map (including data type
> as a
> > > constraint)
> > > - All new gremlin steps are supported from each GLV
> > >
> > > ## Future Questions
> > >
> > > ### Schema validation
> > >
> > > Providers will need lots of flexibility regarding validation modes.
> Some
> > > providers may choose to have write-time validation for all inserts,
> others
> > > may choose validate an entire graph against a schema as a batch job,
> while
> > > others may choose to validate on-commit. For our purposes, we need to
> > > provide a viable reference implementation, as well as ensuring
> sufficient
> > > extension points exist for providers to fulfill their needs.
> > >
> > > ### Dynamic schema updates from data writes
> > >
> > > It would be useful to auto-update the schema graph when data writes
> > > introduce new labels or properties (e.g. addV("newLabel”) automatically
> > > creates a VertexType). Keeping the schema exactly in-sync with such
> > > operations may introduce too much overhead for many purposes. We should
> > > provide appropriate hooks for providers to implement such behaviour if
> > > desired, or to help providers aggregate changes and perform incremental
> > > batch updates to the schema.
> > >
> > > ### Transactions
> > >
> > > The schema graph will need to be transactional if the data
> > >
> > > ### File IO
> > >
> > > It is often useful to persist and load schemas to/from files. This
> > > capability should be build into the GraphSchema class via simple
> store()
> > > and load() methods, using a custom compact JSON representation of the
> > > schema. The specifics of this format are deferred to later discussion.
> > >
> > > GraphSchema exposes file IO directly:
> > > - store(OutputStream): serialize the schema to a compact JSON
> > > representation
> > > - load(InputStream): deserialize a schema from JSON and merge it into
> the
> > > current schema graph
> > >
> > > Schema file IO should be implemented across all GLVs.
> > >
> > > ## Reference Implementation
> > >
> > > TinkerGraph serves as the reference implementation:
> > >
> > > - TinkerGraphSchema extends TinkerGraph implements GraphSchema
> > > - TinkerVertexType extends TinkerVertex implements VertexType
> > > - TinkerPropertyType extends TinkerVertex implements PropertyType
> > > - TinkerEdgeType extends TinkerVertex implements EdgeType
> > > - Recursion guard prevents schema-of-schema (TinkerGraphSchema
> overrides
> > > initSchema())
> > >
> > >
> > > Please let me know any thoughts you may have on the approach. I intend
> to
> > > move this into a proposal PR soon, unless there are any major
> disagreements
> > > over the design.
> > >
> > > Thanks,
> > > Cole
> > >
> >
>
Re: [DISCUSS] Graph Schema Interfaces for TP
Posted by Cole Greer <co...@apache.org>.
Hi Josh,
I agree that the property graph model from Hydra should adapt well for our purposes in TinkerPop. My main concern regarding schema types is that providers aren't unnecessarily burdened when trying to map types from an existing proprietary schema into our new interfaces. I like the idea of our schema types supporting complex composite and union types, however my primary goal is to keep this as simple for providers to adopt as possible.
Thanks,
Cole
On 2026/04/03 03:31:12 Joshua Shinavier wrote:
> Hi Cole. This looks good to me. With respect to the schema types, would you
> please review the property graph model I showed a couple of meetings ago --
> here
> <https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/package-summary.html>
> --
> and let me know if you have any feedback. The types of interest start with
> GraphSchema
> <https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/GraphSchema.html>.
> Ignore Hydra-specific details like PersistentMap/ConsList/Name, etc. If the
> structure is agreeable, I will map the types into a format suitable for use
> in your PR, in an org.apache.tinkerpop namespace. There is an associated
> JSON format for interchange of GraphSchema and any other type we define in
> this way.
>
> Josh
>
>
>
> On Thu, Apr 2, 2026 at 7:17 PM Cole Greer via dev <de...@tinkerpop.apache.org>
> wrote:
>
> > Hi Everyone,
> >
> > The topic of Graph Schema has been discussed extensively in recent
> > TInkerPop Gatherings, and the following proposal has emerged from these
> > gatherings. I believe it is now ready for broad consideration and
> > discussions. I’ve done my best to incorporate initial feedback from Josh,
> > Pieter, Valentyn, Stephen, Kris and others into this proposal, however I
> > won’t claim that it accurately represents the views of anyone other than
> > myself at this time. This is a broad topic and I’m deliberately excluding
> > critical topics to focus this thread on standardizing interfaces for
> > gremlin users and providers to interact with schema (see assumptions for
> > more details).
> >
> > ## Overview
> >
> > This proposal introduces graph schema interfaces for TinkerPop: a way to
> > define vertex types, edge types, and property types as a meta-graph that is
> > itself traversable with Gremlin. The schema describes the structure of a
> > data graph; what kinds of vertices and edges exist, what properties they
> > carry, and how they connect..
> >
> > ## Assumptions
> >
> > - Type keys are element labels: there is a 1-to-1 mapping between a label
> > and a type definition. A vertex labeled "person" corresponds to exactly one
> > VertexType, and an edge labeled "knows" corresponds to exactly one EdgeType.
> > - Java classes are used as a type system: This proposal uses Java classes
> > to define property type constraints. This is intended as a placeholder to
> > be replaced by a proper type system to be defined via a later discussion.
> > - This proposal makes very little consideration of if/when/where/how
> > validation and enforcement of schema takes place. I believe it is important
> > for us to ship something which is flexible and useful to providers out of
> > the box as well as leaving space for providers to plugin existing
> > implementations or build their own if they desire. I’ve left this out of
> > scope for this proposal to focus first on interfaces which give providers
> > the appropriate access to schema.
> >
> > ## Design Points
> >
> > ### 1. Schema-as-Graph
> >
> > `GraphSchema extends Graph`. Providers implement a familiar interface, and
> > users traverse the schema with schema.traversal(). This avoids inventing a
> > parallel API surface. The schema is just another graph.
> >
> > A data graph exposes its schema via Graph.schema(), which returns the
> > GraphSchema instance. Providers that don't support schema return
> > UnsupportedOperationException by default.
> >
> > ### 2. All type definitions are vertices
> >
> > VertexType, EdgeType, and PropertyType are all vertices in the schema
> > meta-graph.
> >
> > - A VertexType vertex represents a vertex label definition (e.g. "person",
> > "software").
> > - An EdgeType vertex represents an edge label definition (e.g. "knows",
> > "created"). Even though it describes edges in the data graph, it is itself
> > a vertex in the schema graph, connected to its endpoint VertexType vertices
> > via from/to edges.
> > - A PropertyType vertex represents a property on a type, connected to its
> > parent type vertex via a “hasProperty" edge.
> >
> > Property definitions are independent per type, no sharing across types.
> >
> > Schema graph example for the classic TinkerPop modern graph:
> > ```
> > (person:vertexType) --hasProperty--> (name:propertyType)
> > (person:vertexType) --hasProperty--> (age:propertyType)
> > (software:vertexType) --hasProperty--> (name:propertyType)
> > (software:vertexType) --hasProperty--> (lang:propertyType)
> > (knows:edgeType) --from--> (person:vertexType)
> > (knows:edgeType) --to--> (person:vertexType)
> > (knows:edgeType) --hasProperty--> (weight:propertyType)
> > (created:edgeType) --from--> (person:vertexType)
> > (created:edgeType) --to--> (software:vertexType)
> > (created:edgeType) --hasProperty--> (weight:propertyType)
> > ```
> >
> > ### 3. Constraints are properties on type vertices
> >
> > Rather than a fixed constraint taxonomy, constraints are regular
> > properties on type vertices, keyed by string via constraint(key, value).
> > This keeps the model extensible such that providers can define their own
> > constraints without changes to the core API.
> >
> > Constraints can be added to VertexType, EdgeType, and PropertyType
> > vertices directly. The most common constraints such as property types and
> > required properties would apply to PropertyTypes, while edge multiplicity
> > constraints (e.g. one-to-many, one-to-one) are naturally expressed as
> > constraints on the EdgeType itself rather than on any property.
> >
> > While constraint keys are arbitrary strings and providers are free to
> > implement any constraints they like, TinkerPop should standardize a set of
> > core constraint keys representing the most common constraints. Examples
> > include “type", “required", “unique", “minValue", “maxValue", etc.
> > Providers that support equivalent constraints are encouraged to follow
> > these conventional names for interoperability.
> >
> > Non-core constraints (custom to a provider) are encouraged to follow a
> > namespaced key convention to avoid collisions, e.g. "tinkergraph:notNull".
> > Core constraint keys are unnamespaced.
> >
> > ### 4. Schema traversal steps in core Gremlin
> >
> > New steps for schema manipulation live directly in
> > GraphTraversal/GraphTraversalSource, not in a separate DSL:
> >
> > - addVType(label) — creates a VertexType vertex
> > - addEType(label) — creates an EdgeType vertex
> > - propertyType(name) — creates a PropertyType vertex and connects it via
> > hasProperty
> > - constraint(key, value) — adds a constraint property to the current type
> > vertex
> >
> > Example: defining a vertex type with properties:
> > ```
> > schema.traversal().addVType("person")
> > .propertyType("name").constraint("type",
> > String.class).constraint("required", true).constraint("unique", true)
> > .propertyType("age").constraint("type", Integer.class)
> > ```
> >
> > Example: defining an edge type with endpoint types and a property:
> > ```
> > schema.traversal().addEType("knows")
> > .from("person").to("person")
> > .propertyType("weight").constraint("type", Double.class)
> > ```
> >
> > This mirrors the addE().from().to() pattern from the data-graph. Here
> > from() and to() take vertex type labels (strings) and create from/to edges
> > in the schema graph connecting the EdgeType to the referenced VertexType
> > vertices.
> >
> > ### 5. Convenience methods for direct access
> >
> > The schema-as-graph model is the source of truth, but traversing it for
> > simple lookups isn’t always convenient. Direct methods provide compact
> > access:
> >
> > GraphSchema methods:
> > - vertexTypes() → Collection<VertexType>
> > - vertexType(String label) → Optional<VertexType>
> > - edgeTypes() → Collection<EdgeType>
> > - edgeType(String label) → Optional<EdgeType>
> > - addVertexType(String label) → VertexType
> > - addEdgeType(String label) → EdgeType
> > - store(OutputStream): serialize the schema to a compact JSON
> > representation
> > - load(InputStream): deserialize and merge a schema from JSON into this
> > schema graph
> >
> > EdgeType methods:
> > - fromVertexTypes() → Collection<VertexType>
> > - toVertexTypes() → Collection<VertexType>
> >
> > Example:
> > ```
> > GraphSchema schema = graph.schema();
> >
> > // Look up a vertex type
> > VertexType person = schema.vertexType("person").orElseThrow();
> >
> > // Inspect its properties
> > for (PropertyType pd : person.propertyTypes()) {
> > System.out.println(pd.name() + " : " + pd.constraint("type"));
> > }
> >
> > // Look up an edge type and its connectivity
> > EdgeType knows = schema.edgeType("knows").orElseThrow();
> > Collection<VertexType> fromTypes = knows.fromVertexTypes();
> > Collection<VertexType> toTypes = knows.toVertexTypes();
> > ```
> >
> > ### 6. Cross-graph jumps
> >
> > Two steps bridge the data graph and schema graph:
> >
> > - type(): from a data traversal, jump to the element's type definition in
> > the schema graph.
> > - instances(): from a schema traversal, jump to all matching elements in
> > the data graph.
> >
> > These compose for round-trip traversals:
> > ```
> > // Get the type definition for "person" vertices
> > g.V().hasLabel("person").type()
> >
> > // Get all instances of a schema type
> > schema.traversal().vertexType("person").instances()
> >
> > // Round-trip: find marko's type, then get all instances of that type
> > g.V().has("person", "name", "marko").type().instances()
> > ```
> >
> > ### 7. Schema restriction strategy
> >
> > There are some steps we will want to restrict in both the data graph and
> > the schema-graph. addVType() wouldn’t make sense in the data-graph, nor
> > would addV() be sensible in the schema-graph. A TraversalStrategy can
> > restrict schema traversals to a safe subset of Gremlin steps
> > (allowlist-based). This prevents accidentally running data element
> > insertions, OLAP computations, complex control flow, or side-effect steps
> > against the schema graph. The strategy should be auto-registered when
> > traversing a GraphSchema instance.
> >
> > The exact allowlist should be a topic for later discussion.
> >
> > ### 8. Instance counts on type vertices
> >
> > VertexType.instanceCount() and EdgeType.instanceCount() return the count
> > of data graph elements matching each type. This is a method rather than a
> > property on the type vertex, keeping the schema graph definitional (not
> > statistical) and giving providers full implementation flexibility.
> >
> > Approximate counts are likely acceptable and preferable for performance in
> > most cases. However, TinkerPop should not stand in the way of providers
> > that prefer exact counts, and should ensure that appropriate hooks are in
> > place in reference implementations so that providers can maintain exact
> > counts if they so desire.
> >
> > Transactional implications need additional consideration. Maintaining
> > accurate counts across concurrent writes, rollbacks, and transaction
> > isolation levels adds significant complexity. This interacts with the
> > broader schema transactions question (see transactions below) and should be
> > addressed alongside it.
> >
> > ### 9. GLV Support
> >
> > Each GLV (Python, JavaScript, .NET, Go) needs:
> >
> > - Schema data classes: Parallel classes to the 4 core Java interfaces,
> > following the same pattern as existing Vertex and Edge classes. These are
> > data containers representing schema objects returned from the server:
> > - GraphSchema: holds collections of VertexTypes and EdgeTypes
> > - VertexType: label, full constraints map, and collection of
> > PropertyTypes
> > - EdgeType: label, full constraints map, from/to VertexType references
> > (same pattern as Edge.outV/Edge.inV), and collection of PropertyTypes
> > - PropertyType: name and full constraints map (including data type as a
> > constraint)
> > - All new gremlin steps are supported from each GLV
> >
> > ## Future Questions
> >
> > ### Schema validation
> >
> > Providers will need lots of flexibility regarding validation modes. Some
> > providers may choose to have write-time validation for all inserts, others
> > may choose validate an entire graph against a schema as a batch job, while
> > others may choose to validate on-commit. For our purposes, we need to
> > provide a viable reference implementation, as well as ensuring sufficient
> > extension points exist for providers to fulfill their needs.
> >
> > ### Dynamic schema updates from data writes
> >
> > It would be useful to auto-update the schema graph when data writes
> > introduce new labels or properties (e.g. addV("newLabel”) automatically
> > creates a VertexType). Keeping the schema exactly in-sync with such
> > operations may introduce too much overhead for many purposes. We should
> > provide appropriate hooks for providers to implement such behaviour if
> > desired, or to help providers aggregate changes and perform incremental
> > batch updates to the schema.
> >
> > ### Transactions
> >
> > The schema graph will need to be transactional if the data
> >
> > ### File IO
> >
> > It is often useful to persist and load schemas to/from files. This
> > capability should be build into the GraphSchema class via simple store()
> > and load() methods, using a custom compact JSON representation of the
> > schema. The specifics of this format are deferred to later discussion.
> >
> > GraphSchema exposes file IO directly:
> > - store(OutputStream): serialize the schema to a compact JSON
> > representation
> > - load(InputStream): deserialize a schema from JSON and merge it into the
> > current schema graph
> >
> > Schema file IO should be implemented across all GLVs.
> >
> > ## Reference Implementation
> >
> > TinkerGraph serves as the reference implementation:
> >
> > - TinkerGraphSchema extends TinkerGraph implements GraphSchema
> > - TinkerVertexType extends TinkerVertex implements VertexType
> > - TinkerPropertyType extends TinkerVertex implements PropertyType
> > - TinkerEdgeType extends TinkerVertex implements EdgeType
> > - Recursion guard prevents schema-of-schema (TinkerGraphSchema overrides
> > initSchema())
> >
> >
> > Please let me know any thoughts you may have on the approach. I intend to
> > move this into a proposal PR soon, unless there are any major disagreements
> > over the design.
> >
> > Thanks,
> > Cole
> >
>
Re: [DISCUSS] Graph Schema Interfaces for TP
Posted by Joshua Shinavier <jo...@gmail.com>.
FYI, here is a demo I put out this morning
<https://www.linkedin.com/posts/joshuashinavier_graph-validation-in-any-language-bringing-activity-7446966175328116736-H3iV?utm_source=social_share_send&utm_medium=member_desktop_web&rcm=ACoAAADERlgBnRuaTav2453BsDPJDQ18M4aAtks>
on property graph validation using Hydra. This is complementary to Cole's
proposal for integrating schema management into the developer workflow. In
the video, I show *how* validation executes, and specifically how the
validation logic itself can be defined in such a way as to be available in
multiple languages, serving different GLVs. GraphSchema and the related
types are visible, as well.
This is one way different providers and application developers might choose
to use or extend predefined validation rules bundled with TinkerPop,
offering some flexibility w.r.t. whether the validation happens on the
back-end, in the client, and/or in a middle tier.
Josh
On Thu, Apr 2, 2026 at 8:31 PM Joshua Shinavier <jo...@gmail.com>
wrote:
> Hi Cole. This looks good to me. With respect to the schema types, would
> you please review the property graph model I showed a couple of meetings
> ago -- here
> <https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/package-summary.html> --
> and let me know if you have any feedback. The types of interest start with
> GraphSchema
> <https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/GraphSchema.html>.
> Ignore Hydra-specific details like PersistentMap/ConsList/Name, etc. If the
> structure is agreeable, I will map the types into a format suitable for use
> in your PR, in an org.apache.tinkerpop namespace. There is an associated
> JSON format for interchange of GraphSchema and any other type we define in
> this way.
>
> Josh
>
>
>
> On Thu, Apr 2, 2026 at 7:17 PM Cole Greer via dev <
> dev@tinkerpop.apache.org> wrote:
>
>> Hi Everyone,
>>
>> The topic of Graph Schema has been discussed extensively in recent
>> TInkerPop Gatherings, and the following proposal has emerged from these
>> gatherings. I believe it is now ready for broad consideration and
>> discussions. I’ve done my best to incorporate initial feedback from Josh,
>> Pieter, Valentyn, Stephen, Kris and others into this proposal, however I
>> won’t claim that it accurately represents the views of anyone other than
>> myself at this time. This is a broad topic and I’m deliberately excluding
>> critical topics to focus this thread on standardizing interfaces for
>> gremlin users and providers to interact with schema (see assumptions for
>> more details).
>>
>> ## Overview
>>
>> This proposal introduces graph schema interfaces for TinkerPop: a way to
>> define vertex types, edge types, and property types as a meta-graph that is
>> itself traversable with Gremlin. The schema describes the structure of a
>> data graph; what kinds of vertices and edges exist, what properties they
>> carry, and how they connect..
>>
>> ## Assumptions
>>
>> - Type keys are element labels: there is a 1-to-1 mapping between a label
>> and a type definition. A vertex labeled "person" corresponds to exactly one
>> VertexType, and an edge labeled "knows" corresponds to exactly one EdgeType.
>> - Java classes are used as a type system: This proposal uses Java classes
>> to define property type constraints. This is intended as a placeholder to
>> be replaced by a proper type system to be defined via a later discussion.
>> - This proposal makes very little consideration of if/when/where/how
>> validation and enforcement of schema takes place. I believe it is important
>> for us to ship something which is flexible and useful to providers out of
>> the box as well as leaving space for providers to plugin existing
>> implementations or build their own if they desire. I’ve left this out of
>> scope for this proposal to focus first on interfaces which give providers
>> the appropriate access to schema.
>>
>> ## Design Points
>>
>> ### 1. Schema-as-Graph
>>
>> `GraphSchema extends Graph`. Providers implement a familiar interface,
>> and users traverse the schema with schema.traversal(). This avoids
>> inventing a parallel API surface. The schema is just another graph.
>>
>> A data graph exposes its schema via Graph.schema(), which returns the
>> GraphSchema instance. Providers that don't support schema return
>> UnsupportedOperationException by default.
>>
>> ### 2. All type definitions are vertices
>>
>> VertexType, EdgeType, and PropertyType are all vertices in the schema
>> meta-graph.
>>
>> - A VertexType vertex represents a vertex label definition (e.g.
>> "person", "software").
>> - An EdgeType vertex represents an edge label definition (e.g. "knows",
>> "created"). Even though it describes edges in the data graph, it is itself
>> a vertex in the schema graph, connected to its endpoint VertexType vertices
>> via from/to edges.
>> - A PropertyType vertex represents a property on a type, connected to its
>> parent type vertex via a “hasProperty" edge.
>>
>> Property definitions are independent per type, no sharing across types.
>>
>> Schema graph example for the classic TinkerPop modern graph:
>> ```
>> (person:vertexType) --hasProperty--> (name:propertyType)
>> (person:vertexType) --hasProperty--> (age:propertyType)
>> (software:vertexType) --hasProperty--> (name:propertyType)
>> (software:vertexType) --hasProperty--> (lang:propertyType)
>> (knows:edgeType) --from--> (person:vertexType)
>> (knows:edgeType) --to--> (person:vertexType)
>> (knows:edgeType) --hasProperty--> (weight:propertyType)
>> (created:edgeType) --from--> (person:vertexType)
>> (created:edgeType) --to--> (software:vertexType)
>> (created:edgeType) --hasProperty--> (weight:propertyType)
>> ```
>>
>> ### 3. Constraints are properties on type vertices
>>
>> Rather than a fixed constraint taxonomy, constraints are regular
>> properties on type vertices, keyed by string via constraint(key, value).
>> This keeps the model extensible such that providers can define their own
>> constraints without changes to the core API.
>>
>> Constraints can be added to VertexType, EdgeType, and PropertyType
>> vertices directly. The most common constraints such as property types and
>> required properties would apply to PropertyTypes, while edge multiplicity
>> constraints (e.g. one-to-many, one-to-one) are naturally expressed as
>> constraints on the EdgeType itself rather than on any property.
>>
>> While constraint keys are arbitrary strings and providers are free to
>> implement any constraints they like, TinkerPop should standardize a set of
>> core constraint keys representing the most common constraints. Examples
>> include “type", “required", “unique", “minValue", “maxValue", etc.
>> Providers that support equivalent constraints are encouraged to follow
>> these conventional names for interoperability.
>>
>> Non-core constraints (custom to a provider) are encouraged to follow a
>> namespaced key convention to avoid collisions, e.g. "tinkergraph:notNull".
>> Core constraint keys are unnamespaced.
>>
>> ### 4. Schema traversal steps in core Gremlin
>>
>> New steps for schema manipulation live directly in
>> GraphTraversal/GraphTraversalSource, not in a separate DSL:
>>
>> - addVType(label) — creates a VertexType vertex
>> - addEType(label) — creates an EdgeType vertex
>> - propertyType(name) — creates a PropertyType vertex and connects it via
>> hasProperty
>> - constraint(key, value) — adds a constraint property to the current type
>> vertex
>>
>> Example: defining a vertex type with properties:
>> ```
>> schema.traversal().addVType("person")
>> .propertyType("name").constraint("type",
>> String.class).constraint("required", true).constraint("unique", true)
>> .propertyType("age").constraint("type", Integer.class)
>> ```
>>
>> Example: defining an edge type with endpoint types and a property:
>> ```
>> schema.traversal().addEType("knows")
>> .from("person").to("person")
>> .propertyType("weight").constraint("type", Double.class)
>> ```
>>
>> This mirrors the addE().from().to() pattern from the data-graph. Here
>> from() and to() take vertex type labels (strings) and create from/to edges
>> in the schema graph connecting the EdgeType to the referenced VertexType
>> vertices.
>>
>> ### 5. Convenience methods for direct access
>>
>> The schema-as-graph model is the source of truth, but traversing it for
>> simple lookups isn’t always convenient. Direct methods provide compact
>> access:
>>
>> GraphSchema methods:
>> - vertexTypes() → Collection<VertexType>
>> - vertexType(String label) → Optional<VertexType>
>> - edgeTypes() → Collection<EdgeType>
>> - edgeType(String label) → Optional<EdgeType>
>> - addVertexType(String label) → VertexType
>> - addEdgeType(String label) → EdgeType
>> - store(OutputStream): serialize the schema to a compact JSON
>> representation
>> - load(InputStream): deserialize and merge a schema from JSON into this
>> schema graph
>>
>> EdgeType methods:
>> - fromVertexTypes() → Collection<VertexType>
>> - toVertexTypes() → Collection<VertexType>
>>
>> Example:
>> ```
>> GraphSchema schema = graph.schema();
>>
>> // Look up a vertex type
>> VertexType person = schema.vertexType("person").orElseThrow();
>>
>> // Inspect its properties
>> for (PropertyType pd : person.propertyTypes()) {
>> System.out.println(pd.name() + " : " + pd.constraint("type"));
>> }
>>
>> // Look up an edge type and its connectivity
>> EdgeType knows = schema.edgeType("knows").orElseThrow();
>> Collection<VertexType> fromTypes = knows.fromVertexTypes();
>> Collection<VertexType> toTypes = knows.toVertexTypes();
>> ```
>>
>> ### 6. Cross-graph jumps
>>
>> Two steps bridge the data graph and schema graph:
>>
>> - type(): from a data traversal, jump to the element's type definition in
>> the schema graph.
>> - instances(): from a schema traversal, jump to all matching elements in
>> the data graph.
>>
>> These compose for round-trip traversals:
>> ```
>> // Get the type definition for "person" vertices
>> g.V().hasLabel("person").type()
>>
>> // Get all instances of a schema type
>> schema.traversal().vertexType("person").instances()
>>
>> // Round-trip: find marko's type, then get all instances of that type
>> g.V().has("person", "name", "marko").type().instances()
>> ```
>>
>> ### 7. Schema restriction strategy
>>
>> There are some steps we will want to restrict in both the data graph and
>> the schema-graph. addVType() wouldn’t make sense in the data-graph, nor
>> would addV() be sensible in the schema-graph. A TraversalStrategy can
>> restrict schema traversals to a safe subset of Gremlin steps
>> (allowlist-based). This prevents accidentally running data element
>> insertions, OLAP computations, complex control flow, or side-effect steps
>> against the schema graph. The strategy should be auto-registered when
>> traversing a GraphSchema instance.
>>
>> The exact allowlist should be a topic for later discussion.
>>
>> ### 8. Instance counts on type vertices
>>
>> VertexType.instanceCount() and EdgeType.instanceCount() return the count
>> of data graph elements matching each type. This is a method rather than a
>> property on the type vertex, keeping the schema graph definitional (not
>> statistical) and giving providers full implementation flexibility.
>>
>> Approximate counts are likely acceptable and preferable for performance
>> in most cases. However, TinkerPop should not stand in the way of providers
>> that prefer exact counts, and should ensure that appropriate hooks are in
>> place in reference implementations so that providers can maintain exact
>> counts if they so desire.
>>
>> Transactional implications need additional consideration. Maintaining
>> accurate counts across concurrent writes, rollbacks, and transaction
>> isolation levels adds significant complexity. This interacts with the
>> broader schema transactions question (see transactions below) and should be
>> addressed alongside it.
>>
>> ### 9. GLV Support
>>
>> Each GLV (Python, JavaScript, .NET, Go) needs:
>>
>> - Schema data classes: Parallel classes to the 4 core Java interfaces,
>> following the same pattern as existing Vertex and Edge classes. These are
>> data containers representing schema objects returned from the server:
>> - GraphSchema: holds collections of VertexTypes and EdgeTypes
>> - VertexType: label, full constraints map, and collection of
>> PropertyTypes
>> - EdgeType: label, full constraints map, from/to VertexType references
>> (same pattern as Edge.outV/Edge.inV), and collection of PropertyTypes
>> - PropertyType: name and full constraints map (including data type as a
>> constraint)
>> - All new gremlin steps are supported from each GLV
>>
>> ## Future Questions
>>
>> ### Schema validation
>>
>> Providers will need lots of flexibility regarding validation modes. Some
>> providers may choose to have write-time validation for all inserts, others
>> may choose validate an entire graph against a schema as a batch job, while
>> others may choose to validate on-commit. For our purposes, we need to
>> provide a viable reference implementation, as well as ensuring sufficient
>> extension points exist for providers to fulfill their needs.
>>
>> ### Dynamic schema updates from data writes
>>
>> It would be useful to auto-update the schema graph when data writes
>> introduce new labels or properties (e.g. addV("newLabel”) automatically
>> creates a VertexType). Keeping the schema exactly in-sync with such
>> operations may introduce too much overhead for many purposes. We should
>> provide appropriate hooks for providers to implement such behaviour if
>> desired, or to help providers aggregate changes and perform incremental
>> batch updates to the schema.
>>
>> ### Transactions
>>
>> The schema graph will need to be transactional if the data
>>
>> ### File IO
>>
>> It is often useful to persist and load schemas to/from files. This
>> capability should be build into the GraphSchema class via simple store()
>> and load() methods, using a custom compact JSON representation of the
>> schema. The specifics of this format are deferred to later discussion.
>>
>> GraphSchema exposes file IO directly:
>> - store(OutputStream): serialize the schema to a compact JSON
>> representation
>> - load(InputStream): deserialize a schema from JSON and merge it into the
>> current schema graph
>>
>> Schema file IO should be implemented across all GLVs.
>>
>> ## Reference Implementation
>>
>> TinkerGraph serves as the reference implementation:
>>
>> - TinkerGraphSchema extends TinkerGraph implements GraphSchema
>> - TinkerVertexType extends TinkerVertex implements VertexType
>> - TinkerPropertyType extends TinkerVertex implements PropertyType
>> - TinkerEdgeType extends TinkerVertex implements EdgeType
>> - Recursion guard prevents schema-of-schema (TinkerGraphSchema overrides
>> initSchema())
>>
>>
>> Please let me know any thoughts you may have on the approach. I intend to
>> move this into a proposal PR soon, unless there are any major disagreements
>> over the design.
>>
>> Thanks,
>> Cole
>>
>
Re: [DISCUSS] Graph Schema Interfaces for TP
Posted by Joshua Shinavier <jo...@gmail.com>.
Hi Cole. This looks good to me. With respect to the schema types, would you
please review the property graph model I showed a couple of meetings ago --
here
<https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/package-summary.html>
--
and let me know if you have any feedback. The types of interest start with
GraphSchema
<https://categoricaldata.net/hydra/hydra-java/javadoc/hydra/pg/model/GraphSchema.html>.
Ignore Hydra-specific details like PersistentMap/ConsList/Name, etc. If the
structure is agreeable, I will map the types into a format suitable for use
in your PR, in an org.apache.tinkerpop namespace. There is an associated
JSON format for interchange of GraphSchema and any other type we define in
this way.
Josh
On Thu, Apr 2, 2026 at 7:17 PM Cole Greer via dev <de...@tinkerpop.apache.org>
wrote:
> Hi Everyone,
>
> The topic of Graph Schema has been discussed extensively in recent
> TInkerPop Gatherings, and the following proposal has emerged from these
> gatherings. I believe it is now ready for broad consideration and
> discussions. I’ve done my best to incorporate initial feedback from Josh,
> Pieter, Valentyn, Stephen, Kris and others into this proposal, however I
> won’t claim that it accurately represents the views of anyone other than
> myself at this time. This is a broad topic and I’m deliberately excluding
> critical topics to focus this thread on standardizing interfaces for
> gremlin users and providers to interact with schema (see assumptions for
> more details).
>
> ## Overview
>
> This proposal introduces graph schema interfaces for TinkerPop: a way to
> define vertex types, edge types, and property types as a meta-graph that is
> itself traversable with Gremlin. The schema describes the structure of a
> data graph; what kinds of vertices and edges exist, what properties they
> carry, and how they connect..
>
> ## Assumptions
>
> - Type keys are element labels: there is a 1-to-1 mapping between a label
> and a type definition. A vertex labeled "person" corresponds to exactly one
> VertexType, and an edge labeled "knows" corresponds to exactly one EdgeType.
> - Java classes are used as a type system: This proposal uses Java classes
> to define property type constraints. This is intended as a placeholder to
> be replaced by a proper type system to be defined via a later discussion.
> - This proposal makes very little consideration of if/when/where/how
> validation and enforcement of schema takes place. I believe it is important
> for us to ship something which is flexible and useful to providers out of
> the box as well as leaving space for providers to plugin existing
> implementations or build their own if they desire. I’ve left this out of
> scope for this proposal to focus first on interfaces which give providers
> the appropriate access to schema.
>
> ## Design Points
>
> ### 1. Schema-as-Graph
>
> `GraphSchema extends Graph`. Providers implement a familiar interface, and
> users traverse the schema with schema.traversal(). This avoids inventing a
> parallel API surface. The schema is just another graph.
>
> A data graph exposes its schema via Graph.schema(), which returns the
> GraphSchema instance. Providers that don't support schema return
> UnsupportedOperationException by default.
>
> ### 2. All type definitions are vertices
>
> VertexType, EdgeType, and PropertyType are all vertices in the schema
> meta-graph.
>
> - A VertexType vertex represents a vertex label definition (e.g. "person",
> "software").
> - An EdgeType vertex represents an edge label definition (e.g. "knows",
> "created"). Even though it describes edges in the data graph, it is itself
> a vertex in the schema graph, connected to its endpoint VertexType vertices
> via from/to edges.
> - A PropertyType vertex represents a property on a type, connected to its
> parent type vertex via a “hasProperty" edge.
>
> Property definitions are independent per type, no sharing across types.
>
> Schema graph example for the classic TinkerPop modern graph:
> ```
> (person:vertexType) --hasProperty--> (name:propertyType)
> (person:vertexType) --hasProperty--> (age:propertyType)
> (software:vertexType) --hasProperty--> (name:propertyType)
> (software:vertexType) --hasProperty--> (lang:propertyType)
> (knows:edgeType) --from--> (person:vertexType)
> (knows:edgeType) --to--> (person:vertexType)
> (knows:edgeType) --hasProperty--> (weight:propertyType)
> (created:edgeType) --from--> (person:vertexType)
> (created:edgeType) --to--> (software:vertexType)
> (created:edgeType) --hasProperty--> (weight:propertyType)
> ```
>
> ### 3. Constraints are properties on type vertices
>
> Rather than a fixed constraint taxonomy, constraints are regular
> properties on type vertices, keyed by string via constraint(key, value).
> This keeps the model extensible such that providers can define their own
> constraints without changes to the core API.
>
> Constraints can be added to VertexType, EdgeType, and PropertyType
> vertices directly. The most common constraints such as property types and
> required properties would apply to PropertyTypes, while edge multiplicity
> constraints (e.g. one-to-many, one-to-one) are naturally expressed as
> constraints on the EdgeType itself rather than on any property.
>
> While constraint keys are arbitrary strings and providers are free to
> implement any constraints they like, TinkerPop should standardize a set of
> core constraint keys representing the most common constraints. Examples
> include “type", “required", “unique", “minValue", “maxValue", etc.
> Providers that support equivalent constraints are encouraged to follow
> these conventional names for interoperability.
>
> Non-core constraints (custom to a provider) are encouraged to follow a
> namespaced key convention to avoid collisions, e.g. "tinkergraph:notNull".
> Core constraint keys are unnamespaced.
>
> ### 4. Schema traversal steps in core Gremlin
>
> New steps for schema manipulation live directly in
> GraphTraversal/GraphTraversalSource, not in a separate DSL:
>
> - addVType(label) — creates a VertexType vertex
> - addEType(label) — creates an EdgeType vertex
> - propertyType(name) — creates a PropertyType vertex and connects it via
> hasProperty
> - constraint(key, value) — adds a constraint property to the current type
> vertex
>
> Example: defining a vertex type with properties:
> ```
> schema.traversal().addVType("person")
> .propertyType("name").constraint("type",
> String.class).constraint("required", true).constraint("unique", true)
> .propertyType("age").constraint("type", Integer.class)
> ```
>
> Example: defining an edge type with endpoint types and a property:
> ```
> schema.traversal().addEType("knows")
> .from("person").to("person")
> .propertyType("weight").constraint("type", Double.class)
> ```
>
> This mirrors the addE().from().to() pattern from the data-graph. Here
> from() and to() take vertex type labels (strings) and create from/to edges
> in the schema graph connecting the EdgeType to the referenced VertexType
> vertices.
>
> ### 5. Convenience methods for direct access
>
> The schema-as-graph model is the source of truth, but traversing it for
> simple lookups isn’t always convenient. Direct methods provide compact
> access:
>
> GraphSchema methods:
> - vertexTypes() → Collection<VertexType>
> - vertexType(String label) → Optional<VertexType>
> - edgeTypes() → Collection<EdgeType>
> - edgeType(String label) → Optional<EdgeType>
> - addVertexType(String label) → VertexType
> - addEdgeType(String label) → EdgeType
> - store(OutputStream): serialize the schema to a compact JSON
> representation
> - load(InputStream): deserialize and merge a schema from JSON into this
> schema graph
>
> EdgeType methods:
> - fromVertexTypes() → Collection<VertexType>
> - toVertexTypes() → Collection<VertexType>
>
> Example:
> ```
> GraphSchema schema = graph.schema();
>
> // Look up a vertex type
> VertexType person = schema.vertexType("person").orElseThrow();
>
> // Inspect its properties
> for (PropertyType pd : person.propertyTypes()) {
> System.out.println(pd.name() + " : " + pd.constraint("type"));
> }
>
> // Look up an edge type and its connectivity
> EdgeType knows = schema.edgeType("knows").orElseThrow();
> Collection<VertexType> fromTypes = knows.fromVertexTypes();
> Collection<VertexType> toTypes = knows.toVertexTypes();
> ```
>
> ### 6. Cross-graph jumps
>
> Two steps bridge the data graph and schema graph:
>
> - type(): from a data traversal, jump to the element's type definition in
> the schema graph.
> - instances(): from a schema traversal, jump to all matching elements in
> the data graph.
>
> These compose for round-trip traversals:
> ```
> // Get the type definition for "person" vertices
> g.V().hasLabel("person").type()
>
> // Get all instances of a schema type
> schema.traversal().vertexType("person").instances()
>
> // Round-trip: find marko's type, then get all instances of that type
> g.V().has("person", "name", "marko").type().instances()
> ```
>
> ### 7. Schema restriction strategy
>
> There are some steps we will want to restrict in both the data graph and
> the schema-graph. addVType() wouldn’t make sense in the data-graph, nor
> would addV() be sensible in the schema-graph. A TraversalStrategy can
> restrict schema traversals to a safe subset of Gremlin steps
> (allowlist-based). This prevents accidentally running data element
> insertions, OLAP computations, complex control flow, or side-effect steps
> against the schema graph. The strategy should be auto-registered when
> traversing a GraphSchema instance.
>
> The exact allowlist should be a topic for later discussion.
>
> ### 8. Instance counts on type vertices
>
> VertexType.instanceCount() and EdgeType.instanceCount() return the count
> of data graph elements matching each type. This is a method rather than a
> property on the type vertex, keeping the schema graph definitional (not
> statistical) and giving providers full implementation flexibility.
>
> Approximate counts are likely acceptable and preferable for performance in
> most cases. However, TinkerPop should not stand in the way of providers
> that prefer exact counts, and should ensure that appropriate hooks are in
> place in reference implementations so that providers can maintain exact
> counts if they so desire.
>
> Transactional implications need additional consideration. Maintaining
> accurate counts across concurrent writes, rollbacks, and transaction
> isolation levels adds significant complexity. This interacts with the
> broader schema transactions question (see transactions below) and should be
> addressed alongside it.
>
> ### 9. GLV Support
>
> Each GLV (Python, JavaScript, .NET, Go) needs:
>
> - Schema data classes: Parallel classes to the 4 core Java interfaces,
> following the same pattern as existing Vertex and Edge classes. These are
> data containers representing schema objects returned from the server:
> - GraphSchema: holds collections of VertexTypes and EdgeTypes
> - VertexType: label, full constraints map, and collection of
> PropertyTypes
> - EdgeType: label, full constraints map, from/to VertexType references
> (same pattern as Edge.outV/Edge.inV), and collection of PropertyTypes
> - PropertyType: name and full constraints map (including data type as a
> constraint)
> - All new gremlin steps are supported from each GLV
>
> ## Future Questions
>
> ### Schema validation
>
> Providers will need lots of flexibility regarding validation modes. Some
> providers may choose to have write-time validation for all inserts, others
> may choose validate an entire graph against a schema as a batch job, while
> others may choose to validate on-commit. For our purposes, we need to
> provide a viable reference implementation, as well as ensuring sufficient
> extension points exist for providers to fulfill their needs.
>
> ### Dynamic schema updates from data writes
>
> It would be useful to auto-update the schema graph when data writes
> introduce new labels or properties (e.g. addV("newLabel”) automatically
> creates a VertexType). Keeping the schema exactly in-sync with such
> operations may introduce too much overhead for many purposes. We should
> provide appropriate hooks for providers to implement such behaviour if
> desired, or to help providers aggregate changes and perform incremental
> batch updates to the schema.
>
> ### Transactions
>
> The schema graph will need to be transactional if the data
>
> ### File IO
>
> It is often useful to persist and load schemas to/from files. This
> capability should be build into the GraphSchema class via simple store()
> and load() methods, using a custom compact JSON representation of the
> schema. The specifics of this format are deferred to later discussion.
>
> GraphSchema exposes file IO directly:
> - store(OutputStream): serialize the schema to a compact JSON
> representation
> - load(InputStream): deserialize a schema from JSON and merge it into the
> current schema graph
>
> Schema file IO should be implemented across all GLVs.
>
> ## Reference Implementation
>
> TinkerGraph serves as the reference implementation:
>
> - TinkerGraphSchema extends TinkerGraph implements GraphSchema
> - TinkerVertexType extends TinkerVertex implements VertexType
> - TinkerPropertyType extends TinkerVertex implements PropertyType
> - TinkerEdgeType extends TinkerVertex implements EdgeType
> - Recursion guard prevents schema-of-schema (TinkerGraphSchema overrides
> initSchema())
>
>
> Please let me know any thoughts you may have on the approach. I intend to
> move this into a proposal PR soon, unless there are any major disagreements
> over the design.
>
> Thanks,
> Cole
>
Re: [DISCUSS] Graph Schema Interfaces for TP
Posted by Cole Greer <co...@apache.org>.
Hi Pieter,
When considering the graph representation of the schema, I suppose the equivalent to your EdgeRoles would be the actual from and to edges between EdgeType and NodeType vertices:
(created:edgeType) --from--> (person:vertexType)
(created:edgeType) --to--> (software:vertexType)
I was considering these as simple edges without properties while drafting the proposal, but you're right that if we want to allow A loves B and A loves C to have distinct multiplicity constraints, those should be modeled as properties on the from/to edge (equivalent to the EdgeRole triple). We could add an equivalent EdgeRole class to the structure API which maps to these edges in the schema-graph, just like how EdgeType and VertexType map to nodes in the schema-graph. Each EdgeRole would be as you suggest a triple of a NodeType, EdgeType, and Direction. This would expand GraphSchema itself to essentially be a Collection of NodeTypes, EdgeTypes, and EdgeRoles. I believe providers should be free to define custom constraints on either the EdgeTypes or EdgeRoles arbitrarily as they wish.
Regarding the transaction synchronizing, I agree that TinkerPop generally has no business imposing restrictions on providers in this manner. We do need to make some choices for the TinkerGraph reference implementation, which is where I think it's likely simplest to force a 1:1 binding between transactions in the schema and data graphs. I suppose my position on this is really that TinkerGraph should do what's easiest for its purposes, and providers should be free to implement transaction semantics as they see fit.
Regarding the lock/unlock, I agree with your suggested methods. The inclusion of graph.tx().unlock() as a convenience/sugar seems simple enough and should prove useful.
Thanks,
Cole
On 2026/04/13 07:31:53 pieter wrote:
> Hi,
>
> Regarding EdgeRole:
>
> In Sqlg, 'EdgeRole' is purely a in memory object to represent the
> triplet '[from]edgeLabel[to]'. The actual information is stored in the
> meta graph on the edge between VertexType and EdgeType. I don't see any
> way of not directly dealing with the triplet as it needs to carry its
> own information describing the triplet's properties like multiplicity,
> uniqueness and order.
>
> e.g.
> A loves[multiplicity:0..1, unique:false, ordered: false] B
> A loves[multiplicity:0..*, unique:true, ordered: true ] C
>
> This information needs to be captured on the triplet. Just having a
> 'from/toNodeTypes' Set will not be powerful enough.
>
> Regarding 'coordinating transactions'
>
> graph.tx().begin();
>
> GraphSchema schema = graph.schema();
>
> VertexType personVertexType =
> schema.traversal().addVType("person").next();
> personVertexType.propertyType("name").constraint("type",
> String.class).constraint("required", true).constraint("unique",
> true).next();
> personVertexType.propertyType("age").constraint("type",
> Integer.class).next();
>
> EdgeType knowsEdgeType =
> schema.traversal().addEType("knows").from("person").to("person").next()
> ;
> knowsEdgeType.propertyType("weight").constraint("type",
> Double.class).next()
>
> Vertex marco = graph.addVertex(T.label, "person", "name", "marco");
> Vertex vadas = graph.addVertex(T.label, "person", "name", "vadas");
> marco.addEdge("knows", vadas);
>
> graph.tx().commit();
>
> What coordinating or synchronizing does TinkerPop need to do?
> Storing(creating,updating,deleting) the schema and associated meta
> data(graph) is the job of the provider in its underlying datastore.
> Each provider will have its own way of managing its schema including
> the persistence and transaction semantics. Afterall, the core business
> of a provider is precisely laying out the graph on disc which inturn
> implies accute knowledge and management of the schema. Of course
> TinkerGraph will have its own reference implemtation, but its
> implentation will be largely irrelevant to providers.
>
> Regarding lock/unlock.
>
> The way I see it 3 methods are needed.
>
> Schema.lock()
>
> Schema.unlock()
> graph.tx().unlock()
>
> On graph.tx().commit() and rollback() the transactional unlock will
> automatically lock again if the schema itself is locked.
>
> Thanks
> Pieter
>
> On Fri, 2026-04-10 at 23:41 +0000, Cole Greer wrote:
> >
> >
> > >
> > > Hi Pieter,
> > > Thanks for the feedback.
> >
> >
> > >
> > >
> > > >
> > > >
> > > > >
> > > > > Will we not need an additional type to represent the
> > > > > 'EdgeLabel:VertexLabel:Direction'?
> > > >
> > >
> >
> >
> > >
> > > Regarding #1, I hadn't considered extracting something like an
> > > EdgeRole into a standalone object. The closest equivalent in the
> > > model I proposed is that each edge type would contain a list of
> > > fromNodeTypes and toNodeTypes. In the example you provided, the
> > > "loves" edge type would have fromNodeTypes of ["A"] and toNodeTypes
> > > of ["B", "C"]. The EdgeType in my proposal should map 1:1 to an
> > > EdgeType + a set of EdgeRoles in the sqlg model.
> >
> >
> > >
> > >
> > > >
> > > >
> > > > >
> > > > > Some backends will support transactions for schema and data
> > > > > changes. Some will support transactions only for data changes.
> > > > > Some
> > > > > databases like Mariadb will silently commit the current
> > > > > transaction when a
> > > > > schema change occurs.
> > > > > Probabaly there are variations on this theme, so it should be
> > > > > up to
> > > > > the provider to specify the granularity of support.
> > > >
> > >
> >
> >
> > >
> > > I agree that providers need flexibility to vary transaction support
> > > according to their specific backends and needs. There is also a bit
> > > of an open question as to how to coordinate transactions between
> > > the
> > > schema graph and data graph. My current thoughts is that the
> > > default
> > > behaviour should be to sync begins, commits, and rollbacks across
> > > the
> > > data and schema graphs as the default behaviour. I would think
> > > providers should be free to manage these independently if they
> > > prefer.
> >
> >
> > >
> > >
> > > >
> > > >
> > > > >
> > > > > Further I suggest adding schema.lock/unlock. If locked no
> > > > > schema changes are allowed. There can also be a transaction
> > > > > scoped
> > > > > unlock() which will just for the current transaction unlock the
> > > > > schema.
> > > > > This will add in a safety net for graphs with a well defined
> > > > > schema where no code should be able to make schema changes.
> > > >
> > >
> >
> >
> > >
> > > I like this suggestion, I think this would make sense as lock() and
> > > unlock() methods on the GraphSchema structure API class. I think it
> > > would be fine to exclude this from the gremlin language and remote
> > > APIs for now, as this could always be added later if it demand
> > > arises. Any thoughts on this approach?
> >
> >
> > >
> > >
> > > >
> > > >
> > > > >
> > > > > Regarding File IO.
> > > > > I am not sure why this is needed. We can simply export/import
> > > > > some gremlin to manage the schema. We already have the language
> > > > > that
> > > > > does this, why is a translation to json needed?
> > > >
> > >
> >
> >
> > >
> > > I would say that File IO isn't strictly necessary, but I also see
> > > little downside in including it. I believe it needs relatively
> > > little
> > > effort to include it, and I don't expect it to be an ongoing
> > > maintenance burden. There's some demand for it as a feature, I
> > > would
> > > use it as a compact and convenient way of persisting and loading my
> > > schemas. Josh has expressed interest in it as well. We have similar
> > > precedent in the data graph side with the io() step existing to
> > > facilitate bulk reads/writes instead of relying on gremlin for all
> > > ingest. I would also see no issue in any providers choosing not to
> > > support the file io in their schema implementations if they so
> > > choose.
> > > Please let me know your thoughts on these matters.
> > > Thanks, Cole
> > > On 2026/04/04 10:24:20 pieter wrote:
> >
> >
> > >
> > >
> > > >
> > > >
> > > > >
> > > > > Hi,
> > > > > This looks good to me.
> > > > > #1
> > > > > Will we not need an additional type to represent the
> > > > > 'EdgeLabel:VertexLabel:Direction'?
> > > > > In Sqlg this is called 'EdgeRole'.
> > > > > class EdgeRole { private EdgeType; private VertexType;
> > > > > private Direction; }
> > > > > Consider the following example
> > > > > VertexType aVertexType = schema.traversal().addVType("A");
> > > > > VertexType bVertexType = schema.traversal().addVType("B");
> > > > > VertexType cVertexType = schema.traversal().addVType("C");
> > > > > EdgeType lovesEdgeType = schema.traversal().addEType("loves")
> > > > > lovesEdgeType.from("A").to("B");
> > > > > lovesEdgeType.from("A").to("C");
> > > > > In this case there are 3 EdgesRoles
> > > > > loves:A:OUT loves:B:IN loves:C:IN
> > > > > Later the user needs to manipulate the 'loves:C:IN' EdgeRole
> > > > > like deleting it or changing the multiplicities... For this the
> > > > > user needs to be able to access the EdgeRole.
> > > > > EdgeRole cLovesEdgeRole = lovesEdgeType.edgeRole(cVertexType);
> > > > > cLovesEdgeRole.drop();
> > > > > #2
> > > > > Regarding transactions.
> > > > > Some backends will support transactions for schema and data
> > > > > changes. Some will support transactions only for data changes.
> > > > > Some
> > > > > databases like Mariadb will silently commit the current
> > > > > transaction when a
> > > > > schema change occurs.
> > > > > Probabaly there are variations on this theme, so it should be
> > > > > up to
> > > > > the provider to specify the granularity of support.
> > > > > Further I suggest adding schema.lock/unlock. If locked no
> > > > > schema changes are allowed. There can also be a transaction
> > > > > scoped
> > > > > unlock() which will just for the current transaction unlock the
> > > > > schema.
> > > > > This will add in a safety net for graphs with a well defined
> > > > > schema where no code should be able to make schema changes.
> > > > > #3
> > > > > Regarding File IO.
> > > > > I am not sure why this is needed. We can simply export/import
> > > > > some gremlin to manage the schema. We already have the language
> > > > > that
> > > > > does this, why is a translation to json needed?
> > > > > Regards Pieter
> > > > > On Fri, 2026-04-03 at 02:16 +0000, Cole Greer via dev wrote:
> > > >
> > >
> > >
> > > >
> > > >
> > > > >
> > > > >
> > > > > >
> > > > > >
> > > > > > >
> > > > > > > Hi Everyone,
> > > > > > > The topic of Graph Schema has been discussed extensively in
> > > > > > > recent TInkerPop Gatherings, and the following proposal has
> > > > > > > emerged from these gatherings. I believe it is now ready
> > > > > > > for broad
> > > > > > > consideration and discussions. I’ve done my best to
> > > > > > > incorporate initial
> > > > > > > feedback from Josh, Pieter, Valentyn, Stephen, Kris and
> > > > > > > others into this proposal, however I won’t claim that it
> > > > > > > accurately represents the views of anyone other than myself
> > > > > > > at this time. This is a broad
> > > > > > > topic and I’m deliberately excluding critical topics to
> > > > > > > focus this
> > > > > > > thread on standardizing interfaces for gremlin users and
> > > > > > > providers to interact with schema (see assumptions for more
> > > > > > > details).
> > > > > > > OverviewThis proposal introduces graph schema interfaces
> > > > > > > for TinkerPop: a
> > > > > > > way to define vertex types, edge types, and property types
> > > > > > > as a meta- graph that is itself traversable with Gremlin.
> > > > > > > The schema
> > > > > > > describes the structure of a data graph; what kinds of
> > > > > > > vertices and edges exist, what properties they carry, and
> > > > > > > how they connect..
> > > > > > > Assumptions * Type keys are element labels: there is a 1-
> > > > > > > to-1 mapping between
> > > > > > > a label and a type definition. A vertex labeled "person"
> > > > > > > corresponds to exactly one VertexType, and an edge
> > > > > > > labeled "knows" corresponds
> > > > > > > to exactly one EdgeType.
> > > > > > > * Java classes are used as a type system: This proposal
> > > > > > > uses Java classes to define property type constraints. This
> > > > > > > is intended as
> > > > > > > a placeholder to be replaced by a proper type system to
> > > > > > > be defined
> > > > > > > via a later discussion.
> > > > > > > * This proposal makes very little consideration of
> > > > > > > if/when/where/how validation and enforcement of schema
> > > > > > > takes place. I believe it is important for us to ship
> > > > > > > something which is flexible and useful
> > > > > > > to providers out of the box as well as leaving space for
> > > > > > > providers
> > > > > > > to plugin existing implementations or build their own if
> > > > > > > they
> > > > > > > desire. I’ve left this out of scope for this proposal to
> > > > > > > focus first on interfaces which give providers the
> > > > > > > appropriate access to schema.
> > > > > > > Design Points 1. Schema-as-GraphGraphSchema extends Graph.
> > > > > > > Providers implement a familiar interface, and users
> > > > > > > traverse the schema with schema.traversal(). This avoids
> > > > > > > inventing a parallel API surface. The schema is just
> > > > > > > another graph.
> > > > > > > A data graph exposes its schema via Graph.schema(), which
> > > > > > > returns
> > > > > > > the GraphSchema instance. Providers that don't support
> > > > > > > schema return UnsupportedOperationException by default.
> > > > > > > 2. All type definitions are verticesVertexType, EdgeType,
> > > > > > > and PropertyType are all vertices in the
> > > > > > > schema meta-graph.
> > > > > > > * A VertexType vertex represents a vertex label definition
> > > > > > > (e.g. "person", "software").
> > > > > > > * An EdgeType vertex represents an edge label definition
> > > > > > > (e.g. "knows", "created"). Even though it describes edges
> > > > > > > in the data graph, it is itself a vertex in the schema
> > > > > > > graph, connected to
> > > > > > > its endpoint VertexType vertices via from/to edges.
> > > > > > > * A PropertyType vertex represents a property on a type,
> > > > > > > connected to its parent type vertex via a “hasProperty"
> > > > > > > edge.
> > > > > > > Property definitions are independent per type, no sharing
> > > > > > > across types.
> > > > > > > Schema graph example for the classic TinkerPop modern
> > > > > > > graph:
> > > > > > > (person:vertexType) --hasProperty--> (name:propertyType)
> > > > > > > (person:vertexType) --hasProperty--> (age:propertyType)
> > > > > > > (software:vertexType) --hasProperty--> (name:propertyType)
> > > > > > > (software:vertexType) --hasProperty--> (lang:propertyType)
> > > > > > > (knows:edgeType) --from--> (person:vertexType)
> > > > > > > (knows:edgeType) --to--> (person:vertexType)
> > > > > > > (knows:edgeType) --hasProperty--> (weight:propertyType)
> > > > > > > (created:edgeType) --from--> (person:vertexType)
> > > > > > > (created:edgeType) --to--> (software:vertexType)
> > > > > > > (created:edgeType) --hasProperty--> (weight:propertyType)
> > > > > > > 3. Constraints are properties on type verticesRather than a
> > > > > > > fixed constraint taxonomy, constraints are regular
> > > > > > > properties on type vertices, keyed by string via
> > > > > > > constraint(key, value). This keeps the model extensible
> > > > > > > such that providers can define their own constraints
> > > > > > > without changes to the core API.
> > > > > > > Constraints can be added to VertexType, EdgeType, and
> > > > > > > PropertyType vertices directly. The most common constraints
> > > > > > > such as property
> > > > > > > types and required properties would apply to PropertyTypes,
> > > > > > > while edge multiplicity constraints (e.g. one-to-many, one-
> > > > > > > to-one) are
> > > > > > > naturally expressed as constraints on the EdgeType itself
> > > > > > > rather than on
> > > > > > > any property.
> > > > > > > While constraint keys are arbitrary strings and providers
> > > > > > > are
> > > > > > > free to implement any constraints they like, TinkerPop
> > > > > > > should standardize
> > > > > > > a set of core constraint keys representing the most common
> > > > > > > constraints. Examples include “type", “required", “unique",
> > > > > > > “minValue", “maxValue", etc. Providers that support
> > > > > > > equivalent constraints
> > > > > > > are encouraged to follow these conventional names for
> > > > > > > interoperability.
> > > > > > > Non-core constraints (custom to a provider) are encouraged
> > > > > > > to
> > > > > > > follow a namespaced key convention to avoid collisions,
> > > > > > > e.g. "tinkergraph:notNull". Core constraint keys are
> > > > > > > unnamespaced.
> > > > > > > 4. Schema traversal steps in core GremlinNew steps for
> > > > > > > schema manipulation live directly in
> > > > > > > GraphTraversal/GraphTraversalSource, not in a separate DSL:
> > > > > > > * addVType(label) — creates a VertexType vertex
> > > > > > > * addEType(label) — creates an EdgeType vertex
> > > > > > > * propertyType(name) — creates a PropertyType vertex and
> > > > > > > connects
> > > > > > > it via hasProperty
> > > > > > > * constraint(key, value) — adds a constraint property to
> > > > > > > the
> > > > > > > current type vertex
> > > > > > > Example: defining a vertex type with properties:
> > > > > > > schema.traversal().addVType("person")
> > > > > > > .propertyType("name").constraint("type",
> > > > > > > String.class).constraint("required",
> > > > > > > true).constraint("unique", true)
> > > > > > > .propertyType("age").constraint("type", Integer.class)
> > > > > > >
> > > > > > > Example: defining an edge type with endpoint types and a
> > > > > > > property:
> > > > > > > schema.traversal().addEType("knows")
> > > > > > > .from("person").to("person")
> > > > > > > .propertyType("weight").constraint("type", Double.class)
> > > > > > >
> > > > > > > This mirrors the addE().from().to() pattern from the data-
> > > > > > > graph.
> > > > > > > Here from() and to() take vertex type labels (strings) and
> > > > > > > create
> > > > > > > from/to edges in the schema graph connecting the EdgeType
> > > > > > > to the
> > > > > > > referenced VertexType vertices.
> > > > > > > 5. Convenience methods for direct accessThe schema-as-graph
> > > > > > > model is the source of truth, but traversing
> > > > > > > it for simple lookups isn’t always convenient. Direct
> > > > > > > methods
> > > > > > > provide compact access:
> > > > > > > GraphSchema methods:
> > > > > > > * vertexTypes() → Collection
> > > > > > > * vertexType(String label) → Optional
> > > > > > > * edgeTypes() → Collection
> > > > > > > * edgeType(String label) → Optional
> > > > > > > * addVertexType(String label) → VertexType
> > > > > > > * addEdgeType(String label) → EdgeType
> > > > > > > * store(OutputStream): serialize the schema to a compact
> > > > > > > JSON representation
> > > > > > > * load(InputStream): deserialize and merge a schema from
> > > > > > > JSON
> > > > > > > into this schema graph
> > > > > > > EdgeType methods:
> > > > > > > * fromVertexTypes() → Collection
> > > > > > > * toVertexTypes() → Collection
> > > > > > > Example:
> > > > > > > GraphSchema schema = graph.schema(); // Look up a vertex
> > > > > > > type VertexType person =
> > > > > > > schema.vertexType("person").orElseThrow(); // Inspect its
> > > > > > > properties for (PropertyType pd : person.propertyTypes()) {
> > > > > > > System.out.println(pd.name() + " : " +
> > > > > > > pd.constraint("type")); } // Look up an edge type and its
> > > > > > > connectivity EdgeType knows =
> > > > > > > schema.edgeType("knows").orElseThrow();
> > > > > > > Collection<VertexType> fromTypes = knows.fromVertexTypes();
> > > > > > > Collection<VertexType> toTypes = knows.toVertexTypes();
> > > > > > > 6. Cross-graph jumpsTwo steps bridge the data graph and
> > > > > > > schema graph:
> > > > > > > * type(): from a data traversal, jump to the element's
> > > > > > > type definition in the schema graph.
> > > > > > > * instances(): from a schema traversal, jump to all
> > > > > > > matching
> > > > > > > elements in the data graph.
> > > > > > > These compose for round-trip traversals:
> > > > > > > // Get the type definition for "person" vertices
> > > > > > > g.V().hasLabel("person").type() // Get all instances of a
> > > > > > > schema type
> > > > > > > schema.traversal().vertexType("person").instances() //
> > > > > > > Round-trip: find marko's type, then get all instances of
> > > > > > > that type g.V().has("person", "name",
> > > > > > > "marko").type().instances()
> > > > > > > 7. Schema restriction strategyThere are some steps we will
> > > > > > > want to restrict in both the data
> > > > > > > graph and the schema-graph. addVType() wouldn’t make sense
> > > > > > > in the data- graph, nor would addV() be sensible in the
> > > > > > > schema-graph. A TraversalStrategy can restrict schema
> > > > > > > traversals to a safe subset
> > > > > > > of Gremlin steps (allowlist-based). This prevents
> > > > > > > accidentally
> > > > > > > running data element insertions, OLAP computations, complex
> > > > > > > control flow,
> > > > > > > or side-effect steps against the schema graph. The strategy
> > > > > > > should
> > > > > > > be auto-registered when traversing a GraphSchema instance.
> > > > > > > The exact allowlist should be a topic for later discussion.
> > > > > > > 8. Instance counts on type
> > > > > > > verticesVertexType.instanceCount() and
> > > > > > > EdgeType.instanceCount() return
> > > > > > > the count of data graph elements matching each type. This
> > > > > > > is a method rather than a property on the type vertex,
> > > > > > > keeping the schema
> > > > > > > graph definitional (not statistical) and giving providers
> > > > > > > full implementation flexibility.
> > > > > > > Approximate counts are likely acceptable and preferable for
> > > > > > > performance in most cases. However, TinkerPop should not
> > > > > > > stand in
> > > > > > > the way of providers that prefer exact counts, and should
> > > > > > > ensure that appropriate hooks are in place in reference
> > > > > > > implementations so
> > > > > > > that providers can maintain exact counts if they so desire.
> > > > > > > Transactional implications need additional consideration.
> > > > > > > Maintaining accurate counts across concurrent writes,
> > > > > > > rollbacks, and
> > > > > > > transaction isolation levels adds significant complexity.
> > > > > > > This interacts with
> > > > > > > the broader schema transactions question (see transactions
> > > > > > > below) and should be addressed alongside it.
> > > > > > > 9. GLV SupportEach GLV (Python, JavaScript, .NET, Go)
> > > > > > > needs:
> > > > > > > * Schema data classes: Parallel classes to the 4 core Java
> > > > > > > interfaces, following the same pattern as existing Vertex
> > > > > > > and
> > > > > > > Edge classes. These are data containers representing
> > > > > > > schema objects returned from the server: - GraphSchema:
> > > > > > > holds collections of VertexTypes and EdgeTypes -
> > > > > > > VertexType: label, full constraints map, and collection of
> > > > > > > PropertyTypes - EdgeType: label, full constraints map,
> > > > > > > from/to VertexType references (same pattern as
> > > > > > > Edge.outV/Edge.inV), and collection
> > > > > > > of PropertyTypes - PropertyType: name and full
> > > > > > > constraints map (including data
> > > > > > > type as a constraint)
> > > > > > > * All new gremlin steps are supported from each GLV
> > > > > > > Future Questions Schema validationProviders will need lots
> > > > > > > of flexibility regarding validation
> > > > > > > modes. Some providers may choose to have write-time
> > > > > > > validation for all inserts, others may choose validate an
> > > > > > > entire graph against a
> > > > > > > schema as a batch job, while others may choose to validate
> > > > > > > on-commit.
> > > > > > > For our purposes, we need to provide a viable reference
> > > > > > > implementation, as well as ensuring sufficient extension
> > > > > > > points exist for
> > > > > > > providers to fulfill their needs.
> > > > > > > Dynamic schema updates from data writesIt would be useful
> > > > > > > to auto-update the schema graph when data
> > > > > > > writes introduce new labels or properties (e.g.
> > > > > > > addV("newLabel”) automatically creates a VertexType).
> > > > > > > Keeping the schema exactly
> > > > > > > in- sync with such operations may introduce too much
> > > > > > > overhead for
> > > > > > > many purposes. We should provide appropriate hooks for
> > > > > > > providers to implement such behaviour if desired, or to
> > > > > > > help providers
> > > > > > > aggregate changes and perform incremental batch updates to
> > > > > > > the schema.
> > > > > > > TransactionsThe schema graph will need to be transactional
> > > > > > > if the data
> > > > > > > File IOIt is often useful to persist and load schemas
> > > > > > > to/from files.
> > > > > > > This capability should be build into the GraphSchema class
> > > > > > > via simple store() and load() methods, using a custom
> > > > > > > compact JSON representation of the schema. The specifics of
> > > > > > > this format are deferred to later discussion.
> > > > > > > GraphSchema exposes file IO directly:
> > > > > > > * store(OutputStream): serialize the schema to a compact
> > > > > > > JSON representation
> > > > > > > * load(InputStream): deserialize a schema from JSON and
> > > > > > > merge it
> > > > > > > into the current schema graph
> > > > > > > Schema file IO should be implemented across all GLVs.
> > > > > > > Reference ImplementationTinkerGraph serves as the reference
> > > > > > > implementation:
> > > > > > > * TinkerGraphSchema extends TinkerGraph implements
> > > > > > > GraphSchema
> > > > > > > * TinkerVertexType extends TinkerVertex implements
> > > > > > > VertexType
> > > > > > > * TinkerPropertyType extends TinkerVertex implements
> > > > > > > PropertyType
> > > > > > > * TinkerEdgeType extends TinkerVertex implements EdgeType
> > > > > > > * Recursion guard prevents schema-of-schema
> > > > > > > (TinkerGraphSchema overrides initSchema())
> > > > > > > Please let me know any thoughts you may have on the
> > > > > > > approach. I intend to move this into a proposal PR soon,
> > > > > > > unless there are any major disagreements over the design.
> > > > > > > Thanks, Cole
> > > > > >
> > > > >
> > > >
> > >
> > >
> > > >
> > > >
> > > > >
> > > >
> > >
>
Re: [DISCUSS] Graph Schema Interfaces for TP
Posted by pieter <pi...@riseup.net>.
Hi,
Regarding EdgeRole:
In Sqlg, 'EdgeRole' is purely a in memory object to represent the
triplet '[from]edgeLabel[to]'. The actual information is stored in the
meta graph on the edge between VertexType and EdgeType. I don't see any
way of not directly dealing with the triplet as it needs to carry its
own information describing the triplet's properties like multiplicity,
uniqueness and order.
e.g.
A loves[multiplicity:0..1, unique:false, ordered: false] B
A loves[multiplicity:0..*, unique:true, ordered: true ] C
This information needs to be captured on the triplet. Just having a
'from/toNodeTypes' Set will not be powerful enough.
Regarding 'coordinating transactions'
graph.tx().begin();
GraphSchema schema = graph.schema();
VertexType personVertexType =
schema.traversal().addVType("person").next();
personVertexType.propertyType("name").constraint("type",
String.class).constraint("required", true).constraint("unique",
true).next();
personVertexType.propertyType("age").constraint("type",
Integer.class).next();
EdgeType knowsEdgeType =
schema.traversal().addEType("knows").from("person").to("person").next()
;
knowsEdgeType.propertyType("weight").constraint("type",
Double.class).next()
Vertex marco = graph.addVertex(T.label, "person", "name", "marco");
Vertex vadas = graph.addVertex(T.label, "person", "name", "vadas");
marco.addEdge("knows", vadas);
graph.tx().commit();
What coordinating or synchronizing does TinkerPop need to do?
Storing(creating,updating,deleting) the schema and associated meta
data(graph) is the job of the provider in its underlying datastore.
Each provider will have its own way of managing its schema including
the persistence and transaction semantics. Afterall, the core business
of a provider is precisely laying out the graph on disc which inturn
implies accute knowledge and management of the schema. Of course
TinkerGraph will have its own reference implemtation, but its
implentation will be largely irrelevant to providers.
Regarding lock/unlock.
The way I see it 3 methods are needed.
Schema.lock()
Schema.unlock()
graph.tx().unlock()
On graph.tx().commit() and rollback() the transactional unlock will
automatically lock again if the schema itself is locked.
Thanks
Pieter
On Fri, 2026-04-10 at 23:41 +0000, Cole Greer wrote:
>
>
> >
> > Hi Pieter,
> > Thanks for the feedback.
>
>
> >
> >
> > >
> > >
> > > >
> > > > Will we not need an additional type to represent the
> > > > 'EdgeLabel:VertexLabel:Direction'?
> > >
> >
>
>
> >
> > Regarding #1, I hadn't considered extracting something like an
> > EdgeRole into a standalone object. The closest equivalent in the
> > model I proposed is that each edge type would contain a list of
> > fromNodeTypes and toNodeTypes. In the example you provided, the
> > "loves" edge type would have fromNodeTypes of ["A"] and toNodeTypes
> > of ["B", "C"]. The EdgeType in my proposal should map 1:1 to an
> > EdgeType + a set of EdgeRoles in the sqlg model.
>
>
> >
> >
> > >
> > >
> > > >
> > > > Some backends will support transactions for schema and data
> > > > changes. Some will support transactions only for data changes.
> > > > Some
> > > > databases like Mariadb will silently commit the current
> > > > transaction when a
> > > > schema change occurs.
> > > > Probabaly there are variations on this theme, so it should be
> > > > up to
> > > > the provider to specify the granularity of support.
> > >
> >
>
>
> >
> > I agree that providers need flexibility to vary transaction support
> > according to their specific backends and needs. There is also a bit
> > of an open question as to how to coordinate transactions between
> > the
> > schema graph and data graph. My current thoughts is that the
> > default
> > behaviour should be to sync begins, commits, and rollbacks across
> > the
> > data and schema graphs as the default behaviour. I would think
> > providers should be free to manage these independently if they
> > prefer.
>
>
> >
> >
> > >
> > >
> > > >
> > > > Further I suggest adding schema.lock/unlock. If locked no
> > > > schema changes are allowed. There can also be a transaction
> > > > scoped
> > > > unlock() which will just for the current transaction unlock the
> > > > schema.
> > > > This will add in a safety net for graphs with a well defined
> > > > schema where no code should be able to make schema changes.
> > >
> >
>
>
> >
> > I like this suggestion, I think this would make sense as lock() and
> > unlock() methods on the GraphSchema structure API class. I think it
> > would be fine to exclude this from the gremlin language and remote
> > APIs for now, as this could always be added later if it demand
> > arises. Any thoughts on this approach?
>
>
> >
> >
> > >
> > >
> > > >
> > > > Regarding File IO.
> > > > I am not sure why this is needed. We can simply export/import
> > > > some gremlin to manage the schema. We already have the language
> > > > that
> > > > does this, why is a translation to json needed?
> > >
> >
>
>
> >
> > I would say that File IO isn't strictly necessary, but I also see
> > little downside in including it. I believe it needs relatively
> > little
> > effort to include it, and I don't expect it to be an ongoing
> > maintenance burden. There's some demand for it as a feature, I
> > would
> > use it as a compact and convenient way of persisting and loading my
> > schemas. Josh has expressed interest in it as well. We have similar
> > precedent in the data graph side with the io() step existing to
> > facilitate bulk reads/writes instead of relying on gremlin for all
> > ingest. I would also see no issue in any providers choosing not to
> > support the file io in their schema implementations if they so
> > choose.
> > Please let me know your thoughts on these matters.
> > Thanks, Cole
> > On 2026/04/04 10:24:20 pieter wrote:
>
>
> >
> >
> > >
> > >
> > > >
> > > > Hi,
> > > > This looks good to me.
> > > > #1
> > > > Will we not need an additional type to represent the
> > > > 'EdgeLabel:VertexLabel:Direction'?
> > > > In Sqlg this is called 'EdgeRole'.
> > > > class EdgeRole { private EdgeType; private VertexType;
> > > > private Direction; }
> > > > Consider the following example
> > > > VertexType aVertexType = schema.traversal().addVType("A");
> > > > VertexType bVertexType = schema.traversal().addVType("B");
> > > > VertexType cVertexType = schema.traversal().addVType("C");
> > > > EdgeType lovesEdgeType = schema.traversal().addEType("loves")
> > > > lovesEdgeType.from("A").to("B");
> > > > lovesEdgeType.from("A").to("C");
> > > > In this case there are 3 EdgesRoles
> > > > loves:A:OUT loves:B:IN loves:C:IN
> > > > Later the user needs to manipulate the 'loves:C:IN' EdgeRole
> > > > like deleting it or changing the multiplicities... For this the
> > > > user needs to be able to access the EdgeRole.
> > > > EdgeRole cLovesEdgeRole = lovesEdgeType.edgeRole(cVertexType);
> > > > cLovesEdgeRole.drop();
> > > > #2
> > > > Regarding transactions.
> > > > Some backends will support transactions for schema and data
> > > > changes. Some will support transactions only for data changes.
> > > > Some
> > > > databases like Mariadb will silently commit the current
> > > > transaction when a
> > > > schema change occurs.
> > > > Probabaly there are variations on this theme, so it should be
> > > > up to
> > > > the provider to specify the granularity of support.
> > > > Further I suggest adding schema.lock/unlock. If locked no
> > > > schema changes are allowed. There can also be a transaction
> > > > scoped
> > > > unlock() which will just for the current transaction unlock the
> > > > schema.
> > > > This will add in a safety net for graphs with a well defined
> > > > schema where no code should be able to make schema changes.
> > > > #3
> > > > Regarding File IO.
> > > > I am not sure why this is needed. We can simply export/import
> > > > some gremlin to manage the schema. We already have the language
> > > > that
> > > > does this, why is a translation to json needed?
> > > > Regards Pieter
> > > > On Fri, 2026-04-03 at 02:16 +0000, Cole Greer via dev wrote:
> > >
> >
> >
> > >
> > >
> > > >
> > > >
> > > > >
> > > > >
> > > > > >
> > > > > > Hi Everyone,
> > > > > > The topic of Graph Schema has been discussed extensively in
> > > > > > recent TInkerPop Gatherings, and the following proposal has
> > > > > > emerged from these gatherings. I believe it is now ready
> > > > > > for broad
> > > > > > consideration and discussions. I’ve done my best to
> > > > > > incorporate initial
> > > > > > feedback from Josh, Pieter, Valentyn, Stephen, Kris and
> > > > > > others into this proposal, however I won’t claim that it
> > > > > > accurately represents the views of anyone other than myself
> > > > > > at this time. This is a broad
> > > > > > topic and I’m deliberately excluding critical topics to
> > > > > > focus this
> > > > > > thread on standardizing interfaces for gremlin users and
> > > > > > providers to interact with schema (see assumptions for more
> > > > > > details).
> > > > > > OverviewThis proposal introduces graph schema interfaces
> > > > > > for TinkerPop: a
> > > > > > way to define vertex types, edge types, and property types
> > > > > > as a meta- graph that is itself traversable with Gremlin.
> > > > > > The schema
> > > > > > describes the structure of a data graph; what kinds of
> > > > > > vertices and edges exist, what properties they carry, and
> > > > > > how they connect..
> > > > > > Assumptions * Type keys are element labels: there is a 1-
> > > > > > to-1 mapping between
> > > > > > a label and a type definition. A vertex labeled "person"
> > > > > > corresponds to exactly one VertexType, and an edge
> > > > > > labeled "knows" corresponds
> > > > > > to exactly one EdgeType.
> > > > > > * Java classes are used as a type system: This proposal
> > > > > > uses Java classes to define property type constraints. This
> > > > > > is intended as
> > > > > > a placeholder to be replaced by a proper type system to
> > > > > > be defined
> > > > > > via a later discussion.
> > > > > > * This proposal makes very little consideration of
> > > > > > if/when/where/how validation and enforcement of schema
> > > > > > takes place. I believe it is important for us to ship
> > > > > > something which is flexible and useful
> > > > > > to providers out of the box as well as leaving space for
> > > > > > providers
> > > > > > to plugin existing implementations or build their own if
> > > > > > they
> > > > > > desire. I’ve left this out of scope for this proposal to
> > > > > > focus first on interfaces which give providers the
> > > > > > appropriate access to schema.
> > > > > > Design Points 1. Schema-as-GraphGraphSchema extends Graph.
> > > > > > Providers implement a familiar interface, and users
> > > > > > traverse the schema with schema.traversal(). This avoids
> > > > > > inventing a parallel API surface. The schema is just
> > > > > > another graph.
> > > > > > A data graph exposes its schema via Graph.schema(), which
> > > > > > returns
> > > > > > the GraphSchema instance. Providers that don't support
> > > > > > schema return UnsupportedOperationException by default.
> > > > > > 2. All type definitions are verticesVertexType, EdgeType,
> > > > > > and PropertyType are all vertices in the
> > > > > > schema meta-graph.
> > > > > > * A VertexType vertex represents a vertex label definition
> > > > > > (e.g. "person", "software").
> > > > > > * An EdgeType vertex represents an edge label definition
> > > > > > (e.g. "knows", "created"). Even though it describes edges
> > > > > > in the data graph, it is itself a vertex in the schema
> > > > > > graph, connected to
> > > > > > its endpoint VertexType vertices via from/to edges.
> > > > > > * A PropertyType vertex represents a property on a type,
> > > > > > connected to its parent type vertex via a “hasProperty"
> > > > > > edge.
> > > > > > Property definitions are independent per type, no sharing
> > > > > > across types.
> > > > > > Schema graph example for the classic TinkerPop modern
> > > > > > graph:
> > > > > > (person:vertexType) --hasProperty--> (name:propertyType)
> > > > > > (person:vertexType) --hasProperty--> (age:propertyType)
> > > > > > (software:vertexType) --hasProperty--> (name:propertyType)
> > > > > > (software:vertexType) --hasProperty--> (lang:propertyType)
> > > > > > (knows:edgeType) --from--> (person:vertexType)
> > > > > > (knows:edgeType) --to--> (person:vertexType)
> > > > > > (knows:edgeType) --hasProperty--> (weight:propertyType)
> > > > > > (created:edgeType) --from--> (person:vertexType)
> > > > > > (created:edgeType) --to--> (software:vertexType)
> > > > > > (created:edgeType) --hasProperty--> (weight:propertyType)
> > > > > > 3. Constraints are properties on type verticesRather than a
> > > > > > fixed constraint taxonomy, constraints are regular
> > > > > > properties on type vertices, keyed by string via
> > > > > > constraint(key, value). This keeps the model extensible
> > > > > > such that providers can define their own constraints
> > > > > > without changes to the core API.
> > > > > > Constraints can be added to VertexType, EdgeType, and
> > > > > > PropertyType vertices directly. The most common constraints
> > > > > > such as property
> > > > > > types and required properties would apply to PropertyTypes,
> > > > > > while edge multiplicity constraints (e.g. one-to-many, one-
> > > > > > to-one) are
> > > > > > naturally expressed as constraints on the EdgeType itself
> > > > > > rather than on
> > > > > > any property.
> > > > > > While constraint keys are arbitrary strings and providers
> > > > > > are
> > > > > > free to implement any constraints they like, TinkerPop
> > > > > > should standardize
> > > > > > a set of core constraint keys representing the most common
> > > > > > constraints. Examples include “type", “required", “unique",
> > > > > > “minValue", “maxValue", etc. Providers that support
> > > > > > equivalent constraints
> > > > > > are encouraged to follow these conventional names for
> > > > > > interoperability.
> > > > > > Non-core constraints (custom to a provider) are encouraged
> > > > > > to
> > > > > > follow a namespaced key convention to avoid collisions,
> > > > > > e.g. "tinkergraph:notNull". Core constraint keys are
> > > > > > unnamespaced.
> > > > > > 4. Schema traversal steps in core GremlinNew steps for
> > > > > > schema manipulation live directly in
> > > > > > GraphTraversal/GraphTraversalSource, not in a separate DSL:
> > > > > > * addVType(label) — creates a VertexType vertex
> > > > > > * addEType(label) — creates an EdgeType vertex
> > > > > > * propertyType(name) — creates a PropertyType vertex and
> > > > > > connects
> > > > > > it via hasProperty
> > > > > > * constraint(key, value) — adds a constraint property to
> > > > > > the
> > > > > > current type vertex
> > > > > > Example: defining a vertex type with properties:
> > > > > > schema.traversal().addVType("person")
> > > > > > .propertyType("name").constraint("type",
> > > > > > String.class).constraint("required",
> > > > > > true).constraint("unique", true)
> > > > > > .propertyType("age").constraint("type", Integer.class)
> > > > > >
> > > > > > Example: defining an edge type with endpoint types and a
> > > > > > property:
> > > > > > schema.traversal().addEType("knows")
> > > > > > .from("person").to("person")
> > > > > > .propertyType("weight").constraint("type", Double.class)
> > > > > >
> > > > > > This mirrors the addE().from().to() pattern from the data-
> > > > > > graph.
> > > > > > Here from() and to() take vertex type labels (strings) and
> > > > > > create
> > > > > > from/to edges in the schema graph connecting the EdgeType
> > > > > > to the
> > > > > > referenced VertexType vertices.
> > > > > > 5. Convenience methods for direct accessThe schema-as-graph
> > > > > > model is the source of truth, but traversing
> > > > > > it for simple lookups isn’t always convenient. Direct
> > > > > > methods
> > > > > > provide compact access:
> > > > > > GraphSchema methods:
> > > > > > * vertexTypes() → Collection
> > > > > > * vertexType(String label) → Optional
> > > > > > * edgeTypes() → Collection
> > > > > > * edgeType(String label) → Optional
> > > > > > * addVertexType(String label) → VertexType
> > > > > > * addEdgeType(String label) → EdgeType
> > > > > > * store(OutputStream): serialize the schema to a compact
> > > > > > JSON representation
> > > > > > * load(InputStream): deserialize and merge a schema from
> > > > > > JSON
> > > > > > into this schema graph
> > > > > > EdgeType methods:
> > > > > > * fromVertexTypes() → Collection
> > > > > > * toVertexTypes() → Collection
> > > > > > Example:
> > > > > > GraphSchema schema = graph.schema(); // Look up a vertex
> > > > > > type VertexType person =
> > > > > > schema.vertexType("person").orElseThrow(); // Inspect its
> > > > > > properties for (PropertyType pd : person.propertyTypes()) {
> > > > > > System.out.println(pd.name() + " : " +
> > > > > > pd.constraint("type")); } // Look up an edge type and its
> > > > > > connectivity EdgeType knows =
> > > > > > schema.edgeType("knows").orElseThrow();
> > > > > > Collection<VertexType> fromTypes = knows.fromVertexTypes();
> > > > > > Collection<VertexType> toTypes = knows.toVertexTypes();
> > > > > > 6. Cross-graph jumpsTwo steps bridge the data graph and
> > > > > > schema graph:
> > > > > > * type(): from a data traversal, jump to the element's
> > > > > > type definition in the schema graph.
> > > > > > * instances(): from a schema traversal, jump to all
> > > > > > matching
> > > > > > elements in the data graph.
> > > > > > These compose for round-trip traversals:
> > > > > > // Get the type definition for "person" vertices
> > > > > > g.V().hasLabel("person").type() // Get all instances of a
> > > > > > schema type
> > > > > > schema.traversal().vertexType("person").instances() //
> > > > > > Round-trip: find marko's type, then get all instances of
> > > > > > that type g.V().has("person", "name",
> > > > > > "marko").type().instances()
> > > > > > 7. Schema restriction strategyThere are some steps we will
> > > > > > want to restrict in both the data
> > > > > > graph and the schema-graph. addVType() wouldn’t make sense
> > > > > > in the data- graph, nor would addV() be sensible in the
> > > > > > schema-graph. A TraversalStrategy can restrict schema
> > > > > > traversals to a safe subset
> > > > > > of Gremlin steps (allowlist-based). This prevents
> > > > > > accidentally
> > > > > > running data element insertions, OLAP computations, complex
> > > > > > control flow,
> > > > > > or side-effect steps against the schema graph. The strategy
> > > > > > should
> > > > > > be auto-registered when traversing a GraphSchema instance.
> > > > > > The exact allowlist should be a topic for later discussion.
> > > > > > 8. Instance counts on type
> > > > > > verticesVertexType.instanceCount() and
> > > > > > EdgeType.instanceCount() return
> > > > > > the count of data graph elements matching each type. This
> > > > > > is a method rather than a property on the type vertex,
> > > > > > keeping the schema
> > > > > > graph definitional (not statistical) and giving providers
> > > > > > full implementation flexibility.
> > > > > > Approximate counts are likely acceptable and preferable for
> > > > > > performance in most cases. However, TinkerPop should not
> > > > > > stand in
> > > > > > the way of providers that prefer exact counts, and should
> > > > > > ensure that appropriate hooks are in place in reference
> > > > > > implementations so
> > > > > > that providers can maintain exact counts if they so desire.
> > > > > > Transactional implications need additional consideration.
> > > > > > Maintaining accurate counts across concurrent writes,
> > > > > > rollbacks, and
> > > > > > transaction isolation levels adds significant complexity.
> > > > > > This interacts with
> > > > > > the broader schema transactions question (see transactions
> > > > > > below) and should be addressed alongside it.
> > > > > > 9. GLV SupportEach GLV (Python, JavaScript, .NET, Go)
> > > > > > needs:
> > > > > > * Schema data classes: Parallel classes to the 4 core Java
> > > > > > interfaces, following the same pattern as existing Vertex
> > > > > > and
> > > > > > Edge classes. These are data containers representing
> > > > > > schema objects returned from the server: - GraphSchema:
> > > > > > holds collections of VertexTypes and EdgeTypes -
> > > > > > VertexType: label, full constraints map, and collection of
> > > > > > PropertyTypes - EdgeType: label, full constraints map,
> > > > > > from/to VertexType references (same pattern as
> > > > > > Edge.outV/Edge.inV), and collection
> > > > > > of PropertyTypes - PropertyType: name and full
> > > > > > constraints map (including data
> > > > > > type as a constraint)
> > > > > > * All new gremlin steps are supported from each GLV
> > > > > > Future Questions Schema validationProviders will need lots
> > > > > > of flexibility regarding validation
> > > > > > modes. Some providers may choose to have write-time
> > > > > > validation for all inserts, others may choose validate an
> > > > > > entire graph against a
> > > > > > schema as a batch job, while others may choose to validate
> > > > > > on-commit.
> > > > > > For our purposes, we need to provide a viable reference
> > > > > > implementation, as well as ensuring sufficient extension
> > > > > > points exist for
> > > > > > providers to fulfill their needs.
> > > > > > Dynamic schema updates from data writesIt would be useful
> > > > > > to auto-update the schema graph when data
> > > > > > writes introduce new labels or properties (e.g.
> > > > > > addV("newLabel”) automatically creates a VertexType).
> > > > > > Keeping the schema exactly
> > > > > > in- sync with such operations may introduce too much
> > > > > > overhead for
> > > > > > many purposes. We should provide appropriate hooks for
> > > > > > providers to implement such behaviour if desired, or to
> > > > > > help providers
> > > > > > aggregate changes and perform incremental batch updates to
> > > > > > the schema.
> > > > > > TransactionsThe schema graph will need to be transactional
> > > > > > if the data
> > > > > > File IOIt is often useful to persist and load schemas
> > > > > > to/from files.
> > > > > > This capability should be build into the GraphSchema class
> > > > > > via simple store() and load() methods, using a custom
> > > > > > compact JSON representation of the schema. The specifics of
> > > > > > this format are deferred to later discussion.
> > > > > > GraphSchema exposes file IO directly:
> > > > > > * store(OutputStream): serialize the schema to a compact
> > > > > > JSON representation
> > > > > > * load(InputStream): deserialize a schema from JSON and
> > > > > > merge it
> > > > > > into the current schema graph
> > > > > > Schema file IO should be implemented across all GLVs.
> > > > > > Reference ImplementationTinkerGraph serves as the reference
> > > > > > implementation:
> > > > > > * TinkerGraphSchema extends TinkerGraph implements
> > > > > > GraphSchema
> > > > > > * TinkerVertexType extends TinkerVertex implements
> > > > > > VertexType
> > > > > > * TinkerPropertyType extends TinkerVertex implements
> > > > > > PropertyType
> > > > > > * TinkerEdgeType extends TinkerVertex implements EdgeType
> > > > > > * Recursion guard prevents schema-of-schema
> > > > > > (TinkerGraphSchema overrides initSchema())
> > > > > > Please let me know any thoughts you may have on the
> > > > > > approach. I intend to move this into a proposal PR soon,
> > > > > > unless there are any major disagreements over the design.
> > > > > > Thanks, Cole
> > > > >
> > > >
> > >
> >
> >
> > >
> > >
> > > >
> > >
> >
Re: [DISCUSS] Graph Schema Interfaces for TP
Posted by Cole Greer <co...@apache.org>.
Hi Pieter,
Thanks for the feedback.
> Will we not need an additional type to represent the
> 'EdgeLabel:VertexLabel:Direction'?
Regarding #1, I hadn't considered extracting something like an EdgeRole into a standalone object. The closest equivalent in the model I proposed is that each edge type would contain a list of fromNodeTypes and toNodeTypes. In the example you provided, the "loves" edge type would have fromNodeTypes of ["A"] and toNodeTypes of ["B", "C"]. The EdgeType in my proposal should map 1:1 to an EdgeType + a set of EdgeRoles in the sqlg model.
> Some backends will support transactions for schema and data changes.
> Some will support transactions only for data changes. Some databases
> like Mariadb will silently commit the current transaction when a schema
> change occurs.
>
> Probabaly there are variations on this theme, so it should be up to the
> provider to specify the granularity of support.
I agree that providers need flexibility to vary transaction support according to their specific backends and needs. There is also a bit of an open question as to how to coordinate transactions between the schema graph and data graph. My current thoughts is that the default behaviour should be to sync begins, commits, and rollbacks across the data and schema graphs as the default behaviour. I would think providers should be free to manage these independently if they prefer.
> Further I suggest adding schema.lock/unlock. If locked no schema
> changes are allowed. There can also be a transaction scoped unlock()
> which will just for the current transaction unlock the schema.
>
> This will add in a safety net for graphs with a well defined schema
> where no code should be able to make schema changes.
I like this suggestion, I think this would make sense as lock() and unlock() methods on the GraphSchema structure API class. I think it would be fine to exclude this from the gremlin language and remote APIs for now, as this could always be added later if it demand arises. Any thoughts on this approach?
> Regarding File IO.
>
> I am not sure why this is needed. We can simply export/import some
> gremlin to manage the schema. We already have the language that does
> this, why is a translation to json needed?
I would say that File IO isn't strictly necessary, but I also see little downside in including it. I believe it needs relatively little effort to include it, and I don't expect it to be an ongoing maintenance burden. There's some demand for it as a feature, I would use it as a compact and convenient way of persisting and loading my schemas. Josh has expressed interest in it as well. We have similar precedent in the data graph side with the io() step existing to facilitate bulk reads/writes instead of relying on gremlin for all ingest. I would also see no issue in any providers choosing not to support the file io in their schema implementations if they so choose.
Please let me know your thoughts on these matters.
Thanks,
Cole
On 2026/04/04 10:24:20 pieter wrote:
> Hi,
>
> This looks good to me.
>
> #1
>
> Will we not need an additional type to represent the
> 'EdgeLabel:VertexLabel:Direction'?
>
> In Sqlg this is called 'EdgeRole'.
>
> class EdgeRole {
> private EdgeType;
> private VertexType;
> private Direction;
> }
>
> Consider the following example
>
> VertexType aVertexType = schema.traversal().addVType("A");
> VertexType bVertexType = schema.traversal().addVType("B");
> VertexType cVertexType = schema.traversal().addVType("C");
> EdgeType lovesEdgeType = schema.traversal().addEType("loves")
> lovesEdgeType.from("A").to("B");
> lovesEdgeType.from("A").to("C");
>
> In this case there are 3 EdgesRoles
>
> loves:A:OUT
> loves:B:IN
> loves:C:IN
>
> Later the user needs to manipulate the 'loves:C:IN' EdgeRole like
> deleting it or changing the multiplicities...
> For this the user needs to be able to access the EdgeRole.
>
> EdgeRole cLovesEdgeRole = lovesEdgeType.edgeRole(cVertexType);
> cLovesEdgeRole.drop();
>
> #2
>
> Regarding transactions.
>
> Some backends will support transactions for schema and data changes.
> Some will support transactions only for data changes. Some databases
> like Mariadb will silently commit the current transaction when a schema
> change occurs.
>
> Probabaly there are variations on this theme, so it should be up to the
> provider to specify the granularity of support.
>
> Further I suggest adding schema.lock/unlock. If locked no schema
> changes are allowed. There can also be a transaction scoped unlock()
> which will just for the current transaction unlock the schema.
>
> This will add in a safety net for graphs with a well defined schema
> where no code should be able to make schema changes.
>
> #3
>
> Regarding File IO.
>
> I am not sure why this is needed. We can simply export/import some
> gremlin to manage the schema. We already have the language that does
> this, why is a translation to json needed?
>
> Regards
> Pieter
>
> On Fri, 2026-04-03 at 02:16 +0000, Cole Greer via dev wrote:
> > Hi Everyone,
> >
> > The topic of Graph Schema has been discussed extensively in recent
> > TInkerPop Gatherings, and the following proposal has emerged from
> > these gatherings. I believe it is now ready for broad consideration
> > and discussions. I’ve done my best to incorporate initial feedback
> > from Josh, Pieter, Valentyn, Stephen, Kris and others into this
> > proposal, however I won’t claim that it accurately represents the
> > views of anyone other than myself at this time. This is a broad topic
> > and I’m deliberately excluding critical topics to focus this thread
> > on standardizing interfaces for gremlin users and providers to
> > interact with schema (see assumptions for more details).
> >
> > ## Overview
> >
> > This proposal introduces graph schema interfaces for TinkerPop: a way
> > to define vertex types, edge types, and property types as a meta-
> > graph that is itself traversable with Gremlin. The schema describes
> > the structure of a data graph; what kinds of vertices and edges
> > exist, what properties they carry, and how they connect..
> >
> > ## Assumptions
> >
> > - Type keys are element labels: there is a 1-to-1 mapping between a
> > label and a type definition. A vertex labeled "person" corresponds to
> > exactly one VertexType, and an edge labeled "knows" corresponds to
> > exactly one EdgeType.
> > - Java classes are used as a type system: This proposal uses Java
> > classes to define property type constraints. This is intended as a
> > placeholder to be replaced by a proper type system to be defined via
> > a later discussion.
> > - This proposal makes very little consideration of if/when/where/how
> > validation and enforcement of schema takes place. I believe it is
> > important for us to ship something which is flexible and useful to
> > providers out of the box as well as leaving space for providers to
> > plugin existing implementations or build their own if they desire.
> > I’ve left this out of scope for this proposal to focus first on
> > interfaces which give providers the appropriate access to schema.
> >
> > ## Design Points
> >
> > ### 1. Schema-as-Graph
> >
> > `GraphSchema extends Graph`. Providers implement a familiar
> > interface, and users traverse the schema with schema.traversal().
> > This avoids inventing a parallel API surface. The schema is just
> > another graph.
> >
> > A data graph exposes its schema via Graph.schema(), which returns the
> > GraphSchema instance. Providers that don't support schema return
> > UnsupportedOperationException by default.
> >
> > ### 2. All type definitions are vertices
> >
> > VertexType, EdgeType, and PropertyType are all vertices in the schema
> > meta-graph.
> >
> > - A VertexType vertex represents a vertex label definition (e.g.
> > "person", "software").
> > - An EdgeType vertex represents an edge label definition (e.g.
> > "knows", "created"). Even though it describes edges in the data
> > graph, it is itself a vertex in the schema graph, connected to its
> > endpoint VertexType vertices via from/to edges.
> > - A PropertyType vertex represents a property on a type, connected to
> > its parent type vertex via a “hasProperty" edge.
> >
> > Property definitions are independent per type, no sharing across
> > types.
> >
> > Schema graph example for the classic TinkerPop modern graph:
> > ```
> > (person:vertexType) --hasProperty--> (name:propertyType)
> > (person:vertexType) --hasProperty--> (age:propertyType)
> > (software:vertexType) --hasProperty--> (name:propertyType)
> > (software:vertexType) --hasProperty--> (lang:propertyType)
> > (knows:edgeType) --from--> (person:vertexType)
> > (knows:edgeType) --to--> (person:vertexType)
> > (knows:edgeType) --hasProperty--> (weight:propertyType)
> > (created:edgeType) --from--> (person:vertexType)
> > (created:edgeType) --to--> (software:vertexType)
> > (created:edgeType) --hasProperty--> (weight:propertyType)
> > ```
> >
> > ### 3. Constraints are properties on type vertices
> >
> > Rather than a fixed constraint taxonomy, constraints are regular
> > properties on type vertices, keyed by string via constraint(key,
> > value). This keeps the model extensible such that providers can
> > define their own constraints without changes to the core API.
> >
> > Constraints can be added to VertexType, EdgeType, and PropertyType
> > vertices directly. The most common constraints such as property types
> > and required properties would apply to PropertyTypes, while edge
> > multiplicity constraints (e.g. one-to-many, one-to-one) are naturally
> > expressed as constraints on the EdgeType itself rather than on any
> > property.
> >
> > While constraint keys are arbitrary strings and providers are free to
> > implement any constraints they like, TinkerPop should standardize a
> > set of core constraint keys representing the most common constraints.
> > Examples include “type", “required", “unique", “minValue",
> > “maxValue", etc. Providers that support equivalent constraints are
> > encouraged to follow these conventional names for interoperability.
> >
> > Non-core constraints (custom to a provider) are encouraged to follow
> > a namespaced key convention to avoid collisions, e.g.
> > "tinkergraph:notNull". Core constraint keys are unnamespaced.
> >
> > ### 4. Schema traversal steps in core Gremlin
> >
> > New steps for schema manipulation live directly in
> > GraphTraversal/GraphTraversalSource, not in a separate DSL:
> >
> > - addVType(label) — creates a VertexType vertex
> > - addEType(label) — creates an EdgeType vertex
> > - propertyType(name) — creates a PropertyType vertex and connects it
> > via hasProperty
> > - constraint(key, value) — adds a constraint property to the current
> > type vertex
> >
> > Example: defining a vertex type with properties:
> > ```
> > schema.traversal().addVType("person")
> > .propertyType("name").constraint("type",
> > String.class).constraint("required", true).constraint("unique", true)
> > .propertyType("age").constraint("type", Integer.class)
> > ```
> >
> > Example: defining an edge type with endpoint types and a property:
> > ```
> > schema.traversal().addEType("knows")
> > .from("person").to("person")
> > .propertyType("weight").constraint("type", Double.class)
> > ```
> >
> > This mirrors the addE().from().to() pattern from the data-graph. Here
> > from() and to() take vertex type labels (strings) and create from/to
> > edges in the schema graph connecting the EdgeType to the referenced
> > VertexType vertices.
> >
> > ### 5. Convenience methods for direct access
> >
> > The schema-as-graph model is the source of truth, but traversing it
> > for simple lookups isn’t always convenient. Direct methods provide
> > compact access:
> >
> > GraphSchema methods:
> > - vertexTypes() → Collection<VertexType>
> > - vertexType(String label) → Optional<VertexType>
> > - edgeTypes() → Collection<EdgeType>
> > - edgeType(String label) → Optional<EdgeType>
> > - addVertexType(String label) → VertexType
> > - addEdgeType(String label) → EdgeType
> > - store(OutputStream): serialize the schema to a compact JSON
> > representation
> > - load(InputStream): deserialize and merge a schema from JSON into
> > this schema graph
> >
> > EdgeType methods:
> > - fromVertexTypes() → Collection<VertexType>
> > - toVertexTypes() → Collection<VertexType>
> >
> > Example:
> > ```
> > GraphSchema schema = graph.schema();
> >
> > // Look up a vertex type
> > VertexType person = schema.vertexType("person").orElseThrow();
> >
> > // Inspect its properties
> > for (PropertyType pd : person.propertyTypes()) {
> > System.out.println(pd.name() + " : " + pd.constraint("type"));
> > }
> >
> > // Look up an edge type and its connectivity
> > EdgeType knows = schema.edgeType("knows").orElseThrow();
> > Collection<VertexType> fromTypes = knows.fromVertexTypes();
> > Collection<VertexType> toTypes = knows.toVertexTypes();
> > ```
> >
> > ### 6. Cross-graph jumps
> >
> > Two steps bridge the data graph and schema graph:
> >
> > - type(): from a data traversal, jump to the element's type
> > definition in the schema graph.
> > - instances(): from a schema traversal, jump to all matching elements
> > in the data graph.
> >
> > These compose for round-trip traversals:
> > ```
> > // Get the type definition for "person" vertices
> > g.V().hasLabel("person").type()
> >
> > // Get all instances of a schema type
> > schema.traversal().vertexType("person").instances()
> >
> > // Round-trip: find marko's type, then get all instances of that type
> > g.V().has("person", "name", "marko").type().instances()
> > ```
> >
> > ### 7. Schema restriction strategy
> >
> > There are some steps we will want to restrict in both the data graph
> > and the schema-graph. addVType() wouldn’t make sense in the data-
> > graph, nor would addV() be sensible in the schema-graph. A
> > TraversalStrategy can restrict schema traversals to a safe subset of
> > Gremlin steps (allowlist-based). This prevents accidentally running
> > data element insertions, OLAP computations, complex control flow, or
> > side-effect steps against the schema graph. The strategy should be
> > auto-registered when traversing a GraphSchema instance.
> >
> > The exact allowlist should be a topic for later discussion.
> >
> > ### 8. Instance counts on type vertices
> >
> > VertexType.instanceCount() and EdgeType.instanceCount() return the
> > count of data graph elements matching each type. This is a method
> > rather than a property on the type vertex, keeping the schema graph
> > definitional (not statistical) and giving providers full
> > implementation flexibility.
> >
> > Approximate counts are likely acceptable and preferable for
> > performance in most cases. However, TinkerPop should not stand in the
> > way of providers that prefer exact counts, and should ensure that
> > appropriate hooks are in place in reference implementations so that
> > providers can maintain exact counts if they so desire.
> >
> > Transactional implications need additional consideration. Maintaining
> > accurate counts across concurrent writes, rollbacks, and transaction
> > isolation levels adds significant complexity. This interacts with the
> > broader schema transactions question (see transactions below) and
> > should be addressed alongside it.
> >
> > ### 9. GLV Support
> >
> > Each GLV (Python, JavaScript, .NET, Go) needs:
> >
> > - Schema data classes: Parallel classes to the 4 core Java
> > interfaces, following the same pattern as existing Vertex and Edge
> > classes. These are data containers representing schema objects
> > returned from the server:
> > - GraphSchema: holds collections of VertexTypes and EdgeTypes
> > - VertexType: label, full constraints map, and collection of
> > PropertyTypes
> > - EdgeType: label, full constraints map, from/to VertexType
> > references (same pattern as Edge.outV/Edge.inV), and collection of
> > PropertyTypes
> > - PropertyType: name and full constraints map (including data type
> > as a constraint)
> > - All new gremlin steps are supported from each GLV
> >
> > ## Future Questions
> >
> > ### Schema validation
> >
> > Providers will need lots of flexibility regarding validation modes.
> > Some providers may choose to have write-time validation for all
> > inserts, others may choose validate an entire graph against a schema
> > as a batch job, while others may choose to validate on-commit. For
> > our purposes, we need to provide a viable reference implementation,
> > as well as ensuring sufficient extension points exist for providers
> > to fulfill their needs.
> >
> > ### Dynamic schema updates from data writes
> >
> > It would be useful to auto-update the schema graph when data writes
> > introduce new labels or properties (e.g. addV("newLabel”)
> > automatically creates a VertexType). Keeping the schema exactly in-
> > sync with such operations may introduce too much overhead for many
> > purposes. We should provide appropriate hooks for providers to
> > implement such behaviour if desired, or to help providers aggregate
> > changes and perform incremental batch updates to the schema.
> >
> > ### Transactions
> >
> > The schema graph will need to be transactional if the data
> >
> > ### File IO
> >
> > It is often useful to persist and load schemas to/from files. This
> > capability should be build into the GraphSchema class via simple
> > store() and load() methods, using a custom compact JSON
> > representation of the schema. The specifics of this format are
> > deferred to later discussion.
> >
> > GraphSchema exposes file IO directly:
> > - store(OutputStream): serialize the schema to a compact JSON
> > representation
> > - load(InputStream): deserialize a schema from JSON and merge it into
> > the current schema graph
> >
> > Schema file IO should be implemented across all GLVs.
> >
> > ## Reference Implementation
> >
> > TinkerGraph serves as the reference implementation:
> >
> > - TinkerGraphSchema extends TinkerGraph implements GraphSchema
> > - TinkerVertexType extends TinkerVertex implements VertexType
> > - TinkerPropertyType extends TinkerVertex implements PropertyType
> > - TinkerEdgeType extends TinkerVertex implements EdgeType
> > - Recursion guard prevents schema-of-schema (TinkerGraphSchema
> > overrides initSchema())
> >
> >
> > Please let me know any thoughts you may have on the approach. I
> > intend to move this into a proposal PR soon, unless there are any
> > major disagreements over the design.
> >
> > Thanks,
> > Cole
>
Re: [DISCUSS] Graph Schema Interfaces for TP
Posted by pieter <pi...@riseup.net>.
Hi,
This looks good to me.
#1
Will we not need an additional type to represent the
'EdgeLabel:VertexLabel:Direction'?
In Sqlg this is called 'EdgeRole'.
class EdgeRole {
private EdgeType;
private VertexType;
private Direction;
}
Consider the following example
VertexType aVertexType = schema.traversal().addVType("A");
VertexType bVertexType = schema.traversal().addVType("B");
VertexType cVertexType = schema.traversal().addVType("C");
EdgeType lovesEdgeType = schema.traversal().addEType("loves")
lovesEdgeType.from("A").to("B");
lovesEdgeType.from("A").to("C");
In this case there are 3 EdgesRoles
loves:A:OUT
loves:B:IN
loves:C:IN
Later the user needs to manipulate the 'loves:C:IN' EdgeRole like
deleting it or changing the multiplicities...
For this the user needs to be able to access the EdgeRole.
EdgeRole cLovesEdgeRole = lovesEdgeType.edgeRole(cVertexType);
cLovesEdgeRole.drop();
#2
Regarding transactions.
Some backends will support transactions for schema and data changes.
Some will support transactions only for data changes. Some databases
like Mariadb will silently commit the current transaction when a schema
change occurs.
Probabaly there are variations on this theme, so it should be up to the
provider to specify the granularity of support.
Further I suggest adding schema.lock/unlock. If locked no schema
changes are allowed. There can also be a transaction scoped unlock()
which will just for the current transaction unlock the schema.
This will add in a safety net for graphs with a well defined schema
where no code should be able to make schema changes.
#3
Regarding File IO.
I am not sure why this is needed. We can simply export/import some
gremlin to manage the schema. We already have the language that does
this, why is a translation to json needed?
Regards
Pieter
On Fri, 2026-04-03 at 02:16 +0000, Cole Greer via dev wrote:
> Hi Everyone,
>
> The topic of Graph Schema has been discussed extensively in recent
> TInkerPop Gatherings, and the following proposal has emerged from
> these gatherings. I believe it is now ready for broad consideration
> and discussions. I’ve done my best to incorporate initial feedback
> from Josh, Pieter, Valentyn, Stephen, Kris and others into this
> proposal, however I won’t claim that it accurately represents the
> views of anyone other than myself at this time. This is a broad topic
> and I’m deliberately excluding critical topics to focus this thread
> on standardizing interfaces for gremlin users and providers to
> interact with schema (see assumptions for more details).
>
> ## Overview
>
> This proposal introduces graph schema interfaces for TinkerPop: a way
> to define vertex types, edge types, and property types as a meta-
> graph that is itself traversable with Gremlin. The schema describes
> the structure of a data graph; what kinds of vertices and edges
> exist, what properties they carry, and how they connect..
>
> ## Assumptions
>
> - Type keys are element labels: there is a 1-to-1 mapping between a
> label and a type definition. A vertex labeled "person" corresponds to
> exactly one VertexType, and an edge labeled "knows" corresponds to
> exactly one EdgeType.
> - Java classes are used as a type system: This proposal uses Java
> classes to define property type constraints. This is intended as a
> placeholder to be replaced by a proper type system to be defined via
> a later discussion.
> - This proposal makes very little consideration of if/when/where/how
> validation and enforcement of schema takes place. I believe it is
> important for us to ship something which is flexible and useful to
> providers out of the box as well as leaving space for providers to
> plugin existing implementations or build their own if they desire.
> I’ve left this out of scope for this proposal to focus first on
> interfaces which give providers the appropriate access to schema.
>
> ## Design Points
>
> ### 1. Schema-as-Graph
>
> `GraphSchema extends Graph`. Providers implement a familiar
> interface, and users traverse the schema with schema.traversal().
> This avoids inventing a parallel API surface. The schema is just
> another graph.
>
> A data graph exposes its schema via Graph.schema(), which returns the
> GraphSchema instance. Providers that don't support schema return
> UnsupportedOperationException by default.
>
> ### 2. All type definitions are vertices
>
> VertexType, EdgeType, and PropertyType are all vertices in the schema
> meta-graph.
>
> - A VertexType vertex represents a vertex label definition (e.g.
> "person", "software").
> - An EdgeType vertex represents an edge label definition (e.g.
> "knows", "created"). Even though it describes edges in the data
> graph, it is itself a vertex in the schema graph, connected to its
> endpoint VertexType vertices via from/to edges.
> - A PropertyType vertex represents a property on a type, connected to
> its parent type vertex via a “hasProperty" edge.
>
> Property definitions are independent per type, no sharing across
> types.
>
> Schema graph example for the classic TinkerPop modern graph:
> ```
> (person:vertexType) --hasProperty--> (name:propertyType)
> (person:vertexType) --hasProperty--> (age:propertyType)
> (software:vertexType) --hasProperty--> (name:propertyType)
> (software:vertexType) --hasProperty--> (lang:propertyType)
> (knows:edgeType) --from--> (person:vertexType)
> (knows:edgeType) --to--> (person:vertexType)
> (knows:edgeType) --hasProperty--> (weight:propertyType)
> (created:edgeType) --from--> (person:vertexType)
> (created:edgeType) --to--> (software:vertexType)
> (created:edgeType) --hasProperty--> (weight:propertyType)
> ```
>
> ### 3. Constraints are properties on type vertices
>
> Rather than a fixed constraint taxonomy, constraints are regular
> properties on type vertices, keyed by string via constraint(key,
> value). This keeps the model extensible such that providers can
> define their own constraints without changes to the core API.
>
> Constraints can be added to VertexType, EdgeType, and PropertyType
> vertices directly. The most common constraints such as property types
> and required properties would apply to PropertyTypes, while edge
> multiplicity constraints (e.g. one-to-many, one-to-one) are naturally
> expressed as constraints on the EdgeType itself rather than on any
> property.
>
> While constraint keys are arbitrary strings and providers are free to
> implement any constraints they like, TinkerPop should standardize a
> set of core constraint keys representing the most common constraints.
> Examples include “type", “required", “unique", “minValue",
> “maxValue", etc. Providers that support equivalent constraints are
> encouraged to follow these conventional names for interoperability.
>
> Non-core constraints (custom to a provider) are encouraged to follow
> a namespaced key convention to avoid collisions, e.g.
> "tinkergraph:notNull". Core constraint keys are unnamespaced.
>
> ### 4. Schema traversal steps in core Gremlin
>
> New steps for schema manipulation live directly in
> GraphTraversal/GraphTraversalSource, not in a separate DSL:
>
> - addVType(label) — creates a VertexType vertex
> - addEType(label) — creates an EdgeType vertex
> - propertyType(name) — creates a PropertyType vertex and connects it
> via hasProperty
> - constraint(key, value) — adds a constraint property to the current
> type vertex
>
> Example: defining a vertex type with properties:
> ```
> schema.traversal().addVType("person")
> .propertyType("name").constraint("type",
> String.class).constraint("required", true).constraint("unique", true)
> .propertyType("age").constraint("type", Integer.class)
> ```
>
> Example: defining an edge type with endpoint types and a property:
> ```
> schema.traversal().addEType("knows")
> .from("person").to("person")
> .propertyType("weight").constraint("type", Double.class)
> ```
>
> This mirrors the addE().from().to() pattern from the data-graph. Here
> from() and to() take vertex type labels (strings) and create from/to
> edges in the schema graph connecting the EdgeType to the referenced
> VertexType vertices.
>
> ### 5. Convenience methods for direct access
>
> The schema-as-graph model is the source of truth, but traversing it
> for simple lookups isn’t always convenient. Direct methods provide
> compact access:
>
> GraphSchema methods:
> - vertexTypes() → Collection<VertexType>
> - vertexType(String label) → Optional<VertexType>
> - edgeTypes() → Collection<EdgeType>
> - edgeType(String label) → Optional<EdgeType>
> - addVertexType(String label) → VertexType
> - addEdgeType(String label) → EdgeType
> - store(OutputStream): serialize the schema to a compact JSON
> representation
> - load(InputStream): deserialize and merge a schema from JSON into
> this schema graph
>
> EdgeType methods:
> - fromVertexTypes() → Collection<VertexType>
> - toVertexTypes() → Collection<VertexType>
>
> Example:
> ```
> GraphSchema schema = graph.schema();
>
> // Look up a vertex type
> VertexType person = schema.vertexType("person").orElseThrow();
>
> // Inspect its properties
> for (PropertyType pd : person.propertyTypes()) {
> System.out.println(pd.name() + " : " + pd.constraint("type"));
> }
>
> // Look up an edge type and its connectivity
> EdgeType knows = schema.edgeType("knows").orElseThrow();
> Collection<VertexType> fromTypes = knows.fromVertexTypes();
> Collection<VertexType> toTypes = knows.toVertexTypes();
> ```
>
> ### 6. Cross-graph jumps
>
> Two steps bridge the data graph and schema graph:
>
> - type(): from a data traversal, jump to the element's type
> definition in the schema graph.
> - instances(): from a schema traversal, jump to all matching elements
> in the data graph.
>
> These compose for round-trip traversals:
> ```
> // Get the type definition for "person" vertices
> g.V().hasLabel("person").type()
>
> // Get all instances of a schema type
> schema.traversal().vertexType("person").instances()
>
> // Round-trip: find marko's type, then get all instances of that type
> g.V().has("person", "name", "marko").type().instances()
> ```
>
> ### 7. Schema restriction strategy
>
> There are some steps we will want to restrict in both the data graph
> and the schema-graph. addVType() wouldn’t make sense in the data-
> graph, nor would addV() be sensible in the schema-graph. A
> TraversalStrategy can restrict schema traversals to a safe subset of
> Gremlin steps (allowlist-based). This prevents accidentally running
> data element insertions, OLAP computations, complex control flow, or
> side-effect steps against the schema graph. The strategy should be
> auto-registered when traversing a GraphSchema instance.
>
> The exact allowlist should be a topic for later discussion.
>
> ### 8. Instance counts on type vertices
>
> VertexType.instanceCount() and EdgeType.instanceCount() return the
> count of data graph elements matching each type. This is a method
> rather than a property on the type vertex, keeping the schema graph
> definitional (not statistical) and giving providers full
> implementation flexibility.
>
> Approximate counts are likely acceptable and preferable for
> performance in most cases. However, TinkerPop should not stand in the
> way of providers that prefer exact counts, and should ensure that
> appropriate hooks are in place in reference implementations so that
> providers can maintain exact counts if they so desire.
>
> Transactional implications need additional consideration. Maintaining
> accurate counts across concurrent writes, rollbacks, and transaction
> isolation levels adds significant complexity. This interacts with the
> broader schema transactions question (see transactions below) and
> should be addressed alongside it.
>
> ### 9. GLV Support
>
> Each GLV (Python, JavaScript, .NET, Go) needs:
>
> - Schema data classes: Parallel classes to the 4 core Java
> interfaces, following the same pattern as existing Vertex and Edge
> classes. These are data containers representing schema objects
> returned from the server:
> - GraphSchema: holds collections of VertexTypes and EdgeTypes
> - VertexType: label, full constraints map, and collection of
> PropertyTypes
> - EdgeType: label, full constraints map, from/to VertexType
> references (same pattern as Edge.outV/Edge.inV), and collection of
> PropertyTypes
> - PropertyType: name and full constraints map (including data type
> as a constraint)
> - All new gremlin steps are supported from each GLV
>
> ## Future Questions
>
> ### Schema validation
>
> Providers will need lots of flexibility regarding validation modes.
> Some providers may choose to have write-time validation for all
> inserts, others may choose validate an entire graph against a schema
> as a batch job, while others may choose to validate on-commit. For
> our purposes, we need to provide a viable reference implementation,
> as well as ensuring sufficient extension points exist for providers
> to fulfill their needs.
>
> ### Dynamic schema updates from data writes
>
> It would be useful to auto-update the schema graph when data writes
> introduce new labels or properties (e.g. addV("newLabel”)
> automatically creates a VertexType). Keeping the schema exactly in-
> sync with such operations may introduce too much overhead for many
> purposes. We should provide appropriate hooks for providers to
> implement such behaviour if desired, or to help providers aggregate
> changes and perform incremental batch updates to the schema.
>
> ### Transactions
>
> The schema graph will need to be transactional if the data
>
> ### File IO
>
> It is often useful to persist and load schemas to/from files. This
> capability should be build into the GraphSchema class via simple
> store() and load() methods, using a custom compact JSON
> representation of the schema. The specifics of this format are
> deferred to later discussion.
>
> GraphSchema exposes file IO directly:
> - store(OutputStream): serialize the schema to a compact JSON
> representation
> - load(InputStream): deserialize a schema from JSON and merge it into
> the current schema graph
>
> Schema file IO should be implemented across all GLVs.
>
> ## Reference Implementation
>
> TinkerGraph serves as the reference implementation:
>
> - TinkerGraphSchema extends TinkerGraph implements GraphSchema
> - TinkerVertexType extends TinkerVertex implements VertexType
> - TinkerPropertyType extends TinkerVertex implements PropertyType
> - TinkerEdgeType extends TinkerVertex implements EdgeType
> - Recursion guard prevents schema-of-schema (TinkerGraphSchema
> overrides initSchema())
>
>
> Please let me know any thoughts you may have on the approach. I
> intend to move this into a proposal PR soon, unless there are any
> major disagreements over the design.
>
> Thanks,
> Cole