Skip to main content

mongreldb_protocol/
request.rs

1//! Canonical request model (spec section 10.4, S1D-001).
2//!
3//! Every protocol adapter (native RPC, HTTP/JSON, Kit, MySQL wire) converts
4//! its wire form into the canonical [`ExecuteRequest`] defined here, so
5//! admission, authorization, resource governance, and execution see exactly
6//! one request shape regardless of how the request arrived.
7//!
8//! # Deadline representation
9//!
10//! The spec model carries `deadline: Option<Instant>`, but
11//! [`std::time::Instant`] is opaque, monotonic, and not serde-able, so the
12//! canonical model instead stores `deadline_unix_micros`: wall-clock
13//! microseconds since the Unix epoch — the same time base as
14//! [`mongreldb_types::hlc::HlcTimestamp::physical_micros`]. This form is
15//! serde-stable in every format, comparable across processes, and converts
16//! to a monotonic `Instant` once at server admission; queue wait, planning,
17//! execution, serialization, and network backpressure all count toward it
18//! (S1D-006). `None` means the server applies its configured default
19//! deadline: a request is never unbounded (spec section 4.9).
20//!
21//! # Identity mapping
22//!
23//! [`AuthenticatedIdentity`] is the protocol-side canonical form of the
24//! engine-side identity (`mongreldb_core::auth::Principal`, the handle
25//! identity carried by every open database handle):
26//!
27//! - [`AuthenticatedIdentity::CatalogUser`] pins the immutable catalog
28//!   identity: `username`, `user_id`, and `created_version` map to
29//!   `Principal::username`, `Principal::user_id`, and
30//!   `Principal::created_epoch`. Roles, permissions, and the admin flag are
31//!   deliberately NOT part of the wire identity: the server re-resolves them
32//!   from the catalog at session open, so username reuse cannot revive a
33//!   stale principal.
34//! - [`AuthenticatedIdentity::Credentialless`] corresponds to deployments
35//!   with catalog auth disabled (embedded mode, or a daemon without
36//!   `--auth-users`): there is no `Principal` at all.
37//! - [`AuthenticatedIdentity::ServicePrincipal`] corresponds to internal
38//!   server components (replication, CDC, maintenance jobs) that act without
39//!   catalog credentials. External adapters MUST never mint one; the
40//!   authorization boundary fails closed on unrecognized principals.
41
42use core::fmt;
43use core::str::FromStr;
44
45use mongreldb_types::ids::{DatabaseId, QueryId};
46
47use crate::prepared::StatementId;
48
49/// Identifies one client session.
50///
51/// Session IDs are allocated by the server at session open (128 bits, drawn
52/// from a CSPRNG by the allocating service; this crate is dependency-frozen
53/// and deliberately does not mint them). The all-zero value is reserved.
54///
55/// Canonical text form: strict lowercase hexadecimal, 32 characters.
56#[repr(transparent)]
57#[derive(
58    Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
59)]
60pub struct SessionId(pub [u8; 16]);
61
62impl SessionId {
63    /// The all-zero session identifier (reserved).
64    pub const ZERO: Self = Self([0u8; 16]);
65
66    /// Wraps raw bytes without copying.
67    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
68        Self(bytes)
69    }
70
71    /// Borrows the raw 16 bytes.
72    pub const fn as_bytes(&self) -> &[u8; 16] {
73        &self.0
74    }
75
76    /// Canonical text form: strict lowercase hexadecimal (32 chars).
77    pub fn to_hex(self) -> String {
78        let mut out = String::with_capacity(32);
79        for byte in self.0 {
80            out.push(char::from_digit((byte >> 4) as u32, 16).expect("nibble"));
81            out.push(char::from_digit((byte & 0x0f) as u32, 16).expect("nibble"));
82        }
83        out
84    }
85}
86
87impl fmt::Display for SessionId {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.write_str(&self.to_hex())
90    }
91}
92
93impl fmt::Debug for SessionId {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(f, "SessionId({})", self.to_hex())
96    }
97}
98
99/// Error returned when parsing a textual [`SessionId`] fails.
100#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
101pub enum SessionIdParseError {
102    /// The text had the wrong number of characters.
103    #[error("invalid session id length: expected 32 hex digits, got {0} chars")]
104    InvalidLength(usize),
105    /// The text contained a non-hexadecimal character.
106    #[error("invalid hex character `{0}` in session id")]
107    InvalidCharacter(char),
108}
109
110impl FromStr for SessionId {
111    type Err = SessionIdParseError;
112
113    /// Parses the canonical 32-character hex form. Lenient by contract, like
114    /// the `mongreldb-types` identifiers: hyphens are ignored at any position
115    /// (so the hyphenated UUID form `8-4-4-4-12` parses) and uppercase hex
116    /// digits are accepted.
117    fn from_str(text: &str) -> Result<Self, Self::Err> {
118        let compact: String = text.chars().filter(|c| *c != '-').collect();
119        if compact.chars().count() != 32 {
120            return Err(SessionIdParseError::InvalidLength(compact.chars().count()));
121        }
122        let mut bytes = [0u8; 16];
123        let mut chars = compact.chars();
124        for byte in &mut bytes {
125            let hi = chars.next().expect("length checked above");
126            let lo = chars.next().expect("length checked above");
127            let hi = hi
128                .to_digit(16)
129                .ok_or(SessionIdParseError::InvalidCharacter(hi))?;
130            let lo = lo
131                .to_digit(16)
132                .ok_or(SessionIdParseError::InvalidCharacter(lo))?;
133            *byte = ((hi << 4) | lo) as u8;
134        }
135        Ok(Self(bytes))
136    }
137}
138
139/// The authenticated identity attached to a request (S1D-001).
140///
141/// See the module-level documentation for the mapping to the engine-side
142/// identity (`mongreldb_core::auth::Principal`).
143#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
144pub enum AuthenticatedIdentity {
145    /// No catalog credentials: embedded mode or a daemon running without
146    /// catalog auth. Carries no identity; the storage authorization layer
147    /// applies its no-auth policy.
148    Credentialless,
149    /// A catalog-authenticated user. Pins the immutable identity
150    /// (`user_id` + `created_version`); grants are re-resolved by the server
151    /// at session open so a recreated username cannot inherit stale rights.
152    CatalogUser {
153        /// Case-sensitive username.
154        username: String,
155        /// Immutable catalog user id (`Principal::user_id`).
156        user_id: u64,
157        /// User generation paired with `user_id` (`Principal::created_epoch`).
158        created_version: u64,
159    },
160    /// An internal server component (replication, CDC, maintenance) acting
161    /// without catalog credentials. Never accepted from external adapters.
162    ServicePrincipal {
163        /// Stable name of the internal component, e.g. `"replication"`.
164        name: String,
165    },
166}
167
168/// MVCC isolation level requested by [`ExecuteCommand::Begin`].
169///
170/// Mirrors `mongreldb_core::txn::IsolationLevel` one-to-one (`Snapshot`,
171/// `ReadCommitted`, `Serializable`). The protocol crate cannot depend on the
172/// core crate (the server wave depends on both), so the enum is duplicated
173/// here and converted at the service boundary.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
175pub enum IsolationLevel {
176    /// Snapshot isolation: the transaction reads one consistent snapshot.
177    #[default]
178    Snapshot,
179    /// Read committed: each statement reads the latest committed state.
180    ReadCommitted,
181    /// Serializable: snapshot reads plus certification at commit.
182    Serializable,
183}
184
185/// One bound parameter value of a [`ExecuteCommand::Sql`] or
186/// [`ExecuteCommand::ExecutePrepared`] request.
187///
188/// The canonical model carries typed scalar values; composite Arrow-native
189/// parameters land with the server wave. [`ParameterValue::type_name`]
190/// provides the canonical type names recorded in
191/// [`crate::prepared::PreparedStatementBinding::parameter_types`].
192#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
193pub enum ParameterValue {
194    /// SQL `NULL`.
195    Null,
196    /// A boolean.
197    Bool(bool),
198    /// A 64-bit signed integer.
199    Integer(i64),
200    /// A double-precision float.
201    Float(f64),
202    /// A UTF-8 string.
203    Text(String),
204    /// Opaque binary data.
205    Bytes(Vec<u8>),
206}
207
208impl ParameterValue {
209    /// The canonical type name of this value, as recorded in a prepared
210    /// statement's `parameter_types`.
211    pub fn type_name(&self) -> &'static str {
212        match self {
213            Self::Null => "NULL",
214            Self::Bool(_) => "BOOL",
215            Self::Integer(_) => "INT64",
216            Self::Float(_) => "FLOAT64",
217            Self::Text(_) => "TEXT",
218            Self::Bytes(_) => "BYTES",
219        }
220    }
221}
222
223/// Administrative operations carried by [`ExecuteCommand::Admin`].
224///
225/// Intentionally lean: user/role management, the operations gated by the
226/// catalog's admin permission today. Further admin verbs are appended in
227/// later stages; discriminants are never reordered or reused (spec
228/// section 4.10).
229#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
230pub enum AdminCommand {
231    /// Create a catalog user.
232    CreateUser {
233        /// Case-sensitive username of the new user.
234        username: String,
235    },
236    /// Drop a catalog user.
237    DropUser {
238        /// Case-sensitive username to drop.
239        username: String,
240    },
241    /// Grant a role to a user.
242    GrantRole {
243        /// Case-sensitive username receiving the role.
244        username: String,
245        /// Role to grant.
246        role: String,
247    },
248    /// Revoke a role from a user.
249    RevokeRole {
250        /// Case-sensitive username losing the role.
251        username: String,
252        /// Role to revoke.
253        role: String,
254    },
255}
256
257/// The operation an [`ExecuteRequest`] asks the server to perform (S1D-001).
258///
259/// Variants are never reordered and discriminants never reused (spec
260/// section 4.10); new commands are only appended.
261#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
262pub enum ExecuteCommand {
263    /// Parse, plan, and execute a SQL statement.
264    Sql {
265        /// The SQL text.
266        text: String,
267        /// Bound parameter values, in statement order.
268        params: Vec<ParameterValue>,
269    },
270    /// Execute a previously prepared statement.
271    ExecutePrepared {
272        /// Session-scoped prepared statement handle from
273        /// [`crate::services::QueryService::prepare`].
274        statement_id: StatementId,
275        /// Bound parameter values, in statement order.
276        params: Vec<ParameterValue>,
277    },
278    /// Begin a transaction on the session.
279    Begin {
280        /// Requested MVCC isolation level.
281        isolation: IsolationLevel,
282    },
283    /// Commit the session's active transaction.
284    Commit,
285    /// Roll back the session's active transaction.
286    Rollback,
287    /// Cancel a running query (S1D-006).
288    Cancel {
289        /// The query to cancel.
290        query_id: QueryId,
291    },
292    /// Fetch the schema of one table.
293    GetSchema {
294        /// Table name.
295        table: String,
296    },
297    /// An administrative operation.
298    Admin(AdminCommand),
299}
300
301/// Conservative default bounds a server applies when a request does not
302/// state one explicitly (spec section 4.9): 100 000 rows.
303pub const DEFAULT_MAX_ROWS: u64 = 100_000;
304/// Conservative default result-size bound: 64 MiB of Arrow IPC payload.
305pub const DEFAULT_MAX_BYTES: u64 = 64 * 1024 * 1024;
306/// Conservative default candidate-count bound: one million candidates.
307pub const DEFAULT_MAX_CANDIDATE_COUNT: u64 = 1_000_000;
308
309/// Per-request result bounds (spec sections 4.9 and 10.4, S1D-001).
310///
311/// Every request is bounded: `None` does NOT mean unbounded, it means the
312/// server applies its configured default (see the `DEFAULT_*` constants for
313/// the conservative shipped defaults) and may clamp explicit values down to
314/// configured ceilings. The effective limits are what the executor
315/// enforces; exceeding them fails the request with
316/// [`mongreldb_types::errors::ErrorCategory::ResourceExhausted`].
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
318pub struct ResultLimits {
319    /// Maximum result rows; `None` applies the server default.
320    pub max_rows: Option<u64>,
321    /// Maximum result bytes; `None` applies the server default.
322    pub max_bytes: Option<u64>,
323    /// Maximum candidate rows scanned during retrieval; `None` applies the
324    /// server default.
325    pub max_candidate_count: Option<u64>,
326}
327
328impl ResultLimits {
329    /// Effective row bound after applying server-enforceable defaults.
330    pub fn effective_max_rows(&self) -> u64 {
331        self.max_rows.unwrap_or(DEFAULT_MAX_ROWS)
332    }
333
334    /// Effective byte bound after applying server-enforceable defaults.
335    pub fn effective_max_bytes(&self) -> u64 {
336        self.max_bytes.unwrap_or(DEFAULT_MAX_BYTES)
337    }
338
339    /// Effective candidate-count bound after applying server-enforceable
340    /// defaults.
341    pub fn effective_max_candidate_count(&self) -> u64 {
342        self.max_candidate_count
343            .unwrap_or(DEFAULT_MAX_CANDIDATE_COUNT)
344    }
345}
346
347/// The canonical request every protocol adapter converts into (S1D-001).
348#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
349pub struct ExecuteRequest {
350    /// Unique identifier of this request, chosen by the caller; never reused,
351    /// so retries with the same id can be deduplicated.
352    pub request_id: [u8; 16],
353    /// Identifier of the query execution this request drives.
354    pub query_id: QueryId,
355    /// Session the request runs on, if sessionful.
356    pub session_id: Option<SessionId>,
357    /// Logical database the request targets.
358    pub database_id: DatabaseId,
359    /// Authenticated identity the request acts as.
360    pub principal: AuthenticatedIdentity,
361    /// The operation to perform.
362    pub command: ExecuteCommand,
363    /// Wall-clock deadline in microseconds since the Unix epoch; see the
364    /// module-level documentation for why this is not an `Instant`.
365    pub deadline_unix_micros: Option<u64>,
366    /// Per-request result bounds; always effective, never unbounded.
367    pub result_limits: ResultLimits,
368    /// Optional resource group the request is admitted into (spec
369    /// section 10.5, workload classes and resource governance).
370    pub resource_group: Option<String>,
371    /// Optional durable idempotency key: present iff the caller may safely
372    /// replay the request after an ambiguous outcome (spec section 11.7).
373    pub idempotency_key: Option<String>,
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::test_support::assert_serde_round_trip;
380
381    fn sample_principal() -> AuthenticatedIdentity {
382        AuthenticatedIdentity::CatalogUser {
383            username: "alice".to_owned(),
384            user_id: 42,
385            created_version: 7,
386        }
387    }
388
389    fn sample_params() -> Vec<ParameterValue> {
390        vec![
391            ParameterValue::Null,
392            ParameterValue::Bool(true),
393            ParameterValue::Integer(-42),
394            ParameterValue::Float(2.5),
395            ParameterValue::Text("hello".to_owned()),
396            ParameterValue::Bytes(vec![0xde, 0xad, 0xbe, 0xef]),
397        ]
398    }
399
400    #[test]
401    fn session_id_text_form_round_trips() {
402        let bytes = [
403            0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76,
404            0x54, 0x32,
405        ];
406        let id = SessionId::from_bytes(bytes);
407        assert_eq!(id.to_hex(), "000123456789abcdeffedcba98765432");
408        assert_eq!(id.to_string(), id.to_hex());
409        assert_eq!(
410            format!("{id:?}"),
411            "SessionId(000123456789abcdeffedcba98765432)"
412        );
413        assert_eq!(id.to_hex().parse::<SessionId>().unwrap(), id);
414        assert_eq!(
415            "00012345-6789-abcd-effe-dcba98765432"
416                .parse::<SessionId>()
417                .unwrap(),
418            id,
419            "hyphenated UUID form parses"
420        );
421        assert_eq!(
422            id.to_hex()
423                .to_ascii_uppercase()
424                .parse::<SessionId>()
425                .unwrap(),
426            id,
427            "uppercase hex parses"
428        );
429        assert_eq!(id.as_bytes(), &bytes);
430        assert_eq!(SessionId::ZERO.as_bytes(), &[0u8; 16]);
431    }
432
433    #[test]
434    fn session_id_parse_rejects_bad_input() {
435        assert_eq!(
436            "".parse::<SessionId>(),
437            Err(SessionIdParseError::InvalidLength(0))
438        );
439        assert_eq!(
440            "abcd".parse::<SessionId>(),
441            Err(SessionIdParseError::InvalidLength(4))
442        );
443        assert_eq!(
444            "g".repeat(32).parse::<SessionId>(),
445            Err(SessionIdParseError::InvalidCharacter('g'))
446        );
447    }
448
449    #[test]
450    fn session_id_serde_round_trip() {
451        assert_serde_round_trip(&SessionId::from_bytes([0x5a; 16]));
452        assert_serde_round_trip(&SessionId::ZERO);
453    }
454
455    #[test]
456    fn authenticated_identity_serde_round_trip() {
457        assert_serde_round_trip(&AuthenticatedIdentity::Credentialless);
458        assert_serde_round_trip(&sample_principal());
459        assert_serde_round_trip(&AuthenticatedIdentity::ServicePrincipal {
460            name: "replication".to_owned(),
461        });
462    }
463
464    #[test]
465    fn isolation_level_serde_round_trip_and_default() {
466        assert_eq!(IsolationLevel::default(), IsolationLevel::Snapshot);
467        for level in [
468            IsolationLevel::Snapshot,
469            IsolationLevel::ReadCommitted,
470            IsolationLevel::Serializable,
471        ] {
472            assert_serde_round_trip(&level);
473        }
474    }
475
476    #[test]
477    fn parameter_value_serde_round_trip_and_type_names() {
478        for value in sample_params() {
479            assert_serde_round_trip(&value);
480        }
481        let names: Vec<&'static str> = sample_params()
482            .iter()
483            .map(ParameterValue::type_name)
484            .collect();
485        assert_eq!(names, ["NULL", "BOOL", "INT64", "FLOAT64", "TEXT", "BYTES"]);
486    }
487
488    #[test]
489    fn admin_command_serde_round_trip() {
490        for command in [
491            AdminCommand::CreateUser {
492                username: "alice".to_owned(),
493            },
494            AdminCommand::DropUser {
495                username: "alice".to_owned(),
496            },
497            AdminCommand::GrantRole {
498                username: "alice".to_owned(),
499                role: "analyst".to_owned(),
500            },
501            AdminCommand::RevokeRole {
502                username: "alice".to_owned(),
503                role: "analyst".to_owned(),
504            },
505        ] {
506            assert_serde_round_trip(&command);
507        }
508    }
509
510    #[test]
511    fn execute_command_serde_round_trip_every_variant() {
512        for command in [
513            ExecuteCommand::Sql {
514                text: "SELECT * FROM t WHERE a = ?".to_owned(),
515                params: sample_params(),
516            },
517            ExecuteCommand::ExecutePrepared {
518                statement_id: StatementId::new(9),
519                params: sample_params(),
520            },
521            ExecuteCommand::Begin {
522                isolation: IsolationLevel::Serializable,
523            },
524            ExecuteCommand::Commit,
525            ExecuteCommand::Rollback,
526            ExecuteCommand::Cancel {
527                query_id: QueryId::new_random(),
528            },
529            ExecuteCommand::GetSchema {
530                table: "events".to_owned(),
531            },
532            ExecuteCommand::Admin(AdminCommand::GrantRole {
533                username: "alice".to_owned(),
534                role: "analyst".to_owned(),
535            }),
536        ] {
537            assert_serde_round_trip(&command);
538        }
539    }
540
541    #[test]
542    fn result_limits_defaults_are_bounded() {
543        let limits = ResultLimits::default();
544        assert_eq!(limits.max_rows, None);
545        assert_eq!(limits.effective_max_rows(), DEFAULT_MAX_ROWS);
546        assert_eq!(limits.effective_max_bytes(), DEFAULT_MAX_BYTES);
547        assert_eq!(
548            limits.effective_max_candidate_count(),
549            DEFAULT_MAX_CANDIDATE_COUNT
550        );
551        let explicit = ResultLimits {
552            max_rows: Some(10),
553            max_bytes: Some(1024),
554            max_candidate_count: Some(50),
555        };
556        assert_eq!(explicit.effective_max_rows(), 10);
557        assert_eq!(explicit.effective_max_bytes(), 1024);
558        assert_eq!(explicit.effective_max_candidate_count(), 50);
559        assert_serde_round_trip(&limits);
560        assert_serde_round_trip(&explicit);
561    }
562
563    #[test]
564    fn execute_request_serde_round_trip() {
565        let full = ExecuteRequest {
566            request_id: [0x11; 16],
567            query_id: QueryId::new_random(),
568            session_id: Some(SessionId::from_bytes([0x22; 16])),
569            database_id: DatabaseId::new_random(),
570            principal: sample_principal(),
571            command: ExecuteCommand::Sql {
572                text: "SELECT 1".to_owned(),
573                params: sample_params(),
574            },
575            deadline_unix_micros: Some(1_758_000_000_000_000),
576            result_limits: ResultLimits {
577                max_rows: Some(500),
578                max_bytes: None,
579                max_candidate_count: Some(10_000),
580            },
581            resource_group: Some("interactive".to_owned()),
582            idempotency_key: Some("req-42".to_owned()),
583        };
584        assert_serde_round_trip(&full);
585        let minimal = ExecuteRequest {
586            session_id: None,
587            deadline_unix_micros: None,
588            result_limits: ResultLimits::default(),
589            resource_group: None,
590            idempotency_key: None,
591            principal: AuthenticatedIdentity::Credentialless,
592            command: ExecuteCommand::Commit,
593            ..full.clone()
594        };
595        assert_serde_round_trip(&minimal);
596    }
597}