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