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