Quickstart

July 21, 2026 ยท View on GitHub

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


1. Prerequisites

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

Install JDK 11 or newer

The client targets JVM 11 bytecode and has no external dependencies, so any JDK 11+ works. Verify it:

java -version
# openjdk version "11.0.x" (or newer)

If you do not have it, install from https://adoptium.net/ or your package manager (for example pacman -S jdk-openjdk, brew install openjdk@21).

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.62.0/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

With Gradle (Kotlin DSL)

Add the dependency to build.gradle.kts:

dependencies {
    implementation("com.visorcraft:mongreldb-kotlin:0.62.0")
}

Make sure your Kotlin compiler targets JVM 11 or newer:

kotlin {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
    }
}

With Gradle (Groovy DSL)

implementation 'com.visorcraft:mongreldb-kotlin:0.62.0'

With Maven

<dependency>
  <groupId>com.visorcraft</groupId>
  <artifactId>mongreldb-kotlin</artifactId>
  <version>0.62.0</version>
</dependency>

The artifact pulls in the Kotlin standard library transitively and nothing else.

4. Write your first program

Create src/main/kotlin/com/example/Main.kt:

package com.example

import com.visorcraft.mongreldb.MongrelDB

fun main() {
    // 1. Connect to the daemon. A null/blank url defaults to
    //    http://127.0.0.1:8453.
    val db = MongrelDB("http://127.0.0.1:8453")

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

    // 3. Create a table. Each column is a Map with id, name, ty, and flags.
    //    The first column is the primary key. Column ids are stable on-wire
    //    identifiers - use them everywhere else.
    val tableId = db.createTable(
        "orders",
        listOf(
            column(1L, "id", "int64", primaryKey = true),
            column(2L, "customer", "varchar", primaryKey = false),
            column(3L, "amount", "float64", primaryKey = false),
            // Optional column keys ride through verbatim: enum_variants
            // constrains the value set; default_value fills an omitted cell.
            enumColumn(4L, "status", listOf("open", "shipped", "closed"), default = "open"),
        ),
    )
    println("created table id: $tableId")

    // 4. Insert rows. cells maps column id (Long) -> value. A null
    //    idempotency key is fine for a one-shot demo.
    db.put("orders", mapOf(1L to 1L, 2L to "Alice", 3L to 99.5))
    db.put("orders", mapOf(1L to 2L, 2L to "Bob", 3L to 150.0))

    // 5. Query with a native index condition. The range index serves this in
    //    sub-millisecond. Projection selects only column ids 1 and 2.
    val rows = db.query("orders")
        .where("range", mapOf("column" to 3L, "min" to 100.0))
        .projection(listOf(1L, 2L))
        .limit(100)
        .execute()
    for (row in rows) {
        println("row: $row")
    }

    // 6. Count the rows.
    println("total rows: ${db.count("orders")}")
}

/** Builds a column descriptor Map for createTable. */
private fun column(id: Long, name: String, ty: String, primaryKey: Boolean): Map<String, Any?> =
    mapOf(
        "id" to id,
        "name" to name,
        "ty" to ty,
        "primary_key" to primaryKey,
        "nullable" to false,
    )

/**
 * Builds an enum-constrained column with a server-side default. The
 * `enum_variants`, `default_value`, and `default_expr` keys are optional
 * column descriptor fields; createTable forwards every key to the daemon
 * verbatim, so a column can carry any server-supported attribute without a
 * client API change.
 *
 * `default_value` accepts a static scalar: string, integer, boolean, explicit
 * null, or the literal strings "now"/"uuid". Dynamic defaults use
 * `default_expr` instead ("now" or "uuid"). When both are present,
 * `default_expr` takes precedence on the server.
 */
private fun enumColumn(
    id: Long,
    name: String,
    variants: List<String>,
    default: String,
): Map<String, Any?> =
    mapOf(
        "id" to id,
        "name" to name,
        "ty" to "int32",
        "primary_key" to false,
        "nullable" to false,
        "enum_variants" to variants,
        "default_value" to default,
    )

Run it (Gradle):

./gradlew run -q --main-class=com.example.Main

You should see:

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

5. What each part does

CodeWhat it does
MongrelDB(url)Builds a client targeting one daemon. Thread-safe once constructed.
db.health()GET /health; returns true when the daemon answers. Always check before real work.
db.createTable(name, columns[, constraints])POST /kit/create_table; optional constraints map carries engine checks. Column ids are the on-wire identifiers; use them everywhere else. Extra descriptor keys such as enum_variants, default_value, and default_expr are forwarded verbatim.
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(listOf(1L, 2L))Server returns only those column ids, saving bandwidth.
.limit(100)Caps the result; check builder.truncated afterward to detect overflow.
.execute()Sends the query and decodes the rows list.
db.count(table)GET /tables/{name}/count.

6. History retention and time travel

MongrelDB keeps a configurable number of historical epochs. You can read older versions of a table with AS OF EPOCH as long as the epoch is not older than the retention floor.

// Keep 100 epochs of history.
db.setHistoryRetentionEpochs(100L)
println(db.historyRetentionEpochs())  // 100
println(db.earliestRetainedEpoch())   // earliest epoch still readable

// Pin the current epoch after a write (the server exposes
// POST /tables/{name}/commit), then update the row.
db.put("orders", mapOf(1L to 1L, 2L to "Alice", 3L to 99.5))
val epoch = 5L // obtained from POST /tables/orders/commit

db.sql("UPDATE orders SET customer = 'Bob' WHERE id = 1")

// Read the older version.
val old = db.sql("SELECT customer FROM orders AS OF EPOCH $epoch WHERE id = 1")
println(old)

Retention requires ADMIN permission when the daemon uses auth. Raising the retention window cannot recover history already pruned by a previously smaller window.

7. 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 Long id, not the string name:

// Wrong:
.where("range", mapOf("column" to "amount", "min" to 100.0))
// Right:
.where("range", mapOf("column" to 3L, "min" to 100.0))

Using an Int where a Long column id is expected. Column ids are Long. Writing mapOf(1 to "Alice") boxes the key to Int, which the flatten step will not accept as a column id. Always use the L suffix: 1L, 2L, ...

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

Calling commit twice on the same Transaction. The second call throws IllegalStateException. Create a fresh db.beginTransaction() 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 sends format: "json" and returns the parsed rows for SELECT, but it returns an empty list (not an exception) for DDL/DML statements that produce no rows. Treat an empty list as "the statement succeeded but had nothing to return," not as a failure.

Pointing at a daemon that requires auth. If the daemon was started with --auth-token or --auth-users, every call throws AuthException unless you construct the client with a token or Basic credentials. See auth.md.

Next steps

  • transactions.md - atomic batches, idempotency, retries
  • queries.md - every native index condition
  • sql.md - recursive CTEs, window functions, CREATE TABLE AS SELECT
  • auth.md - bearer tokens, basic auth, user/role management
  • errors.md - the full exception hierarchy and recovery patterns