Skip to main content

reddb_server/runtime/
impl_queue.rs

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