Skip to main content

fraiseql_functions/store/memory/
mod.rs

1//! In-memory function store for unit tests and local development.
2
3use std::{
4    collections::HashMap,
5    sync::{Arc, Mutex},
6};
7
8use async_trait::async_trait;
9use fraiseql_error::{FraiseQLError, Result};
10
11use super::{FunctionRecord, FunctionStatus, FunctionStore};
12use crate::types::RuntimeType;
13
14/// In-memory function store backed by a `HashMap` behind a `Mutex`.
15///
16/// Thread-safe via an `Arc<Mutex<...>>` interior; suitable for unit tests
17/// and local development scenarios that do not require persistence.
18#[derive(Debug, Clone)]
19pub struct InMemoryFunctionStore {
20    inner: Arc<Mutex<StoreInner>>,
21}
22
23#[derive(Debug, Default)]
24struct StoreInner {
25    /// Latest record per function name.
26    records:      HashMap<String, FunctionRecord>,
27    /// Next pk to assign.
28    next_pk:      i64,
29    /// Next version per function name.
30    next_version: HashMap<String, i32>,
31}
32
33impl Default for InMemoryFunctionStore {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl InMemoryFunctionStore {
40    /// Create a new empty in-memory store.
41    #[must_use]
42    pub fn new() -> Self {
43        Self {
44            inner: Arc::new(Mutex::new(StoreInner {
45                records:      HashMap::new(),
46                next_pk:      1,
47                next_version: HashMap::new(),
48            })),
49        }
50    }
51}
52
53#[async_trait]
54impl FunctionStore for InMemoryFunctionStore {
55    async fn store_function(
56        &self,
57        name: &str,
58        runtime: RuntimeType,
59        bytecode: bytes::Bytes,
60    ) -> Result<FunctionRecord> {
61        let mut guard = self.inner.lock().map_err(|_| FraiseQLError::Validation {
62            message: "function store mutex poisoned".to_string(),
63            path:    None,
64        })?;
65
66        let pk = guard.next_pk;
67        guard.next_pk += 1;
68
69        let version = guard.next_version.entry(name.to_string()).or_insert(0);
70        *version += 1;
71        let ver = *version;
72
73        let record = FunctionRecord {
74            pk_function: pk,
75            name: name.to_string(),
76            runtime,
77            bytecode,
78            version: ver,
79            deployed_at: chrono::Utc::now(),
80            status: FunctionStatus::Active,
81        };
82
83        // Deactivate the previous record for this name (keep only the latest)
84        guard.records.insert(name.to_string(), record.clone());
85        Ok(record)
86    }
87
88    async fn get_function(&self, name: &str) -> Result<Option<FunctionRecord>> {
89        let guard = self.inner.lock().map_err(|_| FraiseQLError::Validation {
90            message: "function store mutex poisoned".to_string(),
91            path:    None,
92        })?;
93
94        let record =
95            guard.records.get(name).filter(|r| r.status == FunctionStatus::Active).cloned();
96
97        Ok(record)
98    }
99
100    async fn list_functions(&self) -> Result<Vec<FunctionRecord>> {
101        let guard = self.inner.lock().map_err(|_| FraiseQLError::Validation {
102            message: "function store mutex poisoned".to_string(),
103            path:    None,
104        })?;
105
106        let mut records: Vec<FunctionRecord> = guard
107            .records
108            .values()
109            .filter(|r| r.status == FunctionStatus::Active)
110            .cloned()
111            .collect();
112
113        // Stable ordering by name for deterministic test assertions
114        records.sort_by(|a, b| a.name.cmp(&b.name));
115        Ok(records)
116    }
117
118    async fn delete_function(&self, name: &str) -> Result<bool> {
119        let mut guard = self.inner.lock().map_err(|_| FraiseQLError::Validation {
120            message: "function store mutex poisoned".to_string(),
121            path:    None,
122        })?;
123
124        let found = match guard.records.get_mut(name).filter(|r| r.status == FunctionStatus::Active)
125        {
126            Some(r) => {
127                r.status = FunctionStatus::Inactive;
128                true
129            },
130            None => false,
131        };
132
133        Ok(found)
134    }
135}
136
137#[cfg(test)]
138mod tests;