Skip to main content

reifydb_sdk/testing/
context.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	cell::RefCell,
6	collections::{BTreeMap, HashMap, HashSet},
7	ops::Bound,
8	sync::Arc,
9};
10
11use postcard::to_stdvec;
12use reifydb_core::{
13	common::CommitVersion,
14	encoded::{key::EncodedKey, row::EncodedRow},
15};
16use reifydb_runtime::sync::mutex::Mutex;
17use reifydb_value::{
18	util::cowvec::CowVec,
19	value::{Value, value_type::ValueType},
20};
21
22thread_local! {
23
24
25
26
27
28	static SHARED_DICTS: RefCell<Option<Arc<Mutex<DictionaryData>>>> = const { RefCell::new(None) };
29}
30
31fn shared_dicts() -> Arc<Mutex<DictionaryData>> {
32	SHARED_DICTS.with(|s| {
33		let mut s = s.borrow_mut();
34		if s.is_none() {
35			*s = Some(Arc::new(Mutex::new(DictionaryData::default())));
36		}
37		s.as_ref().unwrap().clone()
38	})
39}
40
41pub fn seed_test_dictionary(name: &str, id: u64, id_type: ValueType, entries: Vec<(u128, Value)>) {
42	let store = shared_dicts();
43	let mut store = store.lock();
44	store.register(name, id, id_type, &entries);
45
46	store.auto_intern.insert(id);
47}
48
49pub fn clear_test_dictionaries() {
50	SHARED_DICTS.with(|s| *s.borrow_mut() = None);
51}
52
53#[derive(Default)]
54struct DictionaryData {
55	by_name: HashMap<String, (u64, u8)>,
56	id_type_by_dict: HashMap<u64, u8>,
57	find: HashMap<(u64, Vec<u8>), (u128, u8)>,
58	get: HashMap<(u64, u128), Vec<u8>>,
59	next_id: HashMap<u64, u128>,
60	auto_intern: HashSet<u64>,
61}
62
63impl DictionaryData {
64	fn register(&mut self, name: &str, id: u64, id_type: ValueType, entries: &[(u128, Value)]) {
65		let id_type_byte = id_type.to_u8();
66		self.by_name.insert(name.to_string(), (id, id_type_byte));
67		self.id_type_by_dict.insert(id, id_type_byte);
68		let mut next = self.next_id.get(&id).copied().unwrap_or(0);
69		for (entry_id, value) in entries {
70			let value_bytes = to_stdvec(value).expect("serialize dictionary value");
71			self.find.insert((id, value_bytes.clone()), (*entry_id, id_type_byte));
72			self.get.insert((id, *entry_id), value_bytes);
73			next = next.max(*entry_id + 1);
74		}
75		self.next_id.insert(id, next);
76	}
77
78	fn find_or_intern(&mut self, dictionary: u64, value_bytes: &[u8]) -> Option<(u128, u8)> {
79		if let Some(v) = self.find.get(&(dictionary, value_bytes.to_vec())) {
80			return Some(*v);
81		}
82		if !self.auto_intern.contains(&dictionary) {
83			return None;
84		}
85		let id_type_byte = *self.id_type_by_dict.get(&dictionary)?;
86		let entry_id = self.next_id.get(&dictionary).copied().unwrap_or(0);
87		self.find.insert((dictionary, value_bytes.to_vec()), (entry_id, id_type_byte));
88		self.get.insert((dictionary, entry_id), value_bytes.to_vec());
89		self.next_id.insert(dictionary, entry_id + 1);
90		Some((entry_id, id_type_byte))
91	}
92}
93
94#[derive(Clone)]
95pub struct TestContext {
96	state_store: Arc<Mutex<HashMap<EncodedKey, EncodedRow>>>,
97	store: Arc<Mutex<BTreeMap<EncodedKey, EncodedRow>>>,
98	dictionaries: Arc<Mutex<DictionaryData>>,
99	version: CommitVersion,
100	logs: Arc<Mutex<Vec<String>>>,
101}
102
103impl Default for TestContext {
104	fn default() -> Self {
105		Self::new(CommitVersion(1))
106	}
107}
108
109impl TestContext {
110	pub fn new(version: CommitVersion) -> Self {
111		let dictionaries = SHARED_DICTS
112			.with(|s| s.borrow().clone())
113			.unwrap_or_else(|| Arc::new(Mutex::new(DictionaryData::default())));
114		Self {
115			state_store: Arc::new(Mutex::new(HashMap::new())),
116			store: Arc::new(Mutex::new(BTreeMap::new())),
117			dictionaries,
118			version,
119			logs: Arc::new(Mutex::new(Vec::new())),
120		}
121	}
122
123	pub fn seed_dictionary(&self, name: &str, id: u64, id_type: ValueType, entries: &[(u128, Value)]) {
124		self.dictionaries.lock().register(name, id, id_type, entries);
125	}
126
127	pub fn seed_dictionary_interning(&self, name: &str, id: u64, id_type: ValueType, entries: &[(u128, Value)]) {
128		let mut d = self.dictionaries.lock();
129		d.register(name, id, id_type, entries);
130		d.auto_intern.insert(id);
131	}
132
133	pub fn dictionary_id_by_name(&self, name: &str) -> Option<u64> {
134		self.dictionaries.lock().by_name.get(name).map(|(id, _)| *id)
135	}
136
137	pub fn dictionary_find(&self, dictionary: u64, value_bytes: &[u8]) -> Option<(u128, u8)> {
138		self.dictionaries.lock().find_or_intern(dictionary, value_bytes)
139	}
140
141	pub fn dictionary_get(&self, dictionary: u64, id: u128) -> Option<Vec<u8>> {
142		self.dictionaries.lock().get.get(&(dictionary, id)).cloned()
143	}
144
145	pub fn state_store(&self) -> &Arc<Mutex<HashMap<EncodedKey, EncodedRow>>> {
146		&self.state_store
147	}
148
149	pub fn logs(&self) -> Vec<String> {
150		self.logs.lock().clone()
151	}
152
153	pub fn clear_logs(&self) {
154		self.logs.lock().clear();
155	}
156
157	pub fn version(&self) -> CommitVersion {
158		self.version
159	}
160
161	pub fn set_version(&mut self, version: CommitVersion) {
162		self.version = version;
163	}
164
165	pub fn get_state(&self, key: &EncodedKey) -> Option<Vec<u8>> {
166		self.state_store.lock().get(key).map(|v| v.0.to_vec())
167	}
168
169	pub fn set_state(&self, key: EncodedKey, value: Vec<u8>) {
170		self.state_store.lock().insert(key, EncodedRow(CowVec::new(value)));
171	}
172
173	pub fn remove_state(&self, key: &EncodedKey) -> Option<Vec<u8>> {
174		self.state_store.lock().remove(key).map(|v| v.0.to_vec())
175	}
176
177	pub fn has_state(&self, key: &EncodedKey) -> bool {
178		self.state_store.lock().contains_key(key)
179	}
180
181	pub fn state_count(&self) -> usize {
182		self.state_store.lock().len()
183	}
184
185	pub fn clear_state(&self) {
186		self.state_store.lock().clear();
187	}
188
189	pub fn state_keys(&self) -> Vec<EncodedKey> {
190		self.state_store.lock().keys().cloned().collect()
191	}
192
193	pub fn store(&self) -> &Arc<Mutex<BTreeMap<EncodedKey, EncodedRow>>> {
194		&self.store
195	}
196
197	pub fn get_store(&self, key: &EncodedKey) -> Option<EncodedRow> {
198		self.store.lock().get(key).cloned()
199	}
200
201	pub fn set_store(&self, key: EncodedKey, value: EncodedRow) {
202		self.store.lock().insert(key, value);
203	}
204
205	pub fn store_range(&self, start: Bound<EncodedKey>, end: Bound<EncodedKey>) -> Vec<(EncodedKey, EncodedRow)> {
206		self.store.lock().range((start, end)).map(|(k, v)| (k.clone(), v.clone())).collect()
207	}
208
209	pub fn store_prefix(&self, prefix: &EncodedKey) -> Vec<(EncodedKey, EncodedRow)> {
210		self.store
211			.lock()
212			.iter()
213			.filter(|(k, _)| k.as_slice().starts_with(prefix.as_slice()))
214			.map(|(k, v)| (k.clone(), v.clone()))
215			.collect()
216	}
217}
218
219#[cfg(test)]
220pub mod tests {
221	use super::*;
222	use crate::testing::helpers::encode_key;
223
224	#[test]
225	fn test_context_state_operations() {
226		let ctx = TestContext::default();
227		let key = encode_key("test_key");
228		let value = vec![1, 2, 3];
229
230		// Test set and get
231		ctx.set_state(key.clone(), value.clone());
232		assert_eq!(ctx.get_state(&key), Some(value.clone()));
233		assert!(ctx.has_state(&key));
234
235		// Test remove
236		let removed = ctx.remove_state(&key);
237		assert_eq!(removed, Some(value));
238		assert!(!ctx.has_state(&key));
239		assert_eq!(ctx.get_state(&key), None);
240	}
241
242	#[test]
243	fn test_context_logs() {
244		let ctx = TestContext::default();
245
246		// Simulate logging (would be done through callbacks in real usage)
247		ctx.logs.lock().push("Log 1".to_string());
248		ctx.logs.lock().push("Log 2".to_string());
249
250		let logs = ctx.logs();
251		assert_eq!(logs.len(), 2);
252		assert_eq!(logs[0], "Log 1");
253		assert_eq!(logs[1], "Log 2");
254
255		ctx.clear_logs();
256		assert_eq!(ctx.logs().len(), 0);
257	}
258
259	#[test]
260	fn test_context_state_inspection() {
261		let ctx = TestContext::default();
262
263		ctx.set_state(encode_key("key1"), vec![1]);
264		ctx.set_state(encode_key("key2"), vec![2]);
265		ctx.set_state(encode_key("key3"), vec![3]);
266
267		assert_eq!(ctx.state_count(), 3);
268
269		let keys = ctx.state_keys();
270		assert_eq!(keys.len(), 3);
271
272		ctx.clear_state();
273		assert_eq!(ctx.state_count(), 0);
274	}
275}