Skip to main content

reifydb_engine/
flow_sync.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{collections::BTreeSet, num::NonZeroU64, sync::Arc};
5
6use reifydb_catalog::catalog::Catalog;
7use reifydb_codec::{key::encoded::EncodedKey, row::bytes::EncodedBytes};
8use reifydb_core::{
9	common::{ChangeVersion, CommitVersion},
10	flow::dag::FlowDag,
11	interface::{
12		catalog::{
13			config::{ConfigKey, GetConfig},
14			dictionary::Dictionary,
15			flow::OperatorId,
16			id::{TableId, ViewId},
17			object::ObjectId,
18			table::Table,
19			view::View,
20		},
21		change::{Change, ChangeOrigin, Diff},
22	},
23	internal_err,
24	key::any::TaggedKey,
25};
26use reifydb_flow::txn::{Batch, Emit, Find, Get, Intern, Resolve};
27#[cfg(feature = "testing")]
28use reifydb_flow_sync::testing::{InstalledHooks, TestingTxn};
29use reifydb_flow_sync::{
30	graph::build,
31	node::Node,
32	run::{flow_sources, run, run_flow},
33	txn::{Changes, ClockNow, Lookup},
34};
35use reifydb_runtime::context::RuntimeContext;
36use reifydb_transaction::transaction::Transaction;
37use reifydb_value::value::{
38	Value,
39	datetime::DateTime,
40	dictionary::{DictionaryEntryId, DictionaryId},
41};
42use smallvec::smallvec;
43
44use crate::{Result, backfill, transaction::operation::dictionary::DictionaryOperations, vm::services::Services};
45
46pub(crate) fn sync_transactional_views(services: &Services, tx: Transaction<'_>) -> Result<()> {
47	let mut txn = FlowTransaction {
48		tx,
49		catalog: &services.catalog,
50		runtime_context: &services.runtime_context,
51	};
52	#[cfg(feature = "testing")]
53	if let Some(InstalledHooks(hooks)) = services.ioc.try_resolve::<InstalledHooks>() {
54		return run(&mut TestingTxn::over(txn, hooks), &services.routines, &services.runtime_context);
55	}
56	run(&mut txn, &services.routines, &services.runtime_context)
57}
58
59pub(crate) fn load_flow_diff_need(catalog: &Catalog, tx: &mut Transaction<'_>) -> Result<()> {
60	if tx.flow_diff_need_loaded() {
61		return Ok(());
62	}
63	let mut objects = BTreeSet::new();
64	for flow in catalog.list_transactional_flow_dags(tx)?.iter() {
65		objects.extend(flow_sources(flow));
66	}
67	tx.set_flow_diff_need(objects);
68	Ok(())
69}
70
71pub(crate) fn backfill_transactional_view(
72	services: &Arc<Services>,
73	mut tx: Transaction<'_>,
74	view: ViewId,
75) -> Result<()> {
76	let flow = view_flow(&services.catalog, &mut tx, view)?;
77	let mut nodes = build_flow(services, tx.reborrow(), &flow)?;
78	backfill::run(services, tx.reborrow(), &flow_sources(&flow), query_batch_size(services)?, |scan, change| {
79		feed_flow(services, scan.transaction().reborrow(), &flow, &mut nodes, change)
80	})?;
81	let end = tx.flow_entries_from(0).len();
82	tx.set_flow_cursor(end);
83	Ok(())
84}
85
86fn view_flow(catalog: &Catalog, tx: &mut Transaction<'_>, view: ViewId) -> Result<FlowDag> {
87	match catalog.list_flow_dags_asc(tx)?.into_iter().find(|flow| flow.sink_views().any(|sink| sink == view)) {
88		Some(flow) => Ok(flow),
89		None => internal_err!("transactional view {:?} has no flow to backfill", view),
90	}
91}
92
93fn query_batch_size(services: &Services) -> Result<NonZeroU64> {
94	match NonZeroU64::new(u64::from(services.catalog.get_config_uint2(ConfigKey::QueryRowBatchSize))) {
95		Some(batch_size) => Ok(batch_size),
96		None => internal_err!("QUERY_ROW_BATCH_SIZE is 0, which its config validation rejects"),
97	}
98}
99
100fn build_flow(services: &Services, tx: Transaction<'_>, flow: &FlowDag) -> Result<Vec<(OperatorId, Node)>> {
101	let mut txn = FlowTransaction {
102		tx,
103		catalog: &services.catalog,
104		runtime_context: &services.runtime_context,
105	};
106	#[cfg(feature = "testing")]
107	if let Some(InstalledHooks(hooks)) = services.ioc.try_resolve::<InstalledHooks>() {
108		return build(&mut TestingTxn::over(txn, hooks), flow, &services.routines, &services.runtime_context);
109	}
110	build(&mut txn, flow, &services.routines, &services.runtime_context)
111}
112
113fn feed_flow(
114	services: &Services,
115	tx: Transaction<'_>,
116	flow: &FlowDag,
117	nodes: &mut [(OperatorId, Node)],
118	change: Change,
119) -> Result<()> {
120	let mut txn = FlowTransaction {
121		tx,
122		catalog: &services.catalog,
123		runtime_context: &services.runtime_context,
124	};
125	#[cfg(feature = "testing")]
126	if let Some(InstalledHooks(hooks)) = services.ioc.try_resolve::<InstalledHooks>() {
127		return run_flow(&mut TestingTxn::over(txn, hooks), flow, nodes, change);
128	}
129	run_flow(&mut txn, flow, nodes, change)
130}
131
132pub(crate) struct FlowTransaction<'a> {
133	tx: Transaction<'a>,
134	catalog: &'a Catalog,
135	runtime_context: &'a RuntimeContext,
136}
137
138fn tagged(key: &EncodedKey) -> TaggedKey {
139	TaggedKey::decode(key).unwrap_or_else(|| panic!("flow sync produced an undecodable key {key:?}"))
140}
141
142impl Changes for FlowTransaction<'_> {
143	fn cursor(&self) -> usize {
144		self.tx.flow_cursor()
145	}
146
147	fn entries_from(&self, at: usize) -> &[(ObjectId, Diff)] {
148		self.tx.flow_entries_from(at)
149	}
150
151	fn set_cursor(&mut self, at: usize) {
152		self.tx.set_flow_cursor(at)
153	}
154}
155
156impl Get for FlowTransaction<'_> {
157	fn get(&mut self, keys: &[EncodedKey]) -> Result<Vec<Option<EncodedBytes>>> {
158		let keys: Vec<TaggedKey> = keys.iter().map(tagged).collect();
159		Ok(self.tx.get(&keys)?.into_iter().map(|row| row.map(|row| row.bytes)).collect())
160	}
161}
162
163impl Batch for FlowTransaction<'_> {
164	fn set(&mut self, keys: Vec<EncodedKey>, rows: Vec<EncodedBytes>) -> Result<()> {
165		self.tx.set(keys.iter().map(tagged).zip(rows).collect())
166	}
167
168	fn remove(&mut self, keys: Vec<EncodedKey>) -> Result<()> {
169		let keys: Vec<TaggedKey> = keys.iter().map(tagged).collect();
170		let committed = self.tx.get_committed(&keys)?;
171		self.tx.remove(keys
172			.into_iter()
173			.zip(committed)
174			.map(|(key, committed)| (key, committed.map(|row| row.bytes)))
175			.collect())
176	}
177}
178
179impl Emit for FlowTransaction<'_> {
180	fn emit(&mut self, view: ViewId, diff: Diff) -> Result<()> {
181		let object = ObjectId::view(view);
182		self.tx.touch_flow_object(object);
183		if self.tx.wants_flow_diff(object) {
184			self.tx.track_flow_change(Change {
185				origin: ChangeOrigin::Object(object),
186				diffs: smallvec![diff],
187				version: ChangeVersion::from(CommitVersion(0)),
188				changed_at: DateTime::default(),
189			});
190		}
191		Ok(())
192	}
193}
194
195impl Lookup for FlowTransaction<'_> {
196	fn transactional_flows(&mut self) -> Result<Vec<FlowDag>> {
197		Ok(self.catalog.list_transactional_flow_dags(&mut self.tx)?.to_vec())
198	}
199
200	fn view(&mut self, id: ViewId) -> Result<View> {
201		self.catalog.get_view(&mut self.tx, id)
202	}
203
204	fn table(&mut self, id: TableId) -> Result<Table> {
205		self.catalog.get_table(&mut self.tx, id)
206	}
207}
208
209impl Resolve for FlowTransaction<'_> {
210	fn dictionary(&mut self, id: DictionaryId) -> Result<Option<Dictionary>> {
211		self.catalog.find_dictionary(&mut self.tx, id)
212	}
213
214	fn resolve(&mut self, dictionary: &Dictionary, ids: &[DictionaryEntryId]) -> Result<Vec<Option<Value>>> {
215		self.tx.get_from_dictionary(dictionary, ids)
216	}
217}
218
219impl Find for FlowTransaction<'_> {
220	fn find(&mut self, dictionary: &Dictionary, values: &[Value]) -> Result<Vec<Option<DictionaryEntryId>>> {
221		self.tx.find_in_dictionary(dictionary, values)
222	}
223}
224
225impl Intern for FlowTransaction<'_> {
226	fn intern(&mut self, dictionary: &Dictionary, values: &[Value]) -> Result<Vec<DictionaryEntryId>> {
227		self.tx.intern_values(dictionary, values.to_vec())
228	}
229}
230
231impl ClockNow for FlowTransaction<'_> {
232	fn now(&self) -> DateTime {
233		self.runtime_context.clock.now()
234	}
235}
236
237#[cfg(test)]
238mod tests {
239	use reifydb_core::{
240		interface::{
241			catalog::{id::ViewId, object::ObjectId},
242			change::Diff,
243		},
244		value::batch::single_row,
245	};
246	use reifydb_flow::txn::Emit;
247	use reifydb_flow_sync::txn::{Changes, Lookup};
248	use reifydb_test_harness::engine::create_test_admin_transaction;
249	use reifydb_transaction::transaction::{Transaction, admin::AdminTransaction};
250	use reifydb_value::{params::Params, value::Value};
251
252	use super::FlowTransaction;
253	use crate::vm::{Admin, executor::Executor};
254
255	fn admin(executor: &Executor, txn: &mut AdminTransaction, rql: &str) {
256		let r = executor.admin(
257			txn,
258			Admin {
259				rql,
260				params: Params::default(),
261			},
262		);
263		if let Some(e) = r.error {
264			panic!("{rql}: {e:?}");
265		}
266	}
267
268	#[test]
269	fn an_emitted_view_diff_lands_after_the_cursor_and_moving_the_cursor_skips_it() {
270		// Without this the run loop either re-feeds its own view diffs or never hands them to a downstream
271		// flow.
272		let executor = Executor::testing();
273		let mut txn = create_test_admin_transaction();
274		let mut sync = FlowTransaction {
275			tx: Transaction::Admin(&mut txn),
276			catalog: &executor.catalog,
277			runtime_context: &executor.runtime_context,
278		};
279		let at = sync.cursor();
280		let diff = Diff::insert(single_row([("id", Value::Int4(1))]).unwrap());
281		sync.emit(ViewId(7), diff.clone()).unwrap();
282		assert_eq!(sync.entries_from(at), vec![(ObjectId::view(ViewId(7)), diff)]);
283		sync.set_cursor(at + 1);
284		assert!(sync.entries_from(sync.cursor()).is_empty());
285	}
286
287	#[test]
288	fn a_transactional_view_created_in_the_same_txn_is_listed_and_a_deferred_one_is_not() {
289		// A create-then-write txn must maintain the new view; a deferred flow here would be run twice.
290		let executor = Executor::testing();
291		let mut txn = create_test_admin_transaction();
292		admin(&executor, &mut txn, "CREATE NAMESPACE ns");
293		admin(&executor, &mut txn, "CREATE TABLE ns::src { id: int4 }");
294		admin(&executor, &mut txn, "CREATE DEFERRED VIEW ns::d { id: int4 } AS { FROM ns::src }");
295		admin(&executor, &mut txn, "CREATE TRANSACTIONAL VIEW ns::v { id: int4 } AS { FROM ns::src }");
296		let mut sync = FlowTransaction {
297			tx: Transaction::Admin(&mut txn),
298			catalog: &executor.catalog,
299			runtime_context: &executor.runtime_context,
300		};
301		let flows = sync.transactional_flows().unwrap();
302		assert_eq!(flows.len(), 1, "only the transactional view's flow may be listed");
303	}
304}