Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
deb301f
feat: add GQL.g4 minimal ANTLR grammar for MATCH node/edge patterns
spmallette Apr 11, 2026
421c46c
feat: add DeclarativeMatchStep for GQL-based pattern matching
spmallette Apr 11, 2026
691d092
fix: update TinkerGraphGqlPlanner to use canonical GQL.g4 grammar rul…
spmallette Apr 11, 2026
f289202
feat: implement TinkerGraphGqlExecutor with DFS backtracking pattern …
spmallette Apr 11, 2026
56665ea
feat: implement TinkerGraphMatchStep: executor integration and traver…
spmallette Apr 12, 2026
3a2c825
feat: add TinkerGraphDeclarativeMatchStrategy optimization strategy
spmallette Apr 12, 2026
8ea08b7
feat: add match(String) and match(String, Map) grammar rules to Greml…
spmallette Apr 11, 2026
4b18f9e
feat: add DeclarativeMatchVerificationStrategy to prevent terminal ma…
spmallette Apr 12, 2026
b2e9eb5
feat: add match(String) and match(String, Map) spawn methods to Graph…
spmallette Apr 12, 2026
2732946
feat: add match(String) spawn method to Python GLV and integration test
spmallette Apr 12, 2026
abbc0f0
feat: add match(String) and match(String, Map) to JS GLV and integrat…
spmallette Apr 12, 2026
bcc7900
feat: add Match(string) and Match(string, IDictionary) overloads to g…
spmallette Apr 12, 2026
fcd7ed5
feat: add match(String) support to gremlin-go GLV with integration test
spmallette Apr 12, 2026
17042d3
feat: add match(String) and match(String,Map) to grammar visitors
spmallette Apr 12, 2026
bd4cbb2
fix: update TinkerGraphMatchStepTest to not use match() as terminal step
spmallette Apr 12, 2026
d59c895
Fix broken translations
spmallette Apr 12, 2026
1d4bb7e
fix: code review — bugs, per-graph singletons, tests, and translations
spmallette Apr 13, 2026
2e2cd9e
Fixed up match() start step
spmallette Apr 13, 2026
df2021d
Renamed parameter names
spmallette Apr 15, 2026
9fc1c36
refactor: remove DEFAULT_QUERY_LANGUAGE from DeclarativeMatchStep — l…
spmallette Apr 15, 2026
3d2aa78
rename gqlQuery to matchQuery across gremlin-core and all GLVs
spmallette Apr 17, 2026
2cdac00
Added documentation for match step
spmallette Apr 20, 2026
1f4bcc0
GQL engine: label/edge count indexes, DAG executor, live seed selection
spmallette Apr 22, 2026
179b387
refactor: array bindings + lazy Iterator delivery in GQL executor
spmallette Apr 22, 2026
a33396e
feat: GQL property filters — inline map syntax and parameter references
spmallette Apr 23, 2026
24f1cd6
feat: use TinkerGraph vertex index as seed source for property-filter…
spmallette Apr 23, 2026
5552c56
refactor: deduplicate parallel anonymous edges in GQL DFS executor
spmallette Apr 23, 2026
8cd9299
docs: document GQL property filter and parameterized query syntax
spmallette Apr 23, 2026
3c71f0c
feat: align GQL property filter literal types with Gremlin's type system
spmallette Apr 23, 2026
f44b648
refactor: rename QueryNode to QueryVertex to align with TinkerPop ver…
spmallette Apr 24, 2026
827e660
feat: property-aware step cost, best-eligible DAG selection, and adap…
spmallette Apr 30, 2026
707e949
fix: evaluate edge property filter predicates in GQL executor
spmallette May 1, 2026
f1aa1d0
fix: replace unbounded ConcurrentHashMap plan cache with Caffeine LRU…
spmallette May 1, 2026
b9c24d9
test: add gremlin-js integration tests for declarative match(String) …
spmallette May 1, 2026
b07dd06
docs: document edge property filters and update plan cache note
spmallette May 1, 2026
1b61549
fix: close GQL blindspots — variable conflicts, edge equality, plan s…
spmallette May 1, 2026
3c228c2
Added benchmarks for match()
spmallette May 3, 2026
7dfe365
fix: merge predicates and allow label refinement for reused node vari…
spmallette May 5, 2026
3eb669a
fix: use any-value-matches semantics for multi-properties in GQL pred…
spmallette May 5, 2026
5a46b12
test: add edge-case coverage for GQL property predicate and match() p…
spmallette May 5, 2026
4be33ae
Updated documentation for GQL/TinkerGraph
spmallette May 6, 2026
b46d10c
fix: match(String) returns binding Map instead of Optional.empty
spmallette May 7, 2026
459bf9b
fix: correct mock strategy in JS match-test to use _resultsStream
spmallette May 7, 2026
9b188b9
Documentation improvements for match after moving out Optional
spmallette May 8, 2026
56a4e94
test: add executor coverage for Byte, Short, BigInteger, BigDecimal, …
spmallette May 8, 2026
76879c4
Adjust GLV tests gien change to match() returning Map
spmallette May 11, 2026
7474d9d
docs: add Proposal 10 — Generalized GQL Execution Engine (gql-gremlin…
spmallette May 11, 2026
d6f9f87
docs: revise Proposal 10 — replace GraphStatistics/IndexAccess interf…
spmallette May 27, 2026
899c174
feat: implement Proposal 10 — extract gql-gremlin module with general…
spmallette May 27, 2026
a1cd275
docs: brand TinkerGQL, add gql-gremlin module docs and provider guide
spmallette May 27, 2026
afa722b
refactor: centralize GQL planner/executor cache in GqlDeclarativeMatc…
spmallette May 27, 2026
8ac8d5a
Provide a way to configure the cache for the planner explicitly
spmallette May 28, 2026
ddbaa8c
docs: fix provider guide, upgrade notes, and reference docs for gql-g…
spmallette May 28, 2026
8ccd7ba
test: add Gherkin coverage for match(String) / TinkerGQL; remove lang…
spmallette May 28, 2026
82fc02d
test: expand TinkerGQL Gherkin coverage and fix OLAP test exclusion
spmallette May 28, 2026
9a14cfa
fix: add GLV translator support for match(String, Map) form in Python…
spmallette May 29, 2026
abee1df
fix: exclude @TinkerGQL scenarios from Hadoop feature test run
spmallette May 29, 2026
49c3ece
test: verify match(String) wire-protocol round-trip and add cyclic/no…
spmallette May 29, 2026
210671e
feat: guard against path-label/pattern-variable name collision in mat…
spmallette May 29, 2026
f646de6
refactor: polish match() PR — constant, docs, test consolidation
spmallette May 31, 2026
629d489
fix: add DeprecationWarning to Python match(Traversal) form
spmallette Jun 1, 2026
5ff82ff
test: add Gherkin scenarios for multi-pattern bridge join and combine…
spmallette Jun 1, 2026
6d6c6d3
fix: address PR review feedback on match() GQL parsing and Go transla…
spmallette Jun 10, 2026
06084d3
refactor: migrate GQL parser unit tests from tinkergraph-gremlin to g…
spmallette Jun 10, 2026
9546834
refactor: replace MatchWithParams with variadic Match on GraphTravers…
spmallette Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Added string parameter parsing to `GremlinServer` to prevent traversal injection and excessive nesting depths.
* Modified all GLVs to detect unsupported types in `GremlinLang` and throw consistent error for that case.
* Added GraphBinary 4.0 `Graph` (`0x10`) serializer/deserializer to `gremlin-javascript`, `gremlin-dotnet`, and `gremlin-go` so that `subgraph()` results are returned as a detached `Graph` data container.
* Added `match(string)` and `match(string, IDictionary)` overloads for declarative pattern matching.
* Deprecated the traversal-based `match(Traversal[])` in favor of the new string-based API.
* Added the `gql-gremlin` module containing the TinkerGQL engine, a `MATCH`-pattern executor implementing a deliberate minimal subset of ISO GQL, usable by any graph provider.
* Added `Graph.countVerticesByLabel(String)`, `Graph.countEdgesByLabel(String)`, and the `Graph.Index` nested interface as performance-hint extension points for the TinkerGQL engine.
* Added GQL support to TinkerGraph with `gql-gremlin`,

[[release-4-0-0-beta-2]]
=== TinkerPop 4.0.0-beta.2 (April 1, 2026)
Expand Down Expand Up @@ -90,7 +95,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Replace `WebSocket` with `HTTP` (non-streaming) in `gremlin-dotnet`.
* Added `MimeType` to `IMessageSerializer` and split client option to allow separate request and response serialization in `gremlin-dotnet`.
* Added `RequestInterceptor` to `gremlin-dotnet` with `auth` reference implementations.
* Added streaming deserialization to `gremlin-dotnet`, results are now deserialized incrementally from the HTTP response stream via `IAsyncEnumerable<T>`.
* Added streaming deserialization to `gremlin-dotnet`, results are now deserialized incrementally from the HTTP response stream via `IAsyncEnumerable<T>`.
* Target framework for `gremlin-dotnet` updated to `net8.0`.
* Bumped minimum Node.js version for `gremlin-javascript` from 20 to 22.
* Removed `mimeType` connection option in `gremlin-javascript` in favor of direct use of `reader` and `writer` options.
Expand Down
4 changes: 4 additions & 0 deletions docs/src/dev/developer/for-committers.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,10 @@ tries to reference inaccessible properties that are on elements only available b
the traversal does not mind the star graph limitation.
* `@GremlinGroovyNotSupported` - The scenario uses `gremlin-lang` syntax which is incompatible with `gremlin-groovy` and
cannot be resolved with the groovy translator. This is typically for scenarios using ambiguously-types `null` parameters.
* `@TinkerGQL` - The scenario uses `match(String)` with GQL MATCH syntax and requires a graph that supports the
TinkerGQL engine (i.e. has `GqlDeclarativeMatchStrategy` registered). Graph providers that do not support
`match(String)` should add `and not @TinkerGQL` to their `@CucumberOptions` tag filter. TinkerGraph includes
TinkerGQL support out of the box and runs these scenarios without any additional configuration.
* `@InsertionOrderingRequired` - The scenario is reliant on the graph system predictably returning results (vertices,
edges, properties) in the same order in which they were inserted into the graph.
* `@MetaProperties` - The scenario makes use of meta-properties.
Expand Down
7 changes: 4 additions & 3 deletions docs/src/dev/future/proposal-3-remove-closures.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
////
== Proposal 3 - Removing the Need for Closures/Lambda in Gremlin

=== Status
image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"]

This proposal has been accepted through a post to the TinkerPop Dev List and is ready to begin implementaion.
*Proposal 3*

== Removing the Need for Closures/Lambda in Gremlin

=== Motivation

Expand Down
2 changes: 1 addition & 1 deletion docs/src/dev/future/proposal-arrow-flight-2.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ under the License.
////
image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"]

*x.y.z - Proposal 2*
*Proposal 2*

= Gremlin Arrow Flight

Expand Down
2 changes: 1 addition & 1 deletion docs/src/dev/future/proposal-asbool-step-7.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ under the License.

image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"]

*x.y.z - Proposal 7*
*Proposal 7*

== asBool() Step

Expand Down
2 changes: 1 addition & 1 deletion docs/src/dev/future/proposal-asnumber-step-6.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ under the License.

image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"]

*x.y.z - Proposal 6*
*Proposal 6*

== asNumber() Step

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ specific language governing permissions and limitations
under the License.
////

image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"]

*Proposal 8*

== *Specification for the declarative `match()` step*

This document outlines the specification for a new, declarative
Expand Down
2 changes: 1 addition & 1 deletion docs/src/dev/future/proposal-equality-1.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ under the License.
////
image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"]

*x.y.z - Proposal 1*
*Proposal 1*

= Equality, Equivalence, Comparability and Orderability Semantics

Expand Down
2 changes: 1 addition & 1 deletion docs/src/dev/future/proposal-scoping-5.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ under the License.
////
image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"]

*x.y.z - Proposal 5*
*Proposal 5*

== Lazy vs. Eager Evaluation in TP4 ==

Expand Down
2 changes: 1 addition & 1 deletion docs/src/dev/future/proposal-transaction-4.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ under the License.
////
image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"]

*x.y.z - Proposal 4*
*Proposal 4*

== TinkerGraph Transaction Support

Expand Down
2 changes: 1 addition & 1 deletion docs/src/dev/future/proposal-type-predicate-8.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ under the License.

image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"]

*x.y.z - Proposal 8*
*Proposal 8*

== Type Predicate

Expand Down
53 changes: 53 additions & 0 deletions docs/src/dev/provider/gremlin-semantics.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1796,6 +1796,59 @@ See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/j
link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/LTrimLocalStep.java[source (local)],
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#lTrim-step[reference]

[[match-step]]
=== match()

*Description:* Executes a declarative pattern match query against the graph using a provider-supported query
string. The query string format is provider-specific; providers may use TinkerPop's optional `gql-gremlin` module
(which implements the <<tinkergql,TinkerGQL>> dialect) or supply their own engine. Users should consult their
graph system's documentation to determine what format is supported. The second argument, when provided, supplies
bound parameters to the query; how (and whether) these are used is left to the provider.

*Syntax:*

* `match(String matchQuery)` +
* `match(String matchQuery, Map<String, Object> params)`

[width="100%",options="header"]
|=========================================================
|Start Step |Mid Step |Modulated |Domain |Range
|Y |Y |Y (via `with()`) |`any` |`Map<String, Object>`
|=========================================================

*Arguments:*

* `matchQuery` - An opaque query string evaluated by the graph provider. For example, a GQL MATCH expression:
`"MATCH (a:person)-[:knows]->(b:person)"`.
* `params` - Optional bound parameters passed to the provider alongside the query string. May be `null` or empty.

*Modulation:*

`with("queryLanguage", value)` - specifies which query language the provider should use to evaluate the string.
When omitted, the provider uses its native or default language.

*Considerations:*

* The step is a provider-delegation point. `gremlin-core` supplies only the placeholder `DeclarativeMatchStep`;
graph providers replace it with their own implementation via a `TraversalStrategy`.
* The traverser value emitted by the step is a `Map<String, Object>` containing one entry per named variable in
the matched pattern, keyed by variable name. Providers must set this map as the traverser value for each result
row.
* Each named variable is also recorded as a labeled entry in the traverser's path, so results remain retrievable
by name via `select()` when downstream steps require individual values.
* Anonymous elements in the pattern (those without a variable name) are not exposed in the map or path and cannot be
referenced by `select()`.
* When used as a spawn step on `GraphTraversalSource`, no upstream traversers are consumed; the provider
executes the query once and emits one traverser per result row.

*Exceptions*

* If the step is reached and no supporting strategy has replaced the placeholder, an `UnsupportedOperationException` is
thrown.

See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/DeclarativeMatchStep.java[source],
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#match-step[reference]

[[merge-step]]
=== merge()

Expand Down
194 changes: 194 additions & 0 deletions docs/src/dev/provider/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,200 @@ from TinkerPop using the link:https://tinkerpop.apache.org/docs/x.y.z/reference/
Simply provide a `InputFormat` and `OutputFormat` that can be referenced by a `HadoopGraph` instance as discussed
in the link:https://tinkerpop.apache.org/docs/x.y.z/reference/#clonevertexprogram[Reference Documentation].

[[tinkerpop-providers-tinkergql]]
==== Supporting Declarative Pattern Matching (TinkerGQL)

Graph providers can offer declarative pattern matching via the `match(String)` step by integrating the
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#tinkergql[TinkerGQL] engine shipped in the optional
`gql-gremlin` module. TinkerGQL handles GQL `MATCH` parsing, query planning, and execution against any `Graph`
implementation through a thin set of default interface methods, so most providers can get working `match()` support
with only a few lines of wiring code.

===== Maven Dependency

Add `gql-gremlin` alongside your provider's existing TinkerPop dependencies:

[source,xml]
----
<dependency>
<groupId>org.apache.tinkerpop</groupId>
<artifactId>gql-gremlin</artifactId>
<version>x.y.z</version>
</dependency>
----

===== Registering the Strategy

Register `GqlDeclarativeMatchStrategy.instance()` in the global strategy cache from a static initializer on your
graph class. This is exactly how TinkerGraph registers TinkerGQL support:

[source,java]
----
import org.apache.tinkerpop.gremlin.gql.GqlDeclarativeMatchStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
import org.apache.tinkerpop.gremlin.structure.Graph;

public class AcmeGraph implements Graph {

static {
TraversalStrategies.GlobalCache.registerStrategies(
AcmeGraph.class,
TraversalStrategies.GlobalCache.getStrategies(Graph.class).clone()
.addStrategies(GqlDeclarativeMatchStrategy.instance())); // <1>
}
// ...
}
----

<1> `instance()` is a singleton. On first use against a given graph it lazily creates one `DefaultGqlPlanner`
and one `DefaultGqlExecutor` for that graph instance and caches them. All traversals on the same graph
reuse that pair, so the planner's query parse cache is shared automatically.

Providers should also evict the cached pair when the graph is closed to avoid retaining it in memory:

[source,java]
----
@Override
public void close() {
GqlDeclarativeMatchStrategy.evict(this); // <1>
// ... other cleanup
}
----

<1> Removes the per-graph `DefaultGqlPlanner` / `DefaultGqlExecutor` pair from the singleton cache.
`AbstractTinkerGraph.close()` does this automatically for TinkerGraph-based implementations.

===== Performance Hints (Optional, but strongly recommended)

Without these overrides the TinkerGQL planner has no cardinality information. It treats every label count as
unknown (`Long.MAX_VALUE`), which causes it to pick the seed vertex arbitrarily and order extension steps without
regard to label density. On any graph of meaningful size this produces unnecessarily large intermediate result
sets and slow query execution. The fix is two method overrides.

The most common customization — and the one that requires the least code — is telling the planner about your
graph's label cardinalities. Overriding these two methods on your `Graph` implementation is all that is needed
for the planner to make better join-ordering decisions:

[source,java]
----
@Override
public long countVerticesByLabel(final String label) {
return myLabelIndex.getCount(label); // <1>
}

@Override
public long countEdgesByLabel(final String label) {
return myEdgeLabelIndex.getCount(label);
}
----

<1> Return `Long.MAX_VALUE` if the count is unknown. The planner uses these values only for relative comparison
when choosing a seed vertex and ordering extension steps — approximate counts are fine.

These hooks feed directly into `DefaultGqlPlanner`'s seed-selection and step-ordering logic, so no planner
replacement is needed to benefit from them.

===== Property Index Integration (Optional, but strongly recommended)

Without this override every `MATCH` query performs a full scan of all vertices regardless of any inline property
filters in the pattern. A query like `MATCH (p:Person {name: $n})` will iterate every vertex in the graph and
test each one against the predicate, even if your graph has a `name` index that could satisfy it in a single
lookup. The planner is also unable to use index cardinalities to refine seed selection, so join ordering
degrades to label-count-only heuristics.

If your graph maintains property indexes, expose them by returning a custom `Graph.Index` from `Graph.index()`.
When a `MATCH` pattern includes an inline property filter and the executor finds an index for that key, it
replaces the full vertex scan with a targeted index lookup. The planner also uses index cardinalities to
refine seed selection:

[source,java]
----
@Override
public Graph.Index index() {
return new Graph.Index() {

@Override
public Iterator<Vertex> queryVertexIndex(final String key, final Object value) {
return myVertexIndex.lookup(key, value); // <1>
}

@Override
public long countVertexIndex(final String key, final Object value) {
return myVertexIndex.isIndexed(key)
? myVertexIndex.count(key, value)
: Long.MAX_VALUE; // <2>
}
};
}
----

<1> Return an empty iterator (not `null`) when the key is not indexed or no match exists.
<2> Returning `Long.MAX_VALUE` for an un-indexed key signals both the planner and executor to fall back to a
full vertex scan. Any value less than `Long.MAX_VALUE` — including `0` — means "the index was consulted
and returned this count."

===== Custom Executor (Native Engine Integration, Optional)

For providers whose graph has a native traversal API, the most practical advanced customization is replacing
only the *executor* while keeping `DefaultGqlPlanner`. This lets a provider get free GQL parsing and
cardinality-guided join ordering from the default planner, then translate the resulting `GqlMatchPlan` into
native queries rather than running `DefaultGqlExecutor`'s DFS over `Graph.vertices()`.

[source,java]
----
public class AcmeGraph implements Graph {

private final GqlPlanner gqlPlanner = new DefaultGqlPlanner(this); // <1>
private final GqlExecutor gqlExecutor = new AcmeGqlExecutor(this); // <2>

@Override
public GraphTraversalSource traversal() {
return super.traversal()
.withStrategies(GqlDeclarativeMatchStrategy.create(gqlPlanner, gqlExecutor)); // <3>
}
// ...
}
----

<1> The default planner is reused: it handles GQL MATCH parsing, BFS join ordering, and seed selection using
the `countVerticesByLabel`, `countEdgesByLabel`, and `Graph.index()` overrides already in place.
<2> `AcmeGqlExecutor` receives a `GqlMatchPlan` and translates each `ExtensionStep` to a native traversal
or query rather than calling `Graph.vertices()`.
<3> Instances are held as fields and reused so the parse cache is shared across traversals.

The contract for a `GqlExecutor` implementation is: for each seed vertex matching the plan's seed label and
predicates, extend through each `ExtensionStep` in an order that keeps every step's anchor variable bound
before that step executes, and emit one `Element[]` per complete match. Each array's indices must match
`GqlMatchPlan.getVariables()`, with the seed variable at index 0. See `DefaultGqlExecutor` for a reference
implementation.

NOTE: Replacing only the *planner* is rarely worthwhile. Producing a valid `GqlMatchPlan` requires
constructing `ExtensionStep` objects, assigning variable index maps, and satisfying all other
invariants of that concrete class — effort comparable to writing a full executor anyway. The
`Graph` method hooks described above (`countVerticesByLabel`, `countEdgesByLabel`, `Graph.index()`)
are the right way to improve plan quality because `DefaultGqlPlanner` already consults them when
selecting the seed and ordering steps. A custom planner is only warranted when a provider needs a
fundamentally different join strategy (such as a cost-based optimizer backed by richer statistics
than label counts). At that level of investment, consider whether replacing both planner and
executor — or bypassing `gql-gremlin` entirely via the `DeclarativeMatchStep` pattern described
below — is the cleaner path.

===== Implementing a Different Declarative Language

The `GqlDeclarativeMatchStrategy` pattern is not TinkerGQL-specific. Providers that want to support a different
declarative language over the `match(String)` step, for example openCypher, SPARQL, or a proprietary query language,
can follow the same pattern:

. Implement `TraversalStrategy.ProviderOptimizationStrategy` and in its `apply()` method locate all
`DeclarativeMatchStep` instances in the traversal using `TraversalHelper.getStepsOfClass()`.
. Replace each `DeclarativeMatchStep` with a custom step that parses the query string and executes it
against the graph, emitting binding maps in the same form as `GqlMatchStep`.
. Register the strategy in the global cache from a static initializer, exactly as shown above for TinkerGQL.

`DeclarativeMatchStep` is the placeholder step inserted by the `match(String)` overload. The first registered
`ProviderOptimizationStrategy` to claim all `DeclarativeMatchStep` instances in a traversal wins; providers do not
need to interact with `gql-gremlin` at all if they supply their own end-to-end implementation.

[[validating-with-gremlin-test]]
=== Validating with Gremlin-Test

Expand Down
Loading
Loading