Metadata-Version: 2.4
Name: velr
Version: 0.2.42
Summary: Python bindings for Velr, an embedded openCypher graph database built on SQLite
License-Expression: MIT AND LicenseRef-Velr-Runtime-Binary-Redistribution-License
Project-URL: Homepage, https://velr.ai
Project-URL: Documentation, https://velr.ai/docs/get-started/
Project-URL: Issues, https://github.com/velr-ai/velr/issues
Project-URL: Discussions, https://github.com/velr-ai/velr/discussions
Project-URL: Examples, https://github.com/velr-ai/velr-python-examples
Keywords: graph,database,cypher,opencypher,sqlite,embedded,arrow,pandas,polars
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Topic :: Database
Classifier: Topic :: Database :: Database Engines/Servers
Classifier: Topic :: Database :: Front-Ends
Classifier: Topic :: Software Development :: Embedded Systems
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE.runtime
Requires-Dist: cffi>=1.15
Provides-Extra: pandas
Requires-Dist: pandas>=2; extra == "pandas"
Requires-Dist: pyarrow>=15; extra == "pandas"
Provides-Extra: polars
Requires-Dist: polars>=0.20; extra == "polars"
Requires-Dist: pyarrow>=15; extra == "polars"
Provides-Extra: nx
Requires-Dist: networkx>=3.2; extra == "nx"
Provides-Extra: all
Requires-Dist: pandas>=2; extra == "all"
Requires-Dist: polars>=0.20; extra == "all"
Requires-Dist: pyarrow>=15; extra == "all"
Requires-Dist: networkx>=3.2; extra == "all"
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Requires-Dist: pytest-cov>=4; extra == "test"
Requires-Dist: networkx>=3.2; extra == "test"
Requires-Dist: pandas>=2; extra == "test"
Requires-Dist: polars>=0.20; extra == "test"
Requires-Dist: pyarrow>=15; extra == "test"
Dynamic: license-file

# Velr

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 exposes a small,
Pythonic API for executing Cypher queries, iterating result tables, working
with transactions, and exporting results to Arrow, pandas, and Polars.

For the main Velr public entry point, see [velr-ai/velr](https://github.com/velr-ai/velr).  
For the Velr website, see [velr.ai](https://velr.ai/).

## Community

- **Community and questions:** [GitHub Discussions](https://github.com/velr-ai/velr/discussions)
- **Bug reports and feature requests:** [GitHub Issues](https://github.com/velr-ai/velr/issues)
- **Python examples:** [velr-python-examples](https://github.com/velr-ai/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.x` series, we intend to guarantee internal database compatibility within the branch.

### Schema version 8 compatibility

This release's current on-disk schema is version 8. 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.

Fulltext search and vector search are available today through Cypher DDL and
`CALL` syntax. API details may still evolve while Velr remains alpha.

---

## Installation

Install from PyPI:

```bash
pip install velr
```

For Arrow / dataframe workflows, install the optional Python dependencies you want to use:

```bash
pip install pyarrow pandas polars
```

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

```python
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.iter_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:

```python
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:

```python
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, 6, or 7 databases opened by
a schema version 8 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 8 read/write compatibility behavior.

```python
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:

```python
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]))
```

## Maintenance

After large deletes or prune operations on a file-backed database, call
`db.vacuum()` from maintenance code when you want to compact the database file:

```python
from velr.driver import Velr

with Velr.open("mygraph.db") as db:
    db.run("MATCH (n:Expired) DETACH DELETE n")
    db.vacuum()
```

`vacuum()` must run on a writable current-schema connection outside an open
transaction. The equivalent command for scripts is `VACUUM DATABASE`.

---

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

```python
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.iter_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

Fulltext search is available through normal Cypher execution. Define indexes
with `CREATE FULLTEXT INDEX` and query them with
`CALL db.index.fulltext.queryNodes(...)`.

```python
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.iter_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 `OR` between 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 are kept up to date by writes. For file-backed databases,
Velr repairs missing or corrupt fulltext index data automatically when the
database is opened.

---

## Vector Search

Velr supports two vector index shapes.

If your application already computes embeddings, store them as a vector/list
property and index that property. No embedder is needed, and queries pass a
numeric vector/list:

```python
with Velr.open("mygraph.db") as db:
    db.run(
        """
        CREATE (:Chunk {
          id: 'chunk-1',
          text: 'Graph databases store connected data',
          embedding: [0.12, 0.34, 0.56]
        })
        """
    )

    db.run(
        """
        CREATE VECTOR INDEX chunkEmbedding IF NOT EXISTS
        FOR (n:Chunk)
        ON (n.embedding)
        OPTIONS { indexConfig: { dimensions: 3, metric: 'cosine' } }
        """
    )

    with db.exec_one(
        """
        CALL db.index.vector.queryNodes('chunkEmbedding', 10, [0.10, 0.30, 0.50])
        YIELD node, score
        RETURN node, score
        """
    ) as table:
        print(table.to_rows())
```

For text-to-vector search, register an embedding callback and 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(...)`.

`OPTIONS { indexConfig: ... }` configures Velr vector indexes. Prefer the
Neo4j-style names where they exist; shorter aliases are accepted for existing
queries and examples.

```cypher
OPTIONS {
  indexConfig: {
    `vector.dimensions`: 384,
    `vector.similarity_function`: 'cosine',
    embedder: 'text',
    cache_policy: 'required',
    `vector.hnsw.ef_search`: 128
  }
}
```

Core options:

| Option | Accepted aliases | Meaning |
|---|---|---|
| `` `vector.dimensions` `` | `dimensions` | Required vector width. Every stored vector, embedder output, and query vector must contain exactly this many values. |
| `` `vector.similarity_function` `` | `metric`, `similarity_function` | Distance function. Use `cosine` for most semantic embeddings, `l2`/`euclidean` for geometric distance, and `dot`/`inner_product`/`max_inner_product` for inner-product retrieval. Default: `cosine`. |
| `embedder` | `velr.embedder` | Name of a registered callback for `ON EACH [...]` indexes. Omit it for stored-vector indexes such as `ON (n.embedding)`. |
| `embedder_fingerprint` | `embedderFingerprint`, `velr.embedder_fingerprint` | Optional model/version metadata stored with the index definition. Velr does not use it to call the embedder. |
| `cache_policy` | `cachePolicy` | Embedder-backed indexes only. `required` stores generated embeddings in the database so index files can be rebuilt without recomputing them; `best_effort` skips cache write errors; `none` stores no generated embedding cache. Defaults: `required` with `embedder`, `none` without. Stored-vector indexes require `none` because the vector property is already the durable source. |

HNSW tuning options:

| Option | Integer count | Meaning |
|---|---:|---|
| `` `vector.hnsw.m` `` | `1..512` neighbors | Maximum graph connectivity per vector. For example, `16` means each vector keeps roughly 16 HNSW neighbor links. Higher values can improve recall on difficult datasets, with more memory, larger index files, and slower builds. |
| `` `vector.hnsw.ef_construction` `` | `1..3200` candidates | Build-time candidate pool per inserted vector. For example, `320` means the builder considers a working set of 320 candidate neighbors while placing each vector. Higher values can improve index quality and recall, usually with slower index creation. |
| `` `vector.hnsw.ef_search` `` | `1..3200` candidates | Query-time candidate pool. For example, `128` means each search keeps a working set of 128 candidate vectors while traversing the HNSW graph. Higher values can improve recall, usually with slower searches. Velr's default is `64`. |

Velr manages the vector scalar format and storage/cache engines internally. Use
the options above for application configuration.

```python
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.iter_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.

For file-backed databases, Velr can repair missing or corrupt vector index data
from stored vector properties or from registered embedders, depending on how
the index was created.

---

## 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:

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

### `exec_one()`

Use `exec_one()` when the query should yield exactly one table:

```python
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:

```python
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:

```python
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:

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

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

```python
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 `exec_one()` returns.

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` → `None`
* `BOOL` → `bool`
* `INT64` → `int`
* `DOUBLE` → `float`
* `TEXT` → `str` by default
* `JSON` → `str` by default, or parsed Python objects with `parse_json=True`

Example:

```python
with db.exec_one("MATCH (p:Person) RETURN p.name AS name, p.age AS age") as table:
    with table.iter_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:

```python
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 it
* `rollback()` 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 active
* `release_savepoint(name)` releases a named savepoint by name; the named savepoint must be the most recent active named savepoint
* `release()` or `rollback()` 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:

```python
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'})")

        tx.rollback_to("sp1")  # undoes a and b, drops sp2, keeps sp1 active
        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.Table`
* `pandas.DataFrame`
* `polars.DataFrame`

### pandas

```python
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

```python
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

```python
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

```python
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()
```

Use `table.to_rows()` for the normal Python result shape. It returns
ready-to-use row lists and is the recommended path when you intend to read the
whole result. Use `table.iter_rows()` when you want cursor-style iteration: it
yields one row at a time, so you can stop early or avoid building a full Python
list. As with database cursors generally, a query plan can still use temporary
storage for operations such as sorting or aggregation.

`to_rows()` returns a list of row lists. Cypher lists and maps are decoded into
Python lists and dicts. By default it returns every column projected by the
query; prefer expressing the result shape in Cypher with `RETURN`.

Use `to_records()` when you want named dictionaries instead of positional row
lists; it returns the same list-of-dicts shape accepted by `bind_records()`.
Some application schemas store JSON documents in string properties. If you
project one of those properties and want Python dict/list values, pass
`columns` for those projected columns. This is explicit: ordinary Cypher
strings stay strings unless you opt in.

```python
with db.exec_one(
    """
    MATCH (d:Document)
    RETURN d.id AS id, d.metadata AS metadata
    """
) as table:
    rows = table.to_rows()
    decoded_rows = table.to_rows(columns=("metadata",))
    records = table.to_records(columns=("metadata",))
```

For the common case where you only need the final result table, the connection
also has the same shortcut:

```python
rows = db.to_rows("""
MATCH (d:Document)
RETURN d.id AS id, d.metadata AS metadata
""")

records = db.to_records("""
MATCH (d:Document)
RETURN d.id AS id, d.metadata AS metadata
""")
```

Because `to_records()` returns the same list-of-dicts shape accepted by
`bind_records()`, projected results can be rebound under a logical name:

```python
records = db.to_records("""
MATCH (d:Document)
RETURN d.id AS id, d.metadata AS metadata
""")

db.bind_records("_documents", records)
```

---

## 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_arrow_ipc()`
* `bind_pandas()`
* `bind_polars()`
* `bind_numpy()`
* `bind_records()`

`bind_arrow_ipc()` accepts Arrow IPC file / Feather v2 bytes and borrows the
buffer only for the duration of the call.

### Bind a pandas DataFrame

```python
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

```python
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})
    """)
```

`bind_records()` is available on both `Velr` and `VelrTx`. Use it when your
input is naturally a list of JSON-compatible Python dictionaries: `None`,
bools, signed 64-bit integers, finite floats, strings, lists, tuples, and
dicts. It is the write-side counterpart to `to_records()`.

Use `bind_arrow()` when your data is already Arrow-backed, or when an adapter
intentionally wants Arrow's columnar layout for large vector/array-heavy
batches. Use `bind_records(..., types=...)` only when you specifically want
Arrow-style type control while starting from Python records.

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

---

## 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()`.

```python
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 and passes all positive openCypher
TCK tests. Exact error semantics, including error messages, categories, and
timing, are not guaranteed to match other openCypher implementations.

---

## OpenCypher functions

The following openCypher functions and constructors are available:

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

Supported distributions may include prebuilt binary wheels for common platforms.
Where binary wheels are available, Velr is ready to use after installation.

Currently bundled targets:

    * macOS (arm64)
    * Linux x86_64
    * Linux aarch64
    * Windows x86_64

---

## License

See [`LICENSE`](LICENSE) and [`LICENSE.runtime`](LICENSE.runtime) for the full license texts.
