Skip to main content

mongreldb_protocol/
session.rs

1//! Session model (spec section 10.4, S1D-004).
2//!
3//! A [`Session`] is the server-side state of one client session: who is
4//! connected, which database they are on, what transaction is active, which
5//! statements are prepared, the session settings, the read-your-writes
6//! token, and when the session was last active. Sessions are lightweight and
7//! do not own storage (S1D-004): the type here is data only — storage
8//! handles, execution state, and admission slots are owned by the server and
9//! referenced by id. Idle-time bounds (S1D-007) are enforced against
10//! [`Session::last_activity_unix_micros`].
11
12use std::collections::BTreeMap;
13
14use mongreldb_types::hlc::HlcTimestamp;
15use mongreldb_types::ids::{DatabaseId, TransactionId};
16
17use crate::prepared::{PreparedStatementBinding, StatementId};
18use crate::request::{AuthenticatedIdentity, IsolationLevel, SessionId};
19
20/// The transaction state of a session (S1D-004).
21#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub enum TransactionState {
23    /// No transaction is active; statements execute in autocommit mode.
24    Idle,
25    /// A transaction is active on this session.
26    Active {
27        /// The active transaction.
28        transaction_id: TransactionId,
29        /// Isolation level the transaction was begun with.
30        isolation: IsolationLevel,
31    },
32}
33
34impl TransactionState {
35    /// Whether a transaction is currently active.
36    pub fn is_active(&self) -> bool {
37        matches!(self, Self::Active { .. })
38    }
39}
40
41/// The server-side state of one client session (S1D-004).
42///
43/// Data only: a session owns no storage, no executor state, and no admission
44/// slots — it is the lightweight record those subsystems key off.
45#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
46pub struct Session {
47    /// Server-allocated identifier of this session.
48    pub session_id: SessionId,
49    /// Authenticated identity the session acts as; fixed at session open.
50    pub principal: AuthenticatedIdentity,
51    /// Database the session's statements resolve against.
52    pub current_database: DatabaseId,
53    /// Active transaction, if any.
54    pub transaction_state: TransactionState,
55    /// Prepared statements live on this session, by handle.
56    pub prepared_statements: BTreeMap<StatementId, PreparedStatementBinding>,
57    /// Session settings (e.g. `timezone`, `statement_timeout`); keys and
58    /// values are adapter-defined, unknown keys are ignored by the server.
59    pub settings: BTreeMap<String, String>,
60    /// Read-your-writes token: the highest commit timestamp this session has
61    /// durably observed; subsequent reads wait for visibility up to it.
62    pub read_your_writes_token: Option<HlcTimestamp>,
63    /// Last activity, wall-clock microseconds since the Unix epoch (same
64    /// time base as [`crate::request::ExecuteRequest::deadline_unix_micros`]);
65    /// idle reaping (S1D-007) keys off this.
66    pub last_activity_unix_micros: u64,
67}
68
69impl Session {
70    /// Opens a fresh session: no active transaction, no prepared statements,
71    /// default settings, no read-your-writes token yet.
72    pub fn new(
73        session_id: SessionId,
74        principal: AuthenticatedIdentity,
75        current_database: DatabaseId,
76        now_unix_micros: u64,
77    ) -> Self {
78        Self {
79            session_id,
80            principal,
81            current_database,
82            transaction_state: TransactionState::Idle,
83            prepared_statements: BTreeMap::new(),
84            settings: BTreeMap::new(),
85            read_your_writes_token: None,
86            last_activity_unix_micros: now_unix_micros,
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::test_support::assert_serde_round_trip;
95
96    fn sample_session() -> Session {
97        let mut session = Session::new(
98            SessionId::from_bytes([0x33; 16]),
99            AuthenticatedIdentity::CatalogUser {
100                username: "alice".to_owned(),
101                user_id: 42,
102                created_version: 7,
103            },
104            DatabaseId::new_random(),
105            1_758_000_000_000_000,
106        );
107        session.transaction_state = TransactionState::Active {
108            transaction_id: TransactionId::new_random(),
109            isolation: IsolationLevel::Snapshot,
110        };
111        session.prepared_statements.insert(
112            StatementId::new(1),
113            PreparedStatementBinding {
114                statement_id: StatementId::new(1),
115                sql: "SELECT 1".to_owned(),
116                parameter_types: vec![],
117                catalog_version: mongreldb_types::ids::MetadataVersion::new(100),
118                schema_versions: BTreeMap::new(),
119                feature_set: std::collections::BTreeSet::new(),
120            },
121        );
122        session
123            .settings
124            .insert("timezone".to_owned(), "UTC".to_owned());
125        session.read_your_writes_token = Some(HlcTimestamp {
126            physical_micros: 1_758_000_000_000_001,
127            logical: 3,
128            node_tiebreaker: 1,
129        });
130        session
131    }
132
133    #[test]
134    fn new_session_starts_idle_and_empty() {
135        let session = Session::new(
136            SessionId::ZERO,
137            AuthenticatedIdentity::Credentialless,
138            DatabaseId::new_random(),
139            1,
140        );
141        assert_eq!(session.transaction_state, TransactionState::Idle);
142        assert!(!session.transaction_state.is_active());
143        assert!(session.prepared_statements.is_empty());
144        assert!(session.settings.is_empty());
145        assert_eq!(session.read_your_writes_token, None);
146        assert_eq!(session.last_activity_unix_micros, 1);
147    }
148
149    #[test]
150    fn transaction_state_serde_round_trip() {
151        assert_serde_round_trip(&TransactionState::Idle);
152        assert_serde_round_trip(&TransactionState::Active {
153            transaction_id: TransactionId::new_random(),
154            isolation: IsolationLevel::Serializable,
155        });
156    }
157
158    #[test]
159    fn session_serde_round_trip() {
160        assert_serde_round_trip(&sample_session());
161        assert_serde_round_trip(&Session::new(
162            SessionId::from_bytes([0x44; 16]),
163            AuthenticatedIdentity::ServicePrincipal {
164                name: "cdc".to_owned(),
165            },
166            DatabaseId::new_random(),
167            42,
168        ));
169        assert_serde_round_trip(&Session::new(
170            SessionId::from_bytes([0x45; 16]),
171            AuthenticatedIdentity::ExternalPrincipal {
172                provider: "oidc".to_owned(),
173                subject: "alice".to_owned(),
174                username: "alice".to_owned(),
175                user_id: 7,
176                created_version: 8,
177                scopes: vec!["query".to_owned()],
178            },
179            DatabaseId::new_random(),
180            43,
181        ));
182    }
183}