vike
ConceptsData

Store kinds

The layout contract every stored series obeys, the thirteen kinds declared today, and the gate that derives the live set from the code rather than from this table.

Everything Vike Trader keeps on disk as history lives under a store root in a kind= path segment, and that segment is a contract between two ends that never meet: the live recorder, the property and chain recorders, the backfill collectors and the journal materializer on one side; tick replay, the reads a data node serves, and the Data Manager and Studio inventory views on the other. crates/vike-data/src/store_kind.rs's STORE_KINDS is where that contract is written down — one row per stored kind, thirteen of them today, each with its own page here. That producer list is illustrative and not a census: a producer is anything holding a HistStore, so the header declines to claim any list can be complete.

Until that file existed, no site named the contract, and its header records what that cost. crates/vike-data/src/coverage.rs's TICK_KINDS was missing depth, so a whole kind was invisible to the cross-kind coverage report while its rows sat on disk. Five Data Manager verbs built their path from SeriesId::symbol, which is empty for a grouped series, so each silently operated on a series that does not exist — delete was a no-op that returned Ok(()).

What a row declares

One StoreKind names the domain type a decoded row becomes, every Arrow column today's writer emits in schema order with the series_codec! flavor that picks its type and nullability, the Parquet schema-metadata key the kind stamps when it stamps one, how the leaf is partitioned, whether a grouped form exists, what venue and symbol actually mean for that kind, and the commit-key shapes its known producers use.

Two column flavors bite a consumer. A _null column is always present and may hold SQL NULL; an _add column may be absent entirely from a part sealed before it was added, because the _add flavors read through an absent-tolerant column reader. A kind's columns are what today's writer emits, so nothing may assume an _add column is present in an old part.

Partitioning, stated once

Parts are date=-partitioned identically for every kind — <series leaf>/date=YYYY-MM-DD/ — so no per-kind page repeats it. What the partition guarantees is that a part never spans a UTC day: crates/vike-data/src/datafusion_hist/manifest.rs's seal_into_manifest groups a batch's rows by day and seals one part per group.

A date partition holds many parts, not one

A writer seals one part per commit, so the live recorder — committing per buffer flush — produces one every few seconds, and compaction merges them into part-c… outputs alongside. Measured read-only on the live production tape: kind=book/venue=polymarket/group=btc-updown-5m/date=2026-08-08 held 103 parquet files and date=2026-08-07 held 130. manifest.rs's next_part_name is the authority — a per-date index, one above the highest still live, a counter precisely because there are many. A consumer that builds a reader for one-file-per-day mishandles the real 130-file case.

Below the shared kind=…/venue=… prefix the leaf has two spellings. The per-symbol form is symbol=…, and bar alone adds interval=… beneath it so a 1m and a 1d series of one instrument never interleave — the table's only Partition::SymbolInterval row. The grouped form is kind=…/venue=…/group=…, written by DataFusionHist's group_dir, where one part holds many symbols told apart by the row-level symbol_col column. The two are siblings under one parent and a store can hold both at once, so anything building or parsing a path must branch on group first — series_dir_of is the one function that does it correctly, and the five verbs that did not are the incident above. In the same vein, SeriesId::symbol is empty for a grouped series, SeriesId::group carries the name, and SeriesId::label answers for both.

Declare today's reality; pin the contradictions

The table is step one of the workspace's two-step playbook for a capability table: it merged byte-identical in behaviour and writes down what the code does today, including where two sites disagree. Each disagreement is named in a row's notes with both sides cited, and a test asserts it still exists, so whoever resolves one is told to update the table rather than leave a stale warning behind. Two rows carry such a note today. The depth row's is labelled a pinned contradiction: crates/vike-backfill/src/regroup.rs's GROUPABLE_KINDS lists depth as having a grouped form and the store has none. The funding row's is labelled a pinned name collision — two unrelated series are called funding: kind=funding is the account's realized payments, while the market funding-rate history is a bar series under the reserved interval=funding label, carrying the rate on Bar::funding with OHLCV and volume all zero.

The gate's pinned section holds four tests, not two. The other pair pins no row disagreement: both_depth_defaults_refuse and both_catalog_defaults_refuse hold append_depth/scan_depth, and list_series/inventory, to an Err rather than a fabricated empty read.

This is deliberately not schema versioning. crates/vike-data/src/store_kind.rs's header is emphatic about the difference and says versioning must stay deferred: a row records a metadata key a kind already writes, and introduces no version negotiation, no migration and no new on-disk byte.

The gate

crates/vike-data/tests/store_kind_gate.rs derives the live kind set from the code and fails until a row exists — three independent derivations, because each is blind to a different mistake: the kind= segments the store actually writes, read off the first argument of every series_dir/ticks_dir/group_dir call in the backend module tree; the strings each compaction dispatch accepts, each dispatch checked on its own; and the fn append_* verbs on the always-compiled HistStore trait, which reddens first when a kind is added the ordinary way. Shape is gated separately, by parsing the series_codec! invocations and comparing each row's columns and schema-metadata key against the field list they were read from. Every check reads source text on purpose — the codecs sit behind the hist-datafusion feature while the test runs in the default lane, and regroup.rs is in a crate vike-data cannot depend on.

Two residuals are declared, and the declaration is the point. Dataflow is not followed, and that is measured: a SeriesId with kind: "oracle" handed to the declared series_dir_of site builds a kind=oracle directory with no compaction arm and no append verb while the gate stays at 25 passed, 0 failed. And the scope is this crate's src — another crate holding a store root can build a directory with a plain join, and a store written by a newer build already holds kinds no source here mentions.

The thirteen kinds

Four line up in the cross-kind coverage report — TICK_KINDS is trade, quote, book and depth. Three kind ids carry underscores on disk (exec_fill, exec_order, perp_metrics) where their page slugs use hyphens. Most seam verbs state their own default on the trait method; the one pair worth carrying here is depth, where both append_depth and scan_depth return an Err in a store that does not implement them, because a store with no depth lane and a store with no depth rows must not read identically to a caller.

bar · quote · trade · book · depth · properties · equity · exec-fill · exec-order · funding · chain · cohort · perp-metrics

The seam itself, and where a store root resolves when nobody says, are on The history store; its live mirror is the live data seam. What fills a store is data providers and store maintenance; what reads it back is tick replay and the Data Manager's coverage view.

On this page