Quickstart
August 11, 2026 ยท View on GitHub
Zero to a running MongrelDB Ruby 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: the Ruby toolchain and a mongreldb-server
daemon.
Install Ruby 3.0 or newer
MongrelDB Ruby is standard-library only, so any recent Ruby works. Verify it:
ruby -v
# ruby 3.x.x ...
If you do not have it, install from https://www.ruby-lang.org/en/downloads/
or your package manager (e.g. pacman -S ruby, brew install ruby).
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 gem is not yet published to RubyGems, so install it from the git
repository. Add it to a Gemfile and run bundle install:
# Gemfile
source "https://rubygems.org"
gem "mongreldb", git: "https://github.com/visorcraft/MongrelDB-Ruby.git"
Or build and install from a clone:
git clone https://github.com/visorcraft/MongrelDB-Ruby.git
cd MongrelDB-Ruby
gem build mongreldb.gemspec
gem install ./mongreldb-0.64.16.gem
4. Write your first program
Create demo.rb:
require "mongreldb"
# 1. Connect to the daemon. Empty/omitted URL falls back to http://127.0.0.1:8453.
db = MongrelDB::Client.new(url: "http://127.0.0.1:8453")
# 2. Health check before doing anything else.
unless db.health
warn "daemon not reachable"
exit 1
end
# 3. Create a table. Each column has a stable numeric id, a name, a type, and
# flags. The first column is the primary key.
tid = db.create_table("orders", [
{ "id" => 1, "name" => "id", "ty" => "int64", "primary_key" => true, "nullable" => false },
{ "id" => 2, "name" => "customer", "ty" => "varchar", "primary_key" => false, "nullable" => false },
{ "id" => 3, "name" => "amount", "ty" => "float64", "primary_key" => false, "nullable" => false },
])
puts "created table id: #{tid}"
# 4. Insert rows. Cells maps column id -> value. nil means "no idempotency key".
db.put("orders", { 1 => 1, 2 => "Alice", 3 => 99.5 })
db.put("orders", { 1 => 2, 2 => "Bob", 3 => 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.
rows = db.query("orders")
.where("range", "column" => 3, "min" => 100.0)
.projection([1, 2])
.limit(100)
.execute
rows.each { |row| puts "row: #{row.inspect}" }
# 6. Count the rows.
puts "total rows: #{db.count('orders')}"
Run it:
ruby demo.rb
You should see:
created table id: 1
row: {"1"=>2, "2"=>"Bob"}
total rows: 2
5. What each part does
| Code | What it does |
|---|---|
MongrelDB::Client.new(url) | Builds an HTTP client targeting one daemon. Safe to share across threads. |
db.health | GET /health; returns true when the daemon answers. Always check before real work. |
db.create_table(name, cols) | POST /kit/create_table. Column ids are the on-wire identifiers; use them everywhere else. |
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([1, 2]) | 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. Typed columns: enums and defaults
The column hashes passed to create_table are forwarded to the daemon
verbatim, so any column-level constraint the engine supports is just another
key in the hash. Two useful ones:
enum_variants(Array<String>) - restricts anenumcolumn to a fixed set of string values. The engine rejects writes that fall outside the set.default_value(String,Integer, etc.) - the value written into the column when a row omits it. The engine-side default is applied before any client-side default.default_expr("now"or"uuid") - a dynamic server-side default.
Literal "now" and "uuid" strings are expressed through default_value
like any other static string; use default_expr only when you want the dynamic
server-side behavior.
Supply default_value using the JSON scalar type expected by the column.
db.create_table("orders", [
{ "id" => 1, "name" => "id", "ty" => "int64", "primary_key" => true, "nullable" => false },
{ "id" => 2, "name" => "status", "ty" => "enum",
"enum_variants" => ["draft", "active", "archived"],
"default_value" => "draft",
"primary_key" => false, "nullable" => false },
{ "id" => 3, "name" => "amount", "ty" => "float64",
"default_value" => 0,
"primary_key" => false, "nullable" => false },
])
# Omitting the status column falls back to the engine-side default ("draft").
db.put("orders", { 1 => 1, 3 => 99.50 })
Keys that are not set on a column are omitted from the request body - no
null placeholders are sent. The wire-shape conformance test at
spec/create_table_wire_shape_spec.rb
guards these keys against silent renames.
7. 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.
puts db.history_retention_epochs # current window, e.g. 100
puts db.earliest_retained_epoch # oldest readable epoch, e.g. 3
# Widen the window. The response contains the updated values.
resp = db.set_history_retention_epochs(1_000)
puts resp["history_retention_epochs"] # => 1000
# Read the table as it existed at epoch 5.
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.
8. Common pitfalls
Using the column name instead of the column id. Every on-wire API uses the
numeric id from create_table, 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", "column" => "amount", "min" => 100.0)
# Right:
.where("range", "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 ConflictError
(HTTP 409), not as a silent no-op.
Calling commit twice on the same Transaction. The second call raises
RuntimeError: transaction already committed. Create a fresh
db.begin_transaction 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. The /sql endpoint streams Arrow
IPC for SELECT in most builds, so sql returns an empty array (not an
error) for result sets. Use it for DDL/DML and statements whose success is the
signal; use the native query builder for typed row retrieval.
Pointing at a daemon that requires auth. If the daemon was started with
--auth-token or --auth-users, every call raises AuthError unless you pass
token: or username:/password:. 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 error hierarchy and recovery patterns