Packages
reckon_db
5.5.2
5.11.0
5.10.4
5.10.3
5.10.1
5.10.0
5.9.1
5.9.0
5.8.3
5.8.2
5.8.1
5.8.0
5.7.0
5.6.1
5.6.0
5.5.5
5.5.4
5.5.3
5.5.2
5.5.1
5.5.0
5.4.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.0.0
3.1.2
3.1.1
3.0.0
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.2
2.2.0
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.0
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
BEAM-native Event Store built on Khepri/Ra with Raft consensus. Event sourcing, persistent subscriptions, snapshots, and automatic cluster formation via UDP multicast discovery. Ships embedded Rust NIFs for 3-15x acceleration of crypto, hashing, compression, aggregation, filter matching, and grap...
Current section
Files
Jump to
Current section
Files
guides/ccc.md
# CCC Payload Indexes
*Available in reckon-db 5.3.0+.*
CCC (Command Context Consistency) payload indexes extend the DCB conditional-append
primitive with query predicates over event data fields. This document covers the
reckon-db implementation: store configuration, what gets indexed, and the Horus
constraint that shapes the API.
For the conceptual framing — what CCC is, how it relates to DCB, and worked
end-to-end examples — see the
[reckon-gater CCC guide](https://codeberg.org/reckon-db-org/reckon-gater/src/branch/main/guides/ccc.md).
---
## Declaring Indexes
Payload indexes are opt-in per store. Declare them in the `store_config` at
store creation time (or add them before writing events that need to be indexed):
```erlang
Indexes = [
tags, %% always-on tag index
event_type, %% always-on event-type index
{payload, <<"account_id">>}, %% single-field payload index
{payload_hash, [<<"flight_id">>, <<"seat_no">>]} %% composite hash index
],
reckon_gater_api:create_store(StoreId, #{indexes => Indexes}).
```
### `{payload, Key}`
Builds a `[by_payload, Key, Value, SeqKey]` index entry for every event whose
JSON data contains `Key` with a binary (string) value. Enables
`{payload_match, Key, Value}` filters in `append_if_no_tag_matches` and
`dcb_read_context`.
One Khepri subtree lookup per distinct key-value pair checked. Correct when the
command narrows its window by one field value.
### `{payload_hash, [K1, K2, ...]}`
Builds a `[by_payload_hash, Hash, SeqKey]` index entry for every event whose
JSON data contains all of K1, K2, … with binary values. The hash is
`SHA-256(term_to_binary(lists:sort(zip(Keys, Values))))` — field-order
independent. Enables `{payload_hash_match, Keys, Values}` filters.
One Khepri lookup regardless of how many fields are in the combination. Correct
for multi-field uniqueness checks (e.g., "no other reservation for this flight
and seat").
---
## Filter Shapes
Two new shapes added to `tag_filter()` in 5.3.0:
```erlang
{payload_match, Key :: binary(), Value :: binary()}
%% Matches events where data[Key] == Value (string comparison).
%% Requires {payload, Key} declared in the store.
{payload_hash_match, Keys :: [binary()], Values :: [binary()]}
%% Matches events where the combination (Keys, Values) hashes identically
%% to the indexed combination. All supplied keys must be present in the
%% event data as binary values.
%% Requires {payload_hash, Keys} declared in the store.
```
Both compose freely with `and_`, `or_`, `any_of`, `all_of`, and `event_type`:
```erlang
%% event_type=seat_reserved_v1 AND (flight_id, seat_no) == (FL001, 12A)
Filter = {and_, [
{event_type, <<"seat_reserved_v1">>},
{payload_hash_match,
[<<"flight_id">>, <<"seat_no">>],
[<<"FL001">>, <<"12A">>]}
]}.
```
---
## The Horus Constraint
The consistency check runs inside a `khepri:transaction/2` body that Horus
extracts into a portable Raft log entry. Horus rejects any code that calls
`crypto:*` — including SHA-256 — even on branches that can never execute.
`{payload_hash_match, Keys, Values}` requires hashing. The solution: call
`reckon_db_ccc_filter:preprocess_filter/1` **before** entering the transaction.
It rewrites `{payload_hash_match, Keys, Values}` into
`{payload_hash_match_pre, Hash}`, where `Hash` is the pre-computed SHA-256.
The transaction body sees only the pre-computed value.
`append_if_no_tag_matches/4` does this automatically. If you write your own
transaction that calls `reckon_db_ccc_filter:match_any_above_cutoff/3`, call
`preprocess_filter/1` first:
```erlang
ProcessedFilter = reckon_db_ccc_filter:preprocess_filter(TagFilter),
%% Now safe to use ProcessedFilter inside khepri:transaction/2
```
`{payload_match, Key, Value}` does **not** involve hashing and requires no
preprocessing.
---
## What Is Not Indexed
- Non-binary JSON values (numbers, booleans, objects, arrays, null). Cast to
binary at write time if you need them queryable.
- Absent fields. Events missing the declared key produce no index entry and
are invisible to that filter.
- Nested JSON fields. Only top-level keys are indexed.
- Events written before the index was declared. Retroactive re-indexing is not
automatic; re-write events through `append_if_no_tag_matches/4` if you need
historical coverage.
---
## Implementation Modules
| Module | Purpose |
|--------|---------|
| `reckon_db_index_config` | Validates and stores index declarations; `declared_dcb_payload/1` extracts payload declarations |
| `reckon_db_ccc_paths` | Khepri path builders for payload indexes (`by_payload_path/3`, `by_payload_hash_path/2`, `payload_combo_hash/2`) |
| `reckon_db_ccc_filter` | Filter evaluation: `match_seqs/2`, `match_any_above_cutoff/2,3`, `preprocess_filter/1` |
| `reckon_db_dcb` | `extract_payload_entries/2` (outside tx), `write_one_record/3` (writes index entries inside tx) |
---
## See also
- [DCB guide](dcb.md) — the full filter algebra and the conditional-append API
- [DCB Raft design](dcb_raft_design.md) — the Horus constraint and the pre-stamp pattern
- [reckon-gater CCC guide](https://codeberg.org/reckon-db-org/reckon-gater/src/branch/main/guides/ccc.md) — conceptual framing, worked examples, literature