Skip to content

TINKERPOP-3247 Convert request bindings to gremlin-lang string format#3402

Open
kenhuuu wants to merge 2 commits intomasterfrom
stringify-params
Open

TINKERPOP-3247 Convert request bindings to gremlin-lang string format#3402
kenhuuu wants to merge 2 commits intomasterfrom
stringify-params

Conversation

@kenhuuu
Copy link
Copy Markdown
Contributor

@kenhuuu kenhuuu commented Apr 27, 2026

https://issues.apache.org/jira/browse/TINKERPOP-3247

Moving parameters from binary-serialized maps to string representations
makes the request side pure text, decoupling Gremlin language evolution
from GraphBinary versioning. New types can be introduced in minor/patch
versions without touching GraphBinary, eliminating the need for a major
version bump across the ecosystem for every new request-side type.

The asParameter() fallback is replaced with an unsupportedType flag that
records the class name and falls back to toString(). A flag is used
rather than throwing because embedded Traversals build GremlinLang as a
side effect but never send it, so unknown types must not break
execution. All other GLVs throw immediately since they have no embedded
mode and the early throw gives better errors.

Client APIs accept both map and string bindings (but not both at the
same time) because users who use the Client directly with raw Gremlin
strings shouldn't need to hand-craft gremlin-lang map literals. Mixing
both throws immediately to prevent silent loss where one set of bindings
would be discarded.

Edge and VertexProperty tests that relied on the old asParameter
fallback were removed because they aren't supported in gremlin-lang.

@kenhuuu kenhuuu force-pushed the char-duration-binary branch from 316e283 to 8bed359 Compare May 4, 2026 16:26
Base automatically changed from char-duration-binary to master May 4, 2026 19:10
kenhuuu added 2 commits May 4, 2026 16:06
The visitor treated all keyword map keys as their text representation,
so a null key in [null:"value"] was parsed as the String "null" instead
of Java null. This broke round-tripping maps with null keys through
GremlinLang serialization and ANTLR parsing.
Moving parameters from binary-serialized maps to string representations
makes the request side pure text, decoupling Gremlin language evolution
from GraphBinary versioning. New types can be introduced in minor/patch
versions without touching GraphBinary, eliminating the need for a major
version bump across the ecosystem for every new request-side type.

The asParameter() fallback is replaced with an unsupportedType flag that
records the class name and falls back to toString(). A flag is used
rather than throwing because embedded Traversals build GremlinLang as a
side effect but never send it, so unknown types must not break
execution. All other GLVs throw immediately since they have no embedded
mode and the early throw gives better errors.

Client APIs accept both map and string bindings (but not both at the
same time) because users who use the Client directly with raw Gremlin
strings shouldn't need to hand-craft gremlin-lang map literals. Mixing
both throws immediately to prevent silent loss where one set of bindings
would be discarded.

Edge and VertexProperty tests that relied on the old asParameter
fallback were removed because they aren't supported in gremlin-lang.
@kenhuuu kenhuuu force-pushed the stringify-params branch from 6b3e2fc to c7c2154 Compare May 4, 2026 23:21
Comment on lines +108 to +111
final GremlinParser.GenericMapLiteralContext mapCtx = parser.genericMapLiteral();

final ParameterMapVisitor visitor = new ParameterMapVisitor(new GremlinAntlrToJava());
final Map<Object, Object> rawMap = (Map<Object, Object>) visitor.visitGenericMapLiteral(mapCtx);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need some error handling here? Am I right in assuming mapCtx will be null if the parameterMapString is not actually a gremlin-lang map? How would such an error propagate if the user provides a bad parameter string?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling is done via the GremlinErrorListener that throws a GremlinParserException if its not a map so there won't be a null. See the shouldReturnUserFriendlyErrorMessageForMalformedParameterStrings test for more info.

if (value instanceof Traversal) {
throw new GremlinParserException("Traversals are not allowed as parameter values");
}
if (value instanceof Map) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also recurse through map keys?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just extra checking so neither technically needs to be exist, but yes, I'll add it for completeness.

throw new GremlinParserException("Parameter map nesting depth exceeds maximum of " + maxNestingDepth);
}
try {
return super.visitGenericCollectionLiteral(ctx);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I right in understanding that this will result in all objects contained in the collection to be parsed by GenericLiteralVisitor.visitGenericLiteral() instead of ParameterMapVisitor.visitGenericLiteral()? Do we need more careful handling of collections and any composite types to ensure that they are recursively parsed through this class and not handed off to the unguarded GenericLiteralVisitor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks for pointing this out, something is definitely not right here. I modified the test in ParameterMapVisitorTest slightly to assert the exception message and it confirms that it isn't dispatching to the right visitor. I'll have to think about how to make a simple change to fix this.


@Test(expected = GremlinParserException.class)
public void shouldRejectTerminatedTraversalInValue() {
GremlinQueryParser.parseParameters("[\"x\":g.V().drop().iterate()]");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be worth a few more cases which try to bury a terminatedTraversal deeper inside other collections/composite types.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, will add.

return this;
}

addBindingsString(bindingsString: string): Builder {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: addBindings acts as a merge, but addBindingsString is a replacement. I think that's ok to an extent, (we definitely don't want to try merging strings), but might be worth a quick jsdoc. Perhaps it would be also reasonable to only allow addBindingsString to be called once.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I think that should get commented so I'll update that. Regarding allowing it to only be called once, my opinion is that its not necessary for now. The documentation will say "last one wins".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants