Skip to main content

reddb_server/runtime/
impl_queue.rs

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