Skip to main content

xlog_runtime/
relation.rs

1//! Relation management for GPU-based Datalog execution
2//!
3//! This module provides [`RelationStore`], a container for managing named relations
4//! stored as GPU buffers. It provides CRUD operations for relations during query
5//! execution.
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use xlog_core::{Schema, XlogError};
11use xlog_cuda::{CudaBuffer, CudaKernelProvider};
12
13/// Storage for named relations as GPU buffers
14///
15/// `RelationStore` manages a collection of named relations, each stored as a
16/// [`CudaBuffer`]. It provides CRUD operations for relation management during
17/// query execution.
18///
19/// # Thread Safety
20///
21/// This implementation is NOT thread-safe. It is designed for single-threaded
22/// runtime execution in the MVP.
23///
24/// # Example
25///
26/// ```ignore
27/// use xlog_runtime::RelationStore;
28/// use xlog_cuda::CudaBuffer;
29/// use xlog_core::Schema;
30///
31/// let mut store = RelationStore::new(provider);
32///
33/// // Add a relation
34/// store.put("edge", buffer);
35///
36/// // Check if relation exists
37/// if store.contains("edge") {
38///     let edge = store.get("edge").unwrap();
39/// }
40///
41/// // Remove a relation
42/// let removed = store.remove("edge");
43/// ```
44pub struct RelationStore {
45    /// CUDA kernel provider for GPU allocations
46    provider: Arc<CudaKernelProvider>,
47    /// Map of relation names to GPU buffers
48    relations: HashMap<String, VersionedCudaBuffer>,
49    /// Monotonic semantic-mutation epoch used by staged transactions.
50    ///
51    /// Per-name versions are not sufficient to detect clear-and-reinsert ABA:
52    /// a replacement can return to version one while owning different device
53    /// allocations. This epoch never changes for read-only access or reserve.
54    mutation_epoch: u64,
55}
56
57struct VersionedCudaBuffer {
58    buffer: CudaBuffer,
59    version: u64,
60}
61
62impl RelationStore {
63    /// Create a new empty relation store
64    pub fn new(provider: Arc<CudaKernelProvider>) -> Self {
65        Self {
66            provider,
67            relations: HashMap::new(),
68            mutation_epoch: 0,
69        }
70    }
71
72    fn advance_mutation_epoch(&mut self) {
73        self.mutation_epoch = self
74            .mutation_epoch
75            .checked_add(1)
76            .expect("relation-store mutation epoch exhausted");
77    }
78
79    /// Snapshot the semantic store revision for an optimistic transaction.
80    pub(crate) fn mutation_epoch(&self) -> u64 {
81        self.mutation_epoch
82    }
83
84    /// Get a reference to a relation by name
85    ///
86    /// # Arguments
87    /// * `name` - The name of the relation
88    ///
89    /// # Returns
90    /// `Some(&CudaBuffer)` if the relation exists, `None` otherwise
91    pub fn get(&self, name: &str) -> Option<&CudaBuffer> {
92        self.relations.get(name).map(|e| &e.buffer)
93    }
94
95    /// Get a mutable reference to a relation by name
96    ///
97    /// # Arguments
98    /// * `name` - The name of the relation
99    ///
100    /// # Returns
101    /// `Some(&mut CudaBuffer)` if the relation exists, `None` otherwise
102    pub fn get_mut(&mut self, name: &str) -> Option<&mut CudaBuffer> {
103        if self.relations.contains_key(name) {
104            self.advance_mutation_epoch();
105        }
106        self.relations.get_mut(name).map(|e| {
107            // Any mutable access may change the contents; bump the version so cached
108            // indexes can be invalidated conservatively.
109            e.version = e.version.saturating_add(1);
110            &mut e.buffer
111        })
112    }
113
114    /// Get a relation by name along with its current version.
115    pub fn get_with_version(&self, name: &str) -> Option<(&CudaBuffer, u64)> {
116        self.relations.get(name).map(|e| (&e.buffer, e.version))
117    }
118
119    /// Get the current version for a relation.
120    pub fn version(&self, name: &str) -> Option<u64> {
121        self.relations.get(name).map(|e| e.version)
122    }
123
124    /// Store a relation with the given name
125    ///
126    /// If a relation with the same name already exists, it will be replaced.
127    ///
128    /// # Arguments
129    /// * `name` - The name of the relation
130    /// * `buffer` - The GPU buffer containing the relation data
131    pub fn put(&mut self, name: &str, buffer: CudaBuffer) {
132        self.put_owned(name.to_string(), buffer);
133    }
134
135    /// Store a relation using an already-owned name.
136    ///
137    /// This has the same replacement and versioning behavior as [`Self::put`],
138    /// but avoids allocating a second copy of a name that the caller already
139    /// owns. Call [`Self::try_reserve_relations`] before a sequence of inserts
140    /// when that sequence must not grow the relation map.
141    pub fn put_owned(&mut self, name: String, buffer: CudaBuffer) {
142        self.advance_mutation_epoch();
143        let version = self
144            .relations
145            .get(name.as_str())
146            .map(|e| e.version.saturating_add(1))
147            .unwrap_or(1);
148        self.relations
149            .insert(name, VersionedCudaBuffer { buffer, version });
150    }
151
152    /// Reserve map capacity for `additional` new relation names.
153    ///
154    /// This operation is fallible and changes capacity only: relation contents
155    /// and versions remain unchanged. Callers should count only names that are
156    /// not already present before choosing `additional`.
157    pub fn try_reserve_relations(&mut self, additional: usize) -> xlog_core::Result<()> {
158        self.relations.try_reserve(additional).map_err(|error| {
159            XlogError::Execution(format!(
160                "Failed to reserve capacity for {additional} relation entries: {error}"
161            ))
162        })
163    }
164
165    /// Get a relation by name, or insert an empty buffer with the given schema
166    ///
167    /// This is useful for semi-naive evaluation where delta relations may not
168    /// exist yet on the first iteration. If the relation doesn't exist, an empty
169    /// buffer with the given schema is inserted into the store.
170    ///
171    /// # Arguments
172    /// * `name` - The name of the relation
173    /// * `schema` - The schema to use if creating an empty buffer
174    ///
175    /// # Returns
176    /// A reference to the existing buffer, or the newly inserted empty buffer
177    pub fn get_or_insert_empty(
178        &mut self,
179        name: &str,
180        schema: &Schema,
181    ) -> xlog_core::Result<&CudaBuffer> {
182        if !self.relations.contains_key(name) {
183            let buffer = self.provider.create_empty_buffer(schema.clone())?;
184            self.relations
185                .insert(name.to_string(), VersionedCudaBuffer { buffer, version: 1 });
186            self.advance_mutation_epoch();
187        }
188        Ok(&self
189            .relations
190            .get(name)
191            .expect("Relation must exist after insertion")
192            .buffer)
193    }
194
195    /// Get a mutable reference to a relation, or insert an empty buffer with the given schema
196    ///
197    /// This is useful for semi-naive evaluation where delta relations may not
198    /// exist yet on the first iteration. If the relation doesn't exist, an empty
199    /// buffer with the given schema is inserted into the store.
200    ///
201    /// # Arguments
202    /// * `name` - The name of the relation
203    /// * `schema` - The schema to use if creating an empty buffer
204    ///
205    /// # Returns
206    /// A mutable reference to the existing buffer, or the newly inserted empty buffer
207    pub fn get_or_insert_empty_mut(
208        &mut self,
209        name: &str,
210        schema: &Schema,
211    ) -> xlog_core::Result<&mut CudaBuffer> {
212        if !self.relations.contains_key(name) {
213            let buffer = self.provider.create_empty_buffer(schema.clone())?;
214            self.relations
215                .insert(name.to_string(), VersionedCudaBuffer { buffer, version: 1 });
216        }
217        self.advance_mutation_epoch();
218        let entry = self
219            .relations
220            .get_mut(name)
221            .expect("Relation must exist after insertion");
222        entry.version = entry.version.saturating_add(1);
223        Ok(&mut entry.buffer)
224    }
225
226    /// Check if a relation exists in the store
227    ///
228    /// # Arguments
229    /// * `name` - The name of the relation
230    ///
231    /// # Returns
232    /// `true` if the relation exists, `false` otherwise
233    pub fn contains(&self, name: &str) -> bool {
234        self.relations.contains_key(name)
235    }
236
237    /// Remove a relation from the store
238    ///
239    /// # Arguments
240    /// * `name` - The name of the relation
241    ///
242    /// # Returns
243    /// `Some(CudaBuffer)` if the relation existed, `None` otherwise
244    pub fn remove(&mut self, name: &str) -> Option<CudaBuffer> {
245        let removed = self.relations.remove(name).map(|e| e.buffer);
246        if removed.is_some() {
247            self.advance_mutation_epoch();
248        }
249        removed
250    }
251
252    /// Clear all relations from the store
253    ///
254    /// This removes all stored relations. The GPU memory will be freed
255    /// when the CudaBuffer instances are dropped.
256    pub fn clear(&mut self) {
257        if !self.relations.is_empty() {
258            self.relations.clear();
259            self.advance_mutation_epoch();
260        }
261    }
262
263    /// Get the number of relations in the store
264    pub fn len(&self) -> usize {
265        self.relations.len()
266    }
267
268    /// Check if the store is empty
269    pub fn is_empty(&self) -> bool {
270        self.relations.is_empty()
271    }
272
273    /// Get an iterator over relation names
274    pub fn names(&self) -> impl Iterator<Item = &str> {
275        self.relations.keys().map(|s| s.as_str())
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use std::sync::Arc;
283    use xlog_core::{MemoryBudget, ScalarType};
284    use xlog_cuda::{CudaKernelProvider, CudaProviderBuilder};
285
286    fn setup_provider() -> Option<Arc<CudaKernelProvider>> {
287        CudaProviderBuilder::new(0, MemoryBudget::with_limit(1024 * 1024 * 1024))
288            .build()
289            .ok()
290            .map(Arc::new)
291    }
292
293    fn setup_store() -> Option<(RelationStore, Arc<CudaKernelProvider>)> {
294        let provider = setup_provider()?;
295        let store = RelationStore::new(provider.clone());
296        Some((store, provider))
297    }
298
299    fn test_schema() -> Schema {
300        Schema::new(vec![
301            ("a".to_string(), ScalarType::U32),
302            ("b".to_string(), ScalarType::U64),
303        ])
304    }
305
306    fn device_row_count(provider: &CudaKernelProvider, buffer: &CudaBuffer) -> u32 {
307        let mut host_rows = [0u32];
308        provider
309            .device()
310            .inner()
311            .dtoh_sync_copy_into(buffer.num_rows_device(), &mut host_rows)
312            .expect("dtoh row count");
313        host_rows[0]
314    }
315
316    fn make_buffer(provider: &CudaKernelProvider, schema: Schema, rows: usize) -> CudaBuffer {
317        if schema.arity() == 0 {
318            if rows == 0 {
319                return provider.create_empty_buffer(schema).expect("empty buffer");
320            }
321            let rows_u32 = u32::try_from(rows).expect("row count fits u32");
322            let mut d_num_rows = provider.memory().alloc::<u32>(1).expect("alloc");
323            provider
324                .device()
325                .inner()
326                .htod_sync_copy_into(&[rows_u32], &mut d_num_rows)
327                .expect("htod row count");
328            return CudaBuffer::from_columns(Vec::new(), rows as u64, d_num_rows, schema);
329        }
330        if rows == 0 {
331            return provider.create_empty_buffer(schema).expect("empty buffer");
332        }
333        let mut columns: Vec<Vec<u8>> = Vec::with_capacity(schema.arity());
334        for col_idx in 0..schema.arity() {
335            let size = schema
336                .column_type(col_idx)
337                .map(|t| t.size_bytes())
338                .unwrap_or(4);
339            columns.push(vec![0u8; rows * size]);
340        }
341        let slices: Vec<&[u8]> = columns.iter().map(|c| c.as_slice()).collect();
342        provider
343            .create_buffer_from_slices(&slices, schema)
344            .expect("buffer")
345    }
346
347    #[test]
348    fn mutation_epoch_detects_clear_reinsert_aba_without_counting_reserve() {
349        let Some((mut store, provider)) = setup_store() else {
350            return;
351        };
352        let schema = test_schema();
353        assert_eq!(store.mutation_epoch(), 0);
354        store
355            .try_reserve_relations(4)
356            .expect("reserve must not be a semantic mutation");
357        assert_eq!(store.mutation_epoch(), 0);
358
359        store.put("rows", make_buffer(&provider, schema.clone(), 1));
360        let after_first_put = store.mutation_epoch();
361        assert_eq!(after_first_put, 1);
362        assert_eq!(store.version("rows"), Some(1));
363
364        store.clear();
365        let after_clear = store.mutation_epoch();
366        assert!(after_clear > after_first_put);
367        store.put("rows", make_buffer(&provider, schema, 1));
368        assert_eq!(store.version("rows"), Some(1));
369        assert!(
370            store.mutation_epoch() > after_clear,
371            "clear-and-reinsert must not recreate the original transaction revision"
372        );
373    }
374
375    #[test]
376    fn test_new_store_is_empty() {
377        let Some((store, _provider)) = setup_store() else {
378            return;
379        };
380        assert!(store.is_empty());
381        assert_eq!(store.len(), 0);
382    }
383
384    #[test]
385    fn test_put_and_get() {
386        let Some((mut store, provider)) = setup_store() else {
387            return;
388        };
389        let buffer = provider
390            .create_empty_buffer(Schema::new(vec![]))
391            .expect("empty");
392
393        store.put("test_rel", buffer);
394
395        assert!(store.contains("test_rel"));
396        assert!(!store.is_empty());
397        assert_eq!(store.len(), 1);
398
399        let retrieved = store.get("test_rel");
400        assert!(retrieved.is_some());
401    }
402
403    #[test]
404    fn test_get_nonexistent() {
405        let Some((store, _provider)) = setup_store() else {
406            return;
407        };
408        assert!(store.get("nonexistent").is_none());
409    }
410
411    #[test]
412    fn test_contains() {
413        let Some((mut store, provider)) = setup_store() else {
414            return;
415        };
416
417        assert!(!store.contains("test"));
418
419        store.put(
420            "test",
421            provider
422                .create_empty_buffer(Schema::new(vec![]))
423                .expect("empty"),
424        );
425
426        assert!(store.contains("test"));
427        assert!(!store.contains("other"));
428    }
429
430    #[test]
431    fn test_remove() {
432        let Some((mut store, provider)) = setup_store() else {
433            return;
434        };
435        store.put(
436            "test",
437            provider
438                .create_empty_buffer(Schema::new(vec![]))
439                .expect("empty"),
440        );
441
442        assert!(store.contains("test"));
443
444        let removed = store.remove("test");
445        assert!(removed.is_some());
446        assert!(!store.contains("test"));
447        assert!(store.is_empty());
448    }
449
450    #[test]
451    fn test_remove_nonexistent() {
452        let Some((mut store, _provider)) = setup_store() else {
453            return;
454        };
455        let removed = store.remove("nonexistent");
456        assert!(removed.is_none());
457    }
458
459    #[test]
460    fn test_clear() {
461        let Some((mut store, provider)) = setup_store() else {
462            return;
463        };
464        let empty = provider
465            .create_empty_buffer(Schema::new(vec![]))
466            .expect("empty");
467        store.put("rel1", empty);
468        store.put(
469            "rel2",
470            provider
471                .create_empty_buffer(Schema::new(vec![]))
472                .expect("empty"),
473        );
474        store.put(
475            "rel3",
476            provider
477                .create_empty_buffer(Schema::new(vec![]))
478                .expect("empty"),
479        );
480
481        assert_eq!(store.len(), 3);
482
483        store.clear();
484
485        assert!(store.is_empty());
486        assert_eq!(store.len(), 0);
487    }
488
489    #[test]
490    fn test_get_or_insert_empty_existing() {
491        let Some((mut store, provider)) = setup_store() else {
492            return;
493        };
494        let schema = test_schema();
495
496        let buffer = make_buffer(&provider, schema.clone(), 100);
497        store.put("existing", buffer);
498
499        let result = store.get_or_insert_empty("existing", &schema).unwrap();
500        assert_eq!(device_row_count(&provider, result), 100);
501        assert_eq!(result.schema(), &schema);
502        assert_eq!(store.len(), 1);
503    }
504
505    #[test]
506    fn test_get_or_insert_empty_nonexistent() {
507        let Some((mut store, provider)) = setup_store() else {
508            return;
509        };
510        let schema = test_schema();
511
512        assert!(store.is_empty());
513
514        let result = store.get_or_insert_empty("nonexistent", &schema).unwrap();
515        assert_eq!(device_row_count(&provider, result), 0);
516        assert_eq!(result.schema(), &schema);
517        assert!(result.is_empty());
518
519        assert!(store.contains("nonexistent"));
520        assert_eq!(store.len(), 1);
521    }
522
523    #[test]
524    fn test_get_mut() {
525        let Some((mut store, provider)) = setup_store() else {
526            return;
527        };
528        let buffer = make_buffer(&provider, Schema::new(vec![]), 10);
529        store.put("test", buffer);
530
531        {
532            let buf_mut = store.get_mut("test").unwrap();
533            buf_mut.set_row_capacity(50);
534            provider
535                .device()
536                .inner()
537                .htod_sync_copy_into(&[50u32], buf_mut.num_rows_device_mut())
538                .expect("htod row count");
539        }
540
541        assert_eq!(device_row_count(&provider, store.get("test").unwrap()), 50);
542    }
543
544    #[test]
545    fn test_get_mut_nonexistent() {
546        let Some((mut store, _provider)) = setup_store() else {
547            return;
548        };
549        assert!(store.get_mut("nonexistent").is_none());
550    }
551
552    #[test]
553    fn test_get_or_insert_empty_mut() {
554        let Some((mut store, provider)) = setup_store() else {
555            return;
556        };
557        let schema = test_schema();
558
559        {
560            let buf_mut = store.get_or_insert_empty_mut("new_rel", &schema).unwrap();
561            assert_eq!(device_row_count(&provider, buf_mut), 0);
562            buf_mut.set_row_capacity(42);
563            provider
564                .device()
565                .inner()
566                .htod_sync_copy_into(&[42u32], buf_mut.num_rows_device_mut())
567                .expect("htod row count");
568        }
569
570        assert!(store.contains("new_rel"));
571        assert_eq!(
572            device_row_count(&provider, store.get("new_rel").unwrap()),
573            42
574        );
575    }
576
577    #[test]
578    fn test_put_replaces_existing() {
579        let Some((mut store, provider)) = setup_store() else {
580            return;
581        };
582
583        let buffer1 = make_buffer(&provider, Schema::new(vec![]), 10);
584        let buffer2 = make_buffer(&provider, Schema::new(vec![]), 20);
585
586        store.put("test", buffer1);
587        assert_eq!(device_row_count(&provider, store.get("test").unwrap()), 10);
588
589        store.put("test", buffer2);
590        assert_eq!(device_row_count(&provider, store.get("test").unwrap()), 20);
591        assert_eq!(store.len(), 1);
592    }
593
594    #[test]
595    fn reserved_owned_put_reuses_capacity_and_versions_replacements() {
596        let Some((mut store, provider)) = setup_store() else {
597            return;
598        };
599        let capacity_before = store.relations.capacity();
600
601        store
602            .try_reserve_relations(1)
603            .expect("reserve one relation entry");
604        let reserved_capacity = store.relations.capacity();
605        assert!(reserved_capacity > capacity_before);
606
607        store.put_owned(
608            "owned_relation".to_string(),
609            make_buffer(&provider, Schema::new(vec![]), 10),
610        );
611        assert_eq!(store.relations.capacity(), reserved_capacity);
612        assert_eq!(store.version("owned_relation"), Some(1));
613
614        store.put_owned(
615            "owned_relation".to_string(),
616            make_buffer(&provider, Schema::new(vec![]), 20),
617        );
618        assert_eq!(store.relations.capacity(), reserved_capacity);
619        assert_eq!(store.version("owned_relation"), Some(2));
620        assert_eq!(
621            device_row_count(&provider, store.get("owned_relation").unwrap()),
622            20
623        );
624    }
625
626    #[test]
627    fn test_names_iterator() {
628        let Some((mut store, provider)) = setup_store() else {
629            return;
630        };
631        store.put(
632            "alpha",
633            provider
634                .create_empty_buffer(Schema::new(vec![]))
635                .expect("empty"),
636        );
637        store.put(
638            "beta",
639            provider
640                .create_empty_buffer(Schema::new(vec![]))
641                .expect("empty"),
642        );
643        store.put(
644            "gamma",
645            provider
646                .create_empty_buffer(Schema::new(vec![]))
647                .expect("empty"),
648        );
649
650        let mut names: Vec<&str> = store.names().collect();
651        names.sort();
652
653        assert_eq!(names, vec!["alpha", "beta", "gamma"]);
654    }
655
656    #[test]
657    fn test_multiple_operations() {
658        let Some((mut store, provider)) = setup_store() else {
659            return;
660        };
661
662        let empty = provider
663            .create_empty_buffer(Schema::new(vec![]))
664            .expect("empty");
665        store.put("a", empty);
666        store.put(
667            "b",
668            provider
669                .create_empty_buffer(Schema::new(vec![]))
670                .expect("empty"),
671        );
672        store.put(
673            "c",
674            provider
675                .create_empty_buffer(Schema::new(vec![]))
676                .expect("empty"),
677        );
678        assert_eq!(store.len(), 3);
679
680        store.remove("b");
681        assert_eq!(store.len(), 2);
682        assert!(!store.contains("b"));
683
684        store.put("a", make_buffer(&provider, Schema::new(vec![]), 50));
685        assert_eq!(store.len(), 2);
686        assert_eq!(device_row_count(&provider, store.get("a").unwrap()), 50);
687
688        store.clear();
689        assert!(store.is_empty());
690    }
691}