Skip to main content

reddb_server/runtime/
impl_queue.rs

1//! Queue DDL and command execution
2
3use std::collections::{HashMap, HashSet};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Arc;
6
7use crate::runtime::audit_log::{AuditAuthSource, AuditEvent, AuditFieldEscaper, Outcome};
8use crate::runtime::impl_core::{
9    clear_current_auth_identity, clear_current_tenant, current_auth_identity,
10    current_connection_id, current_tenant, set_current_auth_principal, set_current_tenant,
11};
12use crate::runtime::queue_telemetry::NackOutcomeLabel;
13use crate::storage::queue::QueueMode;
14use crate::storage::unified::entity::{QueueMessageData, RowData};
15use crate::storage::unified::{Metadata, MetadataValue, UnifiedStore};
16use crate::telemetry::operator_event::OperatorEvent;
17
18use super::*;
19
20use super::primary_queue_store::PrimaryQueueStore;
21use super::queue_lifecycle::{QueueLifecycle, RetirementOutcome};
22use crate::storage::queue::lifecycle::{
23    QueueSide as LcQueueSide, QueueStore as _, QueueStoreError, QueueTxn,
24};
25
26/// Build a [`QueueLifecycle`] backed by a fresh [`PrimaryQueueStore`] for
27/// the given `queue`, plus a `QueueTxn` bound to the live runtime
28/// connection. The store inside the lifecycle and the standalone
29/// [`PrimaryQueueStore`] returned for ack/nack lookups share the same
30/// underlying [`UnifiedStore`] — calls against either are observable on
31/// the other.
32pub(super) fn runtime_lifecycle(
33    runtime: &RedDBRuntime,
34    queue: &str,
35) -> (
36    QueueLifecycle<PrimaryQueueStore>,
37    PrimaryQueueStore,
38    QueueTxn,
39) {
40    let primary_for_lookup = PrimaryQueueStore::new(runtime.clone());
41    let primary_for_lifecycle = PrimaryQueueStore::new(runtime.clone());
42    let txn = primary_for_lifecycle.new_txn();
43    let cfg = primary_for_lifecycle.lifecycle_config(queue);
44    (
45        QueueLifecycle::new(primary_for_lifecycle, cfg)
46            .with_telemetry(Arc::clone(&runtime.inner.queue_telemetry)),
47        primary_for_lookup,
48        txn,
49    )
50}
51
52/// Slice C of PRD #718 — error surfaced to callers when the wait
53/// registry is cancelled (server shutdown) while a `QUEUE READ … WAIT`
54/// is parked. Kept as a plain `RedDBError::Query` so transports
55/// inherit the message unchanged — there is no separate `Cancelled`
56/// variant on the public error today.
57pub(crate) const QUEUE_READ_WAIT_CANCELLED: &str =
58    "QUEUE READ WAIT cancelled — server shutting down";
59
60/// Slice B of PRD #718 — `red.config` key naming the maximum WAIT
61/// budget the runtime will honour. Values above the cap are rejected
62/// before any waiter is registered.
63pub(crate) const QUEUE_MAX_WAIT_MS_CONFIG_KEY: &str = "red.config.queue.max_wait_ms";
64
65/// Default cap when the operator has not set
66/// [`QUEUE_MAX_WAIT_MS_CONFIG_KEY`] — 60 seconds, in milliseconds.
67pub(crate) const QUEUE_MAX_WAIT_MS_DEFAULT: u64 = 60_000;
68
69/// Outcome of the async live queue-wait edge ([`RedDBRuntime::redwire_queue_wait_json`],
70/// issue #919). Carries the three non-error terminal states the RedWire
71/// session maps to distinct frames, so a timeout never aliases an empty
72/// delivery and a cancellation never aliases a timeout:
73///   - `Delivered` → one `QueueEventPush` per message.
74///   - `TimedOut`  → a distinct `QueueWaitTimeout` frame.
75///   - `Cancelled` → a `StreamError` with the cancellation code.
76///
77/// A genuine runtime failure (bad queue, read error) stays an `Err` on
78/// the surrounding `RedDBResult`.
79#[derive(Debug)]
80pub(crate) enum RedwireWaitOutcome {
81    Delivered(Vec<crate::serde_json::Value>),
82    TimedOut,
83    Cancelled,
84}
85
86/// Slice C of PRD #718 — scope key for the queue wait registry.
87/// Today every connection in the process shares a single namespace;
88/// the helper exists so multi-tenant scoping (e.g. tenant id) can be
89/// threaded through later without touching every call site.
90pub(super) fn queue_wait_scope() -> String {
91    crate::runtime::impl_core::current_tenant().unwrap_or_default()
92}
93
94fn with_redwire_wait_context<T>(
95    auth_identity: &Option<(String, crate::auth::Role)>,
96    auth_principal: &Option<crate::auth::UserId>,
97    tenant: &Option<String>,
98    f: impl FnOnce() -> T,
99) -> T {
100    let previous_auth = current_auth_identity();
101    let previous_principal = super::execution_context::current_auth_principal();
102    let previous_tenant = current_tenant();
103    match tenant {
104        Some(t) => set_current_tenant(t.clone()),
105        None => clear_current_tenant(),
106    }
107    match (auth_identity, auth_principal) {
108        (Some((_, role)), Some(principal)) => set_current_auth_principal(principal.clone(), *role),
109        _ => clear_current_auth_identity(),
110    }
111    let result = f();
112    match previous_tenant {
113        Some(t) => set_current_tenant(t),
114        None => clear_current_tenant(),
115    }
116    match (previous_auth, previous_principal) {
117        (Some((_, role)), Some(principal)) => set_current_auth_principal(principal, role),
118        _ => clear_current_auth_identity(),
119    }
120    result
121}
122
123/// Convert a lifecycle `QueueSide` view into the AST flavour we accept
124/// from `QueueCommand` callers. Both enums are isomorphic but live in
125/// different modules.
126fn ast_side_to_lc(side: reddb_rql::ast::QueueSide) -> LcQueueSide {
127    use reddb_rql::ast::QueueSide as Ast;
128    match side {
129        Ast::Left => LcQueueSide::Left,
130        Ast::Right => LcQueueSide::Right,
131    }
132}
133
134/// Map a `QueueStoreError` (returned by lifecycle methods) onto the
135/// runtime-facing `RedDBError`.
136fn map_qse(err: QueueStoreError) -> RedDBError {
137    match err {
138        QueueStoreError::UnknownDelivery(id) => RedDBError::NotFound(format!(
139            "delivery_id '{id}' does not resolve to a live pending delivery"
140        )),
141        QueueStoreError::UnknownQueue(q) => RedDBError::NotFound(format!("queue '{q}' not found")),
142    }
143}
144
145// ---------------------------------------------------------------------------
146// Outbox metrics (exposed via /metrics)
147// ---------------------------------------------------------------------------
148
149/// Total event push attempts that failed (queue full or other error) and
150/// triggered DLQ routing.
151pub static EVENTS_DRAIN_RETRIES_TOTAL: AtomicU64 = AtomicU64::new(0);
152
153/// Total events routed to the dead-letter queue.
154pub static EVENTS_DLQ_TOTAL: AtomicU64 = AtomicU64::new(0);
155
156/// Total events successfully enqueued to their target queue.
157pub static EVENTS_ENQUEUED_TOTAL: AtomicU64 = AtomicU64::new(0);
158
159/// Warn when total estimated outbox payload bytes exceed this value (1 GiB).
160const OUTBOX_WARN_BYTES: u64 = 1 << 30;
161
162/// Route all new events to DLQ when estimated outbox exceeds this value (10 GiB).
163const OUTBOX_MAX_BYTES: u64 = 10 * (1 << 30);
164
165/// Running estimate of bytes pending in event queues (approximate; not decremented on consume).
166static OUTBOX_APPROX_BYTES: AtomicU64 = AtomicU64::new(0);
167
168pub(super) const QUEUE_META_COLLECTION: &str = "red_queue_meta";
169pub(super) const KIND_QUEUE_PUSH_DEDUP: &str = "queue_push_dedup";
170const QUEUE_POSITION_CENTER: u64 = u64::MAX / 2;
171const WORK_DEFAULT_GROUP: &str = "_work_default";
172const FANOUT_GROUP_PREFIX: &str = "_fanout_";
173
174#[derive(Debug, Clone)]
175pub(super) struct QueueRuntimeConfig {
176    pub(super) mode: QueueMode,
177    pub(super) priority: bool,
178    pub(super) max_size: Option<usize>,
179    pub(super) ttl_ms: Option<u64>,
180    pub(super) dlq: Option<String>,
181    pub(super) max_attempts: u32,
182    pub(super) lock_deadline_ms: u64,
183    pub(super) in_flight_cap_per_group: u32,
184    /// Default retry delay (issue #723) applied to NACK-requeued
185    /// messages before they become re-deliverable. `None` keeps the
186    /// pre-#723 immediate-requeue behaviour. Overridden per-failure by
187    /// an authorized `NACK ... WITH DELAY <duration>`.
188    pub(super) retry_delay_ms: Option<u64>,
189    /// Queue-scoped push idempotency window. `None` means use the engine
190    /// default from the RQL AST crate.
191    pub(super) dedup_window_ms: Option<u64>,
192}
193
194#[derive(Debug, Clone)]
195struct QueueGroupEntry {
196    entity_id: EntityId,
197    group: String,
198}
199
200#[derive(Debug, Clone)]
201pub(super) struct QueuePendingEntry {
202    pub(super) entity_id: EntityId,
203    group: String,
204    pub(super) message_id: EntityId,
205    consumer: String,
206    pub(super) delivered_at_ns: u64,
207    pub(super) delivery_count: u32,
208}
209
210#[derive(Debug, Clone)]
211pub(super) struct QueueAckEntry {
212    entity_id: EntityId,
213    group: String,
214    pub(super) message_id: EntityId,
215}
216
217#[derive(Debug, Clone)]
218pub(super) struct QueueMessageView {
219    pub(super) id: EntityId,
220    position: u64,
221    priority: i32,
222    pub(super) payload: Value,
223    attempts: u32,
224    pub(super) max_attempts: u32,
225    enqueued_at_ns: u64,
226    /// First-delivery instant for delayed messages (issue #722). `None`
227    /// means immediate availability. Sourced from the
228    /// `_available_at_ns` metadata field, populated on push.
229    pub(super) available_at_ns: Option<u64>,
230    /// Optional grouped-delivery ordering key, sourced from message metadata.
231    pub(super) ordering_key: Option<String>,
232}
233
234impl QueueMessageView {
235    /// Whether this message is currently deliverable. Messages whose
236    /// `available_at_ns` lies in the future remain durable and
237    /// inspectable but are filtered out of `QUEUE READ` / `QUEUE POP`
238    /// projections.
239    pub(super) fn is_available_now(&self) -> bool {
240        match self.available_at_ns {
241            Some(at) => at <= now_ns(),
242            None => true,
243        }
244    }
245}
246
247impl RedDBRuntime {
248    /// Slice C of PRD #718 — non-blocking `group_read` plus optional
249    /// `WAIT <duration>` retry. When `wait_ms` is `None` this is the
250    /// pre-slice-C synchronous read. When `Some`, an immediate empty
251    /// projection parks the caller on the shared
252    /// [`crate::runtime::queue_wait_registry::QueueWaitRegistry`] and
253    /// retries on wake until the deadline. Timeout returns an empty
254    /// projection (zero records, no error). Shutdown cancellation
255    /// returns [`QUEUE_READ_WAIT_CANCELLED`].
256    pub(super) fn group_read_with_optional_wait(
257        &self,
258        queue: &str,
259        group: &str,
260        consumer: &str,
261        count: usize,
262        wait_ms: Option<u64>,
263    ) -> RedDBResult<Vec<crate::runtime::queue_lifecycle::DeliveredMessage>> {
264        let do_read =
265            |runtime: &RedDBRuntime| -> RedDBResult<Vec<crate::runtime::queue_lifecycle::DeliveredMessage>> {
266                // #1371 — serialize concurrent group consumers on this queue so
267                // a single available message is claimed (mark_pending) and
268                // returned by exactly one consumer. Without this, woken
269                // competing waiters all observe the message as available and
270                // each delivers it (double-delivery across the wake-all).
271                let read_lock = runtime
272                    .inner
273                    .rmw_locks
274                    .lock_for(queue, "__queue_group_read__");
275                let _read_guard = read_lock.lock();
276                // WAIT is an autocommit live subscription: each retry must see
277                // commits since the previous probe. Keep identity/tenant fixed
278                // and restore the enclosing statement snapshot after the read.
279                let _snapshot_guard = wait_ms
280                    .and_then(|_| crate::runtime::impl_core::capture_current_snapshot())
281                    .map(|mut context| {
282                        context.snapshot = context.manager.fresh_read_snapshot();
283                        crate::runtime::execution_context::CurrentSnapshotGuard::install(context)
284                    });
285                let (lifecycle, _ps, txn) = runtime_lifecycle(runtime, queue);
286                lifecycle
287                    .group_read(&txn, queue, group, consumer, count)
288                    .map_err(map_qse)
289            };
290
291        let delivered = do_read(self)?;
292        let Some(wait_ms) = wait_ms else {
293            return Ok(delivered);
294        };
295        if !delivered.is_empty() {
296            return Ok(delivered);
297        }
298        // Empty under WAIT: park on the registry. WAIT 0 collapses to
299        // a single re-probe of the registry's current state — useful
300        // for tests but the timeout path returns immediately.
301        //
302        // Telemetry (slice D / PRD #718 / #729): we record exactly one
303        // `wait_started` increment at entry, and exactly one terminal
304        // outcome increment + histogram observation at exit, for the
305        // (scope, queue) labels. The histogram measures wall-clock
306        // started→resolved across all re-park iterations of this call.
307        let registry = self.queue_wait_registry();
308        let scope = queue_wait_scope();
309        let deadline = std::time::Instant::now() + std::time::Duration::from_millis(wait_ms);
310        let telemetry = self.queue_telemetry();
311        telemetry.record_wait_started(&scope, queue);
312        let wait_start = std::time::Instant::now();
313        let observe = |outcome: crate::runtime::queue_telemetry::WaitOutcomeLabel| {
314            let elapsed_ms = wait_start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
315            telemetry.record_wait_outcome(&scope, queue, outcome, elapsed_ms);
316        };
317        loop {
318            // Snapshot BEFORE the re-probe so a notify that fires
319            // between the probe and the park bumps the generation and
320            // wait_until returns Woken without ever blocking.
321            let snapshot = registry.snapshot(&scope, queue);
322            let delivered = do_read(self)?;
323            if !delivered.is_empty() {
324                observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Woken);
325                return Ok(delivered);
326            }
327            if registry.is_cancelled() {
328                observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
329                return Err(RedDBError::Query(QUEUE_READ_WAIT_CANCELLED.to_string()));
330            }
331            if std::time::Instant::now() >= deadline {
332                observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
333                return Ok(Vec::new());
334            }
335            // Issue #722: a delayed message becomes deliverable when its
336            // `available_at_ns` passes, but the registry only wakes on
337            // producer commits — a quiet queue with a future due-time
338            // would otherwise sit on the condvar until the user budget
339            // expired. Cap the park horizon at the soonest future
340            // `available_at_ns` so the next loop iteration probes the
341            // queue at-or-just-after the message becomes due. A
342            // `Timeout` from the capped park is not the final answer; we
343            // loop and re-probe before deciding the user budget is up.
344            let park_deadline = match earliest_future_available_at(&self.inner.db.store(), queue) {
345                Some(at_ns) => {
346                    let now_ns = now_ns();
347                    if at_ns <= now_ns {
348                        // Already due; re-probe immediately.
349                        deadline.min(std::time::Instant::now())
350                    } else {
351                        let wait_ns = at_ns - now_ns;
352                        let due_instant =
353                            std::time::Instant::now() + std::time::Duration::from_nanos(wait_ns);
354                        deadline.min(due_instant)
355                    }
356                }
357                None => deadline,
358            };
359            match registry.wait_until(&snapshot, park_deadline) {
360                crate::runtime::queue_wait_registry::WaitOutcome::Woken => continue,
361                crate::runtime::queue_wait_registry::WaitOutcome::Timeout => {
362                    // If this was the user-supplied deadline, give up;
363                    // otherwise loop and re-probe (a delayed message may
364                    // have just become due).
365                    if std::time::Instant::now() >= deadline {
366                        observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
367                        return Ok(Vec::new());
368                    }
369                    continue;
370                }
371                crate::runtime::queue_wait_registry::WaitOutcome::Cancelled => {
372                    observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
373                    return Err(RedDBError::Query(QUEUE_READ_WAIT_CANCELLED.to_string()));
374                }
375            }
376        }
377    }
378
379    /// Issue #917 — async live-wait edge used by the RedWire session.
380    ///
381    /// Unlike [`group_read_with_optional_wait`](Self::group_read_with_optional_wait)
382    /// (the synchronous HTTP/condvar caller), this parks on the
383    /// registry's async wake head and never holds a blocking OS thread:
384    /// the awaiting tokio worker is released back to the runtime for the
385    /// wait duration. On every wake it re-probes the *normal* queue
386    /// delivery path (`group_read`), so a delivered message is genuinely
387    /// claimed, not merely observed. Returns the delivered messages
388    /// rendered as JSON values (so the transport edge stays free of
389    /// runtime queue types); [`RedwireWaitOutcome::TimedOut`] means the
390    /// deadline elapsed without a delivery (issue #919 surfaces this as
391    /// a distinct timeout frame rather than an empty push), and a
392    /// cancellation surfaces as [`RedwireWaitOutcome::Cancelled`] — the
393    /// async analogue of the sync path's [`QUEUE_READ_WAIT_CANCELLED`].
394    /// A genuine runtime failure stays an `Err`.
395    pub(crate) async fn redwire_queue_wait_json(
396        &self,
397        queue: &str,
398        group: Option<&str>,
399        consumer: &str,
400        count: usize,
401        wait_ms: u64,
402        auth_identity: Option<(String, crate::auth::Role)>,
403        auth_principal: Option<crate::auth::UserId>,
404        tenant: Option<String>,
405    ) -> RedDBResult<RedwireWaitOutcome> {
406        let group_owned: RedDBResult<String> =
407            with_redwire_wait_context(&auth_identity, &auth_principal, &tenant, || {
408                let expr = reddb_rql::ast::QueryExpr::QueueCommand(
409                    reddb_rql::ast::QueueCommand::GroupRead {
410                        queue: queue.to_string(),
411                        group: group.map(str::to_string),
412                        consumer: consumer.to_string(),
413                        count,
414                        wait_ms: Some(wait_ms),
415                    },
416                );
417                self.check_query_privilege(&expr)
418                    .map_err(RedDBError::Query)?;
419                let store = self.inner.db.store();
420                ensure_queue_exists(store.as_ref(), queue)?;
421                let config = load_queue_config(store.as_ref(), queue);
422                resolve_read_group(store.as_ref(), queue, group, consumer, &config)
423            });
424        let group_owned = group_owned?;
425        let group_ref = group_owned.as_str();
426
427        let do_read =
428            |runtime: &RedDBRuntime| -> RedDBResult<Vec<crate::runtime::queue_lifecycle::DeliveredMessage>> {
429                with_redwire_wait_context(&auth_identity, &auth_principal, &tenant, || {
430                    let (lifecycle, _ps, txn) = runtime_lifecycle(runtime, queue);
431                    lifecycle
432                        .group_read(&txn, queue, group_ref, consumer, count)
433                        .map_err(map_qse)
434                })
435            };
436
437        let render = |delivered: Vec<crate::runtime::queue_lifecycle::DeliveredMessage>| {
438            RedwireWaitOutcome::Delivered(
439                delivered.into_iter().map(delivered_message_json).collect(),
440            )
441        };
442
443        // Fast path: a message is already deliverable at open time.
444        let delivered = do_read(self)?;
445        if !delivered.is_empty() {
446            return Ok(render(delivered));
447        }
448
449        let registry = self.queue_wait_registry();
450        let scope =
451            with_redwire_wait_context(&auth_identity, &auth_principal, &tenant, queue_wait_scope);
452        let deadline = std::time::Instant::now() + std::time::Duration::from_millis(wait_ms);
453        let telemetry = self.queue_telemetry();
454        telemetry.record_wait_started(&scope, queue);
455        let wait_start = std::time::Instant::now();
456        tracing::debug!(
457            target: "reddb::redwire::queue_wait",
458            queue,
459            group = group_ref,
460            consumer,
461            count,
462            wait_ms,
463            scope = scope.as_str(),
464            "redwire queue wait parked"
465        );
466        let observe = |outcome: crate::runtime::queue_telemetry::WaitOutcomeLabel| {
467            let elapsed_ms = wait_start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
468            telemetry.record_wait_outcome(&scope, queue, outcome, elapsed_ms);
469            tracing::debug!(
470                target: "reddb::redwire::queue_wait",
471                queue,
472                group = group_ref,
473                consumer,
474                count,
475                wait_ms,
476                scope = scope.as_str(),
477                outcome = outcome.as_str(),
478                duration_ms = elapsed_ms,
479                "redwire queue wait resolved"
480            );
481        };
482        loop {
483            // Register the async waiter (snapshot the generation) BEFORE
484            // the re-probe so a notify landing between probe and park is
485            // observed as a generation move rather than a lost wake.
486            let waiter = registry.async_waiter(&scope, queue);
487            let delivered = do_read(self)?;
488            if !delivered.is_empty() {
489                observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Woken);
490                return Ok(render(delivered));
491            }
492            if registry.is_cancelled() {
493                observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
494                return Ok(RedwireWaitOutcome::Cancelled);
495            }
496            if std::time::Instant::now() >= deadline {
497                observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
498                return Ok(RedwireWaitOutcome::TimedOut);
499            }
500            let park_deadline = match earliest_future_available_at(&self.inner.db.store(), queue) {
501                Some(at_ns) => {
502                    let now_ns = now_ns();
503                    if at_ns <= now_ns {
504                        deadline.min(std::time::Instant::now())
505                    } else {
506                        let wait_ns = at_ns - now_ns;
507                        let due_instant =
508                            std::time::Instant::now() + std::time::Duration::from_nanos(wait_ns);
509                        deadline.min(due_instant)
510                    }
511                }
512                None => deadline,
513            };
514            // The async waiter (and its `Arc<Slot>` clone) is a local
515            // dropped on every return below, so an expired or cancelled
516            // wait releases its registry slot reference and frees the
517            // tokio worker the moment this future resolves (AC #4).
518            match registry.wait_until_async(&waiter, park_deadline).await {
519                crate::runtime::queue_wait_registry::WaitOutcome::Woken => continue,
520                crate::runtime::queue_wait_registry::WaitOutcome::Timeout => {
521                    if std::time::Instant::now() >= deadline {
522                        observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
523                        return Ok(RedwireWaitOutcome::TimedOut);
524                    }
525                    continue;
526                }
527                crate::runtime::queue_wait_registry::WaitOutcome::Cancelled => {
528                    observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
529                    return Ok(RedwireWaitOutcome::Cancelled);
530                }
531            }
532        }
533    }
534
535    /// Reject a live queue-wait open whose requested budget exceeds the
536    /// server's maximum wait cap (issue #919), mirroring the SQL
537    /// `QUEUE READ … WAIT` cap (slice B of PRD #718). Returns the
538    /// operator-actionable message (naming the `red.config` key and the
539    /// active cap) when `wait_ms` is over the cap, or `Ok(())` to
540    /// proceed. The transport calls this *before* spawning the wait
541    /// task, so an over-cap request is refused with an explicit error
542    /// and never parks — not silently shortened (AC #3).
543    pub(crate) fn redwire_queue_wait_cap_check(&self, wait_ms: u64) -> Result<(), String> {
544        let cap = self.config_u64(QUEUE_MAX_WAIT_MS_CONFIG_KEY, QUEUE_MAX_WAIT_MS_DEFAULT);
545        if wait_ms > cap {
546            Err(format!(
547                "queue-wait WAIT {wait_ms}ms exceeds server cap {QUEUE_MAX_WAIT_MS_CONFIG_KEY} = {cap}ms"
548            ))
549        } else {
550            Ok(())
551        }
552    }
553
554    pub(crate) fn enqueue_event_payload(
555        &self,
556        queue: &str,
557        payload: Value,
558    ) -> RedDBResult<EntityId> {
559        let _event_snapshot = super::delete_publication::event_context(self)
560            .map(super::execution_context::CurrentSnapshotGuard::install);
561        let store = self.inner.db.store();
562        // Auto-create the queue if it does not exist yet.
563        if store.get_collection(queue).is_none() {
564            crate::runtime::impl_ddl::ensure_event_target_queue_pub(self, queue)?;
565        }
566
567        // Estimate payload bytes for outbox watermark checks.
568        let payload_bytes = estimate_payload_bytes(&payload);
569        let outbox_bytes = OUTBOX_APPROX_BYTES.fetch_add(payload_bytes, Ordering::Relaxed);
570
571        // Hard limit: route directly to DLQ without even trying.
572        if outbox_bytes > OUTBOX_MAX_BYTES {
573            OUTBOX_APPROX_BYTES.fetch_sub(payload_bytes, Ordering::Relaxed);
574            EVENTS_DRAIN_RETRIES_TOTAL.fetch_add(1, Ordering::Relaxed);
575            return self.route_event_to_outbox_dlq(queue, payload, "outbox_max_bytes_exceeded");
576        }
577
578        // Soft limit: warn once per crossing.
579        if outbox_bytes > OUTBOX_WARN_BYTES && outbox_bytes - payload_bytes <= OUTBOX_WARN_BYTES {
580            tracing::warn!(
581                outbox_bytes,
582                warn_threshold = OUTBOX_WARN_BYTES,
583                "event outbox approaching capacity warning threshold"
584            );
585            crate::telemetry::operator_event::OperatorEvent::OutboxDlqActivated {
586                queue: queue.to_string(),
587                dlq: format!("{queue}_outbox_dlq"),
588                reason: "outbox_warn_bytes_exceeded".to_string(),
589            }
590            .emit_global();
591        }
592
593        let config = load_queue_config(store.as_ref(), queue);
594
595        // If the target queue has a max_size and is full, route to DLQ.
596        if let Some(max_size) = config.max_size {
597            let current_len = load_queue_message_views(store.as_ref(), queue)
598                .unwrap_or_default()
599                .len();
600            if current_len >= max_size {
601                OUTBOX_APPROX_BYTES.fetch_sub(payload_bytes, Ordering::Relaxed);
602                EVENTS_DRAIN_RETRIES_TOTAL.fetch_add(1, Ordering::Relaxed);
603                return self.route_event_to_outbox_dlq(queue, payload, "queue_full");
604            }
605            // Warn at 80% capacity.
606            if current_len * 10 >= max_size * 8 {
607                tracing::warn!(
608                    queue = %queue,
609                    size = current_len,
610                    max = max_size,
611                    "event target queue near capacity"
612                );
613            }
614        }
615
616        let id = self.enqueue_event_payload_raw(store.as_ref(), queue, &config, payload)?;
617        EVENTS_ENQUEUED_TOTAL.fetch_add(1, Ordering::Relaxed);
618        Ok(id)
619    }
620
621    /// Route a failed event to `<queue>_outbox_dlq`, auto-creating it if needed.
622    fn route_event_to_outbox_dlq(
623        &self,
624        queue: &str,
625        payload: Value,
626        reason: &str,
627    ) -> RedDBResult<EntityId> {
628        let dlq_name = format!("{queue}_outbox_dlq");
629        EVENTS_DLQ_TOTAL.fetch_add(1, Ordering::Relaxed);
630
631        crate::telemetry::operator_event::OperatorEvent::OutboxDlqActivated {
632            queue: queue.to_string(),
633            dlq: dlq_name.clone(),
634            reason: reason.to_string(),
635        }
636        .emit_global();
637
638        let store = self.inner.db.store();
639        if store.get_collection(&dlq_name).is_none() {
640            crate::runtime::impl_ddl::ensure_event_target_queue_pub(self, &dlq_name)?;
641        }
642        let dlq_config = load_queue_config(store.as_ref(), &dlq_name);
643        let id = self.enqueue_event_payload_raw(store.as_ref(), &dlq_name, &dlq_config, payload)?;
644        EVENTS_ENQUEUED_TOTAL.fetch_add(1, Ordering::Relaxed);
645        Ok(id)
646    }
647
648    /// Low-level event message insert — no size checks, no DLQ routing.
649    fn enqueue_event_payload_raw(
650        &self,
651        store: &UnifiedStore,
652        queue: &str,
653        config: &QueueRuntimeConfig,
654        payload: Value,
655    ) -> RedDBResult<EntityId> {
656        let event_xid = super::delete_publication::event_writer(self);
657        let position = next_queue_position(store, queue, QueueSide::Right)?;
658        let mut entity = UnifiedEntity::new(
659            EntityId::new(0),
660            EntityKind::QueueMessage {
661                queue: queue.to_string(),
662                position,
663            },
664            EntityData::QueueMessage(QueueMessageData {
665                payload,
666                priority: None,
667                enqueued_at_ns: now_ns(),
668                attempts: 0,
669                max_attempts: config.max_attempts,
670                acked: false,
671            }),
672        );
673        if let Some(xid) = self.current_xid().or(event_xid) {
674            entity.set_xmin(xid);
675        }
676        // Keep the resident message and transient WAL copies inside the same
677        // engine budget as the source mutation, including outbox/DLQ writers.
678        let bytes = crate::storage::unified::memory_size::entity_bytes(&entity) as u64;
679        let _admission = self.admit_non_evictable_growth(
680            crate::storage::memory_pools::MemoryPool::SegmentArena,
681            "enqueue event",
682            bytes.saturating_mul(3),
683        )?;
684        let id = store
685            .insert_auto(queue, entity)
686            .map_err(|err| RedDBError::Internal(err.to_string()))?;
687        if let Some(ttl_ms) = config.ttl_ms {
688            store
689                .set_metadata(queue, id, queue_message_ttl_metadata(ttl_ms))
690                .map_err(|err| RedDBError::Internal(err.to_string()))?;
691        }
692        self.invalidate_result_cache_for_table(queue);
693        Ok(id)
694    }
695
696    pub fn execute_create_queue(
697        &self,
698        raw_query: &str,
699        query: &CreateQueueQuery,
700    ) -> RedDBResult<RuntimeQueryResult> {
701        self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
702        if super::impl_ddl::is_system_schema_name(&query.name) {
703            return Err(RedDBError::Query("system schema is read-only".to_string()));
704        }
705        if query.dlq.as_deref() == Some(query.name.as_str()) {
706            return Err(RedDBError::Query(
707                "dead-letter queue must be different from the source queue".to_string(),
708            ));
709        }
710
711        let store = self.inner.db.store();
712        let exists = store.get_collection(&query.name).is_some();
713        if exists {
714            if query.if_not_exists {
715                return Ok(RuntimeQueryResult::ok_message(
716                    raw_query.to_string(),
717                    &format!("queue '{}' already exists", query.name),
718                    "create",
719                ));
720            }
721            return Err(RedDBError::Query(format!(
722                "queue '{}' already exists",
723                query.name
724            )));
725        }
726
727        store
728            .create_collection(&query.name)
729            .map_err(|err| RedDBError::Internal(err.to_string()))?;
730        if let Some(ttl_ms) = query.ttl_ms {
731            self.inner
732                .db
733                .set_collection_default_ttl_ms(&query.name, ttl_ms);
734        }
735        self.inner
736            .db
737            .save_collection_contract(queue_collection_contract(
738                &query.name,
739                query.priority,
740                query.ttl_ms,
741            ))
742            .map_err(|err| RedDBError::Internal(err.to_string()))?;
743        save_queue_config(
744            store.as_ref(),
745            &query.name,
746            &QueueRuntimeConfig {
747                mode: query.mode,
748                priority: query.priority,
749                max_size: query.max_size,
750                ttl_ms: query.ttl_ms,
751                dlq: query.dlq.clone(),
752                max_attempts: query.max_attempts,
753                lock_deadline_ms: query.lock_deadline_ms,
754                in_flight_cap_per_group: query.in_flight_cap_per_group,
755                retry_delay_ms: query.retry_delay_ms,
756                dedup_window_ms: query.dedup_window_ms,
757            },
758        )?;
759
760        if let Some(dlq) = &query.dlq {
761            if store.get_collection(dlq).is_none() {
762                store
763                    .create_collection(dlq)
764                    .map_err(|err| RedDBError::Internal(err.to_string()))?;
765                self.inner
766                    .db
767                    .save_collection_contract(queue_collection_contract(dlq, false, None))
768                    .map_err(|err| RedDBError::Internal(err.to_string()))?;
769            }
770        }
771
772        self.invalidate_result_cache();
773        self.inner
774            .db
775            .persist_metadata()
776            .map_err(|err| RedDBError::Internal(err.to_string()))?;
777        // Issue #120 — feed the queue into the schema-vocabulary so
778        // AskPipeline (#121) can resolve queue references. Queues
779        // have an opaque payload column, so we expose `payload` and
780        // (when configured) the DLQ partner as type-tag context.
781        let mut type_tags = Vec::new();
782        if let Some(dlq) = &query.dlq {
783            type_tags.push(format!("dlq:{}", dlq));
784        }
785        self.schema_vocabulary_apply(
786            crate::runtime::schema_vocabulary::DdlEvent::CreateCollection {
787                collection: query.name.clone(),
788                columns: vec!["payload".to_string()],
789                type_tags,
790                description: None,
791            },
792        );
793
794        let mut msg = format!("queue '{}' created", query.name);
795        msg.push_str(&format!(" (mode={})", query.mode.as_str()));
796        if query.priority {
797            msg.push_str(" (priority)");
798        }
799        if let Some(max_size) = query.max_size {
800            msg.push_str(&format!(" (max_size={max_size})"));
801        }
802        if let Some(ttl_ms) = query.ttl_ms {
803            msg.push_str(&format!(" (ttl={ttl_ms}ms)"));
804        }
805        if let Some(dlq) = &query.dlq {
806            msg.push_str(&format!(
807                " (dlq={dlq}, max_attempts={})",
808                query.max_attempts
809            ));
810        }
811
812        Ok(RuntimeQueryResult::ok_message(
813            raw_query.to_string(),
814            &msg,
815            "create",
816        ))
817    }
818
819    pub fn execute_alter_queue(
820        &self,
821        raw_query: &str,
822        query: &AlterQueueQuery,
823    ) -> RedDBResult<RuntimeQueryResult> {
824        self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
825        let store = self.inner.db.store();
826        ensure_queue_exists(store.as_ref(), &query.name)?;
827
828        let mut config = load_queue_config(store.as_ref(), &query.name);
829        let mut summary: Vec<String> = Vec::new();
830
831        if let Some(new_mode) = query.mode {
832            let pending =
833                load_pending_entries(store.as_ref(), &query.name, None, None).unwrap_or_default();
834            if !pending.is_empty() {
835                tracing::warn!(
836                    queue = %query.name,
837                    pending_count = pending.len(),
838                    new_mode = %new_mode.as_str(),
839                    "ALTER QUEUE SET MODE: {} in-flight messages will drain with old mode; \
840                     new reads use {}",
841                    pending.len(),
842                    new_mode.as_str(),
843                );
844            }
845            config.mode = new_mode;
846            summary.push(format!("mode={}", new_mode.as_str()));
847        }
848        if let Some(max_attempts) = query.max_attempts {
849            config.max_attempts = max_attempts;
850            summary.push(format!("max_attempts={max_attempts}"));
851        }
852        if let Some(lock_deadline_ms) = query.lock_deadline_ms {
853            config.lock_deadline_ms = lock_deadline_ms;
854            summary.push(format!("lock_deadline_ms={lock_deadline_ms}"));
855        }
856        if let Some(in_flight_cap) = query.in_flight_cap_per_group {
857            config.in_flight_cap_per_group = in_flight_cap;
858            summary.push(format!("in_flight_cap_per_group={in_flight_cap}"));
859        }
860        if let Some(dlq) = &query.dlq {
861            if dlq == &query.name {
862                return Err(RedDBError::Query(
863                    "dead-letter queue must be different from the source queue".to_string(),
864                ));
865            }
866            config.dlq = Some(dlq.clone());
867            summary.push(format!("dlq={dlq}"));
868        }
869        if let Some(retry_delay_ms) = query.retry_delay_ms {
870            config.retry_delay_ms = if retry_delay_ms == 0 {
871                None
872            } else {
873                Some(retry_delay_ms)
874            };
875            summary.push(format!("retry_delay_ms={retry_delay_ms}"));
876        }
877        if let Some(dedup_window_ms) = query.dedup_window_ms {
878            config.dedup_window_ms = Some(dedup_window_ms);
879            summary.push(format!("dedup_window_ms={dedup_window_ms}"));
880        }
881
882        save_queue_config(store.as_ref(), &query.name, &config)?;
883
884        self.invalidate_result_cache();
885        self.inner
886            .db
887            .persist_metadata()
888            .map_err(|err| RedDBError::Internal(err.to_string()))?;
889
890        Ok(RuntimeQueryResult::ok_message(
891            raw_query.to_string(),
892            &format!("queue '{}' altered: {}", query.name, summary.join(", ")),
893            "alter",
894        ))
895    }
896
897    pub fn execute_drop_queue(
898        &self,
899        raw_query: &str,
900        query: &DropQueueQuery,
901    ) -> RedDBResult<RuntimeQueryResult> {
902        self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
903        let store = self.inner.db.store();
904        if super::impl_ddl::is_system_schema_name(&query.name) {
905            return Err(RedDBError::Query("system schema is read-only".to_string()));
906        }
907        if store.get_collection(&query.name).is_none() {
908            if query.if_exists {
909                return Ok(RuntimeQueryResult::ok_message(
910                    raw_query.to_string(),
911                    &format!("queue '{}' does not exist", query.name),
912                    "drop",
913                ));
914            }
915            return Err(RedDBError::NotFound(format!(
916                "queue '{}' not found",
917                query.name
918            )));
919        }
920        let actual = crate::runtime::ddl::polymorphic_resolver::resolve(
921            &query.name,
922            &self.inner.db.catalog_model_snapshot(),
923        )?;
924        crate::runtime::ddl::polymorphic_resolver::ensure_model_match(
925            crate::catalog::CollectionModel::Queue,
926            actual,
927        )?;
928
929        store
930            .drop_collection(&query.name)
931            .map_err(|err| RedDBError::Internal(err.to_string()))?;
932        self.inner.db.clear_collection_default_ttl_ms(&query.name);
933        self.inner
934            .db
935            .remove_collection_contract(&query.name)
936            .map_err(|err| RedDBError::Internal(err.to_string()))?;
937        remove_queue_metadata(store.as_ref(), &query.name);
938        self.invalidate_result_cache();
939        self.inner
940            .db
941            .persist_metadata()
942            .map_err(|err| RedDBError::Internal(err.to_string()))?;
943        // Issue #120 — invalidate the schema-vocabulary entry.
944        self.schema_vocabulary_apply(
945            crate::runtime::schema_vocabulary::DdlEvent::DropCollection {
946                collection: query.name.clone(),
947            },
948        );
949
950        Ok(RuntimeQueryResult::ok_message(
951            raw_query.to_string(),
952            &format!("queue '{}' dropped", query.name),
953            "drop",
954        ))
955    }
956
957    pub fn execute_queue_command(
958        &self,
959        raw_query: &str,
960        cmd: &QueueCommand,
961    ) -> RedDBResult<RuntimeQueryResult> {
962        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
963        match cmd {
964            QueueCommand::Push {
965                queue,
966                value,
967                side,
968                priority,
969                key,
970                available,
971                dedup_key,
972            } => {
973                let store = self.inner.db.store();
974                ensure_queue_exists(store.as_ref(), queue)?;
975                let config = load_queue_config(store.as_ref(), queue);
976                if key.is_some() && config.priority {
977                    return Err(RedDBError::Query(format!(
978                        "ordering key is not supported on priority queue '{}'",
979                        queue
980                    )));
981                }
982                if priority.is_some() && !config.priority {
983                    return Err(RedDBError::Query(format!(
984                        "queue '{}' is not a priority queue",
985                        queue
986                    )));
987                }
988                if let Some(dedup_key) = dedup_key.as_deref() {
989                    let now = now_ns();
990                    reclaim_expired_queue_dedup(store.as_ref(), queue, now)?;
991                    if let Some(existing_id) =
992                        find_queue_dedup(store.as_ref(), queue, dedup_key, now)
993                    {
994                        let mut result = UnifiedResult::with_columns(vec![
995                            "message_id".into(),
996                            "side".into(),
997                            "queue".into(),
998                        ]);
999                        let mut record = UnifiedRecord::new();
1000                        record.set("message_id", Value::text(message_id_string(existing_id)));
1001                        record.set(
1002                            "side",
1003                            Value::text(match side {
1004                                QueueSide::Left => "left".to_string(),
1005                                QueueSide::Right => "right".to_string(),
1006                            }),
1007                        );
1008                        record.set("queue", Value::text(queue.clone()));
1009                        result.push(record);
1010
1011                        return Ok(RuntimeQueryResult {
1012                            query: raw_query.to_string(),
1013                            mode: QueryMode::Sql,
1014                            statement: "queue_push",
1015                            engine: "runtime-queue",
1016                            result,
1017                            affected_rows: 1,
1018                            statement_type: "insert",
1019                            bookmark: None,
1020                            notice: None,
1021                        });
1022                    }
1023                }
1024                if let Some(max_size) = config.max_size {
1025                    let current_len =
1026                        load_queue_message_views_with_runtime(Some(self), store.as_ref(), queue)?
1027                            .len();
1028                    if current_len >= max_size {
1029                        return Err(RedDBError::Query(format!(
1030                            "queue '{}' is full (max_size={max_size})",
1031                            queue
1032                        )));
1033                    }
1034                }
1035
1036                let position = next_queue_position(store.as_ref(), queue, *side)?;
1037                let mut entity = UnifiedEntity::new(
1038                    EntityId::new(0),
1039                    EntityKind::QueueMessage {
1040                        queue: queue.clone(),
1041                        position,
1042                    },
1043                    EntityData::QueueMessage(QueueMessageData {
1044                        payload: value.clone(),
1045                        priority: if config.priority { *priority } else { None },
1046                        enqueued_at_ns: now_ns(),
1047                        attempts: 0,
1048                        max_attempts: config.max_attempts,
1049                        acked: false,
1050                    }),
1051                );
1052                // Phase 1.1 MVCC universal: stamp xmin so other
1053                // connections don't see this message until COMMIT.
1054                if let Some(xid) = self.current_xid() {
1055                    entity.set_xmin(xid);
1056                }
1057                // As for event inserts, reserve resident/WAL payload overlap
1058                // before publishing either the message or its metadata.
1059                let bytes = crate::storage::unified::memory_size::entity_bytes(&entity) as u64;
1060                let _admission = self.admit_non_evictable_growth(
1061                    crate::storage::memory_pools::MemoryPool::SegmentArena,
1062                    "queue push",
1063                    bytes.saturating_mul(3),
1064                )?;
1065                let id = store
1066                    .insert_auto(queue, entity)
1067                    .map_err(|err| RedDBError::Internal(err.to_string()))?;
1068                if let Some(dedup_key) = dedup_key.as_deref() {
1069                    let window_ms = config
1070                        .dedup_window_ms
1071                        .unwrap_or(reddb_rql::ast::DEFAULT_QUEUE_DEDUP_WINDOW_MS);
1072                    let expires_at_ns =
1073                        now_ns().saturating_add(window_ms.saturating_mul(1_000_000));
1074                    let metadata_id =
1075                        record_queue_dedup(store.as_ref(), queue, dedup_key, id, expires_at_ns)?;
1076                    self.stamp_xmin_if_in_txn(QUEUE_META_COLLECTION, metadata_id);
1077                    if self.current_xid().is_some() {
1078                        self.record_pending_queue_dedup(
1079                            current_connection_id(),
1080                            queue,
1081                            dedup_key,
1082                            metadata_id,
1083                        );
1084                    }
1085                }
1086                // Resolve per-message availability (issue #722): DELAY is
1087                // relative to the push instant, AVAILABLE AT carries an
1088                // absolute unix-ms. Both collapse to a unix-ns timestamp
1089                // delivery paths compare against. `None` means immediate.
1090                let available_at_ns = available.map(|a| match a {
1091                    reddb_rql::ast::QueueAvailability::DelayMs(ms) => {
1092                        now_ns().saturating_add(ms.saturating_mul(1_000_000))
1093                    }
1094                    reddb_rql::ast::QueueAvailability::AtUnixMs(ms) => ms.saturating_mul(1_000_000),
1095                });
1096                if config.ttl_ms.is_some() || available_at_ns.is_some() || key.is_some() {
1097                    store
1098                        .set_metadata(
1099                            queue,
1100                            id,
1101                            queue_message_metadata(config.ttl_ms, available_at_ns, key.as_deref()),
1102                        )
1103                        .map_err(|err| RedDBError::Internal(err.to_string()))?;
1104                }
1105                // Slice C of PRD #718 — wake `QUEUE READ … WAIT` waiters.
1106                // Under autocommit this fires immediately; inside a txn
1107                // the wake is buffered and replayed on COMMIT (rollback
1108                // discards it so rolled-back enqueues do not deliver).
1109                self.record_queue_wake(&queue_wait_scope(), queue);
1110                self.invalidate_result_cache();
1111
1112                let mut result = UnifiedResult::with_columns(vec![
1113                    "message_id".into(),
1114                    "side".into(),
1115                    "queue".into(),
1116                ]);
1117                let mut record = UnifiedRecord::new();
1118                record.set("message_id", Value::text(message_id_string(id)));
1119                record.set(
1120                    "side",
1121                    Value::text(match side {
1122                        QueueSide::Left => "left".to_string(),
1123                        QueueSide::Right => "right".to_string(),
1124                    }),
1125                );
1126                record.set("queue", Value::text(queue.clone()));
1127                result.push(record);
1128
1129                Ok(RuntimeQueryResult {
1130                    query: raw_query.to_string(),
1131                    mode: QueryMode::Sql,
1132                    statement: "queue_push",
1133                    engine: "runtime-queue",
1134                    result,
1135                    affected_rows: 1,
1136                    statement_type: "insert",
1137                    bookmark: None,
1138                    notice: None,
1139                })
1140            }
1141            QueueCommand::Pop { queue, side, count } => {
1142                let store = self.inner.db.store();
1143                ensure_queue_exists(store.as_ref(), queue)?;
1144                let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1145                let popped = lifecycle
1146                    .pop(queue, ast_side_to_lc(*side), *count, &txn)
1147                    .map_err(map_qse)?;
1148
1149                let mut result =
1150                    UnifiedResult::with_columns(vec!["message_id".into(), "payload".into()]);
1151                for (message_id, payload) in &popped {
1152                    let mut record = UnifiedRecord::new();
1153                    record.set(
1154                        "message_id",
1155                        Value::text(message_id_string(EntityId::new(*message_id))),
1156                    );
1157                    record.set("payload", payload.clone());
1158                    result.push(record);
1159                }
1160                let popped_count = popped.len() as u64;
1161                if popped_count > 0 {
1162                    self.invalidate_result_cache();
1163                }
1164
1165                Ok(RuntimeQueryResult {
1166                    query: raw_query.to_string(),
1167                    mode: QueryMode::Sql,
1168                    statement: "queue_pop",
1169                    engine: "runtime-queue",
1170                    result,
1171                    affected_rows: popped_count,
1172                    statement_type: "delete",
1173                    bookmark: None,
1174                    notice: None,
1175                })
1176            }
1177            QueueCommand::Peek { queue, count } => {
1178                let store = self.inner.db.store();
1179                ensure_queue_exists(store.as_ref(), queue)?;
1180                let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1181                let messages = lifecycle.peek(queue, *count, &txn);
1182
1183                let mut result =
1184                    UnifiedResult::with_columns(vec!["message_id".into(), "payload".into()]);
1185                for message in messages {
1186                    let mut record = UnifiedRecord::new();
1187                    record.set(
1188                        "message_id",
1189                        Value::text(message_id_string(EntityId::new(message.message_id))),
1190                    );
1191                    record.set("payload", message.payload);
1192                    result.push(record);
1193                }
1194
1195                Ok(RuntimeQueryResult {
1196                    query: raw_query.to_string(),
1197                    mode: QueryMode::Sql,
1198                    statement: "queue_peek",
1199                    engine: "runtime-queue",
1200                    result,
1201                    affected_rows: 0,
1202                    statement_type: "select",
1203                    bookmark: None,
1204                    notice: None,
1205                })
1206            }
1207            QueueCommand::Len { queue } => {
1208                let store = self.inner.db.store();
1209                ensure_queue_exists(store.as_ref(), queue)?;
1210                let count =
1211                    load_queue_message_views_with_runtime(Some(self), store.as_ref(), queue)?.len()
1212                        as u64;
1213                let mut result = UnifiedResult::with_columns(vec!["len".into()]);
1214                let mut record = UnifiedRecord::new();
1215                record.set("len", Value::UnsignedInteger(count));
1216                result.push(record);
1217
1218                Ok(RuntimeQueryResult {
1219                    query: raw_query.to_string(),
1220                    mode: QueryMode::Sql,
1221                    statement: "queue_len",
1222                    engine: "runtime-queue",
1223                    result,
1224                    affected_rows: 0,
1225                    statement_type: "select",
1226                    bookmark: None,
1227                    notice: None,
1228                })
1229            }
1230            QueueCommand::Purge { queue } => {
1231                let store = self.inner.db.store();
1232                ensure_queue_exists(store.as_ref(), queue)?;
1233                let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1234                let count = lifecycle.purge(queue, &txn).map_err(map_qse)?;
1235                if count > 0 {
1236                    self.invalidate_result_cache();
1237                }
1238
1239                Ok(RuntimeQueryResult::ok_message(
1240                    raw_query.to_string(),
1241                    &format!("{count} messages purged from queue '{queue}'"),
1242                    "delete",
1243                ))
1244            }
1245            QueueCommand::GroupCreate { queue, group } => {
1246                let store = self.inner.db.store();
1247                ensure_queue_exists(store.as_ref(), queue)?;
1248                if queue_group_exists(store.as_ref(), queue, group)? {
1249                    return Ok(RuntimeQueryResult::ok_message(
1250                        raw_query.to_string(),
1251                        &format!(
1252                            "consumer group '{}' already exists on queue '{}'",
1253                            group, queue
1254                        ),
1255                        "create",
1256                    ));
1257                }
1258                save_queue_group(store.as_ref(), queue, group)?;
1259                self.invalidate_result_cache();
1260
1261                Ok(RuntimeQueryResult::ok_message(
1262                    raw_query.to_string(),
1263                    &format!("consumer group '{}' created on queue '{}'", group, queue),
1264                    "create",
1265                ))
1266            }
1267            QueueCommand::GroupRead {
1268                queue,
1269                group,
1270                consumer,
1271                count,
1272                wait_ms,
1273            } => {
1274                let store = self.inner.db.store();
1275                ensure_queue_exists(store.as_ref(), queue)?;
1276                // Slice B of PRD #718: reject `WAIT` issued inside an
1277                // explicit transaction, and reject `WAIT > cap` before
1278                // any waiter is registered. Both checks fire before the
1279                // lifecycle is touched so a refused statement leaves
1280                // no side effects (no group auto-create, no parking).
1281                if let Some(ms) = *wait_ms {
1282                    if self.current_xid().is_some() {
1283                        return Err(RedDBError::Query(
1284                            "QUEUE READ … WAIT is autocommit-only: refusing to park inside an explicit transaction (BEGIN/COMMIT)"
1285                                .to_string(),
1286                        ));
1287                    }
1288                    let cap =
1289                        self.config_u64(QUEUE_MAX_WAIT_MS_CONFIG_KEY, QUEUE_MAX_WAIT_MS_DEFAULT);
1290                    if ms > cap {
1291                        return Err(RedDBError::Query(format!(
1292                            "QUEUE READ … WAIT {ms}ms exceeds server cap {QUEUE_MAX_WAIT_MS_CONFIG_KEY} = {cap}ms"
1293                        )));
1294                    }
1295                }
1296                // Resolve the consumer group up-front so the lifecycle
1297                // sees the same auto-created `_work_default` / fanout
1298                // group the legacy `read_messages` would have minted.
1299                let config = load_queue_config(store.as_ref(), queue);
1300                let group_owned =
1301                    resolve_read_group(store.as_ref(), queue, group.as_deref(), consumer, &config)?;
1302                let group_ref = group_owned.as_str();
1303                let delivered = self
1304                    .group_read_with_optional_wait(queue, group_ref, consumer, *count, *wait_ms)?;
1305
1306                // Issue #742 — record consumer presence on every read,
1307                // including empty returns. Heartbeat-driven aliveness
1308                // is the contract; pending deliveries don't define it.
1309                {
1310                    let lease_count = u32::try_from(delivered.len()).unwrap_or(u32::MAX);
1311                    let now_ns = std::time::SystemTime::now()
1312                        .duration_since(std::time::UNIX_EPOCH)
1313                        .map(|d| d.as_nanos() as u64)
1314                        .unwrap_or(0);
1315                    self.queue_presence().heartbeat(
1316                        queue,
1317                        group_ref,
1318                        consumer,
1319                        lease_count,
1320                        now_ns,
1321                    );
1322                }
1323
1324                let mut result = UnifiedResult::with_columns(vec![
1325                    "message_id".into(),
1326                    "payload".into(),
1327                    "consumer".into(),
1328                    "delivery_count".into(),
1329                    "attempts".into(),
1330                ]);
1331
1332                for message in delivered {
1333                    let mut record = UnifiedRecord::new();
1334                    record.set(
1335                        "message_id",
1336                        Value::text(message_id_string(EntityId::new(message.message_id))),
1337                    );
1338                    record.set("payload", message.payload);
1339                    record.set("consumer", Value::text(message.consumer));
1340                    record.set(
1341                        "delivery_count",
1342                        Value::UnsignedInteger(u64::from(message.delivery_count)),
1343                    );
1344                    record.set(
1345                        "attempts",
1346                        Value::UnsignedInteger(u64::from(message.delivery_count)),
1347                    );
1348                    result.push(record);
1349                }
1350                if !result.records.is_empty() {
1351                    self.invalidate_result_cache();
1352                }
1353
1354                Ok(RuntimeQueryResult {
1355                    query: raw_query.to_string(),
1356                    mode: QueryMode::Sql,
1357                    statement: "queue_group_read",
1358                    engine: "runtime-queue",
1359                    result,
1360                    affected_rows: 0,
1361                    statement_type: "select",
1362                    bookmark: None,
1363                    notice: None,
1364                })
1365            }
1366            QueueCommand::Pending { queue, group } => {
1367                let store = self.inner.db.store();
1368                ensure_queue_exists(store.as_ref(), queue)?;
1369                require_queue_group(store.as_ref(), queue, group)?;
1370                let mut pending = load_pending_entries(store.as_ref(), queue, Some(group), None)?;
1371                pending.sort_by_key(|entry| entry.delivered_at_ns);
1372                let current_time_ns = now_ns();
1373
1374                let mut result = UnifiedResult::with_columns(vec![
1375                    "message_id".into(),
1376                    "consumer".into(),
1377                    "delivered_at_ns".into(),
1378                    "delivery_count".into(),
1379                    "idle_ms".into(),
1380                ]);
1381                for entry in pending {
1382                    let mut record = UnifiedRecord::new();
1383                    record.set(
1384                        "message_id",
1385                        Value::text(message_id_string(entry.message_id)),
1386                    );
1387                    record.set("consumer", Value::text(entry.consumer));
1388                    record.set(
1389                        "delivered_at_ns",
1390                        Value::UnsignedInteger(entry.delivered_at_ns),
1391                    );
1392                    record.set(
1393                        "delivery_count",
1394                        Value::UnsignedInteger(u64::from(entry.delivery_count)),
1395                    );
1396                    record.set(
1397                        "idle_ms",
1398                        Value::UnsignedInteger(
1399                            current_time_ns.saturating_sub(entry.delivered_at_ns) / 1_000_000,
1400                        ),
1401                    );
1402                    result.push(record);
1403                }
1404
1405                Ok(RuntimeQueryResult {
1406                    query: raw_query.to_string(),
1407                    mode: QueryMode::Sql,
1408                    statement: "queue_pending",
1409                    engine: "runtime-queue",
1410                    result,
1411                    affected_rows: 0,
1412                    statement_type: "select",
1413                    bookmark: None,
1414                    notice: None,
1415                })
1416            }
1417            QueueCommand::Claim {
1418                queue,
1419                group,
1420                consumer,
1421                min_idle_ms,
1422            } => {
1423                let store = self.inner.db.store();
1424                ensure_queue_exists(store.as_ref(), queue)?;
1425                require_queue_group(store.as_ref(), queue, group)?;
1426                let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1427                let delivered = lifecycle
1428                    .claim_delivering(queue, consumer, *min_idle_ms, &txn)
1429                    .map_err(map_qse)?;
1430
1431                let mut result = UnifiedResult::with_columns(vec![
1432                    "message_id".into(),
1433                    "delivery_id".into(),
1434                    "payload".into(),
1435                    "consumer".into(),
1436                    "delivery_count".into(),
1437                ]);
1438
1439                for message in delivered {
1440                    let mut record = UnifiedRecord::new();
1441                    record.set(
1442                        "message_id",
1443                        Value::text(message_id_string(EntityId::new(message.message_id))),
1444                    );
1445                    record.set("delivery_id", Value::text(message.delivery_id));
1446                    record.set("payload", message.payload);
1447                    record.set("consumer", Value::text(message.consumer));
1448                    record.set(
1449                        "delivery_count",
1450                        Value::UnsignedInteger(u64::from(message.delivery_count)),
1451                    );
1452                    result.push(record);
1453                }
1454                if !result.records.is_empty() {
1455                    self.invalidate_result_cache();
1456                }
1457                let affected_rows = result.records.len() as u64;
1458
1459                Ok(RuntimeQueryResult {
1460                    query: raw_query.to_string(),
1461                    mode: QueryMode::Sql,
1462                    statement: "queue_claim",
1463                    engine: "runtime-queue",
1464                    result,
1465                    affected_rows,
1466                    statement_type: "update",
1467                    bookmark: None,
1468                    notice: None,
1469                })
1470            }
1471            QueueCommand::Ack {
1472                queue,
1473                group,
1474                message_id,
1475                delivery_id,
1476            } => {
1477                let store = self.inner.db.store();
1478                ensure_queue_exists(store.as_ref(), queue)?;
1479                let (group_owned, message_entity) = resolve_ack_nack_handle(
1480                    store.as_ref(),
1481                    queue,
1482                    group,
1483                    message_id,
1484                    delivery_id.as_deref(),
1485                )?;
1486                let group_ref = group_owned.as_str();
1487                require_queue_group(store.as_ref(), queue, group_ref)?;
1488                let (lifecycle, ps, txn) = runtime_lifecycle(self, queue);
1489                let did = match delivery_id.as_deref() {
1490                    Some(d) => d.to_string(),
1491                    None => ps
1492                        .find_pending_by_key(queue, message_entity.raw(), group_ref)
1493                        .ok_or_else(|| {
1494                            RedDBError::NotFound(format!(
1495                                "no pending delivery for message '{}' on queue '{}' (group '{}')",
1496                                message_entity.raw(),
1497                                queue,
1498                                group_ref
1499                            ))
1500                        })?,
1501                };
1502                lifecycle.ack(&txn, &did).map_err(map_qse)?;
1503                self.invalidate_result_cache();
1504
1505                Ok(RuntimeQueryResult::ok_message(
1506                    raw_query.to_string(),
1507                    "message acknowledged",
1508                    "update",
1509                ))
1510            }
1511            QueueCommand::Nack {
1512                queue,
1513                group,
1514                message_id,
1515                delivery_id,
1516                delay_ms,
1517            } => {
1518                let store = self.inner.db.store();
1519                ensure_queue_exists(store.as_ref(), queue)?;
1520                let config = load_queue_config(store.as_ref(), queue);
1521                // Issue #723: a per-failure DELAY override is a write
1522                // operation that re-shapes retry behavior; readers must
1523                // not be able to silently re-schedule another worker's
1524                // work. Embedded callers (no auth identity attached)
1525                // are trusted and bypass the check.
1526                if delay_ms.is_some() {
1527                    if let Some((_, role)) = current_auth_identity() {
1528                        if !role.can_write() {
1529                            return Err(RedDBError::InvalidOperation(format!(
1530                                "role '{role}' is not authorized to override NACK retry delay on queue '{queue}'"
1531                            )));
1532                        }
1533                    }
1534                }
1535                let (group_owned, message_entity) = resolve_ack_nack_handle(
1536                    store.as_ref(),
1537                    queue,
1538                    group,
1539                    message_id,
1540                    delivery_id.as_deref(),
1541                )?;
1542                let group_ref = group_owned.as_str();
1543                require_queue_group(store.as_ref(), queue, group_ref)?;
1544                let (lifecycle, ps, txn) = runtime_lifecycle(self, queue);
1545                let did = match delivery_id.as_deref() {
1546                    Some(d) => d.to_string(),
1547                    None => ps
1548                        .find_pending_by_key(queue, message_entity.raw(), group_ref)
1549                        .ok_or_else(|| {
1550                            RedDBError::NotFound(format!(
1551                                "no pending delivery for message '{}' on queue '{}' (group '{}')",
1552                                message_entity.raw(),
1553                                queue,
1554                                group_ref
1555                            ))
1556                        })?,
1557                };
1558                // Resolve the effective retry delay: per-failure
1559                // override wins, then queue default, then zero
1560                // (immediate requeue — pre-#723 behavior).
1561                let effective_delay_ms = delay_ms.or(config.retry_delay_ms).unwrap_or(0);
1562                let pending_attempt = ps.read_pending_attempt(&did).map_err(map_qse)?;
1563                let nack_attempts = pending_attempt.attempts.saturating_add(1);
1564                let retry_available_at_ns = if effective_delay_ms > 0 {
1565                    Some(now_ns().saturating_add(effective_delay_ms.saturating_mul(1_000_000)))
1566                } else {
1567                    None
1568                };
1569                let retry_deadline = if effective_delay_ms > 0 {
1570                    Some(
1571                        std::time::Instant::now()
1572                            + std::time::Duration::from_millis(effective_delay_ms),
1573                    )
1574                } else {
1575                    None
1576                };
1577                let outcome = lifecycle
1578                    .nack_with_retry_deadline(&txn, &did, retry_deadline)
1579                    .map_err(map_qse)?;
1580                // Apply delay only when the message was actually
1581                // requeued — DLQ promotion / drop terminate the
1582                // retry cycle and a delay would be meaningless.
1583                if matches!(outcome, RetirementOutcome::Requeued) && effective_delay_ms > 0 {
1584                    set_message_available_at_ns(
1585                        store.as_ref(),
1586                        queue,
1587                        message_entity,
1588                        retry_available_at_ns,
1589                        config.ttl_ms,
1590                    )?;
1591                }
1592                // Issue #723: routine retries do not flood audit
1593                // channels (telemetry already covers them via
1594                // `queue_nacked_total{outcome=...}`). Significant
1595                // overrides — large delays, destination changes,
1596                // drops — are audited so operators see the events
1597                // that re-shape operational risk.
1598                self.maybe_emit_nack_audit(
1599                    queue,
1600                    group_ref,
1601                    &did,
1602                    *delay_ms,
1603                    config.retry_delay_ms,
1604                    &outcome,
1605                );
1606                let outcome_label = match &outcome {
1607                    RetirementOutcome::Requeued => NackOutcomeLabel::Retry,
1608                    RetirementOutcome::MovedToDlq(_) => NackOutcomeLabel::Dlq,
1609                    RetirementOutcome::Dropped => NackOutcomeLabel::Drop,
1610                };
1611                self.queue_telemetry().record_nacked(
1612                    queue,
1613                    group_ref,
1614                    config.mode.as_str(),
1615                    outcome_label,
1616                );
1617                if let RetirementOutcome::MovedToDlq(dlq) = &outcome {
1618                    OperatorEvent::QueueDlqPromoted {
1619                        queue: queue.to_string(),
1620                        group: group_ref.to_string(),
1621                        dlq: dlq.clone(),
1622                        message_id: pending_attempt.message_id,
1623                        attempts: nack_attempts,
1624                        reason: format!("lifecycle_nack:{did}"),
1625                    }
1626                    .emit(self.audit_log());
1627                }
1628                let message = match outcome {
1629                    RetirementOutcome::Requeued => {
1630                        if effective_delay_ms > 0 {
1631                            format!("message requeued (delay={effective_delay_ms}ms)")
1632                        } else {
1633                            "message requeued".to_string()
1634                        }
1635                    }
1636                    RetirementOutcome::MovedToDlq(dlq) => {
1637                        format!("message moved to dead-letter queue '{}'", dlq)
1638                    }
1639                    RetirementOutcome::Dropped => "message dropped after max attempts".to_string(),
1640                };
1641                self.invalidate_result_cache();
1642
1643                Ok(RuntimeQueryResult::ok_message(
1644                    raw_query.to_string(),
1645                    &message,
1646                    "update",
1647                ))
1648            }
1649            QueueCommand::Move {
1650                source,
1651                destination,
1652                filter,
1653                limit,
1654            } => self.execute_queue_move(raw_query, source, destination, filter.as_ref(), *limit),
1655        }
1656    }
1657
1658    pub fn execute_queue_select(
1659        &self,
1660        raw_query: &str,
1661        query: &QueueSelectQuery,
1662    ) -> RedDBResult<RuntimeQueryResult> {
1663        let store = self.inner.db.store();
1664        ensure_queue_exists(store.as_ref(), &query.queue)?;
1665        let config = load_queue_config(store.as_ref(), &query.queue);
1666        let dlq = queue_is_dead_letter_target(store.as_ref(), &query.queue);
1667        let columns = if query.columns.is_empty() {
1668            queue_projection_default_columns()
1669        } else {
1670            query.columns.clone()
1671        };
1672
1673        let mut messages =
1674            load_queue_message_views_with_runtime(Some(self), store.as_ref(), &query.queue)?;
1675        sort_queue_messages(&mut messages, &config, QueueSide::Left);
1676
1677        let mut result = UnifiedResult::with_columns(columns.clone());
1678        for message in messages {
1679            if query
1680                .filter
1681                .as_ref()
1682                .is_some_and(|filter| !queue_message_matches_filter(&message, dlq, filter))
1683            {
1684                continue;
1685            }
1686            let record = queue_projection_record(&columns, &message, dlq)?;
1687            result.push(record);
1688            if query
1689                .limit
1690                .is_some_and(|limit| result.records.len() >= limit as usize)
1691            {
1692                break;
1693            }
1694        }
1695
1696        Ok(RuntimeQueryResult {
1697            query: raw_query.to_string(),
1698            mode: QueryMode::Sql,
1699            statement: "queue_select",
1700            engine: "runtime-queue",
1701            result,
1702            affected_rows: 0,
1703            statement_type: "select",
1704            bookmark: None,
1705            notice: None,
1706        })
1707    }
1708
1709    fn execute_queue_move(
1710        &self,
1711        raw_query: &str,
1712        source: &str,
1713        destination: &str,
1714        filter: Option<&Filter>,
1715        limit: usize,
1716    ) -> RedDBResult<RuntimeQueryResult> {
1717        if source == destination {
1718            return Err(RedDBError::Query(
1719                "QUEUE MOVE source and destination must be different".to_string(),
1720            ));
1721        }
1722        let store = self.inner.db.store();
1723        ensure_queue_exists(store.as_ref(), source)?;
1724        ensure_queue_exists(store.as_ref(), destination)?;
1725        let source_config = load_queue_config(store.as_ref(), source);
1726        let destination_config = load_queue_config(store.as_ref(), destination);
1727        let source_dlq = queue_is_dead_letter_target(store.as_ref(), source);
1728
1729        let mut messages =
1730            load_queue_message_views_with_runtime(Some(self), store.as_ref(), source)?;
1731        sort_queue_messages(&mut messages, &source_config, QueueSide::Left);
1732        let selected = messages
1733            .into_iter()
1734            .filter(|message| {
1735                filter
1736                    .map(|f| queue_message_matches_filter(message, source_dlq, f))
1737                    .unwrap_or(true)
1738            })
1739            .take(limit)
1740            .collect::<Vec<_>>();
1741
1742        if let Some(max_size) = destination_config.max_size {
1743            let current_len =
1744                load_queue_message_views_with_runtime(Some(self), store.as_ref(), destination)?
1745                    .len();
1746            if current_len + selected.len() > max_size {
1747                return Err(RedDBError::Query(format!(
1748                    "queue '{}' is full (max_size={max_size})",
1749                    destination
1750                )));
1751            }
1752        }
1753
1754        for message in &selected {
1755            let lock = queue_message_lock_handle(self, source, message.id);
1756            let Some(_guard) = lock.try_lock() else {
1757                return Err(RedDBError::Query(format!(
1758                    "message '{}' is locked on queue '{}'",
1759                    message.id.raw(),
1760                    source
1761                )));
1762            };
1763            if queue_message_view_by_id(store.as_ref(), source, message.id)?.is_none() {
1764                return Err(RedDBError::Query(format!(
1765                    "message '{}' is no longer available on queue '{}'",
1766                    message.id.raw(),
1767                    source
1768                )));
1769            }
1770        }
1771
1772        let mut inserted = Vec::new();
1773        for message in &selected {
1774            match insert_moved_queue_message(
1775                store.as_ref(),
1776                destination,
1777                &destination_config,
1778                message,
1779            ) {
1780                Ok(id) => inserted.push(id),
1781                Err(err) => {
1782                    for id in inserted {
1783                        let _ = store.delete(destination, id);
1784                    }
1785                    return Err(err);
1786                }
1787            }
1788        }
1789
1790        let (move_lifecycle, _move_ps, move_txn) = runtime_lifecycle(self, source);
1791        for message in &selected {
1792            move_lifecycle
1793                .delete_with_state(source, message.id.raw(), &move_txn)
1794                .map_err(map_qse)?;
1795        }
1796        if !selected.is_empty() {
1797            self.invalidate_result_cache();
1798        }
1799
1800        let selected_count = selected.len() as u64;
1801        self.audit_log().record_event(
1802            AuditEvent::builder("queue/move")
1803                .source(AuditAuthSource::System)
1804                .outcome(Outcome::Success)
1805                .resource(format!("queue:{source}->{destination}"))
1806                .fields([
1807                    AuditFieldEscaper::field("source", source),
1808                    AuditFieldEscaper::field("destination", destination),
1809                    AuditFieldEscaper::field("selected", selected_count),
1810                    AuditFieldEscaper::field("committed", selected_count),
1811                ])
1812                .build(),
1813        );
1814
1815        let mut result = UnifiedResult::with_columns(vec![
1816            "source".into(),
1817            "destination".into(),
1818            "selected".into(),
1819            "committed".into(),
1820        ]);
1821        let mut record = UnifiedRecord::new();
1822        record.set("source", Value::text(source.to_string()));
1823        record.set("destination", Value::text(destination.to_string()));
1824        record.set("selected", Value::UnsignedInteger(selected_count));
1825        record.set("committed", Value::UnsignedInteger(selected_count));
1826        result.push(record);
1827
1828        Ok(RuntimeQueryResult {
1829            query: raw_query.to_string(),
1830            mode: QueryMode::Sql,
1831            statement: "queue_move",
1832            engine: "runtime-queue",
1833            result,
1834            affected_rows: selected_count,
1835            statement_type: "update",
1836            bookmark: None,
1837            notice: None,
1838        })
1839    }
1840
1841    /// Issue #723: routine retries are observable through metrics
1842    /// (`queue_nacked_total{outcome=...}`) so this is the audit
1843    /// shoulder for the *non-routine* cases: explicit NACK delay
1844    /// overrides whose magnitude or destination-changing impact would
1845    /// be invisible in metrics alone. Specifically:
1846    ///
1847    /// - Explicit override ≥ 60s (a worker decided to defer well past
1848    ///   the queue's default cadence — operators care).
1849    /// - Override that lands on a DLQ promotion or drop (destination
1850    ///   changed; the override may have influenced retire-vs-requeue
1851    ///   accounting on the caller's side and the audit trail needs to
1852    ///   show who asked).
1853    ///
1854    /// Calls with no override are intentionally silent here.
1855    fn maybe_emit_nack_audit(
1856        &self,
1857        queue: &str,
1858        group: &str,
1859        delivery_id: &str,
1860        override_ms: Option<u64>,
1861        default_ms: Option<u64>,
1862        outcome: &RetirementOutcome,
1863    ) {
1864        let Some(override_ms) = override_ms else {
1865            return;
1866        };
1867        let outcome_label = match outcome {
1868            RetirementOutcome::Requeued => "requeued",
1869            RetirementOutcome::MovedToDlq(_) => "dlq",
1870            RetirementOutcome::Dropped => "dropped",
1871        };
1872        const SIGNIFICANT_DELAY_MS: u64 = 60_000;
1873        let destination_changed = !matches!(outcome, RetirementOutcome::Requeued);
1874        if override_ms < SIGNIFICANT_DELAY_MS && !destination_changed {
1875            return;
1876        }
1877        self.audit_log().record_event(
1878            AuditEvent::builder("queue/nack/override")
1879                .source(AuditAuthSource::System)
1880                .outcome(Outcome::Success)
1881                .resource(format!("queue:{queue}"))
1882                .fields([
1883                    AuditFieldEscaper::field("queue", queue),
1884                    AuditFieldEscaper::field("group", group),
1885                    AuditFieldEscaper::field("delivery_id", delivery_id),
1886                    AuditFieldEscaper::field("override_delay_ms", override_ms),
1887                    AuditFieldEscaper::field("default_delay_ms", default_ms.unwrap_or(0)),
1888                    AuditFieldEscaper::field("outcome", outcome_label),
1889                ])
1890                .build(),
1891        );
1892    }
1893
1894    /// Whether `collection` is declared as a queue model. Used to route a
1895    /// `CLAIM` through the [`QueueLifecycle`] seam instead of the raw
1896    /// row-update path (#1609).
1897    pub(super) fn is_queue_collection(&self, collection: &str) -> bool {
1898        self.db()
1899            .collection_contract_arc(collection)
1900            .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Queue)
1901            .unwrap_or(false)
1902    }
1903
1904    /// Route a queue-collection `CLAIM` through the [`QueueLifecycle`]
1905    /// delivery seam (ADR 0020, #1609).
1906    ///
1907    /// A `CLAIM` on a queue collection is a delivery *acquisition* — select
1908    /// and lock pending messages — not a raw UPDATE of the underlying queue
1909    /// rows. Dispatching here keeps the `QueueLifecycle` state machine
1910    /// (ACK/NACK, retry, DLQ, pending delivery) the sole
1911    /// authority for delivery state, instead of `execute_update_inner_tracked`
1912    /// mutating queue storage directly.
1913    ///
1914    /// Shapes the delivery seam cannot express are rejected up front with a
1915    /// clear [`RedDBError::InvalidOperation`] rather than silently falling
1916    /// back to raw storage mutation (see [`validate_queue_claim_shape`]).
1917    pub(super) fn execute_queue_shaped_claim(
1918        &self,
1919        raw_query: &str,
1920        query: &UpdateQuery,
1921    ) -> RedDBResult<RuntimeQueryResult> {
1922        validate_queue_claim_shape(query)?;
1923
1924        let queue = query.table.as_str();
1925        let store = self.inner.db.store();
1926        ensure_queue_exists(store.as_ref(), queue)?;
1927
1928        // A queue-shaped CLAIM maps onto WORK-mode delivery: each message is
1929        // reserved for exactly one consumer. FANOUT queues fan every message
1930        // to every group, which a single-target CLAIM cannot express — those
1931        // must be consumed through GROUP READ.
1932        let config = load_queue_config(store.as_ref(), queue);
1933        if config.mode != QueueMode::Work {
1934            return Err(RedDBError::InvalidOperation(format!(
1935                "CLAIM on queue '{queue}' cannot be expressed: FANOUT delivery \
1936                 must be consumed through GROUP READ, not a queue-shaped CLAIM"
1937            )));
1938        }
1939
1940        // Attribute the delivery to the WORK default consumer group (the same
1941        // group an unqualified `QUEUE READ` uses), so the pending locks the
1942        // lifecycle records resolve for a later ACK/NACK by delivery id.
1943        let group = resolve_read_group(store.as_ref(), queue, None, "", &config)?;
1944        let count = query.claim_limit.unwrap_or(0) as usize;
1945        let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1946        let delivered = lifecycle
1947            .deliver(&txn, queue, &group, count)
1948            .map_err(map_qse)?;
1949
1950        let mut result = UnifiedResult::with_columns(vec!["delivery_id".into(), "payload".into()]);
1951        for message in &delivered {
1952            let mut record = UnifiedRecord::new();
1953            record.set("delivery_id", Value::text(message.delivery_id.clone()));
1954            record.set("payload", message.payload.clone());
1955            result.push(record);
1956        }
1957        let affected_rows = delivered.len() as u64;
1958        if affected_rows > 0 {
1959            self.invalidate_result_cache();
1960        }
1961
1962        Ok(RuntimeQueryResult {
1963            query: raw_query.to_string(),
1964            mode: QueryMode::Sql,
1965            statement: "queue_claim",
1966            engine: "runtime-queue",
1967            result,
1968            affected_rows,
1969            statement_type: "update",
1970            bookmark: None,
1971            notice: None,
1972        })
1973    }
1974}
1975
1976/// Reject `CLAIM` shapes that [`QueueLifecycle`] delivery cannot express
1977/// (#1609). Queue delivery is strictly FIFO (oldest-available first) and
1978/// acquires *up to* N available messages:
1979///
1980/// - a descending `ORDER BY` contradicts FIFO delivery order, and
1981/// - `CLAIM EXACT` demands an all-or-nothing batch the delivery seam does
1982///   not offer.
1983///
1984/// Both surface a clear [`RedDBError::InvalidOperation`] instead of a
1985/// silent raw storage mutation.
1986fn validate_queue_claim_shape(query: &UpdateQuery) -> RedDBResult<()> {
1987    if query.order_by.iter().any(|clause| !clause.ascending) {
1988        return Err(RedDBError::InvalidOperation(format!(
1989            "CLAIM on queue '{}' cannot be expressed: a descending ORDER BY \
1990             conflicts with FIFO queue delivery order",
1991            query.table
1992        )));
1993    }
1994    if query.claim_exact {
1995        return Err(RedDBError::InvalidOperation(format!(
1996            "CLAIM EXACT on queue '{}' cannot be expressed: queue delivery \
1997             acquires up to N available messages, not an exact-or-nothing batch",
1998            query.table
1999        )));
2000    }
2001    Ok(())
2002}
2003
2004fn ensure_queue_exists(store: &UnifiedStore, queue: &str) -> RedDBResult<()> {
2005    if store.get_collection(queue).is_some() {
2006        Ok(())
2007    } else {
2008        Err(RedDBError::NotFound(format!("queue '{}' not found", queue)))
2009    }
2010}
2011
2012pub(super) fn load_queue_config(store: &UnifiedStore, queue: &str) -> QueueRuntimeConfig {
2013    let default = QueueRuntimeConfig {
2014        mode: QueueMode::Work,
2015        priority: false,
2016        max_size: None,
2017        ttl_ms: None,
2018        dlq: None,
2019        max_attempts: reddb_rql::ast::DEFAULT_QUEUE_MAX_ATTEMPTS,
2020        lock_deadline_ms: reddb_rql::ast::DEFAULT_QUEUE_LOCK_DEADLINE_MS,
2021        in_flight_cap_per_group: reddb_rql::ast::DEFAULT_QUEUE_IN_FLIGHT_CAP_PER_GROUP,
2022        retry_delay_ms: None,
2023        dedup_window_ms: None,
2024    };
2025
2026    let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2027        return default;
2028    };
2029    manager
2030        .query_all(|entity| {
2031            entity.data.as_row().is_some_and(|row| {
2032                row_text(row, "kind").as_deref() == Some("queue_config")
2033                    && row_text(row, "queue").as_deref() == Some(queue)
2034            })
2035        })
2036        .into_iter()
2037        .find_map(|entity| {
2038            let row = entity.data.as_row()?;
2039            Some(QueueRuntimeConfig {
2040                mode: row_text(row, "mode")
2041                    .as_deref()
2042                    .and_then(QueueMode::parse)
2043                    .unwrap_or_default(),
2044                priority: row_bool(row, "priority").unwrap_or(false),
2045                max_size: row_u64(row, "max_size").map(|value| value as usize),
2046                ttl_ms: row_u64(row, "ttl_ms"),
2047                dlq: row_text(row, "dlq"),
2048                max_attempts: row_u64(row, "max_attempts")
2049                    .map(|value| value as u32)
2050                    .unwrap_or(reddb_rql::ast::DEFAULT_QUEUE_MAX_ATTEMPTS),
2051                lock_deadline_ms: row_u64(row, "lock_deadline_ms")
2052                    .unwrap_or(reddb_rql::ast::DEFAULT_QUEUE_LOCK_DEADLINE_MS),
2053                in_flight_cap_per_group: row_u64(row, "in_flight_cap_per_group")
2054                    .map(|value| value as u32)
2055                    .unwrap_or(reddb_rql::ast::DEFAULT_QUEUE_IN_FLIGHT_CAP_PER_GROUP),
2056                retry_delay_ms: row_u64(row, "retry_delay_ms").filter(|v| *v > 0),
2057                dedup_window_ms: row_u64(row, "dedup_window_ms"),
2058            })
2059        })
2060        .unwrap_or(default)
2061}
2062
2063pub(super) fn queue_mode_str(store: &UnifiedStore, queue: &str) -> &'static str {
2064    load_queue_config(store, queue).mode.as_str()
2065}
2066
2067fn save_queue_config(
2068    store: &UnifiedStore,
2069    queue: &str,
2070    config: &QueueRuntimeConfig,
2071) -> RedDBResult<()> {
2072    remove_meta_rows(store, |row| {
2073        row_text(row, "kind").as_deref() == Some("queue_config")
2074            && row_text(row, "queue").as_deref() == Some(queue)
2075    });
2076
2077    let mut fields = HashMap::new();
2078    fields.insert("kind".to_string(), Value::text("queue_config".to_string()));
2079    fields.insert("queue".to_string(), Value::text(queue.to_string()));
2080    fields.insert(
2081        "mode".to_string(),
2082        Value::text(config.mode.as_str().to_string()),
2083    );
2084    fields.insert("priority".to_string(), Value::Boolean(config.priority));
2085    fields.insert(
2086        "max_size".to_string(),
2087        config
2088            .max_size
2089            .map(|value| Value::UnsignedInteger(value as u64))
2090            .unwrap_or(Value::Null),
2091    );
2092    fields.insert(
2093        "ttl_ms".to_string(),
2094        config
2095            .ttl_ms
2096            .map(Value::UnsignedInteger)
2097            .unwrap_or(Value::Null),
2098    );
2099    fields.insert(
2100        "dlq".to_string(),
2101        config.dlq.clone().map(Value::text).unwrap_or(Value::Null),
2102    );
2103    fields.insert(
2104        "max_attempts".to_string(),
2105        Value::UnsignedInteger(u64::from(config.max_attempts)),
2106    );
2107    fields.insert(
2108        "lock_deadline_ms".to_string(),
2109        Value::UnsignedInteger(config.lock_deadline_ms),
2110    );
2111    fields.insert(
2112        "in_flight_cap_per_group".to_string(),
2113        Value::UnsignedInteger(u64::from(config.in_flight_cap_per_group)),
2114    );
2115    fields.insert(
2116        "retry_delay_ms".to_string(),
2117        config
2118            .retry_delay_ms
2119            .map(Value::UnsignedInteger)
2120            .unwrap_or(Value::Null),
2121    );
2122    fields.insert(
2123        "dedup_window_ms".to_string(),
2124        config
2125            .dedup_window_ms
2126            .map(Value::UnsignedInteger)
2127            .unwrap_or(Value::Null),
2128    );
2129    insert_meta_row(store, fields).map(|_| ())
2130}
2131
2132fn remove_queue_metadata(store: &UnifiedStore, queue: &str) {
2133    remove_meta_rows(store, |row| {
2134        row_text(row, "queue").as_deref() == Some(queue)
2135    });
2136}
2137
2138fn queue_group_exists(store: &UnifiedStore, queue: &str, group: &str) -> RedDBResult<bool> {
2139    Ok(load_queue_groups(store, queue)?
2140        .into_iter()
2141        .any(|entry| entry.group == group))
2142}
2143
2144pub(super) fn require_queue_group(
2145    store: &UnifiedStore,
2146    queue: &str,
2147    group: &str,
2148) -> RedDBResult<()> {
2149    if queue_group_exists(store, queue, group)? {
2150        Ok(())
2151    } else {
2152        Err(RedDBError::NotFound(format!(
2153            "consumer group '{}' not found on queue '{}'",
2154            group, queue
2155        )))
2156    }
2157}
2158
2159pub(super) fn resolve_read_group(
2160    store: &UnifiedStore,
2161    queue: &str,
2162    group: Option<&str>,
2163    consumer: &str,
2164    config: &QueueRuntimeConfig,
2165) -> RedDBResult<String> {
2166    if let Some(group) = group {
2167        require_queue_group(store, queue, group)?;
2168        return Ok(group.to_string());
2169    }
2170
2171    match config.mode {
2172        QueueMode::Work => {
2173            if !queue_group_exists(store, queue, WORK_DEFAULT_GROUP)? {
2174                save_queue_group(store, queue, WORK_DEFAULT_GROUP)?;
2175            }
2176            Ok(WORK_DEFAULT_GROUP.to_string())
2177        }
2178        QueueMode::Fanout => {
2179            let fanout_group = format!("{FANOUT_GROUP_PREFIX}{consumer}");
2180            if !queue_group_exists(store, queue, &fanout_group)? {
2181                save_queue_group(store, queue, &fanout_group)?;
2182            }
2183            Ok(fanout_group)
2184        }
2185    }
2186}
2187
2188fn load_queue_groups(store: &UnifiedStore, queue: &str) -> RedDBResult<Vec<QueueGroupEntry>> {
2189    let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2190        return Ok(Vec::new());
2191    };
2192    let _publication = manager
2193        .has_backing()
2194        .then(|| store.vault_publication_lock.lock());
2195    let mut groups = Vec::new();
2196    super::function_budget::try_scan(
2197        |visit| manager.try_for_each_entity(usize::MAX, visit),
2198        |entity| {
2199            let Some(row) = entity.data.as_row() else {
2200                return true;
2201            };
2202            if row_text(row, "kind").as_deref() == Some("queue_group")
2203                && row_text(row, "queue").as_deref() == Some(queue)
2204            {
2205                let Some(group) = row_text(row, "group") else {
2206                    return true;
2207                };
2208                groups.push(QueueGroupEntry {
2209                    entity_id: entity.id,
2210                    group,
2211                });
2212            }
2213            true
2214        },
2215    )?;
2216    Ok(groups)
2217}
2218
2219fn save_queue_group(store: &UnifiedStore, queue: &str, group: &str) -> RedDBResult<()> {
2220    let mut fields = HashMap::new();
2221    fields.insert("kind".to_string(), Value::text("queue_group".to_string()));
2222    fields.insert("queue".to_string(), Value::text(queue.to_string()));
2223    fields.insert("group".to_string(), Value::text(group.to_string()));
2224    fields.insert(
2225        "created_at_ns".to_string(),
2226        Value::UnsignedInteger(now_ns()),
2227    );
2228    insert_meta_row(store, fields).map(|_| ())
2229}
2230
2231pub(super) fn load_pending_entries(
2232    store: &UnifiedStore,
2233    queue: &str,
2234    group: Option<&str>,
2235    message_id: Option<EntityId>,
2236) -> RedDBResult<Vec<QueuePendingEntry>> {
2237    let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2238        return Ok(Vec::new());
2239    };
2240    let lock_deadline_ns = load_queue_config(store, queue)
2241        .lock_deadline_ms
2242        .saturating_mul(1_000_000);
2243    let attempts_by_key: HashMap<(String, String, u64), u64> = manager
2244        .query_all(|entity| {
2245            entity.data.as_row().is_some_and(|row| {
2246                row_text(row, "kind").as_deref() == Some("queue_attempts_lc")
2247                    && row_text(row, "queue").as_deref() == Some(queue)
2248            })
2249        })
2250        .into_iter()
2251        .filter_map(|entity| {
2252            let row = entity.data.as_row()?;
2253            Some((
2254                (
2255                    row_text(row, "queue")?,
2256                    row_text(row, "group")?,
2257                    row_u64(row, "message_id")?,
2258                ),
2259                row_u64(row, "attempts").unwrap_or(1),
2260            ))
2261        })
2262        .collect();
2263    Ok(manager
2264        .query_all(|entity| {
2265            entity.data.as_row().is_some_and(|row| {
2266                matches!(
2267                    row_text(row, "kind").as_deref(),
2268                    Some("queue_pending") | Some("queue_pending_lc")
2269                ) && row_text(row, "queue").as_deref() == Some(queue)
2270                    && group
2271                        .map(|group_name| row_text(row, "group").as_deref() == Some(group_name))
2272                        .unwrap_or(true)
2273                    && message_id
2274                        .map(|candidate| row_u64(row, "message_id") == Some(candidate.raw()))
2275                        .unwrap_or(true)
2276            })
2277        })
2278        .into_iter()
2279        .filter_map(|entity| {
2280            let row = entity.data.as_row()?;
2281            let group = row_text(row, "group")?;
2282            let message_id = row_u64(row, "message_id")?;
2283            let kind = row_text(row, "kind")?;
2284            let delivered_at_ns = if kind == "queue_pending_lc" {
2285                row_u64(row, "lock_deadline_ns")
2286                    .unwrap_or(0)
2287                    .saturating_sub(lock_deadline_ns)
2288            } else {
2289                row_u64(row, "delivered_at_ns")?
2290            };
2291            let delivery_count = if kind == "queue_pending_lc" {
2292                attempts_by_key
2293                    .get(&(queue.to_string(), group.clone(), message_id))
2294                    .copied()
2295                    .unwrap_or(1)
2296            } else {
2297                row_u64(row, "delivery_count").unwrap_or(1)
2298            };
2299            Some(QueuePendingEntry {
2300                entity_id: entity.id,
2301                group,
2302                message_id: EntityId::new(message_id),
2303                consumer: row_text(row, "consumer").unwrap_or_default(),
2304                delivered_at_ns,
2305                delivery_count: delivery_count as u32,
2306            })
2307        })
2308        .collect())
2309}
2310
2311pub(super) fn save_queue_pending(
2312    store: &UnifiedStore,
2313    queue: &str,
2314    group: &str,
2315    message_id: EntityId,
2316    consumer: &str,
2317    delivered_at_ns: u64,
2318    delivery_count: u32,
2319) -> RedDBResult<()> {
2320    remove_meta_rows(store, |row| {
2321        row_text(row, "kind").as_deref() == Some("queue_pending")
2322            && row_text(row, "queue").as_deref() == Some(queue)
2323            && row_text(row, "group").as_deref() == Some(group)
2324            && row_u64(row, "message_id") == Some(message_id.raw())
2325    });
2326
2327    let mut fields = HashMap::new();
2328    fields.insert("kind".to_string(), Value::text("queue_pending".to_string()));
2329    fields.insert("queue".to_string(), Value::text(queue.to_string()));
2330    fields.insert("group".to_string(), Value::text(group.to_string()));
2331    fields.insert(
2332        "message_id".to_string(),
2333        Value::UnsignedInteger(message_id.raw()),
2334    );
2335    fields.insert("consumer".to_string(), Value::text(consumer.to_string()));
2336    fields.insert(
2337        "delivered_at_ns".to_string(),
2338        Value::UnsignedInteger(delivered_at_ns),
2339    );
2340    fields.insert(
2341        "delivery_count".to_string(),
2342        Value::UnsignedInteger(u64::from(delivery_count)),
2343    );
2344    insert_meta_row(store, fields).map(|_| ())
2345}
2346
2347pub(super) fn require_pending_entry(
2348    store: &UnifiedStore,
2349    queue: &str,
2350    group: &str,
2351    message_id: EntityId,
2352) -> RedDBResult<QueuePendingEntry> {
2353    load_pending_entries(store, queue, Some(group), Some(message_id))?
2354        .into_iter()
2355        .next()
2356        .ok_or_else(|| {
2357            RedDBError::NotFound(format!(
2358                "message '{}' is not pending in group '{}' on queue '{}'",
2359                message_id.raw(),
2360                group,
2361                queue
2362            ))
2363        })
2364}
2365
2366pub(super) fn load_ack_entries(
2367    store: &UnifiedStore,
2368    queue: &str,
2369    group: Option<&str>,
2370    message_id: Option<EntityId>,
2371) -> RedDBResult<Vec<QueueAckEntry>> {
2372    let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2373        return Ok(Vec::new());
2374    };
2375    Ok(manager
2376        .query_all(|entity| {
2377            entity.data.as_row().is_some_and(|row| {
2378                row_text(row, "kind").as_deref() == Some("queue_ack")
2379                    && row_text(row, "queue").as_deref() == Some(queue)
2380                    && group
2381                        .map(|group_name| row_text(row, "group").as_deref() == Some(group_name))
2382                        .unwrap_or(true)
2383                    && message_id
2384                        .map(|candidate| row_u64(row, "message_id") == Some(candidate.raw()))
2385                        .unwrap_or(true)
2386            })
2387        })
2388        .into_iter()
2389        .filter_map(|entity| {
2390            let row = entity.data.as_row()?;
2391            Some(QueueAckEntry {
2392                entity_id: entity.id,
2393                group: row_text(row, "group")?,
2394                message_id: EntityId::new(row_u64(row, "message_id")?),
2395            })
2396        })
2397        .collect())
2398}
2399
2400pub(super) fn save_queue_ack(
2401    store: &UnifiedStore,
2402    queue: &str,
2403    group: &str,
2404    message_id: EntityId,
2405) -> RedDBResult<()> {
2406    let existing = load_ack_entries(store, queue, Some(group), Some(message_id))?;
2407    if !existing.is_empty() {
2408        return Ok(());
2409    }
2410
2411    let mut fields = HashMap::new();
2412    fields.insert("kind".to_string(), Value::text("queue_ack".to_string()));
2413    fields.insert("queue".to_string(), Value::text(queue.to_string()));
2414    fields.insert("group".to_string(), Value::text(group.to_string()));
2415    fields.insert(
2416        "message_id".to_string(),
2417        Value::UnsignedInteger(message_id.raw()),
2418    );
2419    fields.insert("acked_at_ns".to_string(), Value::UnsignedInteger(now_ns()));
2420    insert_meta_row(store, fields).map(|_| ())
2421}
2422
2423pub(super) fn queue_message_completed_for_all_groups(
2424    store: &UnifiedStore,
2425    queue: &str,
2426    message_id: EntityId,
2427) -> RedDBResult<bool> {
2428    let groups = load_queue_groups(store, queue)?;
2429    let pending = load_pending_entries(store, queue, None, Some(message_id))?;
2430    if !pending.is_empty() {
2431        return Ok(false);
2432    }
2433    if groups.is_empty() {
2434        return Ok(true);
2435    }
2436
2437    let acked_groups = load_ack_entries(store, queue, None, Some(message_id))?
2438        .into_iter()
2439        .map(|entry| entry.group)
2440        .collect::<HashSet<_>>();
2441    Ok(groups
2442        .into_iter()
2443        .all(|group| acked_groups.contains(&group.group)))
2444}
2445
2446fn load_queue_message_views(
2447    store: &UnifiedStore,
2448    queue: &str,
2449) -> RedDBResult<Vec<QueueMessageView>> {
2450    load_queue_message_views_with_runtime(None, store, queue)
2451}
2452
2453/// Kind-aware queue scan (Phase 2.5.5 RLS universal). When the
2454/// caller has a `RedDBRuntime` reference, the gate also applies
2455/// any `CREATE POLICY ... ON MESSAGES OF <queue>` predicate. In
2456/// autocommit / embedded paths that only have the raw store (e.g.
2457/// purge loops) we skip RLS because there's no session identity
2458/// to match against.
2459pub(super) fn load_queue_message_views_with_runtime(
2460    runtime: Option<&RedDBRuntime>,
2461    store: &UnifiedStore,
2462    queue: &str,
2463) -> RedDBResult<Vec<QueueMessageView>> {
2464    let manager = store
2465        .get_collection(queue)
2466        .ok_or_else(|| RedDBError::NotFound(format!("queue '{}' not found", queue)))?;
2467    // Phase 1.2 MVCC universal: capture before parallel scan. Messages
2468    // inserted by another connection's open txn stay invisible to
2469    // consumers until that txn commits (prevents phantom POPs).
2470    let snap_ctx = crate::runtime::impl_core::capture_current_snapshot();
2471    let rls_filter = runtime.and_then(|rt| {
2472        crate::runtime::impl_core::rls_policy_filter_for_kind(
2473            rt,
2474            queue,
2475            reddb_rql::ast::PolicyAction::Select,
2476            reddb_rql::ast::PolicyTargetKind::Messages,
2477        )
2478    });
2479    let rls_enabled_but_denied = runtime.map(|rt| rt.is_rls_enabled(queue)).unwrap_or(false)
2480        && rls_filter.is_none()
2481        && runtime.is_some();
2482    if rls_enabled_but_denied {
2483        // RLS on + no Messages policy for this role = deny-default.
2484        return Ok(Vec::new());
2485    }
2486    let filter_arc = rls_filter.map(std::sync::Arc::new);
2487    let rt_arc = runtime;
2488    Ok(manager
2489        .scan(snap_ctx.as_ref(), move |entity| {
2490            if !matches!(entity.kind, EntityKind::QueueMessage { .. }) {
2491                return false;
2492            }
2493            if let (Some(filter), Some(rt)) = (filter_arc.as_ref(), rt_arc) {
2494                return crate::runtime::query_exec::evaluate_entity_filter_with_db(
2495                    Some(&rt.inner.db),
2496                    entity,
2497                    filter,
2498                    queue,
2499                    queue,
2500                );
2501            }
2502            true
2503        })
2504        .into_iter()
2505        .filter_map(queue_message_view_from_entity)
2506        .map(|mut view| {
2507            view.available_at_ns = read_message_available_at_ns(store, queue, view.id);
2508            view.ordering_key = read_message_ordering_key(store, queue, view.id);
2509            view
2510        })
2511        .collect())
2512}
2513
2514fn queue_message_view_from_entity(entity: UnifiedEntity) -> Option<QueueMessageView> {
2515    let (position, _) = match &entity.kind {
2516        EntityKind::QueueMessage { position, queue } => (*position, queue),
2517        _ => return None,
2518    };
2519    let data = match entity.data {
2520        EntityData::QueueMessage(data) => data,
2521        _ => return None,
2522    };
2523    Some(QueueMessageView {
2524        id: entity.id,
2525        position,
2526        priority: data.priority.unwrap_or(0),
2527        payload: data.payload,
2528        attempts: data.attempts,
2529        max_attempts: data.max_attempts,
2530        enqueued_at_ns: data.enqueued_at_ns,
2531        available_at_ns: None,
2532        ordering_key: None,
2533    })
2534}
2535
2536/// Insert a moved payload onto `queue` using only the payload value —
2537/// priority / attempts / TTL fall back to the destination queue's
2538/// catalog config (mirrors a fresh enqueue rather than carrying source
2539/// metadata over). Used by `PrimaryQueueStore::move_to_queue`, the
2540/// `QueueLifecycle::move_between_queues` adapter that owns only
2541/// `(message_id, payload)` after `pop_messages` retires the source row.
2542pub(super) fn insert_moved_queue_message_payload(
2543    store: &UnifiedStore,
2544    queue: &str,
2545    payload: &Value,
2546) -> RedDBResult<EntityId> {
2547    let config = load_queue_config(store, queue);
2548    let position = next_queue_position(store, queue, QueueSide::Right)?;
2549    let enqueued_at_ns = std::time::SystemTime::now()
2550        .duration_since(std::time::UNIX_EPOCH)
2551        .map(|d| d.as_nanos() as u64)
2552        .unwrap_or(0);
2553    let entity = UnifiedEntity::new(
2554        EntityId::new(0),
2555        EntityKind::QueueMessage {
2556            queue: queue.to_string(),
2557            position,
2558        },
2559        EntityData::QueueMessage(QueueMessageData {
2560            payload: payload.clone(),
2561            priority: None,
2562            enqueued_at_ns,
2563            attempts: 0,
2564            max_attempts: config.max_attempts,
2565            acked: false,
2566        }),
2567    );
2568    let id = store
2569        .insert_auto(queue, entity)
2570        .map_err(|err| RedDBError::Internal(err.to_string()))?;
2571    if let Some(ttl_ms) = config.ttl_ms {
2572        store
2573            .set_metadata(queue, id, queue_message_ttl_metadata(ttl_ms))
2574            .map_err(|err| RedDBError::Internal(err.to_string()))?;
2575    }
2576    Ok(id)
2577}
2578
2579fn insert_moved_queue_message(
2580    store: &UnifiedStore,
2581    queue: &str,
2582    config: &QueueRuntimeConfig,
2583    message: &QueueMessageView,
2584) -> RedDBResult<EntityId> {
2585    let position = next_queue_position(store, queue, QueueSide::Right)?;
2586    let entity = UnifiedEntity::new(
2587        EntityId::new(0),
2588        EntityKind::QueueMessage {
2589            queue: queue.to_string(),
2590            position,
2591        },
2592        EntityData::QueueMessage(QueueMessageData {
2593            payload: message.payload.clone(),
2594            priority: if config.priority {
2595                Some(message.priority)
2596            } else {
2597                None
2598            },
2599            enqueued_at_ns: message.enqueued_at_ns,
2600            attempts: 0,
2601            max_attempts: config.max_attempts,
2602            acked: false,
2603        }),
2604    );
2605    let id = store
2606        .insert_auto(queue, entity)
2607        .map_err(|err| RedDBError::Internal(err.to_string()))?;
2608    if config.ttl_ms.is_some() || message.ordering_key.is_some() {
2609        store
2610            .set_metadata(
2611                queue,
2612                id,
2613                queue_message_metadata(config.ttl_ms, None, message.ordering_key.as_deref()),
2614            )
2615            .map_err(|err| RedDBError::Internal(err.to_string()))?;
2616    }
2617    Ok(id)
2618}
2619
2620fn queue_projection_default_columns() -> Vec<String> {
2621    [
2622        "id",
2623        "payload",
2624        "priority",
2625        "attempts",
2626        "last_error",
2627        "enqueued_at",
2628        "available_at",
2629        "key",
2630        "dlq",
2631        "tenant",
2632    ]
2633    .into_iter()
2634    .map(str::to_string)
2635    .collect()
2636}
2637
2638fn queue_projection_record(
2639    columns: &[String],
2640    message: &QueueMessageView,
2641    dlq: bool,
2642) -> RedDBResult<UnifiedRecord> {
2643    let mut record = UnifiedRecord::new();
2644    for column in columns {
2645        let value = queue_projection_value(message, dlq, column).ok_or_else(|| {
2646            RedDBError::Query(format!("unknown queue projection column '{}'", column))
2647        })?;
2648        record.set(column, value);
2649    }
2650    Ok(record)
2651}
2652
2653fn queue_projection_value(message: &QueueMessageView, dlq: bool, column: &str) -> Option<Value> {
2654    match column {
2655        "id" => Some(Value::text(message_id_string(message.id))),
2656        "payload" => Some(message.payload.clone()),
2657        "priority" => Some(Value::Integer(i64::from(message.priority))),
2658        "attempts" => Some(Value::UnsignedInteger(u64::from(message.attempts))),
2659        "last_error" => Some(Value::Null),
2660        "enqueued_at" => Some(Value::UnsignedInteger(message.enqueued_at_ns)),
2661        "available_at" => Some(Value::UnsignedInteger(
2662            message.available_at_ns.unwrap_or(message.enqueued_at_ns),
2663        )),
2664        "key" => Some(
2665            message
2666                .ordering_key
2667                .as_ref()
2668                .map(|key| Value::text(key.clone()))
2669                .unwrap_or(Value::Null),
2670        ),
2671        "dlq" => Some(Value::Boolean(dlq)),
2672        "tenant" => queue_message_tenant(&message.payload).or(Some(Value::Null)),
2673        _ => None,
2674    }
2675}
2676
2677fn queue_message_tenant(payload: &Value) -> Option<Value> {
2678    let Value::Json(bytes) = payload else {
2679        return None;
2680    };
2681    let json: crate::json::Value = crate::json::from_slice(bytes).ok()?;
2682    json.get("tenant")
2683        .and_then(crate::json::Value::as_str)
2684        .map(|tenant| Value::text(tenant.to_string()))
2685}
2686
2687fn queue_is_dead_letter_target(store: &UnifiedStore, queue: &str) -> bool {
2688    let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2689        return false;
2690    };
2691    !manager
2692        .query_all(|entity| {
2693            entity.data.as_row().is_some_and(|row| {
2694                row_text(row, "kind").as_deref() == Some("queue_config")
2695                    && row_text(row, "dlq").as_deref() == Some(queue)
2696            })
2697        })
2698        .is_empty()
2699}
2700
2701fn queue_message_matches_filter(message: &QueueMessageView, dlq: bool, filter: &Filter) -> bool {
2702    match filter {
2703        Filter::Compare { field, op, value } => queue_filter_field_value(message, dlq, field)
2704            .is_some_and(|candidate| queue_compare_values(&candidate, value, *op)),
2705        Filter::CompareFields { left, op, right } => {
2706            match (
2707                queue_filter_field_value(message, dlq, left),
2708                queue_filter_field_value(message, dlq, right),
2709            ) {
2710                (Some(left), Some(right)) => queue_compare_values(&left, &right, *op),
2711                _ => false,
2712            }
2713        }
2714        Filter::And(left, right) => {
2715            queue_message_matches_filter(message, dlq, left)
2716                && queue_message_matches_filter(message, dlq, right)
2717        }
2718        Filter::Or(left, right) => {
2719            queue_message_matches_filter(message, dlq, left)
2720                || queue_message_matches_filter(message, dlq, right)
2721        }
2722        Filter::Not(inner) => !queue_message_matches_filter(message, dlq, inner),
2723        Filter::IsNull(field) => queue_filter_field_value(message, dlq, field)
2724            .is_none_or(|value| matches!(value, Value::Null)),
2725        Filter::IsNotNull(field) => queue_filter_field_value(message, dlq, field)
2726            .is_some_and(|value| !matches!(value, Value::Null)),
2727        Filter::In { field, values } => {
2728            queue_filter_field_value(message, dlq, field).is_some_and(|candidate| {
2729                values
2730                    .iter()
2731                    .any(|value| queue_values_equal(&candidate, value))
2732            })
2733        }
2734        Filter::Between { field, low, high } => queue_filter_field_value(message, dlq, field)
2735            .is_some_and(|candidate| {
2736                queue_compare_values(&candidate, low, CompareOp::Ge)
2737                    && queue_compare_values(&candidate, high, CompareOp::Le)
2738            }),
2739        Filter::Like { field, pattern } => queue_filter_text(message, dlq, field)
2740            .is_some_and(|value| queue_like_matches(&value, pattern)),
2741        Filter::StartsWith { field, prefix } => {
2742            queue_filter_text(message, dlq, field).is_some_and(|value| value.starts_with(prefix))
2743        }
2744        Filter::EndsWith { field, suffix } => {
2745            queue_filter_text(message, dlq, field).is_some_and(|value| value.ends_with(suffix))
2746        }
2747        Filter::Contains { field, substring } => {
2748            queue_filter_text(message, dlq, field).is_some_and(|value| value.contains(substring))
2749        }
2750        Filter::CompareExpr { .. } => false,
2751    }
2752}
2753
2754fn queue_filter_field_value(
2755    message: &QueueMessageView,
2756    dlq: bool,
2757    field: &FieldRef,
2758) -> Option<Value> {
2759    match field {
2760        FieldRef::TableColumn { table, column } if table.is_empty() => {
2761            queue_projection_value(message, dlq, column)
2762                .or_else(|| queue_payload_field_value(&message.payload, column))
2763        }
2764        FieldRef::TableColumn { column, .. } => queue_projection_value(message, dlq, column)
2765            .or_else(|| queue_payload_field_value(&message.payload, column)),
2766        _ => None,
2767    }
2768}
2769
2770fn queue_payload_field_value(payload: &Value, field: &str) -> Option<Value> {
2771    let Value::Json(bytes) = payload else {
2772        return None;
2773    };
2774    let json: crate::json::Value = crate::json::from_slice(bytes).ok()?;
2775    let value = json.get(field)?;
2776    json_value_to_schema_value(value)
2777}
2778
2779fn json_value_to_schema_value(value: &crate::json::Value) -> Option<Value> {
2780    if matches!(value, crate::json::Value::Null) {
2781        Some(Value::Null)
2782    } else if let Some(value) = value.as_bool() {
2783        Some(Value::Boolean(value))
2784    } else if let Some(value) = value.as_i64() {
2785        Some(Value::Integer(value))
2786    } else if let Some(value) = value.as_u64() {
2787        Some(Value::UnsignedInteger(value))
2788    } else if let Some(value) = value.as_f64() {
2789        Some(Value::Float(value))
2790    } else if let Some(value) = value.as_str() {
2791        Some(Value::text(value.to_string()))
2792    } else {
2793        Some(Value::Json(value.to_string_compact().into_bytes()))
2794    }
2795}
2796
2797fn queue_filter_text(message: &QueueMessageView, dlq: bool, field: &FieldRef) -> Option<String> {
2798    queue_filter_field_value(message, dlq, field).and_then(|value| match value {
2799        Value::Text(value) => Some(value.to_string()),
2800        Value::NodeRef(value) | Value::EdgeRef(value) | Value::TableRef(value) => Some(value),
2801        Value::Integer(value) => Some(value.to_string()),
2802        Value::UnsignedInteger(value) => Some(value.to_string()),
2803        Value::Float(value) => Some(value.to_string()),
2804        Value::Boolean(value) => Some(value.to_string()),
2805        _ => None,
2806    })
2807}
2808
2809fn queue_compare_values(left: &Value, right: &Value, op: CompareOp) -> bool {
2810    match op {
2811        CompareOp::Eq => queue_values_equal(left, right),
2812        CompareOp::Ne => !queue_values_equal(left, right),
2813        CompareOp::Lt => queue_partial_cmp(left, right).is_some_and(|ord| ord.is_lt()),
2814        CompareOp::Le => queue_partial_cmp(left, right).is_some_and(|ord| !ord.is_gt()),
2815        CompareOp::Gt => queue_partial_cmp(left, right).is_some_and(|ord| ord.is_gt()),
2816        CompareOp::Ge => queue_partial_cmp(left, right).is_some_and(|ord| !ord.is_lt()),
2817    }
2818}
2819
2820fn queue_values_equal(left: &Value, right: &Value) -> bool {
2821    if let (Some(left), Some(right)) = (queue_value_number(left), queue_value_number(right)) {
2822        return (left - right).abs() < f64::EPSILON;
2823    }
2824    match (left, right) {
2825        (Value::Text(left), Value::Text(right)) => left == right,
2826        (Value::Boolean(left), Value::Boolean(right)) => left == right,
2827        _ => left == right,
2828    }
2829}
2830
2831fn queue_partial_cmp(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
2832    if let (Some(left), Some(right)) = (queue_value_number(left), queue_value_number(right)) {
2833        return left.partial_cmp(&right);
2834    }
2835    match (left, right) {
2836        (Value::Text(left), Value::Text(right)) => Some(left.cmp(right)),
2837        _ => None,
2838    }
2839}
2840
2841fn queue_value_number(value: &Value) -> Option<f64> {
2842    match value {
2843        Value::Integer(value) => Some(*value as f64),
2844        Value::UnsignedInteger(value) => Some(*value as f64),
2845        Value::Float(value) => Some(*value),
2846        Value::Text(value) => value.parse().ok(),
2847        _ => None,
2848    }
2849}
2850
2851fn queue_like_matches(value: &str, pattern: &str) -> bool {
2852    if pattern == "%" {
2853        return true;
2854    }
2855    let starts_wild = pattern.starts_with('%');
2856    let ends_wild = pattern.ends_with('%');
2857    let needle = pattern.trim_matches('%');
2858    match (starts_wild, ends_wild) {
2859        (true, true) => value.contains(needle),
2860        (true, false) => value.ends_with(needle),
2861        (false, true) => value.starts_with(needle),
2862        (false, false) => value == needle,
2863    }
2864}
2865
2866pub(super) fn queue_message_view_by_id(
2867    store: &UnifiedStore,
2868    queue: &str,
2869    message_id: EntityId,
2870) -> RedDBResult<Option<QueueMessageView>> {
2871    let manager = queue_manager(store, queue)?;
2872    Ok(manager
2873        .get(message_id)
2874        .and_then(queue_message_view_from_entity)
2875        .map(|mut view| {
2876            view.available_at_ns = read_message_available_at_ns(store, queue, view.id);
2877            view
2878        }))
2879}
2880
2881pub(super) fn sort_queue_messages(
2882    messages: &mut [QueueMessageView],
2883    config: &QueueRuntimeConfig,
2884    side: QueueSide,
2885) {
2886    messages.sort_by(|left, right| {
2887        if config.priority {
2888            right
2889                .priority
2890                .cmp(&left.priority)
2891                .then_with(|| match side {
2892                    QueueSide::Left => left.position.cmp(&right.position),
2893                    QueueSide::Right => right.position.cmp(&left.position),
2894                })
2895                .then_with(|| left.id.raw().cmp(&right.id.raw()))
2896        } else {
2897            match side {
2898                QueueSide::Left => left.position.cmp(&right.position),
2899                QueueSide::Right => right.position.cmp(&left.position),
2900            }
2901            .then_with(|| left.id.raw().cmp(&right.id.raw()))
2902        }
2903    });
2904}
2905
2906pub(super) fn next_queue_position(
2907    store: &UnifiedStore,
2908    queue: &str,
2909    side: QueueSide,
2910) -> RedDBResult<u64> {
2911    let manager = store
2912        .get_collection(queue)
2913        .ok_or_else(|| RedDBError::NotFound(format!("queue '{queue}' not found")))?;
2914    let snapshot = crate::runtime::impl_core::capture_current_snapshot();
2915    let mut boundary: Option<u64> = None;
2916    // Positioning needs one scalar, not copies of every queued payload. The
2917    // fallible merged visitor also refuses a partial/empty answer on cold I/O.
2918    manager.try_scan_for_each(snapshot.as_ref(), usize::MAX, |entity| {
2919        let EntityKind::QueueMessage { position, .. } = &entity.kind else {
2920            return true;
2921        };
2922        if !matches!(entity.data, EntityData::QueueMessage(_)) {
2923            return true;
2924        }
2925        boundary = Some(boundary.map_or(*position, |current| match side {
2926            QueueSide::Left => current.min(*position),
2927            QueueSide::Right => current.max(*position),
2928        }));
2929        true
2930    })?;
2931    Ok(
2932        boundary.map_or(QUEUE_POSITION_CENTER, |position| match side {
2933            QueueSide::Left => position.saturating_sub(1),
2934            QueueSide::Right => position.saturating_add(1),
2935        }),
2936    )
2937}
2938
2939pub(super) fn increment_queue_attempts(
2940    store: &UnifiedStore,
2941    queue: &str,
2942    message_id: EntityId,
2943) -> RedDBResult<u32> {
2944    let manager = queue_manager(store, queue)?;
2945    let mut entity = manager
2946        .get(message_id)
2947        .ok_or_else(|| RedDBError::NotFound(format!("message '{}' not found", message_id.raw())))?;
2948    match &mut entity.data {
2949        EntityData::QueueMessage(message) => {
2950            message.attempts = message.attempts.saturating_add(1);
2951            let attempts = message.attempts;
2952            manager
2953                .update(entity)
2954                .map_err(|err| RedDBError::Internal(err.to_string()))?;
2955            Ok(attempts)
2956        }
2957        _ => Err(RedDBError::Query(format!(
2958            "entity '{}' is not a queue message",
2959            message_id.raw()
2960        ))),
2961    }
2962}
2963
2964pub(super) fn queue_message_attempts(
2965    store: &UnifiedStore,
2966    queue: &str,
2967    message_id: EntityId,
2968) -> RedDBResult<u32> {
2969    Ok(queue_message_data(store, queue, message_id)?.attempts)
2970}
2971
2972pub(super) fn queue_message_max_attempts(
2973    store: &UnifiedStore,
2974    queue: &str,
2975    message_id: EntityId,
2976) -> RedDBResult<u32> {
2977    Ok(queue_message_data(store, queue, message_id)?.max_attempts)
2978}
2979
2980pub(super) fn queue_message_payload(
2981    store: &UnifiedStore,
2982    queue: &str,
2983    message_id: EntityId,
2984) -> RedDBResult<Value> {
2985    Ok(queue_message_data(store, queue, message_id)?.payload)
2986}
2987
2988pub(super) fn queue_message_pending_any(
2989    store: &UnifiedStore,
2990    queue: &str,
2991    message_id: EntityId,
2992) -> RedDBResult<bool> {
2993    Ok(!load_pending_entries(store, queue, None, Some(message_id))?.is_empty())
2994}
2995
2996pub(super) fn queue_message_pending_for_group(
2997    store: &UnifiedStore,
2998    queue: &str,
2999    group: &str,
3000    message_id: EntityId,
3001) -> RedDBResult<bool> {
3002    Ok(!load_pending_entries(store, queue, Some(group), Some(message_id))?.is_empty())
3003}
3004
3005pub(super) fn queue_message_acked_for_group(
3006    store: &UnifiedStore,
3007    queue: &str,
3008    group: &str,
3009    message_id: EntityId,
3010) -> RedDBResult<bool> {
3011    Ok(!load_ack_entries(store, queue, Some(group), Some(message_id))?.is_empty())
3012}
3013
3014fn queue_manager(
3015    store: &UnifiedStore,
3016    queue: &str,
3017) -> RedDBResult<Arc<crate::storage::unified::SegmentManager>> {
3018    store
3019        .get_collection(queue)
3020        .ok_or_else(|| RedDBError::NotFound(format!("queue '{}' not found", queue)))
3021}
3022
3023pub(super) fn queue_message_data(
3024    store: &UnifiedStore,
3025    queue: &str,
3026    message_id: EntityId,
3027) -> RedDBResult<QueueMessageData> {
3028    let manager = queue_manager(store, queue)?;
3029    let entity = manager
3030        .get(message_id)
3031        .ok_or_else(|| RedDBError::NotFound(format!("message '{}' not found", message_id.raw())))?;
3032    match entity.data {
3033        EntityData::QueueMessage(message) => Ok(message),
3034        _ => Err(RedDBError::Query(format!(
3035            "entity '{}' is not a queue message",
3036            message_id.raw()
3037        ))),
3038    }
3039}
3040
3041fn insert_meta_row(store: &UnifiedStore, fields: HashMap<String, Value>) -> RedDBResult<EntityId> {
3042    let _ = store.get_or_create_collection(QUEUE_META_COLLECTION);
3043    let id = store
3044        .insert_auto(
3045            QUEUE_META_COLLECTION,
3046            UnifiedEntity::new(
3047                EntityId::new(0),
3048                EntityKind::TableRow {
3049                    table: Arc::from(QUEUE_META_COLLECTION),
3050                    row_id: 0,
3051                },
3052                EntityData::Row(RowData {
3053                    columns: Vec::new(),
3054                    named: Some(fields),
3055                    schema: None,
3056                }),
3057            ),
3058        )
3059        .map_err(|err| RedDBError::Internal(err.to_string()))?;
3060    Ok(id)
3061}
3062
3063pub(super) fn remove_meta_rows(store: &UnifiedStore, predicate: impl Fn(&RowData) -> bool + Sync) {
3064    let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
3065        return;
3066    };
3067    let rows = manager.query_all(|entity| entity.data.as_row().is_some_and(&predicate));
3068    for row in rows {
3069        let _ = store.delete(QUEUE_META_COLLECTION, row.id);
3070    }
3071}
3072
3073pub(super) fn reclaim_expired_queue_dedup(
3074    store: &UnifiedStore,
3075    queue: &str,
3076    now_ns: u64,
3077) -> RedDBResult<usize> {
3078    let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
3079        return Ok(0);
3080    };
3081    let queue = queue.to_string();
3082    let rows = manager.query_all(|entity| {
3083        entity.data.as_row().is_some_and(|row| {
3084            row_text(row, "kind").as_deref() == Some(KIND_QUEUE_PUSH_DEDUP)
3085                && row_text(row, "queue").as_deref() == Some(&queue)
3086                && row_u64(row, "expires_at_ns")
3087                    .map(|expires_at| expires_at <= now_ns)
3088                    .unwrap_or(true)
3089        })
3090    });
3091    let count = rows.len();
3092    for row in rows {
3093        store
3094            .delete(QUEUE_META_COLLECTION, row.id)
3095            .map_err(|err| RedDBError::Internal(err.to_string()))?;
3096    }
3097    Ok(count)
3098}
3099
3100pub(super) fn find_queue_dedup(
3101    store: &UnifiedStore,
3102    queue: &str,
3103    dedup_key: &str,
3104    now_ns: u64,
3105) -> Option<EntityId> {
3106    let manager = store.get_collection(QUEUE_META_COLLECTION)?;
3107    let queue = queue.to_string();
3108    let dedup_key = dedup_key.to_string();
3109    let snap_ctx = crate::runtime::impl_core::capture_current_snapshot();
3110    manager
3111        .scan(snap_ctx.as_ref(), |entity| {
3112            entity.data.as_row().is_some_and(|row| {
3113                row_text(row, "kind").as_deref() == Some(KIND_QUEUE_PUSH_DEDUP)
3114                    && row_text(row, "queue").as_deref() == Some(&queue)
3115                    && row_text(row, "dedup_key").as_deref() == Some(&dedup_key)
3116                    && row_u64(row, "expires_at_ns")
3117                        .map(|expires_at| expires_at > now_ns)
3118                        .unwrap_or(false)
3119            })
3120        })
3121        .into_iter()
3122        .filter_map(|entity| {
3123            let row = entity.data.as_row()?;
3124            row_u64(row, "message_id").map(EntityId::new)
3125        })
3126        .next()
3127}
3128
3129pub(super) fn record_queue_dedup(
3130    store: &UnifiedStore,
3131    queue: &str,
3132    dedup_key: &str,
3133    message_id: EntityId,
3134    expires_at_ns: u64,
3135) -> RedDBResult<EntityId> {
3136    let mut fields = HashMap::new();
3137    fields.insert(
3138        "kind".to_string(),
3139        Value::text(KIND_QUEUE_PUSH_DEDUP.to_string()),
3140    );
3141    fields.insert("queue".to_string(), Value::text(queue.to_string()));
3142    fields.insert("dedup_key".to_string(), Value::text(dedup_key.to_string()));
3143    fields.insert(
3144        "message_id".to_string(),
3145        Value::UnsignedInteger(message_id.raw()),
3146    );
3147    fields.insert(
3148        "expires_at_ns".to_string(),
3149        Value::UnsignedInteger(expires_at_ns),
3150    );
3151    insert_meta_row(store, fields)
3152}
3153
3154pub(super) fn delete_meta_entity(store: &UnifiedStore, entity_id: EntityId) {
3155    let _ = store.delete(QUEUE_META_COLLECTION, entity_id);
3156}
3157
3158fn queue_message_lock_key(queue: &str, message_id: EntityId) -> String {
3159    format!("{queue}:{}", message_id.raw())
3160}
3161
3162pub(super) fn queue_message_lock_handle(
3163    runtime: &RedDBRuntime,
3164    queue: &str,
3165    message_id: EntityId,
3166) -> Arc<parking_lot::Mutex<()>> {
3167    let key = queue_message_lock_key(queue, message_id);
3168    if let Some(lock) = runtime.inner.queue_message_locks.read().get(&key).cloned() {
3169        return lock;
3170    }
3171
3172    let mut locks = runtime.inner.queue_message_locks.write();
3173    locks
3174        .entry(key)
3175        .or_insert_with(|| Arc::new(parking_lot::Mutex::new(())))
3176        .clone()
3177}
3178
3179pub(super) fn forget_queue_message_lock(runtime: &RedDBRuntime, queue: &str, message_id: EntityId) {
3180    runtime
3181        .inner
3182        .queue_message_locks
3183        .write()
3184        .remove(&queue_message_lock_key(queue, message_id));
3185}
3186
3187fn parse_message_id(value: &str) -> RedDBResult<EntityId> {
3188    let raw = value.strip_prefix('e').unwrap_or(value);
3189    raw.parse::<u64>()
3190        .map(EntityId::new)
3191        .map_err(|_| RedDBError::Query(format!("invalid message id '{}'", value)))
3192}
3193
3194/// ADR 0026: resolve the ACK/NACK handle. When `delivery_id` is supplied,
3195/// it wins unconditionally — strict failure if the handle does not resolve
3196/// to a live pending delivery on `queue`. When only the legacy tuple is
3197/// supplied, emit a rate-limited deprecation log line and use the tuple.
3198/// At least one handle must be present.
3199pub(super) fn resolve_ack_nack_handle(
3200    store: &UnifiedStore,
3201    queue: &str,
3202    group_hint: &str,
3203    message_id_hint: &str,
3204    delivery_id: Option<&str>,
3205) -> RedDBResult<(String, EntityId)> {
3206    if let Some(did) = delivery_id {
3207        return resolve_delivery_id(store, queue, did);
3208    }
3209    if group_hint.is_empty() || message_id_hint.is_empty() {
3210        return Err(RedDBError::Query(
3211            "ACK/NACK requires either GROUP <group> '<message_id>' or WITH delivery_id = '<id>'"
3212                .to_string(),
3213        ));
3214    }
3215    log_tuple_deprecation(queue);
3216    let entity = parse_message_id(message_id_hint)?;
3217    Ok((group_hint.to_string(), entity))
3218}
3219
3220fn resolve_delivery_id(
3221    store: &UnifiedStore,
3222    queue: &str,
3223    delivery_id: &str,
3224) -> RedDBResult<(String, EntityId)> {
3225    let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
3226        return Err(RedDBError::Query(format!(
3227            "delivery_id '{}' does not resolve to a live pending delivery",
3228            delivery_id
3229        )));
3230    };
3231    for entity in manager.query_all(|entity| {
3232        entity.data.as_row().is_some_and(|row| {
3233            row_text(row, "kind").as_deref() == Some("queue_pending_lc")
3234                && row_text(row, "delivery_id").as_deref() == Some(delivery_id)
3235        })
3236    }) {
3237        if let Some(row) = entity.data.as_row() {
3238            let row_queue = row_text(row, "queue").unwrap_or_default();
3239            let row_group = row_text(row, "group").unwrap_or_default();
3240            let row_message = row_u64(row, "message_id").unwrap_or(0);
3241            if row_queue != queue {
3242                return Err(RedDBError::Query(format!(
3243                    "delivery_id '{}' belongs to queue '{}', not '{}'",
3244                    delivery_id, row_queue, queue
3245                )));
3246            }
3247            return Ok((row_group, EntityId::new(row_message)));
3248        }
3249    }
3250    Err(RedDBError::Query(format!(
3251        "delivery_id '{}' does not resolve to a live pending delivery",
3252        delivery_id
3253    )))
3254}
3255
3256/// Per-(connection, queue) rate-limited "tuple ACK is deprecated" log line.
3257/// One emission per minute matches ADR 0026's operational guidance.
3258fn log_tuple_deprecation(queue: &str) {
3259    use std::sync::atomic::Ordering;
3260    use std::sync::{Mutex, OnceLock};
3261    use std::time::Instant;
3262
3263    static LAST_EMIT: OnceLock<Mutex<HashMap<(u64, String), Instant>>> = OnceLock::new();
3264    const COOLDOWN: std::time::Duration = std::time::Duration::from_secs(60);
3265
3266    let map = LAST_EMIT.get_or_init(|| Mutex::new(HashMap::new()));
3267    let key = (super::impl_core::current_connection_id(), queue.to_string());
3268    let now = Instant::now();
3269    let mut guard = match map.lock() {
3270        Ok(g) => g,
3271        Err(_) => return,
3272    };
3273    let should_emit =
3274        !matches!(guard.get(&key), Some(prev) if now.duration_since(*prev) < COOLDOWN);
3275    if should_emit {
3276        guard.insert(key.clone(), now);
3277        drop(guard);
3278        TUPLE_DEPRECATION_EMITS.fetch_add(1, Ordering::Relaxed);
3279        tracing::warn!(
3280            target: "reddb::queue_lifecycle",
3281            queue = queue,
3282            connection_id = key.0,
3283            "ACK/NACK by (queue, group, message_id) tuple is deprecated; \
3284             switch to the server-issued delivery_id (ADR 0026). \
3285             The tuple path will be removed one minor release after introduction.",
3286        );
3287    }
3288}
3289
3290/// Total count of tuple-deprecation log emissions since process start.
3291/// Intentionally process-wide and `pub` so the transport-bridge
3292/// integration tests can observe that the legacy tuple path emitted a
3293/// deprecation while the `delivery_id` path stayed silent, without
3294/// having to plumb a `tracing::Subscriber` through every test.
3295pub static TUPLE_DEPRECATION_EMITS: std::sync::atomic::AtomicU64 =
3296    std::sync::atomic::AtomicU64::new(0);
3297
3298fn message_id_string(message_id: EntityId) -> String {
3299    message_id.raw().to_string()
3300}
3301
3302/// Issue #917 — render a delivered queue message as the JSON object the
3303/// RedWire `QueueEventPush` frame carries. Mirrors the column shape the
3304/// SQL `QUEUE READ` projection emits (`message_id` / `payload` /
3305/// `consumer` / `delivery_count`) so the wire push and the pull path
3306/// stay client-compatible.
3307fn delivered_message_json(
3308    message: crate::runtime::queue_lifecycle::DeliveredMessage,
3309) -> crate::serde_json::Value {
3310    use crate::serde_json::{Map, Value as JsonValue};
3311    let mut obj = Map::new();
3312    obj.insert(
3313        "message_id".to_string(),
3314        JsonValue::String(message_id_string(EntityId::new(message.message_id))),
3315    );
3316    obj.insert("payload".to_string(), message.payload.to_json());
3317    obj.insert("consumer".to_string(), JsonValue::String(message.consumer));
3318    obj.insert(
3319        "delivery_count".to_string(),
3320        JsonValue::Number(message.delivery_count as f64),
3321    );
3322    JsonValue::Object(obj)
3323}
3324
3325/// Slice 10 of issue #527 — render-time scan of pending entries
3326/// per (queue, group) for `queue_pending_gauge` exposition. Walks
3327/// `red_queue_meta` live so the gauge cannot drift from the source
3328/// of truth.
3329pub(crate) fn pending_counts_by_group(
3330    store: &UnifiedStore,
3331) -> std::collections::BTreeMap<(String, String), u64> {
3332    let mut counts: std::collections::BTreeMap<(String, String), u64> =
3333        std::collections::BTreeMap::new();
3334    let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
3335        return counts;
3336    };
3337    for entity in manager.query_all(|entity| {
3338        entity
3339            .data
3340            .as_row()
3341            .is_some_and(|row| row_text(row, "kind").as_deref() == Some("queue_pending"))
3342    }) {
3343        if let Some(row) = entity.data.as_row() {
3344            let queue = row_text(row, "queue");
3345            let group = row_text(row, "group");
3346            if let (Some(q), Some(g)) = (queue, group) {
3347                *counts.entry((q, g)).or_insert(0) += 1;
3348            }
3349        }
3350    }
3351    counts
3352}
3353
3354pub(super) fn row_text(row: &RowData, field: &str) -> Option<String> {
3355    match row.get_field(field)?.clone() {
3356        Value::Text(value) => Some(value.to_string()),
3357        Value::NodeRef(value) => Some(value),
3358        Value::EdgeRef(value) => Some(value),
3359        Value::TableRef(value) => Some(value),
3360        _ => None,
3361    }
3362}
3363
3364pub(super) fn row_u64(row: &RowData, field: &str) -> Option<u64> {
3365    match row.get_field(field)?.clone() {
3366        Value::UnsignedInteger(value) => Some(value),
3367        Value::Integer(value) if value >= 0 => Some(value as u64),
3368        Value::Float(value) if value >= 0.0 => Some(value as u64),
3369        Value::Text(value) => value.parse().ok(),
3370        _ => None,
3371    }
3372}
3373
3374fn row_bool(row: &RowData, field: &str) -> Option<bool> {
3375    match row.get_field(field)?.clone() {
3376        Value::Boolean(value) => Some(value),
3377        Value::Text(value) => match value.to_ascii_lowercase().as_str() {
3378            "true" => Some(true),
3379            "false" => Some(false),
3380            _ => None,
3381        },
3382        _ => None,
3383    }
3384}
3385
3386fn queue_collection_contract(
3387    name: &str,
3388    priority: bool,
3389    ttl_ms: Option<u64>,
3390) -> crate::physical::CollectionContract {
3391    let now = current_unix_ms();
3392    let mut context_index_fields = Vec::new();
3393    if priority {
3394        context_index_fields.push("priority".to_string());
3395    }
3396
3397    crate::physical::CollectionContract {
3398        name: name.to_string(),
3399        declared_model: crate::catalog::CollectionModel::Queue,
3400        schema_mode: crate::catalog::SchemaMode::Dynamic,
3401        origin: crate::physical::ContractOrigin::Explicit,
3402        version: 1,
3403        created_at_unix_ms: now,
3404        updated_at_unix_ms: now,
3405        default_ttl_ms: ttl_ms,
3406        vector_dimension: None,
3407        vector_metric: None,
3408        context_index_fields,
3409        declared_columns: Vec::new(),
3410        table_def: None,
3411        timestamps_enabled: false,
3412        context_index_enabled: false,
3413        metrics_raw_retention_ms: None,
3414        metrics_rollup_policies: Vec::new(),
3415        metrics_tenant_identity: None,
3416        metrics_namespace: None,
3417        // Queues manipulate messages via push/pop/ack — the row DML
3418        // paths never apply. Flag it as append_only so inadvertent
3419        // `UPDATE/DELETE FROM queue_name` statements fail loudly.
3420        append_only: true,
3421        subscriptions: Vec::new(),
3422        analytics_config: Vec::new(),
3423        session_key: None,
3424        session_gap_ms: None,
3425        retention_duration_ms: None,
3426        analytical_storage: None,
3427
3428        ai_policy: None,
3429    }
3430}
3431
3432fn current_unix_ms() -> u128 {
3433    std::time::SystemTime::now()
3434        .duration_since(std::time::UNIX_EPOCH)
3435        .unwrap_or_default()
3436        .as_millis()
3437}
3438
3439pub(super) fn now_ns() -> u64 {
3440    std::time::SystemTime::now()
3441        .duration_since(std::time::UNIX_EPOCH)
3442        .unwrap_or_default()
3443        .as_nanos() as u64
3444}
3445
3446pub(super) fn queue_message_ttl_metadata(ttl_ms: u64) -> Metadata {
3447    queue_message_metadata(Some(ttl_ms), None, None)
3448}
3449
3450/// Build the per-message metadata row attached to a queue message. Both
3451/// fields are optional — `_ttl_ms` carries the per-message TTL (existing
3452/// behaviour) and `_available_at_ns` carries the first-delivery instant
3453/// for delayed messages (issue #722). When both are present they share
3454/// the same row, since `UnifiedStore::set_metadata` replaces the entry.
3455pub(super) fn queue_message_metadata(
3456    ttl_ms: Option<u64>,
3457    available_at_ns: Option<u64>,
3458    ordering_key: Option<&str>,
3459) -> Metadata {
3460    let mut fields = HashMap::new();
3461    if let Some(ttl_ms) = ttl_ms {
3462        fields.insert(
3463            "_ttl_ms".to_string(),
3464            if ttl_ms <= i64::MAX as u64 {
3465                MetadataValue::Int(ttl_ms as i64)
3466            } else {
3467                MetadataValue::Timestamp(ttl_ms)
3468            },
3469        );
3470    }
3471    if let Some(at_ns) = available_at_ns {
3472        fields.insert(
3473            "_available_at_ns".to_string(),
3474            MetadataValue::Timestamp(at_ns),
3475        );
3476    }
3477    if let Some(key) = ordering_key {
3478        fields.insert(
3479            "_ordering_key".to_string(),
3480            MetadataValue::String(key.to_string()),
3481        );
3482    }
3483    Metadata::with_fields(fields)
3484}
3485
3486/// Smallest future `available_at_ns` among messages currently sitting
3487/// on `queue`. Used by `QUEUE READ … WAIT` (issue #722) to cap the
3488/// condvar park horizon: without this, a waiter on a quiet queue with
3489/// only delayed messages would never wake when one became due, since
3490/// the wait registry is only notified by producer commits. Returns
3491/// `None` when no future-dated message exists (the common case — the
3492/// caller falls back to the user-supplied wait budget).
3493pub(super) fn earliest_future_available_at(store: &UnifiedStore, queue: &str) -> Option<u64> {
3494    let now_ns = now_ns();
3495    let views = load_queue_message_views_with_runtime(None, store, queue).ok()?;
3496    views
3497        .iter()
3498        .filter_map(|v| v.available_at_ns)
3499        .filter(|at| *at > now_ns)
3500        .min()
3501}
3502
3503/// Update the `_available_at_ns` metadata for a queue message in place
3504/// without dropping any `_ttl_ms` already present (issue #723 — used by
3505/// NACK retry delay). `available_at_ns = None` clears the field. The
3506/// `fallback_ttl_ms` argument is consulted only when the existing
3507/// metadata row carries no `_ttl_ms` — keeps the per-message TTL in
3508/// sync with the queue default in the common case where no per-message
3509/// TTL was set explicitly. Tolerant of missing collections / entities:
3510/// returns `Ok(())` rather than failing the surrounding NACK if the
3511/// underlying message has gone away between resolution and metadata
3512/// update (a benign race; the next delivery cycle reflects truth).
3513pub(super) fn set_message_available_at_ns(
3514    store: &UnifiedStore,
3515    queue: &str,
3516    message_id: EntityId,
3517    available_at_ns: Option<u64>,
3518    fallback_ttl_ms: Option<u64>,
3519) -> RedDBResult<()> {
3520    let existing_ttl_ms = store
3521        .get_metadata(queue, message_id)
3522        .and_then(|md| match md.get("_ttl_ms")? {
3523            MetadataValue::Int(i) if *i >= 0 => Some(*i as u64),
3524            MetadataValue::Timestamp(t) => Some(*t),
3525            _ => None,
3526        })
3527        .or(fallback_ttl_ms);
3528    let existing_ordering_key = read_message_ordering_key(store, queue, message_id);
3529    let metadata = queue_message_metadata(
3530        existing_ttl_ms,
3531        available_at_ns,
3532        existing_ordering_key.as_deref(),
3533    );
3534    match store.set_metadata(queue, message_id, metadata) {
3535        Ok(()) => Ok(()),
3536        Err(crate::storage::StoreError::CollectionNotFound(_)) => Ok(()),
3537        Err(err) => Err(RedDBError::Internal(err.to_string())),
3538    }
3539}
3540
3541/// Read the `_ordering_key` metadata for a queue message. Returns `None`
3542/// for keyless messages or when the metadata row is absent.
3543pub(super) fn read_message_ordering_key(
3544    store: &UnifiedStore,
3545    queue: &str,
3546    message_id: EntityId,
3547) -> Option<String> {
3548    let md = store.get_metadata(queue, message_id)?;
3549    match md.get("_ordering_key")? {
3550        MetadataValue::String(value) => Some(value.clone()),
3551        _ => None,
3552    }
3553}
3554
3555/// Read the `_available_at_ns` metadata for a queue message. Returns
3556/// `None` for messages with no delay (the common case) or when the
3557/// metadata row is missing entirely.
3558pub(super) fn read_message_available_at_ns(
3559    store: &UnifiedStore,
3560    queue: &str,
3561    message_id: EntityId,
3562) -> Option<u64> {
3563    let md = store.get_metadata(queue, message_id)?;
3564    match md.get("_available_at_ns")? {
3565        MetadataValue::Timestamp(t) => Some(*t),
3566        MetadataValue::Int(i) if *i >= 0 => Some(*i as u64),
3567        _ => None,
3568    }
3569}
3570
3571/// Rough payload byte estimate for outbox watermark tracking.
3572fn estimate_payload_bytes(payload: &Value) -> u64 {
3573    match payload {
3574        Value::Json(v) => v.len() as u64,
3575        Value::Text(s) => s.len() as u64,
3576        _ => 64,
3577    }
3578}
3579
3580#[cfg(test)]
3581mod presence_integration_tests {
3582    use super::*;
3583    use crate::storage::queue::presence::{PresenceState, DEFAULT_PRESENCE_TTL_MS};
3584    use crate::{RedDBOptions, RedDBRuntime};
3585
3586    /// Issue #742 acceptance: `QUEUE READ` must register and refresh
3587    /// consumer presence. The snapshot exposes the same `(queue, group,
3588    /// consumer)` triple the read named, with `state = Active`.
3589    #[test]
3590    fn queue_read_emits_consumer_presence_heartbeat() {
3591        let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3592        rt.execute_query("CREATE QUEUE tasks").unwrap();
3593        rt.execute_query("QUEUE GROUP CREATE tasks workers")
3594            .unwrap();
3595        rt.execute_query("QUEUE PUSH tasks {'job':'a'}").unwrap();
3596        rt.execute_query("QUEUE READ tasks GROUP workers CONSUMER w1")
3597            .unwrap();
3598
3599        let snap = rt.queue_consumer_presence_snapshot(DEFAULT_PRESENCE_TTL_MS);
3600        assert_eq!(snap.len(), 1, "exactly one heartbeat recorded");
3601        let row = &snap[0];
3602        assert_eq!(row.queue, "tasks");
3603        assert_eq!(row.group, "workers");
3604        assert_eq!(row.consumer, "w1");
3605        assert_eq!(row.state, PresenceState::Active);
3606
3607        let counts = rt.queue_active_consumer_counts(DEFAULT_PRESENCE_TTL_MS);
3608        assert_eq!(
3609            counts[&("tasks".to_string(), "workers".to_string())],
3610            1,
3611            "active count reflects the live consumer"
3612        );
3613    }
3614
3615    /// Issue #742 acceptance: a read that returns no messages must
3616    /// still heartbeat — aliveness is independent of pending
3617    /// deliveries.
3618    #[test]
3619    fn empty_queue_read_still_heartbeats() {
3620        let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3621        rt.execute_query("CREATE QUEUE empty_q").unwrap();
3622        rt.execute_query("QUEUE GROUP CREATE empty_q workers")
3623            .unwrap();
3624        // No PUSH — the queue is empty.
3625        rt.execute_query("QUEUE READ empty_q GROUP workers CONSUMER w1")
3626            .unwrap();
3627
3628        let snap = rt.queue_consumer_presence_snapshot(DEFAULT_PRESENCE_TTL_MS);
3629        assert_eq!(
3630            snap.len(),
3631            1,
3632            "empty read still registers consumer presence"
3633        );
3634        assert_eq!(snap[0].state, PresenceState::Active);
3635        assert_eq!(
3636            snap[0].lease_count, 0,
3637            "no messages delivered → zero leases"
3638        );
3639    }
3640}