CLI reference#

$ h5i-db <command> <db> [args…] [--format table|json|jsonl|csv|arrow]

The h5i-db binary is non-interactive by design: no prompts, no pager, SQL from an argument or stdin, results on stdout, diagnostics on stderr. The database path is always the first positional argument of a command (there is no global --db flag).

Global behavior#

Output formats#

--format is global and defaults to table.

Format Behavior
table Human-readable aligned columns (buffered; metadata commands render pretty JSON)
json One JSON array of row objects; explicit nulls; empty result is []
jsonl One compact JSON object per row per line
csv With header row; empty result still emits the header
arrow Arrow IPC stream on stdout; lossless, pipes into other tools

Errors and exit codes#

Errors are a single JSON envelope on stderr:

{
  "schema_version": 2,
  "code": "table_not_found",
  "message": "table \"trade\" not found",
  "retryable": false,
  "hint": "run `h5i-db tables <db>` to list tables",
  "did_you_mean": "trades",
  "next_actions": [
    {"cmd": "h5i-db schema market.db trades", "why": "\"trade\" does not exist; \"trades\" is the closest name"},
    {"cmd": "h5i-db tables market.db", "why": "list the tables that do exist"}
  ]
}
Field Use
code Stable machine-readable identifier; branch on this, not on message
retryable Whether re-running the same call can plausibly succeed
hint One line of prose for a human
did_you_mean Closest existing identifier when the failure looks like a typo; absent otherwise
next_actions Commands you can run verbatim, best first. <db> is already substituted with the database you invoked
schema_version Bumped on any breaking change to this shape

Exit codes are stable and branchable:

Code Meaning
0 Success (including broken pipe from … \| head)
2 User error: bad arguments, bad SQL, missing table
3 Conflict: another writer moved the head; usually retryable
4 Limit exceeded: --max-rows, --max-bytes, memory budget, timeout
5 Internal error

Diagnostics volume is controlled with RUST_LOG (default warn).

Shared write flags#

Commands that commit a version (ingest, restore, replace-range, delete-range, compact) accept:

Flag Meaning
--expected-version <N> Require the table head to be exactly version N (optimistic guard); mismatch exits 3
--note <text> Free-text note recorded in the version manifest
--idempotency-key <token> Make the mutation replayable exactly once

--idempotency-key is what makes an unattended ingest loop safe to retry. A repeat carrying the same key finds the commit it already produced and returns it with "segments_added": 0 instead of writing the rows a second time, which matters because a duplicated append does not error: it just leaves the data wrong from then on. The key is recorded in the commit's own manifest, and retries are deduplicated against the last 64 commits.

h5i-db ingest market.db trades day.parquet --idempotency-key load-2026-07-01

Database & tables#

h5i-db context#

Everything needed to orient, in one call: every table's columns, size, segment count, time range and head version, which operations the policy gates, the snapshots that exist, and any plan already staged and waiting for review. It replaces a tablesschemasampleversions walk repeated per table.

$ h5i-db context market.db --format json
$ h5i-db context market.db --budget 2000        # cap the answer in tokens
$ h5i-db context market.db --stale-after 15m    # flag tables whose head is old
Flag Meaning
--budget <tokens> Approximate ceiling. Detail is shed in a fixed order (columns, snapshots, notes, then whole tables smallest-first) and whatever went is counted under omitted, with the command that recovers it
--stale-after <dur> Report per-table commit age and flag anything older than this

Output is deterministic for a given database state, so it can be cached as a preamble; --stale-after is the only flag that makes it depend on the clock.

h5i-db init#

Create a new database directory.

$ h5i-db init market.db

h5i-db create-table#

Create a table. The schema comes from --schema JSON or --like a data file (exactly one required).

$ h5i-db create-table market.db trades --like ticks.parquet --time-column ts
$ h5i-db create-table market.db bars \
    --schema '[{"name":"ts","type":"timestamp_us","nullable":false},
               {"name":"symbol","type":"utf8"},{"name":"close","type":"float64"}]' \
    --time-column ts --sort-key ts,symbol
Flag Meaning
--schema <json> Explicit schema. Types: int8..int64, uint8..uint64, float32/float64, utf8, bool, timestamp_s/ms/us/ns (UTC), date32, date64. Aliases accepted: int→int32, long/bigint→int64, float→float32, double→float64, string/str/text→utf8, boolean→bool, date→date32, timestamp→timestamp_ns. nullable defaults to true.
--like <file> Infer the schema from a Parquet/CSV/Arrow file
--time-column <col> Time index column; strongly recommended for time-series tables, and forced non-nullable
--sort-key <cols> Comma-separated sort key (defaults to the time column)
--target-segment-mb <N> Target segment size in MiB of in-memory data (default 128)

h5i-db tables#

List tables with row counts and time ranges. Columns: table, version, rows, bytes, segments, time_range, time_column.

h5i-db schema#

Show a table's schema and options (schema_revision, time_column, sort_key, fields with types and nullability).

h5i-db sample#

Show the first rows of a table.

Flag Meaning
-n, --rows <N> Row count (default 10)
--version <N> Read at a specific version

h5i-db rename#

Rename a table: a catalog edit, no data moves.

$ h5i-db rename market.db trades trades_raw

h5i-db drop-table#

Drop a table and its data. Refuses if the table is pinned by a snapshot, and requires --yes:

$ h5i-db drop-table market.db scratch --yes

Irreversible

drop-table permanently deletes data; it is the one command that bypasses versioning. Snapshots protect tables from it, so use them.


Reading & querying#

h5i-db query#

Run SQL. The query comes from the argument, or stdin when -.

$ h5i-db query market.db "SELECT symbol, vwap(price, size) FROM trades GROUP BY symbol" \
    --format json --max-rows 1000 --timeout 30s
$ cat report.sql | h5i-db query market.db - --format csv > out.csv
Flag Meaning
--max-rows <N> Abort after N produced rows
--max-bytes <N> Stop after N output bytes (checked at batch boundaries); truncation exits 4 with a limit_exceeded envelope
--timeout <dur> Query timeout, e.g. 30s, 5m
--memory-limit-mb <N> Memory budget in MiB; enables disk spilling under pressure
--spill-dir <path> Spill directory (with --memory-limit-mb)
--threads <N> Number of threads / partitions
--stats Print scan/pruning statistics to stderr after the query
--predicate-cache Read and build immutable predicate-cache sidecars
--as-of <at> Pin every table at a read point: a version, an RFC3339 availability timestamp, or a snapshot name
--decision-time <ts> Hide rows stamped after this RFC3339 instant
--embargo <dur> Extra gap subtracted from --decision-time, e.g. 1d

See the SQL reference for h5i() time travel, asof_join, and the time-series function library.

Point-in-time reads#

--as-of and --decision-time bound what a query can read, on two different clocks. --as-of is the commit clock: it pins every table at a version, so a restatement that landed later is not visible. --decision-time is the data clock: it hides rows whose time column is later than the instant, so a window cannot reach forward and a join cannot pull in a later timestamp.

They are independent because under a bulk load the two clocks are far apart: ten years of history committed this morning has a first commit of today, so an arrival pin at a historical instant resolves to nothing while an event-time cutoff at that same instant is exactly what you want. When the clocks do agree, an RFC3339 --as-of also supplies the decision time, so the common case stays one flag.

# One decision date, both axes, for a walk-forward step.
h5i-db query market.db "SELECT vwap(price, size) FROM trades" \
    --as-of 2026-07-01T00:00:00Z --decision-time 2026-07-01T00:00:00Z --embargo 1d

H5I_DB_AS_OF, H5I_DB_DECISION_TIME and H5I_DB_EMBARGO set the same things for a whole shell, which is how you hand a bounded view to a script or an agent rather than relying on it to pass the flag every time. An explicit flag still wins over the environment.

The bound is part of the table, not a filter you compose with, so a query that explicitly asks for later rows still gets none. Three things refuse rather than being quietly exempt: a table with no time column, a table whose time column is a bare integer carrying no unit, and, under --decision-time, the table functions (h5i(), asof_join(), gapfill(), resample(), tail(), latest_on()), which read their tables directly and cannot have the cutoff pushed into them. Under --as-of alone those functions work normally, resolving at the pinned version; selecting a different read point from inside a pinned session is refused.

h5i-db versions#

List a table's committed versions: version, op, committed_at, rows, bytes, segments, note.

h5i-db arrival-delta#

How much a query's answer moved because data arrived late: run it once at the current head and once at an earlier read point, and report the difference. What moves is what depended on commits that had not landed at the earlier point, which in a backtest is the part of a metric a live run starting that day could not have earned.

$ h5i-db arrival-delta market.db \
    "SELECT symbol, vwap(price, size) AS vwap FROM trades GROUP BY symbol" \
    --as-of 2026-07-01T16:00:00Z --format json
Flag Meaning
--as-of <at> The decision point: an integer version, an RFC3339 timestamp (matched by commit availability time, like h5i()), or a snapshot name
--tolerance <f> Absolute per-cell delta below which a difference is treated as noise (default 1e-9)

The report carries changed, per-column head → asof (delta) for the common single-row-metric case, max_abs_delta, row_count_differs, withheld_versions (per table, the head-vs-as-of version gap), vacuous, and notes.

Read it as a measurement, not a verdict. Look-ahead comes in many shapes and this sees one of them: rows that exist now but had not been published. A signal that reads its own bar leaves the delta unmoved, so no value of it, high or low, clears a query of look-ahead; bound the event-time axis with query --decision-time for that. And check vacuous before reading the number: when both read points resolve to the same version, which is the normal state of a bulk-loaded database, the zero is arithmetic rather than evidence, and the report says so in notes.


Writing data#

h5i-db ingest#

Ingest Parquet/CSV/Arrow into a table, from a file or from stdin with -.

$ h5i-db ingest market.db trades ticks.parquet
$ curl -s https://…/ticks.csv | h5i-db ingest market.db trades - --input-format csv
Flag Meaning
--input-format <fmt> auto (default) | parquet | csv | arrow. Auto uses the file extension, or sniffs leading bytes on stdin
--mode <mode> append (default) for a strict ordered append; write to replace the table contents
--retries <N> Retry appends on version conflicts (default 5; safe for pure appends)

Plus the shared write flags. Input batches are schema-aligned against the table (purely representational casts like timezone-less CSV timestamps are applied automatically). CSV assumes a header row.

Arrow over stdin

Pipe an Arrow IPC stream, not an IPC file: a file's random-access footer can't be consumed from a pipe. h5i-db detects the difference and tells you.

h5i-db restore#

Make a historical version current. History only moves forward, so restore commits a new version holding the old contents.

$ h5i-db restore market.db trades 42 --note "roll back bad load"

h5i-db replace-range#

Replace all rows in [start, end) of the time column with the given input.

$ h5i-db replace-range market.db trades \
    --start 2026-07-01T09:30:00Z --end 2026-07-01T16:00:00Z \
    --input corrected.parquet --plan
Flag Meaning
--start <t> Range start, inclusive; RFC3339, or a raw integer in the column's unit
--end <t> Range end, exclusive
--input <file> Replacement data (or - for stdin); omit to delete the range
--input-format <fmt> As in ingest
--plan Stage a previewable plan instead of committing immediately

h5i-db delete-range#

Delete all rows in [start, end), shorthand for replace-range with no input. Same --start/--end/--plan flags.

$ h5i-db delete-range market.db trades --start 09:30… --end 09:31… --plan
{"plan_id": "5c41…", "summary": {"rows_affected": 12481, "segments_reused": 127}}

Plans, policy & snapshots#

h5i-db plan#

Manage previewable-mutation plans (created by --plan above).

Subcommand Meaning
plan list <db> <table> Pending plans: plan_id, op, base_version, created_at, expired, summary
plan show <db> <table> <plan-id> Full plan JSON on stdout; before/after row samples on stderr
plan apply <db> <table> <plan-id> Publish the plan; fails with a conflict if the head moved
plan discard <db> <table> <plan-id> Drop the plan; staged segments become vacuumable

Plans expire after 7 days; see plan hygiene.

h5i-db policy#

The mutation policy decides which operations may commit without a reviewed plan.

$ h5i-db policy show market.db
$ h5i-db policy set market.db direct_delete=false direct_write=false

Keys: direct_append, direct_write, direct_replace, direct_delete, direct_restore, direct_compact, each true/false. The update is an atomic read-modify-write.

The mutation policy gates who may write directly; the data policy below gates what data may be written at all.

h5i-db data-policy#

Opt-in, per-table data-safety constraints checked on every write (and at plan time), so a violating mutation is refused before it can be applied. A table with no policy is unconstrained and pays only one metadata lookup on the write path; reads are never touched.

Subcommand Meaning
data-policy get <db> <table> Show the table's policy (empty when unset)
data-policy set <db> <table> <policy> Install a typed JSON policy (inline JSON, @path to read a file, or - for stdin)
data-policy clear <db> <table> Remove the policy (writes become unconstrained)
$ h5i-db data-policy set market.db trades '{"constraints":[
    {"name":"positive_price",
     "predicate":{"compare":{"column":"price","op":"gt","value":{"float":0.0}}},
     "on_fail":"reject"}]}'

Predicates compose not_null, compare, and in_set with and/or/not; on_fail is reject (fail the write) or warn. Evaluation is fail-closed: a NULL never satisfies a comparison. A violation raises data_policy_violation (exit 2, not retryable).

h5i-db snapshot#

Pin table versions under a name.

Subcommand Meaning
snapshot create <db> <name> [tables…] Pin current versions (all tables when omitted); --note supported
snapshot list <db> List snapshots
snapshot delete <db> <name> Delete a snapshot (the versions it pinned remain readable by number)

Maintenance & tools#

h5i-db compact#

Rewrite small segments into target-sized ones, as a new version. Row count is verified preserved.

Flag Meaning
--target-mb <N> Override the target segment size (MiB of in-memory data)

h5i-db vacuum#

Remove unreachable objects, dry run unless --apply.

$ h5i-db vacuum market.db                    # inspect candidates
$ h5i-db vacuum market.db --apply            # actually delete
Flag Meaning
[table] Restrict to one table (optional positional)
--grace-seconds <N> Never touch objects newer than this (default 3600)
--apply Actually delete

Read the cadence guidance before scripting this.

h5i-db verify#

Check structural integrity: checksums, hash chain, object existence.

$ h5i-db verify market.db trades --deep

--deep additionally re-reads every segment and verifies content checksums. Problems are reported in the chosen format and the command exits non-zero.

h5i-db demo#

Build a small database and walk the whole arc on real data: ingest, the metric a strategy would have traded on, a vendor restatement previewed through plan/apply, the arrival delta that correction reveals, and a session that cannot read past its decision instant. Takes about a second.

$ h5i-db demo
$ h5i-db demo --dir ./scratch --keep      # keep the database it builds

h5i-db ui#

Launch the local review UI, loopback only.

Flag Meaning
--port <N> Port (default 7351)
--allow-mutations Enable plan apply/discard from the UI (default: read-only)

The UI shows pending plans with previews, the version timeline with audit badges, version diffs, and an SQL scratchpad that reports pruning per query.