Skip to content

Python Driver

A lightweight, in-process Python API for querying, mutating, and inspecting the Velr graph database.

Velr currently requires Python 3.12 or newer.

The Python driver provides:

  • Connection management via Velr.open
  • Executing Cypher queries via run, exec, and exec_one
  • Transactional control via begin_tx and VelrTx
  • Savepoints via savepoint, savepoint_named, and rollback_to
  • Streaming result tables via Stream, Table, Rows, and Cell
  • Converting results to PyArrow, pandas, and Polars
  • Binding external data via bind_arrow, bind_pandas, bind_polars, bind_numpy, and bind_records
  • EXPLAIN / EXPLAIN ANALYZE via explain and explain_analyze

Thread safety

Velr connections and active result handles are not safe for concurrent use from multiple threads.

If you need parallelism:

  • open one connection per thread
  • do not share active connections
  • do not share transactions, streams, tables, row iterators, or explain traces across threads

Opening a Database

from velr.driver import Velr

with Velr.open(None) as db:
    db.run("CREATE (:Person {name:'Alice'})")

Open a file-backed database instead of an in-memory database:

from velr.driver import Velr

with Velr.open("mygraph.db") as db:
    db.run("CREATE (:Person {name:'Alice'})")

Path semantics:

  • None → in-memory database
  • "path" → file-backed database at that path

Executing Queries

Queries may produce zero or more result tables.

run: Execute and discard results

Use this when you want to execute a query and do not need to consume any returned rows.

from velr.driver import Velr

with Velr.open(None) as db:
    db.run("CREATE (:Movie {title:'Interstellar', released:2014})")

exec: Stream result tables

Use this when a query or script may produce one or more tables.

from velr.driver import Velr

MOVIES_CREATE = r"""
CREATE
  (keanu:Person:Actor {name:'Keanu Reeves', born:1964}),
  (nolan:Person:Director {name:'Christopher Nolan'}),
  (matrix:Movie {title:'The Matrix', released:1999, genres:['Sci-Fi','Action']}),
  (inception:Movie {title:'Inception', released:2010, genres:['Sci-Fi','Heist']}),
  (keanu)-[:ACTED_IN {roles:['Neo']}]->(matrix),
  (nolan)-[:DIRECTED]->(inception);
"""

with Velr.open(None) as db:
    db.run(MOVIES_CREATE)

    with db.exec(
        "MATCH (m:Movie {title:'The Matrix'}) RETURN m.title AS title; "
        "MATCH (m:Movie {title:'Inception'}) RETURN m.released AS released"
    ) as stream:
        for table in stream.iter_tables():
            print(table.column_names())
            print(table.collect(lambda row: [cell.as_python() for cell in row]))

exec_one: Expect exactly one result table

Use this when the query is expected to produce exactly one table.

from velr.driver import Velr

with Velr.open(None) as db:
    db.run("CREATE (:Person {name:'Alice', age:30})")

    with db.exec_one("MATCH (p:Person) RETURN p.name AS name, p.age AS age") as table:
        print(table.column_names())
        print(table.collect(lambda row: [cell.as_python() for cell in row]))

Reading Results

A Table provides:

  • column_names()
  • column_count()
  • rows()
  • collect(...)
  • to_pyarrow()
  • to_pandas()
  • to_polars()

Example:

from velr.driver import Velr

MOVIES_CREATE = r"""
CREATE
  (keanu:Person:Actor {name:'Keanu Reeves', born:1964}),
  (nolan:Person:Director {name:'Christopher Nolan'}),
  (matrix:Movie {title:'The Matrix', released:1999, genres:['Sci-Fi','Action']}),
  (inception:Movie {title:'Inception', released:2010, genres:['Sci-Fi','Heist']}),
  (keanu)-[:ACTED_IN {roles:['Neo']}]->(matrix),
  (nolan)-[:DIRECTED]->(inception);
"""

with Velr.open(None) as db:
    db.run(MOVIES_CREATE)

    with db.exec_one(
        "MATCH (m:Movie {title:'Inception'}) "
        "RETURN m.title AS title, m.released AS year, m.genres AS genres"
    ) as table:
        print(table.column_names())

        with table.rows() as rows:
            row = next(rows)

    title, year, genres = row
    print(title.as_python())
    print(year.as_python())
    print(genres.as_python())

Cell

Rows are yielded as tuples of Cell objects.

Use Cell.as_python() to convert values to normal Python objects.

Supported conversions include:

  • NULLNone
  • BOOLbool
  • INT64int
  • DOUBLEfloat
  • TEXTstr by default
  • JSONstr by default, or parsed Python objects with parse_json=True

Example:

with db.exec_one("MATCH (p:Person) RETURN p.name AS name, p.age AS age") as table:
    with table.rows() as rows:
        for row in rows:
            print(row[0].as_python(), row[1].as_python())

Table lifetime

Table lifetime depends on how a table was obtained.

Tables from exec()

Tables produced by exec() remain valid while the stream is open.

with db.exec("MATCH (n) RETURN n") as stream:
    table = stream.next_table()
    # table is valid here

Once the stream is closed, any still-open tables produced by it are also closed.

Tables from exec_one()

Tables returned by exec_one() are parented to the connection or transaction rather than to a stream.

That means they remain usable after exec_one() returns.

Even so, tables should still be closed when no longer needed, ideally by using them as context managers.


Transactions

Velr supports explicit transactions via VelrTx.

Opening a transaction

from velr.driver import Velr

with Velr.open(None) as db:
    with db.begin_tx() as tx:
        tx.run("CREATE (:Movie {title:'Interstellar', released:2014})")
        tx.commit()

Running queries inside a transaction

from velr.driver import Velr

with Velr.open(None) as db:
    with db.begin_tx() as tx:
        tx.run("CREATE (:Movie {title:'Interstellar', released:2014})")

        with tx.exec_one("MATCH (m:Movie) RETURN count(m) AS c") as table:
            with table.rows() as rows:
                for (count_cell,) in rows:
                    print("Count:", count_cell.as_python())

        tx.commit()

Commit / rollback

tx.commit()
# or
tx.rollback()

If a transaction context exits without commit(), it is rolled back.

After commit() or rollback(), a transaction can no longer be used.


Savepoints

Savepoints let you roll back part of a transaction.

Scoped savepoint

with Velr.open(None) as db:
    with db.begin_tx() as tx:
        tx.run("CREATE (:Temp {k:'outer'})")

        with tx.savepoint() as sp:
            tx.run("CREATE (:Temp {k:'inner'})")
            sp.rollback()

        tx.commit()

Named savepoint

with Velr.open(None) as db:
    with db.begin_tx() as tx:
        with tx.savepoint_named("sp1") as sp:
            tx.run("CREATE (:Temp {k:'inner'})")
            sp.rollback()  # rollback to savepoint, then release it

        tx.commit()

Roll back to a named savepoint

with Velr.open(None) as db:
    with db.begin_tx() as tx:
        tx.run("CREATE (:Temp {k:'outer'})")

        tx.savepoint_named("sp1")
        tx.run("CREATE (:Temp {k:'inner'})")

        tx.rollback_to("sp1")
        tx.commit()

Converting Results to PyArrow, pandas, and Polars

Velr can export result tables as Arrow IPC and convert them into:

  • pyarrow.Table
  • pandas.DataFrame
  • polars.DataFrame

pandas

with Velr.open(None) as db:
    db.run(MOVIES_CREATE)

    df = db.to_pandas(
        "MATCH (m:Movie) "
        "RETURN m.title AS title, m.released AS released "
        "ORDER BY released"
    )
    print(df)

Polars

with Velr.open(None) as db:
    db.run(MOVIES_CREATE)

    df = db.to_polars(
        "MATCH (m:Movie) "
        "RETURN m.title AS title, m.released AS released "
        "ORDER BY released"
    )
    print(df)

PyArrow

with Velr.open(None) as db:
    db.run(MOVIES_CREATE)

    tbl = db.to_pyarrow(
        "MATCH (m:Movie) "
        "RETURN m.title AS title, m.released AS released "
        "ORDER BY released"
    )
    print(tbl)

Export from an existing table

with db.exec_one("MATCH (m:Movie) RETURN m.title AS title") as table:
    pa_tbl = table.to_pyarrow()
    df = table.to_pandas()
    pl_df = table.to_polars()

Binding External Data with BIND(...)

Velr can bind external columnar data under a logical name and query it from Cypher.

Supported bind helpers include:

  • bind_arrow()
  • bind_pandas()
  • bind_polars()
  • bind_numpy()
  • bind_records()

Bind a pandas DataFrame

import pandas as pd
from velr.driver import Velr

df = pd.DataFrame(
    [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 41},
    ]
)

with Velr.open(None) as db:
    db.bind_pandas("_people", df)

    db.run("""
    UNWIND BIND('_people') AS r
    CREATE (:Person {name:r.name, age:r.age})
    """)

    out = db.to_pandas("MATCH (p:Person) RETURN p.name AS name, p.age AS age ORDER BY age")
    print(out)

Bind a list of dicts

rows = [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 41},
]

with Velr.open(None) as db:
    db.bind_records("_people", rows)
    db.run("""
    UNWIND BIND('_people') AS r
    CREATE (:Person {name:r.name, age:r.age})
    """)

EXPLAIN and EXPLAIN ANALYZE

The Python driver exposes explain traces on both connections and transactions.

Basic EXPLAIN

with Velr.open(None) as db:
    with db.explain("MATCH (p:Person) RETURN p.name AS name") as xp:
        print(xp.to_compact_string())

Use explain() for planning output and explain_analyze() to include execution details.

Explain APIs

Velr exposes explain traces through:

  • Velr.explain()
  • Velr.explain_analyze()
  • VelrTx.explain()
  • VelrTx.explain_analyze()

These return an ExplainTrace, which can be navigated incrementally or fully materialized with snapshot().


Query language support

Velr supports most of openCypher, but some features are not yet implemented.

Notable missing features:

  • REMOVE clause
  • Query parameters (for example $name)
  • The query planner does not yet use indexes in all cases where expected.

Supported functions

Velr currently supports these openCypher functions:

Scalars

  • id()
  • type()
  • length()
  • nodes()
  • relationships()
  • coalesce()
  • labels()
  • properties()
  • keys()

Aggregates

  • count()
  • sum()
  • avg()
  • min()
  • max()
  • collect()

API Summary

Velr

  • open(path: str | None) -> Velr
  • run(query) -> None
  • exec(query) -> Stream
  • exec_one(query) -> Table
  • begin_tx() -> VelrTx
  • to_pyarrow(query) -> pyarrow.Table
  • to_pandas(query) -> pandas.DataFrame
  • to_polars(query) -> polars.DataFrame
  • bind_arrow(logical, columns) -> None
  • bind_pandas(logical, df, ...) -> None
  • bind_polars(logical, df, ...) -> None
  • bind_numpy(logical, data, ...) -> None
  • bind_records(logical, rows, ...) -> None
  • explain(query) -> ExplainTrace
  • explain_analyze(query) -> ExplainTrace

VelrTx

  • run(query) -> None
  • exec(query) -> StreamTx
  • exec_one(query) -> Table
  • commit() -> None
  • rollback() -> None
  • savepoint() -> Savepoint
  • savepoint_named(name) -> Savepoint
  • rollback_to(name) -> None
  • bind_arrow(logical, columns) -> None
  • bind_pandas(logical, df, ...) -> None
  • bind_polars(logical, df, ...) -> None
  • bind_numpy(logical, data, ...) -> None
  • bind_records(logical, rows, ...) -> None
  • explain(query) -> ExplainTrace
  • explain_analyze(query) -> ExplainTrace

Savepoint

  • release() -> None
  • rollback() -> None

Result types

  • Stream
  • StreamTx
  • Table
  • Rows
  • Cell
  • ExplainTrace