Skip to main content

reifydb_sdk/testing/
callbacks.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::{
5	alloc::{Layout, alloc, dealloc, realloc as system_realloc},
6	slice::from_raw_parts,
7};
8
9use reifydb_type::util::cowvec::CowVec;
10
11/// Allocate memory using system allocator
12#[unsafe(no_mangle)]
13extern "C" fn test_alloc(size: usize) -> *mut u8 {
14	if size == 0 {
15		return ptr::null_mut();
16	}
17
18	let layout = match Layout::from_size_align(size, 8) {
19		Ok(layout) => layout,
20		Err(_) => return ptr::null_mut(),
21	};
22
23	unsafe { alloc(layout) }
24}
25
26/// Free memory allocated by test_alloc
27#[unsafe(no_mangle)]
28unsafe extern "C" fn test_free(ptr: *mut u8, size: usize) {
29	if ptr.is_null() || size == 0 {
30		return;
31	}
32
33	let layout = match Layout::from_size_align(size, 8) {
34		Ok(layout) => layout,
35		Err(_) => return,
36	};
37
38	unsafe { dealloc(ptr, layout) }
39}
40
41/// Reallocate memory
42#[unsafe(no_mangle)]
43unsafe extern "C" fn test_realloc(ptr: *mut u8, old_size: usize, new_size: usize) -> *mut u8 {
44	if ptr.is_null() {
45		return test_alloc(new_size);
46	}
47
48	if new_size == 0 {
49		unsafe { test_free(ptr, old_size) };
50		return ptr::null_mut();
51	}
52
53	let old_layout = match Layout::from_size_align(old_size, 8) {
54		Ok(layout) => layout,
55		Err(_) => return ptr::null_mut(),
56	};
57
58	let new_layout = match Layout::from_size_align(new_size, 8) {
59		Ok(layout) => layout,
60		Err(_) => return ptr::null_mut(),
61	};
62
63	unsafe { system_realloc(ptr, old_layout, new_layout.size()) }
64}
65
66/// Helper to get TestContext from FFI context
67unsafe fn get_test_context(ctx: *mut ContextFFI) -> &'static TestContext {
68	unsafe {
69		let txn_ptr = (*ctx).txn_ptr;
70		&*(txn_ptr as *const TestContext)
71	}
72}
73
74/// Get state value from TestContext
75#[unsafe(no_mangle)]
76extern "C" fn test_state_get(
77	_operator_id: u64,
78	ctx: *mut ContextFFI,
79	key_ptr: *const u8,
80	key_len: usize,
81	output: *mut BufferFFI,
82) -> i32 {
83	if ctx.is_null() || key_ptr.is_null() || output.is_null() {
84		return FFI_ERROR_NULL_PTR;
85	}
86
87	unsafe {
88		let test_ctx = get_test_context(ctx);
89
90		// Convert raw bytes to EncodedKey
91		let key_bytes = from_raw_parts(key_ptr, key_len);
92		let key = EncodedKey(CowVec::new(key_bytes.to_vec()));
93
94		// Get from TestContext state store
95		match test_ctx.get_state(&key) {
96			Some(value_bytes) => {
97				// Allocate and copy value
98				let value_ptr = test_alloc(value_bytes.len());
99				if value_ptr.is_null() {
100					return -2; // Allocation failed
101				}
102
103				ptr::copy_nonoverlapping(value_bytes.as_ptr(), value_ptr, value_bytes.len());
104
105				(*output).ptr = value_ptr;
106				(*output).len = value_bytes.len();
107				(*output).cap = value_bytes.len();
108
109				FFI_OK
110			}
111			None => FFI_NOT_FOUND,
112		}
113	}
114}
115
116/// Set state value in TestContext
117#[unsafe(no_mangle)]
118extern "C" fn test_state_set(
119	_operator_id: u64,
120	ctx: *mut ContextFFI,
121	key_ptr: *const u8,
122	key_len: usize,
123	value_ptr: *const u8,
124	value_len: usize,
125) -> i32 {
126	if ctx.is_null() || key_ptr.is_null() || value_ptr.is_null() {
127		return FFI_ERROR_NULL_PTR;
128	}
129
130	unsafe {
131		let test_ctx = get_test_context(ctx);
132
133		// Convert raw bytes to EncodedKey
134		let key_bytes = from_raw_parts(key_ptr, key_len);
135		let key = EncodedKey(CowVec::new(key_bytes.to_vec()));
136
137		// Convert raw bytes to value
138		let value_bytes = from_raw_parts(value_ptr, value_len);
139
140		// Set in TestContext
141		test_ctx.set_state(key, value_bytes.to_vec());
142
143		FFI_OK
144	}
145}
146
147/// Remove state value from TestContext
148#[unsafe(no_mangle)]
149extern "C" fn test_state_remove(_operator_id: u64, ctx: *mut ContextFFI, key_ptr: *const u8, key_len: usize) -> i32 {
150	if ctx.is_null() || key_ptr.is_null() {
151		return FFI_ERROR_NULL_PTR;
152	}
153
154	unsafe {
155		let test_ctx = get_test_context(ctx);
156
157		// Convert raw bytes to EncodedKey
158		let key_bytes = from_raw_parts(key_ptr, key_len);
159		let key = EncodedKey(CowVec::new(key_bytes.to_vec()));
160
161		// Remove from TestContext
162		test_ctx.remove_state(&key);
163
164		FFI_OK
165	}
166}
167
168/// Clear all state in TestContext
169#[unsafe(no_mangle)]
170extern "C" fn test_state_clear(_operator_id: u64, ctx: *mut ContextFFI) -> i32 {
171	if ctx.is_null() {
172		return FFI_ERROR_NULL_PTR;
173	}
174
175	unsafe {
176		let test_ctx = get_test_context(ctx);
177		test_ctx.clear_state();
178		FFI_OK
179	}
180}
181
182/// Internal structure for state iterators
183#[repr(C)]
184struct TestStateIterator {
185	/// Collected key-value pairs (snapshot at creation time)
186	items: Vec<(Vec<u8>, Vec<u8>)>,
187	/// Current position in iteration
188	position: usize,
189}
190
191/// Create an iterator for state with a specific prefix
192#[unsafe(no_mangle)]
193extern "C" fn test_state_prefix(
194	_operator_id: u64,
195	ctx: *mut ContextFFI,
196	prefix_ptr: *const u8,
197	prefix_len: usize,
198	iterator_out: *mut *mut StateIteratorFFI,
199) -> i32 {
200	if ctx.is_null() || iterator_out.is_null() {
201		return FFI_ERROR_NULL_PTR;
202	}
203
204	unsafe {
205		let test_ctx = get_test_context(ctx);
206
207		// Get prefix bytes (can be empty for full scan)
208		let prefix_bytes = if prefix_ptr.is_null() || prefix_len == 0 {
209			vec![]
210		} else {
211			from_raw_parts(prefix_ptr, prefix_len).to_vec()
212		};
213
214		// Collect all matching key-value pairs from TestContext
215		let state_store = test_ctx.state_store();
216		let state = state_store.lock().unwrap();
217
218		let mut items: Vec<(Vec<u8>, Vec<u8>)> = state
219			.iter()
220			.filter(|(key, _)| {
221				if prefix_bytes.is_empty() {
222					true // Full scan
223				} else {
224					key.0.starts_with(&prefix_bytes) // Prefix match
225				}
226			})
227			.map(|(key, value)| (key.0.to_vec(), value.0.to_vec()))
228			.collect();
229
230		// Sort by key for deterministic iteration order
231		items.sort_by(|a, b| a.0.cmp(&b.0));
232
233		// Create iterator structure
234		let iter = Box::new(TestStateIterator {
235			items,
236			position: 0,
237		});
238
239		// Leak the box and cast to opaque pointer
240		*iterator_out = Box::into_raw(iter) as *mut StateIteratorFFI;
241
242		FFI_OK
243	}
244}
245
246/// Get the next key-value pair from a state iterator
247#[unsafe(no_mangle)]
248extern "C" fn test_state_iterator_next(
249	iterator: *mut StateIteratorFFI,
250	key_out: *mut BufferFFI,
251	value_out: *mut BufferFFI,
252) -> i32 {
253	if iterator.is_null() || key_out.is_null() || value_out.is_null() {
254		return FFI_ERROR_NULL_PTR;
255	}
256
257	unsafe {
258		// Cast opaque pointer back to TestStateIterator
259		let iter = &mut *(iterator as *mut TestStateIterator);
260
261		// Check if we have more items
262		if iter.position >= iter.items.len() {
263			return FFI_END_OF_ITERATION;
264		}
265
266		let (key, value) = &iter.items[iter.position];
267		iter.position += 1;
268
269		// Allocate and copy key
270		let key_ptr = test_alloc(key.len());
271		if key_ptr.is_null() {
272			return -2; // Allocation failed
273		}
274		ptr::copy_nonoverlapping(key.as_ptr(), key_ptr, key.len());
275		(*key_out).ptr = key_ptr;
276		(*key_out).len = key.len();
277		(*key_out).cap = key.len();
278
279		// Allocate and copy value
280		let value_ptr = test_alloc(value.len());
281		if value_ptr.is_null() {
282			// Free the key we just allocated
283			test_free(key_ptr, key.len());
284			return -2; // Allocation failed
285		}
286		ptr::copy_nonoverlapping(value.as_ptr(), value_ptr, value.len());
287		(*value_out).ptr = value_ptr;
288		(*value_out).len = value.len();
289		(*value_out).cap = value.len();
290
291		FFI_OK
292	}
293}
294
295/// Free a state iterator
296#[unsafe(no_mangle)]
297extern "C" fn test_state_iterator_free(iterator: *mut StateIteratorFFI) {
298	if iterator.is_null() {
299		return;
300	}
301
302	unsafe {
303		// Cast back to TestStateIterator and drop
304		let _ = Box::from_raw(iterator as *mut TestStateIterator);
305	}
306}
307
308/// Bound type constants for FFI
309const BOUND_UNBOUNDED: u8 = 0;
310const BOUND_INCLUDED: u8 = 1;
311const BOUND_EXCLUDED: u8 = 2;
312
313/// Create an iterator for state within a range
314#[unsafe(no_mangle)]
315extern "C" fn test_state_range(
316	_operator_id: u64,
317	ctx: *mut ContextFFI,
318	start_ptr: *const u8,
319	start_len: usize,
320	start_bound_type: u8,
321	end_ptr: *const u8,
322	end_len: usize,
323	end_bound_type: u8,
324	iterator_out: *mut *mut StateIteratorFFI,
325) -> i32 {
326	if ctx.is_null() || iterator_out.is_null() {
327		return FFI_ERROR_NULL_PTR;
328	}
329
330	unsafe {
331		let test_ctx = get_test_context(ctx);
332
333		// Parse start bound
334		let start_key = if start_bound_type == BOUND_UNBOUNDED || start_ptr.is_null() {
335			None
336		} else {
337			Some(from_raw_parts(start_ptr, start_len).to_vec())
338		};
339
340		// Parse end bound
341		let end_key = if end_bound_type == BOUND_UNBOUNDED || end_ptr.is_null() {
342			None
343		} else {
344			Some(from_raw_parts(end_ptr, end_len).to_vec())
345		};
346
347		// Collect all matching key-value pairs from TestContext
348		let state_store = test_ctx.state_store();
349		let state = state_store.lock().unwrap();
350
351		let mut items: Vec<(Vec<u8>, Vec<u8>)> = state
352			.iter()
353			.filter(|(key, _)| {
354				let key_bytes = key.0.as_slice();
355
356				// Check start bound
357				let start_ok = match (&start_key, start_bound_type) {
358					(None, _) => true,
359					(Some(start), BOUND_INCLUDED) => key_bytes >= start.as_slice(),
360					(Some(start), BOUND_EXCLUDED) => key_bytes > start.as_slice(),
361					_ => true,
362				};
363
364				// Check end bound
365				let end_ok = match (&end_key, end_bound_type) {
366					(None, _) => true,
367					(Some(end), BOUND_INCLUDED) => key_bytes <= end.as_slice(),
368					(Some(end), BOUND_EXCLUDED) => key_bytes < end.as_slice(),
369					_ => true,
370				};
371
372				start_ok && end_ok
373			})
374			.map(|(key, value)| (key.0.to_vec(), value.0.to_vec()))
375			.collect();
376
377		// Sort by key for deterministic iteration order
378		items.sort_by(|a, b| a.0.cmp(&b.0));
379
380		// Create iterator structure
381		let iter = Box::new(TestStateIterator {
382			items,
383			position: 0,
384		});
385
386		// Leak the box and cast to opaque pointer
387		*iterator_out = Box::into_raw(iter) as *mut StateIteratorFFI;
388
389		FFI_OK
390	}
391}
392
393/// Capture log message to TestContext
394#[unsafe(no_mangle)]
395unsafe extern "C" fn test_log_message(_operator_id: u64, _level: u32, _message: *const u8, _message_len: usize) {
396	unimplemented!()
397}
398
399/// Store get - returns not found (store not available in tests)
400extern "C" fn test_store_get(_ctx: *mut ContextFFI, _key: *const u8, _key_len: usize, _output: *mut BufferFFI) -> i32 {
401	unimplemented!()
402}
403
404/// Store contains_key - returns false (store not available in tests)
405extern "C" fn test_store_contains_key(
406	_ctx: *mut ContextFFI,
407	_key: *const u8,
408	_key_len: usize,
409	_result: *mut u8,
410) -> i32 {
411	unimplemented!()
412}
413
414/// Store prefix - returns empty iterator (store not available in tests)
415extern "C" fn test_store_prefix(
416	_ctx: *mut ContextFFI,
417	_prefix: *const u8,
418	_prefix_len: usize,
419	_iterator_out: *mut *mut StoreIteratorFFI,
420) -> i32 {
421	unimplemented!()
422}
423
424/// Store range - returns empty iterator (store not available in tests)
425extern "C" fn test_store_range(
426	_ctx: *mut ContextFFI,
427	_start: *const u8,
428	_start_len: usize,
429	_start_bound_type: u8,
430	_end: *const u8,
431	_end_len: usize,
432	_end_bound_type: u8,
433	_iterator_out: *mut *mut StoreIteratorFFI,
434) -> i32 {
435	unimplemented!()
436}
437
438/// Store iterator next - no-op (no iterators created)
439extern "C" fn test_store_iterator_next(
440	_iterator: *mut StoreIteratorFFI,
441	_key_out: *mut BufferFFI,
442	_value_out: *mut BufferFFI,
443) -> i32 {
444	unimplemented!()
445}
446
447/// Store iterator free - no-op
448extern "C" fn test_store_iterator_free(_iterator: *mut StoreIteratorFFI) {
449	unimplemented!()
450}
451
452use std::ptr;
453
454use reifydb_abi::{
455	callbacks::{
456		builder::BuilderCallbacks, catalog::CatalogCallbacks, host::HostCallbacks, log::LogCallbacks,
457		memory::MemoryCallbacks, rql::RqlCallbacks, state::StateCallbacks, store::StoreCallbacks,
458	},
459	catalog::{namespace::NamespaceFFI, table::TableFFI},
460	constants::{FFI_END_OF_ITERATION, FFI_ERROR_INTERNAL, FFI_ERROR_NULL_PTR, FFI_NOT_FOUND, FFI_OK},
461	context::{
462		context::ContextFFI,
463		iterators::{StateIteratorFFI, StoreIteratorFFI},
464	},
465	data::buffer::BufferFFI,
466};
467use reifydb_core::encoded::key::EncodedKey;
468
469use crate::testing::{
470	context::TestContext,
471	registry::{
472		test_acquire, test_bitvec_ptr, test_commit, test_data_ptr, test_emit_diff, test_grow, test_offsets_ptr,
473		test_release,
474	},
475};
476
477/// Find namespace by ID - stub implementation
478extern "C" fn test_catalog_find_namespace(
479	_ctx: *mut ContextFFI,
480	_namespace_id: u64,
481	_version: u64,
482	_output: *mut NamespaceFFI,
483) -> i32 {
484	1 // Not found
485}
486
487/// Find namespace by name - stub implementation
488extern "C" fn test_catalog_find_namespace_by_name(
489	_ctx: *mut ContextFFI,
490	_name_ptr: *const u8,
491	_name_len: usize,
492	_version: u64,
493	_output: *mut NamespaceFFI,
494) -> i32 {
495	1 // Not found
496}
497
498/// Find table by ID - stub implementation
499extern "C" fn test_catalog_find_table(
500	_ctx: *mut ContextFFI,
501	_table_id: u64,
502	_version: u64,
503	_output: *mut TableFFI,
504) -> i32 {
505	1 // Not found
506}
507
508/// Find table by name - stub implementation
509extern "C" fn test_catalog_find_table_by_name(
510	_ctx: *mut ContextFFI,
511	_namespace_id: u64,
512	_name_ptr: *const u8,
513	_name_len: usize,
514	_version: u64,
515	_output: *mut TableFFI,
516) -> i32 {
517	1 // Not found
518}
519
520/// Free namespace - stub implementation
521extern "C" fn test_catalog_free_namespace(_namespace: *mut NamespaceFFI) {
522	// No-op in test callbacks
523}
524
525/// Free table - stub implementation
526extern "C" fn test_catalog_free_table(_table: *mut TableFFI) {
527	// No-op in test callbacks
528}
529
530unsafe extern "C" fn test_rql(
531	_ctx: *mut ContextFFI,
532	_rql_ptr: *const u8,
533	_rql_len: usize,
534	_params_ptr: *const u8,
535	_params_len: usize,
536	_result_out: *mut BufferFFI,
537) -> i32 {
538	FFI_ERROR_INTERNAL
539}
540
541/// Create the complete host callbacks structure for testing
542pub fn create_test_callbacks() -> HostCallbacks {
543	HostCallbacks {
544		memory: MemoryCallbacks {
545			alloc: test_alloc,
546			free: test_free,
547			realloc: test_realloc,
548		},
549		state: StateCallbacks {
550			get: test_state_get,
551			set: test_state_set,
552			remove: test_state_remove,
553			clear: test_state_clear,
554			prefix: test_state_prefix,
555			range: test_state_range,
556			iterator_next: test_state_iterator_next,
557			iterator_free: test_state_iterator_free,
558		},
559		log: LogCallbacks {
560			message: test_log_message,
561		},
562		store: StoreCallbacks {
563			get: test_store_get,
564			contains_key: test_store_contains_key,
565			prefix: test_store_prefix,
566			range: test_store_range,
567			iterator_next: test_store_iterator_next,
568			iterator_free: test_store_iterator_free,
569		},
570		catalog: CatalogCallbacks {
571			find_namespace: test_catalog_find_namespace,
572			find_namespace_by_name: test_catalog_find_namespace_by_name,
573			find_table: test_catalog_find_table,
574			find_table_by_name: test_catalog_find_table_by_name,
575			free_namespace: test_catalog_free_namespace,
576			free_table: test_catalog_free_table,
577		},
578		rql: RqlCallbacks {
579			rql: test_rql,
580		},
581		builder: BuilderCallbacks {
582			acquire: test_acquire,
583			data_ptr: test_data_ptr,
584			offsets_ptr: test_offsets_ptr,
585			bitvec_ptr: test_bitvec_ptr,
586			grow: test_grow,
587			commit: test_commit,
588			release: test_release,
589			emit_diff: test_emit_diff,
590		},
591	}
592}