Skip to content

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.

Postgres is the quickest to try: docker run -d -p 5432:5432 -e POSTGRES_USER=app -e POSTGRES_PASSWORD=secret postgres, then:

Terminal window
DATABASE_TYPE=POSTGRES
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=app
DATABASE_USER=app
DATABASE_PASSWORD=secret

For 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.

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 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.

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.