Stats Cache and Caching - dathere/qsv GitHub Wiki
Tier: Advanced
qsv caches at four levels. The flagship is the stats cache β feeding pre-computed statistics into "smart" downstream commands so they don't redo work. The other three are the frequency cache, the fetch / fetchpost / describegpt HTTP cache, and the Luau lookup-table cache.
This page explains the file layouts, who reads them, when they invalidate, and how to deliberately bust them.
qsv leans heavily on sidecar files next to your CSV. Run stats once, and downstream commands get smarter and faster without re-reading the source file. The convention is:
| File | Producer | Consumers |
|---|---|---|
<csv>.idx |
qsv index |
count, sample, slice, stats, frequency, split, schema, luau
|
<csv>.stats.csv |
qsv stats |
Human-readable; not parsed by other commands |
<csv>.stats.csv.data.jsonl |
qsv stats --stats-jsonl |
The actual cache used by frequency, schema, validate, pragmastat, pivotp, sqlp scoresql, tojsonl, sample, describegpt
|
<csv>.stats.csv.json |
qsv stats |
stats itself β records the arguments and qsv version used to build the cache; checked on the next run to decide whether the cache is still valid or must be recomputed |
<csv>.freq.csv.data.jsonl |
qsv frequency --frequency-jsonl |
frequency itself (cache hits), describegpt, scoresql, viz
|
<csv>.schema.json |
qsv schema |
validate |
<csv>.pschema.json |
qsv schema --polars |
sqlp, joinp, pivotp
|
my_file.csv β source
my_file.csv.idx β row-offset index
my_file.stats.csv β human-readable stats CSV
my_file.stats.csv.data.jsonl β machine-readable JSONL (the actual cache)
my_file.stats.csv.json β cache signature (args + qsv version) used for invalidation
The minimum to be useful:
qsv stats --stats-jsonl my_file.csvThe full version for max downstream benefit:
qsv stats --everything --infer-dates --infer-boolean --cardinality --stats-jsonl my_file.csvOr β set an env var to let qsv create it automatically the next time a smart command needs it:
export QSV_STATSCACHE_MODE=forceThree modes:
-
auto(default) β use the cache if it exists and is fresh -
forceβ auto-create the cache on first smart-command invocation -
noneβ ignore the cache entirely
-
frequencyβ short-circuits all-unique columns (rowcount == cardinality) using a sentinelALL_UNIQUEvalue; needs the cache to handle ID columns without OOM -
schemaβ skips redundant type inference; faster generation -
validateβ uses inferred ranges, formats, and patterns from the cache -
pragmastatβ date-aware mode requires the cache (stats -E --infer-dates --stats-jsonl) -
pivotpβ smart aggregation auto-selection based on data type -
tojsonlβ smart JSON type inference (string/number/bool/null per column) -
sampleβ extra checks for--systematic,--weighted,--clustermodes -
sqlp scoresqlβ query plan analysis (filter selectivity, join cardinality) -
describegptβ deterministic statistical context for the LLM
The cache is timestamp-keyed against the source CSV. If the source file's mtime changes, the cache is considered stale and silently regenerated (unless QSV_STATSCACHE_MODE=none).
Important
Two validators, two files β they are not the same check. stats itself also compares the arguments and the qsv version recorded in the <csv>.stats.csv.json signature sidecar before reusing its own .stats.csv. Downstream consumers read <csv>.stats.csv.data.jsonl and judge it by mtime and parsing options only β no version check. So after a qsv upgrade, stats recomputes while a .data.jsonl written by the older version keeps being served to schema, frequency, tojsonl and viz smart until the source CSV's mtime changes. On upgrade, regenerate deliberately: qsv stats --force --stats-jsonl <file>.
New in 24.0.0: moarstats and pragmastat now also treat the baseline as stale when its .stats.csv.json sidecar was written by a different qsv version, so their appended columns are recomputed after an upgrade (#4655). --flexible is part of the parsing-options key: a cache built from ragged data under --flexible is refused to a strict run, while a strict cache can still serve a --flexible run (#4616). And the .stats.csv / .stats.csv.data.jsonl pair is never more readable than its source CSV β a 0600 input gets 0600 caches, a world-readable one gets reusable ones (#4622). For a symlinked input, stats --stats-jsonl and moarstats now write the cache beside the link's canonical target, where readers look for it β before, viz smart --smarter re-ran plain stats on the link and silently dropped the moarstats enrichment (#4697).
Deliberate bust:
# Delete the whole set, including the .json signature sidecar, so a signature
# never outlives the statistics it describes.
rm my_file.stats.csv my_file.stats.csv.data.jsonl my_file.stats.csv.json
# Or force a rerun:
qsv stats --force --stats-jsonl my_file.csvImportant
New in 23.0.1. Three stats-cache bugs are fixed. All three were silent β no error, exit code 0 β and every stats-cache consumer (schema, frequency, viz smart, describegpt, tojsonl, validate, pragmastat, β¦) inherited the results. Regenerate any stats cache built by 22.0.1 or earlier with qsv stats --force --stats-jsonl.
1. An args-changed recompute left a stale sidecar behind (#4439). Deleting <FILESTEM>.stats.csv on an args change did not delete its <FILESTEM>.stats.csv.json signature sidecar, so the next run validated against the stale sidecar and trusted it. The reproducer is three ordinary runs:
qsv stats f.csv -E -c 1 # writes the full cache
qsv stats f.csv --typesonly # args changed: CSV deleted, sidecar survives
qsv stats f.csv -E # served the --typesonly output as though it were --everythingschema, frequency, tojsonl and viz smart all inherited that wrong answer. The sidecar is now deleted with its CSV, as a unit.
2. A CSV with an empty column name broke every stats-cache consumer (#4410, #4412). The CSVβJSON conversion behind the cache omitted any key whose cell was empty, so a column with an empty name lost its field key β and field was the only non-optional string in the cache record, so the cache then failed to deserialize with missing field `field` . Reported against tojsonl, but schema and frequency failed identically. field is now always emitted, keyed by name rather than position, so a future reordering of the stats columns cannot silently reintroduce it. field is also now #[serde(default)], so the poisoned caches already written to disk β which still pass mtime validation after an upgrade β keep loading rather than hard-failing.
3. Date renderings are no longer coerced to 0.0 in the cache (#4441). On a Date/DateTime column, cells like median are type-dependent RFC3339 renderings, not numbers, but were typed as floats for the JSON conversion β which coerced them to 0.0. They are now carried losslessly as strings; a consumer that wants a number parses it, using the row's type to know that's meaningful. Pre-fix caches stay on disk and still pass mtime validation, so the fabricated zeros a pre-fix cache carries for a date row are dropped at load time β only a rendering that reads as a number on a date row is dropped, so this is a no-op for any cache written after the fix.
my_file.freq.csv.data.jsonl β frequency distribution per column
qsv frequency --frequency-jsonl my_file.csvStored as JSONL with metadata per column. Two sentinel values appear for memory-bounded columns:
-
ALL_UNIQUEβ whenrowcount == cardinality(typical of ID columns) -
HIGH_CARDINALITYβ when cardinality exceeds the smaller ofQSV_FREQ_HIGH_CARD_THRESHOLD(default 100) andQSV_FREQ_HIGH_CARD_PCT(default 90%) of rowcount
-
describegptβ frequency context for LLM data dictionaries -
sqlp scoresqlβ filter-selectivity scoring
Same as the stats cache: stale if source CSV's mtime is newer. The cache is NOT used when --ignore-case, --no-trim, or --weight are active (those change how values are bucketed). Use --force to regenerate.
Note
New in 24.0.0: the frequency cache also checks the qsv version that wrote it (#4618). A plain frequency run that finds a cache from another version, for the same column selection, recomputes and rewrites it in place β it refreshes an existing cache but never creates one. viz rejects a version-stale cache without rewriting it and computes its own. --flexible is part of the key too (see above).
my_file.pschema.json β Polars schema (column types + Polars dtypes)
qsv schema --polars my_file.csvEvery Polars-powered command: sqlp, joinp, pivotp (and lens, count, color, prompt, scoresql when reading Polars-supported formats).
Pre-computed Polars dtypes mean Polars skips the inference scan it would otherwise do. Big speedup for repeated queries against the same file. Generate it once, check it into git, every Polars command becomes faster.
If the file's columns change, regenerate:
qsv schema --polars --force my_file.csvWarning
22.0.1 migrated the cached crate 2.0.2 β 3.0.0, swapping the on-disk cache backend from sled to redb. Two things change for you β one is a one-time cost, the other silently inverts a setting you may already have set.
Affected caches: fetch/fetchpost (--disk-cache & Redis), geocode (the OpenCage result cache), apply summarize, and describegpt.
redb cannot read sled's format, so entries are recomputed on first run after upgrading. Expect one slow run, then normal speed. Redis caches self-heal; nothing to do.
| Setting | Under sled (β€ 21.1.0) | Under redb (22.0.1+) |
|---|---|---|
TTL of 0
|
Every entry immediately stale (effectively no caching) | Cache indefinitely β time-based expiration disabled |
This applies to QSV_DISKCACHE_TTL_SECS, QSV_REDIS_TTL_SECS, geocode --cache-ttl, and apply summarize. cached v3 rejects a zero TTL at build time, so 0 had to be given a meaning β and it got the opposite one.
Caution
If you used TTL=0 as a way to disable caching, it now does the reverse and caches forever. Use --no-cache (geocode/apply), or simply omit the cache-enable flags, to actually turn caching off.
-
geocode cache-inforeports the entry count as"unknown", not0.RedbCache::cache_size()isOk(None)by design, and reporting0would be actively misleading. - The recomputable external-API caches (fetch/fetchpost disk cache, geocode OpenCage lookups) keep sled's non-durable behavior via
.durable(false), avoiding an fsync per write on the per-row hot paths.
Four cache backends, chosen via CLI flag:
-
In-memory LRU (default) β non-persistent, 2M entries, lost on process exit. Tune with
--mem-cache-size. -
Disk (
--disk-cache) β stored at~/.qsv-cache/fetch/(or--disk-cache-dir). TTL: 28 days (QSV_DISKCACHE_TTL_SECS). -
Redis (
--redis-cache) β shared across machines. Defaultredis://127.0.0.1:6379/1. TTL: 28 days (QSV_REDIS_TTL_SECS). -
None (
--no-cache) β for live data.
Cache keys are the URL + GET parameters. Two calls to the same URL return the cached value (within TTL).
Cache keys include the POST body β so two identical posts share a cache slot.
Cache keys include the prompt and the data context β so identical questions against unchanged data are cached.
By TTL or by manual delete:
# Disk cache
rm -rf ~/.qsv-cache/fetch/
# Redis cache (database 1 for fetch, 2 for fetchpost, 3 for describegpt)
redis-cli -n 1 FLUSHDBOr set QSV_DISKCACHE_TTL_REFRESH / QSV_REDIS_TTL_REFRESH to refresh TTL on cache hit (keeps hot URLs cached indefinitely).
See HTTP & Web and Recipe: Fetch & Cache.
When a Luau script calls qsv_register_lookup("table", "URL_OR_PATH"):
- Local files are read directly.
- Remote URLs (http://, https://, dathere://, ckan://) are downloaded and cached in
$QSV_CACHE_DIR(default~/.qsv-cache/).
The cache key includes the URL. Subsequent runs reuse the cached copy until you delete it.
See Lookup Tables and Scripting (Luau / Python) β luau.
| Cache | Bust by |
|---|---|
| Stats / frequency / Polars schema / JSON schema |
qsv <cmd> --force <file> or rm <file>.stats.csv* etc. |
| Source-CSV-driven (auto) | Modify the source CSV (mtime change auto-invalidates) |
| Index |
rm <file>.idx or qsv index --force <file>
|
| HTTP disk cache | rm -rf ~/.qsv-cache/fetch/ |
| HTTP Redis cache | redis-cli -n <db> FLUSHDB |
| Lookup-table cache | rm -rf $QSV_CACHE_DIR/lookup-tables/ |
| All qsv-generated caches beside a file/tree |
qsv clean --force (dry-run by default; --stale for changed/orphaned only) |
-
Commit
.stats.csv.data.jsonland.pschema.jsonto git for reference datasets you query repeatedly. Tiny files, huge speedup. -
Don't commit
.idxβ they're regenerated cheaply and platform-specific. -
In CI, prefer
QSV_STATSCACHE_MODE=forceβ eliminates "did I remember to run stats first?" mistakes. - For multi-stage Make-based pipelines, list the cache files as explicit dependencies so
makeinvalidates them when the source changes.
- Performance Tuning β when each cache matters
-
Environment Variables β
QSV_STATSCACHE_MODE,QSV_CACHE_DIR, TTL vars -
docs/PERFORMANCE.mdβ canonical performance reference -
docs/STATS_DEFINITIONS.mdβ what's in the stats cache - Aggregation & Statistics β stats
- Validation & Schema β schema
-
SQL & Polars β
.pschema.jsonforsqlp/joinp/pivotp - HTTP & Web β fetch / fetchpost cache modes
- Lookup Tables β Luau cache
- Recipe: Build a Data Pipeline β practical cache layout