Skip to main content

reifydb_sub_flow/operator/distinct/
operator.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{collections::HashSet, sync::LazyLock};
5
6use indexmap::IndexMap;
7use postcard::{from_bytes, to_stdvec};
8use reifydb_abi::operator::capabilities::OperatorCapability;
9use reifydb_codec::{encoded::shape::RowShape, key::encoded::EncodedKey};
10use reifydb_core::{
11	interface::{
12		catalog::flow::FlowNodeId,
13		change::{Change, Diff},
14	},
15	value::column::columns::Columns,
16};
17use reifydb_engine::{
18	expression::{
19		compile::{CompiledExpr, compile_expression},
20		context::CompileContext,
21	},
22	vm::stack::SymbolTable,
23};
24use reifydb_routine::routine::registry::Routines;
25use reifydb_rql::expression::Expression;
26use reifydb_runtime::context::RuntimeContext;
27use reifydb_sdk::operator::Tick;
28use reifydb_value::{
29	Result,
30	error::Error,
31	params::Params,
32	util::hash::Hash128,
33	value::{blob::Blob, duration::Duration},
34};
35
36use crate::{
37	error::FlowStateError,
38	operator::{
39		Operator, OperatorCell,
40		distinct::state::{DistinctEntry, DistinctLayout, DistinctState},
41		stateful::{raw::RawStatefulOperator, row::RowNumberProvider, single::SingleStateful, utils},
42	},
43	transaction::{FlowTransaction, slot::PersistFn},
44};
45
46const ENTRY_KEY_PREFIX: u8 = 0x01;
47const LAYOUT_KEY_PREFIX: u8 = 0x02;
48
49struct DistinctWorkingSet {
50	state: DistinctState,
51	loaded: HashSet<Hash128>,
52}
53
54pub(super) static EMPTY_PARAMS: Params = Params::None;
55pub(super) static EMPTY_SYMBOL_TABLE: LazyLock<SymbolTable> = LazyLock::new(SymbolTable::new);
56
57pub struct DistinctOperator {
58	parent: OperatorCell,
59	pub(super) node: FlowNodeId,
60	pub(super) compiled_expressions: Vec<CompiledExpr>,
61	pub(super) shape: RowShape,
62	pub(super) routines: Routines,
63	pub(super) runtime_context: RuntimeContext,
64	pub(super) ttl_nanos: Option<u64>,
65	pub(super) row_number_provider: RowNumberProvider,
66}
67
68impl DistinctOperator {
69	pub fn new(
70		parent: OperatorCell,
71		node: FlowNodeId,
72		expressions: Vec<Expression>,
73		routines: Routines,
74		runtime_context: RuntimeContext,
75		ttl_nanos: Option<u64>,
76	) -> Self {
77		let symbols = SymbolTable::new();
78		let compile_ctx = CompileContext {
79			symbols: &symbols,
80		};
81		let compiled_expressions: Vec<CompiledExpr> = expressions
82			.iter()
83			.map(|e| compile_expression(&compile_ctx, e))
84			.collect::<Result<Vec<_>>>()
85			.expect("Failed to compile expressions");
86
87		Self {
88			parent,
89			node,
90			compiled_expressions,
91			shape: RowShape::operator_state(),
92			routines,
93			runtime_context,
94			ttl_nanos,
95			row_number_provider: RowNumberProvider::new(node),
96		}
97	}
98
99	pub(crate) fn output_schema(&self) -> Option<Columns> {
100		self.parent.output_schema()
101	}
102
103	pub(super) fn entry_key(hash: Hash128) -> EncodedKey {
104		let mut bytes = Vec::with_capacity(1 + 16);
105		bytes.push(ENTRY_KEY_PREFIX);
106		bytes.extend_from_slice(&hash.0.to_be_bytes());
107		EncodedKey::new(bytes)
108	}
109
110	fn layout_storage_key() -> EncodedKey {
111		EncodedKey::new(vec![LAYOUT_KEY_PREFIX])
112	}
113
114	pub(super) fn hash_from_entry_key(key: &[u8]) -> Option<Hash128> {
115		if key.first() != Some(&ENTRY_KEY_PREFIX) || key.len() != 1 + 16 {
116			return None;
117		}
118		let mut bytes = [0u8; 16];
119		bytes.copy_from_slice(&key[1..17]);
120		Some(Hash128(u128::from_be_bytes(bytes)))
121	}
122
123	fn load_entry(&self, txn: &mut FlowTransaction, hash: Hash128) -> Result<Option<DistinctEntry>> {
124		match utils::state_get(self.node, txn, &Self::entry_key(hash))? {
125			Some(row) => {
126				let blob = self.shape.get_blob(&row, 0);
127				if blob.is_empty() {
128					return Ok(None);
129				}
130				let entry: DistinctEntry = from_bytes(blob.as_ref()).map_err(|e| {
131					Error::from(FlowStateError::Decode {
132						state: "DistinctEntry",
133						cause: e.to_string(),
134					})
135				})?;
136				Ok(Some(entry))
137			}
138			None => Ok(None),
139		}
140	}
141
142	fn load_layout(&self, txn: &mut FlowTransaction) -> Result<DistinctLayout> {
143		match utils::state_get(self.node, txn, &Self::layout_storage_key())? {
144			Some(row) => {
145				let blob = self.shape.get_blob(&row, 0);
146				if blob.is_empty() {
147					return Ok(DistinctLayout::new());
148				}
149				from_bytes(blob.as_ref()).map_err(|e| {
150					Error::from(FlowStateError::Decode {
151						state: "DistinctLayout",
152						cause: e.to_string(),
153					})
154				})
155			}
156			None => Ok(DistinctLayout::new()),
157		}
158	}
159
160	#[cfg(test)]
161	pub(super) fn count_entries(&self, txn: &mut FlowTransaction) -> usize {
162		utils::state_scan_all(self.node, txn)
163			.unwrap()
164			.iter()
165			.filter(|(k, _)| Self::hash_from_entry_key(k.as_ref()).is_some())
166			.count()
167	}
168
169	fn batch_hashes(&self, diffs: &[Diff]) -> Result<HashSet<Hash128>> {
170		let mut touched: HashSet<Hash128> = HashSet::new();
171		for diff in diffs {
172			match diff {
173				Diff::Insert {
174					post,
175					..
176				} => touched.extend(self.compute_hashes(post)?),
177				Diff::Update {
178					pre,
179					post,
180					..
181				} => {
182					touched.extend(self.compute_hashes(pre)?);
183					touched.extend(self.compute_hashes(post)?);
184				}
185				Diff::Remove {
186					pre,
187					..
188				} => touched.extend(self.compute_hashes(pre)?),
189			}
190		}
191		Ok(touched)
192	}
193}
194
195impl RawStatefulOperator for DistinctOperator {}
196
197impl SingleStateful for DistinctOperator {
198	fn layout(&self) -> RowShape {
199		self.shape.clone()
200	}
201}
202
203impl Operator for DistinctOperator {
204	fn id(&self) -> FlowNodeId {
205		self.node
206	}
207
208	fn capabilities(&self) -> &[OperatorCapability] {
209		OperatorCapability::STANDARD_WITH_TICK
210	}
211
212	fn ticks(&self) -> Option<Duration> {
213		self.ticks_interval()
214	}
215
216	fn apply(&self, txn: &mut FlowTransaction, change: Change) -> Result<Change> {
217		let node_id = self.node;
218		let shape = self.shape.clone();
219		let touched = self.batch_hashes(&change.diffs)?;
220
221		let (mut working, persist) = txn.take_operator_state::<DistinctWorkingSet, _>(node_id, |txn| {
222			let layout = self.load_layout(txn)?;
223			let working = DistinctWorkingSet {
224				state: DistinctState {
225					entries: IndexMap::new(),
226					layout,
227				},
228				loaded: HashSet::new(),
229			};
230			let persist: PersistFn = Box::new(move |txn, value| {
231				let working =
232					*value.downcast::<DistinctWorkingSet>().expect("DistinctWorkingSet slot type");
233				for hash in &working.loaded {
234					let key = Self::entry_key(*hash);
235					match working.state.entries.get(hash) {
236						Some(entry) => {
237							let bytes = to_stdvec(entry).map_err(|e| {
238								Error::from(FlowStateError::Encode {
239									state: "DistinctEntry",
240									cause: e.to_string(),
241								})
242							})?;
243							let mut row = shape.allocate();
244							shape.set_blob(&mut row, 0, &Blob::from(bytes));
245							utils::state_set(node_id, txn, &key, row)?;
246						}
247						None => utils::state_drop(node_id, txn, &key)?,
248					}
249				}
250				let layout_bytes = to_stdvec(&working.state.layout).map_err(|e| {
251					Error::from(FlowStateError::Encode {
252						state: "DistinctLayout",
253						cause: e.to_string(),
254					})
255				})?;
256				let mut layout_row = shape.allocate();
257				shape.set_blob(&mut layout_row, 0, &Blob::from(layout_bytes));
258				utils::state_set(node_id, txn, &Self::layout_storage_key(), layout_row)?;
259				Ok(())
260			});
261			Ok((working, persist))
262		})?;
263
264		for &hash in &touched {
265			if working.loaded.insert(hash)
266				&& let Some(entry) = self.load_entry(txn, hash)?
267			{
268				working.state.entries.insert(hash, entry);
269			}
270		}
271
272		let mut result = Vec::new();
273		for diff in change.diffs {
274			match diff {
275				Diff::Insert {
276					post,
277					..
278				} => {
279					let insert_result = self.process_insert(txn, &mut working.state, &post)?;
280					result.extend(insert_result);
281				}
282				Diff::Update {
283					pre,
284					post,
285					..
286				} => {
287					let update_result =
288						self.process_update(txn, &mut working.state, &pre, &post)?;
289					result.extend(update_result);
290				}
291				Diff::Remove {
292					pre,
293					..
294				} => {
295					let remove_result = self.process_remove(txn, &mut working.state, &pre)?;
296					result.extend(remove_result);
297				}
298			}
299		}
300
301		txn.put_operator_state(node_id, working, persist);
302
303		Ok(Change::from_flow(self.node, change.version, result, change.changed_at))
304	}
305
306	fn tick(&self, txn: &mut FlowTransaction, tick: Tick) -> Result<Option<Change>> {
307		self.tick_evict(txn, tick)
308	}
309}