Cypher on a relational database
Write a Cypher statement once, and run it on whichever store you connect to. Neo4j, Memgraph, FalkorDB and Neptune run it as written. On Postgres and Oracle, the SQL stores Drivine for TypeScript supports, Drivine compiles it to SQL over the graph’s two tables, one of nodes and one of relationships, and gives back the rows a graph engine gives. SQLite is drivine4j’s for now.
Connect
Section titled “Connect”Postgres is the quickest to try: docker run -d -p 5432:5432 -e POSTGRES_USER=app -e POSTGRES_PASSWORD=secret postgres,
then:
DATABASE_TYPE=POSTGRESDATABASE_HOST=localhostDATABASE_PORT=5432DATABASE_NAME=appDATABASE_USER=appDATABASE_PASSWORD=secretFor Oracle 23ai or later, set DATABASE_TYPE=ORACLE, DATABASE_PORT=1521 and DATABASE_NAME to the service, such as
FREEPDB1. On either, DATABASE_DEFAULT_GRAPH_PATH names the graph’s tables, drivine unless given. The
Postgres and Oracle sections have the rest.
Write and read
Section titled “Write and read”Give a persistence manager the statement through cypherStatement. A statement that doesn’t say it is Cypher is SQL
on these stores, as it always has been.
import { cypherStatement, DatabaseRegistry, PersistenceManagerFactory, PostgresGraph, QuerySpecification} from '@liberation-data/drivine';
DatabaseRegistry.buildOrResolveFromEnv();const pm = new PersistenceManagerFactory().get();
// Postgres only, and only until an object manager has worked on the graph: make its tables. Oracle makes them itself.await pm.execute(new QuerySpecification(new PostgresGraph(process.env.DATABASE_DEFAULT_GRAPH_PATH).ensuring));
await pm.execute( new QuerySpecification( cypherStatement(` MERGE (a:Person {name: $from}) MERGE (b:Person {name: $to}) MERGE (a)-[:KNOWS {since: $since}]->(b)`) ).bind({ from: 'Keanu', to: 'Carrie-Anne', since: 1999 }));
const friends = await pm.query<{ name: string; since: number }>( new QuerySpecification<{ name: string; since: number }>( cypherStatement(` MATCH (p:Person {name: $name})-[k:KNOWS]->(f:Person) RETURN {name: f.name, since: k.since} AS friend ORDER BY f.name`) ).bind({ name: 'Keanu' }));// [{ name: 'Carrie-Anne', since: 1999 }]A statement that writes runs as one statement, applied whole or not at all. Each row is one value, so return one map or a scalar, as on a graph engine.
What compiles
Section titled “What compiles”What each statement gives on each store, compiled to SQL or not, is the shared
Cypher spec. This repository runs version
v0.6.4 of it on Neo4j, Postgres and Oracle, and
drivine-python and drivine4j run the same spec. Reads and writes compile, with variable-length relationships and
quantified path patterns among them. Cypher the compiler doesn’t take is refused by name before the store is sent
anything, e.g. A path variable in CREATE or MERGE is Cypher, and is not compiled to SQL yet.
Differences you’ll meet
Section titled “Differences you’ll meet”The results match a graph engine’s, with a few differences. Cypher on Oracle and Postgres documents them; the ones you’re likeliest to meet:
- A node in a row is its properties.
- A temporal value is its ISO 8601 text.
- Postgres works float arithmetic out in exact decimal, so a result can differ in its last digits.
- Text sorts by the database’s collation on Postgres (Postgres), and Oracle reads an empty string as null (Oracle).
You may not need Cypher at all: the object manager loads and saves your classes on every store without it.

