Skip to main content

AVL-DB

PyPI Python versions CI License

avldb is a synchronous embedded document database for typed Pydantic models. It provides NeDB-style CRUD and MongoDB-like queries while leaving record placement, persistence, indexes, and caching to an extensible backend contract.

The built-in backends use the Rust-backed rs-avl ordered set for indexes, but custom backends may use a different index engine.

Installation

pip install avldb

Python 3.10+ and CPython are supported.

Quick start

from pathlib import Path

from avldb import Collection, DiskBackend, Document


class User(Document):
    # This is application data. avldb's database ID remains user._id.
    id: int
    name: str
    age: int
    roles: list[str] = []


with Collection(User, backend=DiskBackend(Path("users.avldb"))) as users:
    users.ensure_index("id", unique=True)
    users.ensure_index("age")
    users.ensure_index("roles")

    users.insert(User(id=1001, name="Ada", age=36, roles=["admin", "author"]))
    users.insert(User(id=1002, name="Grace", age=29, roles=["author"]))

    adults = users.find({"age": {"$gte": 18}}).sort({"age": -1}).all()
    authors = users.find({"roles": "author"}).all()

Database identifiers are monotonic UUIDv7 strings. Access them as document._id, query with {"_id": value}, and serialize with model_dump(by_alias=True). Models may independently define an id field.

Full reads return the collection's Pydantic model. Projected reads return plain dictionaries because partial data may not satisfy the complete model schema.

Queries and updates

Queries support:

  • Equality and deep equality.
  • Nested dot paths and arrays of subdocuments.
  • $lt, $lte, $gt, $gte, $in, $nin, $ne, $exists, and $regex.
  • $size and $elemMatch for arrays.
  • $or, $and, $not, and callable $where expressions.
  • Inclusion/exclusion projection, multi-field sorting, skip, and limit.

Updates may replace a document or use $set, $unset, $inc, $min, $max, $push, $pop, $addToSet, and $pull, including $each and $slice. Single-document updates are the default; multi=True updates or removes every match. Upserts are supported.

result = users.update(
    {"id": 1001},
    {"$inc": {"age": 1}, "$addToSet": {"roles": "maintainer"}},
    return_updated=True,
)

How query execution works

Collection owns query semantics but never owns all documents or an index tree. For an indexed range such as:

users.find({"age": {"$gte": 18, "$lt": 65}})

the planner performs:

BackendView.lookup("age", 18 <= value < 65)
    → candidate document IDs
BackendView.fetch(candidate IDs)
    → matching content records only
full NeDB matcher
    → final results

Multiple usable indexes are intersected. Fully indexed $or branches are unioned. The full matcher always verifies candidates. If no safe index exists, BackendView.scan() streams content instead of loading the collection into memory.

Iterating an unsorted cursor also streams matched records and keeps its backend view open for the iteration. Calling .all() intentionally materializes the returned results, and sorting must materialize all matches before ordering them.

Backends

The backend owns:

  • Physical document placement and lookup.
  • The mandatory unique _id index and all secondary indexes.
  • Index persistence, caching, and implementation technology.
  • Atomic content/index commits and revision conflict detection.
  • Compaction and cleanup.

Collection uses only this small snapshot contract:

class StorageBackend:
    def open(self) -> None: ...
    def view(self) -> BackendView: ...
    def commit(self, base_revision: str, changes: ChangeSet) -> str: ...
    def compact(self) -> str: ...
    def close(self) -> None: ...


class BackendView:
    revision: str
    indexes: Mapping[str, IndexSpec]

    def scan(self) -> Iterator[dict[str, object]]: ...
    def lookup(self, field: str, lookup: IndexLookup) -> set[str]: ...
    def fetch(self, document_ids: Iterable[str]) -> Iterator[dict[str, object]]: ...

Views represent immutable revisions. commit must apply document changes and every affected index atomically or expose none of them. A stale revision raises WriteConflictError. This makes queries, bulk insertion, unique constraints, multi-updates, and index creation backend-independent.

MemoryBackend

MemoryBackend is the smallest reference implementation:

  • Documents live in a record array.
  • The _id index resolves IDs to array slots.
  • Secondary indexes use AVL trees.
  • Commits build copy-on-write state and atomically swap one revision.
  • Old backend views remain stable after later writes.

DiskBackend

DiskBackend uses a directory, not a single database file:

users.avldb/
├── manifest.json
├── database.lock
├── content/
│   └── <revision>.jsonl
└── indexes/
    ├── <_id-index>/
    │   └── <revision>.jsonl
    ├── <age-index>/
    │   └── <revision>.jsonl
    └── <roles-index>/
        └── <revision>.jsonl

Content and every index use separate immutable segments. A commit writes and fsyncs all segments before atomically replacing manifest.json; the manifest is the sole commit marker. Interrupted staging leaves unreferenced files that are ignored and cleaned later.

The persisted _id index maps IDs to content segment byte offsets. Opening a database reads index files but does not read every document. Indexed queries fetch only candidate content records. Compaction writes consolidated content and index snapshots, publishes a new manifest, then cleans segments no active view needs.

Disk storage allows one writer backend for a datastore directory. Calls through one collection are thread-safe and serialized.

Source layout

The package is grouped by responsibility while keeping the root import API small and stable:

src/avldb/
├── core/        # Document models, matching, updates, cursors, and planning
├── indexing/    # The reusable rs-avl index adapter
├── storage/     # Backend contract, codecs, and memory/disk implementations
├── contracts.py # Shared public dataclasses and protocols
└── exceptions.py

Application code should generally import from avldb. Backend implementations can import the extension contract from avldb.storage.

Implementing another backend

See Writing a custom backend for the contract, implementation rules, and a complete lock-free list backend you can run.

Subclass StorageBackend and return a BackendView implementation. The shared backend conformance tests illustrate the required behavior. Important rules:

  1. A view must remain logically immutable until closed.
  2. lookup returns document IDs, never physical locations.
  3. fetch resolves IDs using backend-owned primary index state.
  4. scan streams live documents and must not require preloading them.
  5. commit validates its base revision and atomically updates documents, unique constraints, secondary indexes, and index metadata.
  6. Failed commits must remain invisible.

An S3 implementation can map content/index segments to immutable objects and the manifest to a small object updated conditionally with its ETag. It may cache downloaded indexes as AVL trees, use another ordered structure, or query an external index service without changing Collection.

No S3 dependency is bundled.

Full example

The runnable example demonstrates typed models with a domain id, unique, range, nested and multikey indexes, projections, updates, upsert, TTL cleanup, reopen, and compaction:

uv run python examples/basic.py

Development

Install UV, clone the repository, and create the locked development environment:

git clone https://github.com/anuradhawick/avldb.git
cd avldb
uv sync --locked

Run the test suite and examples inside that environment:

uv run pytest
uv run python examples/basic.py
uv run python examples/custom_backend.py

Format Python files with Black, or check formatting without changing files:

uv run black src tests examples benchmarks
uv run black --check src tests examples benchmarks

Build the same source and wheel artifacts used for publication:

uv build --no-sources

CI checks Black formatting and runs the tests on every supported Python version. When dependencies change, run uv lock and commit the updated uv.lock file.

Benchmarks

uv run python benchmarks/indexed_queries.py

The benchmark compares repeated indexed queries with streaming scans over the same dataset. It contains no timing assertion because absolute performance is environment-dependent.

Licensed under your choice of Apache-2.0 or GPL-3.0-only.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

avldb-0.1.0.tar.gz (86.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

avldb-0.1.0-py3-none-any.whl (51.2 kB view details)

Uploaded Python 3

File details

Details for the file avldb-0.1.0.tar.gz.

File metadata

  • Download URL: avldb-0.1.0.tar.gz
  • Upload date:
  • Size: 86.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for avldb-0.1.0.tar.gz
Algorithm Hash digest
SHA256 905c3e63aace1c810adaa7396b9264212caf50ed0b23d9880760a11bea2dc0ac
MD5 fde56756c072487db19800f196c6d81b
BLAKE2b-256 3c9ce77044400a0b7ab87b23489e41d202c2ef8590a7a01a5601732e0e9a62be

See more details on using hashes here.

File details

Details for the file avldb-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: avldb-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 51.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for avldb-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4bd64f998dd8a73205157a01915877c0a22d465f06112475a7a5a4167f6a6150
MD5 ba0721d33d5007bb776d97498fa03985
BLAKE2b-256 c49053386d6ede1dd019fefc0ab90f8eb4c670d76ca4fd0f18389bbb0ec2ecf7

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page