Quickstart
July 21, 2026 ยท View on GitHub
Zero to a running MongrelDB Swift 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 Swift toolchain and a mongreldb-server
daemon.
Install Swift 5.9 or newer
MongrelDB Swift is standard-library + Foundation only, so any recent Swift toolchain works. Verify it:
swift --version
# Swift 5.9.x ...
If you do not have it, install from https://swift.org/install/ or your
package manager (e.g. pacman -S swift, brew install swift).
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
Create a new executable package and add the dependency:
mkdir MDBDemo && cd MDBDemo
swift package init --type executable
Edit Package.swift:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MDBDemo",
dependencies: [
.package(url: "https://github.com/visorcraft/MongrelDB-Swift.git", from: "0.62.0")
],
targets: [
.executableTarget(
name: "MDBDemo",
dependencies: ["MongrelDB"]
)
]
)
Resolve the dependency:
swift package resolve
4. Write your first program
Replace Sources/MDBDemo/main.swift:
import MongrelDB
@main
struct Demo {
static func main() async {
// 1. Connect to the daemon. nil baseURL falls back to http://127.0.0.1:8453.
let db = MongrelDBClient(baseURL: "http://127.0.0.1:8453")
// 2. Health check before doing anything else.
guard await db.health() else {
fatalError("daemon not reachable")
}
do {
// 3. Create a table. Each column has a stable numeric id, a name,
// a type, and flags. The first column is the primary key.
// Every column dict is sent verbatim to the engine, so any
// extra key it understands (enum_variants, default_value, ...)
// passes through untouched.
let tid = try await db.createTable("orders", columns: [
["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": "status", "ty": "enum",
"enum_variants": ["draft", "open", "closed"],
"default_value": "draft",
],
["id": 4, "name": "amount", "ty": "float64", "primary_key": false, "nullable": false],
])
print("created table id: \(tid)")
// Every column dictionary is sent verbatim, so the engine sees all
// keys you supply: `enum_variants`, `default_value` for static
// defaults, and `default_expr: "now"` / `"uuid"` for dynamic
// defaults. Literal "now" / "uuid" strings use `default_value`.
// 4. Insert rows. Cells maps column id -> value.
_ = try await db.put("orders", cells: [1: 1, 2: "Alice", 3: "open", 4: 99.5])
_ = try await db.put("orders", cells: [1: 2, 2: "Bob", 3: "open", 4: 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.
let rows: [[String: Any]] = try await db.query("orders")
.where("range", params: ["column": 4, "min": 100.0])
.projection([1, 2])
.limit(100)
.execute()
for row in rows {
print("row: \(row)")
}
// 6. Count the rows.
let n = try await db.count("orders")
print("total rows: \(n)")
} catch {
fatalError("error: \(error)")
}
}
}
Run it:
swift run
You should see:
created table id: 1
row: ["1": 2, "2": "Bob"]
total rows: 2
5. What each part does
| Code | What it does |
|---|---|
MongrelDBClient(baseURL:) | Builds an HTTP client targeting one daemon. Safe to share across tasks. |
await db.health() | GET /health; returns true when the daemon answers. Always check before real work. |
try await db.createTable(_:columns:) | POST /kit/create_table. Column ids are the on-wire identifiers; use them everywhere else. |
try await db.put(_:cells:) | Single-op transaction: POST /kit/txn with one put op. cells is flattened to [col_id, val, ...]. |
db.query(_:).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. |
try await ...execute() | Sends the query and decodes the rows array. |
try await db.count(_:) | 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.
print(try await db.historyRetentionEpochs()) // current window, e.g. 1024
print(try await db.earliestRetainedEpoch()) // oldest readable epoch, e.g. 3
// Widen the window. The response contains the updated values.
let resp = try await db.setHistoryRetentionEpochs(1_000)
print(resp["history_retention_epochs"] ?? "") // 1000
// Read the table as it existed at a captured commit epoch.
_ = try await db.put("orders", cells: [1: 1, 2: 99.5])
if let insertEpoch = db.lastEpoch {
let rows = try await db.sql(
"SELECT id, amount FROM orders AS OF EPOCH \(insertEpoch)"
)
print(rows)
}
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 Int id, not the
String name:
// Wrong:
.where("range", params: ["column": "amount", "min": 100.0])
// Right:
.where("range", params: ["column": 4, "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 traps
with a precondition failure. 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. 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 throws 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