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