Python Driver¶
Velr is an embedded property-graph database from Velr.ai, written in Rust, built on top of SQLite (persisting to a standard SQLite database file) and queried using the openCypher language.
It runs in-process and is designed for local, embedded, and edge use cases.
This package provides the Python bindings for Velr. It wraps a bundled native runtime with a C ABI, implemented in Rust, and exposes a small, Pythonic API for executing Cypher queries, streaming result tables, working with transactions, and exporting results to Arrow, pandas, and Polars.
This page follows the public Python package README. For the latest release-specific notes, see the Python driver README and the Velr package on PyPI.
For the main Velr public entry point, see velr-ai/velr. For the Velr website, see velr.ai.
Community¶
- Community and questions: GitHub Discussions
- Bug reports and feature requests: GitHub Issues
- Python examples: velr-python-examples
We’d love to have you join the Velr community.
Release status¶
Velr is currently in public alpha.
- The Python API is still evolving.
- Velr supports openCypher and passes all positive openCypher TCK tests. Exact error semantics are not guaranteed to match other openCypher implementations.
- Velr 0.2.14 includes a breaking on-disk storage change; existing databases from earlier releases must be recreated by re-importing the source data.
- Starting with the
0.3.xseries, we intend to guarantee internal database compatibility within the branch.
Schema version 7 compatibility¶
This release's current on-disk schema is version 7. Supported older databases
can be opened with Velr.open() or Velr.open_readonly() without changing the
file. Reads continue to work on those databases, but writes (CREATE, MERGE,
SET, DELETE, DETACH DELETE, and other mutating queries) are only available
after migrating to the current schema version. This is intentional: migration
is an explicit maintenance operation, not a side effect of opening a database.
Velr is already usable for real workflows and representative use cases, but rough edges remain and the API is not yet stable.
BM25 fulltext search and vector/ANN search are available today through Cypher
DDL and CALL syntax. API details may still evolve while Velr remains alpha.
Installation¶
Install from PyPI:
For Arrow / dataframe workflows, install the optional Python dependencies you want to use:
Licensing in simple terms¶
- The Python binding source code in this package is licensed under MIT.
- The bundled native runtime binaries may be used and freely redistributed in unmodified form under the terms of
LICENSE.runtime.
Quick start¶
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())
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'})")
Open an existing database for reads only:
from velr.driver import Velr
with Velr.open_readonly("mygraph.db") as db:
with db.exec_one("MATCH (n) RETURN count(n) AS count") as table:
print(table.collect(lambda row: [cell.as_python() for cell in row]))
open_readonly() never creates, initializes, migrates, or repairs a database.
The file must already exist and have a supported Velr schema version. Older
supported databases, such as schema version 3, 4, 5, or 6 databases opened by a
schema version 7 runtime, remain available for reads. Writes and features that
require the current schema fail with a normal query error until the database is
explicitly migrated.
Schema migration¶
Velr does not migrate supported older databases automatically on open. Use the
driver migration API, or run MIGRATE DATABASE, from maintenance code when you
intend to update the on-disk schema. See the release-status note above for the
schema version 7 read/write compatibility behavior.
from velr.driver import Velr
with Velr.open("mygraph.db") as db:
if db.needs_migration():
report = db.migrate()
print(report.status, report.from_version, report.to_version, report.steps)
The equivalent Cypher command is useful for scripts and tools that already work through query execution:
from velr.driver import Velr
with Velr.open("mygraph.db") as db:
with db.exec_one("MIGRATE DATABASE") as table:
print(table.collect(lambda row: [cell.as_python() for cell in row]))
Introspection¶
Use SHOW CURRENT GRAPH SHAPE to inspect the observed schema of the graph. It
reports the shape present in stored data: node labels, relationship types,
properties, observed value types, and counts. It is an observed shape surface,
not a declared GQL graph type.
SHOW CURRENT GRAPH SHAPE is available on schema version 5 or newer databases.
Older supported databases can still be opened for reads, but must be migrated
explicitly before this command is valid. Schema version 5 introduced this
inventory through the write planner instead of persistent graph-shape triggers.
The default projection returns element_kind, element_name, property_name,
observed_type, owner_count, present_count, and missing_count.
YIELD * exposes the full row shape, including surface, source_label,
target_label, required, storage_class, and tag.
from velr.driver import Velr
with Velr.open("mygraph.db") as db:
with db.exec_one(
"""
SHOW CURRENT GRAPH SHAPE
YIELD element_kind, element_name, property_name, observed_type, owner_count
WHERE element_kind = 'node_property'
RETURN element_name, property_name, observed_type, owner_count
"""
) as table:
with table.rows() as rows:
for row in rows:
print([cell.as_python() for cell in row])
Use YIELD to compose the command with WHERE and RETURN. Plain
SHOW CURRENT GRAPH SHAPE returns the default projection; YIELD * exposes the
full current row shape.
Fulltext Search¶
BM25 fulltext search is available through normal Cypher execution. Define
indexes with CREATE FULLTEXT INDEX and query them with
CALL db.index.fulltext.queryNodes(...).
from velr.driver import Velr
with Velr.open("mygraph.db") as db:
db.run(
"""
CREATE FULLTEXT INDEX paperText
FOR (n:Paper) ON EACH [n.title, n.abstract]
"""
)
with db.exec_one(
"""
CALL db.index.fulltext.queryNodes('paperText', 'abstract:vector')
YIELD node, score
RETURN node, score
"""
) as table:
with table.rows() as rows:
for row in rows:
print([cell.as_python() for cell in row])
The query string supports this fulltext grammar:
- Terms:
vector search - Phrases:
"vector search" - Field scoping by indexed property:
title:graph,abstract:"vector search" - Boolean operators and grouping:
graph AND (vector OR semantic) - Default
ORbetween adjacent terms:vector search - Required and excluded terms:
+vector -draft - Phrase slop:
"vector search"~2 - Phrase prefix on the last phrase term:
"vector sea"* - Boosts:
title:graph^2.0 - Match all indexed nodes:
*
Field scoping applies to the next term or phrase only. For example,
title:graph search searches graph in title and search in the default
fulltext field.
score is a non-normalized relevance score. Higher scores are better within a
single query result set; scores are not guaranteed to be in 0..1 or
comparable across different queries.
Fulltext indexes use a sidecar next to file-backed databases. The sidecar is kept up to date by writes and rebuilt on open if it is missing or corrupt.
Vector Search¶
Register an embedding callback, then reference it from CREATE VECTOR INDEX.
Velr invokes the callback for index maintenance when indexed source values
change and for text queries passed to CALL db.index.vector.queryNodes(...).
from velr.driver import Velr
def embed_text(text: str, dimensions: int) -> list[float]:
# Call your embedding model here.
return [0.0] * dimensions
def embedder(inputs):
vectors = []
for input in inputs:
text = "\n".join(
str(field.value)
for field in input.fields
if field.value_type == "string"
)
prefix = "query: " if input.purpose == "query" else "passage: "
vectors.append(embed_text(prefix + text, input.dimensions))
return vectors
with Velr.open("mygraph.db") as db:
db.register_vector_embedder("text", embedder)
db.run(
"""
CREATE VECTOR INDEX paperEmbedding IF NOT EXISTS
FOR (n:Paper)
ON EACH [n.title, n.abstract]
OPTIONS { indexConfig: { dimensions: 384, metric: 'cosine', embedder: 'text' } }
"""
)
with db.exec_one(
"""
CALL db.index.vector.queryNodes('paperEmbedding', 10, 'paper about greek letters')
YIELD node, score
RETURN node, score
"""
) as table:
with table.rows() as rows:
for row in rows:
print([cell.as_python() for cell in row])
ON EACH [n.title, n.abstract] passes both property values to the callback in
that order. Query text is passed as one unnamed string field. Vector score is
metric-dependent and non-normalized; higher scores are better within a single
query result set.
Query model¶
A query may produce zero or more result tables.
Velr exposes three main ways to run Cypher:
run()executes a query or script and drains all result tables.exec()returns a stream of result tables.exec_one()expects exactly one result table.
run()¶
Use run() when you only care about side effects:
exec_one()¶
Use exec_one() when the query should yield exactly one table:
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]))
exec()¶
Use exec() when a query or script may produce multiple result tables:
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]))
Bounded result previews¶
Pass max_result_rows when a host needs projected column names and a small row
sample without rewriting the Cypher text:
from velr.driver import Velr
with Velr.open_readonly("mygraph.db") as db:
with db.exec_one(
"MATCH (n) RETURN labels(n) AS labels, n.name AS name ORDER BY name",
max_result_rows=20,
) as table:
columns = table.column_names()
sample = table.collect(lambda row: [cell.as_python() for cell in row])
print(columns)
print(sample)
max_result_rows=0 preserves column metadata and makes row cursors return no
rows:
with Velr.open_readonly("mygraph.db") as db:
with db.exec_one("MATCH (n) RETURN n.name AS name", max_result_rows=0) as table:
assert table.column_names() == ["name"]
assert table.collect(lambda row: row) == []
The cap is enforced by Velr during result emission, not by appending or
injecting Cypher LIMIT, and applies independently to each result table
produced by exec(). Existing Cypher LIMIT clauses still apply, so a query
with LIMIT 3 and max_result_rows=5 emits at most three rows, while
LIMIT 10 with max_result_rows=5 emits at most five rows. It is not a timeout
or cancellation mechanism; keep read-only validation and execution deadlines as
separate host concerns.
Query parameter binding¶
Pass params to bind openCypher parameters out of band. Query text uses
$name; parameter names in Python omit the leading $. Values are passed as
Cypher values, not interpolated into query text, so a Python str is always a
Cypher string value.
from velr.driver import Velr
with Velr.open(None) as db:
db.run(
"CREATE (:Person {name: $name, age: $age})",
params={"name": "Alice", "age": 42},
)
with db.exec_one(
"MATCH (p:Person) WHERE p.age >= $min_age RETURN p.name AS name ORDER BY name",
max_result_rows=20,
params={"min_age": 18},
) as table:
print(table.column_names())
print(table.collect(lambda row: [cell.as_python() for cell in row]))
Supported parameter values are None, booleans, signed 64-bit integers, finite
floats, strings, lists/tuples, and dicts with string keys.
Table lifetime and ownership¶
Table lifetime depends on how a table was obtained.
Tables from exec()¶
Tables pulled from exec() are stream-scoped.
They remain valid while the producing stream remains open, and closing the stream closes any still-open tables produced by that stream.
with db.exec("MATCH (n) RETURN n") as stream:
table = stream.next_table()
# table is valid here
# stream is now closed, so any still-open table from it is also closed
Tables from exec_one()¶
Tables returned by exec_one() are parent-scoped, not stream-scoped.
Velr.exec_one()returns a table parented to the connection.VelrTx.exec_one()returns a table parented to the transaction.
That means the returned table remains usable after the internal stream logic used by exec_one() has finished.
Even so, tables should still be closed when no longer needed, ideally by using them as context managers.
Rows and cells¶
Rows are exposed through Rows. Each yielded row is a tuple of Cell objects.
Cell.as_python() converts values to normal Python objects:
NULL→NoneBOOL→boolINT64→intDOUBLE→floatTEXT→strby defaultJSON→strby default, or parsed Python objects withparse_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())
For convenience and safety, TEXT and JSON payloads are copied into Python bytes as rows are read, so row contents remain valid after the next fetch.
Transactions and savepoints¶
Use begin_tx() to open 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()
If a transaction context exits without commit(), it is rolled back.
After commit() or rollback(), a transaction can no longer be used.
Savepoints¶
Velr supports two savepoint styles:
savepoint()creates a scoped, handle-owned savepoint.savepoint_named(name)creates a transaction-owned named savepoint.
Scoped savepoints are owned by the Python handle:
- dropping the handle closes the savepoint
release()releases itrollback()rolls back to it and releases it
Named savepoints are owned by the transaction:
- dropping the returned Python handle does not remove the named savepoint
rollback_to(name)rolls back to that named savepoint, discards any newer named savepoints, and keeps the target named savepoint activerelease_savepoint(name)releases a named savepoint by name; the named savepoint must be the most recent active named savepointrelease()orrollback()on a named savepoint handle consume that named savepoint
Active named savepoints are released automatically during commit() so that surviving changes are preserved in the committed transaction.
Example:
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:'a'})")
tx.savepoint_named("sp2")
tx.run("CREATE (:Temp {k:'b'})")
# Undoes a and b, drops sp2,
# and keeps sp1 active.
tx.rollback_to("sp1")
tx.run("CREATE (:Temp {k:'c'})")
tx.release_savepoint("sp1")
tx.commit()
pandas / Polars / PyArrow interop¶
Velr can export result tables as Arrow IPC and convert them into:
pyarrow.Tablepandas.DataFramepolars.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 Arrow, pandas, Polars, NumPy, and records¶
Velr can also 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 support¶
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().
with Velr.open(None) as db:
with db.explain("MATCH (p:Person) RETURN p.name AS name") as xp:
print(xp.to_compact_string())
Query language support¶
Velr supports the openCypher query language. Exact error semantics, including error messages, categories, and timing, are not guaranteed to match other openCypher implementations.
Supported functions¶
Velr currently supports these openCypher functions and constructors:
Graph and path¶
id()type()labels()keys()properties()length()nodes()relationships()
Lists and predicates¶
size()head()last()tail()reverse()range()all()any()none()single()
Strings and conversion¶
coalesce()toInteger()toString()toLower()trim()substring()split()
Numeric¶
abs()ceil()rand()sign()sqrt()
Temporal¶
date()time()localtime()datetime()localdatetime()duration()datetime.fromepoch()datetime.fromepochmillis()date.realtime(),date.transaction(),date.statement()time.realtime(),time.transaction(),time.statement()localtime.realtime(),localtime.transaction(),localtime.statement()datetime.realtime(),datetime.transaction(),datetime.statement()localdatetime.realtime(),localdatetime.transaction(),localdatetime.statement()
Aggregates¶
count()sum()avg()min()max()collect()percentileDisc()percentileCont()
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
Platform support¶
The Python package wraps a bundled native runtime implemented in Rust.
Supported distributions may include prebuilt binary wheels for common platforms. Where binary wheels are available, the compiled runtime is included with the package.
Currently bundled targets:
- macOS (arm64)
- Linux x86_64
- Linux aarch64
- Windows x86_64
License¶
See LICENSE and LICENSE.runtime for the full license texts.