Skip to main content

reddb_server/runtime/
impl_queue.rs

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