kevy4.0

Views (VIEW.* / view_*)

A view is a named composition of declared indexes — an AND/OR/DIFF tree of index shapes with an ordering index — queryable as one unit, either evaluated per query (virtual) or maintained incrementally on every write (materialized, optionally top-K bounded). Views are the engine's answer to the "hot list" query — WHERE state = 'ready' AND pri BETWEEN 0 AND 100 ORDER BY pri DESC LIMIT 10 — as a declared access path instead of a query-time scan.

IDX.CREATE j_pri  ON PREFIX job: FIELD pri  TYPE i64 KIND range
IDX.CREATE j_state ON PREFIX job: FIELD state TYPE str KIND range

VIEW.CREATE ready_jobs
    QUERY ( AND j_pri RANGE 0 100 j_state EQ ready )
    ORDER BY j_pri DESC
    MODE materialized TOPK 100
VIEW.QUERY ready_jobs LIMIT 10

Quick start (server)

Everything a view composes must exist first — leaves and the ORDER BY index are declared IDX.* indexes over the same prefix domain (indexes.md):

kevy-cli -p 6004 IDX.CREATE j_pri ON PREFIX job: FIELD pri TYPE i64 KIND range
kevy-cli -p 6004 IDX.CREATE j_state ON PREFIX job: FIELD state TYPE str KIND range
kevy-cli -p 6004 VIEW.CREATE ready_jobs QUERY ( AND j_pri RANGE 0 100 j_state EQ ready ) ORDER BY j_pri DESC MODE materialized TOPK 100
kevy-cli -p 6004 HSET job:1 pri 10 state ready
kevy-cli -p 6004 HSET job:2 pri 99 state blocked
kevy-cli -p 6004 VIEW.QUERY ready_jobs LIMIT 10

The reply pages key, order-value pairs in view order with a resume CURSOR. The tree grammar (one line, parenthesized):

tree     = '(' AND|OR|DIFF sub sub ')' | leaf
leaf     = <index> RANGE <min> <max> | <index> EQ <value>

Supporting verbs: VIEW.LIST (catalog + mode + shape), VIEW.EXPLAIN name (the tree with per-leaf cardinalities), VIEW.VERIFY name (members / bytes / order-exclusions — bytes and order-exclusions are materialized-only; a virtual view stores nothing and reports both as 0, and verifying one costs a full fresh eval_tree, which is the one place a virtual view is the expensive one), VIEW.REBUILD name (force a materialized rebuild — answer- preserving), VIEW.DROP name.

The three structural rules

  1. Components are named indexes. Leaves carry a shape (RANGE min max | EQ v, coerced to the referenced index's type at CREATE); the view layer holds no predicates of its own. Trees are depth ≤ 3, ≤ 4 leaves; AND/OR may be re-ordered by the engine, DIFF is fixed left-minus-right.
  2. A view stores membership + order only — never field values. ORDER BY <index> supplies the sort key; rows absent from the order index are excluded (counted, visible in VIEW.VERIFY).
  3. Hydration is dereference, not query. VIA <template> (e.g. user:{key.1}; {key} = member key, {key.N} = its N-th :-segment) maps each member to a target key; VIEW.QUERY … FIELDS f… reads those fields on the targets' owning shards in a second internal fan-out. Missing target = nil row fields. Targets take no predicates.

Modes and cost model

Choosing: a dashboard top-100 over a high-write domain wants MODE materialized TOPK 100 (bounded memory, O(1) rejection of non-contenders); an occasionally-paged report over a moderate domain wants MODE virtual (zero write tax, always fresh). VIEW.EXPLAIN shows per-leaf cardinalities — the selectivity data for the call.

DIFF: the declared anti-join

DIFF is left-minus-right and NOT commutative — the one tree node whose children the engine never re-orders. The canonical use is "eligible but not excluded":

VIEW.CREATE assignable
    QUERY ( DIFF j_pri RANGE 0 100 j_state EQ quarantined )
    ORDER BY j_pri DESC
    MODE virtual

— every job in the working priority band, minus the quarantined ones.

In SQL terms this is the WHERE … AND NOT EXISTS (…) shape — as a declared access path instead of a per-query subquery (rds-workloads.md maps the rest of that vocabulary).

Consistency

Same envelope as indexes (indexes.md): per-shard atomic with the triggering write, cross-shard merged without a global snapshot (SCAN-class).

Embedded

Typed API behind the index cargo feature (on by default) — the tree is passed as a value, no text grammar in-process, and every call returns KevyResult (4.0's single error currency):

use kevy_embedded::{
    Config, IndexKind, IndexValType, IndexValue, Store, ViewLeaf,
    ViewMode, ViewTree,
};

fn main() -> kevy_embedded::KevyResult<()> {
    let store = Store::open(Config::default())?;
    store.idx_create(b"j_pri", b"job:", b"pri", IndexValType::I64,
                     IndexKind::Range)?;
    store.idx_create(b"j_state", b"job:", b"state", IndexValType::Str,
                     IndexKind::Range)?;

    let tree = ViewTree::And(
        Box::new(ViewTree::Leaf(ViewLeaf {
            index: b"j_pri".to_vec(),
            min: IndexValue::I64(0),
            max: IndexValue::I64(100),
        })),
        Box::new(ViewTree::Leaf(ViewLeaf {
            index: b"j_state".to_vec(),
            min: IndexValue::Str(b"ready".to_vec()),
            max: IndexValue::Str(b"ready".to_vec()),   // EQ = same min/max
        })),
    );
    store.view_create(b"ready_jobs", tree, b"j_pri", /*desc*/ true,
                      ViewMode::Materialized { top_k: 100 })?;

    store.hset(b"job:1", &[
        (b"pri".as_slice(), b"10".as_slice()),
        (b"state".as_slice(), b"ready".as_slice()),
    ])?;

    // One page: (Vec<(key, order_value)>, Option<resume_cursor>).
    let (rows, _next) = store.view_query(b"ready_jobs", None, 10)?;
    for (key, val) in &rows {
        println!("{} {val:?}", String::from_utf8_lossy(key));
    }
    Ok(())
}

Performance

Measured envelope — the clamps live in bench/viewgate.sh, run against a real server at 1M rows:

Memory per member ≈ order_value_width + key_len + 48 bytes, reported live by VIEW.VERIFY.

See also