Quickstart

July 21, 2026 ยท View on GitHub

Column maps pass schema keys through unchanged. default_value accepts any JSON scalar; supply the type expected by the column. Use default_expr -> "now" or "uuid" for a dynamic default.

Zero to a running MongrelDB Scala program. This guide assumes a fresh machine and walks through installing the prerequisites, starting the daemon, and writing a complete program.


1. Prerequisites

You need the Scala/Java toolchain and a mongreldb-server daemon.

Install Java 11+ and sbt

java -version    # 11 or newer
sbt sbtVersion   # sbt 1.10+

If you do not have them, install from your package manager or https://www.scala-lang.org/download/.

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

2. Start the daemon

By default mongreldb-server listens on http://127.0.0.1:8453.

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

Sanity-check it:

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

3. Create a project and pull in the client

sbt

// build.sbt
scalaVersion := "3.3.3"
libraryDependencies += "com.visorcraft" %% "mongreldb-scala" % "0.62.0"

scala-cli

//> using scala 3.3.3
//> using dep com.visorcraft::mongreldb-scala:0.62.0

4. Write your first program

import com.visorcraft.mongreldb.MongrelDB

object Demo:
  def main(args: Array[String]): Unit =
    val db = MongrelDB("http://127.0.0.1:8453")

    if !db.health then
      System.err.println("daemon not reachable")
      sys.exit(1)

    val tid = db.createTable("orders", List(
      Map("id" -> 1, "name" -> "id",       "ty" -> "int64",   "primary_key" -> true,  "nullable" -> false),
      Map("id" -> 2, "name" -> "customer", "ty" -> "varchar", "primary_key" -> false, "nullable" -> false),
      Map("id" -> 3, "name" -> "amount",   "ty" -> "float64", "primary_key" -> false, "nullable" -> false),
    ))
    println(s"created table id: $tid")

    db.put("orders", Map(1L -> 1L, 2L -> "Alice", 3L -> 99.5))
    db.put("orders", Map(1L -> 2L, 2L -> "Bob",   3L -> 150.0))

    val rows = db.query("orders")
      .where("range", Map("column" -> 3L, "min" -> 100L))
      .projection(List(1L, 2L))
      .limit(100L)
      .execute()
    rows.foreach(println)

    println(s"total rows: ${db.count("orders")}")

Run it:

scala-cli run demo.scala

5. What each part does

CodeWhat it does
MongrelDB(url)Builds an HTTP client targeting one daemon. Safe to share across threads.
db.healthGET /health; returns true when the daemon answers.
db.createTable(name, cols)POST /kit/create_table. Column ids are the on-wire identifiers.
db.put(table, cells)Single-op transaction: POST /kit/txn with one put op.
db.query(table).where(...)Builds a /kit/query body that pushes a condition down to a native index.
.executeSends the query and decodes the rows array.
db.count(table)GET /tables/{name}/count.

6. History retention and time travel

MongrelDB keeps a durable MVCC history window. You can inspect it, widen it, and query older epochs with AS OF EPOCH.

println(db.historyRetentionEpochs)  // current window, e.g. 100
println(db.earliestRetainedEpoch)   // oldest readable epoch, e.g. 3

// Widen the window. The response contains the updated values.
val resp = db.setHistoryRetentionEpochs(1_000L)
println(resp("history_retention_epochs")) // 1000

// Read the table as it existed at epoch 5.
val rows = db.sql("SELECT id, amount FROM orders AS OF EPOCH 5")

Increasing retention cannot restore history that has already been pruned. The window is a durable GC/time-travel policy, so it requires admin privileges when the daemon is running with auth.

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 integer id:

// Wrong:
.where("range", Map("column" -> "amount", "min" -> 100L))
// Right:
.where("range", Map("column" -> 3L, "min" -> 100L))

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

Calling commit twice on the same Transaction. The second call throws IllegalStateException. Create a fresh db.begin() for each logical unit.

Expecting sql to always return rows. The /sql endpoint streams Arrow IPC for SELECT in most builds, so sql returns an empty list. Use the native query builder for typed row retrieval; use sql for DDL/DML.

Next steps