Skip to main content

krabka_client_streams/dsl/
kgrouped_table.rs

1//! `KGroupedTable<KR, VR>`, the handle between `KTable::group_by` and a terminal
2//! table aggregation such as `count`, `reduce`, or `aggregate`.
3//!
4//! Unlike `KGroupedStream`, the input is a `Change<V>` change-stream, and the
5//! repartition topic carries a `Change<VR>` through the `Changed` serde.
6//! `KTable.groupBy` always repartitions.
7
8use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
9
10use crate::{
11    dsl::{
12        builder::InternalStreamsBuilder,
13        config::Materialized,
14        graph::{GraphNodeKind, LowerState, NodeId},
15        kgrouped::mint_store_name,
16        ktable::KTable,
17        names,
18        processors::{
19            change::Change, table_aggregate::KTableAggregateProcessor,
20            tuple_forwarder::TupleForwarder,
21        },
22    },
23    processor::serde::{Changed, Consumed, DefaultSerde, I64Serde, Produced, Serde},
24    topology::NodeHandle,
25};
26
27/// The erased thunk that wires the `Change`-carrying repartition
28/// `sink → topic → source`.
29///
30/// Its arguments are `(state, parent_name, sink_name, source_name, topic)`. The
31/// parent node here forwards `Record<KR, Change<VR>>`, which is the SELECT
32/// output.
33pub(crate) type ChangedRepartitionLowerFn =
34    Box<dyn FnOnce(&mut LowerState, String, String, String, String) + Send>;
35
36/// Build a [`ChangedRepartitionLowerFn`] that captures the grouped key serde and
37/// a `Changed`-wrapped value serde, so the repartition round-trip carries
38/// `Change<VR>`.
39pub(crate) fn repartition_lower_changed<KR, VR, KS, VS>(
40    key_serde: KS,
41    value_serde: VS,
42) -> ChangedRepartitionLowerFn
43where
44    KR: Any + Send + Sync + Clone,
45    VR: Any + Send + Sync + Clone,
46    KS: Serde<KR> + Clone + 'static,
47    VS: Serde<VR> + Clone + 'static,
48{
49    Box::new(
50        move |state: &mut LowerState,
51              parent_name: String,
52              sink_name: String,
53              source_name: String,
54              topic: String| {
55            let parent = NodeHandle::<KR, Change<VR>>::from_name(parent_name);
56            state
57                .topology
58                .add_sink_explicit::<KR, Change<VR>, KS, Changed<VS>, _, _>(
59                    sink_name,
60                    topic.clone(),
61                    [parent],
62                    Produced::with(key_serde.clone(), Changed::new(value_serde.clone())),
63                );
64            state.topology.add_repartition_topic(topic.clone());
65            state
66                .topology
67                .add_source_explicit::<KR, Change<VR>, KS, Changed<VS>>(
68                    source_name,
69                    [topic],
70                    Consumed::with(key_serde, Changed::new(value_serde)),
71                );
72        },
73    )
74}
75
76/// Handle produced by `KTable::group_by[_explicit]`.
77pub struct KGroupedTable<KR, VR> {
78    builder: Rc<RefCell<InternalStreamsBuilder>>,
79    /// The `KTABLE-SELECT` repartition-map node, which `group_by` already
80    /// recorded at call time.
81    ///
82    /// This mirrors the JVM, which mints the node when the caller calls
83    /// `groupBy()`, *before* the terminal op mints the result store. The terminal
84    /// op wires the repartition off this node.
85    select_node: NodeId,
86    repartition_lower: Option<ChangedRepartitionLowerFn>,
87    _pd: PhantomData<fn() -> (KR, VR)>,
88}
89
90impl<KR, VR> KGroupedTable<KR, VR>
91where
92    KR: Any + Send + Sync + Clone + PartialEq,
93    VR: Any + Send + Sync + Clone,
94{
95    pub(crate) fn new(
96        builder: Rc<RefCell<InternalStreamsBuilder>>,
97        select_node: NodeId,
98        repartition_lower: ChangedRepartitionLowerFn,
99    ) -> Self {
100        Self {
101            builder,
102            select_node,
103            repartition_lower: Some(repartition_lower),
104            _pd: PhantomData,
105        }
106    }
107
108    /// `count` into a materialized `KTable<KR, i64>`.
109    pub fn count_explicit<KS, VS>(
110        self,
111        materialized: impl Into<Materialized<KS, VS>>,
112    ) -> KTable<KR, i64, KS, VS>
113    where
114        KS: Serde<KR> + Clone + 'static,
115        VS: Serde<i64> + Clone + 'static,
116    {
117        self.aggregate_inner(
118            materialized.into(),
119            names::KTABLE_AGGREGATE_STORE,
120            || 0i64,
121            |_k: &KR, _v: &VR, a: i64| a + 1,
122            |_k: &KR, _v: &VR, a: i64| a - 1,
123        )
124    }
125
126    /// `reduce` folds each group with `adder` and undoes with `subtractor`.
127    ///
128    /// The result type stays `VR`, and the first value for a group seeds it. An
129    /// empty group starts at `VR::default()`, and `adder(default, first)` equals
130    /// `first` for the additive reduce. This is exact JVM parity. Use
131    /// `aggregate()` as the escape hatch for anything else.
132    pub fn reduce_explicit<KS, VS, Add, Sub>(
133        self,
134        adder: Add,
135        subtractor: Sub,
136        materialized: impl Into<Materialized<KS, VS>>,
137    ) -> KTable<KR, VR, KS, VS>
138    where
139        VR: Default,
140        KS: Serde<KR> + Clone + 'static,
141        VS: Serde<VR> + Clone + 'static,
142        Add: Fn(&VR, &VR) -> VR + Clone + Send + Sync + 'static,
143        Sub: Fn(&VR, &VR) -> VR + Clone + Send + Sync + 'static,
144    {
145        let materialized = materialized.into();
146        let store_name = mint_store_name(&self.builder, &materialized, names::KTABLE_REDUCE_STORE);
147        self.lower::<KS, VS, VR, _, _, _>(
148            materialized,
149            store_name,
150            VR::default,
151            move |_k: &KR, v: &VR, acc: VR| adder(&acc, v),
152            move |_k: &KR, v: &VR, acc: VR| subtractor(&acc, v),
153        )
154    }
155
156    /// `aggregate` is the general subtract-and-add aggregation into a
157    /// `KTable<KR, T>`.
158    pub fn aggregate_explicit<KS, VS, T, I, Add, Sub>(
159        self,
160        init: I,
161        adder: Add,
162        subtractor: Sub,
163        materialized: impl Into<Materialized<KS, VS>>,
164    ) -> KTable<KR, T, KS, VS>
165    where
166        T: Any + Send + Clone,
167        KS: Serde<KR> + Clone + 'static,
168        VS: Serde<T> + Clone + 'static,
169        I: Fn() -> T + Clone + Send + Sync + 'static,
170        Add: Fn(&KR, &VR, T) -> T + Clone + Send + Sync + 'static,
171        Sub: Fn(&KR, &VR, T) -> T + Clone + Send + Sync + 'static,
172    {
173        self.aggregate_inner(
174            materialized.into(),
175            names::KTABLE_AGGREGATE_STORE,
176            init,
177            adder,
178            subtractor,
179        )
180    }
181
182    /// `count` with default serdes.
183    pub fn count(
184        self,
185        store_name: impl Into<String>,
186    ) -> KTable<KR, i64, <KR as DefaultSerde>::Serde, I64Serde>
187    where
188        KR: DefaultSerde,
189        <KR as DefaultSerde>::Serde: Serde<KR> + Clone,
190    {
191        self.count_explicit(
192            Materialized::with(<KR as DefaultSerde>::Serde::default(), I64Serde)
193                .as_store(store_name),
194        )
195    }
196
197    /// `aggregate` with default serdes.
198    pub fn aggregate<T, I, Add, Sub>(
199        self,
200        init: I,
201        adder: Add,
202        subtractor: Sub,
203        store_name: impl Into<String>,
204    ) -> KTable<KR, T, <KR as DefaultSerde>::Serde, <T as DefaultSerde>::Serde>
205    where
206        T: DefaultSerde + Any + Send + Clone,
207        KR: DefaultSerde,
208        <KR as DefaultSerde>::Serde: Serde<KR> + Clone,
209        <T as DefaultSerde>::Serde: Serde<T> + Clone,
210        I: Fn() -> T + Clone + Send + Sync + 'static,
211        Add: Fn(&KR, &VR, T) -> T + Clone + Send + Sync + 'static,
212        Sub: Fn(&KR, &VR, T) -> T + Clone + Send + Sync + 'static,
213    {
214        self.aggregate_explicit(
215            init,
216            adder,
217            subtractor,
218            Materialized::with(
219                <KR as DefaultSerde>::Serde::default(),
220                <T as DefaultSerde>::Serde::default(),
221            )
222            .as_store(store_name),
223        )
224    }
225
226    fn aggregate_inner<KS, VS, T, I, Add, Sub>(
227        self,
228        materialized: Materialized<KS, VS>,
229        store_prefix: &'static str,
230        init: I,
231        adder: Add,
232        subtractor: Sub,
233    ) -> KTable<KR, T, KS, VS>
234    where
235        T: Any + Send + Clone,
236        KS: Serde<KR> + Clone + 'static,
237        VS: Serde<T> + Clone + 'static,
238        I: Fn() -> T + Clone + Send + Sync + 'static,
239        Add: Fn(&KR, &VR, T) -> T + Clone + Send + Sync + 'static,
240        Sub: Fn(&KR, &VR, T) -> T + Clone + Send + Sync + 'static,
241    {
242        let store_name = mint_store_name(&self.builder, &materialized, store_prefix);
243        self.lower::<KS, VS, T, I, Add, Sub>(materialized, store_name, init, adder, subtractor)
244    }
245
246    /// Record SELECT → repartition(Changed) → AGGREGATE and its store, then
247    /// return a `KTable<KR, T>`.
248    fn lower<KS, VS, T, I, Add, Sub>(
249        mut self,
250        materialized: Materialized<KS, VS>,
251        store_name: String,
252        init: I,
253        adder: Add,
254        subtractor: Sub,
255    ) -> KTable<KR, T, KS, VS>
256    where
257        T: Any + Send + Clone,
258        KS: Serde<KR> + Clone + 'static,
259        VS: Serde<T> + Clone + 'static,
260        I: Fn() -> T + Clone + Send + Sync + 'static,
261        Add: Fn(&KR, &VR, T) -> T + Clone + Send + Sync + 'static,
262        Sub: Fn(&KR, &VR, T) -> T + Clone + Send + Sync + 'static,
263    {
264        let Materialized {
265            key_serde,
266            value_serde,
267            logging,
268            caching,
269            ..
270        } = materialized;
271        let suppress_factory = crate::dsl::ktable::kv_suppress_factory::<KR, T, KS, VS>(
272            key_serde.clone(),
273            value_serde.clone(),
274        );
275        let rp_lower = self
276            .repartition_lower
277            .take()
278            .expect("repartition_lower consumed");
279        // The SELECT (repartition-map) node was recorded at `group_by` time so its
280        // name-counter index precedes this terminal op's store (JVM order).
281        let select_id = self.select_node;
282        let mut g = self.builder.borrow_mut();
283
284        // KTable.groupBy ALWAYS repartitions. Mint sink+source for the repartition
285        // (the JVM table-repartition path mints NO null-key filter — the
286        // repartition-map already emits keyed records), then a Repartition node
287        // fed by SELECT.
288        let sink_name = g.new_processor_name(names::SINK);
289        let source_name = g.new_processor_name(names::SOURCE);
290        let topic_store = store_name.clone();
291        let rp_id = g.graph.add(
292            source_name.clone(),
293            GraphNodeKind::Repartition {
294                topic: format!("{topic_store}{}", names::REPARTITION_SUFFIX),
295                partitions: None,
296            },
297            vec![select_id],
298        );
299        g.graph.nodes[rp_id].lower = Some(Box::new(move |state: &mut LowerState| {
300            let parent_name = state.handle_name[&select_id].clone();
301            let topic = format!(
302                "{}-{topic_store}{}",
303                state.app_id,
304                names::REPARTITION_SUFFIX
305            );
306            rp_lower(
307                state,
308                parent_name,
309                sink_name.clone(),
310                source_name.clone(),
311                topic,
312            );
313            state.handle_name.insert(rp_id, source_name.clone());
314        }));
315
316        // 3) AGGREGATE node fed by the repartition source.
317        let agg_name = g.new_processor_name(names::KTABLE_AGGREGATE);
318        let agg_id = g.graph.add(
319            agg_name.clone(),
320            GraphNodeKind::Aggregate {
321                store_name: store_name.clone(),
322                changelog: logging,
323            },
324            vec![rp_id],
325        );
326        let store_for_thunk = store_name.clone();
327        let key_serde_lower = key_serde.clone();
328        let value_serde_lower = value_serde.clone();
329        g.graph.nodes[agg_id].lower = Some(Box::new(move |state: &mut LowerState| {
330            let parent = NodeHandle::<KR, Change<VR>>::from_name(state.handle_name[&rp_id].clone());
331            let store_for_proc = store_for_thunk.clone();
332            let h = state
333                .topology
334                .add_processor::<KR, Change<VR>, KR, Change<T>, _, _, _>(
335                    agg_name.clone(),
336                    move || KTableAggregateProcessor {
337                        store_name: store_for_proc.clone(),
338                        init: init.clone(),
339                        adder: adder.clone(),
340                        subtractor: subtractor.clone(),
341                        forwarder: TupleForwarder::default(),
342                        _pd: PhantomData,
343                    },
344                    [parent],
345                );
346            if logging {
347                state.topology.add_state_store::<KR, T, KS, VS>(
348                    store_for_thunk.clone(),
349                    key_serde_lower.clone(),
350                    value_serde_lower.clone(),
351                    [h.name().to_string()],
352                );
353            } else {
354                state
355                    .topology
356                    .add_state_store_no_changelog::<KR, T, KS, VS>(
357                        store_for_thunk.clone(),
358                        key_serde_lower.clone(),
359                        value_serde_lower.clone(),
360                    );
361            }
362            state.topology.mark_store_caching(&store_for_thunk, caching);
363            state.handle_name.insert(agg_id, h.name().to_string());
364        }));
365
366        drop(g);
367        KTable::new(
368            Rc::clone(&self.builder),
369            agg_id,
370            Some(store_name),
371            None,
372            key_serde,
373            value_serde,
374        )
375        .with_suppress_factory(Some(suppress_factory))
376    }
377}