miden_note_transport_node/database/
mod.rs1mod 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#[async_trait::async_trait]
13pub trait DatabaseBackend: Send + Sync {
14 async fn connect(
16 config: DatabaseConfig,
17 metrics: MetricsDatabase,
18 ) -> Result<Self, DatabaseError>
19 where
20 Self: Sized;
21
22 async fn store_note(&self, note: &StoredNote) -> Result<(), DatabaseError>;
24
25 async fn fetch_notes(
27 &self,
28 tag: NoteTag,
29 cursor: u64,
30 ) -> Result<Vec<StoredNote>, DatabaseError>;
31
32 async fn get_stats(&self) -> Result<(u64, u64), DatabaseError>;
34
35 async fn cleanup_old_notes(&self, retention_days: u32) -> Result<u64, DatabaseError>;
37
38 async fn note_exists(&self, note_id: NoteId) -> Result<bool, DatabaseError>;
40}
41
42pub struct Database {
44 backend: Box<dyn DatabaseBackend>,
45}
46
47#[derive(Debug, Clone)]
49pub struct DatabaseConfig {
50 pub url: String,
52 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 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 pub async fn store_note(&self, note: &StoredNote) -> Result<(), DatabaseError> {
77 self.backend.store_note(note).await?;
78 Ok(())
79 }
80
81 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 pub async fn get_stats(&self) -> Result<(u64, u64), DatabaseError> {
92 self.backend.get_stats().await
93 }
94
95 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 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(¬e).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 assert!(db.note_exists(note.header.id()).await.unwrap());
138
139 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 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(¬e).await.unwrap();
160
161 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 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}