1use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc, sync::Arc};
13
14use krabka_units::prelude::*;
15
16use crate::{
17 dsl::{
18 builder::InternalStreamsBuilder,
19 graph::{GraphNodeKind, LowerState, NodeId},
20 kstream::KStream,
21 names,
22 processors::{
23 change::Change,
24 fk::{
25 processors::{
26 FkJoinOutputProcessor, ForeignTableJoinProcessor, SubscriptionJoinProcessor,
27 SubscriptionReceiveProcessor, SubscriptionResolverProcessor,
28 SubscriptionSendProcessor,
29 },
30 subscription::{SubscriptionResponseWrapper, SubscriptionWrapper},
31 wrapper_serde::{SubscriptionResponseWrapperSerde, SubscriptionWrapperSerde},
32 },
33 ktable_join::{
34 JoinKind, KTableKTableJoinOtherProcessor, KTableKTableJoinThisProcessor,
35 },
36 stateless::MergeProcessor,
37 table::{
38 KTableFilterProcessor, KTableMapValuesProcessor, KTableMapValuesViewProcessor,
39 KTableToStreamProcessor,
40 },
41 tuple_forwarder::TupleForwarder,
42 },
43 },
44 processor::serde::{DefaultSerde, Serde, SerdeArc},
45 topology::NodeHandle,
46};
47
48pub(crate) type SuppressStoreFactory = Arc<dyn Fn(&mut LowerState, &str, &str, bool) + Send + Sync>;
63
64struct ForeignKeyJoinGraph {
65 registration_base: String,
66 send_name: String,
67 registration_sink: String,
68 registration_source: String,
69 subscription_store: String,
70 receive_name: String,
71 subscription_join_name: String,
72 foreign_join_name: String,
73 response_base: String,
74 response_sink: String,
75 response_source: String,
76 resolver_name: String,
77 output_name: String,
78 output_id: NodeId,
79}
80
81fn allocate_foreign_key_join_graph(
82 builder: &mut InternalStreamsBuilder,
83 parents: (NodeId, NodeId),
84) -> ForeignKeyJoinGraph {
85 let registration_base = builder.new_processor_name(names::FK_SUBSCRIPTION_REGISTRATION);
86 let send_name = builder.new_processor_name(names::FK_SUBSCRIPTION_REGISTRATION);
87 let registration_sink = builder.new_processor_name(names::KTABLE_SINK);
88 let registration_source = builder.new_processor_name(names::KTABLE_SOURCE);
89 let subscription_store = builder.new_processor_name(names::FK_SUBSCRIPTION_STATE_STORE);
90 let receive_name = builder.new_processor_name(names::FK_SUBSCRIPTION_PROCESSOR);
91 let subscription_join_name = builder.new_processor_name(names::FK_SUBSCRIPTION_PROCESSOR);
92 let foreign_join_name = builder.new_processor_name(names::FK_SUBSCRIPTION_PROCESSOR);
93 let response_base = builder.new_processor_name(names::FK_SUBSCRIPTION_RESPONSE);
94 let response_sink = builder.new_processor_name(names::KTABLE_SINK);
95 let response_source = builder.new_processor_name(names::KTABLE_SOURCE);
96 let resolver_name = builder.new_processor_name(names::FK_RESPONSE_RESOLVER);
97 let output_name = builder.new_processor_name(names::FK_OUTPUT);
98 let result_store = builder.new_processor_name(names::FK_OUTPUT_STATE_STORE);
99 let output_id = builder.graph.add(
100 output_name.clone(),
101 GraphNodeKind::TableProcessor {
102 store_name: Some(result_store.clone()),
103 },
104 vec![parents.0, parents.1],
105 );
106 ForeignKeyJoinGraph {
107 registration_base,
108 send_name,
109 registration_sink,
110 registration_source,
111 subscription_store,
112 receive_name,
113 subscription_join_name,
114 foreign_join_name,
115 response_base,
116 response_sink,
117 response_source,
118 resolver_name,
119 output_name,
120 output_id,
121 }
122}
123
124pub(crate) fn kv_suppress_factory<K, V, KS, VS>(
132 key_serde: KS,
133 value_serde: VS,
134) -> SuppressStoreFactory
135where
136 K: Any + Send + Sync + Clone,
137 V: Any + Send + Clone,
138 KS: Serde<K> + Clone + 'static,
139 VS: Serde<V> + Clone + 'static,
140{
141 Arc::new(
142 move |state: &mut LowerState, store_name: &str, proc_name: &str, logging: bool| {
143 state.topology.add_suppress_store::<K, V, KS, VS>(
144 store_name.to_string(),
145 key_serde.clone(),
146 value_serde.clone(),
147 logging,
148 [proc_name.to_string()],
149 );
150 },
151 )
152}
153
154pub struct KTable<K, V, KS = <K as DefaultSerde>::Serde, VS = <V as DefaultSerde>::Serde> {
162 pub(crate) builder: Rc<RefCell<InternalStreamsBuilder>>,
163 pub(crate) node: NodeId,
164 #[allow(dead_code)]
165 pub(crate) store_name: Option<String>,
166 #[allow(dead_code)]
167 pub(crate) source_topic: Option<String>,
168 pub(crate) window_grace: Option<Time>,
171 pub(crate) versioned_retention: Option<Time>,
176 pub(crate) suppress_store_factory: Option<SuppressStoreFactory>,
181 pub(crate) key_serde: KS,
182 pub(crate) value_serde: VS,
183 pub(crate) _pd: PhantomData<fn() -> (K, V)>,
184}
185
186impl<K, V, KS, VS> KTable<K, V, KS, VS> {
187 pub(crate) fn new(
188 builder: Rc<RefCell<InternalStreamsBuilder>>,
189 node: NodeId,
190 store_name: Option<String>,
191 source_topic: Option<String>,
192 key_serde: KS,
193 value_serde: VS,
194 ) -> Self {
195 Self {
196 builder,
197 node,
198 store_name,
199 source_topic,
200 window_grace: None,
201 versioned_retention: None,
202 suppress_store_factory: None,
203 key_serde,
204 value_serde,
205 _pd: PhantomData,
206 }
207 }
208
209 pub fn with_key_serde<NewKS>(self, serde: NewKS) -> KTable<K, V, NewKS, VS> {
210 KTable {
211 builder: self.builder,
212 node: self.node,
213 store_name: self.store_name,
214 source_topic: self.source_topic,
215 window_grace: self.window_grace,
216 versioned_retention: self.versioned_retention,
217 suppress_store_factory: self.suppress_store_factory,
218 key_serde: serde,
219 value_serde: self.value_serde,
220 _pd: PhantomData,
221 }
222 }
223
224 pub fn with_value_serde<NewVS>(self, serde: NewVS) -> KTable<K, V, KS, NewVS> {
225 KTable {
226 builder: self.builder,
227 node: self.node,
228 store_name: self.store_name,
229 source_topic: self.source_topic,
230 window_grace: self.window_grace,
231 versioned_retention: self.versioned_retention,
232 suppress_store_factory: self.suppress_store_factory,
233 key_serde: self.key_serde,
234 value_serde: serde,
235 _pd: PhantomData,
236 }
237 }
238
239 pub(crate) fn key_serde(&self) -> Arc<dyn Serde<K>>
241 where
242 KS: Serde<K> + Clone + 'static,
243 {
244 Arc::new(self.key_serde.clone())
245 }
246
247 pub(crate) fn value_serde(&self) -> Arc<dyn Serde<V>>
249 where
250 VS: Serde<V> + Clone + 'static,
251 {
252 Arc::new(self.value_serde.clone())
253 }
254
255 pub(crate) fn node_id(&self) -> NodeId {
259 self.node
260 }
261
262 #[allow(dead_code)]
264 pub(crate) fn store_name(&self) -> Option<&str> {
265 self.store_name.as_deref()
266 }
267
268 #[allow(dead_code)]
272 pub(crate) fn source_topic(&self) -> Option<&str> {
273 self.source_topic.as_deref()
274 }
275
276 #[must_use]
280 pub(crate) fn with_window_grace(mut self, grace: Option<Time>) -> Self {
281 self.window_grace = grace;
282 self
283 }
284
285 #[must_use]
290 pub(crate) fn with_versioned_retention(mut self, retention: Option<Time>) -> Self {
291 self.versioned_retention = retention;
292 self
293 }
294
295 #[must_use]
299 pub(crate) fn with_suppress_factory(mut self, factory: Option<SuppressStoreFactory>) -> Self {
300 self.suppress_store_factory = factory;
301 self
302 }
303}
304
305impl<K, V, KS, VS> KTable<K, V, KS, VS>
306where
307 K: Any + Send + Sync + Clone,
308 V: Any + Send + Clone,
309{
310 #[cfg(test)]
319 pub(crate) fn collect_changes(
320 &self,
321 buf: crate::dsl::processors::fk::processors::ChangeBuffer<K, V>,
322 ) where
323 K: 'static,
324 V: Sync + 'static,
325 {
326 let parent_id = self.node;
327 let mut g = self.builder.borrow_mut();
328 let name = g.new_processor_name(names::TABLE_TOSTREAM);
329 let id = g.graph.add(
330 name.clone(),
331 GraphNodeKind::TableProcessor { store_name: None },
332 vec![parent_id],
333 );
334 g.graph.nodes[id].lower = Some(Box::new(move |state: &mut LowerState| {
335 let parent =
336 NodeHandle::<K, Change<V>>::from_name(state.handle_name[&parent_id].clone());
337 let buf = buf.clone();
338 let h = state.topology.add_processor::<K, Change<V>, K, V, _, _, _>(
339 name.clone(),
340 move || crate::dsl::processors::fk::processors::ChangeCollectorProcessor::<K, V> {
341 buf: buf.clone(),
342 _pd: PhantomData,
343 },
344 [parent],
345 );
346 state.handle_name.insert(id, h.name().to_string());
347 }));
348 }
349
350 #[must_use]
353 pub fn to_stream(&self) -> KStream<K, V, KS, VS>
354 where
355 KS: Clone,
356 VS: Clone,
357 {
358 let parent_id = self.node;
359 let mut g = self.builder.borrow_mut();
360 let name = g.new_processor_name(names::TABLE_TOSTREAM);
361 let id = g.graph.add(
362 name.clone(),
363 GraphNodeKind::TableProcessor { store_name: None },
364 vec![parent_id],
365 );
366 g.graph.nodes[id].lower = Some(Box::new(move |state: &mut LowerState| {
367 let parent =
369 NodeHandle::<K, Change<V>>::from_name(state.handle_name[&parent_id].clone());
370 let h = state.topology.add_processor::<K, Change<V>, K, V, _, _, _>(
371 name.clone(),
372 || KTableToStreamProcessor { _pd: PhantomData },
373 [parent],
374 );
375 state.handle_name.insert(id, h.name().to_string());
376 }));
377 drop(g);
378 KStream::new(
379 Rc::clone(&self.builder),
380 id,
381 self.key_serde.clone(),
382 self.value_serde.clone(),
383 )
384 }
385
386 pub fn map_values<V2, F>(&self, f: F) -> KTable<K, V2, KS, <V2 as DefaultSerde>::Serde>
394 where
395 V2: DefaultSerde + Any + Send + Clone,
396 F: Fn(&V) -> V2 + Clone + Send + Sync + 'static,
397 KS: Clone,
398 {
399 let grace = self.window_grace;
400 let parent_id = self.node;
401 let mut g = self.builder.borrow_mut();
402 let name = g.new_processor_name(names::TABLE_MAPVALUES);
403 let id = g.graph.add(
404 name.clone(),
405 GraphNodeKind::TableProcessor { store_name: None },
406 vec![parent_id],
407 );
408 let f2 = f.clone();
409 g.graph.nodes[id].lower = Some(Box::new(move |state: &mut LowerState| {
410 let parent =
412 NodeHandle::<K, Change<V>>::from_name(state.handle_name[&parent_id].clone());
413 let h = state
414 .topology
415 .add_processor::<K, Change<V>, K, Change<V2>, _, _, _>(
416 name.clone(),
417 move || KTableMapValuesViewProcessor {
418 f: f2.clone(),
419 _pd: PhantomData,
420 },
421 [parent],
422 );
423 state.handle_name.insert(id, h.name().to_string());
424 }));
425 drop(g);
426 KTable::new(
427 Rc::clone(&self.builder),
428 id,
429 None,
430 None,
431 self.key_serde.clone(),
432 <V2 as DefaultSerde>::Serde::default(),
433 )
434 .with_window_grace(grace)
435 }
436
437 pub fn map_values_materialized<V2, NKS, NVS, F>(
440 &self,
441 f: F,
442 materialized: impl Into<crate::dsl::config::Materialized<NKS, NVS>>,
443 ) -> KTable<K, V2, NKS, NVS>
444 where
445 V2: Any + Send + Clone,
446 NKS: Serde<K> + Clone + 'static,
447 NVS: Serde<V2> + Clone + 'static,
448 F: Fn(&V) -> V2 + Clone + Send + Sync + 'static,
449 {
450 let materialized = materialized.into();
451 let grace = self.window_grace;
452 let store_name = mint_table_store(&self.builder, &materialized, names::TABLE_MAPVALUES);
453 let key_serde = materialized.key_serde.clone();
454 let value_serde = materialized.value_serde.clone();
455 let key_serde_for_ktable = key_serde.clone();
456 let value_serde_for_ktable = value_serde.clone();
457 let caching = materialized.caching_enabled();
458 let crate::dsl::config::Materialized {
459 key_serde,
460 value_serde,
461 ..
462 } = materialized;
463 let parent_id = self.node;
464 let mut g = self.builder.borrow_mut();
465 let name = g.new_processor_name(names::TABLE_MAPVALUES);
466 let id = g.graph.add(
467 name.clone(),
468 GraphNodeKind::TableProcessor {
469 store_name: Some(store_name.clone()),
470 },
471 vec![parent_id],
472 );
473 let f2 = f.clone();
474 let store_for_thunk = store_name.clone();
475 g.graph.nodes[id].lower = Some(Box::new(move |state: &mut LowerState| {
476 let parent =
478 NodeHandle::<K, Change<V>>::from_name(state.handle_name[&parent_id].clone());
479 let store_for_proc = store_for_thunk.clone();
480 let h = state
481 .topology
482 .add_processor::<K, Change<V>, K, Change<V2>, _, _, _>(
483 name.clone(),
484 move || KTableMapValuesProcessor {
485 f: f2.clone(),
486 store_name: store_for_proc.clone(),
487 forwarder: TupleForwarder::default(),
488 _pd: PhantomData,
489 },
490 [parent],
491 );
492 state.topology.add_state_store::<K, V2, NKS, NVS>(
493 store_for_thunk.clone(),
494 key_serde.clone(),
495 value_serde.clone(),
496 [h.name().to_string()],
497 );
498 state.topology.mark_store_caching(&store_for_thunk, caching);
499 state.handle_name.insert(id, h.name().to_string());
500 }));
501 drop(g);
502 KTable::new(
503 Rc::clone(&self.builder),
504 id,
505 Some(store_name),
506 None,
507 key_serde_for_ktable,
508 value_serde_for_ktable,
509 )
510 .with_window_grace(grace)
511 }
512
513 #[must_use]
519 pub fn filter<NKS, NVS, P>(
520 &self,
521 predicate: P,
522 materialized: impl Into<crate::dsl::config::Materialized<NKS, NVS>>,
523 ) -> KTable<K, V, NKS, NVS>
524 where
525 NKS: Serde<K> + Clone + 'static,
526 NVS: Serde<V> + Clone + 'static,
527 P: Fn(&K, &V) -> bool + Clone + Send + Sync + 'static,
528 {
529 let materialized = materialized.into();
530 let grace = self.window_grace;
531 let suppress_factory = self.suppress_store_factory.clone();
534 let store_name = mint_table_store(&self.builder, &materialized, names::TABLE_FILTER);
535 let key_serde = materialized.key_serde.clone();
536 let value_serde = materialized.value_serde.clone();
537 let key_serde_for_ktable = key_serde.clone();
538 let value_serde_for_ktable = value_serde.clone();
539 let caching = materialized.caching_enabled();
540 let crate::dsl::config::Materialized {
541 key_serde,
542 value_serde,
543 ..
544 } = materialized;
545 let parent_id = self.node;
546 let mut g = self.builder.borrow_mut();
547 let name = g.new_processor_name(names::TABLE_FILTER);
548 let id = g.graph.add(
549 name.clone(),
550 GraphNodeKind::TableProcessor {
551 store_name: Some(store_name.clone()),
552 },
553 vec![parent_id],
554 );
555 let p2 = predicate.clone();
556 let store_for_thunk = store_name.clone();
557 g.graph.nodes[id].lower = Some(Box::new(move |state: &mut LowerState| {
558 let parent =
561 NodeHandle::<K, Change<V>>::from_name(state.handle_name[&parent_id].clone());
562 let store_for_proc = store_for_thunk.clone();
563 let h = state
564 .topology
565 .add_processor::<K, Change<V>, K, Change<V>, _, _, _>(
566 name.clone(),
567 move || KTableFilterProcessor {
568 predicate: p2.clone(),
569 store_name: store_for_proc.clone(),
570 forwarder: TupleForwarder::default(),
571 _pd: PhantomData,
572 },
573 [parent],
574 );
575 state.topology.add_state_store::<K, V, NKS, NVS>(
576 store_for_thunk.clone(),
577 key_serde.clone(),
578 value_serde.clone(),
579 [h.name().to_string()],
580 );
581 state.topology.mark_store_caching(&store_for_thunk, caching);
582 state.handle_name.insert(id, h.name().to_string());
583 }));
584 drop(g);
585 KTable::new(
586 Rc::clone(&self.builder),
587 id,
588 Some(store_name),
589 None,
590 key_serde_for_ktable,
591 value_serde_for_ktable,
592 )
593 .with_window_grace(grace)
594 .with_suppress_factory(suppress_factory)
595 }
596
597 pub fn group_by<KR, VR, M>(
603 &self,
604 mapper: M,
605 ) -> crate::dsl::kgrouped_table::KGroupedTable<KR, VR>
606 where
607 KR: DefaultSerde + Any + Send + Sync + Clone + PartialEq,
608 VR: DefaultSerde + Any + Send + Sync + Clone,
609 <KR as DefaultSerde>::Serde: Serde<KR> + Clone,
610 <VR as DefaultSerde>::Serde: Serde<VR> + Clone,
611 M: Fn(&K, &V) -> (KR, VR) + Clone + Send + Sync + 'static,
612 K: Any + Send + Sync + Clone,
613 V: Any + Send + Clone,
614 {
615 self.group_by_explicit(
616 mapper,
617 crate::dsl::config::Grouped::with(
618 <KR as DefaultSerde>::Serde::default(),
619 <VR as DefaultSerde>::Serde::default(),
620 ),
621 )
622 }
623
624 pub fn group_by_explicit<KR, VR, GKS, GVS, M>(
626 &self,
627 mapper: M,
628 grouped: impl Into<crate::dsl::config::Grouped<GKS, GVS>>,
629 ) -> crate::dsl::kgrouped_table::KGroupedTable<KR, VR>
630 where
631 KR: Any + Send + Sync + Clone + PartialEq,
632 VR: Any + Send + Sync + Clone,
633 GKS: Serde<KR> + Clone + 'static,
634 GVS: Serde<VR> + Clone + 'static,
635 M: Fn(&K, &V) -> (KR, VR) + Clone + Send + Sync + 'static,
636 K: Any + Send + Sync + Clone,
637 V: Any + Send + Clone,
638 {
639 use crate::dsl::processors::table_aggregate::KTableRepartitionMapProcessor;
640
641 let grouped = grouped.into();
642 let parent_id = self.node;
643
644 let mut g = self.builder.borrow_mut();
651 let select_name = g.new_processor_name(names::KTABLE_SELECT);
652 let select_id = g.graph.add(
653 select_name.clone(),
654 GraphNodeKind::TableProcessor { store_name: None },
655 vec![parent_id],
656 );
657 let mapper2 = mapper.clone();
658 g.graph.nodes[select_id].lower = Some(Box::new(move |state: &mut LowerState| {
659 let parent =
660 NodeHandle::<K, Change<V>>::from_name(state.handle_name[&parent_id].clone());
661 let h = state
662 .topology
663 .add_processor::<K, Change<V>, KR, Change<VR>, _, _, _>(
664 select_name.clone(),
665 move || KTableRepartitionMapProcessor {
666 mapper: mapper2.clone(),
667 _pd: PhantomData,
668 },
669 [parent],
670 );
671 state.handle_name.insert(select_id, h.name().to_string());
672 }));
673 drop(g);
674
675 crate::dsl::kgrouped_table::KGroupedTable::new(
676 Rc::clone(&self.builder),
677 select_id,
678 crate::dsl::kgrouped_table::repartition_lower_changed::<KR, VR, GKS, GVS>(
679 grouped.key_serde,
680 grouped.value_serde,
681 ),
682 )
683 }
684
685 pub fn join<VB, VR, F, VBS>(
698 &self,
699 other: &KTable<K, VB, KS, VBS>,
700 joiner: F,
701 ) -> KTable<K, VR, KS, <VR as DefaultSerde>::Serde>
702 where
703 V: Sync,
704 VB: Any + Send + Sync + Clone,
705 VR: DefaultSerde + Any + Send + Clone,
706 F: Fn(&V, &VB) -> VR + Clone + Send + Sync + 'static,
707 KS: Clone,
708 VBS: Clone,
709 {
710 let jf = move |a: Option<&V>, b: Option<&VB>| {
712 joiner(
713 a.expect("inner join: a present"),
714 b.expect("inner join: b present"),
715 )
716 };
717 self.join_impl(other, jf, JoinKind::inner())
718 }
719
720 pub fn left_join<VB, VR, F, VBS>(
727 &self,
728 other: &KTable<K, VB, KS, VBS>,
729 joiner: F,
730 ) -> KTable<K, VR, KS, <VR as DefaultSerde>::Serde>
731 where
732 V: Sync,
733 VB: Any + Send + Sync + Clone,
734 VR: DefaultSerde + Any + Send + Clone,
735 F: Fn(&V, Option<&VB>) -> VR + Clone + Send + Sync + 'static,
736 KS: Clone,
737 VBS: Clone,
738 {
739 let jf = move |a: Option<&V>, b: Option<&VB>| joiner(a.expect("left join: a present"), b);
740 self.join_impl(other, jf, JoinKind::left())
741 }
742
743 pub fn outer_join<VB, VR, F, VBS>(
748 &self,
749 other: &KTable<K, VB, KS, VBS>,
750 joiner: F,
751 ) -> KTable<K, VR, KS, <VR as DefaultSerde>::Serde>
752 where
753 V: Sync,
754 VB: Any + Send + Sync + Clone,
755 VR: DefaultSerde + Any + Send + Clone,
756 F: Fn(Option<&V>, Option<&VB>) -> VR + Clone + Send + Sync + 'static,
757 KS: Clone,
758 VBS: Clone,
759 {
760 self.join_impl(other, joiner, JoinKind::outer())
761 }
762
763 fn join_impl<VB, VR, JF, VBS>(
778 &self,
779 other: &KTable<K, VB, KS, VBS>,
780 jf: JF,
781 kind: JoinKind,
782 ) -> KTable<K, VR, KS, <VR as DefaultSerde>::Serde>
783 where
784 V: Sync,
785 VB: Any + Send + Sync + Clone,
786 VR: DefaultSerde + Any + Send + Clone,
787 JF: Fn(Option<&V>, Option<&VB>) -> VR + Clone + Send + Sync + 'static,
788 KS: Clone,
789 VBS: Clone,
790 {
791 let a_store = self
792 .store_name()
793 .expect("KTable-KTable join: left table must be materialized")
794 .to_string();
795 let b_store = other
796 .store_name()
797 .expect("KTable-KTable join: right table must be materialized")
798 .to_string();
799 let a_src = self.source_topic().map(str::to_string);
800 let b_src = other.source_topic().map(str::to_string);
801 let self_node = self.node;
802 let other_node = other.node;
803
804 let this_versioned_store = self.versioned_retention.is_some().then(|| a_store.clone());
808 let other_versioned_store = other.versioned_retention.is_some().then(|| b_store.clone());
809
810 let other_is_versioned_this = other.versioned_retention.is_some();
817 let other_is_versioned_other = self.versioned_retention.is_some();
818
819 let mut g = self.builder.borrow_mut();
820 let join_this = g.new_processor_name(names::KTABLE_JOIN_THIS);
821 let join_other = g.new_processor_name(names::KTABLE_JOIN_OTHER);
822 let merge = g.new_processor_name(names::KTABLE_MERGE);
823
824 let this_id = g.graph.add(
826 join_this.clone(),
827 GraphNodeKind::StatelessProcessor {
828 repartition_required: false,
829 },
830 vec![self_node],
831 );
832 let b_store_this = b_store.clone();
833 let a_store_this = a_store.clone();
834 let jf_this = jf.clone();
835 let join_this_name = join_this.clone();
836 let this_versioned = this_versioned_store.clone();
837 g.graph.nodes[this_id].lower = Some(Box::new(move |state: &mut LowerState| {
838 let parent =
839 NodeHandle::<K, Change<V>>::from_name(state.handle_name[&self_node].clone());
840 let store_for_proc = b_store_this.clone();
841 let jf_for_proc = jf_this.clone();
842 let self_versioned = this_versioned.clone();
843 let h = state
844 .topology
845 .add_processor::<K, Change<V>, K, Change<VR>, _, _, _>(
846 join_this_name.clone(),
847 move || KTableKTableJoinThisProcessor {
848 other_store: store_for_proc.clone(),
849 joiner: jf_for_proc.clone(),
850 kind,
851 self_versioned_store: self_versioned.clone(),
852 other_is_versioned: other_is_versioned_this,
855 _pd: PhantomData,
856 },
857 [parent],
858 );
859 state
860 .topology
861 .connect_processor_store(h.name(), &b_store_this);
862 if this_versioned.is_some() {
865 state
866 .topology
867 .connect_processor_store(h.name(), &a_store_this);
868 }
869 state.handle_name.insert(this_id, h.name().to_string());
870 }));
871
872 let other_id = g.graph.add(
874 join_other.clone(),
875 GraphNodeKind::StatelessProcessor {
876 repartition_required: false,
877 },
878 vec![other_node],
879 );
880 let a_store_other = a_store.clone();
881 let b_store_other = b_store.clone();
882 let jf_other = jf.clone();
883 let join_other_name = join_other.clone();
884 let other_versioned = other_versioned_store.clone();
885 g.graph.nodes[other_id].lower = Some(Box::new(move |state: &mut LowerState| {
886 let parent =
887 NodeHandle::<K, Change<VB>>::from_name(state.handle_name[&other_node].clone());
888 let store_for_proc = a_store_other.clone();
889 let jf_for_proc = jf_other.clone();
890 let self_versioned = other_versioned.clone();
891 let h = state
892 .topology
893 .add_processor::<K, Change<VB>, K, Change<VR>, _, _, _>(
894 join_other_name.clone(),
895 move || KTableKTableJoinOtherProcessor {
896 other_store: store_for_proc.clone(),
897 joiner: jf_for_proc.clone(),
898 kind,
899 self_versioned_store: self_versioned.clone(),
900 other_is_versioned: other_is_versioned_other,
903 _pd: PhantomData,
904 },
905 [parent],
906 );
907 state
908 .topology
909 .connect_processor_store(h.name(), &a_store_other);
910 if other_versioned.is_some() {
913 state
914 .topology
915 .connect_processor_store(h.name(), &b_store_other);
916 }
917 state.handle_name.insert(other_id, h.name().to_string());
918 }));
919
920 let merge_id = g.graph.add(
922 merge.clone(),
923 GraphNodeKind::StatelessProcessor {
924 repartition_required: false,
925 },
926 vec![this_id, other_id],
927 );
928 g.graph.nodes[merge_id].lower = Some(Box::new(move |state: &mut LowerState| {
929 let this_parent =
930 NodeHandle::<K, Change<VR>>::from_name(state.handle_name[&this_id].clone());
931 let other_parent =
932 NodeHandle::<K, Change<VR>>::from_name(state.handle_name[&other_id].clone());
933 let h = state
934 .topology
935 .add_processor::<K, Change<VR>, K, Change<VR>, _, _, _>(
936 merge.clone(),
937 || MergeProcessor::<K, Change<VR>> { _pd: PhantomData },
938 [this_parent, other_parent],
939 );
940 if let (Some(a), Some(bb)) = (&a_src, &b_src) {
942 state
943 .topology
944 .add_copartition_group([a.clone(), bb.clone()]);
945 }
946 state.handle_name.insert(merge_id, h.name().to_string());
947 }));
948 drop(g);
949 KTable::new(
950 Rc::clone(&self.builder),
951 merge_id,
952 None,
953 None,
954 self.key_serde.clone(),
955 <VR as DefaultSerde>::Serde::default(),
956 )
957 }
958
959 pub fn join_on_foreign_key<KO, VB, VR, FKE, J, KOS, KosOther, VbsOther>(
974 &self,
975 other: &KTable<KO, VB, KosOther, VbsOther>,
976 fk_extractor: FKE,
977 joiner: J,
978 fk_serde: KOS,
979 ) -> KTable<K, VR, KS, <VR as DefaultSerde>::Serde>
980 where
981 KO: Any + Send + Sync + Clone,
982 VB: Any + Send + Sync + Clone,
983 VR: DefaultSerde + Any + Send + Sync + Clone,
984 K: Send + Sync,
985 V: Send + Sync,
986 FKE: Fn(&V) -> KO + Clone + Send + Sync + 'static,
987 J: Fn(&V, &VB) -> VR + Clone + Send + Sync + 'static,
988 KOS: Serde<KO> + Clone + 'static,
989 KS: Serde<K> + Clone + 'static,
990 VS: Serde<V> + Clone + 'static,
991 KosOther: Clone,
992 VbsOther: Serde<VB> + Clone + 'static,
993 {
994 let jf = move |a: &V, b: Option<&VB>| joiner(a, b.expect("inner FK join: foreign present"));
996 self.fk_join_impl(other, fk_extractor, jf, fk_serde, false)
997 }
998
999 pub fn left_join_on_foreign_key<KO, VB, VR, FKE, J, KOS, KosOther, VbsOther>(
1004 &self,
1005 other: &KTable<KO, VB, KosOther, VbsOther>,
1006 fk_extractor: FKE,
1007 joiner: J,
1008 fk_serde: KOS,
1009 ) -> KTable<K, VR, KS, <VR as DefaultSerde>::Serde>
1010 where
1011 KO: Any + Send + Sync + Clone,
1012 VB: Any + Send + Sync + Clone,
1013 VR: DefaultSerde + Any + Send + Sync + Clone,
1014 K: Send + Sync,
1015 V: Send + Sync,
1016 FKE: Fn(&V) -> KO + Clone + Send + Sync + 'static,
1017 J: Fn(&V, Option<&VB>) -> VR + Clone + Send + Sync + 'static,
1018 KOS: Serde<KO> + Clone + 'static,
1019 KS: Serde<K> + Clone + 'static,
1020 VS: Serde<V> + Clone + 'static,
1021 KosOther: Clone,
1022 VbsOther: Serde<VB> + Clone + 'static,
1023 {
1024 self.fk_join_impl(other, fk_extractor, joiner, fk_serde, true)
1025 }
1026
1027 fn foreign_key_join_sources<KO, VB, KosOther, VbsOther>(
1042 &self,
1043 other: &KTable<KO, VB, KosOther, VbsOther>,
1044 ) -> (String, String, String, String) {
1045 let left_store = self
1046 .store_name()
1047 .expect("FK join: left table must be a materialized source table")
1048 .to_string();
1049 let right_store = other
1050 .store_name()
1051 .expect("FK join: right table must be a materialized source table")
1052 .to_string();
1053 let left_topic = self
1054 .source_topic()
1055 .expect("FK join: left table must be sourced from a single topic")
1056 .to_string();
1057 let right_topic = other
1058 .source_topic()
1059 .expect("FK join: right table must be sourced from a single topic")
1060 .to_string();
1061 (left_store, right_store, left_topic, right_topic)
1062 }
1063
1064 fn fk_join_impl<KO, VB, VR, FKE, JF, KOS, KosOther, VbsOther>(
1065 &self,
1066 other: &KTable<KO, VB, KosOther, VbsOther>,
1067 fk_extractor: FKE,
1068 jf: JF,
1069 fk_serde: KOS,
1070 is_left: bool,
1071 ) -> KTable<K, VR, KS, <VR as DefaultSerde>::Serde>
1072 where
1073 KO: Any + Send + Sync + Clone,
1074 VB: Any + Send + Sync + Clone,
1075 VR: DefaultSerde + Any + Send + Sync + Clone,
1076 K: Send + Sync,
1077 V: Send + Sync,
1078 FKE: Fn(&V) -> KO + Clone + Send + Sync + 'static,
1079 JF: Fn(&V, Option<&VB>) -> VR + Clone + Send + Sync + 'static,
1080 KOS: Serde<KO> + Clone + 'static,
1081 KS: Serde<K> + Clone + 'static,
1082 VS: Serde<V> + Clone + 'static,
1083 KosOther: Clone,
1084 VbsOther: Serde<VB> + Clone + 'static,
1085 {
1086 let (sa, sb, a_src, b_src) = self.foreign_key_join_sources(other);
1087 let k_serde = self.key_serde();
1090 let left_value_serde = self.value_serde();
1091 let right_value_serde = other.value_serde();
1092 let self_node = self.node_id();
1093 let other_node = other.node_id();
1094
1095 let mut g = self.builder.borrow_mut();
1096 let graph = allocate_foreign_key_join_graph(&mut g, (self_node, other_node));
1097 let output_id = graph.output_id;
1098
1099 let thunk = move |state: &mut LowerState| {
1100 let app = state.app_id.clone();
1101 let registration_topic = format!(
1102 "{app}-{}{}",
1103 graph.registration_base,
1104 names::FK_TOPIC_SUFFIX
1105 );
1106 let response_topic = format!("{app}-{}{}", graph.response_base, names::FK_TOPIC_SUFFIX);
1107
1108 let a_parent =
1109 NodeHandle::<K, Change<V>>::from_name(state.handle_name[&self_node].clone());
1110 let b_parent =
1111 NodeHandle::<KO, Change<VB>>::from_name(state.handle_name[&other_node].clone());
1112
1113 let send_h = state
1115 .topology
1116 .add_processor::<K, Change<V>, KO, SubscriptionWrapper, _, _, _>(
1117 graph.send_name.clone(),
1118 {
1119 let fke = fk_extractor.clone();
1120 let left_serde = left_value_serde.clone();
1121 let ko = fk_serde.clone();
1122 let ks = k_serde.clone();
1123 move || SubscriptionSendProcessor {
1124 fk_extractor: fke.clone(),
1125 va_serde: Box::new(SerdeArc(left_serde.clone())),
1126 ko_serde: Box::new(ko.clone()),
1127 k_serde: Box::new(SerdeArc(ks.clone())),
1128 is_left,
1129 _pd: PhantomData,
1130 }
1131 },
1132 [a_parent],
1133 );
1134 state
1135 .topology
1136 .add_sink_explicit::<KO, SubscriptionWrapper, KOS, SubscriptionWrapperSerde, _, _>(
1137 graph.registration_sink.clone(),
1138 registration_topic.clone(),
1139 [&send_h],
1140 crate::processor::serde::Produced::with(
1141 fk_serde.clone(),
1142 SubscriptionWrapperSerde,
1143 ),
1144 );
1145 state
1146 .topology
1147 .add_repartition_topic(registration_topic.clone());
1148 let reg_src_h = state
1149 .topology
1150 .add_source_explicit::<KO, SubscriptionWrapper, KOS, SubscriptionWrapperSerde>(
1151 graph.registration_source.clone(),
1152 [registration_topic.clone()],
1153 crate::processor::serde::Consumed::with(
1154 fk_serde.clone(),
1155 SubscriptionWrapperSerde,
1156 ),
1157 );
1158
1159 let receive_h = state
1161 .topology
1162 .add_processor::<KO, SubscriptionWrapper, KO, SubscriptionWrapper, _, _, _>(
1163 graph.receive_name.clone(),
1164 {
1165 let store = graph.subscription_store.clone();
1166 let ko = fk_serde.clone();
1167 move || SubscriptionReceiveProcessor {
1168 store_name: store.clone(),
1169 ko_serde: Box::new(ko.clone()),
1170 _pd: PhantomData,
1171 }
1172 },
1173 [®_src_h],
1174 );
1175 let sub_join_h = state
1176 .topology
1177 .add_processor::<KO, SubscriptionWrapper, K, SubscriptionResponseWrapper, _, _, _>(
1178 graph.subscription_join_name.clone(),
1179 {
1180 let b = sb.clone();
1181 let ks = k_serde.clone();
1182 let right_serde = right_value_serde.clone();
1183 move || SubscriptionJoinProcessor::<KO, K, VB> {
1184 b_store: b.clone(),
1185 k_serde: Box::new(SerdeArc(ks.clone())),
1186 vb_serde: Box::new(SerdeArc(right_serde.clone())),
1187 _pd: PhantomData,
1188 }
1189 },
1190 [&receive_h],
1191 );
1192
1193 let foreign_join_h = state
1195 .topology
1196 .add_processor::<KO, Change<VB>, K, SubscriptionResponseWrapper, _, _, _>(
1197 graph.foreign_join_name.clone(),
1198 {
1199 let store = graph.subscription_store.clone();
1200 let ko = fk_serde.clone();
1201 let ks = k_serde.clone();
1202 let right_serde = right_value_serde.clone();
1203 move || ForeignTableJoinProcessor::<KO, K, VB> {
1204 store_name: store.clone(),
1205 ko_serde: Box::new(ko.clone()),
1206 k_serde: Box::new(SerdeArc(ks.clone())),
1207 vb_serde: Box::new(SerdeArc(right_serde.clone())),
1208 _pd: PhantomData,
1209 }
1210 },
1211 [&b_parent],
1212 );
1213
1214 state.topology.add_fk_subscription_store(
1218 graph.subscription_store.clone(),
1219 [
1220 receive_h.name().to_string(),
1221 foreign_join_h.name().to_string(),
1222 ],
1223 );
1224 state
1226 .topology
1227 .connect_processor_store(sub_join_h.name(), &sb);
1228
1229 state
1231 .topology
1232 .add_sink_explicit::<K, SubscriptionResponseWrapper, _, SubscriptionResponseWrapperSerde, _, _>(
1233 graph.response_sink.clone(),
1234 response_topic.clone(),
1235 [&sub_join_h, &foreign_join_h],
1236 crate::processor::serde::Produced::with(
1237 SerdeArc(k_serde.clone()),
1238 SubscriptionResponseWrapperSerde,
1239 ),
1240 );
1241 state.topology.add_repartition_topic(response_topic.clone());
1242
1243 let resp_src_h = state
1245 .topology
1246 .add_source_explicit::<K, SubscriptionResponseWrapper, _, SubscriptionResponseWrapperSerde>(
1247 graph.response_source.clone(),
1248 [response_topic.clone()],
1249 crate::processor::serde::Consumed::with(
1250 SerdeArc(k_serde.clone()),
1251 SubscriptionResponseWrapperSerde,
1252 ),
1253 );
1254 let resolver_h = state
1255 .topology
1256 .add_processor::<K, SubscriptionResponseWrapper, K, Change<VR>, _, _, _>(
1257 graph.resolver_name.clone(),
1258 {
1259 let a = sa.clone();
1260 let left_serde = left_value_serde.clone();
1261 let right_serde = right_value_serde.clone();
1262 let joiner = jf.clone();
1263 move || SubscriptionResolverProcessor::<K, V, VB, VR, JF> {
1264 a_store: a.clone(),
1265 va_serde: Box::new(SerdeArc(left_serde.clone())),
1266 vb_serde: Box::new(SerdeArc(right_serde.clone())),
1267 joiner: joiner.clone(),
1268 is_left,
1269 _pd: PhantomData,
1270 }
1271 },
1272 [&resp_src_h],
1273 );
1274 state
1276 .topology
1277 .connect_processor_store(resolver_h.name(), &sa);
1278
1279 let output_h = state
1280 .topology
1281 .add_processor::<K, Change<VR>, K, Change<VR>, _, _, _>(
1282 graph.output_name.clone(),
1283 || FkJoinOutputProcessor::<K, VR> { _pd: PhantomData },
1284 [&resolver_h],
1285 );
1286
1287 state
1295 .topology
1296 .add_copartition_group([a_src.clone(), response_topic.clone()]);
1297 state
1298 .topology
1299 .add_copartition_group([b_src.clone(), registration_topic.clone()]);
1300
1301 state
1302 .handle_name
1303 .insert(output_id, output_h.name().to_string());
1304 };
1305 g.graph.nodes[output_id].lower = Some(Box::new(thunk));
1306 drop(g);
1307 KTable::new(
1308 Rc::clone(&self.builder),
1309 output_id,
1310 None,
1311 None,
1312 self.key_serde.clone(),
1313 <VR as DefaultSerde>::Serde::default(),
1314 )
1315 }
1316}
1317
1318impl<K, V, KS, VS> KTable<K, V, KS, VS>
1319where
1320 K: Any + Send + Sync + Clone,
1321 V: Any + Send + Clone,
1322{
1323 #[must_use]
1336 pub fn suppress(&self, suppressed: crate::dsl::suppress::Suppressed<K>) -> KTable<K, V, KS, VS>
1339 where
1340 KS: Clone,
1341 VS: Clone,
1342 {
1343 let wait = match suppressed.wait {
1344 crate::dsl::suppress::WaitKind::UpstreamGrace => {
1345 self.window_grace.unwrap_or(Time::ZERO)
1346 }
1347 crate::dsl::suppress::WaitKind::Fixed(wait) => wait,
1348 };
1349 let buffer_time = suppressed.buffer_time;
1350 let max_records = suppressed.buffer.record_cap();
1351 let max_bytes = suppressed.buffer.byte_cap();
1352 let emit_early = suppressed.buffer.is_emit_early();
1353 let logging = suppressed.logging;
1354 let factory = self.suppress_store_factory.clone().expect(
1357 "suppress requires a serde-carrying KTable (a windowed/session aggregation \
1358 or builder.table); a mapValues-derived view has no value serde for the buffer",
1359 );
1360 let parent_id = self.node;
1361 let mut g = self.builder.borrow_mut();
1362 let name = g.new_processor_name(names::KTABLE_SUPPRESS);
1363 let store_name = g.new_processor_name(names::KTABLE_SUPPRESS_STORE);
1366 let store_for_thunk = store_name.clone();
1367 let id = g.graph.add(
1368 name.clone(),
1369 GraphNodeKind::TableProcessor { store_name: None },
1370 vec![parent_id],
1371 );
1372 g.graph.nodes[id].lower = Some(Box::new(move |state: &mut LowerState| {
1373 let parent =
1374 NodeHandle::<K, Change<V>>::from_name(state.handle_name[&parent_id].clone());
1375 let store_for_proc = store_for_thunk.clone();
1376 let h = state
1377 .topology
1378 .add_processor::<K, Change<V>, K, Change<V>, _, _, _>(
1379 name.clone(),
1380 move || {
1381 crate::dsl::processors::suppress::KTableSuppressProcessor::<K, V>::new(
1382 store_for_proc.clone(),
1383 wait,
1384 buffer_time,
1385 max_records,
1386 max_bytes,
1387 emit_early,
1388 )
1389 },
1390 [parent],
1391 );
1392 let proc_name = h.name().to_string();
1393 factory(state, &store_for_thunk, &proc_name, logging);
1396 state.handle_name.insert(id, proc_name);
1397 }));
1398 drop(g);
1399 KTable::new(
1402 Rc::clone(&self.builder),
1403 id,
1404 Some(store_name),
1405 None,
1406 self.key_serde.clone(),
1407 self.value_serde.clone(),
1408 )
1409 .with_window_grace(self.window_grace)
1410 .with_suppress_factory(self.suppress_store_factory.clone())
1411 }
1412}
1413
1414fn mint_table_store<KS, VS>(
1419 builder: &Rc<RefCell<InternalStreamsBuilder>>,
1420 materialized: &crate::dsl::config::Materialized<KS, VS>,
1421 prefix: &str,
1422) -> String {
1423 match &materialized.store_name {
1424 Some(name) => name.clone(),
1425 None => builder.borrow_mut().new_processor_name(prefix),
1426 }
1427}
1428
1429#[cfg(test)]
1430mod tests {
1431 use assert2::check;
1432 use krabka_units::prelude::*;
1433
1434 #[test]
1435 fn versioned_table_handle_carries_retention() {
1436 use crate::{
1437 dsl::{builder::StreamsBuilder, config::Materialized},
1438 processor::serde::{I64Serde, StringSerde},
1439 };
1440
1441 let b = StreamsBuilder::new();
1442 let t = b.table_explicit::<StringSerde, I64Serde>(
1443 "in",
1444 crate::processor::serde::Consumed::with(StringSerde, I64Serde),
1445 Materialized::with(StringSerde, I64Serde).as_versioned("vt", minutes(10)),
1446 );
1447 check!(t.versioned_retention == Some(minutes(10)));
1448
1449 let plain = b.table_explicit::<StringSerde, I64Serde>(
1450 "in2",
1451 crate::processor::serde::Consumed::with(StringSerde, I64Serde),
1452 Materialized::with(StringSerde, I64Serde).as_store("pt"),
1453 );
1454 check!(plain.versioned_retention == None);
1455 }
1456}
1457
1458#[cfg(test)]
1459mod fk_exec_tests {
1460 use std::sync::{Arc, Mutex};
1461
1462 use crate::{
1463 dsl::builder::StreamsBuilder,
1464 processor::serde::{Consumed, StringSerde},
1465 test_driver::TopologyTestDriver,
1466 };
1467
1468 type Out = Arc<Mutex<Vec<(Option<String>, Option<String>)>>>;
1469 type Step<'a> = (
1472 &'a str,
1473 &'a str,
1474 &'a str,
1475 i64,
1476 &'a [(&'a str, Option<&'a str>)],
1477 );
1478
1479 fn pipe(d: &mut TopologyTestDriver, topic: &str, key: &str, val: &str, ts: i64) {
1487 d.pipe_input(
1488 topic,
1489 Consumed::with(StringSerde, StringSerde),
1490 Some(key.to_string()),
1491 val.to_string(),
1492 ts,
1493 );
1494 }
1495
1496 fn run_sequence(buf: &Out, d: &mut TopologyTestDriver, steps: &[Step]) {
1500 let mut seen = 0usize;
1501 for (topic, key, val, ts, want) in steps {
1502 pipe(d, topic, key, val, *ts);
1503 let all = buf.lock().unwrap().clone();
1504 let step_out: Vec<(Option<String>, Option<String>)> = all[seen..].to_vec();
1505 seen = all.len();
1506 let want_owned: Vec<(Option<String>, Option<String>)> = want
1507 .iter()
1508 .map(|(k, v)| (Some((*k).to_string()), v.map(str::to_string)))
1509 .collect();
1510 assert_eq!(step_out, want_owned, "step {topic}:{key}={val}@{ts}");
1511 }
1512 }
1513
1514 fn tables(
1515 b: &StreamsBuilder,
1516 ) -> (super::KTable<String, String>, super::KTable<String, String>) {
1517 let ta = b.table::<String, String>("a", "sa");
1518 let tb = b.table::<String, String>("b", "sb");
1519 (ta, tb)
1520 }
1521
1522 #[test]
1531 fn fk_inner_sequence_matches_behavior_json() {
1532 let b = StreamsBuilder::new();
1533 let (ta, tb) = tables(&b);
1534 let buf: Out = Arc::new(Mutex::new(Vec::new()));
1535 ta.join_on_foreign_key(
1536 &tb,
1537 |va: &String| va.clone(),
1538 |va: &String, vb: &String| format!("{va}{vb}"),
1539 StringSerde,
1540 )
1541 .collect_changes(buf.clone());
1542 drop(ta);
1543 drop(tb);
1544 let built = b.build("app").unwrap();
1545 let mut d = TopologyTestDriver::new(&built).unwrap();
1546 run_sequence(
1547 &buf,
1548 &mut d,
1549 &[
1550 ("a", "k1", "A", 0, &[]),
1551 ("b", "A", "X", 1, &[("k1", Some("AX"))]),
1552 ("a", "k1", "A2", 2, &[("k1", None)]),
1553 ("a", "k2", "A", 3, &[("k2", Some("AX"))]),
1554 ("b", "A", "Y", 4, &[("k2", Some("AY"))]),
1555 ("a", "k1", "B", 5, &[("k1", None)]),
1556 ],
1557 );
1558 }
1559
1560 #[test]
1568 fn fk_left_sequence_matches_behavior_json() {
1569 let b = StreamsBuilder::new();
1570 let (ta, tb) = tables(&b);
1571 let buf: Out = Arc::new(Mutex::new(Vec::new()));
1572 ta.left_join_on_foreign_key(
1573 &tb,
1574 |va: &String| va.clone(),
1575 |va: &String, vb: Option<&String>| format!("{va}{}", vb.map_or("_", String::as_str)),
1576 StringSerde,
1577 )
1578 .collect_changes(buf.clone());
1579 drop(ta);
1580 drop(tb);
1581 let built = b.build("app").unwrap();
1582 let mut d = TopologyTestDriver::new(&built).unwrap();
1583 run_sequence(
1584 &buf,
1585 &mut d,
1586 &[
1587 ("a", "k1", "A", 0, &[("k1", Some("A_"))]),
1588 ("b", "A", "X", 1, &[("k1", Some("AX"))]),
1589 ("a", "k1", "A2", 2, &[("k1", Some("A2_"))]),
1590 ("a", "k2", "A", 3, &[("k2", Some("AX"))]),
1591 ("b", "A", "Y", 4, &[("k2", Some("AY"))]),
1592 ("a", "k1", "B", 5, &[("k1", Some("B_"))]),
1593 ],
1594 );
1595 }
1596}