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