Packages
reckon_db
5.11.0
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
src/reckon_db_stream_path.erl
%% @doc The single source of truth for the Khepri stream layout.
%%
%% reckon-db stores events under a **structural aggregate-type subtree**
%% (DESIGN_STREAM_NAMESPACE.md, Model C):
%%
%% ```
%% USER: [streams, <<"ride">>, <<"abc…hex">>, PaddedVersion] -> #event{}
%% SYSTEM: [streams, <<"$link">>, <<"name">>, PaddedVersion] -> #event{}
%% DCB: [streams, <<"_dcb">>, SeqKey] -> #event{}
%% '''
%%
%% The aggregate **type** (the user-id prefix, or the `$'-namespace for
%% system streams) is a real Khepri path level rather than a leading
%% substring of an opaque id. This makes type-scoped operations
%% (`list_streams', replay, links) navigate a subtree instead of scanning
%% the whole store.
%%
%% This module is the ONLY place that knows the 4-level shape. Every other
%% module builds stream paths through these functions, so the layout lives
%% in exactly one file. The opaque round-trip — a stream id written under
%% `event_path/2' must reconstruct byte-identical via `stream_id_from_path/1'
%% — is the load-bearing correctness property and is exercised directly by
%% `reckon_db_stream_path_tests'.
%%
%% The DCB pseudo-stream (`_dcb') keeps its existing 2-level
%% `[streams, _dcb, SeqKey]' shape (a flat seq-keyed log, not an aggregate);
%% its paths are built by {@link reckon_db_dcb_paths}, but the readers here
%% (`stream_id_from_path/1') tolerate it because global reads span both
%% depths.
%%
%% @author rgfaber
-module(reckon_db_stream_path).
-include("reckon_db.hrl").
-include_lib("khepri/include/khepri.hrl").
-export([
event_path/2,
stream_path/1,
versions_pattern/1,
type_streams_pattern/1,
all_events_pattern/0,
stream_id_from_path/1,
type_of/1
]).
%%====================================================================
%% Path builders
%%====================================================================
%% @doc Khepri path of a single event:
%% `[streams, Type, Id, PaddedVersion]'.
%%
%% Raises `{invalid_stream_id, StreamId}' for ids that don't decompose
%% (including the DCB pseudo-stream, whose paths are built elsewhere).
-spec event_path(binary(), binary()) -> khepri_path:native_path().
event_path(StreamId, PaddedVersion) ->
{Type, Id} = id_nodes(StreamId),
[streams, Type, Id, PaddedVersion].
%% @doc Khepri path of the aggregate node (all versions live beneath it):
%% `[streams, Type, Id]'. Used by exists/delete and version counting.
-spec stream_path(binary()) -> khepri_path:native_path().
stream_path(StreamId) ->
{Type, Id} = id_nodes(StreamId),
[streams, Type, Id].
%% @doc Pattern matching every version node of one stream:
%% `[streams, Type, Id, *]'. Used by `get_version' (count).
-spec versions_pattern(binary()) -> khepri_path:native_pattern().
versions_pattern(StreamId) ->
{Type, Id} = id_nodes(StreamId),
[streams, Type, Id, ?KHEPRI_WILDCARD_STAR].
%% @doc Pattern matching every aggregate-id node of one type:
%% `[streams, Type, *]'. Used by links to navigate a type subtree
%% (`order-*') instead of scanning the whole store.
-spec type_streams_pattern(binary()) -> khepri_path:native_pattern().
type_streams_pattern(Type) when is_binary(Type) ->
[streams, Type, ?KHEPRI_WILDCARD_STAR].
%% @doc Pattern matching every regular (user/system) event leaf across
%% the store: `[streams, *, *, leaf-with-data]'. This is the 4-level
%% regular shape ONLY — the 2-level DCB log is at a different depth and
%% must be queried separately (see `reckon_db_streams' global readers).
-spec all_events_pattern() -> khepri_path:native_pattern().
all_events_pattern() ->
[streams,
?KHEPRI_WILDCARD_STAR,
?KHEPRI_WILDCARD_STAR,
#if_all{conditions = [
?KHEPRI_WILDCARD_STAR,
#if_has_data{has_data = true}
]}].
%%====================================================================
%% Path readers (the inverse — reconstruct the opaque id)
%%====================================================================
%% @doc Reconstruct the opaque stream id from a Khepri path. The inverse
%% of `event_path/2' / `stream_path/1'. Tolerates the 2-level DCB node
%% (returns the reserved `_dcb' id) so global readers spanning both
%% depths can label every result.
-spec stream_id_from_path(khepri_path:native_path()) -> binary().
stream_id_from_path([streams, ?DCB_STREAM | _]) ->
?DCB_STREAM;
stream_id_from_path([streams, Type, Id | _]) when is_binary(Type), is_binary(Id) ->
recombine(Type, Id);
stream_id_from_path(_) ->
<<>>.
%% @doc The aggregate-type node for a stream id (`<<"ride">>',
%% `<<"$link">>'). Raises for ids that don't decompose.
-spec type_of(binary()) -> binary().
type_of(StreamId) ->
{Type, _Id} = id_nodes(StreamId),
Type.
%%====================================================================
%% Internal
%%====================================================================
%% @private Split a stream id into its {TypeNode, IdNode} Khepri path
%% segments. System streams keep their leading `$' on the type node so
%% the `$'-namespace stays structurally reserved and distinct from any
%% user type of the same name.
-spec id_nodes(binary()) -> {binary(), binary()}.
id_nodes(StreamId) ->
case reckon_gater_stream_id:parts(StreamId) of
{user, Type, Id} ->
{Type, Id};
{system, Ns, Name} ->
{<<"$", Ns/binary>>, Name};
{error, malformed} ->
erlang:error({invalid_stream_id, StreamId})
end.
%% @private Rejoin a {TypeNode, IdNode} pair into the opaque id. System
%% type nodes start with `$' and rejoin with `:'; user type nodes rejoin
%% with `-'.
-spec recombine(binary(), binary()) -> nonempty_binary().
recombine(<<"$", _/binary>> = Type, Name) ->
<<Type/binary, ":", Name/binary>>;
recombine(Type, Id) ->
<<Type/binary, "-", Id/binary>>.