Quickstart

July 31, 2026 ยท View on GitHub

Zero to a running MongrelDB D program in fifteen minutes. This guide assumes a fresh machine and walks through installing the prerequisites, starting the daemon, and writing, running, and understanding a complete program.


1. Prerequisites

You need two things installed: a D toolchain and a mongreldb-server daemon.

Install D 2.100 or newer

Any recent D compiler works (LDC or DMD). Verify it:

dmd --version
# or
ldc2 --version

If you do not have it, install from https://dlang.org/download.html or your package manager (e.g. pacman -S dlang, brew install ldc). The D package manager dub ships with the compiler.

Install mongreldb-server

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.63.1/mongreldb-server-linux-x64
chmod +x bin/mongreldb-server

Verify it runs:

./bin/mongreldb-server --version

2. Start the daemon

By default mongreldb-server listens on http://127.0.0.1:8453 and stores data in the current working directory.

mkdir -p /tmp/mdb-data && cd /tmp/mdb-data
/path/to/mongreldb-server

In another terminal, sanity-check it:

curl http://127.0.0.1:8453/health
# ok

Leave the daemon running for the rest of this guide.

3. Create a project and pull in the client

The client is not on code.dlang.org yet, so clone the repo and register it as a local DUB package (it is pure Phobos, so there is nothing else to fetch):

git clone https://github.com/visorcraft/MongrelDB-D.git
dub add-local MongrelDB-D

mkdir demo && cd demo
dub init

Then add mongreldb to the dependencies in dub.json; the locally registered checkout satisfies the version constraint. Alternatively, skip dub add-local and point a path dependency directly at the checkout:

{
    "dependencies": {
        "mongreldb": {"path": "../MongrelDB-D"}
    }
}

4. Write your first program

Create source/app.d:

import mongreldb;
import std.stdio;
import std.json;

void main()
{
    // 1. Connect to the daemon. Empty URL falls back to http://127.0.0.1:8453.
    auto db = new MongrelDBClient("http://127.0.0.1:8453");

    // 2. Health check before doing anything else.
    if (!db.health())
    {
        writeln("daemon not reachable");
        return;
    }

    // 3. Create a table. Each Column has a stable numeric id, a name, a type,
    //    and flags. The first column is the primary key. Two optional
    //    trailing fields are constraint hints:
    //      - enum_variants : a closed set of allowed string values for a
    //                        column declared with ty="enum".
    //      - default_value : a server-side fill ("now" or "uuid") applied
    //                        when an insert omits the column.
    long tid = db.createTable("orders", [
        Column(1, "id",       "int64",   true,  false),
        Column(2, "customer", "varchar", false, false),
        Column(3, "amount",   "float64", false, false),
        Column(4, "status",   "enum",    false, false,
                cast(string[])["pending", "shipped", "cancelled"], ""),
        Column(5, "note",     "varchar", false, false,
                cast(string[])[], ""),
    ]);
    writeln("created table id: ", tid);

    // 4. Insert rows. Cell.of pairs a column id with a value. put() is a
    //    one-op transaction; the optional third argument is an idempotency key.
    //    The `status` cell is required (enum, no default); the `note` cell
    //    is optional and the server leaves it unset when omitted.
    db.put("orders", [Cell.of(1, 1L), Cell.of(2, "Alice"), Cell.of(3, 99.50),
                      Cell.of(4, "shipped"), Cell.of(5, "priority")]);
    db.put("orders", [Cell.of(1, 2L), Cell.of(2, "Bob"),   Cell.of(3, 150.00),
                      Cell.of(4, "pending")]);

    // 5. Query with a native index condition. The range index serves this in
    //    sub-millisecond. projection() selects only column ids 1 and 2.
    auto q = db.query("orders")
        .where("range", parseJSON(`{"column": 3, "min": 100.0}`))
        .projection([1L, 2L])
        .limit(100);
    auto rows = q.execute();
    foreach (row; rows)
    {
        writeln("row: ", row);
    }

    // 6. Count the rows.
    writeln("total rows: ", db.count("orders"));
}

Run it:

dub run

You should see:

created table id: 1
row: {"1":2,"2":"Bob"}
total rows: 2

5. What each part does

CodeWhat it does
new MongrelDBClient(url)Builds an HTTP client targeting one daemon. Safe to share across fibers/threads.
db.health()GET /health; returns true when the daemon answers. Always check before real work.
db.createTable(name, cols) / db.createTable(name, cols, constraints)POST /kit/create_table. Column ids are the on-wire identifiers; use them everywhere else. Trailing enum_variants and default_value fields encode closed-string and server-side default hints; the third argument forwards native table constraints.
db.put(table, cells)Single-op transaction: POST /kit/txn with one put op. cells is flattened to [col_id, val, ...].
db.query(table).where(...)Builds a /kit/query body. where pushes a condition down to a native index.
.projection([1L, 2L])Server returns only those column ids, saving bandwidth.
.limit(100)Caps the result; check q.truncated afterward to detect overflow.
.execute()Sends the query and decodes the rows array.
db.count(table)GET /tables/{name}/count.

6. Column constraints

Column carries two optional server-side hints alongside its name, type, and flags. Both default to "absent" - existing callers that never set them keep producing byte-identical wire payloads.

enum_variants - closed-string columns

When ty is "enum" and enum_variants is a non-empty list, the engine treats the column as a closed set of allowed string values. Anything outside the list is rejected at insert / commit time:

Column(4, "status", "enum", false, false,
        cast(string[])["pending", "shipped", "cancelled"], "")

The server returns HTTP 400 "enum column requires non-empty enum_variants" if you declare ty = "enum" with an empty list. An empty enum_variants on a non-enum column is silently dropped from the wire and treated as "any string".

Legacy default_value - literal string defaults

The trailing default_value is treated as a literal string default applied at insert stage time when an insert omits the column or supplies it as Null:

ValueEffect
""Absent from the wire - no default is configured.
any stringSerialized as a JSON string literal.

For typed static defaults (numbers, booleans, explicit null) use default_value_json; for dynamic server-side generators ("now" or "uuid") use default_expr, which takes precedence over both default-value fields.

Putting it together

A constraint-bearing table with both fields, including a row that exercises the defaults, lives in ../examples/column_constraints.d.

Typed static and dynamic defaults

For literal defaults of any JSON scalar type, set default_value_json to the raw JSON text. For server-side generators ("now", "uuid"), set default_expr, which takes precedence over both default-value fields.

// A single table demonstrating every default shape.
auto colMsg = Column(2, "msg",   "varchar");
colMsg.default_value_json = `"hello"`;
auto colCount = Column(3, "count", "int64");
colCount.default_value_json = `0`;
auto colOk = Column(4, "ok",    "bool");
colOk.default_value_json = `true`;
auto colMeta = Column(5, "meta",  "varchar");
colMeta.default_value_json = `null`;
auto colTag = Column(6, "tag",   "varchar");
colTag.default_value_json = `"now"`;
auto colTs = Column(7, "ts",    "timestamp_nanos");
colTs.default_expr = "now";

db.createTable("events", [
    Column(1, "id", "int64", true, false),
    colMsg, colCount, colOk, colMeta, colTag, colTs,
]);

The legacy string default_value is still supported, but default_value_json makes numeric, boolean, and null defaults unambiguous.

7. History retention

The daemon retains a window of recent committed epochs so you can query older snapshots with SQL AS OF EPOCH. The default window is 1024 epochs.

auto settings = db.historyRetention();
writeln("retained epochs: ", settings.historyRetentionEpochs);
writeln("earliest epoch:  ", settings.earliestRetainedEpoch);

// Expand or shrink the window. When catalog auth is enabled, the caller must
// hold the ADMIN role.
db.setHistoryRetentionEpochs(100);

You cannot restore history that has already been pruned: raising the window only affects future epochs. Query a retained snapshot like this:

// value_at_insert_epoch is the value as of the captured epoch.
auto rows = db.sql(format!"SELECT value FROM events AS OF EPOCH %d WHERE id = 1"(
        insertEpoch));

8. Common pitfalls

Using the column name instead of the column id. Every on-wire API uses the numeric id from createTable, never the name. The query builder's column alias maps to the server's column_id - pass the integer id, not the string name:

// Wrong:
.where("range", parseJSON(`{"column": "amount", "min": 100.0}`))
// Right:
.where("range", parseJSON(`{"column": 3, "min": 100.0}`))

Treating a single put as non-transactional. put is a one-op transaction. A unique constraint violation surfaces as a ConflictException (HTTP 409), not as a silent no-op.

Calling commit twice on the same Transaction. The second call throws Exception("mongreldb: transaction already committed"). Create a fresh db.begin() for each logical unit of work.

Reusing a QueryBuilder and expecting a fresh truncated. truncated reflects the most recent execute(). Build a new query, or re-run execute() before reading it.

Expecting sql to always return rows. sql requests format: "json", so a SELECT returns its rows decoded into a JSONValue[]. Statements that produce no rows (DDL/DML, or an empty result set) return an empty array (not an error).

Pointing at a daemon that requires auth. If the daemon was started with --auth-token or --auth-users, every call raises AuthException unless you pass token or username/password to the constructor. See auth.md.

Next steps