Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Running Improv

This page is the operator's quick reference: build, run each front end, run the tests, check coverage, and the security knobs. The getting-started guide walks through modeling itself.

Build

# Nix (recommended): toolchain + GUI runtime libs + dev tools
nix develop
cargo build --workspace --release

# Or plain Debian/Ubuntu (what the EC2 dev box used):
sudo apt-get install -y build-essential pkg-config libssl-dev libsqlite3-dev \
  libxkbcommon-x11-0 libgl1 libegl1 libgl1-mesa-dri python3 bubblewrap
curl -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.97.0   # MSRV; any newer stable works
cargo build --workspace --release

The binaries land in target/release/: improv (CLI), improv-tui, improv-gui and improv-server.

A model to play with

IMPROV=target/release/improv bash docs/demo/sample-model.sh sample.db   # Time × Product × Region
target/release/improv list sample.db

Or build one by hand:

I=target/release/improv
$I init m.db
$I add-category m.db 1 Time;    $I add-item m.db 10 1 Q1; $I add-item m.db 11 1 Q2
$I add-category m.db 2 Product; $I add-item m.db 20 2 Widget
$I add-measure m.db 100 Price number input Time Product
$I add-measure m.db 101 Quantity number input Time Product
$I add-derived m.db 102 Revenue "Price * Quantity"
$I set m.db 100 10 --at Time=Q1 Product=Widget   # or --at Time=Q1,Product=Widget
$I set m.db 101 5  --at Time=Q1 --at Product=Widget
$I eval m.db 102
$I save-view m.db 1 "Revenue by quarter" 102 --rows Time --cols Product

Run each front end

Front endCommandNotes
CLIimprov --helpScriptable; see man docs/man/improv.1.
TUIimprov-tui m.dbArrows move, e/Enter edit a cell (or a derived measure's formula), Shift+arrows select a range (Sum in the status line), p pivot, [ ] page, f/F filter, Tab measure, R revert to saved, ?/F1 function help, Tab completes names in a formula, q quit (autosaves).
GUIimprov-gui m.dbNeXTSTEP-style workspace: application menu top-left, worksheet windows, model browser. Double-click or Enter edits a cell; right-click a tile to move or filter it; type Name = formula on the formula list's last line. Run inside nix develop on NixOS so the GL/X11 libraries are found.
Streamingproducer | improv stream m.db RevenueEdits in on stdin, changed cells out on stdout; saved at end of input.
ServerIMPROV_API_TOKEN=secret improv-server m.dbJSON API on 127.0.0.1:3000; see The HTTP Server.
PostgresCREATE EXTENSION pg_improvModels over your tables; see Improv in Postgres.

Ways to Use Improv explains when to pick which.

Example server calls:

curl -s -H 'Authorization: Bearer secret' localhost:3000/measures
curl -s -H 'Authorization: Bearer secret' -X POST localhost:3000/measures/102/eval \
  -H 'content-type: application/json' -d '{"edits":[]}'

Open a Postgres-hosted model

Every front end also accepts a Postgres URL naming a model instead of a file:

improv list      'postgres://me:pw@127.0.0.1/sales?model=plan'
improv-tui       'postgres://me:pw@127.0.0.1/sales?model=plan'
improv-gui       'postgres://me:pw@127.0.0.1/sales?model=plan'
improv-server    'postgres://me:pw@127.0.0.1/sales?model=plan'

?model= is required; the other parameters go to libpq as usual. The model lives in the improv.* catalog (created on first open when the pg_improv extension isn't installed). Input measures bound to a table (improv.binding) are read from it, and their edited cells are written back to that table with UPDATE/INSERT (values only, no DDL). Unbound cells go to improv.cell. As with a file store, a save fails with "reload and retry" when another writer changed the model after you loaded it. There is no TLS in this build, so a non-loopback host needs an explicit sslmode=disable (use an SSH tunnel to a loopback port instead).

Security defaults (and how to relax them)

Everything stored in a model is treated as untrusted: a shared model can carry code or SQL.

BehaviorDefaultOverride
External functions run sandboxed (bubblewrap: read-only /usr, no $HOME, no network, cleared env, rlimits)always, regardless of the model's pure flagIMPROV_EXTFN_TRUSTED=1 (host decides, never the model)
No bubblewrap installedrefuse to run external functionsIMPROV_EXTFN_ALLOW_UNSANDBOXED=1 (rlimits only)
Server's refresh scheduleroffIMPROV_SCHEDULER=1 plus IMPROV_EXTFN_ALLOW=<file> listing sha256(name\0body) digests
Server with no token on a non-loopback addressrefuses to startIMPROV_ALLOW_OPEN=1
Read-only API tokensnoneIMPROV_API_TOKENS_RO=a,b
Server limits1 MiB body, 30 s request, 64 in flight, 10 s header read, 10k edits per /evalIMPROV_HEADER_TIMEOUT_MS
SQL refresh queriessingle SELECT/WITH only, run read-only; DuckDB external access offnone
Postgres transportloopback/socket only (no TLS built in)sslmode=disable in the connection string to accept cleartext to a remote host
CSV export of text starting = + - @ \t \rprefixed with ' (formula-injection safe)ExportSpec { neutralize_formulas: false } (API)
Two processes saving one storethe second save is refused with "reload and retry" (no clobbering)none

Tests

cargo test --workspace                          # ~750 tests, ~1 min
cargo test --workspace --release -- --ignored   # stress: 5M cells verified; the 10M case needs ~7 GB

Live Postgres tests run when IMPROV_TEST_PG is set, e.g.

IMPROV_TEST_PG="host=127.0.0.1 user=improv password=improv dbname=improv_test" \
  cargo test -p improv_storage_sql -p improv_storage_pg -p improv_store

The sandbox tests need python3 and bubblewrap; without them they skip (and say so) rather than fail.

Coverage (≥ 90% functions and branches per crate)

rustup toolchain install nightly -c llvm-tools-preview
cargo install cargo-llvm-cov
scripts/coverage.sh            # prints a per-crate table; exits 1 below 90%
scripts/coverage.sh --html     # also writes target/coverage-html/html/index.html

Inline #[cfg(test)] modules are excluded from the denominator (via the coverage_nightly cfg cargo-llvm-cov sets), so the percentages measure library code. CI runs the same gate (coverage job).

Full quality gate

P=$(ls crates | grep -v '^pg_improv$' | sed 's/^/-p improv_/')   # pg_improv has its own workspace
cargo fmt $P -- --check
cargo clippy $P --all-targets --all-features -- -D warnings
cargo test --workspace
cargo deny check
typos
RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --no-deps
scripts/gui-shot-check.sh       # GUI screenshot vs golden (needs Xvfb, ImageMagick, xdotool)
scripts/coverage.sh             # coverage gate (nightly)
(cd crates/pg_improv && cargo fmt --check \
  && cargo clippy --features pg17 --all-targets -- -D warnings \
  && cargo pgrx test pg17 && ./tests/logical.sh)   # the extension