Pure-Odin HTTP client for MongrelDB - embedded+server database with SQL, vector search, full-text search, and AI-native retrieval.
No package manager required - just source files. Talks to the daemon's JSON API over libcurl (via a small C FFI wrapper) and bundles a self-contained JSON layer. The API mirrors the MongrelDB PHP and Go clients.
| Surface | Package | Install |
|---|---|---|
| Odin client | mongreldb |
copy the mongreldb/ directory into your project, or add it as a git submodule |
- Odin (recent dev build) - this client uses
core:fmt,core:mem,core:strings,core:os, and C FFI - libcurl (the HTTP transport). On Debian/Ubuntu install
libcurl4-openssl-dev; on Fedoracurl-devel. The client links it viaforeign import lib "system:curl". - A running
mongreldb-serverdaemon
- Typed CRUD over the Kit transaction endpoint:
put(with optional idempotency keys for safe retries),upsert(insert-or-update on PK conflict), anddelete/delete_by_pkby row id or primary key. Cells are a{column_id, JSONValue}pair flattened to the server's on-wire[col_id, value, ...]array. - Fluent query builder that pushes conditions down to the engine's specialized indexes for sub-millisecond lookups: primary key, learned-range, bitmap equality, null checks, and FM-index full-text search. Friendly aliases (
column->column_id,min/max->lo/hi) are translated to the server's on-wire keys. - Idempotent batch transactions - operations staged locally on a
Transactionand committed atomically, with the engine enforcing unique, foreign-key, and check constraints at commit time. Idempotency keys return the original response on duplicate commits, even after a crash. - Full SQL access through the DataFusion-backed
/sqlendpoint (JSON format requested): recursive CTEs, window functions,CREATE TABLE AS SELECT, materialized views, and multi-statement execution. - Schema management: typed table creation, full schema catalog (
map[string]JSONValue), and per-table descriptors. - Typed errors: a single
Mongrel_Errorenum youswitchon -.Auth(401/403),.Not_Found(404),.Conflict(409),.Query(everything else non-2xx),.Http(transport),.Json(malformed response), plus.Response_Too_Largeand.Already_Committed. - Self-contained JSON layer - a local
JSONValueunion, ordered-object type, strict recursive-descent parser, and compact serializer. No dependency on any particular version ofcore:encoding/json.
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 typed error set and recovery patterns.
package main
import "core:fmt"
import m "mdb:mongreldb"
main :: proc() {
// Connect to a running mongreldb-server daemon.
db := m.connect("http://127.0.0.1:8453", m.Options{})
ok, err := m.health(db)
if err != .None_ || !ok {
fmt.eprintf("daemon not reachable: %s\n", m.mongrel_error_string(err))
return
}
// Create a table. Column ids are stable on-wire identifiers.
cols := []m.Column{
{id = 1, name = "id", ty = "int64", primary_key = true},
{id = 2, name = "customer", ty = "varchar"},
{id = 3, name = "amount", ty = "float64"},
}
tid, cerr := m.create_table(db, "orders", cols)
if cerr != .None_ { panic(m.mongrel_error_string(cerr)) }
// Insert rows (cells pair column id + value).
r := []m.Cell{
{1, m.int_value(1)},
{2, m.string_value("Alice")},
{3, m.float_value(99.5)},
}
_, perr := m.put(db, "orders", r, "")
if perr != .None_ { panic(m.mongrel_error_string(perr)) }
// Query with a native index condition (learned-range index).
qb := m.query(db, "orders")
defer m.free_query_builder(&qb)
m.where_(&qb, "range_f64", range_for(3, 100.0, 0.0))
rows, qerr := m.execute(&qb)
if qerr != .None_ { panic(m.mongrel_error_string(qerr)) }
fmt.printf("rows: %d\n", len(rows))
n, _ := m.count(db, "orders")
fmt.printf("count: %lld\n", n) // 1
}
// range_for builds a range condition payload {"column": id, "min": lo}.
range_for :: proc(id: i64, lo, hi: f64) -> m.JSONObject {
o := m.json_object_make()
m.json_object_set(&o, "column", m.int_value(id))
m.json_object_set(&o, "min", m.float_value(lo))
if hi > 0 {
m.json_object_set(&o, "max", m.float_value(hi))
}
return o
}// Bearer token (--auth-token mode)
db := m.connect("http://127.0.0.1:8453", m.Options{token = "my-secret-token"})
// HTTP Basic (--auth-users mode)
db := m.connect("http://127.0.0.1:8453", m.Options{
username = "admin",
password = "s3cret",
})A Bearer token takes precedence over Basic credentials when both are supplied. The client guards against CR/LF in credentials to prevent request-smuggling through the auth header.
Operations are staged locally on a Transaction and committed atomically. The engine enforces unique, foreign-key, and check constraints at commit time.
txn := m.begin(db)
defer m.free_transaction(&txn)
cells := []m.Cell{{1, m.int_value(10)}, {2, m.string_value("Dave")}}
m.txn_put(&txn, "orders", cells, false)
// atomic - all or nothing. The idempotency key makes it safe to retry.
results, err := m.commit(&txn, "charge-order-123")
if err == .Conflict {
// constraint violated - the engine already rolled back the whole batch.
}Conditions push down to the engine's specialized indexes. The builder accepts friendly aliases that are translated to the server's on-wire keys: column (-> column_id), min/max (-> lo/hi).
// Range query (learned-range index).
q := m.query(db, "orders")
defer m.free_query_builder(&q)
cond := m.json_object_make()
m.json_object_set(&cond, "column", m.int_value(3))
m.json_object_set(&cond, "min", m.float_value(50.0))
m.where_(&q, "range_f64", cond)
m.limit_(&q, 100)
rows, err := m.execute(&q)
// Primary-key lookup (the fastest path).
pk := m.query(db, "orders")
defer m.free_query_builder(&pk)
pk_cond := m.json_object_make()
m.json_object_set(&pk_cond, "value", m.int_value(42))
m.where_(&pk, "pk", pk_cond)Query rows come back as JSONValue objects, each {"row_id": ..., "cells": [col_id, value, ...]}. Walk the flat cells array in pairs to read values by column id.
_, err := m.sql(db, "INSERT INTO orders (id, customer, amount) VALUES (99, 'Zoe', 999.0)")
_, err = m.sql(db, "CREATE TABLE archive AS SELECT * FROM orders WHERE amount > 500")
// Recursive CTEs and window functions
_, err = m.sql(db,
"WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM r WHERE n<10) SELECT n FROM r")The /sql endpoint is requested in JSON format. For statements that yield no rows (DDL/DML) sql returns an empty slice with no error.
Every non-2xx response is mapped to a typed error. switch on the variant.
_, err := m.schema_for(db, "missing_table")
switch err {
case .None_: fmt.println("ok")
case .Not_Found: fmt.eprintln("not found")
case .Conflict: fmt.eprintln("constraint violation")
case .Auth: fmt.eprintln("not authorized")
case: fmt.eprintf("query/server error: %s\n", m.mongrel_error_string(err))
}| HTTP status | Error |
|---|---|
| 401, 403 | .Auth |
| 404 | .Not_Found |
| 402, 409 | .Conflict |
| 400, other non-2xx | .Query |
| 3xx, 5xx | .Http |
| transport failure | .Http |
| malformed JSON | .Json |
| body > 256 MB | .Response_Too_Large |
| commit/rollback on a spent transaction | .Already_Committed |
| Procedure | Description |
|---|---|
connect(url, options) -> Client |
Construct a client (url defaults to http://127.0.0.1:8453) |
health(db) -> (bool, Mongrel_Error) |
Check daemon health |
table_names(db) -> ([]string, Mongrel_Error) |
List table names |
create_table(db, name, columns) -> (i64, Mongrel_Error) |
Create a table; returns the table id |
create_table_with_constraints(db, name, columns, constraints) -> (i64, Mongrel_Error) |
Create a table and attach the top-level engine constraints JSON object |
drop_table(db, name) -> Mongrel_Error |
Drop a table |
count(db, table) -> (i64, Mongrel_Error) |
Row count |
put(db, table, cells, key) -> (JSONValue, Mongrel_Error) |
Insert a row |
upsert(db, table, cells, update_cells, key) -> (JSONValue, Mongrel_Error) |
Insert or update on PK conflict |
delete(db, table, row_id) -> Mongrel_Error |
Delete by row id |
delete_by_pk(db, table, pk) -> Mongrel_Error |
Delete by primary key |
query(db, table) -> QueryBuilder |
Start a native query |
begin(db) -> Transaction |
Start a batch |
sql(db, sql) -> ([]JSONValue, Mongrel_Error) |
Execute SQL |
schema(db) -> (map[string]JSONValue, Mongrel_Error) |
Full schema catalog |
schema_for(db, table) -> (JSONValue, Mongrel_Error) |
Single-table descriptor |
| Procedure | Description |
|---|---|
where_(qb, type, params) -> ^QueryBuilder |
Add a native condition (AND-ed) |
projection(qb, column_ids) -> ^QueryBuilder |
Set column projection |
limit_(qb, n) -> ^QueryBuilder |
Set row limit |
execute(qb) -> ([]JSONValue, Mongrel_Error) |
Run the query; returns the rows |
free_query_builder(qb) |
Release the builder's dynamic storage |
| Procedure | Description |
|---|---|
txn_put(t, table, cells, returning) -> (^Transaction, Mongrel_Error) |
Stage an insert |
txn_delete(t, table, row_id) -> (^Transaction, Mongrel_Error) |
Stage a delete by row id |
txn_delete_by_pk(t, table, pk) -> (^Transaction, Mongrel_Error) |
Stage a delete by primary key |
txn_count(t) -> int |
Number of staged operations |
commit(t, key) -> ([]JSONValue, Mongrel_Error) |
Commit atomically |
rollback(t) -> Mongrel_Error |
Discard all operations |
free_transaction(t) |
Release the transaction's dynamic storage |
| Procedure | Description |
|---|---|
int_value(i64) -> JSONValue |
Integer cell value |
float_value(f64) -> JSONValue |
Float cell value |
string_value(string, allocator = context.allocator) -> JSONValue |
String cell value; clones s into the allocator so the value owns its storage |
bool_value(bool) -> JSONValue |
Boolean cell value |
null_value() -> JSONValue |
Null cell value |
Strings in JSONValues must be heap-owned. Never embed a string literal or borrowed slice directly in a
JSONValue-json_destroywould try to free non-heap memory. Usestring_value(orm.jstrinside the library) for any string placed in a value tree.
Odin has no package manager; the library is the mongreldb/ directory (the package is declared as package mongreldb). Examples and tests import it through a collection named mdb that points at the repo root, so import "mdb:mongreldb" resolves to the mongreldb/ directory:
# Build the library alone (compiles the whole package).
odin build mongreldb -build-mode:lib -vet
# Run the test suite (`odin test` discovers every @(test) proc).
# Live tests self-skip without a daemon; the wire-shape tests always run.
odin test tests -collection:mdb=. -vet
# Build and run an example (each example is its own package; use -file).
odin run examples/basic_crud.odin -file -collection:mdb=. -out:bin/basic_crud
./bin/basic_crudThe test suite is a live integration suite: against a running mongreldb-server daemon it exercises the full client surface (a 14-operation conformance matrix). It also carries a pure wire-shape test that needs no daemon. Live tests self-skip when no daemon is reachable, so odin test tests is safe to run offline.
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.46.2/mongreldb-server-linux-x64
chmod +x bin/mongreldb-serverCopy the mongreldb/ directory (the three .odin files) into your project, or add this repo as a git submodule, then point a collection at the directory that contains mongreldb/ so the package resolves:
git submodule add https://github.com/visorcraft/MongrelDB-Odin.git vendor
# then build with (the collection points at the parent of mongreldb/):
odin build your_app.odin -collection:mdb=vendorIn your code, import the package through the collection:
import m "mdb:mongreldb"Contributions are welcome. Please:
- Open an issue first for non-trivial changes.
- Add focused tests near your change - the suite must stay green.
- Keep the client a thin wrapper over
mongreldb-server. - Match the existing style: tabs for indentation,
snake_casewith a trailing underscore where a name would otherwise clash with a keyword (where_,limit_).
See CONTRIBUTING.md for the full guide.
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
