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}