Skip to main content

alopex_core/sql/
subquery.rs

1//! Generic subquery execution primitives.
2//!
3//! These helpers intentionally model the current naive execution shape: callers
4//! provide the concrete subplan executor and this module supplies only the
5//! reusable control-flow skeletons.
6
7use std::collections::HashMap;
8use std::hash::Hash;
9
10/// Execute a nested subplan scan.
11///
12/// This is a thin wrapper around the provided executor closure.
13pub fn nested_scan<T, E>(execute_subplan: impl FnOnce() -> Result<Vec<T>, E>) -> Result<Vec<T>, E> {
14    execute_subplan()
15}
16
17/// Scan candidates until a match is found.
18///
19/// The predicate is fallible so SQL executors can preserve comparison errors
20/// while still short-circuiting on the first matching candidate.
21pub fn semi_join_probe<T, E>(
22    candidates: &[T],
23    mut is_match: impl FnMut(&T) -> Result<bool, E>,
24) -> Result<bool, E> {
25    for candidate in candidates {
26        if is_match(candidate)? {
27            return Ok(true);
28        }
29    }
30    Ok(false)
31}
32
33/// HashMap-backed materialization cache for subquery results.
34///
35/// Values are cloned out of the cache so callers can keep ownership semantics
36/// equivalent to freshly materialized subquery output.
37#[derive(Debug, Clone)]
38pub struct MaterializeCache<K, V> {
39    values: HashMap<K, V>,
40}
41
42impl<K, V> Default for MaterializeCache<K, V> {
43    fn default() -> Self {
44        Self {
45            values: HashMap::new(),
46        }
47    }
48}
49
50impl<K: Hash + Eq, V: Clone> MaterializeCache<K, V> {
51    /// Create an empty materialization cache.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Return the cached value for `key`, or compute and cache it.
57    pub fn get_or_try_insert_with<E>(
58        &mut self,
59        key: K,
60        materialize: impl FnOnce() -> Result<V, E>,
61    ) -> Result<V, E> {
62        if let Some(value) = self.values.get(&key) {
63            return Ok(value.clone());
64        }
65
66        let value = materialize()?;
67        self.values.insert(key, value.clone());
68        Ok(value)
69    }
70}
71
72/// Create an empty materialization cache.
73pub fn materialize_cache<K: Hash + Eq, V: Clone>() -> MaterializeCache<K, V> {
74    MaterializeCache::new()
75}
76
77#[cfg(test)]
78mod tests {
79    use super::{materialize_cache, nested_scan, semi_join_probe};
80
81    #[test]
82    fn nested_scan_delegates_to_executor() {
83        let rows = nested_scan(|| Ok::<_, ()>(vec![1, 2, 3])).unwrap();
84
85        assert_eq!(rows, vec![1, 2, 3]);
86    }
87
88    #[test]
89    fn semi_join_probe_short_circuits_on_match() {
90        let mut visited = Vec::new();
91        let matched = semi_join_probe(&[1, 2, 3], |value| {
92            visited.push(*value);
93            Ok::<_, ()>(*value == 2)
94        })
95        .unwrap();
96
97        assert!(matched);
98        assert_eq!(visited, vec![1, 2]);
99    }
100
101    #[test]
102    fn materialize_cache_runs_uncached_materializer_once() {
103        let mut cache = materialize_cache();
104        let mut calls = 0;
105
106        let first = cache
107            .get_or_try_insert_with("subquery", || {
108                calls += 1;
109                Ok::<_, ()>(vec![1, 2])
110            })
111            .unwrap();
112        let second = cache
113            .get_or_try_insert_with("subquery", || {
114                calls += 1;
115                Ok::<_, ()>(vec![3, 4])
116            })
117            .unwrap();
118
119        assert_eq!(first, vec![1, 2]);
120        assert_eq!(second, vec![1, 2]);
121        assert_eq!(calls, 1);
122    }
123}