miden_note_transport_node/database/
mod.rs

1mod error;
2mod maintenance;
3mod sqlite;
4
5pub use self::error::DatabaseError;
6pub use self::maintenance::DatabaseMaintenance;
7use self::sqlite::SqliteDatabase;
8use crate::metrics::MetricsDatabase;
9use crate::types::{NoteId, NoteTag, StoredNote};
10
11/// Database operations
12#[async_trait::async_trait]
13pub trait DatabaseBackend: Send + Sync {
14    /// Connect to the database
15    async fn connect(
16        config: DatabaseConfig,
17        metrics: MetricsDatabase,
18    ) -> Result<Self, DatabaseError>
19    where
20        Self: Sized;
21
22    /// Store a new note
23    async fn store_note(&self, note: &StoredNote) -> Result<(), DatabaseError>;
24
25    /// Fetch notes by tag
26    async fn fetch_notes(
27        &self,
28        tag: NoteTag,
29        cursor: u64,
30    ) -> Result<Vec<StoredNote>, DatabaseError>;
31
32    /// Get statistics about the database
33    async fn get_stats(&self) -> Result<(u64, u64), DatabaseError>;
34
35    /// Clean up old notes based on retention policy
36    async fn cleanup_old_notes(&self, retention_days: u32) -> Result<u64, DatabaseError>;
37
38    /// Check if a note exists
39    async fn note_exists(&self, note_id: NoteId) -> Result<bool, DatabaseError>;
40}
41
42/// Database manager for the transport layer
43pub struct Database {
44    backend: Box<dyn DatabaseBackend>,
45}
46
47/// [`Database`] configuration
48#[derive(Debug, Clone)]
49pub struct DatabaseConfig {
50    /// Database URL
51    pub url: String,
52    /// Retention period in days
53    pub retention_days: u32,
54}
55
56impl Default for DatabaseConfig {
57    fn default() -> Self {
58        Self {
59            url: ":memory:".to_string(),
60            retention_days: 30,
61        }
62    }
63}
64
65impl Database {
66    /// Connect to a database (with `SQLite` backend)
67    pub async fn connect(
68        config: DatabaseConfig,
69        metrics: MetricsDatabase,
70    ) -> Result<Self, DatabaseError> {
71        let backend = SqliteDatabase::connect(config, metrics).await?;
72        Ok(Self { backend: Box::new(backend) })
73    }
74
75    /// Store a new note
76    pub async fn store_note(&self, note: &StoredNote) -> Result<(), DatabaseError> {
77        self.backend.store_note(note).await?;
78        Ok(())
79    }
80
81    /// Fetch notes by tag with cursor-based pagination
82    pub async fn fetch_notes(
83        &self,
84        tag: NoteTag,
85        cursor: u64,
86    ) -> Result<Vec<StoredNote>, DatabaseError> {
87        self.backend.fetch_notes(tag, cursor).await
88    }
89
90    /// Get statistics about the database
91    pub async fn get_stats(&self) -> Result<(u64, u64), DatabaseError> {
92        self.backend.get_stats().await
93    }
94
95    /// Clean up old notes based on retention policy
96    pub async fn cleanup_old_notes(&self, retention_days: u32) -> Result<u64, DatabaseError> {
97        self.backend.cleanup_old_notes(retention_days).await
98    }
99
100    /// Check if a note exists
101    pub async fn note_exists(&self, note_id: NoteId) -> Result<bool, DatabaseError> {
102        self.backend.note_exists(note_id).await
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use chrono::Utc;
109
110    use super::*;
111    use crate::metrics::Metrics;
112    use crate::test_utils::{TAG_LOCAL_ANY, test_note_header};
113
114    #[tokio::test]
115    async fn test_sqlite_database() {
116        let db = Database::connect(DatabaseConfig::default(), Metrics::default().db)
117            .await
118            .unwrap();
119        let start = Utc::now();
120
121        let note = StoredNote {
122            header: test_note_header(),
123            details: vec![1, 2, 3, 4],
124            created_at: Utc::now(),
125        };
126
127        db.store_note(&note).await.unwrap();
128
129        let fetched_notes = db
130            .fetch_notes(TAG_LOCAL_ANY.into(), start.timestamp_micros().try_into().unwrap())
131            .await
132            .unwrap();
133        assert_eq!(fetched_notes.len(), 1);
134        assert_eq!(fetched_notes[0].header.id(), note.header.id());
135
136        // Test note exists
137        assert!(db.note_exists(note.header.id()).await.unwrap());
138
139        // Test stats
140        let (total_notes, total_tags) = db.get_stats().await.unwrap();
141        assert_eq!(total_notes, 1);
142        assert_eq!(total_tags, 1);
143    }
144
145    #[tokio::test]
146    async fn test_fetch_notes_timestamp_filtering() {
147        let db = Database::connect(DatabaseConfig::default(), Metrics::default().db)
148            .await
149            .unwrap();
150
151        // Create a note with a specific received_at time
152        let received_time = Utc::now();
153        let note = StoredNote {
154            header: test_note_header(),
155            details: vec![1, 2, 3, 4],
156            created_at: received_time,
157        };
158
159        db.store_note(&note).await.unwrap();
160
161        // Fetch notes with cursor before the note was received - should return the note
162        let before_cursor = (received_time - chrono::Duration::seconds(1))
163            .timestamp_micros()
164            .try_into()
165            .unwrap();
166        let fetched_notes = db.fetch_notes(TAG_LOCAL_ANY.into(), before_cursor).await.unwrap();
167        assert_eq!(fetched_notes.len(), 1);
168        assert_eq!(fetched_notes[0].header.id(), note.header.id());
169
170        // Fetch notes with cursor after the note was received - should return empty
171        let after_cursor = (received_time + chrono::Duration::seconds(1))
172            .timestamp_micros()
173            .try_into()
174            .unwrap();
175        let fetched_notes = db.fetch_notes(TAG_LOCAL_ANY.into(), after_cursor).await.unwrap();
176        assert_eq!(fetched_notes.len(), 0);
177    }
178}