Skip to content

Supported Drivers

Runic's OGM is database-agnostic. Every backend is hidden behind the GraphDriver / AsyncGraphDriver Protocol so the rest of the stack (Session, Repository, QueryBuilder) never talks to the database directly.

Use create_driver() as the recommended entry-point, or instantiate a driver class directly for advanced cases.

python
from runic.ogm import create_driver

# FalkorDB
driver = create_driver("falkordb", host="localhost", port=6379, graph="myapp")
# ArcadeDB (via Bolt)
driver = create_driver(
    "arcadedb", host="localhost", port=7687,
    database="mydb", username="root", password="secret",
)
# Neo4j
driver = create_driver(
    "neo4j", host="localhost", port=7687,
    database="neo4j", username="neo4j", password="secret",
)
# Memgraph
driver = create_driver(
    "memgraph", host="localhost", port=7687,
    database="memgraph", username="", password="",
)
# Apache AGE (PostgreSQL graph extension)
driver = create_driver(
    "age", host="localhost", port=5432,
    database="postgres", graph="my_graph",
    username="postgres", password="secret",
)

Feature matrix

FeatureFalkorDBArcadeDBNeo4jMemgraphApache AGE
Protocol / clientRedis (falkordb)Bolt (neo4j)Bolt (neo4j)Bolt (neo4j)SQL (psycopg3)
Sync driver
Async driver
Vector KNN queries✓ — native vecf32✓ — CALL vector.neighbors✓ — CALL db.index.vector.queryNodes✓ — CALL vector_search.search✗ — use pgvector
Fulltext search✓ — db.idx.fulltext.queryNodes✗ — not supported by ArcadeDB OGM driver✓ — CALL db.index.fulltext.queryNodes✓ — CALL text_search.search_all✗ — use PostgreSQL FTS
String interning (intern())
TypeConverter Cypher wrappers✓ — vecf32(), toPoint()
TLS / encrypted connections✗ — Redis, no TLS✗ — bolt:// only✓ — bolt+s://✓ — bolt+s://✓ — via PostgreSQL SSL
Multiple graphs per connection✓ — select_graph()✓ — one graph per driver
ACID transactions✗ — each query is atomic✓ — begin / commit / rollback✓ — begin / commit / rollback✓ — begin / commit / rollback✓ — psycopg3 implicit BEGIN
Migrate adapter (create_adapter)✓ — FalkorDBAdapter✓ — ArcadeDBAdapter✓ — Neo4jAdapter✓ — MemgraphAdapter✓ — AGEAdapter
IndexManager DDL✓ — range / fulltext / vector / unique✓ — range / fulltext / unique (vector via HTTP API)✓ — range / fulltext / vector / unique (IF NOT EXISTS)✓ — range / text / vector / unique✗ — log.warning only (PostgreSQL-level DDL required)
Multi-label nodes✗ — emulated via _labels property
GeoLocation in-place update✓ — SET n.geo = toPoint($v)✗ — stored as {latitude, longitude} map✓ — SET n.geo = point($v)✓ — SET n.geo = point($v)✗ — agtype point not supported via psycopg
Undirected MERGE (direction="BOTH")✗ — falls back to OUTGOING automatically
Required Python packagefalkordbneo4jneo4jneo4jpsycopg[binary]

FalkorDB

Supported

  • Sync (FalkorDBDriver) and async (AsyncFalkorDBDriver) execution.
  • Full fulltext search via CALL db.idx.fulltext.queryNodes().
  • Vector KNN using vecf32(alias.field) <-> vecf32($vec) (FalkorDB native similarity syntax).
  • Field() options interned=True (wraps the value in intern() on write) and custom TypeConverter Cypher functions (e.g. vecf32, toPoint).
  • Multiple named graphs on the same server via graph= parameter.

Not supported / limitations

  • No TLS — FalkorDB communicates over Redis, which this driver does not encrypt.
  • AsyncFalkorDBDriver requires an async FalkorDB graph handle; there is no built-in create_async_falkordb_driver factory — you must pass the handle yourself.
python
from runic.ogm import create_driver, Session

driver = create_driver("falkordb", host="localhost", port=6379, graph="myapp")
with Session(driver) as session:
    ...

# Async — build the handle manually
from falkordb import FalkorDB
from runic.ogm.driver.falkordb import AsyncFalkorDBDriver

async_handle = FalkorDB(host="localhost", port=6379).select_graph("myapp")
async_driver = AsyncFalkorDBDriver(async_handle)

ArcadeDB

ArcadeDB is accessed over the Bolt protocol using the neo4j Python driver (encrypted=False).

Supported

  • Sync execution via BoltDriver.
  • Vector KNN via CALL vector.neighbors('<type>[<field>]', $vec, $k) YIELD node, distance.
  • Standard MATCH/MERGE/DELETE Cypher queries.

Not supported / limitations

  • No async driver.
  • No fulltext search via the OGM query builder. The migrate adapter issues CREATE FULLTEXT INDEX ON \` (prop)` DDL where supported; ArcadeDB may accept or reject it depending on configuration.
  • No TypeConverter Cypher wrappers. Raw Python values stored as-is.
  • Plaintext Bolt only. create_arcadedb_driver forces bolt://.
  • No vector index DDL. create_vector_index() logs a warning and directs you to the ArcadeDB HTTP management API.
  • No GeoLocation in-place update. SET n.geo = point($v) is not supported via ArcadeDB's Bolt interface. GeoLocation values are stored and returned as a plain map ({"latitude": …, "longitude": …}). Updating the geo field requires re-saving the whole node. Tests marked requires_geo_update are automatically skipped for this backend (ArcadeDBDialect.supports_geo_update = False).
python
from runic.ogm import create_driver, Session

driver = create_driver(
    "arcadedb",
    host="localhost", port=7687,
    database="mydb", username="root", password="playwithdata",
)
with Session(driver) as session:
    ...

Neo4j

Neo4j is accessed over the Bolt protocol using the neo4j Python driver.

Supported

  • Sync execution via BoltDriver.

  • Fulltext search via CALL db.index.fulltext.queryNodes(). The query uses an index named after the label (e.g. Person).

  • Vector KNN via CALL db.index.vector.queryNodes(). A vector index named {label}_{prop} (e.g. Article_embedding) must exist.

  • TLS via bolt+s:// (set encrypted=True, the default).

  • Migrate adapter (create_adapter("neo4j", ...)) — issues full DDL for all index/constraint types via IF NOT EXISTS for idempotency.

  • IndexManager — pass a Neo4jAdapter to IndexManager to create indexes from your entity definitions:

    python
    from runic.migrate.adapters import create_adapter
    from runic.migrate import IndexManager
    
    adapter = create_adapter("neo4j", database="neo4j", password="secret")
    manager = IndexManager(adapter)
    manager.create_indexes(Person)   # issues CREATE INDEX / CONSTRAINT DDL

Index naming convention (Neo4j 5.x)

text
fulltext:  CREATE FULLTEXT INDEX {label}  IF NOT EXISTS FOR (n:{label}) ON EACH [n.prop1, n.prop2]
range:     CREATE INDEX {label}_{prop}    IF NOT EXISTS FOR (n:{label}) ON (n.{prop})
vector:    CREATE VECTOR INDEX {label}_{prop}  IF NOT EXISTS FOR (n:{label}) ON (n.{prop})
unique:    CREATE CONSTRAINT {label}_{prop}_unique  IF NOT EXISTS FOR (n:{label}) REQUIRE n.{prop} IS UNIQUE

Not supported / limitations

  • No async driver.
  • No TypeConverter Cypher wrappers.
  • Vector index dimension is not stored in Field() metadata; pass dimension when calling create_vector_index() directly, or pre-create vector indexes via Cypher DDL.
python
from runic.ogm import create_driver, Session

driver = create_driver(
    "neo4j",
    host="localhost", port=7687,
    database="neo4j", username="neo4j", password="secret",
    encrypted=True,
)
with Session(driver) as session:
    ...

Memgraph

Memgraph is accessed over the Bolt protocol using the neo4j Python driver, with Memgraph-specific text_search and vector_search MAGE module procedures.

Supported

  • Sync execution via BoltDriver.

  • Fulltext search via CALL text_search.search_all(). Uses a whole-label text index named after the label (CREATE TEXT INDEX {label} ON :{label}).

  • Vector KNN via CALL vector_search.search(). A vector index named {label}_{prop} must exist.

  • TLS available (set encrypted=True).

  • Migrate adapter (create_adapter("memgraph", ...)) — issues DDL for range, text, vector, and unique constraint creation.

  • IndexManager — pass a MemgraphAdapter to IndexManager:

    python
    from runic.migrate.adapters import create_adapter
    from runic.migrate import IndexManager
    
    adapter = create_adapter("memgraph", database="memgraph")
    manager = IndexManager(adapter)
    manager.create_indexes(Post)    # issues CREATE INDEX / CONSTRAINT DDL

Index naming convention (Memgraph)

text
text index: CREATE TEXT INDEX {label} ON :{label}         (whole-label; one per label)
range:      CREATE INDEX ON :{label}({prop})               (idempotent)
vector:     CREATE VECTOR INDEX {label}_{prop} ON :{label}({prop}) WITH CONFIG {...}
unique:     CREATE CONSTRAINT ON (n:{label}) ASSERT n.{prop} IS UNIQUE

INFO

Memgraph text indexes cover the entire label — a single text index per label is created regardless of how many index_type="FULLTEXT" fields are declared. Full-text queries search all string properties on the node. Requires the MAGE text_search module.

Not supported / limitations

  • No async driver.
  • No TypeConverter Cypher wrappers.
  • Vector index dimension is not stored in Field() metadata; pass dimension when calling create_vector_index() directly, or pre-create vector indexes via Cypher DDL.
python
from runic.ogm import create_driver, Session

driver = create_driver(
    "memgraph",
    host="localhost", port=7687,
    database="memgraph", username="", password="",
)
with Session(driver) as session:
    ...

Apache AGE

Apache AGE is a PostgreSQL extension that adds openCypher graph query support to an existing PostgreSQL database. Cypher queries are executed via the cypher() SQL function wrapped in a SELECT statement:

text
SELECT * FROM cypher('graph_name', $$ CYPHER $$ [, params::agtype])
    AS (col0 agtype, ...);

The runic driver uses psycopg3 (psycopg[binary]) for the PostgreSQL connection and handles the cypher() wrapping automatically. Parameters are serialised as an agtype JSON map and passed as the third argument to cypher(), making them available inside the Cypher query as $param_name — identical to how runic's QueryBuilder emits $p0, $p1, etc.

Supported

  • Sync execution via AGEDriver.
  • Automatic agtype decoding — vertices and edges are returned as AGENode / AGEEdge wrappers.
  • Standard MATCH/MERGE/DELETE Cypher queries.
  • Automatic graph creation on first connect (if the graph does not exist).
  • TLS — supported via PostgreSQL SSL (pass SSL keyword arguments directly to psycopg.connect by instantiating AGEDriver manually).

Not supported / limitations

  • No async driver. Async support requires an async psycopg3 connection which is not yet wired up.
  • No multi-label nodes. AGE stores each vertex under exactly one PostgreSQL table (one label). The OGM emulates multi-label hierarchies by injecting a _labels array property on CREATE and filtering with WHERE "SubLabel" IN n._labels on queries. Tests requiring true multi-label behaviour (@pytest.mark.requires_multi_label) are automatically skipped for this backend (AGEDriver.supports_multi_label = False).
  • No GeoLocation in-place update. AGE's agtype does not expose a point() constructor via the psycopg3 interface. GeoLocation values are stored as a plain agtype map ({"latitude": …, "longitude": …}) and read back the same way. Re-saving the full node is required to update geo coordinates.
  • No fulltext search in Cypher. Use PostgreSQL tsvector/tsquery full-text search directly on the underlying tables.
  • No vector KNN in Cypher. Use pgvector on the underlying tables.
  • No TypeConverter Cypher wrappers (no vecf32(), intern()).
  • No index DDL in runic's migration adapter. AGE does not expose Cypher-level index creation; create PostgreSQL indexes on the underlying ag_label tables directly.
python
from runic.ogm import create_driver, Session

driver = create_driver(
    "age",
    host="localhost",
    port=5432,
    database="postgres",
    graph="my_graph",
    username="postgres",
    password="secret",
)
with Session(driver) as session:
    ...

Prerequisites — the age extension must be installed in PostgreSQL:

sql
-- run once as superuser
CREATE EXTENSION IF NOT EXISTS age;

The runic driver runs LOAD 'age' and sets search_path = ag_catalog, "$user", public automatically on every new connection.


Generic Bolt (custom backends)

BoltDriver can connect to any Bolt-compatible graph database by supplying a custom GraphDialect.

python
from runic.ogm.driver.bolt import BoltDriver
from myapp.dialects import MyDialect

driver = BoltDriver.from_params(
    host="localhost",
    port=7687,
    database="neo4j",
    username="neo4j",
    password="secret",
    dialect=MyDialect(),
    encrypted=True,          # switches to bolt+s://
)

TLS note — the encrypted flag is a convenience wrapper: it rewrites bolt://bolt+s:// (or vice-versa). You can bypass it by passing a URI directly to the BoltDriver constructor.

runic - Graph schema migrations and OGM for Cypher-based graph databases. · Impressum