One SQL surface spans both stores: the live unsealed tip (redb) and the sealed Parquet history,
unioned as DuckDB views. You never think about the seam - a query over usdc__transfer sees every
row from deployment to the block indexed a moment ago. Reach it via nuthatch sql (a REPL when
called with no query), GET /sql, or the MCP sql tool.
Naming
- Every decoded event is a view named
{alias}__{event}in snake_case:usdc__transfer,staking__stake_delegated..tablesin the REPL (orGET /tables) lists them. - Factory children share their template’s tables (
{template}__{event}), distinguished byaddress. - Authored views and recipe derivations appear as
ordinary views alongside the event tables, described in
/schemalike everything else.
Columns
Every event table carries the implicit columns block_number, block_timestamp, log_index,
tx_hash, and address (the emitting contract), plus one column per event parameter.
Two footguns, both machine-tracked in semantic.toml:
- Reserved words. Solidity loves
fromandto; SQL reserves them. Double-quote:SELECT "from", "to" FROM usdc__transfer. - Big integers. A
uint256column likevalueis stored exactly and can’t be summed directly. Every big-int column gets a derived*_decsibling (value_dec) for arithmetic:sum(value_dec),value_dec > 1e6.
Get either wrong and the error comes back with a fix hint derived from the real schema - the
binder knows the nearest table name, the quoting rule, and the _dec convention.
Semantics & guards
- SELECT/WITH only. The surface is read-only by construction; the ingest thread is the single writer, and queries attach the sealed segments read-only.
- Deterministic and finality-aware. Sealed segments are immutable; only the hot tip can change under a reorg, and the union converges with it.
- Guarded: a 30-second timeout, a row cap, a 64 MiB result-byte cap, and 2 concurrent analytical queries. A rejection is
the node protecting itself - narrow the query rather than fighting the guard. Validate cheaply
first with
explain. - Provenance-stamped. Results carry the block range and the content-addressed segment hashes they were computed from, so a number can be cited against immutable data and re-derived by anyone.
-- the shape of a typical answer
SELECT date_trunc('day', to_timestamp(block_timestamp)) AS day,
count(*) AS transfers,
sum(value_dec) / 1e6 AS volume_usdc
FROM usdc__transfer
WHERE block_number > 20000000
GROUP BY 1 ORDER BY 1 DESC LIMIT 30;