Gremlin

ArcadeDB supports Gremlin 3.7.x as query engine and in the Gremlin Server. You can execute a gremlin query from pretty much everywhere.

If you’re using ArcadeDB as embedded, please add the dependency to the arcadedb-gremlin library. If you’re using Maven include this dependency in your pom.xml file.

<dependency>
    <groupId>com.arcadedb</groupId>
    <artifactId>arcadedb-gremlin</artifactId>
    <version>26.10.2</version>
</dependency>

CRITICAL SECURITY NOTICE:

ArcadeDB uses the secure Java Gremlin engine by default.

DO NOT use the Groovy engine (arcadedb.gremlin.engine=groovy) in production or with untrusted users. The Groovy engine has a CRITICAL Remote Code Execution (RCE) vulnerability that allows authenticated users to execute arbitrary operating system commands.

See Gremlin Security for complete details about this vulnerability.

Gremlin from Java API

In order to execute a Gremlin query, you need to include the relevant jars, i.e. the apache-tinkerpop-gremlin-server libraries, plus gremlin-groovy, plus opencypher-util-9.0, in your class path. To execute a Gremlin query, use "gremlin" as first parameter in the query method. Example:

ResultSet result = database.query("gremlin", "g.V().has('name','Michelle').has('lastName','Besso').out('IsFriendOf')");

If you’re application is mostly based on Gremlin, the best way is to use the ArcadeGraph class as an entrypoint. Example:

try (final ArcadeGraph graph = ArcadeGraph.open("./databases/graph")) {
  Vertex michelleFriend = graph.traversal().V().has("name","Michelle").has("lastName","Besso").out("IsFriendOf").next();
}

You can also work with a remote ArcadeDB Server:

try( RemoteDatabase database = new RemoteDatabase("127.0.0.1", 2480, "graph", "root", "playwithdata") ){
  try (final ArcadeGraph graph = ArcadeGraph.open(database)) {
    Vertex michelleFriend = graph.traversal().V().has("name","Michelle").has("lastName","Besso").out("IsFriendOf").next();
  }
}

Since a RemoteDatabase instance cannot be shared between threads, if your application is multi-threads, you can use the ArcadeGraphFactory to acquire ArcadeGraph instances from a pool. Example:

try (ArcadeGraphFactory pool = ArcadeGraphFactory.withRemote("127.0.0.1", 2480, "mydb", "root", "playwithdata")) {
  try( ArcadeGraph graph = pool.get() ){
    // DO SOMETHING WITH ARCADE GRAPH
  }
}
Remember to call ArcadeGraph.close() to release the ArcadeGraph instance back into the pool. ArcadeGraph implements Closable, so you can just use it in a try block like in the example above.

Transactions and closing a graph

Starting from version 26.9.1, closing an ArcadeGraph rolls back any transaction still open, which is the TinkerPop default. Changes that never reached commit() are not saved just because the graph was closed:

try (final ArcadeGraph graph = ArcadeGraph.open("./databases/graph")) {
  graph.tx().begin();
  graph.addVertex("Person");
  graph.tx().commit(); // <-- WITHOUT THIS, THE VERTEX IS NOT SAVED
}

If you want the graph to commit when it closes, ask for it explicitly:

graph.tx().onClose(Transaction.CLOSE_BEHAVIOR.COMMIT);
Before 26.9.1 an open transaction was committed on close. If your application relied on that, add the onClose() call above.

An ArcadeGraph borrowed from an ArcadeGraphFactory pool is always returned clean: any transaction left open is rolled back before the instance goes back into the pool, so the next caller never inherits your uncommitted changes. Do not keep using an instance after calling close(), and note that drop() is not allowed on a pooled instance, because the database is shared with the rest of the pool.

Vertex property identifiers

Starting from version 26.9.1, VertexProperty.id() returns a String in the form #<bucket>:<offset>-<propertyName>, consistent with vertex and edge identifiers. It previously returned a Long computed from hash codes, which could come out the same for two different properties of the same vertex, making them compare as equal and disappear from results such as properties().dedup().

If your application casts or parses a vertex property id as a number, update it to handle a String.

Gremlin through Postgres Driver

You can execute a Gremlin query against ArcadeDB server by using the Postgres driver and prefixing the query with {gremlin}. Example:

"{gremlin} g.V().has('name','Michelle').has('lastName','Besso').out('IsFriendOf')"

ArcadeDB server will execute the query g.V().has('name','Michelle').has('lastName','Besso').out('IsFriendOf') using the Gremlin query language.

Gremlin through HTTP/JSON

You can execute a Gremlin query against ArcadeDB server by using HTTP/JSON API. Example of executing an idempotent query with HTTP GET command:

curl "http://localhost:2480/query/graph/gremlin/g.V().has('name','Michelle').has('lastName','Besso').out('IsFriendOf')"

Example of executing a non-idempotent query (updates the database):

curl -X POST "http://localhost:2480/command/graph" -d "{'language': 'gremlin', 'command': 'g.V().has(\"name\",\"Michelle\").has(\"lastName\",\"Besso\").out(\"IsFriendOf\")'}"

Use the Gremlin Server

Apache TinkerPop Gremlin provides a standalone server to allow remote access with a Gremlin client. In order to use the Gremlin Server with ArcadeDB, you have to enable it from ArcadeDB’s server plugin system:

~/arcadedb $ bin/server.sh -Darcadedb.server.plugins="GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin"

If you’re using MS Windows OS, replace bin/server.sh with bin\server.bat.

At startup, the Gremlin Server plugin looks for the file config/gremlin-server.yaml under ArcadeDB path. If the file is present, the Gremlin Server will be configured with the settings contained in the YAML file, otherwise the default configuration will be used.

You can also override single configuration settings by using ArcadeDB’s settings and prefixing the configuration key with gremlin.. All the configuration settings with such a prefix will be passed to the Gremlin Server plugin.

Multi-Database Support

ArcadeDB’s Gremlin Server includes ArcadeGraphManager, a custom implementation of TinkerPop’s GraphManager interface that provides dynamic, on-demand registration of databases as Gremlin graphs.

Key Features:

  • Automatic Registration: When a Gremlin client requests a graph/traversal source by database name, ArcadeGraphManager automatically registers it if the database exists in ArcadeDB Server

  • Multi-Database Access: Access any database on the server through Gremlin without static configuration

  • The g Alias: The standard g alias automatically maps to the default database ("graph" if it exists, otherwise the first available database)

How It Works:

Instead of static graph configuration in YAML files, databases are registered dynamically when first accessed. Each database name becomes a valid traversal source alias:

// Connect to a specific database using its name as the alias
var cluster = Cluster.build()
    .port(8182)
    .addContactPoint("localhost")
    .credentials("root", "password")
    .create();

// Access database "mydb" - registered automatically on first access
var g = traversal().withRemote(DriverRemoteConnection.using(cluster, "mydb"));

// Or use the standard "g" alias for the default database
var gDefault = traversal().withRemote(DriverRemoteConnection.using(cluster, "g"));

Script-Based Queries:

For script-based Gremlin queries (e.g., via client.submit()), the g variable is automatically bound to the default database:

Client client = cluster.connect();
// The "g" variable is automatically available
ResultSet results = client.submit("g.V().hasLabel('Person').count()");

Benefits:

  • No need to manually configure graphs in gremlin-server.yaml or gremlin-server.groovy

  • Supports ArcadeDB’s multi-tenant architecture where a single server hosts multiple databases

  • Each database is isolated and accessed independently through its name

  • Reduces configuration complexity for deployments with many databases

Legacy static configuration via gremlin-server.yaml graphs section is no longer required. All databases are available automatically

Migrating from Static Graph Configuration

If you configured graphs statically in gremlin-server.yaml, here are the key changes to be aware of:

What Changed:

  1. Graph Registration: Graphs are now registered dynamically by ArcadeGraphManager instead of statically in configuration files

  2. Configuration Files: The graphs: section in gremlin-server.yaml is no longer required

  3. The graph Binding: The internal graph variable is now managed by ArcadeGraphManager to properly support TinkerPop sessions and transactions

Backward Compatibility:

  • Existing Gremlin queries continue to work without modification

  • The g alias still works and maps to the default database

  • Script-based queries using g are fully supported

  • Bytecode-based queries work with any database name as the alias

Configuration Migration:

If you had custom gremlin-server.yaml configuration:

# OLD configuration (no longer needed)
graphs:
  graph: conf/arcadedb.properties

This static configuration is now optional. ArcadeGraphManager will automatically register any database when it is first accessed. You can safely remove the graphs: section.

Custom gremlin-server.groovy:

If you customized gremlin-server.groovy, ensure you are NOT overwriting the graph binding:

// CORRECT: Only define the traversal source 'g'
globals << [g : traversal().withEmbedded(graph)]

// WRONG: Do NOT overwrite 'graph' - this breaks session support
// globals << [graph : traversal().withEmbedded(graph)]  // DON'T DO THIS

The graph variable must remain a Graph object (not a GraphTraversalSource) for TinkerPop’s SessionOpProcessor to work correctly with transactions.

Accessing Multiple Databases:

To access a specific database, use the database name as the traversal source alias:

// Use the database name directly
var g = traversal().withRemote(DriverRemoteConnection.using(cluster, "customers"));
var g2 = traversal().withRemote(DriverRemoteConnection.using(cluster, "products"));

Transactions via Remote Driver:

Gremlin transactions now work correctly via the remote driver:

GraphTraversalSource g = traversal().withRemote(
    DriverRemoteConnection.using(cluster, "mydb"));

Transaction tx = g.tx();
GraphTraversalSource gtx = tx.begin();
try {
    gtx.addV("Person").property("name", "Alice").iterate();
    tx.commit();
} catch (Exception e) {
    tx.rollback();
}

Docker Example:

Start the Gremlin Server with the OpenBeer dataset imported as a database named openbeer:

docker run -d --name arcadeDB -p 2480:2480 -p 8182:8182 \
       --env ARCADEDB_SETTINGS="-Darcadedb.server.rootPassword=playwithdata \
          -Darcadedb.server.defaultDatabases=openbeer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} \
          -Darcadedb.server.plugins=GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin " \
          arcadedata/arcadedb:latest
When running ArcadeDB with Docker, expose port 8182 for Gremlin Server and use -e to pass JVM settings.
Gremlin Server listens on 8182 unless you set gremlin.port (for example -Dgremlin.port=8183). The remote ArcadeGraph client reads the port from the server; if the port is remapped outside ArcadeDB, set arcadedb.gremlin.client.port on the client.

Connecting to the Gremlin Server:

var cluster = Cluster.build()
    .port(8182)
    .addContactPoint("localhost")
    .credentials("root", "password")
    .create();

// Connect using the database name as the alias
var g = traversal().withRemote(DriverRemoteConnection.using(cluster, "openbeer"));

// Or use "g" for the default database
var gDefault = traversal().withRemote(DriverRemoteConnection.using(cluster, "g"));

Listing and terminating running traversals

Since v26.10.2 a Gremlin traversal can be stopped while it runs, whichever way it was sent. The HTTP list queries server command, Cypher SHOW TRANSACTIONS and the Studio Running Queries tab list it with language gremlin; a request sent to the Gremlin Server is listed with protocol gremlin, one sent over HTTP or another protocol with that protocol. terminate query or TERMINATE TRANSACTIONS stops it: the traversal checks for the request inside its adjacency steps (out(), in(), both(), outE(), bothE(), …​) and while its results are iterated, so even a traversal that expands a large graph stops promptly, and the client receives an error saying that it was terminated.

Known Limitations with the Gremlin Implementation

  • ArcadeDB automatically handles the conversion between compatible types, such as strings and numbers when possible. Gremlin does not. So if you define a schema with the ArcadeDB API and then you use Gremlin for a traversal, ensure you’re using the same type you defined in the schema. For example, if you define a property "id" to be a string, and then you’re executing traversal by using integers for the ids, the result could be unpredictable.

  • ArcadeDB’s Gremlin implementation always tries to optimize the Gremlin traversal by using ArcadeDB’s internal query. While this is easy with simple traversal using .has() and .hasLabel(), it is unable to optimize more complex traversal with select() and where(). Instead of executing an optimized query, it could result in a full scan of the type, leaving to Gremlin the filtering. While the result of the traversal is still correct, the performance would be heavily impacted. Please consider using ArcadeDB’s SQL or Native Select for the best performance with complex traversals.

  • A hasLabel('X') that directly follows g.V() or g.E() is answered from the type X polymorphically, like SQL’s SELECT FROM X: it matches X and every type that extends it. This holds whichever way ArcadeDB executes it (a type scan, an index lookup on a property of X or of one of its super types, or count()), so g.V().hasLabel('X').count() always equals the number of elements g.V().hasLabel('X') returns. A hasLabel() further down a traversal, for example after out(), compares the element’s label exactly, as TinkerPop defines it.

  • (Since v26.10.2) A g.V().hasLabel('X').has('k', v) answered from an index keeps working while a CREATE INDEX or DROP INDEX runs on the same type: the traversal checks its index again when it opens the scan and, if that index is gone, uses the index now on the property or scans the type. Before, such a traversal could fail or return a wrong count.

  • A hasLabel('bucket:<name>') that directly follows g.V() or g.E() restricts the traversal to that one bucket, and count() counts that bucket.

  • (Since v26.11.1) A hasLabel() naming nothing, whether a type or a bucket that does not exist, a bucket no type owns, or a type of the other element kind, matches no element, with or without a following count(): g.V().hasLabel('bucket:Missing').count() answers 0, like g.V().hasLabel('Missing').count(). Before, the bucket form with count() failed with SchemaException: Bucket with name 'Missing' was not found (issue #10763). Writing to such a bucket, addV('bucket:Missing') or addE('bucket:Missing'), is refused with IllegalArgumentException: Bucket 'Missing' does not exist (or does not belong to any type), since a write cannot create a bucket.

  • (Since v26.11.1) A query sent as text that ends with an eager terminal step answers the step’s value as one result, null included: g.V().coalesce(values('x'), constant(null)).next() answers one result whose result is null, as the same traversal without next() does. Before, a null value failed the query with a NullPointerException (issue #10770). tryNext() answers the value it holds, or no result when the traversal is empty.

  • A query sent as text (graph.gremlin(…​), POST /api/v1/command) returns a map result, such as elementMap() or groupCount(), as one result whose properties are the map’s keys. The shape is always a flat map, whatever the data. The id and label tokens of elementMap() and valueMap(true) are named @rid and @type, so a property called id or label never overwrites them. Keys that print the same, for example the keys 1 (Integer) and 1L of a groupCount(), are told apart by their type: 1:Integer and 1:Long. A null key is named null.

  • The edges of a LIGHTWEIGHT edge type have no record, so they have no usable RID. Gremlin still lists, counts and deletes them: g.E().hasLabel('X'), outE(), inE(), bothE(), out('X').count() and drop() all work. Listing a lightweight type by g.E() walks the vertices, so it costs more than the scan of a regular edge type.

  • (Since v26.11.1) An element dropped earlier in a traversal, for example by sideEffect(drop()), behaves as in TinkerGraph: reading it finds no properties, edges or adjacent vertices (valueMap() answers {}, inV() from a dropped edge answers nothing), and writing to it fails with IllegalStateException: Vertex with id …​ was removed., so g.V().has('uid', 1).sideEffect(drop()).addE('r').to(…​) creates no edge. Dropping an element that is already gone is still a no-op. Before, the dropped element kept answering its old properties, property() on it was silently ignored, and addE() from it committed an edge whose source vertex no longer existed (issue #10196). A rolled back transaction makes the element usable again.

For more information about Gremlin:

If you’re using Gremlin with ArcadeDB, check out gdotv graph tool. It is compatible with ArcadeDB and provides a powerful visual debugger, advanced graph analytics, and much more.

debugging queries short