Skip to main content

reifydb_engine/
test_harness.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{mem, ops::Deref, sync::Arc};
5
6use reifydb_catalog::{
7	cache::CatalogCache,
8	catalog::{
9		Catalog,
10		namespace::NamespaceToCreate,
11		table::{TableColumnToCreate, TableToCreate},
12	},
13};
14#[cfg(not(target_arch = "wasm32"))]
15use reifydb_cdc::storage::recent_cache::RecentCdcCache;
16use reifydb_cdc::{
17	consume::wake::CdcWakeRegistry,
18	produce::{
19		producer::{CdcProducerEventListener, spawn_cdc_producer},
20		watermark::CdcProducerWatermark,
21	},
22	storage::CdcStore,
23};
24use reifydb_core::{
25	actors::cdc::CdcProduceHandle,
26	event::{EventBus, transaction::PostCommitEvent},
27	interface::catalog::id::NamespaceId,
28	util::ioc::IocContainer,
29};
30use reifydb_extension::transform::registry::Transforms;
31use reifydb_routine::{
32	function::default_native_functions, procedure::default_native_procedures, routine::registry::Routines,
33};
34use reifydb_runtime::{
35	Runtime, RuntimeConfig,
36	actor::system::{ActorSpawner, ActorSystem},
37	context::{
38		RuntimeContext,
39		clock::{Clock, MockClock},
40		rng::Rng,
41	},
42	pool::{PoolConfig, Pools},
43};
44#[cfg(not(target_arch = "wasm32"))]
45use reifydb_sqlite::SqliteConfig;
46use reifydb_store_multi::{MultiStore, gc::epoch::listener::VersionEpochListener};
47use reifydb_store_single::SingleStore;
48use reifydb_transaction::{
49	dictionary::DictionaryAllocatorRegistry,
50	interceptor::{factory::InterceptorFactory, interceptors::Interceptors},
51	multi::transaction::MultiTransaction,
52	single::SingleTransaction,
53	transaction::admin::AdminTransaction,
54};
55use reifydb_value::{
56	fragment::Fragment,
57	params::Params,
58	value::{constraint::TypeConstraint, frame::frame::Frame, identity::IdentityId, value_type::ValueType},
59};
60
61use crate::{engine::StandardEngine, vm::services::EngineConfig};
62
63pub struct TestEngine {
64	engine: StandardEngine,
65	mock_clock: MockClock,
66	_runtime: Runtime,
67}
68
69impl Default for TestEngine {
70	fn default() -> Self {
71		Self::new()
72	}
73}
74
75impl TestEngine {
76	pub fn new() -> Self {
77		Self::builder().with_cdc().build()
78	}
79
80	pub fn builder() -> TestEngineBuilder {
81		TestEngineBuilder::default()
82	}
83
84	pub fn admin(&self, rql: &str) -> Vec<Frame> {
85		let r = self.engine.admin_as(IdentityId::system(), rql, Params::None);
86		if let Some(e) = r.error {
87			panic!("admin failed: {e:?}\nrql: {rql}")
88		}
89		r.frames
90	}
91
92	pub fn command(&self, rql: &str) -> Vec<Frame> {
93		let r = self.engine.command_as(IdentityId::system(), rql, Params::None);
94		if let Some(e) = r.error {
95			panic!("command failed: {e:?}\nrql: {rql}")
96		}
97		r.frames
98	}
99
100	pub fn query(&self, rql: &str) -> Vec<Frame> {
101		let r = self.engine.query_as(IdentityId::system(), rql, Params::None);
102		if let Some(e) = r.error {
103			panic!("query failed: {e:?}\nrql: {rql}")
104		}
105		r.frames
106	}
107
108	pub fn admin_err(&self, rql: &str) -> String {
109		let r = self.engine.admin_as(IdentityId::system(), rql, Params::None);
110		match r.error {
111			Some(e) => format!("{e:?}"),
112			None => panic!("Expected error but admin succeeded\nrql: {rql}"),
113		}
114	}
115
116	pub fn command_err(&self, rql: &str) -> String {
117		let r = self.engine.command_as(IdentityId::system(), rql, Params::None);
118		match r.error {
119			Some(e) => format!("{e:?}"),
120			None => panic!("Expected error but command succeeded\nrql: {rql}"),
121		}
122	}
123
124	pub fn query_err(&self, rql: &str) -> String {
125		let r = self.engine.query_as(IdentityId::system(), rql, Params::None);
126		match r.error {
127			Some(e) => format!("{e:?}"),
128			None => panic!("Expected error but query succeeded\nrql: {rql}"),
129		}
130	}
131
132	pub fn row_count(frames: &[Frame]) -> usize {
133		frames.first().map(|f| f.row_count()).unwrap_or(0)
134	}
135
136	pub fn identity() -> IdentityId {
137		IdentityId::system()
138	}
139
140	pub fn inner(&self) -> &StandardEngine {
141		&self.engine
142	}
143
144	pub fn mock_clock(&self) -> MockClock {
145		self.mock_clock.clone()
146	}
147}
148
149impl Deref for TestEngine {
150	type Target = StandardEngine;
151
152	fn deref(&self) -> &StandardEngine {
153		&self.engine
154	}
155}
156
157#[derive(Default)]
158pub struct TestEngineBuilder {
159	cdc: bool,
160	#[cfg(not(target_arch = "wasm32"))]
161	sqlite_cdc: Option<SqliteConfig>,
162}
163
164impl TestEngineBuilder {
165	pub fn with_cdc(mut self) -> Self {
166		self.cdc = true;
167		self
168	}
169
170	#[cfg(not(target_arch = "wasm32"))]
171	pub fn with_sqlite_cdc(mut self, config: SqliteConfig) -> Self {
172		self.cdc = true;
173		self.sqlite_cdc = Some(config);
174		self
175	}
176
177	pub fn build(self) -> TestEngine {
178		let mock_clock = MockClock::from_millis(1000);
179		let runtime = make_test_runtime(&mock_clock);
180		let spawner = runtime.spawner();
181		let clock = runtime.clock().clone();
182		let rng = runtime.rng().clone();
183
184		let eventbus = EventBus::new(&spawner);
185		let multi_store = MultiStore::testing_memory_with_eventbus(eventbus.clone());
186		let single_store = SingleStore::testing_memory();
187		let single = SingleTransaction::new(single_store.clone(), eventbus.clone());
188		let catalog_cache = CatalogCache::new();
189		let multi = MultiTransaction::new(
190			multi_store.clone(),
191			single.clone(),
192			eventbus.clone(),
193			spawner.clone(),
194			clock.clone(),
195			rng.clone(),
196			Arc::new(catalog_cache.clone()),
197		)
198		.unwrap();
199
200		let mut ioc = IocContainer::new();
201		ioc = ioc.register(catalog_cache.clone());
202		ioc = ioc.register(spawner.clone()).register(clock.clone()).register(rng.clone());
203		ioc = ioc.register(single_store.clone());
204		ioc = ioc.register(eventbus.clone());
205
206		#[cfg(not(target_arch = "wasm32"))]
207		let cdc_store = match self.sqlite_cdc {
208			Some(config) => CdcStore::sqlite(config, RecentCdcCache::DEFAULT_CAPACITY),
209			None => CdcStore::memory(),
210		};
211		#[cfg(target_arch = "wasm32")]
212		let cdc_store = CdcStore::memory();
213		ioc = ioc.register(cdc_store.clone());
214
215		let cdc_producer_watermark = CdcProducerWatermark::new();
216		ioc = ioc.register(cdc_producer_watermark.clone());
217
218		let cdc_wake_registry = CdcWakeRegistry::new();
219		ioc = ioc.register(cdc_wake_registry.clone());
220
221		let ioc_for_cdc = ioc.clone();
222
223		let engine = StandardEngine::new(
224			multi,
225			single.clone(),
226			eventbus.clone(),
227			InterceptorFactory::default(),
228			Catalog::new(catalog_cache),
229			EngineConfig {
230				runtime_context: RuntimeContext::new(clock.clone(), rng.clone()),
231				routines: {
232					let b = Routines::builder();
233					let b = default_native_functions(b);
234					default_native_procedures(b).configure()
235				},
236				transforms: Transforms::empty(),
237				ioc,
238				#[cfg(not(reifydb_single_threaded))]
239				remote_registry: None,
240			},
241		);
242
243		if self.cdc {
244			register_cdc_producer(
245				&spawner,
246				clock.clone(),
247				cdc_store,
248				multi_store,
249				&engine,
250				&eventbus,
251				ioc_for_cdc,
252				cdc_producer_watermark,
253				cdc_wake_registry,
254			);
255		}
256
257		TestEngine {
258			engine,
259			mock_clock,
260			_runtime: runtime,
261		}
262	}
263}
264
265#[inline]
266fn make_test_runtime(mock_clock: &MockClock) -> Runtime {
267	let config = RuntimeConfig::default().seeded(1000);
268	let config = RuntimeConfig {
269		clock: Clock::Mock(mock_clock.clone()),
270		..config
271	};
272	let pools = PoolConfig {
273		coordination_threads: 2,
274		flow_threads: 2,
275		task_threads: 2,
276		compute_threads: 2,
277		async_threads: 2,
278	};
279	Runtime::from_config(config, pools)
280}
281
282#[allow(clippy::too_many_arguments)]
283fn register_cdc_producer(
284	spawner: &ActorSpawner,
285	clock: Clock,
286	cdc_store: CdcStore,
287	multi_store: MultiStore,
288	engine: &StandardEngine,
289	eventbus: &EventBus,
290	ioc_for_cdc: IocContainer,
291	watermark: CdcProducerWatermark,
292	wake_registry: CdcWakeRegistry,
293) {
294	let cdc_handle = spawn_cdc_producer(
295		spawner,
296		cdc_store,
297		multi_store,
298		engine.clone(),
299		eventbus.clone(),
300		clock.clone(),
301		watermark,
302		wake_registry,
303	);
304	eventbus.register::<PostCommitEvent, _>(CdcProducerEventListener::new(
305		cdc_handle.actor_ref().clone(),
306		clock.clone(),
307	));
308	eventbus.register::<PostCommitEvent, _>(VersionEpochListener::new(engine.version_epoch().clone(), clock));
309	ioc_for_cdc.register_service::<Arc<CdcProduceHandle>>(Arc::new(cdc_handle));
310}
311
312pub fn create_test_admin_transaction() -> AdminTransaction {
313	let multi_store = MultiStore::testing_memory();
314	let single_store = SingleStore::testing_memory();
315
316	let pools = Pools::new(PoolConfig::sync_only());
317	let actor_system = ActorSystem::new(pools, Clock::Real);
318	let spawner = actor_system.spawner();
319	mem::forget(actor_system);
320	let event_bus = EventBus::new(&spawner);
321	let single = SingleTransaction::new(single_store, event_bus.clone());
322	let multi = MultiTransaction::new(
323		multi_store,
324		single.clone(),
325		event_bus.clone(),
326		spawner,
327		Clock::Mock(MockClock::from_millis(1000)),
328		Rng::seeded(42),
329		Arc::new(CatalogCache::new()),
330	)
331	.unwrap();
332
333	let mut txn = AdminTransaction::new(
334		multi,
335		single,
336		event_bus,
337		Interceptors::new(),
338		IdentityId::system(),
339		Clock::Mock(MockClock::from_millis(1000)),
340	)
341	.unwrap();
342	txn.set_dictionary_allocators(DictionaryAllocatorRegistry::new());
343	txn
344}
345
346pub fn create_test_admin_transaction_with_internal_shape() -> AdminTransaction {
347	let multi_store = MultiStore::testing_memory();
348	let single_store = SingleStore::testing_memory();
349
350	let pools = Pools::new(PoolConfig::sync_only());
351	let actor_system = ActorSystem::new(pools, Clock::Real);
352	let spawner = actor_system.spawner();
353	mem::forget(actor_system);
354	let event_bus = EventBus::new(&spawner);
355	let single = SingleTransaction::new(single_store, event_bus.clone());
356	let multi = MultiTransaction::new(
357		multi_store,
358		single.clone(),
359		event_bus.clone(),
360		spawner,
361		Clock::Mock(MockClock::from_millis(1000)),
362		Rng::seeded(42),
363		Arc::new(CatalogCache::new()),
364	)
365	.unwrap();
366	let mut result = AdminTransaction::new(
367		multi,
368		single.clone(),
369		event_bus.clone(),
370		Interceptors::new(),
371		IdentityId::system(),
372		Clock::Mock(MockClock::from_millis(1000)),
373	)
374	.unwrap();
375	result.set_dictionary_allocators(DictionaryAllocatorRegistry::new());
376
377	let catalog_cache = CatalogCache::new();
378	let catalog = Catalog::new(catalog_cache);
379
380	let namespace = catalog
381		.create_namespace(
382			&mut result,
383			NamespaceToCreate {
384				namespace_fragment: None,
385				name: "reifydb".to_string(),
386				local_name: "reifydb".to_string(),
387				parent_id: NamespaceId::ROOT,
388				grpc: None,
389				token: None,
390			},
391		)
392		.unwrap();
393
394	catalog.create_table(
395		&mut result,
396		TableToCreate {
397			name: Fragment::internal("flows"),
398			namespace: namespace.id(),
399			columns: vec![
400				TableColumnToCreate {
401					name: Fragment::internal("id"),
402					fragment: Fragment::None,
403					constraint: TypeConstraint::unconstrained(ValueType::Int8),
404					properties: vec![],
405					auto_increment: true,
406					dictionary_id: None,
407				},
408				TableColumnToCreate {
409					name: Fragment::internal("data"),
410					fragment: Fragment::None,
411					constraint: TypeConstraint::unconstrained(ValueType::Blob),
412					properties: vec![],
413					auto_increment: false,
414					dictionary_id: None,
415				},
416			],
417			retention_strategy: None,
418			primary_key_columns: None,
419			partition_by: vec![],
420			underlying: false,
421		},
422	)
423	.unwrap();
424
425	result
426}