Skip to main content

heliosdb_nano/session/
types.rs

1//! Session types and definitions
2//!
3//! This module contains the core types for multi-user session management:
4//!
5//! - [`SessionId`] - Unique identifier for database sessions
6//! - [`Session`] - Active session state with isolation level and statistics
7//! - [`User`] - User credentials with Argon2 password hashing
8//! - [`IsolationLevel`] - Transaction isolation levels (ReadCommitted, RepeatableRead, Serializable)
9//!
10//! # Example
11//!
12//! ```rust,no_run
13//! use heliosdb_nano::session::{User, Session, IsolationLevel};
14//!
15//! // Create a user with password
16//! let user = User::new("alice", "secure_password");
17//!
18//! // Verify password
19//! assert!(user.verify_password("secure_password"));
20//!
21//! // Create a session with snapshot isolation
22//! let session = Session::new(user.id, IsolationLevel::RepeatableRead);
23//! ```
24
25use std::sync::atomic::{AtomicU64, Ordering};
26use argon2::{
27    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
28    Argon2,
29};
30
31/// Unique identifier for a database session
32///
33/// Each session gets a unique ID that is used to track the session's
34/// state, transactions, and resource usage. Session IDs are monotonically
35/// increasing and never reused within a database instance.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub struct SessionId(pub u64);
38
39impl SessionId {
40    /// Generate a new unique session ID
41    pub fn new() -> Self {
42        static COUNTER: AtomicU64 = AtomicU64::new(1);
43        Self(COUNTER.fetch_add(1, Ordering::SeqCst))
44    }
45}
46
47impl Default for SessionId {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53/// User identifier
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub struct UserId(pub u64);
56
57/// User credentials
58#[derive(Debug, Clone)]
59pub struct User {
60    pub id: UserId,
61    pub name: String,
62    pub password_hash: Option<String>,
63}
64
65impl User {
66    /// Create a new user with Argon2-hashed password
67    pub fn new(name: impl Into<String>, password: impl Into<String>) -> Self {
68        static COUNTER: AtomicU64 = AtomicU64::new(1);
69        let password_str = password.into();
70
71        // Hash password with Argon2id (recommended variant)
72        let password_hash = if password_str.is_empty() {
73            None
74        } else {
75            let salt = SaltString::generate(&mut OsRng);
76            let argon2 = Argon2::default();
77            argon2
78                .hash_password(password_str.as_bytes(), &salt)
79                .ok()
80                .map(|hash| hash.to_string())
81        };
82
83        Self {
84            id: UserId(COUNTER.fetch_add(1, Ordering::SeqCst)),
85            name: name.into(),
86            password_hash,
87        }
88    }
89
90    /// Create a user without a password (for internal/system use)
91    pub fn new_passwordless(name: impl Into<String>) -> Self {
92        static COUNTER: AtomicU64 = AtomicU64::new(1);
93        Self {
94            id: UserId(COUNTER.fetch_add(1, Ordering::SeqCst)),
95            name: name.into(),
96            password_hash: None,
97        }
98    }
99
100    /// Verify a password against the stored hash
101    /// Returns false if no password hash is set
102    pub fn verify_password(&self, password: &str) -> bool {
103        match &self.password_hash {
104            Some(hash_str) => {
105                if let Ok(parsed_hash) = PasswordHash::new(hash_str) {
106                    Argon2::default()
107                        .verify_password(password.as_bytes(), &parsed_hash)
108                        .is_ok()
109                } else {
110                    false
111                }
112            }
113            None => false,
114        }
115    }
116
117    /// Check if the user has a password set
118    pub fn has_password(&self) -> bool {
119        self.password_hash.is_some()
120    }
121
122    /// Update the user's password
123    pub fn set_password(&mut self, password: &str) {
124        if password.is_empty() {
125            self.password_hash = None;
126        } else {
127            let salt = SaltString::generate(&mut OsRng);
128            let argon2 = Argon2::default();
129            self.password_hash = argon2
130                .hash_password(password.as_bytes(), &salt)
131                .ok()
132                .map(|hash| hash.to_string());
133        }
134    }
135}
136
137/// Isolation level for transactions
138///
139/// Controls how concurrent transactions interact with each other.
140/// Higher isolation levels provide stronger consistency guarantees
141/// at the cost of reduced concurrency.
142///
143/// # Isolation Level Comparison
144///
145/// | Level | Dirty Reads | Non-Repeatable Reads | Phantom Reads |
146/// |-------|-------------|---------------------|---------------|
147/// | ReadCommitted | No | Yes | Yes |
148/// | RepeatableRead | No | No | Possible |
149/// | Serializable | No | No | No |
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum IsolationLevel {
152    /// Read Committed isolation
153    ///
154    /// Each statement sees a fresh snapshot of committed data.
155    /// Two identical queries in the same transaction may return
156    /// different results if another transaction commits between them.
157    ReadCommitted,
158    /// Repeatable Read isolation (also known as Snapshot Isolation)
159    ///
160    /// The transaction sees a consistent snapshot taken at the start.
161    /// All statements in the transaction see the same data, regardless
162    /// of concurrent commits.
163    RepeatableRead,
164    /// Serializable isolation
165    ///
166    /// Provides full serializability with conflict detection.
167    /// Transactions appear to execute one at a time, even when running
168    /// concurrently. May abort transactions to prevent anomalies.
169    Serializable,
170}
171
172impl IsolationLevel {
173    /// Alias for `RepeatableRead` - commonly called Snapshot Isolation
174    #[allow(non_upper_case_globals)]
175    pub const Snapshot: Self = Self::RepeatableRead;
176}
177
178impl Default for IsolationLevel {
179    fn default() -> Self {
180        Self::ReadCommitted
181    }
182}
183
184/// Active database session state
185///
186/// Represents an active connection to the database with its associated
187/// transaction state, isolation level, and usage statistics.
188///
189/// # Lifecycle
190///
191/// 1. Session is created via [`EmbeddedDatabase::create_session`]
192/// 2. Commands are executed within the session context
193/// 3. Session is destroyed via [`EmbeddedDatabase::destroy_session`]
194///
195/// [`EmbeddedDatabase::create_session`]: crate::EmbeddedDatabase::create_session
196/// [`EmbeddedDatabase::destroy_session`]: crate::EmbeddedDatabase::destroy_session
197#[derive(Debug, Clone)]
198pub struct Session {
199    /// Unique session identifier
200    pub id: SessionId,
201    /// User who owns this session
202    pub user_id: UserId,
203    /// Transaction isolation level for this session
204    pub isolation_level: IsolationLevel,
205    /// Active transaction ID (None if no transaction in progress)
206    pub active_txn: Option<u64>,
207    /// Session creation timestamp (Unix epoch seconds)
208    pub created_at: u64,
209    /// Last activity timestamp (Unix epoch seconds)
210    pub last_activity: u64,
211    /// Cumulative session statistics
212    pub stats: SessionStats,
213}
214
215impl Session {
216    /// Create a new session
217    pub fn new(user_id: UserId, isolation_level: IsolationLevel) -> Self {
218        let now = std::time::SystemTime::now()
219            .duration_since(std::time::UNIX_EPOCH)
220            .unwrap_or_default()
221            .as_secs();
222
223        Self {
224            id: SessionId::new(),
225            user_id,
226            isolation_level,
227            active_txn: None,
228            created_at: now,
229            last_activity: now,
230            stats: SessionStats::default(),
231        }
232    }
233
234    /// Update last activity timestamp
235    pub fn touch(&mut self) {
236        self.last_activity = std::time::SystemTime::now()
237            .duration_since(std::time::UNIX_EPOCH)
238            .unwrap_or_default()
239            .as_secs();
240    }
241}
242
243/// Cumulative session statistics for monitoring and quota enforcement
244///
245/// Tracks all activity within a session including transaction counts,
246/// query counts, and I/O statistics. Used for resource monitoring,
247/// quota enforcement, and debugging.
248#[derive(Debug, Clone, Default)]
249pub struct SessionStats {
250    /// Total transactions started in this session
251    pub transactions_started: u64,
252    /// Total transactions successfully committed
253    pub transactions_committed: u64,
254    /// Total transactions rolled back (explicit or due to error)
255    pub transactions_aborted: u64,
256    /// Total SQL statements executed
257    pub queries_executed: u64,
258    /// Total bytes read from storage
259    pub bytes_read: u64,
260    /// Total bytes written to storage
261    pub bytes_written: u64,
262}
263
264#[cfg(test)]
265#[allow(clippy::unwrap_used, clippy::expect_used)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn test_user_password_hashing() {
271        let user = User::new("alice", "secret123");
272        assert!(user.has_password());
273        assert!(user.verify_password("secret123"));
274        assert!(!user.verify_password("wrongpassword"));
275        assert!(!user.verify_password(""));
276    }
277
278    #[test]
279    fn test_user_empty_password() {
280        let user = User::new("bob", "");
281        assert!(!user.has_password());
282        assert!(!user.verify_password(""));
283        assert!(!user.verify_password("anypassword"));
284    }
285
286    #[test]
287    fn test_user_passwordless() {
288        let user = User::new_passwordless("system");
289        assert!(!user.has_password());
290        assert!(!user.verify_password("anypassword"));
291    }
292
293    #[test]
294    fn test_user_set_password() {
295        let mut user = User::new_passwordless("charlie");
296        assert!(!user.has_password());
297
298        user.set_password("newpassword");
299        assert!(user.has_password());
300        assert!(user.verify_password("newpassword"));
301
302        user.set_password("");
303        assert!(!user.has_password());
304    }
305
306    #[test]
307    fn test_user_unique_ids() {
308        let user1 = User::new("user1", "pass1");
309        let user2 = User::new("user2", "pass2");
310        assert_ne!(user1.id, user2.id);
311    }
312
313    #[test]
314    fn test_session_creation() {
315        let user = User::new("testuser", "testpass");
316        let session = Session::new(user.id, IsolationLevel::ReadCommitted);
317        assert_eq!(session.user_id, user.id);
318        assert_eq!(session.isolation_level, IsolationLevel::ReadCommitted);
319        assert!(session.active_txn.is_none());
320    }
321}