Skip to main content

mongreldb_query/
query_registry.rs

1use crate::{MongrelQueryError, Result, SqlTestHook};
2use mongreldb_core::{CancellationReason, ExecutionControl};
3use parking_lot::Mutex;
4use std::collections::{HashMap, VecDeque};
5use std::fmt;
6use std::str::FromStr;
7use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
8use std::sync::{Arc, Weak};
9use std::time::{Duration, Instant};
10
11const DEFAULT_MAX_ACTIVE: usize = 1_024;
12const DEFAULT_MAX_FINISHED: usize = 2_048;
13const DEFAULT_MAX_FINISHED_BYTES: usize = 8 * 1024 * 1024;
14const DEFAULT_MAX_COMPACT: usize = 100_000;
15const DEFAULT_MAX_COMPACT_BYTES: usize = 32 * 1024 * 1024;
16const DEFAULT_FINISHED_TTL: Duration = Duration::from_secs(60);
17const MAX_METADATA_BYTES: usize = 256;
18
19#[derive(Clone, Copy, PartialEq, Eq, Hash)]
20pub struct QueryId([u8; 16]);
21
22impl QueryId {
23    pub fn random() -> Result<Self> {
24        let mut bytes = [0; 16];
25        getrandom::getrandom(&mut bytes).map_err(|error| {
26            MongrelQueryError::Core(mongreldb_core::MongrelError::Other(format!(
27                "query id randomness failed: {error}"
28            )))
29        })?;
30        Ok(Self(bytes))
31    }
32
33    pub fn as_bytes(&self) -> &[u8; 16] {
34        &self.0
35    }
36}
37
38impl fmt::Display for QueryId {
39    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40        for byte in self.0 {
41            write!(formatter, "{byte:02x}")?;
42        }
43        Ok(())
44    }
45}
46
47impl fmt::Debug for QueryId {
48    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49        fmt::Display::fmt(self, formatter)
50    }
51}
52
53impl FromStr for QueryId {
54    type Err = MongrelQueryError;
55
56    fn from_str(value: &str) -> Result<Self> {
57        if value.len() != 32 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
58            return Err(MongrelQueryError::Core(
59                mongreldb_core::MongrelError::InvalidArgument(
60                    "query id must be exactly 32 hexadecimal characters".into(),
61                ),
62            ));
63        }
64        let mut bytes = [0; 16];
65        for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
66            let text = std::str::from_utf8(chunk).map_err(|_| {
67                MongrelQueryError::Core(mongreldb_core::MongrelError::InvalidArgument(
68                    "query id is not valid UTF-8".into(),
69                ))
70            })?;
71            bytes[index] = u8::from_str_radix(text, 16).map_err(|_| {
72                MongrelQueryError::Core(mongreldb_core::MongrelError::InvalidArgument(
73                    "query id contains invalid hexadecimal".into(),
74                ))
75            })?;
76        }
77        Ok(Self(bytes))
78    }
79}
80
81impl serde::Serialize for QueryId {
82    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
83    where
84        S: serde::Serializer,
85    {
86        serializer.serialize_str(&self.to_string())
87    }
88}
89
90impl<'de> serde::Deserialize<'de> for QueryId {
91    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
92    where
93        D: serde::Deserializer<'de>,
94    {
95        let value = <String as serde::Deserialize>::deserialize(deserializer)?;
96        value.parse().map_err(serde::de::Error::custom)
97    }
98}
99
100#[derive(Debug, Clone, Default)]
101pub struct SqlQueryOptions {
102    pub query_id: Option<QueryId>,
103    pub timeout: Option<Duration>,
104    pub owner: Option<String>,
105    pub session_id: Option<String>,
106    pub parent_control: Option<ExecutionControl>,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110#[repr(u8)]
111pub enum SqlQueryPhase {
112    Queued = 0,
113    Planning = 1,
114    Executing = 2,
115    Streaming = 3,
116    Serializing = 4,
117    CommitCritical = 5,
118    Cancelling = 6,
119    Completed = 7,
120    Failed = 8,
121    Cancelled = 9,
122}
123
124impl SqlQueryPhase {
125    fn from_u8(value: u8) -> Self {
126        match value {
127            0 => Self::Queued,
128            1 => Self::Planning,
129            2 => Self::Executing,
130            3 => Self::Streaming,
131            4 => Self::Serializing,
132            5 => Self::CommitCritical,
133            6 => Self::Cancelling,
134            7 => Self::Completed,
135            8 => Self::Failed,
136            9 => Self::Cancelled,
137            _ => Self::Failed,
138        }
139    }
140
141    fn is_terminal(self) -> bool {
142        matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
143    }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum CancelOutcome {
148    Accepted,
149    AlreadyCancelling,
150    TooLate,
151    AlreadyFinished,
152    NotFound,
153}
154
155#[derive(Debug, Clone, Default, PartialEq, Eq)]
156pub struct DurableOutcome {
157    pub committed: bool,
158    pub committed_statements: usize,
159    pub last_commit_epoch: Option<u64>,
160    pub first_commit_statement_index: Option<usize>,
161    pub last_commit_statement_index: Option<usize>,
162}
163
164impl DurableOutcome {
165    fn record_commit(&mut self, statement_index: usize, epoch: Option<u64>) {
166        self.committed = true;
167        if self.last_commit_statement_index != Some(statement_index) {
168            self.committed_statements = self.committed_statements.saturating_add(1);
169        }
170        self.first_commit_statement_index
171            .get_or_insert(statement_index);
172        self.last_commit_statement_index = Some(statement_index);
173        if epoch.is_some() {
174            self.last_commit_epoch = epoch;
175        }
176    }
177}
178
179#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
180pub enum SerializationOutcome {
181    #[default]
182    NotStarted,
183    InProgress,
184    Succeeded,
185    Failed,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum QueryTerminalErrorCategory {
190    Cancellation,
191    Deadline,
192    ResultLimit,
193    Serialization,
194    Execution,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct QueryTerminalError {
199    pub code: String,
200    pub category: QueryTerminalErrorCategory,
201}
202
203#[derive(Debug, Clone, Default, PartialEq, Eq)]
204struct QueryOutcomeState {
205    durable: DurableOutcome,
206    serialization: SerializationOutcome,
207    terminal_error: Option<QueryTerminalError>,
208    terminal_state_override: Option<QueryTerminalState>,
209    cancellation_reason_override: Option<CancellationReason>,
210    phase_override: Option<SqlQueryPhase>,
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum QueryTerminalState {
215    OutcomeUnknown,
216    Completed,
217    FailedBeforeCommit,
218    CancelledBeforeCommit,
219    DeadlineBeforeCommit,
220    Committed,
221    CommittedWithError,
222    PartiallyCommitted,
223    CancelledAfterCommit,
224    DeadlineAfterCommit,
225}
226
227#[derive(Debug, Clone)]
228pub struct QueryStatus {
229    pub query_id: QueryId,
230    pub owner: Option<String>,
231    pub session_id: Option<String>,
232    pub phase: SqlQueryPhase,
233    pub started_at: Instant,
234    pub deadline: Option<Instant>,
235    pub operation: String,
236    pub sql_fingerprint: [u8; 32],
237    pub cancellation_reason: CancellationReason,
238    /// Backward-compatible projection of `durable_outcome.committed`.
239    pub committed: bool,
240    pub durable_outcome: DurableOutcome,
241    pub terminal_error: Option<QueryTerminalError>,
242    pub serialization_outcome: SerializationOutcome,
243    pub outcome_unknown: bool,
244    terminal_state_override: Option<QueryTerminalState>,
245    pub completed_statements: usize,
246    pub statement_index: usize,
247    pub cancel_requested_at: Option<Instant>,
248    pub queue_duration: Duration,
249    pub planning_duration: Duration,
250    pub execution_duration: Duration,
251    pub serialization_duration: Duration,
252    pub cancel_requested_phase: Option<SqlQueryPhase>,
253    pub cancel_observed_phase: Option<SqlQueryPhase>,
254    pub commit_fence_outcome: CommitFenceOutcome,
255}
256
257impl QueryStatus {
258    pub fn terminal_state(&self) -> Option<QueryTerminalState> {
259        if !self.phase.is_terminal() {
260            return None;
261        }
262        if let Some(terminal_state) = self.terminal_state_override {
263            return Some(terminal_state);
264        }
265        if !self.durable_outcome.committed {
266            return Some(match (self.phase, self.cancellation_reason) {
267                (SqlQueryPhase::Completed, _) => QueryTerminalState::Completed,
268                (SqlQueryPhase::Cancelled, CancellationReason::Deadline) => {
269                    QueryTerminalState::DeadlineBeforeCommit
270                }
271                (SqlQueryPhase::Cancelled, _) => QueryTerminalState::CancelledBeforeCommit,
272                _ => QueryTerminalState::FailedBeforeCommit,
273            });
274        }
275        Some(match (self.phase, self.cancellation_reason) {
276            (SqlQueryPhase::Completed, _) => QueryTerminalState::Committed,
277            (SqlQueryPhase::Cancelled, CancellationReason::Deadline) => {
278                QueryTerminalState::DeadlineAfterCommit
279            }
280            (SqlQueryPhase::Cancelled, _) => QueryTerminalState::CancelledAfterCommit,
281            _ if self
282                .durable_outcome
283                .last_commit_statement_index
284                .is_some_and(|index| index < self.statement_index) =>
285            {
286                QueryTerminalState::PartiallyCommitted
287            }
288            _ => QueryTerminalState::CommittedWithError,
289        })
290    }
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum CommitFenceOutcome {
295    NotReached,
296    CancelWon,
297    CommitWon,
298}
299
300#[derive(Debug, Clone)]
301struct QueryTrace {
302    phase_started_at: Instant,
303    queue_duration: Duration,
304    planning_duration: Duration,
305    execution_duration: Duration,
306    serialization_duration: Duration,
307    cancel_requested_phase: Option<SqlQueryPhase>,
308    cancel_observed_phase: Option<SqlQueryPhase>,
309    commit_fence_outcome: CommitFenceOutcome,
310}
311
312impl QueryTrace {
313    fn new(started_at: Instant) -> Self {
314        Self {
315            phase_started_at: started_at,
316            queue_duration: Duration::ZERO,
317            planning_duration: Duration::ZERO,
318            execution_duration: Duration::ZERO,
319            serialization_duration: Duration::ZERO,
320            cancel_requested_phase: None,
321            cancel_observed_phase: None,
322            commit_fence_outcome: CommitFenceOutcome::NotReached,
323        }
324    }
325
326    fn transition(&mut self, phase: SqlQueryPhase) {
327        let now = Instant::now();
328        let elapsed = now.saturating_duration_since(self.phase_started_at);
329        match phase {
330            SqlQueryPhase::Queued => self.queue_duration += elapsed,
331            SqlQueryPhase::Planning => self.planning_duration += elapsed,
332            SqlQueryPhase::Executing | SqlQueryPhase::Streaming | SqlQueryPhase::CommitCritical => {
333                self.execution_duration += elapsed
334            }
335            SqlQueryPhase::Serializing => self.serialization_duration += elapsed,
336            SqlQueryPhase::Cancelling
337            | SqlQueryPhase::Completed
338            | SqlQueryPhase::Failed
339            | SqlQueryPhase::Cancelled => {}
340        }
341        self.phase_started_at = now;
342    }
343}
344
345#[derive(Debug)]
346struct RegisteredQuery {
347    id: QueryId,
348    owner: Option<String>,
349    session_id: Option<String>,
350    control: ExecutionControl,
351    phase: AtomicU8,
352    started_at: Instant,
353    deadline: Option<Instant>,
354    operation: Mutex<String>,
355    sql_fingerprint: Mutex<[u8; 32]>,
356    outcome: Mutex<QueryOutcomeState>,
357    completed_statements: AtomicUsize,
358    statement_index: AtomicUsize,
359    cancel_requested_at: Mutex<Option<Instant>>,
360    trace: Mutex<QueryTrace>,
361}
362
363impl RegisteredQuery {
364    fn phase(&self) -> SqlQueryPhase {
365        SqlQueryPhase::from_u8(self.phase.load(Ordering::Acquire))
366    }
367
368    fn status(&self) -> QueryStatus {
369        let outcome = self.outcome.lock().clone();
370        let phase = outcome.phase_override.unwrap_or_else(|| self.phase());
371        let mut trace = self.trace.lock().clone();
372        trace.transition(phase);
373        QueryStatus {
374            query_id: self.id,
375            owner: self.owner.clone(),
376            session_id: self.session_id.clone(),
377            phase,
378            started_at: self.started_at,
379            deadline: self.deadline,
380            operation: self.operation.lock().clone(),
381            sql_fingerprint: *self.sql_fingerprint.lock(),
382            cancellation_reason: outcome
383                .cancellation_reason_override
384                .unwrap_or_else(|| self.control.reason()),
385            committed: outcome.durable.committed,
386            durable_outcome: outcome.durable,
387            terminal_error: outcome.terminal_error,
388            serialization_outcome: outcome.serialization,
389            outcome_unknown: outcome.terminal_state_override
390                == Some(QueryTerminalState::OutcomeUnknown),
391            terminal_state_override: outcome.terminal_state_override,
392            completed_statements: self.completed_statements.load(Ordering::Acquire),
393            statement_index: self.statement_index.load(Ordering::Acquire),
394            cancel_requested_at: *self.cancel_requested_at.lock(),
395            queue_duration: trace.queue_duration,
396            planning_duration: trace.planning_duration,
397            execution_duration: trace.execution_duration,
398            serialization_duration: trace.serialization_duration,
399            cancel_requested_phase: trace.cancel_requested_phase,
400            cancel_observed_phase: trace.cancel_observed_phase,
401            commit_fence_outcome: trace.commit_fence_outcome,
402        }
403    }
404
405    fn record_transition(&self, phase: SqlQueryPhase) {
406        self.trace.lock().transition(phase);
407    }
408}
409
410#[derive(Debug, Clone)]
411struct FinishedQuery {
412    status: QueryStatus,
413    finished_at: Instant,
414    approximate_bytes: usize,
415}
416
417#[derive(Debug, Clone)]
418pub struct CompactFinishedQuery {
419    pub query_id: QueryId,
420    pub owner: Option<String>,
421    pub session_id: Option<String>,
422    pub phase: SqlQueryPhase,
423    pub terminal_state: QueryTerminalState,
424    pub cancellation_reason: CancellationReason,
425    pub durable_outcome: DurableOutcome,
426    pub serialization_outcome: SerializationOutcome,
427    pub terminal_error: Option<QueryTerminalError>,
428    pub completed_statements: usize,
429    pub statement_index: usize,
430    finished_at: Instant,
431    approximate_bytes: usize,
432}
433
434impl CompactFinishedQuery {
435    fn from_status(status: QueryStatus, finished_at: Instant) -> Self {
436        let terminal_state = status
437            .terminal_state()
438            .unwrap_or(QueryTerminalState::OutcomeUnknown);
439        let approximate_bytes = std::mem::size_of::<Self>()
440            + status.owner.as_ref().map_or(0, String::len)
441            + status.session_id.as_ref().map_or(0, String::len)
442            + status
443                .terminal_error
444                .as_ref()
445                .map_or(0, |error| error.code.len());
446        Self {
447            query_id: status.query_id,
448            owner: status.owner,
449            session_id: status.session_id,
450            phase: status.phase,
451            terminal_state,
452            cancellation_reason: status.cancellation_reason,
453            durable_outcome: status.durable_outcome,
454            serialization_outcome: status.serialization_outcome,
455            terminal_error: status.terminal_error,
456            completed_statements: status.completed_statements,
457            statement_index: status.statement_index,
458            finished_at,
459            approximate_bytes,
460        }
461    }
462}
463
464#[derive(Debug, Default)]
465struct RegistryState {
466    active: HashMap<QueryId, Arc<RegisteredQuery>>,
467    detailed: HashMap<QueryId, FinishedQuery>,
468    detailed_lru: VecDeque<QueryId>,
469    detailed_bytes: usize,
470    compact: HashMap<QueryId, CompactFinishedQuery>,
471    compact_lru: VecDeque<QueryId>,
472    compact_bytes: usize,
473    demotions: u64,
474    compact_evictions: u64,
475    active_rejections: u64,
476}
477
478#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
479pub struct QueryRegistryStats {
480    pub active: usize,
481    pub queued: usize,
482    pub detailed: usize,
483    pub compact: usize,
484    pub detailed_bytes: usize,
485    pub compact_bytes: usize,
486    pub demotions: u64,
487    pub compact_evictions: u64,
488    pub active_rejections: u64,
489    pub oldest_compact_age: Duration,
490}
491
492#[derive(Debug)]
493pub struct SqlQueryRegistry {
494    state: Mutex<RegistryState>,
495    max_active: usize,
496    max_detailed: usize,
497    max_detailed_bytes: usize,
498    max_compact: usize,
499    max_compact_bytes: usize,
500    finished_ttl: Duration,
501}
502
503impl Default for SqlQueryRegistry {
504    fn default() -> Self {
505        Self::new_with_limits(
506            DEFAULT_MAX_ACTIVE,
507            DEFAULT_MAX_FINISHED,
508            DEFAULT_MAX_FINISHED_BYTES,
509            DEFAULT_MAX_COMPACT,
510            DEFAULT_MAX_COMPACT_BYTES,
511            DEFAULT_FINISHED_TTL,
512        )
513    }
514}
515
516impl SqlQueryRegistry {
517    pub fn new(
518        max_active: usize,
519        max_finished: usize,
520        max_finished_bytes: usize,
521        finished_ttl: Duration,
522    ) -> Self {
523        Self::new_with_limits(
524            max_active,
525            max_finished,
526            max_finished_bytes,
527            DEFAULT_MAX_COMPACT,
528            DEFAULT_MAX_COMPACT_BYTES,
529            finished_ttl,
530        )
531    }
532
533    pub fn new_with_limits(
534        max_active: usize,
535        max_detailed: usize,
536        max_detailed_bytes: usize,
537        max_compact: usize,
538        max_compact_bytes: usize,
539        finished_ttl: Duration,
540    ) -> Self {
541        Self {
542            state: Mutex::new(RegistryState::default()),
543            max_active: max_active.max(1),
544            max_detailed,
545            max_detailed_bytes,
546            max_compact,
547            max_compact_bytes,
548            finished_ttl,
549        }
550    }
551
552    pub fn register(self: &Arc<Self>, options: SqlQueryOptions) -> Result<RegisteredSqlQuery> {
553        validate_metadata("owner", options.owner.as_deref())?;
554        validate_metadata("session id", options.session_id.as_deref())?;
555        let id = match options.query_id {
556            Some(id) => id,
557            None => QueryId::random()?,
558        };
559        let deadline_base = Instant::now();
560        let deadline = options
561            .timeout
562            .map(|timeout| {
563                deadline_base.checked_add(timeout).ok_or_else(|| {
564                    MongrelQueryError::Core(mongreldb_core::MongrelError::InvalidArgument(
565                        "query timeout exceeds the monotonic clock range".into(),
566                    ))
567                })
568            })
569            .transpose()?;
570        let control = match options.parent_control {
571            Some(parent) => parent.child_with_deadline(deadline),
572            None => ExecutionControl::new(deadline),
573        };
574        let started_at = Instant::now();
575        let query = Arc::new(RegisteredQuery {
576            id,
577            owner: options.owner,
578            session_id: options.session_id,
579            deadline: control.deadline(),
580            control,
581            phase: AtomicU8::new(SqlQueryPhase::Queued as u8),
582            started_at,
583            operation: Mutex::new("UNKNOWN".into()),
584            sql_fingerprint: Mutex::new([0; 32]),
585            outcome: Mutex::new(QueryOutcomeState::default()),
586            completed_statements: AtomicUsize::new(0),
587            statement_index: AtomicUsize::new(0),
588            cancel_requested_at: Mutex::new(None),
589            trace: Mutex::new(QueryTrace::new(started_at)),
590        });
591        let mut state = self.state.lock();
592        self.prune_locked(&mut state);
593        if state.active.contains_key(&id)
594            || state.detailed.contains_key(&id)
595            || state.compact.contains_key(&id)
596        {
597            return Err(MongrelQueryError::QueryIdConflict { query_id: id });
598        }
599        if state.active.len() >= self.max_active {
600            state.active_rejections = state.active_rejections.saturating_add(1);
601            return Err(MongrelQueryError::QueryRegistryFull);
602        }
603        state.active.insert(id, Arc::clone(&query));
604        Ok(RegisteredSqlQuery {
605            registry: Arc::downgrade(self),
606            query,
607        })
608    }
609
610    pub fn cancel(&self, query_id: QueryId) -> CancelOutcome {
611        let query = {
612            let mut state = self.state.lock();
613            self.prune_locked(&mut state);
614            if let Some(query) = state.active.get(&query_id) {
615                Some(Arc::clone(query))
616            } else if state.detailed.contains_key(&query_id)
617                || state.compact.contains_key(&query_id)
618            {
619                return CancelOutcome::AlreadyFinished;
620            } else {
621                return CancelOutcome::NotFound;
622            }
623        };
624        match query {
625            Some(query) => query.request_cancel(CancellationReason::ClientRequest),
626            None => CancelOutcome::NotFound,
627        }
628    }
629
630    pub fn status(&self, query_id: QueryId) -> Option<QueryStatus> {
631        let mut state = self.state.lock();
632        self.prune_locked(&mut state);
633        state
634            .active
635            .get(&query_id)
636            .map(|query| query.status())
637            .or_else(|| {
638                state
639                    .detailed
640                    .get(&query_id)
641                    .map(|finished| finished.status.clone())
642            })
643    }
644
645    pub fn compact_finished_status(&self, query_id: QueryId) -> Option<CompactFinishedQuery> {
646        let mut state = self.state.lock();
647        self.prune_locked(&mut state);
648        state.compact.get(&query_id).cloned()
649    }
650
651    pub fn cancel_session(&self, session_id: &str, reason: CancellationReason) -> usize {
652        let queries = {
653            let state = self.state.lock();
654            state
655                .active
656                .values()
657                .filter(|query| query.session_id.as_deref() == Some(session_id))
658                .cloned()
659                .collect::<Vec<_>>()
660        };
661        let mut accepted = 0;
662        for query in queries {
663            if query.request_cancel(reason) == CancelOutcome::Accepted {
664                accepted += 1;
665            }
666        }
667        accepted
668    }
669
670    pub fn cancel_all(&self, reason: CancellationReason) -> usize {
671        let queries = self
672            .state
673            .lock()
674            .active
675            .values()
676            .cloned()
677            .collect::<Vec<_>>();
678        let mut accepted = 0;
679        for query in queries {
680            if query.request_cancel(reason) == CancelOutcome::Accepted {
681                accepted += 1;
682            }
683        }
684        accepted
685    }
686
687    pub fn active_count(&self) -> usize {
688        self.state.lock().active.len()
689    }
690
691    pub fn active_statuses(&self) -> Vec<QueryStatus> {
692        self.state
693            .lock()
694            .active
695            .values()
696            .map(|query| query.status())
697            .collect()
698    }
699
700    pub fn active_for_session(&self, session_id: &str) -> usize {
701        self.state
702            .lock()
703            .active
704            .values()
705            .filter(|query| query.session_id.as_deref() == Some(session_id))
706            .count()
707    }
708
709    pub fn queued_count(&self) -> usize {
710        self.state
711            .lock()
712            .active
713            .values()
714            .filter(|query| query.phase() == SqlQueryPhase::Queued)
715            .count()
716    }
717
718    pub fn entry_count(&self) -> usize {
719        let mut state = self.state.lock();
720        self.prune_locked(&mut state);
721        state.active.len() + state.detailed.len() + state.compact.len()
722    }
723
724    pub fn approximate_bytes(&self) -> usize {
725        let mut state = self.state.lock();
726        self.prune_locked(&mut state);
727        state.detailed_bytes
728            + state.compact_bytes
729            + state
730                .active
731                .len()
732                .saturating_mul(std::mem::size_of::<RegisteredQuery>())
733    }
734
735    pub fn finished_count(&self) -> usize {
736        let mut state = self.state.lock();
737        self.prune_locked(&mut state);
738        state.detailed.len() + state.compact.len()
739    }
740
741    pub fn stats(&self) -> QueryRegistryStats {
742        let mut state = self.state.lock();
743        self.prune_locked(&mut state);
744        let now = Instant::now();
745        QueryRegistryStats {
746            active: state.active.len(),
747            queued: state
748                .active
749                .values()
750                .filter(|query| query.phase() == SqlQueryPhase::Queued)
751                .count(),
752            detailed: state.detailed.len(),
753            compact: state.compact.len(),
754            detailed_bytes: state.detailed_bytes,
755            compact_bytes: state.compact_bytes,
756            demotions: state.demotions,
757            compact_evictions: state.compact_evictions,
758            active_rejections: state.active_rejections,
759            oldest_compact_age: state
760                .compact_lru
761                .front()
762                .and_then(|id| state.compact.get(id))
763                .map_or(Duration::ZERO, |entry| {
764                    now.saturating_duration_since(entry.finished_at)
765                }),
766        }
767    }
768
769    fn finish(&self, query: &Arc<RegisteredQuery>) {
770        debug_assert!(query.phase().is_terminal());
771        let status = query.status();
772        let approximate_bytes = std::mem::size_of::<FinishedQuery>()
773            + status.owner.as_ref().map_or(0, String::len)
774            + status.session_id.as_ref().map_or(0, String::len)
775            + status.operation.len()
776            + status
777                .terminal_error
778                .as_ref()
779                .map_or(0, |error| error.code.len());
780        let mut state = self.state.lock();
781        if state.active.remove(&query.id).is_none() {
782            return;
783        }
784        let finished_at = Instant::now();
785        if self.max_detailed > 0 && self.max_detailed_bytes > 0 {
786            state.detailed_bytes = state.detailed_bytes.saturating_add(approximate_bytes);
787            state.detailed_lru.push_back(query.id);
788            state.detailed.insert(
789                query.id,
790                FinishedQuery {
791                    status,
792                    finished_at,
793                    approximate_bytes,
794                },
795            );
796        } else {
797            self.insert_compact_locked(
798                &mut state,
799                CompactFinishedQuery::from_status(status, finished_at),
800            );
801        }
802        self.prune_locked(&mut state);
803    }
804
805    fn prune_locked(&self, state: &mut RegistryState) {
806        let now = Instant::now();
807        while let Some(query_id) = state.detailed_lru.front().copied() {
808            let Some(entry) = state.detailed.get(&query_id) else {
809                state.detailed_lru.pop_front();
810                continue;
811            };
812            let expired = now.saturating_duration_since(entry.finished_at) >= self.finished_ttl;
813            let over_limit = state.detailed.len() > self.max_detailed
814                || state.detailed_bytes > self.max_detailed_bytes;
815            if !expired && !over_limit {
816                break;
817            }
818            state.detailed_lru.pop_front();
819            if let Some(entry) = state.detailed.remove(&query_id) {
820                state.detailed_bytes = state.detailed_bytes.saturating_sub(entry.approximate_bytes);
821                if !expired {
822                    state.demotions = state.demotions.saturating_add(1);
823                    self.insert_compact_locked(
824                        state,
825                        CompactFinishedQuery::from_status(entry.status, entry.finished_at),
826                    );
827                }
828            }
829        }
830        while let Some(query_id) = state.compact_lru.front().copied() {
831            let Some(entry) = state.compact.get(&query_id) else {
832                state.compact_lru.pop_front();
833                continue;
834            };
835            let expired = now.saturating_duration_since(entry.finished_at) >= self.finished_ttl;
836            let over_limit = state.compact.len() > self.max_compact
837                || state.compact_bytes > self.max_compact_bytes;
838            if !expired && !over_limit {
839                break;
840            }
841            state.compact_lru.pop_front();
842            if let Some(entry) = state.compact.remove(&query_id) {
843                state.compact_bytes = state.compact_bytes.saturating_sub(entry.approximate_bytes);
844                if !expired {
845                    state.compact_evictions = state.compact_evictions.saturating_add(1);
846                }
847            }
848        }
849    }
850
851    fn insert_compact_locked(&self, state: &mut RegistryState, entry: CompactFinishedQuery) {
852        if self.max_compact == 0 || self.max_compact_bytes == 0 {
853            state.compact_evictions = state.compact_evictions.saturating_add(1);
854            return;
855        }
856        state.compact_bytes = state.compact_bytes.saturating_add(entry.approximate_bytes);
857        state.compact_lru.push_back(entry.query_id);
858        state.compact.insert(entry.query_id, entry);
859    }
860}
861
862impl RegisteredQuery {
863    fn request_cancel(&self, reason: CancellationReason) -> CancelOutcome {
864        loop {
865            let phase = self.phase();
866            match phase {
867                SqlQueryPhase::Queued
868                | SqlQueryPhase::Planning
869                | SqlQueryPhase::Executing
870                | SqlQueryPhase::Streaming
871                | SqlQueryPhase::Serializing => {
872                    if self
873                        .phase
874                        .compare_exchange(
875                            phase as u8,
876                            SqlQueryPhase::Cancelling as u8,
877                            Ordering::AcqRel,
878                            Ordering::Acquire,
879                        )
880                        .is_ok()
881                    {
882                        self.control.cancel(reason);
883                        *self.cancel_requested_at.lock() = Some(Instant::now());
884                        let mut trace = self.trace.lock();
885                        trace.transition(phase);
886                        trace.cancel_requested_phase = Some(phase);
887                        if trace.commit_fence_outcome == CommitFenceOutcome::NotReached {
888                            trace.commit_fence_outcome = CommitFenceOutcome::CancelWon;
889                        }
890                        return CancelOutcome::Accepted;
891                    }
892                }
893                SqlQueryPhase::Cancelling | SqlQueryPhase::Cancelled => {
894                    return CancelOutcome::AlreadyCancelling;
895                }
896                SqlQueryPhase::CommitCritical => return CancelOutcome::TooLate,
897                SqlQueryPhase::Completed | SqlQueryPhase::Failed => {
898                    return CancelOutcome::AlreadyFinished;
899                }
900            }
901        }
902    }
903}
904
905#[derive(Debug, Clone)]
906pub struct RegisteredSqlQuery {
907    registry: Weak<SqlQueryRegistry>,
908    query: Arc<RegisteredQuery>,
909}
910
911enum TerminalFailure {
912    Error {
913        code: String,
914        category: QueryTerminalErrorCategory,
915    },
916    Serialization(String),
917}
918
919/// Query-specific state attached to a fresh DataFusion `TaskContext` for each
920/// execution. Reusable logical and physical plans never own this value.
921pub(crate) struct SqlTaskContext {
922    query: RegisteredSqlQuery,
923    test_hook: Option<SqlTestHook>,
924}
925
926impl SqlTaskContext {
927    pub(crate) fn new(query: RegisteredSqlQuery, test_hook: Option<SqlTestHook>) -> Self {
928        Self { query, test_hook }
929    }
930
931    pub(crate) fn query(&self) -> &RegisteredSqlQuery {
932        &self.query
933    }
934
935    pub(crate) fn test_hook(&self) -> Option<&SqlTestHook> {
936        self.test_hook.as_ref()
937    }
938}
939
940impl RegisteredSqlQuery {
941    pub fn id(&self) -> QueryId {
942        self.query.id
943    }
944
945    pub fn control(&self) -> &ExecutionControl {
946        &self.query.control
947    }
948
949    pub fn phase(&self) -> SqlQueryPhase {
950        self.query.phase()
951    }
952
953    pub fn status(&self) -> QueryStatus {
954        self.query.status()
955    }
956
957    pub fn set_sql_metadata(&self, sql: &str) {
958        let operation = sql
959            .split_whitespace()
960            .next()
961            .unwrap_or("UNKNOWN")
962            .chars()
963            .take(64)
964            .collect::<String>()
965            .to_ascii_uppercase();
966        *self.query.operation.lock() = operation;
967        *self.query.sql_fingerprint.lock() = crate::normalized_sql_fingerprint(sql);
968    }
969
970    pub fn transition(&self, expected: SqlQueryPhase, next: SqlQueryPhase) -> Result<()> {
971        self.query
972            .phase
973            .compare_exchange(
974                expected as u8,
975                next as u8,
976                Ordering::AcqRel,
977                Ordering::Acquire,
978            )
979            .map(|_| self.query.record_transition(expected))
980            .map_err(|actual| {
981                let actual = SqlQueryPhase::from_u8(actual);
982                if actual == SqlQueryPhase::Cancelling {
983                    self.cancellation_error()
984                } else {
985                    MongrelQueryError::InvalidQueryState(format!(
986                        "query {} expected {expected:?}, found {actual:?}",
987                        self.id()
988                    ))
989                }
990            })
991    }
992
993    pub fn enter_commit_critical(&self) -> Result<()> {
994        if let Err(error) = self.checkpoint() {
995            self.query.trace.lock().commit_fence_outcome = CommitFenceOutcome::CancelWon;
996            return Err(error);
997        }
998        self.transition(SqlQueryPhase::Executing, SqlQueryPhase::CommitCritical)?;
999        self.query.trace.lock().commit_fence_outcome = CommitFenceOutcome::CommitWon;
1000        Ok(())
1001    }
1002
1003    pub fn exit_commit_critical(&self) -> Result<()> {
1004        self.transition(SqlQueryPhase::CommitCritical, SqlQueryPhase::Executing)
1005    }
1006
1007    pub fn begin_serialization(&self) -> Result<()> {
1008        self.checkpoint()?;
1009        match self.phase() {
1010            SqlQueryPhase::Executing => {
1011                self.transition(SqlQueryPhase::Executing, SqlQueryPhase::Serializing)
1012            }
1013            SqlQueryPhase::Streaming => {
1014                self.transition(SqlQueryPhase::Streaming, SqlQueryPhase::Serializing)
1015            }
1016            phase => Err(MongrelQueryError::InvalidQueryState(format!(
1017                "query {} cannot serialize from {phase:?}",
1018                self.id()
1019            ))),
1020        }?;
1021        self.query.outcome.lock().serialization = SerializationOutcome::InProgress;
1022        Ok(())
1023    }
1024
1025    /// Records one statement whose effects are durably published.
1026    pub fn record_commit(&self, statement_index: usize, epoch: u64) {
1027        self.query
1028            .outcome
1029            .lock()
1030            .durable
1031            .record_commit(statement_index, Some(epoch));
1032    }
1033
1034    pub fn durable_outcome(&self) -> DurableOutcome {
1035        self.query.outcome.lock().durable.clone()
1036    }
1037
1038    pub fn commit_outcome_error(&self, message: impl Into<String>) -> MongrelQueryError {
1039        let durable = self.durable_outcome();
1040        MongrelQueryError::CommitOutcome {
1041            query_id: self.id(),
1042            committed: durable.committed,
1043            committed_statements: durable.committed_statements,
1044            last_commit_epoch: durable.last_commit_epoch,
1045            first_commit_statement_index: durable.first_commit_statement_index,
1046            last_commit_statement_index: durable.last_commit_statement_index,
1047            completed_statements: self.query.completed_statements.load(Ordering::Acquire),
1048            statement_index: self.query.statement_index.load(Ordering::Acquire),
1049            message: message.into(),
1050        }
1051    }
1052
1053    pub fn result_limit_error(&self, message: impl Into<String>) -> MongrelQueryError {
1054        let durable = self.durable_outcome();
1055        MongrelQueryError::ResultLimitExceeded {
1056            query_id: self.id(),
1057            committed: durable.committed,
1058            committed_statements: durable.committed_statements,
1059            last_commit_epoch: durable.last_commit_epoch,
1060            first_commit_statement_index: durable.first_commit_statement_index,
1061            last_commit_statement_index: durable.last_commit_statement_index,
1062            completed_statements: self.query.completed_statements.load(Ordering::Acquire),
1063            statement_index: self.query.statement_index.load(Ordering::Acquire),
1064            message: message.into(),
1065        }
1066    }
1067
1068    /// A fenced mutation failed without an exact durable receipt. Never claim
1069    /// `committed=false`: storage may have changed before the error surfaced.
1070    pub fn outcome_unknown_error(&self, message: impl Into<String>) -> MongrelQueryError {
1071        let message = message.into();
1072        self.mark_outcome_unknown();
1073        MongrelQueryError::OutcomeUnknown {
1074            query_id: self.id(),
1075            message,
1076        }
1077    }
1078
1079    /// Restore a terminal receipt onto a newly registered idempotent replay so
1080    /// status polling for the replay query ID reports the same durable result.
1081    #[allow(clippy::too_many_arguments)]
1082    pub fn restore_replayed_outcome(
1083        &self,
1084        durable: DurableOutcome,
1085        completed_statements: usize,
1086        statement_index: usize,
1087        serialization: SerializationOutcome,
1088        terminal_error: Option<QueryTerminalError>,
1089        terminal_state: QueryTerminalState,
1090        cancellation_reason: CancellationReason,
1091        phase: SqlQueryPhase,
1092    ) {
1093        let mut outcome = self.query.outcome.lock();
1094        outcome.durable = durable;
1095        outcome.serialization = serialization;
1096        outcome.terminal_error = terminal_error;
1097        outcome.terminal_state_override = Some(terminal_state);
1098        outcome.cancellation_reason_override = Some(cancellation_reason);
1099        outcome.phase_override = Some(phase);
1100        self.query
1101            .completed_statements
1102            .store(completed_statements, Ordering::Release);
1103        self.query
1104            .statement_index
1105            .store(statement_index, Ordering::Release);
1106    }
1107
1108    /// The daemon found a durable idempotency intent without a receipt. The
1109    /// previous write may have committed, so `committed=false` is not known.
1110    pub fn mark_outcome_unknown(&self) {
1111        let mut outcome = self.query.outcome.lock();
1112        outcome.terminal_state_override = Some(QueryTerminalState::OutcomeUnknown);
1113        outcome.terminal_error = Some(QueryTerminalError {
1114            code: "QUERY_OUTCOME_UNKNOWN".into(),
1115            category: QueryTerminalErrorCategory::Execution,
1116        });
1117    }
1118
1119    pub fn record_terminal_error(
1120        &self,
1121        code: impl Into<String>,
1122        category: QueryTerminalErrorCategory,
1123    ) {
1124        Self::set_terminal_error(&mut self.query.outcome.lock(), code.into(), category);
1125    }
1126
1127    pub fn record_serialization_failure(&self, code: impl Into<String>) {
1128        let mut outcome = self.query.outcome.lock();
1129        Self::set_serialization_failure(&mut outcome, code.into());
1130    }
1131
1132    fn set_terminal_error(
1133        outcome: &mut QueryOutcomeState,
1134        code: String,
1135        category: QueryTerminalErrorCategory,
1136    ) {
1137        outcome.terminal_error = Some(QueryTerminalError { code, category });
1138    }
1139
1140    fn set_serialization_failure(outcome: &mut QueryOutcomeState, code: String) {
1141        let code = if outcome.durable.committed && code.starts_with("SERIALIZATION_") {
1142            "SERIALIZATION_FAILED_AFTER_COMMIT".into()
1143        } else {
1144            code
1145        };
1146        outcome.serialization = SerializationOutcome::Failed;
1147        outcome.terminal_error = Some(QueryTerminalError {
1148            code,
1149            category: QueryTerminalErrorCategory::Serialization,
1150        });
1151    }
1152
1153    pub fn begin_statement(&self, index: usize) {
1154        self.query.statement_index.store(index, Ordering::Release);
1155    }
1156
1157    pub fn complete_statement(&self, index: usize) {
1158        self.query
1159            .completed_statements
1160            .store(index.saturating_add(1), Ordering::Release);
1161    }
1162
1163    pub fn complete_current_statement(&self) {
1164        let index = self.query.statement_index.load(Ordering::Acquire);
1165        self.complete_statement(index);
1166    }
1167
1168    pub fn request_cancel(&self, reason: CancellationReason) -> CancelOutcome {
1169        self.query.request_cancel(reason)
1170    }
1171
1172    pub fn checkpoint(&self) -> Result<()> {
1173        let durable = self.durable_outcome();
1174        let result = self
1175            .query
1176            .control
1177            .checkpoint()
1178            .map_err(|error| match error {
1179                mongreldb_core::MongrelError::DeadlineExceeded => {
1180                    MongrelQueryError::DeadlineExceeded {
1181                        query_id: self.id(),
1182                        timeout_ms: self.query.deadline.map(|deadline| {
1183                            deadline
1184                                .saturating_duration_since(self.query.started_at)
1185                                .as_millis()
1186                                .min(u128::from(u64::MAX)) as u64
1187                        }),
1188                        committed: durable.committed,
1189                        committed_statements: durable.committed_statements,
1190                        last_commit_epoch: durable.last_commit_epoch,
1191                        first_commit_statement_index: durable.first_commit_statement_index,
1192                        last_commit_statement_index: durable.last_commit_statement_index,
1193                        completed_statements: self
1194                            .query
1195                            .completed_statements
1196                            .load(Ordering::Acquire),
1197                        cancelled_statement_index: self
1198                            .query
1199                            .statement_index
1200                            .load(Ordering::Acquire),
1201                    }
1202                }
1203                mongreldb_core::MongrelError::Cancelled => self.cancellation_error(),
1204                other => MongrelQueryError::Core(other),
1205            });
1206        if result.is_err() && self.query.control.is_cancelled() {
1207            let mut trace = self.query.trace.lock();
1208            trace.cancel_observed_phase = trace.cancel_requested_phase.or(Some(self.phase()));
1209        }
1210        result
1211    }
1212
1213    pub fn complete(&self) -> Result<()> {
1214        self.try_complete()
1215    }
1216
1217    pub fn try_complete(&self) -> Result<()> {
1218        if self.phase() == SqlQueryPhase::Completed {
1219            self.finish(SqlQueryPhase::Completed, None);
1220            return Ok(());
1221        }
1222        if let Err(error) = self.checkpoint() {
1223            self.fail();
1224            return Err(error);
1225        }
1226        let phase = self.finish(SqlQueryPhase::Completed, None);
1227        match phase {
1228            SqlQueryPhase::Completed => Ok(()),
1229            SqlQueryPhase::Cancelled => Err(self.cancellation_error()),
1230            other => Err(MongrelQueryError::InvalidQueryState(format!(
1231                "query {} completed from terminal phase {other:?}",
1232                self.id()
1233            ))),
1234        }
1235    }
1236
1237    pub fn fail(&self) {
1238        self.finish(SqlQueryPhase::Failed, None);
1239    }
1240
1241    pub fn fail_result_limit(&self) {
1242        self.fail_with_error(
1243            "RESULT_LIMIT_EXCEEDED",
1244            QueryTerminalErrorCategory::ResultLimit,
1245        );
1246    }
1247
1248    pub fn fail_with_error(&self, code: impl Into<String>, category: QueryTerminalErrorCategory) {
1249        self.finish(
1250            SqlQueryPhase::Failed,
1251            Some(TerminalFailure::Error {
1252                code: code.into(),
1253                category,
1254            }),
1255        );
1256    }
1257
1258    pub fn fail_serialization(&self) {
1259        self.finish(
1260            SqlQueryPhase::Failed,
1261            Some(TerminalFailure::Serialization(
1262                "SERIALIZATION_FAILED".into(),
1263            )),
1264        );
1265    }
1266
1267    fn finish(
1268        &self,
1269        requested: SqlQueryPhase,
1270        mut failure: Option<TerminalFailure>,
1271    ) -> SqlQueryPhase {
1272        debug_assert!(matches!(
1273            requested,
1274            SqlQueryPhase::Completed | SqlQueryPhase::Failed
1275        ));
1276        let explicit_failure = failure.is_some();
1277        let mut outcome = self.query.outcome.lock();
1278        let phase = loop {
1279            let current = self.phase();
1280            if current.is_terminal() {
1281                break current;
1282            }
1283            let terminal = if current == SqlQueryPhase::Cancelling
1284                || (!explicit_failure && self.query.control.is_cancelled())
1285            {
1286                while !self.query.control.is_cancelled() {
1287                    std::hint::spin_loop();
1288                }
1289                SqlQueryPhase::Cancelled
1290            } else {
1291                requested
1292            };
1293            if self
1294                .query
1295                .phase
1296                .compare_exchange(
1297                    current as u8,
1298                    terminal as u8,
1299                    Ordering::AcqRel,
1300                    Ordering::Acquire,
1301                )
1302                .is_ok()
1303            {
1304                self.query.record_transition(current);
1305                if terminal == SqlQueryPhase::Cancelled && outcome.phase_override.is_some() {
1306                    outcome.phase_override = None;
1307                    outcome.terminal_state_override = None;
1308                    outcome.cancellation_reason_override = None;
1309                    outcome.terminal_error = None;
1310                    outcome.serialization = SerializationOutcome::Failed;
1311                }
1312                if terminal == SqlQueryPhase::Failed {
1313                    match failure.take() {
1314                        Some(TerminalFailure::Error { code, category }) => {
1315                            Self::set_terminal_error(&mut outcome, code, category);
1316                        }
1317                        Some(TerminalFailure::Serialization(code)) => {
1318                            Self::set_serialization_failure(&mut outcome, code);
1319                        }
1320                        None => {}
1321                    }
1322                }
1323                Self::finalize_outcome(&mut outcome, terminal, self.query.control.reason());
1324                break terminal;
1325            }
1326        };
1327        Self::finalize_outcome(&mut outcome, phase, self.query.control.reason());
1328        drop(outcome);
1329        if let Some(registry) = self.registry.upgrade() {
1330            registry.finish(&self.query);
1331        }
1332        phase
1333    }
1334
1335    fn finalize_outcome(
1336        outcome: &mut QueryOutcomeState,
1337        phase: SqlQueryPhase,
1338        reason: CancellationReason,
1339    ) {
1340        match (phase, outcome.serialization) {
1341            (SqlQueryPhase::Completed, SerializationOutcome::InProgress) => {
1342                outcome.serialization = SerializationOutcome::Succeeded;
1343            }
1344            (
1345                SqlQueryPhase::Failed | SqlQueryPhase::Cancelled,
1346                SerializationOutcome::InProgress,
1347            ) => {
1348                outcome.serialization = SerializationOutcome::Failed;
1349            }
1350            _ => {}
1351        }
1352        if phase == SqlQueryPhase::Completed || outcome.terminal_error.is_some() {
1353            return;
1354        }
1355        let committed = outcome.durable.committed;
1356        let (code, category) = match reason {
1357            CancellationReason::Deadline => (
1358                if committed {
1359                    "DEADLINE_AFTER_COMMIT"
1360                } else {
1361                    "DEADLINE_EXCEEDED"
1362                },
1363                QueryTerminalErrorCategory::Deadline,
1364            ),
1365            CancellationReason::None if outcome.serialization == SerializationOutcome::Failed => (
1366                if committed {
1367                    "SERIALIZATION_FAILED_AFTER_COMMIT"
1368                } else {
1369                    "SERIALIZATION_FAILED"
1370                },
1371                QueryTerminalErrorCategory::Serialization,
1372            ),
1373            CancellationReason::None => ("QUERY_FAILED", QueryTerminalErrorCategory::Execution),
1374            _ => (
1375                if committed {
1376                    "QUERY_CANCELLED_AFTER_COMMIT"
1377                } else {
1378                    "QUERY_CANCELLED"
1379                },
1380                QueryTerminalErrorCategory::Cancellation,
1381            ),
1382        };
1383        outcome.terminal_error = Some(QueryTerminalError {
1384            code: code.into(),
1385            category,
1386        });
1387    }
1388
1389    fn cancellation_error(&self) -> MongrelQueryError {
1390        let durable = self.durable_outcome();
1391        let mut reason = self.query.control.reason();
1392        while reason == CancellationReason::None && self.query.phase() == SqlQueryPhase::Cancelling
1393        {
1394            std::thread::yield_now();
1395            reason = self.query.control.reason();
1396        }
1397        match reason {
1398            CancellationReason::Deadline => MongrelQueryError::DeadlineExceeded {
1399                query_id: self.id(),
1400                timeout_ms: self.query.deadline.map(|deadline| {
1401                    deadline
1402                        .saturating_duration_since(self.query.started_at)
1403                        .as_millis()
1404                        .min(u128::from(u64::MAX)) as u64
1405                }),
1406                committed: durable.committed,
1407                committed_statements: durable.committed_statements,
1408                last_commit_epoch: durable.last_commit_epoch,
1409                first_commit_statement_index: durable.first_commit_statement_index,
1410                last_commit_statement_index: durable.last_commit_statement_index,
1411                completed_statements: self.query.completed_statements.load(Ordering::Acquire),
1412                cancelled_statement_index: self.query.statement_index.load(Ordering::Acquire),
1413            },
1414            reason => MongrelQueryError::QueryCancelled {
1415                query_id: self.id(),
1416                reason,
1417                committed: durable.committed,
1418                committed_statements: durable.committed_statements,
1419                last_commit_epoch: durable.last_commit_epoch,
1420                first_commit_statement_index: durable.first_commit_statement_index,
1421                last_commit_statement_index: durable.last_commit_statement_index,
1422                completed_statements: self.query.completed_statements.load(Ordering::Acquire),
1423                cancelled_statement_index: self.query.statement_index.load(Ordering::Acquire),
1424            },
1425        }
1426    }
1427}
1428
1429pub struct RegisteredQueryGuard {
1430    query: Option<RegisteredSqlQuery>,
1431}
1432
1433impl RegisteredQueryGuard {
1434    pub fn new(query: RegisteredSqlQuery) -> Self {
1435        Self { query: Some(query) }
1436    }
1437
1438    pub fn query(&self) -> &RegisteredSqlQuery {
1439        self.query
1440            .as_ref()
1441            .expect("registered query guard consumed")
1442    }
1443
1444    pub fn complete(self) -> Result<()> {
1445        self.try_complete()
1446    }
1447
1448    pub fn try_complete(mut self) -> Result<()> {
1449        if let Some(query) = self.query.take() {
1450            query.try_complete()
1451        } else {
1452            Ok(())
1453        }
1454    }
1455
1456    pub fn fail(mut self) {
1457        if let Some(query) = self.query.take() {
1458            query.fail();
1459        }
1460    }
1461
1462    pub fn fail_result_limit(mut self) {
1463        if let Some(query) = self.query.take() {
1464            query.fail_result_limit();
1465        }
1466    }
1467
1468    pub fn fail_with_error(
1469        mut self,
1470        code: impl Into<String>,
1471        category: QueryTerminalErrorCategory,
1472    ) {
1473        if let Some(query) = self.query.take() {
1474            query.fail_with_error(code, category);
1475        }
1476    }
1477
1478    pub fn fail_serialization(mut self) {
1479        if let Some(query) = self.query.take() {
1480            query.fail_serialization();
1481        }
1482    }
1483
1484    pub fn into_query(mut self) -> RegisteredSqlQuery {
1485        self.query.take().expect("registered query guard consumed")
1486    }
1487}
1488
1489impl Drop for RegisteredQueryGuard {
1490    fn drop(&mut self) {
1491        if let Some(query) = self.query.take() {
1492            query.fail();
1493        }
1494    }
1495}
1496
1497fn validate_metadata(name: &str, value: Option<&str>) -> Result<()> {
1498    if value.is_some_and(|value| value.len() > MAX_METADATA_BYTES) {
1499        return Err(MongrelQueryError::Core(
1500            mongreldb_core::MongrelError::InvalidArgument(format!(
1501                "{name} exceeds {MAX_METADATA_BYTES} bytes"
1502            )),
1503        ));
1504    }
1505    Ok(())
1506}
1507
1508#[cfg(test)]
1509mod tests {
1510    use super::*;
1511
1512    #[test]
1513    fn query_ids_are_random_strict_and_round_trip() {
1514        let first = QueryId::random().unwrap();
1515        let second = QueryId::random().unwrap();
1516        assert_ne!(first, second);
1517        assert_eq!(first.to_string().parse::<QueryId>().unwrap(), first);
1518        assert!("abc".parse::<QueryId>().is_err());
1519        assert!("0000000000000000000000000000000z"
1520            .parse::<QueryId>()
1521            .is_err());
1522    }
1523
1524    #[test]
1525    fn unrepresentable_timeout_is_rejected_without_panicking() {
1526        let registry = Arc::new(SqlQueryRegistry::default());
1527        assert!(matches!(
1528            registry.register(SqlQueryOptions {
1529                timeout: Some(Duration::MAX),
1530                ..SqlQueryOptions::default()
1531            }),
1532            Err(MongrelQueryError::Core(
1533                mongreldb_core::MongrelError::InvalidArgument(_)
1534            ))
1535        ));
1536        assert_eq!(registry.active_count(), 0);
1537    }
1538
1539    #[test]
1540    fn active_and_retained_query_ids_are_rejected() {
1541        let registry = Arc::new(SqlQueryRegistry::new(1, 1, 1024, Duration::from_secs(60)));
1542        let id = QueryId::random().unwrap();
1543        let query = registry
1544            .register(SqlQueryOptions {
1545                query_id: Some(id),
1546                ..SqlQueryOptions::default()
1547            })
1548            .unwrap();
1549        assert!(matches!(
1550            registry.register(SqlQueryOptions {
1551                query_id: Some(id),
1552                ..SqlQueryOptions::default()
1553            }),
1554            Err(MongrelQueryError::QueryIdConflict { .. })
1555        ));
1556        query.complete().unwrap();
1557        assert_eq!(registry.active_count(), 0);
1558        assert_eq!(registry.finished_count(), 1);
1559        assert!(matches!(
1560            registry.register(SqlQueryOptions {
1561                query_id: Some(id),
1562                ..SqlQueryOptions::default()
1563            }),
1564            Err(MongrelQueryError::QueryIdConflict { .. })
1565        ));
1566        assert_eq!(registry.cancel(id), CancelOutcome::AlreadyFinished);
1567    }
1568
1569    #[test]
1570    fn query_id_can_be_reused_after_its_tombstone_expires() {
1571        let registry = Arc::new(SqlQueryRegistry::new(1, 1, 1024, Duration::ZERO));
1572        let id = QueryId::random().unwrap();
1573        registry
1574            .register(SqlQueryOptions {
1575                query_id: Some(id),
1576                ..SqlQueryOptions::default()
1577            })
1578            .unwrap()
1579            .complete()
1580            .unwrap();
1581
1582        let replacement = registry
1583            .register(SqlQueryOptions {
1584                query_id: Some(id),
1585                ..SqlQueryOptions::default()
1586            })
1587            .unwrap();
1588        assert_eq!(registry.status(id).unwrap().phase, SqlQueryPhase::Queued);
1589        replacement.complete().unwrap();
1590    }
1591
1592    #[test]
1593    fn status_capacity_eviction_does_not_release_query_id() {
1594        for (max_finished, max_finished_bytes) in [(1, usize::MAX), (10, 1)] {
1595            let owner = "o".repeat(MAX_METADATA_BYTES);
1596            let session_id = "s".repeat(MAX_METADATA_BYTES);
1597            let registry = Arc::new(SqlQueryRegistry::new(
1598                1,
1599                max_finished,
1600                max_finished_bytes,
1601                Duration::from_secs(60),
1602            ));
1603            let first_id = QueryId::random().unwrap();
1604            registry
1605                .register(SqlQueryOptions {
1606                    query_id: Some(first_id),
1607                    owner: Some(owner.clone()),
1608                    session_id: Some(session_id.clone()),
1609                    ..SqlQueryOptions::default()
1610                })
1611                .unwrap()
1612                .complete()
1613                .unwrap();
1614            registry
1615                .register(SqlQueryOptions::default())
1616                .unwrap()
1617                .complete()
1618                .unwrap();
1619
1620            assert!(registry.status(first_id).is_none());
1621            let compact = registry.compact_finished_status(first_id).unwrap();
1622            assert_eq!(compact.owner, Some(owner));
1623            assert_eq!(compact.session_id, Some(session_id));
1624            assert_eq!(registry.cancel(first_id), CancelOutcome::AlreadyFinished);
1625            assert!(matches!(
1626                registry.register(SqlQueryOptions {
1627                    query_id: Some(first_id),
1628                    ..SqlQueryOptions::default()
1629                }),
1630                Err(MongrelQueryError::QueryIdConflict { .. })
1631            ));
1632        }
1633    }
1634
1635    #[test]
1636    fn compact_overflow_evicts_instead_of_rejecting_new_work() {
1637        let registry = Arc::new(SqlQueryRegistry::new_with_limits(
1638            1,
1639            0,
1640            0,
1641            1,
1642            usize::MAX,
1643            Duration::from_secs(60),
1644        ));
1645        let first_id = QueryId::random().unwrap();
1646        registry
1647            .register(SqlQueryOptions {
1648                query_id: Some(first_id),
1649                ..SqlQueryOptions::default()
1650            })
1651            .unwrap()
1652            .complete()
1653            .unwrap();
1654        registry
1655            .register(SqlQueryOptions::default())
1656            .unwrap()
1657            .complete()
1658            .unwrap();
1659
1660        assert_eq!(registry.finished_count(), 1);
1661        assert_eq!(registry.cancel(first_id), CancelOutcome::NotFound);
1662        let replacement = registry
1663            .register(SqlQueryOptions {
1664                query_id: Some(first_id),
1665                ..SqlQueryOptions::default()
1666            })
1667            .unwrap();
1668        replacement.complete().unwrap();
1669        assert_eq!(registry.stats().compact_evictions, 2);
1670    }
1671
1672    #[test]
1673    fn compact_identity_accounting_covers_maximum_metadata() {
1674        let registry = Arc::new(SqlQueryRegistry::new(1, 0, 0, Duration::from_secs(60)));
1675        let owner = "o".repeat(MAX_METADATA_BYTES);
1676        let session_id = "s".repeat(MAX_METADATA_BYTES);
1677        let query_id = QueryId::random().unwrap();
1678        registry
1679            .register(SqlQueryOptions {
1680                query_id: Some(query_id),
1681                owner: Some(owner.clone()),
1682                session_id: Some(session_id.clone()),
1683                ..SqlQueryOptions::default()
1684            })
1685            .unwrap()
1686            .complete()
1687            .unwrap();
1688
1689        let compact = registry.compact_finished_status(query_id).unwrap();
1690        assert_eq!(compact.owner, Some(owner));
1691        assert_eq!(compact.session_id, Some(session_id));
1692        assert_eq!(registry.approximate_bytes(), registry.stats().compact_bytes);
1693    }
1694
1695    #[test]
1696    fn ten_thousand_completed_queries_never_consume_active_capacity() {
1697        let registry = Arc::new(SqlQueryRegistry::new_with_limits(
1698            1,
1699            2,
1700            4096,
1701            10_000,
1702            8 * 1024 * 1024,
1703            Duration::from_secs(60),
1704        ));
1705        for _ in 0..10_000 {
1706            registry
1707                .register(SqlQueryOptions::default())
1708                .unwrap()
1709                .complete()
1710                .unwrap();
1711        }
1712        let stats = registry.stats();
1713        assert_eq!(stats.active, 0);
1714        assert_eq!(stats.detailed, 2);
1715        assert_eq!(stats.compact, 9_998);
1716        assert_eq!(stats.active_rejections, 0);
1717        assert!(stats.detailed_bytes <= 4096);
1718        assert!(stats.compact_bytes <= 8 * 1024 * 1024);
1719    }
1720
1721    #[test]
1722    #[ignore = "release qualification characterization; set MONGRELDB_REGISTRY_CHARACTERIZATION_SECONDS"]
1723    fn registry_high_qps_characterization() {
1724        let seconds = std::env::var("MONGRELDB_REGISTRY_CHARACTERIZATION_SECONDS")
1725            .ok()
1726            .and_then(|value| value.parse::<u64>().ok())
1727            .unwrap_or(300);
1728        for rate in [100_u64, 500, 1_000] {
1729            let registry = Arc::new(SqlQueryRegistry::new_with_limits(
1730                1_024,
1731                2_048,
1732                8 * 1024 * 1024,
1733                100_000,
1734                32 * 1024 * 1024,
1735                Duration::from_secs(60),
1736            ));
1737            let started = Instant::now();
1738            for second in 0..seconds {
1739                let deadline = started + Duration::from_secs(second + 1);
1740                for _ in 0..rate {
1741                    registry
1742                        .register(SqlQueryOptions::default())
1743                        .unwrap()
1744                        .complete()
1745                        .unwrap();
1746                }
1747                std::thread::sleep(deadline.saturating_duration_since(Instant::now()));
1748            }
1749            let stats = registry.stats();
1750            assert_eq!(stats.active, 0);
1751            assert_eq!(stats.active_rejections, 0);
1752            assert!(stats.detailed <= 2_048);
1753            assert!(stats.detailed_bytes <= 8 * 1024 * 1024);
1754            assert!(stats.compact <= 100_000);
1755            assert!(stats.compact_bytes <= 32 * 1024 * 1024);
1756            eprintln!(
1757                "registry characterization: rate={rate} qps seconds={seconds} operations={} detailed={} compact={} detailed_bytes={} compact_bytes={} demotions={} compact_evictions={} active_rejections={}",
1758                rate * seconds,
1759                stats.detailed,
1760                stats.compact,
1761                stats.detailed_bytes,
1762                stats.compact_bytes,
1763                stats.demotions,
1764                stats.compact_evictions,
1765                stats.active_rejections,
1766            );
1767        }
1768    }
1769
1770    #[test]
1771    fn cancel_and_commit_fence_have_one_winner() {
1772        for cancel_first in [true, false] {
1773            let registry = Arc::new(SqlQueryRegistry::default());
1774            let query = registry.register(SqlQueryOptions::default()).unwrap();
1775            query
1776                .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1777                .unwrap();
1778            if cancel_first {
1779                assert_eq!(
1780                    query.request_cancel(CancellationReason::ClientRequest),
1781                    CancelOutcome::Accepted
1782                );
1783                assert!(query.enter_commit_critical().is_err());
1784                let status = query.status();
1785                assert_eq!(
1786                    status.cancel_requested_phase,
1787                    Some(SqlQueryPhase::Executing)
1788                );
1789                assert_eq!(status.cancel_observed_phase, Some(SqlQueryPhase::Executing));
1790                assert_eq!(status.commit_fence_outcome, CommitFenceOutcome::CancelWon);
1791            } else {
1792                query.enter_commit_critical().unwrap();
1793                assert_eq!(
1794                    query.request_cancel(CancellationReason::ClientRequest),
1795                    CancelOutcome::TooLate
1796                );
1797                query.record_commit(0, 7);
1798                query.complete().unwrap();
1799                let status = registry.status(query.id()).unwrap();
1800                assert!(status.committed);
1801                assert_eq!(status.commit_fence_outcome, CommitFenceOutcome::CommitWon);
1802            }
1803        }
1804    }
1805
1806    #[test]
1807    fn cancel_and_terminal_completion_have_one_winner() {
1808        for _ in 0..100 {
1809            let registry = Arc::new(SqlQueryRegistry::default());
1810            let query = registry.register(SqlQueryOptions::default()).unwrap();
1811            query
1812                .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1813                .unwrap();
1814            query.begin_serialization().unwrap();
1815
1816            let barrier = Arc::new(std::sync::Barrier::new(3));
1817            let cancel_query = query.clone();
1818            let cancel_barrier = Arc::clone(&barrier);
1819            let cancel = std::thread::spawn(move || {
1820                cancel_barrier.wait();
1821                cancel_query.request_cancel(CancellationReason::ClientRequest)
1822            });
1823            let complete_query = query.clone();
1824            let complete_barrier = Arc::clone(&barrier);
1825            let complete = std::thread::spawn(move || {
1826                complete_barrier.wait();
1827                complete_query.try_complete()
1828            });
1829            barrier.wait();
1830
1831            let cancel = cancel.join().unwrap();
1832            let complete = complete.join().unwrap();
1833            let status = registry.status(query.id()).unwrap();
1834            match cancel {
1835                CancelOutcome::Accepted => {
1836                    assert!(matches!(
1837                        complete,
1838                        Err(MongrelQueryError::QueryCancelled { .. })
1839                    ));
1840                    assert_eq!(status.phase, SqlQueryPhase::Cancelled);
1841                }
1842                CancelOutcome::AlreadyFinished => {
1843                    complete.unwrap();
1844                    assert_eq!(status.phase, SqlQueryPhase::Completed);
1845                }
1846                other => panic!("unexpected cancel outcome {other:?}"),
1847            }
1848        }
1849    }
1850
1851    #[test]
1852    fn externally_claimed_completion_is_finalized_and_retained() {
1853        let registry = Arc::new(SqlQueryRegistry::default());
1854        let query = registry.register(SqlQueryOptions::default()).unwrap();
1855        query
1856            .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1857            .unwrap();
1858        query.begin_serialization().unwrap();
1859        query
1860            .transition(SqlQueryPhase::Serializing, SqlQueryPhase::Completed)
1861            .unwrap();
1862
1863        query.try_complete().unwrap();
1864
1865        assert_eq!(registry.active_count(), 0);
1866        let status = registry.status(query.id()).unwrap();
1867        assert_eq!(status.phase, SqlQueryPhase::Completed);
1868        assert_eq!(
1869            status.serialization_outcome,
1870            SerializationOutcome::Succeeded
1871        );
1872    }
1873
1874    #[test]
1875    fn zero_batch_completion_checks_expired_deadline() {
1876        let registry = Arc::new(SqlQueryRegistry::default());
1877        let query = registry
1878            .register(SqlQueryOptions {
1879                timeout: Some(Duration::from_millis(1)),
1880                ..SqlQueryOptions::default()
1881            })
1882            .unwrap();
1883        query
1884            .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1885            .unwrap();
1886        query.begin_serialization().unwrap();
1887        std::thread::sleep(Duration::from_millis(5));
1888
1889        assert!(matches!(
1890            query.try_complete(),
1891            Err(MongrelQueryError::DeadlineExceeded { .. })
1892        ));
1893        let status = registry.status(query.id()).unwrap();
1894        assert_eq!(status.phase, SqlQueryPhase::Cancelled);
1895        assert_eq!(
1896            status.terminal_state(),
1897            Some(QueryTerminalState::DeadlineBeforeCommit)
1898        );
1899    }
1900
1901    #[test]
1902    fn parent_cancellation_wins_terminal_completion() {
1903        let registry = Arc::new(SqlQueryRegistry::default());
1904        let parent = ExecutionControl::new(None);
1905        let query = registry
1906            .register(SqlQueryOptions {
1907                parent_control: Some(parent.clone()),
1908                ..SqlQueryOptions::default()
1909            })
1910            .unwrap();
1911        query
1912            .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1913            .unwrap();
1914        query.begin_serialization().unwrap();
1915        parent.cancel(CancellationReason::SessionClosed);
1916
1917        assert!(matches!(
1918            query.try_complete(),
1919            Err(MongrelQueryError::QueryCancelled {
1920                reason: CancellationReason::SessionClosed,
1921                ..
1922            })
1923        ));
1924        assert_eq!(
1925            registry.status(query.id()).unwrap().phase,
1926            SqlQueryPhase::Cancelled
1927        );
1928    }
1929
1930    #[test]
1931    fn explicit_serialization_failure_beats_passive_parent_cancel() {
1932        let registry = Arc::new(SqlQueryRegistry::default());
1933        let parent = ExecutionControl::new(None);
1934        let query = registry
1935            .register(SqlQueryOptions {
1936                parent_control: Some(parent.clone()),
1937                ..SqlQueryOptions::default()
1938            })
1939            .unwrap();
1940        query
1941            .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1942            .unwrap();
1943        query.begin_serialization().unwrap();
1944        parent.cancel(CancellationReason::SessionClosed);
1945        query.fail_serialization();
1946
1947        let status = registry.status(query.id()).unwrap();
1948        assert_eq!(status.phase, SqlQueryPhase::Failed);
1949        assert_eq!(status.terminal_error.unwrap().code, "SERIALIZATION_FAILED");
1950    }
1951
1952    #[test]
1953    fn serialization_failure_after_commit_keeps_exact_durable_outcome() {
1954        let registry = Arc::new(SqlQueryRegistry::default());
1955        let query = registry.register(SqlQueryOptions::default()).unwrap();
1956        query
1957            .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1958            .unwrap();
1959        query.begin_statement(2);
1960        query.enter_commit_critical().unwrap();
1961        query.record_commit(2, 73);
1962        query.exit_commit_critical().unwrap();
1963        query.begin_serialization().unwrap();
1964        query.fail_serialization();
1965
1966        let status = registry.status(query.id()).unwrap();
1967        assert_eq!(status.phase, SqlQueryPhase::Failed);
1968        assert_eq!(status.durable_outcome.last_commit_epoch, Some(73));
1969        assert_eq!(status.durable_outcome.committed_statements, 1);
1970        assert_eq!(
1971            status.terminal_error.unwrap().code,
1972            "SERIALIZATION_FAILED_AFTER_COMMIT"
1973        );
1974        assert!(matches!(
1975            query.commit_outcome_error("encode failed"),
1976            MongrelQueryError::CommitOutcome {
1977                committed: true,
1978                committed_statements: 1,
1979                last_commit_epoch: Some(73),
1980                first_commit_statement_index: Some(2),
1981                last_commit_statement_index: Some(2),
1982                statement_index: 2,
1983                ..
1984            }
1985        ));
1986    }
1987
1988    #[test]
1989    fn guard_cleans_up_dropped_execution_without_raw_sql() {
1990        let registry = Arc::new(SqlQueryRegistry::default());
1991        let query = registry
1992            .register(SqlQueryOptions {
1993                owner: Some("alice".into()),
1994                session_id: Some("session".into()),
1995                ..SqlQueryOptions::default()
1996            })
1997            .unwrap();
1998        query.set_sql_metadata("SELECT secret FROM docs WHERE token = 'private'");
1999        let id = query.id();
2000        drop(RegisteredQueryGuard::new(query));
2001        let status = registry.status(id).unwrap();
2002        assert_eq!(status.phase, SqlQueryPhase::Failed);
2003        assert_eq!(status.operation, "SELECT");
2004        assert_ne!(status.sql_fingerprint, [0; 32]);
2005    }
2006
2007    #[test]
2008    fn sql_operation_metadata_is_bounded() {
2009        let registry = Arc::new(SqlQueryRegistry::default());
2010        let query = registry.register(SqlQueryOptions::default()).unwrap();
2011        let id = query.id();
2012        query.set_sql_metadata(&format!("{} value", "x".repeat(4096)));
2013        drop(RegisteredQueryGuard::new(query));
2014        assert_eq!(registry.status(id).unwrap().operation.len(), 64);
2015    }
2016
2017    #[test]
2018    fn tombstone_keeps_durable_and_terminal_outcomes() {
2019        let registry = Arc::new(SqlQueryRegistry::default());
2020        let query = registry.register(SqlQueryOptions::default()).unwrap();
2021        let id = query.id();
2022        query
2023            .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
2024            .unwrap();
2025        query.begin_statement(0);
2026        query.enter_commit_critical().unwrap();
2027        query.record_commit(0, 41);
2028        query.record_commit(0, 41);
2029        query.exit_commit_critical().unwrap();
2030        query.complete_statement(0);
2031        query.begin_statement(1);
2032        query.begin_serialization().unwrap();
2033        let active_outcome = query.status().durable_outcome;
2034
2035        assert_eq!(
2036            query.request_cancel(CancellationReason::ClientRequest),
2037            CancelOutcome::Accepted
2038        );
2039        query.fail();
2040
2041        let status = registry.status(id).unwrap();
2042        assert_eq!(status.durable_outcome, active_outcome);
2043        assert_eq!(
2044            status.durable_outcome,
2045            DurableOutcome {
2046                committed: true,
2047                committed_statements: 1,
2048                last_commit_epoch: Some(41),
2049                first_commit_statement_index: Some(0),
2050                last_commit_statement_index: Some(0),
2051            }
2052        );
2053        assert_eq!(status.serialization_outcome, SerializationOutcome::Failed);
2054        assert_eq!(status.commit_fence_outcome, CommitFenceOutcome::CommitWon);
2055        assert_eq!(
2056            status.terminal_error,
2057            Some(QueryTerminalError {
2058                code: "QUERY_CANCELLED_AFTER_COMMIT".into(),
2059                category: QueryTerminalErrorCategory::Cancellation,
2060            })
2061        );
2062        assert_eq!(
2063            status.terminal_state(),
2064            Some(QueryTerminalState::CancelledAfterCommit)
2065        );
2066    }
2067
2068    #[test]
2069    fn cancellation_error_carries_durable_receipt() {
2070        let registry = Arc::new(SqlQueryRegistry::default());
2071        let query = registry.register(SqlQueryOptions::default()).unwrap();
2072        query
2073            .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
2074            .unwrap();
2075        query.begin_statement(0);
2076        query.enter_commit_critical().unwrap();
2077        query.record_commit(0, 73);
2078        query.exit_commit_critical().unwrap();
2079        query.complete_statement(0);
2080        query.begin_statement(1);
2081        query.begin_serialization().unwrap();
2082        assert_eq!(
2083            query.request_cancel(CancellationReason::ClientRequest),
2084            CancelOutcome::Accepted
2085        );
2086
2087        assert!(matches!(
2088            query.checkpoint(),
2089            Err(MongrelQueryError::QueryCancelled {
2090                committed: true,
2091                committed_statements: 1,
2092                last_commit_epoch: Some(73),
2093                first_commit_statement_index: Some(0),
2094                last_commit_statement_index: Some(0),
2095                completed_statements: 1,
2096                cancelled_statement_index: 1,
2097                ..
2098            })
2099        ));
2100        query.fail();
2101    }
2102
2103    #[test]
2104    fn explicit_terminal_error_category_survives_tombstone() {
2105        let registry = Arc::new(SqlQueryRegistry::default());
2106        let query = registry.register(SqlQueryOptions::default()).unwrap();
2107        let id = query.id();
2108        query.record_terminal_error(
2109            "RESULT_LIMIT_EXCEEDED",
2110            QueryTerminalErrorCategory::ResultLimit,
2111        );
2112        query.fail();
2113
2114        let status = registry.status(id).unwrap();
2115        assert_eq!(
2116            status.terminal_error,
2117            Some(QueryTerminalError {
2118                code: "RESULT_LIMIT_EXCEEDED".into(),
2119                category: QueryTerminalErrorCategory::ResultLimit,
2120            })
2121        );
2122        assert_eq!(
2123            status.terminal_state(),
2124            Some(QueryTerminalState::FailedBeforeCommit)
2125        );
2126    }
2127
2128    #[test]
2129    fn serialization_failure_code_records_commit_outcome() {
2130        let registry = Arc::new(SqlQueryRegistry::default());
2131        let query = registry.register(SqlQueryOptions::default()).unwrap();
2132        let id = query.id();
2133        query.record_commit(0, 7);
2134        query.record_serialization_failure("SERIALIZATION_FAILED");
2135        query.fail();
2136
2137        let status = registry.status(id).unwrap();
2138        assert_eq!(
2139            status.terminal_error.as_ref().unwrap().code,
2140            "SERIALIZATION_FAILED_AFTER_COMMIT"
2141        );
2142        assert_eq!(
2143            status.terminal_state(),
2144            Some(QueryTerminalState::CommittedWithError)
2145        );
2146    }
2147}