fadodb

module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2026 License: MIT

README

FadoDB

ci Go Reference Go Report Card

An embeddable active document database, written in Go, MIT licensed. Website: fadodb.org.

Status: Phase 3, the statechart, is done and released as v0.3.0; Phase 4, rules and actions, is next. The Go API stores documents of typed classes as commits with recalculation, history and an event log, with attachments, lookups into reference tables, filters and read-only SQL; moves them through their statecharts with guards and eventless transitions; enforces field rights and transition rules for the principal; and previews any change at a moment the caller chooses. Rules, the row-level access rules, QUERY and the server come with their phases. The numbers are in docs/perf/, the plan in ROADMAP.md.

What it is

A document class declares typed variables, Excel-like formulas, a statechart, and rules that fire actions when the state changes. FadoDB keeps an append-only event log and a typed history row for every version, evaluates formulas on the server and in the browser from one compiled bytecode, exposes every document class as a queryable SQL view, and enforces user and group access rules that are themselves formulas. Analytics and process mining are SQL over the event log.

A class is one JSON document. This one defines, stores, computes and moves through its states today, and its rights and transition rules are enforced; its rules are compiled and checked now and run with Phase 4, the class-level access rule with Phase 5, and formulas over lines arrive with composites:

{
  "name": "Invoice",
  "title": "\"Invoice for \" & customer",
  "variables": {
    "customer": {"type": "text", "required": true},
    "due":      {"type": "date", "default": "ADDDAYS(TODAY(), 30)"},
    "discount": {"type": "decimal", "scale": 2, "default": "0", "min": 0, "max": "0.50"},
    "net":      {"type": "money", "scale": 2},
    "lines":    {"type": "rows", "schema": {"sku": "text", "qty": "int", "price": "money"}},
    "total":    {"formula": "ROUND(net * (1 - discount), 2)"}
  },
  "states": {"initial": "draft", "list": ["draft", "submitted", "approved", "paid"], "final": ["paid"]},
  "transitions": [
    {"name": "submit",  "from": "draft",     "to": "submitted", "guard": "total > 0"},
    {"name": "approve", "from": "submitted", "to": "approved",  "guard": "INGROUP(\"finance\")"},
    {"name": "pay",     "from": "approved",  "to": "paid"}
  ],
  "rules": [
    {"on": "enter", "state": "paid",
     "do": [{"action": "create", "class": "Receipt", "values": {"invoice": "ID()", "amount": "total"}}]}
  ],
  "access": {"read": "INGROUP(\"finance\") OR customer = USER()"},
  "rights": {"*": {"discount": {"write": ["sales"]}}, "approved": {"discount": {"read": ["finance"]}}}
}

Try the document store

db, err := fado.Open("app.fado", fado.Options{})
_, err = db.DefineClass(ctx, invoiceJSON)

alice := fado.Principal{ID: "alice", Groups: []string{"sales"}}
doc, err := db.Create(ctx, "Invoice", fado.Values{"customer": "ACME", "net": "100.00 PLN"},
	fado.As(alice), fado.Key("order-42"))                    // the key makes a retry the same commit
doc, err = db.Set(ctx, "Invoice", doc.ID, fado.Values{"discount": "0.10"}, fado.As(alice))
fmt.Println(doc.Version, doc.Values["total"])             // 2 90.00 PLN
doc, err = db.Transition(ctx, "Invoice", doc.ID, "submit", nil, fado.As(alice))
fmt.Println(doc.Version, doc.State)                       // 3 submitted; approve needs finance

open, err := db.Query(ctx, fado.Filter{Class: "Invoice", Where: "state = \"draft\" AND total > 50.00", Order: []string{"-due"}})
rows, err := db.SQL(ctx, `SELECT count(*), sum(total) FROM Invoice`, nil)
history, err := db.History(ctx, "Invoice", doc.ID)       // every version, who committed it, when
preview, err := db.Preview(ctx, "Invoice", doc.ID, fado.Values{"discount": "0.50"}, "", fado.As(alice))

Every change is a commit: the assignments, the recalculation of the formulas that depend on them, a typed history row and an event, in one transaction. A transition is a commit too, its guard evaluated over the recalculated document, and every eventless transition that follows is a commit of its own in the same transaction; Transitions says which moves are open. The principal decides what a call may set and sees: a value a right refuses, or a transition its rule refuses, comes back as forbidden, and a variable the principal may not read in the document's state is absent from what they get back; the row-level access rule waits for Phase 5. A preview, a listing or a read may be asked at a moment the caller chooses, fado.At(t), and a preview reports the moment it ran under; a commit runs at the database's clock. A refusal is a fado.Error with a stable code and the variable it names; a definition's problems come back at once as Diagnostics, each with a code and the place. Files are attached as File values behind a blob store, reference data lives in explicit tables that formulas read with LOOKUP, and a class can be redefined as a new version that touches no existing document. fadodb check FILE verifies a database file. The full walk-through is fado/example_test.go, and fado/readme_test.go runs the class and the calls above.

Install

Each release has archives for Linux, macOS and Windows on amd64 and arm64, with a checksums.txt, on the releases page. Unpack the one for your platform and put fadodb on your path. With Go 1.26 or newer you can build it instead:

go install github.com/MojoHand70/fadodb/cmd/fadodb@latest

Every release before 1.0 is a pre-release. fadodb version prints the version, the commit and the build date.

Try the formula engine

$ fadodb formula eval -var qty:int=3 -var price:money='9.99 PLN' 'ROUND(qty * price * (1 - 10%), 2)'
26.97 PLN
$ fadodb formula eval -var a:decimal=0.10 -var b:decimal=0.20 'a + b = 0.30'
TRUE
$ fadodb formula eval -var name:text=invoice 'UPPER(LEFT(name, 3)) & "-" & TEXT(42)'
"INV-42"
$ fadodb formula eval -sql duckdb -var due:date 'DAYS(due, TODAY())'
value  (CAST("due" AS DOUBLE) - floor(CAST($now AS DOUBLE) / CAST(86400 AS DOUBLE)))
error  NULL
params now

Numbers come in two families: number is a float64 with a spreadsheet's arithmetic, and decimal, int and money are exact, an int64 mantissa at a declared scale, with rounding written in the formula and overflow an error. A date is a calendar day and a datetime an instant. The same formula gives the same result on the VM, on SQLite and on DuckDB: the conformance suite under conformance/ is the specification, and the property tests generate formulas to check that the three agree. The semantics are in ADR-0019, ADR-0022, ADR-0023 and ADR-0024, the size limits in ADR-0021, and the numbers in docs/perf/.

Design principles

  • Fast by construction. Every feature has a performance budget, and CI fails on benchmark regressions.
  • Small. Pure Go core on the standard library plus modernc.org/sqlite. No cgo in the core. DuckDB analytics and Postgres are separate modules.
  • Deterministic. Same class, same values, same user: same result on the server, in SQL, and in the browser.
  • Records, not cells. A redefinition of a class never changes a stored document; a document in a terminal state never changes at all.
  • Protocol first. One JSON command set over HTTP, WebSocket, a C ABI and the Go API, so any frontend or language can attach.
  • Honest status. Docs describe what works today.

The reasoning is in docs/architecture.md and the architecture decision records.

Build

Go 1.26 or newer.

make tools    # staticcheck, govulncheck, benchstat
make build    # bin/fadodb
make gates    # what CI runs

Layout

cmd/fadodb/        command-line tool: formula eval, check; the server later
fado/              public Go API
internal/formula/  lexer, parser, binder, bytecode, VM, SQL generator
internal/model/    document classes, schemas, tables, validation, the diff between class versions
internal/store/    SQLite store: documents, history, events, tables, lookups, filters, blobs, class versions
internal/query/    the filter compiler and the SQL endpoint's authorizer
internal/state/    statechart engine (Phase 3)
internal/rules/    event-condition-action rules and built-in actions (Phase 4)
internal/auth/     users, groups, access rules as predicates (Phase 5)
internal/server/   HTTP and WebSocket (Phase 6)
analytics/         the DuckDB module: conformance and store benchmarks today, the projection in Phase 8
bench/             end-to-end performance scenarios behind the budgets
docs/              architecture, ADRs, dependency budget, benchmarks
scripts/           the gates CI runs

Contributing

See CONTRIBUTING.md. Design review and conformance cases are the most useful contributions at this stage.

License

MIT, see LICENSE.

Directories

Path Synopsis
Package bench is the end-to-end performance suite: the scenarios behind the budgets of ADR-0012, written against database/sql so that the same scenario runs on every storage engine under consideration.
Package bench is the end-to-end performance suite: the scenarios behind the budgets of ADR-0012, written against database/sql so that the same scenario runs on every storage engine under consideration.
cmd
fadodb command
Command fadodb is the FadoDB command-line tool and, later, its server.
Command fadodb is the FadoDB command-line tool and, later, its server.
Package fado is the public Go API for embedding FadoDB.
Package fado is the public Go API for embedding FadoDB.
internal
auth
Package auth holds users, groups, API keys and sessions, and compiles access rules into SQL predicates.
Package auth holds users, groups, API keys and sessions, and compiles access rules into SQL predicates.
formula
Package formula holds the formula language: lexer, parser, function catalog, binder, bytecode, the Go virtual machine and the SQL generator with its SQLite and DuckDB dialects.
Package formula holds the formula language: lexer, parser, function catalog, binder, bytecode, the Go virtual machine and the SQL generator with its SQLite and DuckDB dialects.
formula/fuzz
Package fuzz generates random, well-typed formulas with inputs and runs each one on the Go VM and on a SQL dialect, reporting any divergence.
Package fuzz generates random, well-typed formulas with inputs and runs each one on the Go VM and on a SQL dialect, reporting any divergence.
formula/sqlitefn
Package sqlitefn registers the formula engine's Go functions into the pure-Go SQLite driver, so that the SQLite dialect of the SQL generator agrees with the Go VM by construction (ADR-0019).
Package sqlitefn registers the formula engine's Go functions into the pure-Go SQLite driver, so that the SQLite dialect of the SQL generator agrees with the Go VM by construction (ADR-0019).
model
Package model defines document classes: the class definition as JSON, its validation into a compiled Class, the named schemas and the predefined vocabulary, and the diagnostics a definition earns.
Package model defines document classes: the class definition as JSON, its validation into a compiled Class, the named schemas and the predefined vocabulary, and the diagnostics a definition earns.
query
Package query compiles the JSON filter of the Go API and the command set into SQL over a class's view, and guards the read-only SQL endpoint (docs/architecture.md, "Querying"; ADR-0038).
Package query compiles the JSON filter of the Go API and the command set into SQL over a class's view, and guards the read-only SQL endpoint (docs/architecture.md, "Querying"; ADR-0038).
rules
Package rules evaluates event-condition-action rules and runs built-in actions, synchronously inside the transaction or through the outbox.
Package rules evaluates event-condition-action rules and runs built-in actions, synchronously inside the transaction or through the outbox.
server
Package server exposes the JSON command set over HTTP and WebSocket.
Package server exposes the JSON command set over HTTP and WebSocket.
state
Package state is the statechart engine of ADR-0008 and ADR-0040: which named transition a command means, which transitions leave a state, and the chain of eventless transitions a commit follows after its recalculation, with its bound.
Package state is the statechart engine of ADR-0008 and ADR-0040: which named transition a command means, which transitions leave a state, and the chain of eventless transitions a commit follows after its recalculation, with its bound.
store
Package store is the SQLite storage layer and the document engine of Phase 2: one file holds the classes, the schemas, the typed document tables of ADR-0014, the event log of ADR-0006 and ADR-0026, and the database's own metadata.
Package store is the SQLite storage layer and the document engine of Phase 2: one file holds the classes, the schemas, the typed document tables of ADR-0014, the event log of ADR-0006 and ADR-0026, and the database's own metadata.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL