Supported Engines
The same models, queries and DSL run on each graph store. Switch the engine with DATABASE_TYPE.
| Engine | Type | Object manager | Transactions | Search |
|---|---|---|---|---|
| Neo4j 5 | NEO4J |
yes | full | vector, full-text |
| Memgraph | MEMGRAPH |
yes | full | vector, text |
| FalkorDB | FALKORDB |
yes | each statement commits by itself | vector, full-text |
| Oracle 23ai / 26ai | ORACLE |
yes | full | vector, Oracle Text |
| Amazon Neptune | NEPTUNE |
no: PersistenceManager only |
full | no |
| Postgres | POSTGRES |
no: plain SQL on PersistenceManager |
full | no |
The default. Port 7687. DATABASE_NAME selects the database, neo4j unless given.
Memgraph
Section titled “Memgraph”Bolt-compatible, so Drivine runs it on the Neo4j driver; only the Cypher differs. Port 7687, and empty credentials by
default. CascadeType.DELETE_ORPHAN isn’t available: Memgraph can’t use EXISTS inside WITH.
removedTargets: DELETE_UNREFERENCED works.
FalkorDB
Section titled “FalkorDB”An in-memory graph database on Redis. Port 6379; DATABASE_NAME is the graph, graph unless given.
Transactions. FalkorDB commits each statement by itself. A save is one statement, so it is still atomic, and so is a
saveAll of fragments that fits in one. @Transactional() methods run, each statement committing as it goes.
DATABASE_TRANSACTION_MODE says what to do about that:
WARN, the default: a rollback logs a warning, and so does the first batch of more than one statement, which runs statement by statement.STRICT: starting a transaction throws, so a@Transactional()method fails at its first statement, and a batch of more than one statement is refused withNonAtomicBatchExceptionbefore any of it runs.
Map keys. The FalkorDB client writes parameters into the statement text, and FalkorDB accepts no quoted map key. So
Drivine refuses a parameter map key that isn’t a plain identifier, such as a stored name with a $, rather than risk
injection.
Oracle
Section titled “Oracle”Oracle Database 23ai or later, through oracledb in thin mode. Oracle doesn’t speak Cypher: Drivine reads the graph
with SQL/PGQ (GRAPH_TABLE) and writes it as rows. Models, the DSL and the object manager are the same as on the graph
engines.
DATABASE_TYPE=ORACLEDATABASE_HOST=localhostDATABASE_PORT=1521 # the defaultDATABASE_NAME=FREEPDB1 # the service: requiredDATABASE_USER=appDATABASE_PASSWORD=secretDATABASE_DEFAULT_GRAPH_PATH=drivine # names the property graph and its tables; this is the defaultThe graph’s name names tables, so it is letters, digits and underscores, begins with a letter, and is at most 114
characters. In code, it is defaultGraphPath on the connection’s properties, or on the builder:
DatabaseRegistry.getInstance().builder().withProperties(properties).defaultGraphPath('tenant_a').register('TENANT_A').
Two databases over one Oracle service, each naming its own graph, keep apart: each manager reads, writes and indexes
its own graph’s tables alone.
How a graph is stored. Two tables and a property graph over them, made when a manager first speaks to the graph:
<graph>_node (nid, labels, props, stamp) and <graph>_edge (eid, src, dst, type, props), with labels and props
as JSON. Labels, property names and relationship types are data, so @NodeLabels, @PropertyBag and edges work as
they do elsewhere. A node’s stamp is the stamp column. A third table, <graph>_schema, records the indexes and
constraints Drivine made. The layout is drivine4j’s, so the JVM and Node read one store.
Notes:
- A statement you write yourself is SQL. Parameters are named
$name, as in Cypher. A statement that writes and returns rows is a PL/SQL block that hands them back withDBMS_SQL.RETURN_RESULT. - A predicate you write yourself, for a load or a delete, is SQL over the root’s row: its
props,labelsandstamp. - A save is one block and checks the stamp, as on the graph engines. A save turned away by a deadlock (
ORA-00060) or a serialization failure (ORA-08177) is run again. Another writer that changes a node in SQL should give it a new stamp: the first sixteen characters ofstampfor its properties or labels, the last sixteen for its relationships. - Index the id of each class you save:
uniquenessConstraint('Person', 'id')makes the unique index a node is found by. Without one, a save reads the table. - Oracle reads an empty string as null, so a comparison with
''in the DSL is written to find a property stored empty. matchestakes a regular expression in Oracle’s POSIX dialect, matched against the whole value.- A vector index is a table of embeddings beside the nodes, kept in step by a trigger, with an Oracle vector index over
it by neighbour partitions;
loadNearestis approximate through it. With no index made, it measures every node of the label and is exact. - A full-text index is a table of the indexed text beside the nodes, with an Oracle Text index over it.
loadMatchingreads words, a"phrase",+must,-notand aprefix*. The index holds what has been committed, so a search doesn’t see its own transaction’s writes. Oracle Text is in the regular Free image, not the slim one; without it, or with no index made, a search counts the words in the text of every node of the label.
Amazon Neptune
Section titled “Amazon Neptune”Neptune connects over Bolt, on PersistenceManager: write the statements yourself. Neptune has no date() and no list
properties, so store dates as text and annotate lists with @JsonPacked.
Postgres
Section titled “Postgres”Plain SQL on PersistenceManager, with cursors and transactions as on the graph engines.

