IVF with dense vectors (clustered)

August 11, 2026 · View on GitHub

MongrelDB logo

MongrelDB Mojo Client

Pure Mojo client for MongrelDB - embedded+server database with SQL, vector search, full-text search, and AI-native retrieval.
No external packages required - built on Python's standard-library urllib via Mojo's seamless Python interop. The API mirrors the MongrelDB Python, Ruby, and Go clients.

Mojo CI License

Package

SurfacePackageInstall
Mojo clientmongreldbnot published to a registry - use from source (below)

Requirements

What It Provides

  • Typed CRUD over the Kit transaction endpoint: put, upsert (insert-or-update on PK conflict), delete by row id or primary key, all with optional idempotency keys for safe retries.
  • Fluent query builder that pushes conditions down to the engine's specialized indexes for sub-millisecond lookups: bitmap equality/IN, learned-range, null checks, FM-index full-text search, HNSW vector similarity (ann), and sparse vector match. Friendly aliases (column -> column_id, min/max -> lo/hi) are translated to the server's on-wire keys.
  • Idempotent batch transactions - operations staged locally and committed atomically, with the engine enforcing unique, foreign-key, and check constraints at commit time.
  • Full SQL access through the DataFusion-backed /sql endpoint. JSON mode (sql) decodes row arrays; sql_arrow requests raw Arrow IPC bytes (format: "arrow").
  • Schema management: typed table creation, full schema catalog, and per-table descriptors. Column dictionaries preserve scalar default_value and dynamic default_expr ("now" or "uuid").
  • User/role/credentials management via SQL: Argon2id-hashed catalog users, roles, and GRANT/REVOKE table-level permissions, all executed through sql.
  • Maintenance: compaction (all tables or per-table).
  • Auth: Bearer token (--auth-token mode) and HTTP Basic (--auth-users mode), with the bearer token taking precedence. Credentials are CRLF-validated to prevent header injection.
  • Typed error categories: MongrelDBError (base), AuthError (401/403), NotFoundError (404), ConflictError (409, with code + op index), and QueryError (everything else, including network failures) - raised as Error values whose message carries the category prefix.
  • Response size limit (256 MB) to guard client memory against a malicious or buggy server.

How it works

Mojo is a Python superset with seamless interop. This client calls Python's standard-library urllib.request, json, and base64 via Python.import_module

  • no third-party packages and no C FFI. All request/response logic (auth, error mapping, cell flattening, alias translation) is written in Mojo.

Install

The package is not published to a registry yet, so clone this repo and use it from source:

git clone https://github.com/visorcraft/MongrelDB-Mojo.git
cd MongrelDB-Mojo
mojo run -I src examples/basic_crud.mojo

The client package lives under src/mongreldb/; run your own programs against the checkout the same way, with mojo run -I src.

Examples

Task-focused, commented guides live in docs/:

  • Quickstart - install, start the daemon, write and run a complete program.
  • Transactions - batch commits, idempotency keys, constraint handling.
  • Queries - every native condition type and the index it pushes down to.
  • SQL - recursive CTEs, window functions, advanced SQL.
  • Authentication - Bearer token, HTTP Basic, and open modes.
  • Errors - the error categories and recovery patterns.

Quick Example

from python import Python, PythonObject
from collections import Optional
from mongreldb import MongrelDB

fn main() raises:
    var db = MongrelDB("http://127.0.0.1:8453")

    # Create a table. Column ids are stable on-wire identifiers.
    var columns = Python.list()
    columns.append(col(1, "id", "int64", primary_key=True))
    columns.append(col(2, "customer", "varchar"))
    columns.append(col(3, "amount", "float64"))
    var check = Python.dict()
    check["id"] = 1
    check["name"] = "id_present"
    var expr = Python.dict()
    expr["IsNotNull"] = 1
    check["expr"] = expr
    var constraints = Python.dict()
    var checks = Python.list()
    checks.append(check)
    constraints["checks"] = checks
    _ = db.create_table("orders", columns, constraints)

    _ = db.put("orders", cells3(1, 1, 2, "Alice", 3, 99.50))
    _ = db.put("orders", cells3(1, 2, 2, "Bob",   3, 150.00))

    var params = Python.dict()
    params["column"] = 3
    params["min"] = 100.0
    params["max"] = 200.0
    params["min_inclusive"] = True
    params["max_inclusive"] = True
    var q = db.query("orders").where("range_f64", params).limit(100)
    var rows = q.execute()
    print(db.count("orders"))   # 2

    _ = db.sql("UPDATE orders SET amount = 200.0 WHERE customer = 'Bob'")


def col(
    col_id: Int,
    name: String,
    ty: String,
    *,
    primary_key: Bool = False,
    default_value: Optional[PythonObject] = None,
) -> PythonObject:
    c = Python.dict()
    c["id"] = col_id
    c["name"] = name
    c["ty"] = ty
    c["primary_key"] = primary_key
    c["nullable"] = False
    if default_value:
        c["default_value"] = default_value.value()
    return c


def cells3(
    k1: PythonObject, v1: PythonObject,
    k2: PythonObject, v2: PythonObject,
    k3: PythonObject, v3: PythonObject,
) -> PythonObject:
    d = Python.dict()
    d[k1] = v1
    d[k2] = v2
    d[k3] = v3
    return d

Authentication

# Bearer token (--auth-token mode)
var db1 = MongrelDB("http://127.0.0.1:8453", "my-secret-token")

# HTTP Basic (--auth-users mode)
var db2 = MongrelDB("http://127.0.0.1:8453", "", "admin", "s3cret")

# Defaults: daemon address 127.0.0.1:8453, no auth.
var db3 = MongrelDB()

Batch transactions

var txn = db.begin()
_ = txn.put("orders", cells3(1, 10, 2, "Dave", 3, 50.0))
_ = txn.put("orders", cells3(1, 11, 2, "Eve",  3, 75.0))
_ = txn.delete_by_pk("orders", 2)

try:
    var results = txn.commit()   # atomic - all or nothing
except e:
    if String(e).contains("ConflictError"):
        print("constraint violated")

SQL

db.sql("INSERT INTO orders (id, customer, amount) VALUES (99, 'Zoe', 999.0)")
db.sql("CREATE TABLE archive AS SELECT * FROM orders WHERE amount > 500")

ANN index backends

The engine's ann index is swappable across three backends - hnsw (the default), diskann, and ivf - selected with the algorithm option. Quantization is independently configurable: dense, binary_sign, or product (product quantization, with num_subvectors, bits_per_subvector, pq_training_samples, pq_seed, and pq_rerank_factor). These are ordinary DDL strings run through sql, so no client changes are needed.

# DiskANN (in-memory Vamana graph)
db.sql("CREATE INDEX orders_emb_diskann ON orders USING ann (embedding) WITH (algorithm = 'diskann', quantization = 'dense', diskann_l = 50, diskann_r = 64, beam_width = 8)")

# IVF with dense vectors (clustered)
db.sql("CREATE INDEX orders_emb_ivf ON orders USING ann (embedding) WITH (algorithm = 'ivf', quantization = 'dense', nlist = 1024, nprobe = 16)")

# HNSW with product quantization (recall-tuned)
db.sql("CREATE INDEX orders_emb_hnsw_pq ON orders USING ann (embedding) WITH (algorithm = 'hnsw', quantization = 'product', m = 16, ef_construction = 200, ef_search = 50, num_subvectors = 32, pq_training_samples = 50000, pq_rerank_factor = 8)")

History retention

Control how far back time-travel queries can read. The window is measured in epochs (monotonically increasing commit numbers).

# Keep at least 1000 epochs of history readable.
var result = db.set_history_retention_epochs(1000)
print(result["history_retention_epochs"])  # 1000
print(result["earliest_retained_epoch"])   # oldest epoch still available

print(db.history_retention_epochs())       # 1000
print(db.earliest_retained_epoch())        # oldest readable epoch

# Read a table as it existed at a specific epoch.
var rows = db.sql("SELECT label FROM orders AS OF EPOCH 42 WHERE id = 1")

Raising retention prevents history from being garbage collected, but it cannot restore epochs that have already been pruned. These endpoints require admin privileges when the daemon runs with auth enabled.

Error handling

Every non-2xx response is mapped to a typed error category. Mojo raises only the built-in Error type, so the category, HTTP status, structured code, and op index are embedded in the error message - match on the category prefix:

try:
    db.put("orders", cells(1, 1))
except e:
    var msg = String(e)
    if msg.contains("ConflictError"):
        print("constraint violated")
    elif msg.contains("NotFoundError"):
        print("not found")
    else:
        print("query/server error: " + msg)

API reference

MongrelDB

MethodDescription
MongrelDB(url, token, username, password)Construct a client (url defaults to http://127.0.0.1:8453)
health() -> BoolCheck daemon health
table_names() -> List[String]List table names
create_table(name, columns, constraints, indexes) -> IntCreate a table with optional constraints and all index definitions
drop_table(name) -> NoneDrop a table
count(table) -> IntRow count
put(table, cells, idempotency_key) -> dictInsert a row
upsert(table, cells, update_cells, idempotency_key) -> dictUpsert a row
delete(table, row_id) -> NoneDelete by row id
delete_by_pk(table, pk) -> NoneDelete by primary key
query(table) -> QueryBuilderStart a native query
sql(sql) -> listExecute SQL (JSON mode)
sql_arrow(sql) -> BytesExecute SQL requesting raw Arrow IPC
schema() -> dictFull schema catalog
schema_for(table) -> dictSingle-table descriptor
set_history_retention_epochs(epochs) -> dictSet the history retention window
history_retention_epochs() -> IntGet the current retention window
earliest_retained_epoch() -> IntGet the oldest readable epoch
compact() -> dictCompact all tables
compact_table(table) -> dictCompact one table
begin() -> TransactionStart a batch

QueryBuilder

MethodDescription
where(cond_type, params) -> SelfAdd a native condition (AND-ed)
projection(column_ids) -> SelfSet column projection
limit(n) -> SelfSet row limit
offset(n) -> SelfSkip matching rows before the limit
build() -> dictBuild the request payload
execute() -> listRun the query
truncated() -> BoolWhether the last execute result hit the limit

Transaction

MethodDescription
put(table, cells, returning) -> SelfStage an insert
upsert(table, cells, update_cells, returning) -> SelfStage an upsert
delete(table, row_id) -> SelfStage a delete by row id
delete_by_pk(table, pk) -> SelfStage a delete by primary key
count() -> IntNumber of staged operations
commit(idempotency_key) -> listCommit atomically
rollback() -> NoneDiscard all operations

Errors

CategoryHTTP statusNotes
MongrelDBError-Base category for all client errors
AuthError401, 403Bad or missing credentials
NotFoundError404Missing table, schema, or resource
ConflictError409Constraint violation; carries code and op_index
QueryError400, 5xx, networkEverything else

Building and testing

The test suite is split into two layers:

  • Offline unit tests - query-builder alias translation, URL escaping, error mapping. No daemon needed.
  • Live integration tests - boots a real mongreldb-server daemon and exercises the full client surface (the 14-operation conformance matrix). Live tests skip cleanly when no binary is available.
mojo run -I src tests/live_test.mojo   # runs the whole suite

Fetch a prebuilt server binary from the MongrelDB releases:

mkdir -p bin
curl -fsSL -o bin/mongreldb-server \
  https://github.com/visorcraft/MongrelDB/releases/download/v0.64.16/mongreldb-server-linux-x64
chmod +x bin/mongreldb-server

The live harness resolves the binary in this order: the MONGRELDB_SERVER env var, ./bin/mongreldb-server, mongreldb-server on PATH. Or point it at an already-running daemon with MONGRELDB_URL.

Contributing

Contributions are welcome. Please:

  1. Open an issue first for non-trivial changes.
  2. Add focused tests near your change - the suite must stay green.
  3. Run mojo run -I src tests/live_test.mojo before submitting.
  4. Keep the client dependency-free (Python standard library only).
  • Mongrel — Commercial multi-system workbench with native MongrelDB support.
  • MongrelDB Viewer — Free, open-source MongrelDB GUI and MCP server.

License

Dual-licensed under the MIT License or the Apache License, Version 2.0, at your option. See MIT OR Apache-2.0 for the full text.

SPDX-License-Identifier: MIT OR Apache-2.0