Skip to content

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.

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.

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 with NonAtomicBatchException before 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 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.

Terminal window
DATABASE_TYPE=ORACLE
DATABASE_HOST=localhost
DATABASE_PORT=1521 # the default
DATABASE_NAME=FREEPDB1 # the service: required
DATABASE_USER=app
DATABASE_PASSWORD=secret
DATABASE_DEFAULT_GRAPH_PATH=drivine # names the property graph and its tables; this is the default

The 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 with DBMS_SQL.RETURN_RESULT.
  • A predicate you write yourself, for a load or a delete, is SQL over the root’s row: its props, labels and stamp.
  • 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 of stamp for 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.
  • matches takes 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; loadNearest is 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. loadMatching reads words, a "phrase", +must, -not and a prefix*. 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.

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.

Plain SQL on PersistenceManager, with cursors and transactions as on the graph engines.