README.md

August 2, 2026 · View on GitHub

MongrelDB logo

MongrelDB F# Client

Pure F# client for MongrelDB - embedded+server database with SQL, vector search, full-text search, AI-native retrieval, and configurable MVCC history retention.
No external dependencies at runtime - built on .NET HttpClient and System.Text.Json. The API mirrors the MongrelDB PHP, Go, Ruby, and Java clients.

NuGet .NET CI License

Package

SurfacePackageInstall
F# clientVisorcraft.MongrelDBdotnet add package Visorcraft.MongrelDB

Requirements

What It Provides

  • Typed CRUD over the Kit transaction endpoint: Put, Upsert (insert-or-update on PK conflict), Delete by row id or primary key, all with optional idempotency keys for safe retries.
  • Fluent query builder that pushes conditions down to the engine's specialized indexes for sub-millisecond lookups: bitmap equality/IN, learned-range, null checks, FM-index full-text search, HNSW vector similarity (ann), and sparse vector match. Friendly aliases (column -> column_id, min/max -> lo/hi) are translated to the server's on-wire keys.
  • Idempotent batch transactions - operations staged locally and committed atomically, with the engine enforcing unique, foreign-key, and check constraints at commit time. Idempotency keys return the original response on duplicate commits, even after a crash.
  • Full SQL access through the DataFusion-backed /sql endpoint: recursive CTEs, window functions, CREATE TABLE AS SELECT, materialized views, and multi-statement execution.
  • Schema management: typed table creation with enum/default fields and native constraints, full schema catalog, and per-table descriptors.

Column dictionaries preserve enum_variants, scalar default_value (strings, numbers, booleans, explicit null, and the literal string "now"), dynamic default_expr ("now" or "uuid"), and table constraints.checks. default_expr is not an alias for default_value; it is evaluated by the engine on each insert.

  • History retention and time-travel queries: configure the number of MVCC epochs kept with SetHistoryRetentionEpochs, inspect the floor with EarliestRetainedEpoch, and read past states via SQL AS OF EPOCH.
  • User/role/credentials management via SQL: Argon2id-hashed catalog users, roles, and GRANT/REVOKE table-level permissions, all executed through Sql.
  • Maintenance: compaction (all tables or per-table).
  • Auth: Bearer token (--auth-token mode) and HTTP Basic (--auth-users mode), with the bearer token taking precedence.
  • Typed exception hierarchy: MongrelDBException (base), AuthException (401/403), NotFoundException (404), ConflictException (409, with error code + op index), and QueryException (everything else, including network failures).
  • Robust JSON handling: NaN and Infinity raise a clear QueryException instead of corrupting data; the /sql endpoint's Arrow IPC bodies are tolerated gracefully.

Install

dotnet add package Visorcraft.MongrelDB

Or reference the project directly:

<ProjectReference Include="path/to/mongreldb_fsharp/src/Visorcraft.MongrelDB/Visorcraft.MongrelDB.fsproj" />

Examples

Task-focused, commented guides live in docs/:

  • Quickstart - install, start the daemon, write and run a complete program.
  • Transactions - batch commits, idempotency keys, constraint handling.
  • Queries - every native condition type and the index it pushes down to.
  • SQL - recursive CTEs, window functions, advanced SQL.
  • Authentication - Bearer token, HTTP Basic, and open modes.
  • Errors - the exception hierarchy and recovery patterns.

Quick Example

open System.Collections.Generic
open Visorcraft.MongrelDB

// Connect to a running mongreldb-server daemon.
let db = new Client(url = "http://127.0.0.1:8453")

let col id name ty =
    let d = Dictionary<string, obj>()
    d.["id"] <- box id; d.["name"] <- box name; d.["ty"] <- box ty
    d.["primary_key"] <- box (id = 1); d.["nullable"] <- box false
    upcast d

// Create a table. Column ids are stable on-wire identifiers.
db.CreateTable("orders", [| col 1 "id" "int64"; col 2 "customer" "varchar"; col 3 "amount" "float64" |]) |> ignore

let cells pairs =
    let d = Dictionary<int, obj>()
    for (k, v) in pairs do d.[k] <- v
    upcast d

// Insert rows (cells map column id -> value).
db.Put("orders", cells [1, box 1; 2, box "Alice"; 3, box 99.50]) |> ignore
db.Put("orders", cells [1, box 2; 2, box "Bob";   3, box 150.00]) |> ignore

// Upsert (insert or update on PK conflict).
db.Upsert("orders",
          cells [1, box 1; 2, box "Alice"; 3, box 120.00],
          cells [3, box 120.00]) |> ignore

// Query with a native index condition (learned-range index).
let cond = Dictionary<string, obj>(); cond.["column"] <- box 3; cond.["min"] <- box 100.0
let rows = db.Query("orders")
              .Where("range_f64", cond)
              .ProjectionOf([| 1; 2 |])
              .LimitTo(100)
              .Execute()

printfn "%d" (db.Count("orders"))   // 2

// Run SQL.
db.Sql("UPDATE orders SET amount = 200.0 WHERE customer = 'Bob'") |> ignore

Authentication

// Bearer token (--auth-token mode)
let a = new Client(url = "http://127.0.0.1:8453", token = "my-secret-token")

// HTTP Basic (--auth-users mode)
let b = new Client(url = "http://127.0.0.1:8453", username = "admin", password = "s3cret")

// Arguments are optional; the daemon address defaults to 127.0.0.1:8453.
let c = new Client()

Batch transactions

Operations are staged locally and committed atomically. The engine enforces unique, foreign-key, and check constraints at commit time.

let txn = db.BeginTransaction()
txn.Put("orders", cells [1, box 10; 2, box "Dave"; 3, box 50.00]) |> ignore
txn.Put("orders", cells [1, box 11; 2, box "Eve";  3, box 75.00]) |> ignore
txn.DeleteByPk("orders", box 2) |> ignore

try
    let results = txn.Commit()                 // atomic - all or nothing
    printfn "Staged %d operations" txn.Count
with :? ConflictException as e ->
    printfn "Constraint violated: %s - %s" e.ErrorCode e.Message

// Idempotent commit - safe to retry; the daemon returns the original response.
let txn2 = db.BeginTransaction()
txn2.Put("orders", cells [1, box 20; 2, box "Frank"; 3, box 100.00]) |> ignore
txn2.Commit(idempotencyKey = "order-20-create") |> ignore

Error handling

Every non-2xx response is mapped to a typed exception. Catch the specific type for the category, or MongrelDBException for any client failure.

try
    let c = Dictionary<int, obj>(); c.[1] <- box 1
    db.Put("orders", c) |> ignore   // duplicate PK (with a UNIQUE constraint)
with
| :? ConflictException as e ->
    printfn "Constraint: %s" e.ErrorCode        // UNIQUE_VIOLATION
    printfn "Op index: %A" e.OpIndex             // offending op in the transaction
| :? AuthException as e ->
    printfn "Not authorized: %s" e.Message
| :? NotFoundException as e ->
    printfn "Not found: %s" e.Message
| :? QueryException as e ->
    printfn "Query/server error: %s" e.Message
| :? MongrelDBException as e ->
    printfn "Error: %s" e.Message

API reference

Client

MemberDescription
new(?url, ?token, ?username, ?password, ?timeout, ?httpClient)Construct a client (url defaults to http://127.0.0.1:8453)
Health() -> boolCheck daemon health
TableNames() -> string[]List table names
CreateTable(name, columns) -> int64Create a table; returns the table id
CreateTable(name, columns, constraints) -> int64Create a table with a constraints block
CreateTable(name, columns, constraints, indexes) -> int64Create a table with all six index kinds and options
DropTable(name) -> unitDrop a table
Count(table) -> int64Row count
Put(table, cells, ?idempotencyKey) -> IDictionaryInsert a row
Upsert(table, cells, ?updateCells, ?idempotencyKey) -> IDictionaryUpsert a row
Delete(table, rowId) -> unitDelete by row id
DeleteByPk(table, pk) -> unitDelete by primary key
Query(table) -> QueryBuilderStart a native query
Sql(sql) -> IDictionary[]Execute SQL (requests JSON output)
Schema() -> IDictionaryFull schema catalog
SchemaFor(table) -> IDictionarySingle-table descriptor
Compact() -> IDictionaryCompact all tables
CompactTable(name) -> IDictionaryCompact one table
BeginTransaction() -> TransactionStart a batch
HistoryRetention() -> uint64 * uint64Current retention window (history_retention_epochs, earliest_retained_epoch)
HistoryRetentionEpochs() -> uint64Current history_retention_epochs value
EarliestRetainedEpoch() -> uint64Lowest queryable epoch for AS OF EPOCH
SetHistoryRetentionEpochs(epochs) -> uint64 * uint64Set the retention window and return the new state
Get(path), Post(path, body), HttpDelete(path) -> ResponseLow-level HTTP

QueryBuilder

MemberDescription
Where(condType, parameters) -> QueryBuilderAdd a native condition (AND-ed)
ProjectionOf(columnIds) -> QueryBuilderSet column projection
LimitTo(limit) -> QueryBuilderSet row limit
OffsetBy(offset) -> QueryBuilderSkip matching rows before the limit
Build() -> IDictionaryBuild the request payload
Execute() -> IDictionary[]Run the query
Truncated -> boolWhether the last Execute result hit the limit

Transaction

MemberDescription
Put(table, cells, ?returning) -> TransactionStage an insert
Upsert(table, cells, ?updateCells, ?returning) -> TransactionStage an upsert
Delete(table, rowId) -> TransactionStage a delete by row id
DeleteByPk(table, pk) -> TransactionStage a delete by primary key
Count -> intNumber of staged operations
Commit(?idempotencyKey) -> IDictionary[]Commit atomically
Rollback() -> unitDiscard all operations

Exceptions

ClassHTTP statusNotes
MongrelDBException-Base class for all client errors
AuthException401, 403Bad or missing credentials
NotFoundException404Missing table, schema, or resource
ConflictException409Constraint violation; carries ErrorCode and OpIndex
QueryException400, 5xx, networkEverything else

Building and testing

The test suite uses xUnit. It is split into two layers:

  • Offline unit tests - exception hierarchy, query-builder alias translation, cells flattening, and JSON decoding. No daemon needed.
  • Live integration tests - boots a real mongreldb-server daemon and exercises the full client surface. Skips automatically when no binary is available.
dotnet build
dotnet test                  # runs the whole suite (live tests skip without a daemon)

Fetch a prebuilt server binary from the MongrelDB releases and place it at ./bin/mongreldb-server, set MONGRELDB_SERVER, or install it on PATH:

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

The live harness resolves the binary in this order: the MONGRELDB_SERVER env var, ./bin/mongreldb-server, mongreldb-server on PATH. Or point it at an already-running daemon with MONGRELDB_URL.

Contributing

Contributions are welcome. Please:

  1. Open an issue first for non-trivial changes.
  2. Add focused tests near your change - the suite must stay green.
  3. Run dotnet test before submitting.
  4. Keep the client dependency-free (BCL only at runtime).
  • Mongrel — Commercial multi-system workbench with native MongrelDB support.
  • MongrelDB Viewer — Free, open-source MongrelDB GUI and MCP server.

License

Dual-licensed under the MIT License or the Apache License, Version 2.0, at your option. See MIT OR Apache-2.0 for the full text.

SPDX-License-Identifier: MIT OR Apache-2.0