Skip to main content

reddb_server/runtime/
impl_kv.rs

1//! KV DSL command execution and KvAtomicOps module.
2//!
3//! Handles `KV PUT key = value [EXPIRE n] [TAGS [...]] [IF NOT EXISTS]`,
4//! `KV GET key`, and `KV DELETE key`.
5
6use crate::application::ports::RuntimeEntityPort;
7use crate::storage::unified::{Metadata, MetadataValue};
8
9use super::impl_core::{current_auth_identity, current_connection_id, current_tenant};
10use super::*;
11
12/// Default collection name for bare-key KV operations.
13pub const KV_DEFAULT_COLLECTION: &str = "kv_default";
14
15fn vault_master_key_ref(collection: &str) -> String {
16    format!("red.vault.{collection}.master_key")
17}
18
19fn keyed_model_name(model: crate::catalog::CollectionModel) -> &'static str {
20    match model {
21        crate::catalog::CollectionModel::Kv => "kv",
22        crate::catalog::CollectionModel::Vault => "vault",
23        crate::catalog::CollectionModel::Config => "config",
24        _ => "collection",
25    }
26}
27
28#[derive(Debug, Clone)]
29struct VaultEntry {
30    id: crate::storage::EntityId,
31    key: String,
32    value: crate::storage::schema::Value,
33    metadata: Metadata,
34    created_at: u64,
35    updated_at: u64,
36    sequence_id: u64,
37    version: i64,
38    tombstone: bool,
39    op: String,
40}
41
42impl super::keyed_spine::KeyedVersion for VaultEntry {
43    fn key(&self) -> &str {
44        &self.key
45    }
46
47    fn version(&self) -> i64 {
48        self.version
49    }
50}
51
52impl VaultEntry {
53    fn from_keyed_row(
54        version: super::keyed_spine::KeyedRowVersion,
55        metadata: Metadata,
56        created_at: u64,
57        updated_at: u64,
58        sequence_id: u64,
59    ) -> Self {
60        Self {
61            id: version.id,
62            key: version.key,
63            value: version.value,
64            metadata,
65            created_at,
66            updated_at,
67            sequence_id,
68            version: version.version,
69            tombstone: version.tombstone,
70            op: version.op,
71        }
72    }
73}
74
75/// Atomic KV operations interface — the seam that transports and drivers depend on.
76///
77/// All three verbs delegate to the runtime's existing `create_kv` / `get_kv` /
78/// `delete_kv` plumbing; this struct adds the auto-create and upsert logic.
79pub struct KvAtomicOps<'a> {
80    runtime: &'a RedDBRuntime,
81}
82
83impl<'a> KvAtomicOps<'a> {
84    pub fn new(runtime: &'a RedDBRuntime) -> Self {
85        Self { runtime }
86    }
87
88    /// Insert or update a KV entry. Auto-creates the collection when needed.
89    ///
90    /// Insert or update a KV entry. Returns `(created: bool, id: EntityId)`.
91    pub fn set(
92        &self,
93        model: crate::catalog::CollectionModel,
94        collection: &str,
95        key: &str,
96        value: crate::storage::schema::Value,
97        ttl_ms: Option<u64>,
98        if_not_exists: bool,
99    ) -> RedDBResult<(bool, crate::storage::EntityId)> {
100        self.set_with_tags_for_model(model, collection, key, value, ttl_ms, &[], if_not_exists)
101    }
102
103    pub fn set_with_tags(
104        &self,
105        collection: &str,
106        key: &str,
107        value: crate::storage::schema::Value,
108        ttl_ms: Option<u64>,
109        tags: &[String],
110        if_not_exists: bool,
111    ) -> RedDBResult<(bool, crate::storage::EntityId)> {
112        self.set_with_tags_for_model(
113            crate::catalog::CollectionModel::Kv,
114            collection,
115            key,
116            value,
117            ttl_ms,
118            tags,
119            if_not_exists,
120        )
121    }
122
123    fn set_with_tags_for_model(
124        &self,
125        model: crate::catalog::CollectionModel,
126        collection: &str,
127        key: &str,
128        value: crate::storage::schema::Value,
129        ttl_ms: Option<u64>,
130        tags: &[String],
131        if_not_exists: bool,
132    ) -> RedDBResult<(bool, crate::storage::EntityId)> {
133        self.set_with_tags_and_metadata_for_model(
134            model,
135            collection,
136            key,
137            value,
138            ttl_ms,
139            tags,
140            if_not_exists,
141            Vec::new(),
142        )
143    }
144
145    pub fn set_with_tags_and_metadata(
146        &self,
147        collection: &str,
148        key: &str,
149        value: crate::storage::schema::Value,
150        ttl_ms: Option<u64>,
151        tags: &[String],
152        if_not_exists: bool,
153        metadata: Vec<(String, MetadataValue)>,
154    ) -> RedDBResult<(bool, crate::storage::EntityId)> {
155        self.set_with_tags_and_metadata_for_model(
156            crate::catalog::CollectionModel::Kv,
157            collection,
158            key,
159            value,
160            ttl_ms,
161            tags,
162            if_not_exists,
163            metadata,
164        )
165    }
166
167    fn set_with_tags_and_metadata_for_model(
168        &self,
169        model: crate::catalog::CollectionModel,
170        collection: &str,
171        key: &str,
172        value: crate::storage::schema::Value,
173        ttl_ms: Option<u64>,
174        tags: &[String],
175        if_not_exists: bool,
176        mut metadata: Vec<(String, MetadataValue)>,
177    ) -> RedDBResult<(bool, crate::storage::EntityId)> {
178        self.ensure_keyed_collection(model, collection)?;
179
180        if model == crate::catalog::CollectionModel::Vault {
181            let latest = self.get_vault_entry(collection, key)?;
182            let was_present = latest.as_ref().is_some_and(|entry| !entry.tombstone);
183            if if_not_exists && was_present {
184                return Ok((false, latest.expect("checked present").id));
185            }
186            let entry = self.append_vault_version(collection, key, value, "put", false, tags)?;
187            self.runtime.record_kv_watch_event(
188                if latest.is_some() {
189                    crate::replication::cdc::ChangeOperation::Update
190                } else {
191                    crate::replication::cdc::ChangeOperation::Insert
192                },
193                collection,
194                key,
195                entry.id.raw(),
196                latest.as_ref().map(vault_entry_metadata_json),
197                Some(vault_entry_metadata_json(&entry)),
198            );
199            return Ok((!was_present, entry.id));
200        }
201
202        let existing = self.get_entry(model, collection, key)?;
203        let was_present = existing.is_some();
204
205        if if_not_exists && was_present {
206            let (_, id) = existing.unwrap();
207            self.runtime.inner.kv_stats.incr_puts();
208            return Ok((false, id));
209        }
210
211        let before = existing
212            .as_ref()
213            .map(|(value, _)| crate::presentation::entity_json::storage_value_to_json(value));
214        let op = if was_present {
215            crate::replication::cdc::ChangeOperation::Update
216        } else {
217            crate::replication::cdc::ChangeOperation::Insert
218        };
219        let after = Some(crate::presentation::entity_json::storage_value_to_json(
220            &value,
221        ));
222
223        // Versioned KV retains MVCC history: the prior visible version is
224        // tombstoned (set_xmax) *after* the new version is created, so both
225        // coexist physically — exactly like a table-row update. Non-versioned
226        // KV keeps last-writer-wins: pre-delete the old row before creating
227        // fresh (also handles TTL refresh).
228        let versioned = self.is_versioned_collection(model, collection);
229        // Capture the prior *visible* version (if any) before the write so we
230        // can tombstone it. On a re-PUT after a delete (`was_present` false)
231        // there is no visible prior, but earlier versions still exist
232        // physically — see the xmin restamp below.
233        let prior_versioned_entity = if versioned && was_present {
234            self.get_entity(model, collection, key)?
235        } else {
236            None
237        };
238        if was_present && !versioned {
239            self.delete(model, collection, key)?;
240        }
241
242        if let Some(ttl_metadata) = ttl_metadata(ttl_ms) {
243            metadata.extend(ttl_metadata.fields);
244        }
245        if let Some(tags_metadata) = kv_tags_metadata(tags) {
246            metadata.push(tags_metadata);
247        }
248
249        let output = self
250            .runtime
251            .create_kv(crate::application::entity::CreateKvInput {
252                collection: collection.to_string(),
253                key: key.to_string(),
254                value,
255                metadata,
256            })?;
257
258        // Every versioned PUT stamps the new version's xmin from ONE monotonic
259        // xid (begin()/commit() or the active tx xid) — NOT the autocommit
260        // pool, whose batched reservation is not monotonic against commit
261        // root_xids and would let AS OF resolve the wrong version. The prior
262        // visible version (if any) is tombstoned with that SAME xid, so
263        // old.xmax == new.xmin and exactly one version is visible to any
264        // snapshot (mirrors `apply_loaded_sql_update_row_core`). Restamping
265        // unconditionally also fixes the re-PUT-after-delete case, where no
266        // visible prior exists but the pool xmin would still scramble ordering
267        // against existing tombstoned versions.
268        if versioned {
269            let version_xid = match self.runtime.current_xid() {
270                Some(xid) => xid,
271                None => {
272                    let mgr = self.runtime.snapshot_manager();
273                    let xid = mgr.begin();
274                    mgr.commit(xid);
275                    xid
276                }
277            };
278            if let Some(new_entity) = output.entity.clone() {
279                self.restamp_xmin(collection, new_entity, version_xid)?;
280            }
281            if let Some(prior) = prior_versioned_entity {
282                // First-committer-wins (ADR 0014): inside an explicit
283                // transaction, defer the prior version's tombstone to the
284                // commit-time conflict check by recording it as a pending
285                // versioned update — exactly like a table-row versioned
286                // UPDATE (`persist_applied_entity_mutations`). The check
287                // rejects this txn's COMMIT if the prior version was
288                // already tombstoned by a concurrent committed writer.
289                // Autocommit (`current_xid()` is None) commits eagerly and
290                // records nothing, matching the table path.
291                let previous_xmax = prior.xmax;
292                let old_id = prior.id;
293                self.tombstone_version(collection, prior, version_xid)?;
294                if self.runtime.current_xid().is_some() {
295                    self.runtime.record_pending_versioned_update(
296                        crate::runtime::impl_core::current_connection_id(),
297                        collection,
298                        old_id,
299                        output.id,
300                        version_xid,
301                        previous_xmax,
302                    );
303                }
304            }
305        }
306        if model == crate::catalog::CollectionModel::Kv {
307            self.runtime
308                .inner
309                .kv_tag_index
310                .replace(collection, key, output.id, tags);
311        }
312
313        if model == crate::catalog::CollectionModel::Kv {
314            self.runtime
315                .record_kv_watch_event(op, collection, key, output.id.raw(), before, after);
316        }
317
318        if model == crate::catalog::CollectionModel::Kv {
319            self.runtime.inner.kv_stats.incr_puts();
320        }
321        Ok((!was_present, output.id))
322    }
323
324    /// Retrieve a KV value by key. Returns `None` when not found.
325    pub fn get(
326        &self,
327        model: crate::catalog::CollectionModel,
328        collection: &str,
329        key: &str,
330    ) -> RedDBResult<Option<crate::storage::schema::Value>> {
331        let result = self.get_entry(model, collection, key)?;
332        if model == crate::catalog::CollectionModel::Kv {
333            self.runtime.inner.kv_stats.incr_gets();
334        }
335        Ok(result.map(|(v, _)| v))
336    }
337
338    /// Delete a KV entry. Returns `true` if the key existed.
339    pub fn delete(
340        &self,
341        model: crate::catalog::CollectionModel,
342        collection: &str,
343        key: &str,
344    ) -> RedDBResult<bool> {
345        self.ensure_declared_model(model, collection)?;
346
347        // Versioned KV deletes tombstone the prior visible version
348        // (set_xmax) instead of reclaiming it physically, so history and
349        // time-travel reads survive — VACUUM reclaims the version later.
350        if self.is_versioned_collection(model, collection) {
351            let Some(entity) = self.get_entity(model, collection, key)? else {
352                return Ok(false);
353            };
354            let id = entity.id;
355            let value = kv_value_from_entity(&entity);
356            let xid = self.runtime.current_xid().unwrap_or_else(|| {
357                let mgr = self.runtime.snapshot_manager();
358                let xid = mgr.begin();
359                mgr.commit(xid);
360                xid
361            });
362            // First-committer-wins (ADR 0014): inside an explicit
363            // transaction, defer this tombstone to the commit-time
364            // conflict check by recording it as a pending tombstone —
365            // exactly like a table-row DELETE (`delete_entities_batch`).
366            // The check rejects this txn's COMMIT if the version was
367            // already tombstoned by a concurrent committed writer.
368            // Autocommit (`current_xid()` is None) commits eagerly and
369            // records nothing, matching the table path.
370            let previous_xmax = entity.xmax;
371            self.tombstone_version(collection, entity, xid)?;
372            if self.runtime.current_xid().is_some() {
373                self.runtime.record_pending_tombstone(
374                    crate::runtime::impl_core::current_connection_id(),
375                    collection,
376                    id,
377                    xid,
378                    previous_xmax,
379                );
380            }
381            self.runtime.inner.kv_tag_index.remove(collection, key);
382            self.runtime.record_kv_watch_event(
383                crate::replication::cdc::ChangeOperation::Delete,
384                collection,
385                key,
386                id.raw(),
387                value
388                    .as_ref()
389                    .map(crate::presentation::entity_json::storage_value_to_json),
390                None,
391            );
392            self.runtime.inner.kv_stats.incr_deletes();
393            return Ok(true);
394        }
395
396        let found = self.get_entry(model, collection, key)?;
397        if let Some((value, id)) = found {
398            let store = self.runtime.inner.db.store();
399            let deleted = store
400                .delete(collection, id)
401                .map_err(|err| RedDBError::Internal(err.to_string()))?;
402            if deleted {
403                store.context_index().remove_entity(id);
404                if model == crate::catalog::CollectionModel::Kv {
405                    self.runtime.inner.kv_tag_index.remove(collection, key);
406                    self.runtime.record_kv_watch_event(
407                        crate::replication::cdc::ChangeOperation::Delete,
408                        collection,
409                        key,
410                        id.raw(),
411                        Some(crate::presentation::entity_json::storage_value_to_json(
412                            &value,
413                        )),
414                        None,
415                    );
416                    self.runtime.inner.kv_stats.incr_deletes();
417                }
418            }
419            Ok(deleted)
420        } else {
421            Ok(false)
422        }
423    }
424
425    /// Tombstone a KV version by stamping `xmax = xid` and persisting it.
426    ///
427    /// The physical row stays alive (history); VACUUM reclaims it once no
428    /// snapshot can see it. Used by versioned PUT (old version) and
429    /// versioned DELETE.
430    fn tombstone_version(
431        &self,
432        collection: &str,
433        mut entity: crate::storage::UnifiedEntity,
434        xid: crate::storage::transaction::snapshot::Xid,
435    ) -> RedDBResult<()> {
436        let store = self.runtime.inner.db.store();
437        let Some(manager) = store.get_collection(collection) else {
438            return Ok(());
439        };
440        let id = entity.id;
441        entity.set_xmax(xid);
442        manager
443            .update(entity.clone())
444            .map_err(|err| RedDBError::Internal(format!("{err:?}")))?;
445        store
446            .persist_entities_to_pager(collection, std::slice::from_ref(&entity))
447            .map_err(|err| RedDBError::Internal(err.to_string()))?;
448        // The tombstoned version drops out of the live context index but
449        // stays physically resident for AS OF reads until VACUUM.
450        store.context_index().remove_entity(id);
451        Ok(())
452    }
453
454    /// Restamp a freshly-created KV version's `xmin` to a monotonic version
455    /// xid and persist it. `create_kv` stamps a (non-monotonic) autocommit
456    /// pool xid; the versioned write path overrides it so the new version's
457    /// xmin equals the old version's xmax, preserving AS OF ordering against
458    /// commit root_xids.
459    fn restamp_xmin(
460        &self,
461        collection: &str,
462        mut entity: crate::storage::UnifiedEntity,
463        xid: crate::storage::transaction::snapshot::Xid,
464    ) -> RedDBResult<()> {
465        let store = self.runtime.inner.db.store();
466        let Some(manager) = store.get_collection(collection) else {
467            return Ok(());
468        };
469        entity.set_xmin(xid);
470        manager
471            .update(entity.clone())
472            .map_err(|err| RedDBError::Internal(format!("{err:?}")))?;
473        store
474            .persist_entities_to_pager(collection, std::slice::from_ref(&entity))
475            .map_err(|err| RedDBError::Internal(err.to_string()))?;
476        Ok(())
477    }
478
479    /// Atomically increment (or decrement) a counter key. Returns the new value.
480    ///
481    /// - Missing key initialises at `by` (Redis-compat).
482    /// - Non-integer value returns an error before any mutation.
483    pub fn incr(
484        &self,
485        model: crate::catalog::CollectionModel,
486        collection: &str,
487        key: &str,
488        by: i64,
489        ttl_ms: Option<u64>,
490    ) -> RedDBResult<i64> {
491        if model == crate::catalog::CollectionModel::Vault {
492            return Err(RedDBError::InvalidOperation(
493                "VAULT INCR is not supported for sealed secrets".to_string(),
494            ));
495        }
496        let rmw_lock = self.runtime.inner.rmw_locks.lock_for(collection, key);
497        let _rmw_guard = rmw_lock.lock();
498        self.ensure_kv_collection(collection)?;
499        let existing = self.runtime.get_kv(collection, key)?;
500        let current: i64 = match existing.as_ref() {
501            None => 0,
502            Some((crate::storage::schema::Value::Integer(n), _)) => *n,
503            Some((crate::storage::schema::Value::Float(f), _)) => *f as i64,
504            Some((other, _)) => {
505                return Err(RedDBError::Internal(format!(
506                    "INCR on non-integer value: {:?}",
507                    other
508                )));
509            }
510        };
511
512        let next = current
513            .checked_add(by)
514            .ok_or_else(|| RedDBError::Internal(format!("INCR overflow: {current} + {by}")))?;
515
516        // Delete then re-create so TTL is refreshed.
517        if existing.is_some() {
518            self.runtime.delete_kv(collection, key)?;
519        }
520
521        let meta_vec: Vec<(String, crate::storage::unified::MetadataValue)> = ttl_metadata(ttl_ms)
522            .map(|m| m.fields.into_iter().collect())
523            .unwrap_or_default();
524
525        let output = self
526            .runtime
527            .create_kv(crate::application::entity::CreateKvInput {
528                collection: collection.to_string(),
529                key: key.to_string(),
530                value: crate::storage::schema::Value::Integer(next),
531                metadata: meta_vec,
532            })?;
533        self.runtime
534            .inner
535            .kv_tag_index
536            .replace(collection, key, output.id, &[]);
537
538        self.runtime.record_kv_watch_event(
539            if existing.is_some() {
540                crate::replication::cdc::ChangeOperation::Update
541            } else {
542                crate::replication::cdc::ChangeOperation::Insert
543            },
544            collection,
545            key,
546            output.id.raw(),
547            existing
548                .as_ref()
549                .map(|(value, _)| crate::presentation::entity_json::storage_value_to_json(value)),
550            Some(crate::presentation::entity_json::storage_value_to_json(
551                &crate::storage::schema::Value::Integer(next),
552            )),
553        );
554
555        self.runtime.inner.kv_stats.incr_incrs();
556        Ok(next)
557    }
558
559    /// Compare-and-set: atomically swap `key` from `expected` to `new_value`.
560    ///
561    /// Returns `(ok, current)`:
562    /// - `ok = true`  → swap applied; `current` is the value *before* the swap.
563    /// - `ok = false` → swap skipped; `current` holds the actual current value.
564    ///
565    /// `expected = None` means the caller expects the key to be *absent* (create-if-absent).
566    pub fn cas(
567        &self,
568        model: crate::catalog::CollectionModel,
569        collection: &str,
570        key: &str,
571        expected: Option<&crate::storage::schema::Value>,
572        new_value: crate::storage::schema::Value,
573        ttl_ms: Option<u64>,
574    ) -> RedDBResult<(bool, Option<crate::storage::schema::Value>)> {
575        if model == crate::catalog::CollectionModel::Vault {
576            return Err(RedDBError::InvalidOperation(
577                "VAULT CAS is not supported for sealed secrets".to_string(),
578            ));
579        }
580        let rmw_lock = self.runtime.inner.rmw_locks.lock_for(collection, key);
581        let _rmw_guard = rmw_lock.lock();
582        self.ensure_kv_collection(collection)?;
583        let current = self.runtime.get_kv(collection, key)?.map(|(v, _)| v);
584
585        let matches = match (&current, expected) {
586            (None, None) => true,
587            (Some(cur), Some(exp)) => cur == exp,
588            _ => false,
589        };
590
591        if !matches {
592            self.runtime.inner.kv_stats.incr_cas_conflict();
593            return Ok((false, current));
594        }
595
596        // Swap: delete old entry (if present), write new one.
597        if current.is_some() {
598            self.runtime.delete_kv(collection, key)?;
599        }
600
601        let meta_vec: Vec<(String, crate::storage::unified::MetadataValue)> = ttl_metadata(ttl_ms)
602            .map(|m| m.fields.into_iter().collect())
603            .unwrap_or_default();
604
605        let output = self
606            .runtime
607            .create_kv(crate::application::entity::CreateKvInput {
608                collection: collection.to_string(),
609                key: key.to_string(),
610                value: new_value.clone(),
611                metadata: meta_vec,
612            })?;
613        self.runtime
614            .inner
615            .kv_tag_index
616            .replace(collection, key, output.id, &[]);
617
618        self.runtime.record_kv_watch_event(
619            if current.is_some() {
620                crate::replication::cdc::ChangeOperation::Update
621            } else {
622                crate::replication::cdc::ChangeOperation::Insert
623            },
624            collection,
625            key,
626            output.id.raw(),
627            current
628                .as_ref()
629                .map(crate::presentation::entity_json::storage_value_to_json),
630            Some(crate::presentation::entity_json::storage_value_to_json(
631                &new_value,
632            )),
633        );
634
635        self.runtime.inner.kv_stats.incr_cas_success();
636        Ok((true, current))
637    }
638
639    pub fn invalidate_tags(&self, collection: &str, tags: &[String]) -> RedDBResult<usize> {
640        self.runtime
641            .check_write(crate::runtime::write_gate::WriteKind::Dml)?;
642        self.runtime.check_kv_invalidate_policy(collection)?;
643        self.ensure_kv_collection(collection)?;
644        let entries = self
645            .runtime
646            .inner
647            .kv_tag_index
648            .entries_for_tags(collection, tags);
649        if entries.is_empty() {
650            return Ok(0);
651        }
652
653        let store = self.runtime.inner.db.store();
654        let mut removed = 0usize;
655        for (key, id) in entries {
656            let before = store
657                .get(collection, id)
658                .and_then(|entity| kv_value_from_entity(&entity));
659            let deleted = store
660                .delete(collection, id)
661                .map_err(|err| RedDBError::Internal(err.to_string()))?;
662            if deleted {
663                store.context_index().remove_entity(id);
664                self.runtime.inner.kv_tag_index.remove(collection, &key);
665                self.runtime.record_kv_watch_event(
666                    crate::replication::cdc::ChangeOperation::Delete,
667                    collection,
668                    &key,
669                    id.raw(),
670                    before
671                        .as_ref()
672                        .map(crate::presentation::entity_json::storage_value_to_json),
673                    None,
674                );
675                removed += 1;
676            }
677        }
678        if removed > 0 {
679            self.runtime.inner.kv_stats.incr_deletes();
680        }
681        Ok(removed)
682    }
683
684    pub fn tags_for_key(&self, collection: &str, key: &str) -> Vec<String> {
685        self.runtime
686            .inner
687            .kv_tag_index
688            .tags_for_key(collection, key)
689    }
690
691    /// Auto-create a KV collection if it does not exist yet.
692    fn ensure_kv_collection(&self, collection: &str) -> RedDBResult<()> {
693        self.ensure_keyed_collection(crate::catalog::CollectionModel::Kv, collection)
694    }
695
696    fn ensure_keyed_collection(
697        &self,
698        model: crate::catalog::CollectionModel,
699        collection: &str,
700    ) -> RedDBResult<()> {
701        let store = self.runtime.inner.db.store();
702        if store.get_collection(collection).is_some() {
703            return self.ensure_declared_model(model, collection);
704        }
705        if model != crate::catalog::CollectionModel::Kv {
706            return Err(RedDBError::NotFound(format!(
707                "{} collection '{collection}' does not exist",
708                keyed_model_name(model)
709            )));
710        }
711        // Check config gate: red.config.kv.default_collection (default = true).
712        let auto_create = self
713            .runtime
714            .config_bool("red.config.kv.default_collection", true);
715        if !auto_create {
716            return Err(RedDBError::NotFound(format!(
717                "kv collection '{collection}' does not exist and auto-create is disabled \
718                 (red.config.kv.default_collection = false)"
719            )));
720        }
721        store
722            .create_collection(collection)
723            .map_err(|err| RedDBError::Internal(err.to_string()))?;
724        self.runtime
725            .inner
726            .db
727            .save_collection_contract(kv_collection_contract(collection))
728            .map_err(|err| RedDBError::Internal(err.to_string()))?;
729        Ok(())
730    }
731
732    fn get_entry(
733        &self,
734        model: crate::catalog::CollectionModel,
735        collection: &str,
736        key: &str,
737    ) -> RedDBResult<Option<(crate::storage::schema::Value, crate::storage::EntityId)>> {
738        let Some(entity) = self.get_entity(model, collection, key)? else {
739            return Ok(None);
740        };
741        Ok(kv_value_from_entity(&entity).map(|value| (value, entity.id)))
742    }
743
744    /// Whether `collection` retains MVCC history (opted into versioning).
745    ///
746    /// Versioned KV reads must do MVCC version-selection; non-versioned
747    /// collections (config `red_config`, the secrets vault) keep the
748    /// last-writer-wins fast path. Internal `red_*` collections are
749    /// never versioned, so this stays cheap for them.
750    fn is_versioned_collection(
751        &self,
752        model: crate::catalog::CollectionModel,
753        collection: &str,
754    ) -> bool {
755        // Only plain KV ever opts into versioning; the vault and config
756        // tiers are non-versioned by construction.
757        if model != crate::catalog::CollectionModel::Kv {
758            return false;
759        }
760        self.runtime.vcs_is_versioned(collection).unwrap_or(false)
761    }
762
763    fn get_entity(
764        &self,
765        model: crate::catalog::CollectionModel,
766        collection: &str,
767        key: &str,
768    ) -> RedDBResult<Option<crate::storage::UnifiedEntity>> {
769        self.ensure_declared_model(model, collection)?;
770        let store = self.runtime.inner.db.store();
771        let Some(manager) = store.get_collection(collection) else {
772            return Ok(None);
773        };
774
775        // Versioned KV: multiple physical versions per logical key can
776        // coexist (tombstoned + live). Select the snapshot-visible
777        // version with the greatest xmin — mirroring the table-row
778        // resolver's per-logical-id rule, keyed on the KV `key` field.
779        if self.is_versioned_collection(model, collection) {
780            let resolver =
781                crate::runtime::table_row_mvcc_resolver::TableRowMvccReadResolver::current_statement();
782            let mut best: Option<crate::storage::UnifiedEntity> = None;
783            for entity in manager.query_all(|_| true) {
784                if !kv_entity_has_key(&entity, key) {
785                    continue;
786                }
787                if resolver.resolve_read_candidate(&entity).is_none() {
788                    continue;
789                }
790                let better = match &best {
791                    Some(current) => entity.xmin >= current.xmin,
792                    None => true,
793                };
794                if better {
795                    best = Some(entity);
796                }
797            }
798            return Ok(best);
799        }
800
801        let entities = manager.query_all(|_| true);
802        for entity in entities {
803            if kv_entity_has_key(&entity, key) {
804                return Ok(Some(entity));
805            }
806        }
807        Ok(None)
808    }
809
810    fn latest_kv_entries(
811        &self,
812        collection: &str,
813        prefix: Option<&str>,
814    ) -> RedDBResult<Vec<(String, crate::storage::UnifiedEntity)>> {
815        self.ensure_declared_model(crate::catalog::CollectionModel::Kv, collection)?;
816        let store = self.runtime.inner.db.store();
817        let Some(manager) = store.get_collection(collection) else {
818            return Ok(Vec::new());
819        };
820        // Versioned KV: per logical key, keep the snapshot-visible
821        // version with the greatest xmin; keys whose every version is
822        // tombstoned (no visible version) drop out entirely. Mirrors
823        // the per-key version-selection used by `get_entity`.
824        let versioned =
825            self.is_versioned_collection(crate::catalog::CollectionModel::Kv, collection);
826        let resolver = versioned.then(|| {
827            crate::runtime::table_row_mvcc_resolver::TableRowMvccReadResolver::current_statement()
828        });
829
830        let mut entries: std::collections::BTreeMap<String, crate::storage::UnifiedEntity> =
831            std::collections::BTreeMap::new();
832        for entity in manager.query_all(|_| true) {
833            let key = match &entity.data {
834                crate::storage::EntityData::Row(row) => row
835                    .named
836                    .as_ref()
837                    .and_then(|named| named.get("key"))
838                    .and_then(|value| match value {
839                        crate::storage::schema::Value::Text(key) => Some(key.to_string()),
840                        _ => None,
841                    }),
842                _ => None,
843            };
844            let Some(key) = key else {
845                continue;
846            };
847            if prefix.is_some_and(|prefix| !key.starts_with(prefix)) {
848                continue;
849            }
850            if let Some(resolver) = resolver.as_ref() {
851                // Skip versions not visible to the current snapshot
852                // (tombstoned by a visible deleter, or not yet created).
853                if resolver.resolve_read_candidate(&entity).is_none() {
854                    continue;
855                }
856            }
857            let should_replace = match entries.get(&key) {
858                Some(existing) if versioned => entity.xmin >= existing.xmin,
859                Some(existing) => entity.id.raw() >= existing.id.raw(),
860                None => true,
861            };
862            if should_replace {
863                entries.insert(key, entity);
864            }
865        }
866        Ok(entries.into_iter().collect())
867    }
868
869    fn get_vault_entry(&self, collection: &str, key: &str) -> RedDBResult<Option<VaultEntry>> {
870        self.vault_versions(collection, key)
871            .map(super::keyed_spine::latest_version)
872    }
873
874    fn get_vault_entry_version(
875        &self,
876        collection: &str,
877        key: &str,
878        version: i64,
879    ) -> RedDBResult<Option<VaultEntry>> {
880        Ok(self
881            .vault_versions(collection, key)?
882            .into_iter()
883            .find(|entry| entry.version == version))
884    }
885
886    fn vault_versions(&self, collection: &str, key: &str) -> RedDBResult<Vec<VaultEntry>> {
887        self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
888        let store = self.runtime.inner.db.store();
889        let Some(manager) = store.get_collection(collection) else {
890            return Ok(Vec::new());
891        };
892        let entities = manager.query_all(|_| true);
893        let mut versions = Vec::new();
894        for entity in entities {
895            let crate::storage::EntityData::Row(ref row) = entity.data else {
896                continue;
897            };
898            let Some(version) =
899                super::keyed_spine::row_version(entity.id, row, entity.sequence_id as i64)
900            else {
901                continue;
902            };
903            if version.key != key {
904                continue;
905            }
906            let metadata = manager.get_metadata(entity.id).unwrap_or_default();
907            versions.push(VaultEntry::from_keyed_row(
908                version,
909                metadata,
910                entity.created_at,
911                entity.updated_at,
912                entity.sequence_id,
913            ));
914        }
915        Ok(versions)
916    }
917
918    fn latest_vault_entries(
919        &self,
920        collection: &str,
921        prefix: Option<&str>,
922    ) -> RedDBResult<Vec<VaultEntry>> {
923        self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
924        let store = self.runtime.inner.db.store();
925        let Some(manager) = store.get_collection(collection) else {
926            return Ok(Vec::new());
927        };
928        let mut versions = Vec::new();
929        for entity in manager.query_all(|_| true) {
930            let crate::storage::EntityData::Row(ref row) = entity.data else {
931                continue;
932            };
933            let Some(version) =
934                super::keyed_spine::row_version(entity.id, row, entity.sequence_id as i64)
935            else {
936                continue;
937            };
938            let metadata = manager.get_metadata(entity.id).unwrap_or_default();
939            let entry = VaultEntry::from_keyed_row(
940                version,
941                metadata,
942                entity.created_at,
943                entity.updated_at,
944                entity.sequence_id,
945            );
946            versions.push(entry);
947        }
948        Ok(super::keyed_spine::latest_versions(versions, prefix))
949    }
950
951    fn append_vault_version(
952        &self,
953        collection: &str,
954        key: &str,
955        value: crate::storage::schema::Value,
956        op: &str,
957        tombstone: bool,
958        tags: &[String],
959    ) -> RedDBResult<VaultEntry> {
960        self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
961        let version = self
962            .get_vault_entry(collection, key)?
963            .map(|entry| entry.version)
964            .unwrap_or(0)
965            + 1;
966        let stored_value = if tombstone {
967            crate::storage::schema::Value::Null
968        } else {
969            self.runtime.seal_vault_value(collection, value)?
970        };
971        let now = current_unix_ms() as i64;
972        let fields = vec![
973            (
974                "key".to_string(),
975                crate::storage::schema::Value::text(key.to_string()),
976            ),
977            ("value".to_string(), stored_value),
978            (
979                "version".to_string(),
980                crate::storage::schema::Value::Integer(version),
981            ),
982            (
983                "tombstone".to_string(),
984                crate::storage::schema::Value::Boolean(tombstone),
985            ),
986            (
987                "op".to_string(),
988                crate::storage::schema::Value::text(op.to_string()),
989            ),
990            (
991                "created_at_ms".to_string(),
992                crate::storage::schema::Value::Integer(now),
993            ),
994        ];
995        let mut row = crate::storage::RowData::new(Vec::new());
996        row.named = Some(fields.into_iter().collect());
997        let entity = crate::storage::UnifiedEntity::new(
998            crate::storage::EntityId::new(0),
999            crate::storage::EntityKind::TableRow {
1000                table: std::sync::Arc::from(collection),
1001                row_id: 0,
1002            },
1003            crate::storage::EntityData::Row(row),
1004        );
1005        let id = self
1006            .runtime
1007            .inner
1008            .db
1009            .store()
1010            .insert(collection, entity)
1011            .map_err(|err| RedDBError::Internal(err.to_string()))?;
1012        if !tags.is_empty() {
1013            self.runtime
1014                .inner
1015                .db
1016                .store()
1017                .set_metadata(
1018                    collection,
1019                    id,
1020                    Metadata::with_fields(vault_tags_metadata(tags)),
1021                )
1022                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1023            self.runtime
1024                .inner
1025                .kv_tag_index
1026                .replace(collection, key, id, tags);
1027        }
1028        self.get_vault_entry_version(collection, key, version)?
1029            .ok_or_else(|| RedDBError::Internal(format!("vault version {id} was not readable")))
1030    }
1031
1032    fn purge_vault_versions(&self, collection: &str, key: &str) -> RedDBResult<usize> {
1033        self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
1034        let versions = self.vault_versions(collection, key)?;
1035        let store = self.runtime.inner.db.store();
1036        let mut purged = 0usize;
1037        for entry in versions {
1038            if store
1039                .delete(collection, entry.id)
1040                .map_err(|err| RedDBError::Internal(err.to_string()))?
1041            {
1042                store.context_index().remove_entity(entry.id);
1043                purged += 1;
1044            }
1045        }
1046        Ok(purged)
1047    }
1048
1049    fn ensure_declared_model(
1050        &self,
1051        model: crate::catalog::CollectionModel,
1052        collection: &str,
1053    ) -> RedDBResult<()> {
1054        let Some(contract) = self.runtime.inner.db.collection_contract(collection) else {
1055            return Ok(());
1056        };
1057        if contract.declared_model == model
1058            || contract.declared_model == crate::catalog::CollectionModel::Mixed
1059        {
1060            return Ok(());
1061        }
1062        Err(RedDBError::InvalidOperation(format!(
1063            "collection '{}' is declared as '{}' and does not allow '{}' operations",
1064            collection,
1065            keyed_model_name(contract.declared_model),
1066            keyed_model_name(model)
1067        )))
1068    }
1069}
1070
1071impl RedDBRuntime {
1072    pub(crate) fn seal_vault_value(
1073        &self,
1074        collection: &str,
1075        value: crate::storage::schema::Value,
1076    ) -> RedDBResult<crate::storage::schema::Value> {
1077        let key = self.vault_encryption_key(collection)?;
1078        let plaintext = value.to_bytes();
1079        let nonce_bytes = crate::auth::store::random_bytes(12);
1080        let mut nonce = [0u8; 12];
1081        nonce.copy_from_slice(&nonce_bytes[..12]);
1082        let aad = format!("reddb.vault.{collection}");
1083        let ciphertext =
1084            crate::crypto::aes_gcm::aes256_gcm_encrypt(&key, &nonce, aad.as_bytes(), &plaintext);
1085        let mut payload = Vec::with_capacity(12 + ciphertext.len());
1086        payload.extend_from_slice(&nonce);
1087        payload.extend_from_slice(&ciphertext);
1088        Ok(crate::storage::schema::Value::Secret(payload))
1089    }
1090
1091    fn vault_key_available(&self, collection: &str) -> bool {
1092        self.vault_encryption_key(collection).is_ok()
1093    }
1094
1095    fn vault_encryption_key(&self, collection: &str) -> RedDBResult<[u8; 32]> {
1096        let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1097            RedDBError::Query("vault sealed_unavailable: no key provider is configured".to_string())
1098        })?;
1099        if !auth_store.is_vault_backed() {
1100            return Err(RedDBError::Query(
1101                "vault sealed_unavailable: key provider is sealed".to_string(),
1102            ));
1103        }
1104
1105        if let Some(hex_key) = auth_store.vault_kv_get(&vault_master_key_ref(collection)) {
1106            return decode_vault_key(&hex_key);
1107        }
1108        auth_store.vault_secret_key().ok_or_else(|| {
1109            RedDBError::Query("vault sealed_unavailable: cluster vault key is missing".to_string())
1110        })
1111    }
1112
1113    fn unseal_vault_value(
1114        &self,
1115        collection: &str,
1116        sealed: &crate::storage::schema::Value,
1117    ) -> RedDBResult<crate::storage::schema::Value> {
1118        let crate::storage::schema::Value::Secret(payload) = sealed else {
1119            return Err(RedDBError::Query(
1120                "vault unseal failed: stored value is not sealed".to_string(),
1121            ));
1122        };
1123        if payload.len() < 12 {
1124            return Err(RedDBError::Query(
1125                "vault unseal failed: sealed payload is truncated".to_string(),
1126            ));
1127        }
1128        let key = self.vault_encryption_key(collection)?;
1129        let mut nonce = [0u8; 12];
1130        nonce.copy_from_slice(&payload[..12]);
1131        let aad = format!("reddb.vault.{collection}");
1132        let plaintext = crate::crypto::aes_gcm::aes256_gcm_decrypt(
1133            &key,
1134            &nonce,
1135            aad.as_bytes(),
1136            &payload[12..],
1137        )
1138        .map_err(|_| RedDBError::Query("vault unseal failed: decryption failed".to_string()))?;
1139        let (value, consumed) =
1140            crate::storage::schema::Value::from_bytes(&plaintext).map_err(|err| {
1141                RedDBError::Query(format!("vault unseal failed: bad plaintext value: {err}"))
1142            })?;
1143        if consumed != plaintext.len() {
1144            return Err(RedDBError::Query(
1145                "vault unseal failed: trailing plaintext bytes".to_string(),
1146            ));
1147        }
1148        Ok(value)
1149    }
1150
1151    /// Internal peek of a vault entry's unsealed value, bypassing audit and
1152    /// policy checks. Used by `SecretRefGuard` to detect chained `secret_ref`
1153    /// indirection at config write time without emitting vault read events.
1154    /// Returns `Ok(None)` if the entry is absent, tombstoned, or its sealed
1155    /// payload cannot be decrypted with the available key material.
1156    pub(crate) fn peek_vault_unsealed(
1157        &self,
1158        collection: &str,
1159        key: &str,
1160    ) -> RedDBResult<Option<crate::storage::schema::Value>> {
1161        let ops = KvAtomicOps::new(self);
1162        let Some(entry) = ops.get_vault_entry(collection, key)? else {
1163            return Ok(None);
1164        };
1165        if entry.tombstone {
1166            return Ok(None);
1167        }
1168        Ok(self.unseal_vault_value(collection, &entry.value).ok())
1169    }
1170
1171    fn vault_target_resource(collection: &str, key: &str) -> String {
1172        if collection == "red.vault" {
1173            return format!("red.vault/{}", key.to_ascii_lowercase());
1174        }
1175        format!("{collection}.{key}")
1176    }
1177
1178    fn current_vault_actor() -> String {
1179        current_auth_identity()
1180            .map(|(principal, _)| principal)
1181            .unwrap_or_else(|| "anonymous".to_string())
1182    }
1183
1184    fn vault_request_id() -> String {
1185        let conn_id = current_connection_id();
1186        if conn_id == 0 {
1187            "embedded".to_string()
1188        } else {
1189            format!("conn-{conn_id}")
1190        }
1191    }
1192
1193    fn check_vault_capability(
1194        &self,
1195        action: &str,
1196        collection: &str,
1197        key: &str,
1198    ) -> Result<(), String> {
1199        let Some(auth_store) = self.inner.auth_store.read().clone() else {
1200            return Ok(());
1201        };
1202        if !auth_store.iam_authorization_enabled() {
1203            return Ok(());
1204        }
1205        let Some((principal, role)) = current_auth_identity() else {
1206            return Err(
1207                "IAM authorization is enabled; vault capability check requires an authenticated principal"
1208                    .to_string(),
1209            );
1210        };
1211        let tenant = current_tenant();
1212        let principal_id = crate::auth::UserId::from_parts(tenant.as_deref(), &principal);
1213        let mut resource = crate::auth::policies::ResourceRef::new(
1214            "vault",
1215            Self::vault_target_resource(collection, key),
1216        );
1217        if let Some(ref tenant) = tenant {
1218            resource = resource.with_tenant(tenant.clone());
1219        }
1220        let ctx = crate::auth::policies::EvalContext {
1221            principal_tenant: tenant.clone(),
1222            current_tenant: tenant,
1223            peer_ip: None,
1224            mfa_present: false,
1225            now_ms: crate::utils::now_unix_millis() as u128,
1226            principal_is_admin_role: role == crate::auth::Role::Admin,
1227            principal_is_platform_scoped: principal_id.tenant.is_none(),
1228        };
1229        if auth_store.check_policy_authz_with_role(&principal_id, action, &resource, &ctx, role) {
1230            Ok(())
1231        } else {
1232            Err(format!(
1233                "principal=`{}` action=`{}` resource=`vault:{}` denied by IAM policy",
1234                principal,
1235                action,
1236                Self::vault_target_resource(collection, key)
1237            ))
1238        }
1239    }
1240
1241    fn check_system_vault_capability(
1242        &self,
1243        action: &str,
1244        collection: &str,
1245        key: &str,
1246    ) -> Result<(), String> {
1247        if collection != "red.vault" {
1248            return Ok(());
1249        }
1250        self.check_vault_capability(action, collection, key)
1251    }
1252
1253    fn audit_vault_unseal(
1254        &self,
1255        collection: &str,
1256        key: &str,
1257        outcome: crate::runtime::audit_log::Outcome,
1258        reason: &str,
1259        entry: Option<&VaultEntry>,
1260    ) {
1261        let actor = Self::current_vault_actor();
1262        let request_id = Self::vault_request_id();
1263        let mut builder = crate::runtime::audit_log::AuditEvent::builder("vault/unseal")
1264            .principal(actor.clone())
1265            .source(crate::runtime::audit_log::AuditAuthSource::Password)
1266            .resource(format!(
1267                "vault:{}",
1268                Self::vault_target_resource(collection, key)
1269            ))
1270            .outcome(outcome)
1271            .correlation_id(request_id.clone())
1272            .fields([
1273                crate::runtime::audit_log::AuditFieldEscaper::field("actor", actor),
1274                crate::runtime::audit_log::AuditFieldEscaper::field("collection", collection),
1275                crate::runtime::audit_log::AuditFieldEscaper::field("key", key),
1276                crate::runtime::audit_log::AuditFieldEscaper::field(
1277                    "target",
1278                    Self::vault_target_resource(collection, key),
1279                ),
1280                crate::runtime::audit_log::AuditFieldEscaper::field("reason", reason),
1281                crate::runtime::audit_log::AuditFieldEscaper::field("request_id", request_id),
1282                crate::runtime::audit_log::AuditFieldEscaper::field(
1283                    "connection_id",
1284                    current_connection_id(),
1285                ),
1286            ]);
1287        if let Some(tenant) = current_tenant() {
1288            builder = builder.tenant(tenant);
1289        }
1290        if let Some(entry) = entry {
1291            builder = builder.fields([
1292                crate::runtime::audit_log::AuditFieldEscaper::field("entity_id", entry.id.raw()),
1293                crate::runtime::audit_log::AuditFieldEscaper::field(
1294                    "sequence_id",
1295                    entry.sequence_id,
1296                ),
1297            ]);
1298        }
1299        self.audit_log().record_event(builder.build());
1300    }
1301
1302    fn audit_vault_lifecycle(
1303        &self,
1304        operation: &str,
1305        collection: &str,
1306        key: &str,
1307        outcome: crate::runtime::audit_log::Outcome,
1308        reason: &str,
1309        entry: Option<&VaultEntry>,
1310    ) {
1311        let actor = Self::current_vault_actor();
1312        let request_id = Self::vault_request_id();
1313        let mut builder =
1314            crate::runtime::audit_log::AuditEvent::builder(format!("vault/{operation}"))
1315                .principal(actor.clone())
1316                .source(crate::runtime::audit_log::AuditAuthSource::Password)
1317                .resource(format!(
1318                    "vault:{}",
1319                    Self::vault_target_resource(collection, key)
1320                ))
1321                .outcome(outcome)
1322                .correlation_id(request_id.clone())
1323                .fields([
1324                    crate::runtime::audit_log::AuditFieldEscaper::field("actor", actor),
1325                    crate::runtime::audit_log::AuditFieldEscaper::field("collection", collection),
1326                    crate::runtime::audit_log::AuditFieldEscaper::field("key", key),
1327                    crate::runtime::audit_log::AuditFieldEscaper::field(
1328                        "target",
1329                        Self::vault_target_resource(collection, key),
1330                    ),
1331                    crate::runtime::audit_log::AuditFieldEscaper::field("reason", reason),
1332                    crate::runtime::audit_log::AuditFieldEscaper::field("request_id", request_id),
1333                    crate::runtime::audit_log::AuditFieldEscaper::field(
1334                        "connection_id",
1335                        current_connection_id(),
1336                    ),
1337                ]);
1338        if let Some(tenant) = current_tenant() {
1339            builder = builder.tenant(tenant);
1340        }
1341        if let Some(entry) = entry {
1342            builder = builder.fields([
1343                crate::runtime::audit_log::AuditFieldEscaper::field("entity_id", entry.id.raw()),
1344                crate::runtime::audit_log::AuditFieldEscaper::field("version", entry.version),
1345                crate::runtime::audit_log::AuditFieldEscaper::field(
1346                    "sequence_id",
1347                    entry.sequence_id,
1348                ),
1349            ]);
1350        }
1351        self.audit_log().record_event(builder.build());
1352    }
1353
1354    fn emit_vault_control_event(
1355        &self,
1356        kind: crate::runtime::control_events::EventKind,
1357        outcome: crate::runtime::control_events::Outcome,
1358        action: &'static str,
1359        collection: &str,
1360        key: &str,
1361        reason: &str,
1362        entry: Option<&VaultEntry>,
1363        extra_fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
1364    ) -> RedDBResult<()> {
1365        use crate::runtime::control_events::{
1366            ActorRef, ControlEvent, ControlEventCtx, ControlEventLedger, Sensitivity,
1367        };
1368        use std::borrow::Cow;
1369
1370        let tenant = current_tenant();
1371        let principal = current_auth_identity().map(|(principal, _)| principal);
1372        let actor_user = principal
1373            .as_ref()
1374            .map(|principal| crate::auth::UserId::from_parts(tenant.as_deref(), principal));
1375        let request_id = Self::vault_request_id();
1376        let actor = actor_user
1377            .as_ref()
1378            .map(ActorRef::User)
1379            .unwrap_or(ActorRef::Anonymous);
1380        let ctx = ControlEventCtx {
1381            actor,
1382            scope: tenant.as_ref().map(|scope| Cow::Borrowed(scope.as_str())),
1383            request_id: Some(Cow::Borrowed(request_id.as_str())),
1384            trace_id: None,
1385        };
1386
1387        let target = Self::vault_target_resource(collection, key);
1388        let mut fields = std::collections::HashMap::new();
1389        fields.insert("path".to_string(), Sensitivity::raw(target.clone()));
1390        fields.insert("collection".to_string(), Sensitivity::raw(collection));
1391        fields.insert("key".to_string(), Sensitivity::raw(key));
1392        fields.insert(
1393            "connection_id".to_string(),
1394            Sensitivity::raw(current_connection_id().to_string()),
1395        );
1396        if let Some(entry) = entry {
1397            fields.insert(
1398                "entity_id".to_string(),
1399                Sensitivity::raw(entry.id.raw().to_string()),
1400            );
1401            fields.insert(
1402                "sequence_id".to_string(),
1403                Sensitivity::raw(entry.sequence_id.to_string()),
1404            );
1405            fields.insert(
1406                "version".to_string(),
1407                Sensitivity::raw(entry.version.to_string()),
1408            );
1409            fields.insert("op".to_string(), Sensitivity::raw(entry.op.clone()));
1410            fields.insert(
1411                "tombstone".to_string(),
1412                Sensitivity::raw(entry.tombstone.to_string()),
1413            );
1414            if !entry.tombstone {
1415                fields.insert(
1416                    "fingerprint".to_string(),
1417                    Sensitivity::raw(vault_fingerprint(&entry.value)),
1418                );
1419            }
1420            fields.insert(
1421                "tags".to_string(),
1422                Sensitivity::raw(format!("{:?}", vault_tags_value(&entry.metadata))),
1423            );
1424        }
1425        for (key, value) in extra_fields {
1426            fields.insert(key, value);
1427        }
1428
1429        let event = ControlEvent {
1430            kind,
1431            outcome,
1432            action: Cow::Borrowed(action),
1433            resource: Some(format!("vault:{target}")),
1434            reason: Some(reason.to_string()),
1435            matched_policy_id: None,
1436            fields,
1437        };
1438        let ledger = self.inner.control_event_ledger.read();
1439        match ledger.emit(&ctx, event) {
1440            Ok(_) => Ok(()),
1441            Err(err) if self.inner.control_event_config.require_persistence() => {
1442                Err(RedDBError::Internal(err.to_string()))
1443            }
1444            Err(_) => Ok(()),
1445        }
1446    }
1447
1448    pub(crate) fn resolve_vault_secret_value(
1449        &self,
1450        collection: &str,
1451        key: &str,
1452    ) -> RedDBResult<Value> {
1453        let ops = KvAtomicOps::new(self);
1454        let entry = ops.get_vault_entry(collection, key)?;
1455        if let Err(reason) = self.check_vault_capability("vault:read", collection, key) {
1456            self.audit_vault_unseal(
1457                collection,
1458                key,
1459                crate::runtime::audit_log::Outcome::Denied,
1460                &reason,
1461                entry.as_ref(),
1462            );
1463            self.emit_vault_control_event(
1464                crate::runtime::control_events::EventKind::VaultRead,
1465                crate::runtime::control_events::Outcome::Denied,
1466                "vault:read",
1467                collection,
1468                key,
1469                &reason,
1470                entry.as_ref(),
1471                Vec::new(),
1472            )?;
1473            return Err(RedDBError::Query(reason));
1474        }
1475        let Some(entry) = entry else {
1476            let reason = "not_found";
1477            self.audit_vault_unseal(
1478                collection,
1479                key,
1480                crate::runtime::audit_log::Outcome::Denied,
1481                reason,
1482                None,
1483            );
1484            self.emit_vault_control_event(
1485                crate::runtime::control_events::EventKind::VaultRead,
1486                crate::runtime::control_events::Outcome::Denied,
1487                "vault:read",
1488                collection,
1489                key,
1490                reason,
1491                None,
1492                Vec::new(),
1493            )?;
1494            return Err(RedDBError::NotFound(format!(
1495                "vault secret '{}.{}' not found",
1496                collection, key
1497            )));
1498        };
1499        if entry.tombstone {
1500            let reason = "deleted";
1501            self.audit_vault_unseal(
1502                collection,
1503                key,
1504                crate::runtime::audit_log::Outcome::Denied,
1505                reason,
1506                Some(&entry),
1507            );
1508            self.emit_vault_control_event(
1509                crate::runtime::control_events::EventKind::VaultRead,
1510                crate::runtime::control_events::Outcome::Denied,
1511                "vault:read",
1512                collection,
1513                key,
1514                reason,
1515                Some(&entry),
1516                Vec::new(),
1517            )?;
1518            return Err(RedDBError::NotFound(format!(
1519                "vault secret '{}.{}' is deleted",
1520                collection, key
1521            )));
1522        }
1523        match self.unseal_vault_value(collection, &entry.value) {
1524            Ok(value) => {
1525                self.audit_vault_unseal(
1526                    collection,
1527                    key,
1528                    crate::runtime::audit_log::Outcome::Success,
1529                    "ok",
1530                    Some(&entry),
1531                );
1532                self.emit_vault_control_event(
1533                    crate::runtime::control_events::EventKind::VaultRead,
1534                    crate::runtime::control_events::Outcome::Allowed,
1535                    "vault:read",
1536                    collection,
1537                    key,
1538                    "ok",
1539                    Some(&entry),
1540                    Vec::new(),
1541                )?;
1542                Ok(value)
1543            }
1544            Err(err) => {
1545                let reason = err.to_string();
1546                self.audit_vault_unseal(
1547                    collection,
1548                    key,
1549                    crate::runtime::audit_log::Outcome::Error,
1550                    &reason,
1551                    Some(&entry),
1552                );
1553                self.emit_vault_control_event(
1554                    crate::runtime::control_events::EventKind::VaultRead,
1555                    crate::runtime::control_events::Outcome::Error,
1556                    "vault:read",
1557                    collection,
1558                    key,
1559                    &reason,
1560                    Some(&entry),
1561                    Vec::new(),
1562                )?;
1563                Err(err)
1564            }
1565        }
1566    }
1567
1568    /// Dispatch a `KV PUT / GET / DELETE` command.
1569    pub fn execute_kv_command(
1570        &self,
1571        raw_query: &str,
1572        cmd: &crate::storage::query::ast::KvCommand,
1573    ) -> RedDBResult<RuntimeQueryResult> {
1574        use crate::storage::query::ast::KvCommand;
1575
1576        let ops = KvAtomicOps::new(self);
1577
1578        match cmd {
1579            KvCommand::Put {
1580                model,
1581                collection,
1582                key,
1583                value,
1584                ttl_ms,
1585                tags,
1586                if_not_exists,
1587            } => {
1588                if *model == crate::catalog::CollectionModel::Vault {
1589                    self.check_system_vault_capability("vault:write", collection, key)
1590                        .map_err(RedDBError::Query)?;
1591                }
1592                self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1593                let (created, id) = ops.set_with_tags_for_model(
1594                    *model,
1595                    collection,
1596                    key,
1597                    value.clone(),
1598                    *ttl_ms,
1599                    tags,
1600                    *if_not_exists,
1601                )?;
1602
1603                let mut result = UnifiedResult::with_columns(vec![
1604                    "ok".into(),
1605                    "collection".into(),
1606                    "key".into(),
1607                    "id".into(),
1608                    "created".into(),
1609                    "tags".into(),
1610                ]);
1611                let mut record = UnifiedRecord::new();
1612                record.set("ok", Value::Boolean(true));
1613                record.set("collection", Value::text(collection.clone()));
1614                record.set("key", Value::text(key.clone()));
1615                record.set("id", Value::Integer(id.raw() as i64));
1616                record.set("created", Value::Boolean(created));
1617                record.set("tags", kv_tags_value(tags));
1618                result.push(record);
1619
1620                Ok(RuntimeQueryResult {
1621                    query: raw_query.to_string(),
1622                    mode: crate::storage::query::modes::QueryMode::Sql,
1623                    statement: if *model == crate::catalog::CollectionModel::Vault {
1624                        "vault_put"
1625                    } else {
1626                        "kv_put"
1627                    },
1628                    engine: if *model == crate::catalog::CollectionModel::Vault {
1629                        "vault"
1630                    } else {
1631                        "kv"
1632                    },
1633                    result,
1634                    affected_rows: 1,
1635                    statement_type: if created { "insert" } else { "update" },
1636                    bookmark: None,
1637                    notice: None,
1638                })
1639            }
1640            KvCommand::InvalidateTags { collection, tags } => {
1641                let invalidated = ops.invalidate_tags(collection, tags)?;
1642
1643                let mut result = UnifiedResult::with_columns(vec![
1644                    "ok".into(),
1645                    "collection".into(),
1646                    "invalidated".into(),
1647                    "tags".into(),
1648                ]);
1649                let mut record = UnifiedRecord::new();
1650                record.set("ok", Value::Boolean(true));
1651                record.set("collection", Value::text(collection.clone()));
1652                record.set("invalidated", Value::Integer(invalidated as i64));
1653                record.set("tags", kv_tags_value(tags));
1654                result.push(record);
1655
1656                Ok(RuntimeQueryResult {
1657                    query: raw_query.to_string(),
1658                    mode: crate::storage::query::modes::QueryMode::Sql,
1659                    statement: "kv_invalidate_tags",
1660                    engine: "kv",
1661                    result,
1662                    affected_rows: invalidated as u64,
1663                    statement_type: "delete",
1664                    bookmark: None,
1665                    notice: None,
1666                })
1667            }
1668
1669            KvCommand::Rotate {
1670                collection,
1671                key,
1672                value,
1673                tags,
1674            } => {
1675                self.check_system_vault_capability("vault:write", collection, key)
1676                    .map_err(RedDBError::Query)?;
1677                self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1678                let entry = ops.append_vault_version(
1679                    collection,
1680                    key,
1681                    value.clone(),
1682                    "rotate",
1683                    false,
1684                    tags,
1685                )?;
1686                self.record_kv_watch_event(
1687                    crate::replication::cdc::ChangeOperation::Update,
1688                    collection,
1689                    key,
1690                    entry.id.raw(),
1691                    None,
1692                    Some(vault_entry_metadata_json(&entry)),
1693                );
1694                self.audit_vault_lifecycle(
1695                    "rotate",
1696                    collection,
1697                    key,
1698                    crate::runtime::audit_log::Outcome::Success,
1699                    "ok",
1700                    Some(&entry),
1701                );
1702                self.emit_vault_control_event(
1703                    crate::runtime::control_events::EventKind::VaultRotate,
1704                    crate::runtime::control_events::Outcome::Allowed,
1705                    "vault:rotate",
1706                    collection,
1707                    key,
1708                    "ok",
1709                    Some(&entry),
1710                    Vec::new(),
1711                )?;
1712                Ok(vault_write_result(
1713                    raw_query,
1714                    "vault_rotate",
1715                    "update",
1716                    collection,
1717                    key,
1718                    &entry,
1719                    1,
1720                ))
1721            }
1722
1723            KvCommand::List {
1724                model,
1725                collection,
1726                prefix,
1727                limit,
1728                offset,
1729                as_json,
1730            } => {
1731                if *model == crate::catalog::CollectionModel::Vault {
1732                    if *as_json {
1733                        return Err(RedDBError::Query(
1734                            "LIST VAULT AS JSON is not supported; vault list only exposes metadata"
1735                                .to_string(),
1736                        ));
1737                    }
1738                    let mut entries = ops.latest_vault_entries(collection, prefix.as_deref())?;
1739                    entries.sort_by(|left, right| left.key.cmp(&right.key));
1740                    let mut result = UnifiedResult::with_columns(vec![
1741                        "collection".into(),
1742                        "key".into(),
1743                        "version".into(),
1744                        "fingerprint".into(),
1745                        "tags".into(),
1746                        "created_at".into(),
1747                        "updated_at".into(),
1748                        "status".into(),
1749                        "tombstone".into(),
1750                        "op".into(),
1751                    ]);
1752                    let mut visible = Vec::new();
1753                    for entry in entries {
1754                        match self.check_vault_capability(
1755                            "vault:read_metadata",
1756                            collection,
1757                            &entry.key,
1758                        ) {
1759                            Ok(()) => {
1760                                self.emit_vault_control_event(
1761                                    crate::runtime::control_events::EventKind::VaultMetadataRead,
1762                                    crate::runtime::control_events::Outcome::Allowed,
1763                                    "vault:read_metadata",
1764                                    collection,
1765                                    &entry.key,
1766                                    "ok",
1767                                    Some(&entry),
1768                                    Vec::new(),
1769                                )?;
1770                                visible.push(entry);
1771                            }
1772                            Err(reason) => {
1773                                self.emit_vault_control_event(
1774                                    crate::runtime::control_events::EventKind::VaultMetadataRead,
1775                                    crate::runtime::control_events::Outcome::Denied,
1776                                    "vault:read_metadata",
1777                                    collection,
1778                                    &entry.key,
1779                                    &reason,
1780                                    Some(&entry),
1781                                    Vec::new(),
1782                                )?;
1783                            }
1784                        }
1785                    }
1786                    for entry in visible
1787                        .into_iter()
1788                        .skip(*offset)
1789                        .take(limit.unwrap_or(usize::MAX))
1790                    {
1791                        push_vault_metadata_record(&mut result, collection, &entry.key, &entry);
1792                    }
1793                    Ok(RuntimeQueryResult {
1794                        query: raw_query.to_string(),
1795                        mode: crate::storage::query::modes::QueryMode::Sql,
1796                        statement: "vault_list",
1797                        engine: "vault",
1798                        result,
1799                        affected_rows: 0,
1800                        statement_type: "select",
1801                        bookmark: None,
1802                        notice: None,
1803                    })
1804                } else {
1805                    let mut result = UnifiedResult::with_columns(vec![
1806                        "rid".into(),
1807                        "collection".into(),
1808                        "kind".into(),
1809                        "tenant".into(),
1810                        "created_at".into(),
1811                        "updated_at".into(),
1812                        "key".into(),
1813                        "value".into(),
1814                        "tags".into(),
1815                    ]);
1816                    let entries = ops.latest_kv_entries(collection, prefix.as_deref())?;
1817                    if *as_json {
1818                        let mut tree = crate::json::Value::Object(crate::json::Map::new());
1819                        for (key, entity) in entries
1820                            .into_iter()
1821                            .skip(*offset)
1822                            .take(limit.unwrap_or(usize::MAX))
1823                        {
1824                            let Some(value) = kv_value_from_entity(&entity) else {
1825                                continue;
1826                            };
1827                            let relative = match prefix {
1828                                Some(pfx) if key == *pfx => "",
1829                                Some(pfx) => key
1830                                    .strip_prefix(pfx.as_str())
1831                                    .map(|tail| tail.strip_prefix('.').unwrap_or(tail))
1832                                    .unwrap_or(key.as_str()),
1833                                None => key.as_str(),
1834                            };
1835                            insert_kv_json_path(
1836                                &mut tree,
1837                                relative,
1838                                crate::presentation::entity_json::storage_value_to_json(&value),
1839                            );
1840                        }
1841                        Ok(kv_list_json_result(raw_query, collection, prefix, tree))
1842                    } else {
1843                        for (key, entity) in entries
1844                            .into_iter()
1845                            .skip(*offset)
1846                            .take(limit.unwrap_or(usize::MAX))
1847                        {
1848                            let mut record = UnifiedRecord::new();
1849                            record.set("rid", Value::UnsignedInteger(entity.id.raw()));
1850                            record.set("collection", Value::text(collection.clone()));
1851                            record.set("kind", Value::text("kv"));
1852                            record.set("tenant", Value::Null);
1853                            record.set("created_at", Value::UnsignedInteger(entity.created_at));
1854                            record.set("updated_at", Value::UnsignedInteger(entity.updated_at));
1855                            record.set("key", Value::text(key.clone()));
1856                            record.set(
1857                                "value",
1858                                kv_value_from_entity(&entity)
1859                                    .unwrap_or(crate::storage::schema::Value::Null),
1860                            );
1861                            record.set("tags", kv_tags_value(&ops.tags_for_key(collection, &key)));
1862                            result.push(record);
1863                        }
1864                        Ok(RuntimeQueryResult {
1865                            query: raw_query.to_string(),
1866                            mode: crate::storage::query::modes::QueryMode::Sql,
1867                            statement: "kv_list",
1868                            engine: "kv",
1869                            result,
1870                            affected_rows: 0,
1871                            statement_type: "select",
1872                            bookmark: None,
1873                            notice: None,
1874                        })
1875                    }
1876                }
1877            }
1878
1879            KvCommand::History { collection, key } => {
1880                let latest = ops.get_vault_entry(collection, key)?;
1881                if let Err(reason) =
1882                    self.check_vault_capability("vault:read_metadata", collection, key)
1883                {
1884                    self.emit_vault_control_event(
1885                        crate::runtime::control_events::EventKind::VaultMetadataRead,
1886                        crate::runtime::control_events::Outcome::Denied,
1887                        "vault:read_metadata",
1888                        collection,
1889                        key,
1890                        &reason,
1891                        latest.as_ref(),
1892                        Vec::new(),
1893                    )?;
1894                    return Err(RedDBError::Query(reason));
1895                }
1896                let versions =
1897                    super::keyed_spine::history_versions(ops.vault_versions(collection, key)?);
1898                let result = vault_history_result(collection, key, &versions);
1899                self.emit_vault_control_event(
1900                    crate::runtime::control_events::EventKind::VaultMetadataRead,
1901                    crate::runtime::control_events::Outcome::Allowed,
1902                    "vault:read_metadata",
1903                    collection,
1904                    key,
1905                    "ok",
1906                    latest.as_ref(),
1907                    Vec::new(),
1908                )?;
1909                Ok(RuntimeQueryResult {
1910                    query: raw_query.to_string(),
1911                    mode: crate::storage::query::modes::QueryMode::Sql,
1912                    statement: "vault_history",
1913                    engine: "vault",
1914                    result,
1915                    affected_rows: 0,
1916                    statement_type: "select",
1917                    bookmark: None,
1918                    notice: None,
1919                })
1920            }
1921
1922            KvCommand::Purge { collection, key } => {
1923                let entry = ops.get_vault_entry(collection, key)?;
1924                if let Err(reason) = self.check_vault_capability("vault:purge", collection, key) {
1925                    self.audit_vault_lifecycle(
1926                        "purge",
1927                        collection,
1928                        key,
1929                        crate::runtime::audit_log::Outcome::Denied,
1930                        &reason,
1931                        entry.as_ref(),
1932                    );
1933                    self.emit_vault_control_event(
1934                        crate::runtime::control_events::EventKind::VaultPurge,
1935                        crate::runtime::control_events::Outcome::Denied,
1936                        "vault:purge",
1937                        collection,
1938                        key,
1939                        &reason,
1940                        entry.as_ref(),
1941                        Vec::new(),
1942                    )?;
1943                    return Err(RedDBError::Query(reason));
1944                }
1945                self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1946                let purged = ops.purge_vault_versions(collection, key)?;
1947                self.audit_vault_lifecycle(
1948                    "purge",
1949                    collection,
1950                    key,
1951                    crate::runtime::audit_log::Outcome::Success,
1952                    "ok",
1953                    entry.as_ref(),
1954                );
1955                self.emit_vault_control_event(
1956                    crate::runtime::control_events::EventKind::VaultPurge,
1957                    crate::runtime::control_events::Outcome::Allowed,
1958                    "vault:purge",
1959                    collection,
1960                    key,
1961                    "ok",
1962                    entry.as_ref(),
1963                    vec![(
1964                        "purged".to_string(),
1965                        crate::runtime::control_events::Sensitivity::raw(purged.to_string()),
1966                    )],
1967                )?;
1968                let mut result = UnifiedResult::with_columns(vec![
1969                    "ok".into(),
1970                    "collection".into(),
1971                    "key".into(),
1972                    "purged".into(),
1973                ]);
1974                let mut record = UnifiedRecord::new();
1975                record.set("ok", Value::Boolean(true));
1976                record.set("collection", Value::text(collection.clone()));
1977                record.set("key", Value::text(key.clone()));
1978                record.set("purged", Value::Integer(purged as i64));
1979                result.push(record);
1980                Ok(RuntimeQueryResult {
1981                    query: raw_query.to_string(),
1982                    mode: crate::storage::query::modes::QueryMode::Sql,
1983                    statement: "vault_purge",
1984                    engine: "vault",
1985                    result,
1986                    affected_rows: purged as u64,
1987                    statement_type: "delete",
1988                    bookmark: None,
1989                    notice: None,
1990                })
1991            }
1992
1993            KvCommand::Get {
1994                model,
1995                collection,
1996                key,
1997            } => {
1998                if *model == crate::catalog::CollectionModel::Vault {
1999                    let entry = ops.get_vault_entry(collection, key)?;
2000                    if let Err(reason) =
2001                        self.check_vault_capability("vault:read_metadata", collection, key)
2002                    {
2003                        self.emit_vault_control_event(
2004                            crate::runtime::control_events::EventKind::VaultMetadataRead,
2005                            crate::runtime::control_events::Outcome::Denied,
2006                            "vault:read_metadata",
2007                            collection,
2008                            key,
2009                            &reason,
2010                            entry.as_ref(),
2011                            Vec::new(),
2012                        )?;
2013                        return Err(RedDBError::Query(reason));
2014                    }
2015                    let key_available = self.vault_key_available(collection);
2016                    let result =
2017                        vault_metadata_result(collection, key, entry.as_ref(), key_available);
2018                    self.emit_vault_control_event(
2019                        crate::runtime::control_events::EventKind::VaultMetadataRead,
2020                        crate::runtime::control_events::Outcome::Allowed,
2021                        "vault:read_metadata",
2022                        collection,
2023                        key,
2024                        "ok",
2025                        entry.as_ref(),
2026                        Vec::new(),
2027                    )?;
2028                    return Ok(RuntimeQueryResult {
2029                        query: raw_query.to_string(),
2030                        mode: crate::storage::query::modes::QueryMode::Sql,
2031                        statement: "vault_get",
2032                        engine: "vault",
2033                        result,
2034                        affected_rows: 0,
2035                        statement_type: "select",
2036                        bookmark: None,
2037                        notice: None,
2038                    });
2039                }
2040
2041                let entity = ops.get_entity(*model, collection, key)?;
2042                let value = entity.as_ref().and_then(kv_value_from_entity);
2043                if *model == crate::catalog::CollectionModel::Kv {
2044                    self.inner.kv_stats.incr_gets();
2045                }
2046                let mut result = UnifiedResult::with_columns(vec![
2047                    "rid".into(),
2048                    "collection".into(),
2049                    "kind".into(),
2050                    "tenant".into(),
2051                    "created_at".into(),
2052                    "updated_at".into(),
2053                    "key".into(),
2054                    "value".into(),
2055                    "tags".into(),
2056                ]);
2057                let mut record = UnifiedRecord::new();
2058                if let Some(entity) = entity.as_ref() {
2059                    record.set("rid", Value::UnsignedInteger(entity.id.raw()));
2060                    record.set("created_at", Value::UnsignedInteger(entity.created_at));
2061                    record.set("updated_at", Value::UnsignedInteger(entity.updated_at));
2062                } else {
2063                    record.set("rid", Value::Null);
2064                    record.set("created_at", Value::Null);
2065                    record.set("updated_at", Value::Null);
2066                }
2067                record.set("collection", Value::text(collection.clone()));
2068                record.set("kind", Value::text(keyed_model_name(*model).to_string()));
2069                record.set("tenant", Value::Null);
2070                record.set("key", Value::text(key.clone()));
2071                record.set(
2072                    "value",
2073                    value.unwrap_or(crate::storage::schema::Value::Null),
2074                );
2075                record.set("tags", kv_tags_value(&ops.tags_for_key(collection, key)));
2076                result.push(record);
2077
2078                Ok(RuntimeQueryResult {
2079                    query: raw_query.to_string(),
2080                    mode: crate::storage::query::modes::QueryMode::Sql,
2081                    statement: "kv_get",
2082                    engine: "kv",
2083                    result,
2084                    affected_rows: 0,
2085                    statement_type: "select",
2086                    bookmark: None,
2087                    notice: None,
2088                })
2089            }
2090            KvCommand::Watch {
2091                model,
2092                collection,
2093                key,
2094                prefix,
2095                from_lsn,
2096            } => {
2097                let watch_key = if *prefix {
2098                    format!("{key}.*")
2099                } else {
2100                    key.clone()
2101                };
2102                let endpoint = match from_lsn {
2103                    Some(lsn) => format!(
2104                        "/collections/{collection}/{}/{watch_key}/watch?since_lsn={lsn}",
2105                        keyed_model_name(*model)
2106                    ),
2107                    None => format!(
2108                        "/collections/{collection}/{}/{watch_key}/watch",
2109                        keyed_model_name(*model)
2110                    ),
2111                };
2112                let mut result = UnifiedResult::with_columns(vec![
2113                    "collection".into(),
2114                    "key".into(),
2115                    "prefix".into(),
2116                    "from_lsn".into(),
2117                    "watch_url".into(),
2118                    "streaming".into(),
2119                ]);
2120                let mut record = UnifiedRecord::new();
2121                record.set("collection", Value::text(collection.clone()));
2122                record.set("key", Value::text(watch_key));
2123                record.set("prefix", Value::Boolean(*prefix));
2124                record.set(
2125                    "from_lsn",
2126                    from_lsn
2127                        .map(Value::UnsignedInteger)
2128                        .unwrap_or(crate::storage::schema::Value::Null),
2129                );
2130                record.set("watch_url", Value::text(endpoint));
2131                record.set("streaming", Value::Boolean(true));
2132                result.push(record);
2133
2134                Ok(RuntimeQueryResult {
2135                    query: raw_query.to_string(),
2136                    mode: crate::storage::query::modes::QueryMode::Sql,
2137                    statement: "kv_watch",
2138                    engine: keyed_model_name(*model),
2139                    result,
2140                    affected_rows: 0,
2141                    statement_type: "stream",
2142                    bookmark: None,
2143                    notice: None,
2144                })
2145            }
2146
2147            KvCommand::Unseal {
2148                collection,
2149                key,
2150                version,
2151            } => {
2152                let latest = ops.get_vault_entry(collection, key)?;
2153                let entry = match version {
2154                    Some(version) => ops.get_vault_entry_version(collection, key, *version)?,
2155                    None => latest.clone(),
2156                };
2157                let action = match (version, latest.as_ref()) {
2158                    (Some(requested), Some(latest)) if *requested == latest.version => "vault:read",
2159                    (Some(_), _) => "vault:unseal_history",
2160                    _ => "vault:read",
2161                };
2162                let event_kind = if action == "vault:read" {
2163                    crate::runtime::control_events::EventKind::VaultRead
2164                } else {
2165                    crate::runtime::control_events::EventKind::VaultUnseal
2166                };
2167                if let Err(reason) = self.check_vault_capability(action, collection, key) {
2168                    self.audit_vault_unseal(
2169                        collection,
2170                        key,
2171                        crate::runtime::audit_log::Outcome::Denied,
2172                        &reason,
2173                        entry.as_ref(),
2174                    );
2175                    self.emit_vault_control_event(
2176                        event_kind,
2177                        crate::runtime::control_events::Outcome::Denied,
2178                        action,
2179                        collection,
2180                        key,
2181                        &reason,
2182                        entry.as_ref(),
2183                        Vec::new(),
2184                    )?;
2185                    return Err(RedDBError::Query(reason));
2186                }
2187                let Some(entry) = entry else {
2188                    let reason = "not_found";
2189                    self.audit_vault_unseal(
2190                        collection,
2191                        key,
2192                        crate::runtime::audit_log::Outcome::Denied,
2193                        reason,
2194                        None,
2195                    );
2196                    self.emit_vault_control_event(
2197                        event_kind,
2198                        crate::runtime::control_events::Outcome::Denied,
2199                        action,
2200                        collection,
2201                        key,
2202                        reason,
2203                        None,
2204                        Vec::new(),
2205                    )?;
2206                    return Err(RedDBError::NotFound(format!(
2207                        "vault secret '{}.{}' not found",
2208                        collection, key
2209                    )));
2210                };
2211                if entry.tombstone {
2212                    let reason = "deleted";
2213                    self.audit_vault_unseal(
2214                        collection,
2215                        key,
2216                        crate::runtime::audit_log::Outcome::Denied,
2217                        reason,
2218                        Some(&entry),
2219                    );
2220                    self.emit_vault_control_event(
2221                        event_kind,
2222                        crate::runtime::control_events::Outcome::Denied,
2223                        action,
2224                        collection,
2225                        key,
2226                        reason,
2227                        Some(&entry),
2228                        Vec::new(),
2229                    )?;
2230                    return Err(RedDBError::NotFound(format!(
2231                        "vault secret '{}.{}' is deleted",
2232                        collection, key
2233                    )));
2234                }
2235                match self.unseal_vault_value(collection, &entry.value) {
2236                    Ok(value) => {
2237                        self.audit_vault_unseal(
2238                            collection,
2239                            key,
2240                            crate::runtime::audit_log::Outcome::Success,
2241                            "ok",
2242                            Some(&entry),
2243                        );
2244                        self.emit_vault_control_event(
2245                            event_kind,
2246                            crate::runtime::control_events::Outcome::Allowed,
2247                            action,
2248                            collection,
2249                            key,
2250                            "ok",
2251                            Some(&entry),
2252                            Vec::new(),
2253                        )?;
2254                        let mut result = UnifiedResult::with_columns(vec![
2255                            "collection".into(),
2256                            "key".into(),
2257                            "value".into(),
2258                        ]);
2259                        let mut record = UnifiedRecord::new();
2260                        record.set("collection", Value::text(collection.clone()));
2261                        record.set("key", Value::text(key.clone()));
2262                        record.set("value", value);
2263                        result.push(record);
2264                        Ok(RuntimeQueryResult {
2265                            query: raw_query.to_string(),
2266                            mode: crate::storage::query::modes::QueryMode::Sql,
2267                            statement: "vault_unseal",
2268                            engine: "vault",
2269                            result,
2270                            affected_rows: 0,
2271                            statement_type: "select",
2272                            bookmark: None,
2273                            notice: None,
2274                        })
2275                    }
2276                    Err(err) => {
2277                        let reason = err.to_string();
2278                        self.audit_vault_unseal(
2279                            collection,
2280                            key,
2281                            crate::runtime::audit_log::Outcome::Error,
2282                            &reason,
2283                            Some(&entry),
2284                        );
2285                        self.emit_vault_control_event(
2286                            event_kind,
2287                            crate::runtime::control_events::Outcome::Error,
2288                            action,
2289                            collection,
2290                            key,
2291                            &reason,
2292                            Some(&entry),
2293                            Vec::new(),
2294                        )?;
2295                        Err(err)
2296                    }
2297                }
2298            }
2299
2300            KvCommand::Incr {
2301                model,
2302                collection,
2303                key,
2304                by,
2305                ttl_ms,
2306            } => {
2307                self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2308                let new_value = ops.incr(*model, collection, key, *by, *ttl_ms)?;
2309
2310                let mut result = UnifiedResult::with_columns(vec![
2311                    "ok".into(),
2312                    "collection".into(),
2313                    "key".into(),
2314                    "value".into(),
2315                ]);
2316                let mut record = UnifiedRecord::new();
2317                record.set("ok", Value::Boolean(true));
2318                record.set("collection", Value::text(collection.clone()));
2319                record.set("key", Value::text(key.clone()));
2320                record.set("value", Value::Integer(new_value));
2321                result.push(record);
2322
2323                Ok(RuntimeQueryResult {
2324                    query: raw_query.to_string(),
2325                    mode: crate::storage::query::modes::QueryMode::Sql,
2326                    statement: "kv_incr",
2327                    engine: "kv",
2328                    result,
2329                    affected_rows: 1,
2330                    statement_type: "update",
2331                    bookmark: None,
2332                    notice: None,
2333                })
2334            }
2335
2336            KvCommand::Cas {
2337                model,
2338                collection,
2339                key,
2340                expected,
2341                new_value,
2342                ttl_ms,
2343            } => {
2344                self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2345                let (ok, current) = ops.cas(
2346                    *model,
2347                    collection,
2348                    key,
2349                    expected.as_ref(),
2350                    new_value.clone(),
2351                    *ttl_ms,
2352                )?;
2353
2354                let mut result = UnifiedResult::with_columns(vec![
2355                    "ok".into(),
2356                    "collection".into(),
2357                    "key".into(),
2358                    "current".into(),
2359                ]);
2360                let mut record = UnifiedRecord::new();
2361                record.set("ok", Value::Boolean(ok));
2362                record.set("collection", Value::text(collection.clone()));
2363                record.set("key", Value::text(key.clone()));
2364                record.set(
2365                    "current",
2366                    current.unwrap_or(crate::storage::schema::Value::Null),
2367                );
2368                result.push(record);
2369
2370                Ok(RuntimeQueryResult {
2371                    query: raw_query.to_string(),
2372                    mode: crate::storage::query::modes::QueryMode::Sql,
2373                    statement: "kv_cas",
2374                    engine: "kv",
2375                    result,
2376                    affected_rows: if ok { 1 } else { 0 },
2377                    statement_type: "update",
2378                    bookmark: None,
2379                    notice: None,
2380                })
2381            }
2382
2383            KvCommand::Delete {
2384                model,
2385                collection,
2386                key,
2387            } => {
2388                if *model == crate::catalog::CollectionModel::Vault {
2389                    self.check_system_vault_capability("vault:write", collection, key)
2390                        .map_err(RedDBError::Query)?;
2391                    self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2392                    let entry = ops.append_vault_version(
2393                        collection,
2394                        key,
2395                        Value::Null,
2396                        "delete",
2397                        true,
2398                        &[],
2399                    )?;
2400                    self.record_kv_watch_event(
2401                        crate::replication::cdc::ChangeOperation::Delete,
2402                        collection,
2403                        key,
2404                        entry.id.raw(),
2405                        None,
2406                        Some(vault_entry_metadata_json(&entry)),
2407                    );
2408                    self.audit_vault_lifecycle(
2409                        "delete",
2410                        collection,
2411                        key,
2412                        crate::runtime::audit_log::Outcome::Success,
2413                        "ok",
2414                        Some(&entry),
2415                    );
2416                    return Ok(vault_write_result(
2417                        raw_query,
2418                        "vault_delete",
2419                        "delete",
2420                        collection,
2421                        key,
2422                        &entry,
2423                        1,
2424                    ));
2425                }
2426                self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2427                let deleted = ops.delete(*model, collection, key)?;
2428
2429                let mut result = UnifiedResult::with_columns(vec![
2430                    "ok".into(),
2431                    "collection".into(),
2432                    "key".into(),
2433                    "deleted".into(),
2434                ]);
2435                let mut record = UnifiedRecord::new();
2436                record.set("ok", Value::Boolean(true));
2437                record.set("collection", Value::text(collection.clone()));
2438                record.set("key", Value::text(key.clone()));
2439                record.set("deleted", Value::Boolean(deleted));
2440                result.push(record);
2441
2442                Ok(RuntimeQueryResult {
2443                    query: raw_query.to_string(),
2444                    mode: crate::storage::query::modes::QueryMode::Sql,
2445                    statement: "kv_delete",
2446                    engine: "kv",
2447                    result,
2448                    affected_rows: if deleted { 1 } else { 0 },
2449                    statement_type: "delete",
2450                    bookmark: None,
2451                    notice: None,
2452                })
2453            }
2454        }
2455    }
2456
2457    pub fn vault_watch_events_since(
2458        &self,
2459        collection: &str,
2460        key: &str,
2461        since_lsn: u64,
2462        max_count: usize,
2463    ) -> Vec<crate::replication::cdc::KvWatchEvent> {
2464        self.kv_watch_events_since(collection, key, since_lsn, max_count)
2465            .into_iter()
2466            .filter(|event| {
2467                self.check_vault_capability("vault:read_metadata", &event.collection, &event.key)
2468                    .is_ok()
2469            })
2470            .map(vault_filter_watch_event)
2471            .collect()
2472    }
2473
2474    pub fn vault_watch_events_since_prefix(
2475        &self,
2476        collection: &str,
2477        prefix: &str,
2478        since_lsn: u64,
2479        max_count: usize,
2480    ) -> Vec<crate::replication::cdc::KvWatchEvent> {
2481        self.kv_watch_events_since_prefix(collection, prefix, since_lsn, max_count)
2482            .into_iter()
2483            .filter(|event| {
2484                self.check_vault_capability("vault:read_metadata", &event.collection, &event.key)
2485                    .is_ok()
2486            })
2487            .map(vault_filter_watch_event)
2488            .collect()
2489    }
2490
2491    fn check_kv_invalidate_policy(&self, collection: &str) -> RedDBResult<()> {
2492        let auth_store = match self.inner.auth_store.read().clone() {
2493            Some(store) => store,
2494            None => return Ok(()),
2495        };
2496        let (username, role) = match crate::runtime::impl_core::current_auth_identity() {
2497            Some(identity) => identity,
2498            None => return Ok(()),
2499        };
2500        if role < crate::auth::Role::Write {
2501            return Err(RedDBError::Query(format!(
2502                "principal=`{username}` role=`{role:?}` cannot invalidate KV tags"
2503            )));
2504        }
2505        if !auth_store.iam_authorization_enabled() {
2506            return Ok(());
2507        }
2508
2509        let tenant = crate::runtime::impl_core::current_tenant();
2510        let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
2511        let mut resource =
2512            crate::auth::policies::ResourceRef::new("kv".to_string(), collection.to_string());
2513        if let Some(tenant) = tenant.clone() {
2514            resource = resource.with_tenant(tenant);
2515        }
2516        let ctx = crate::auth::policies::EvalContext {
2517            principal_tenant: tenant.clone(),
2518            current_tenant: tenant,
2519            peer_ip: None,
2520            mfa_present: false,
2521            now_ms: current_unix_ms(),
2522            principal_is_admin_role: role == crate::auth::Role::Admin,
2523            principal_is_platform_scoped: principal.tenant.is_none(),
2524        };
2525        if auth_store.check_policy_authz_with_role(
2526            &principal,
2527            "kv:invalidate",
2528            &resource,
2529            &ctx,
2530            role,
2531        ) {
2532            Ok(())
2533        } else {
2534            Err(RedDBError::Query(format!(
2535                "principal=`{username}` action=`kv:invalidate` resource=`kv:{collection}` denied by IAM policy"
2536            )))
2537        }
2538    }
2539}
2540
2541fn ttl_metadata(ttl_ms: Option<u64>) -> Option<Metadata> {
2542    let ttl_ms = ttl_ms?;
2543    Some(Metadata::with_fields(
2544        [(
2545            "_ttl_ms".to_string(),
2546            if ttl_ms <= i64::MAX as u64 {
2547                MetadataValue::Int(ttl_ms as i64)
2548            } else {
2549                MetadataValue::Timestamp(ttl_ms)
2550            },
2551        )]
2552        .into_iter()
2553        .collect(),
2554    ))
2555}
2556
2557fn vault_write_result(
2558    raw_query: &str,
2559    statement: &'static str,
2560    statement_type: &'static str,
2561    collection: &str,
2562    key: &str,
2563    entry: &VaultEntry,
2564    affected_rows: u64,
2565) -> RuntimeQueryResult {
2566    let mut result = UnifiedResult::with_columns(vec![
2567        "ok".into(),
2568        "collection".into(),
2569        "key".into(),
2570        "version".into(),
2571        "fingerprint".into(),
2572        "tombstone".into(),
2573        "op".into(),
2574        "id".into(),
2575    ]);
2576    let mut record = UnifiedRecord::new();
2577    record.set("ok", Value::Boolean(true));
2578    record.set("collection", Value::text(collection.to_string()));
2579    record.set("key", Value::text(key.to_string()));
2580    record.set("version", Value::Integer(entry.version));
2581    if entry.tombstone {
2582        record.set("fingerprint", Value::Null);
2583    } else {
2584        record.set("fingerprint", Value::text(vault_fingerprint(&entry.value)));
2585    }
2586    record.set("tombstone", Value::Boolean(entry.tombstone));
2587    record.set("op", Value::text(entry.op.clone()));
2588    record.set("id", Value::Integer(entry.id.raw() as i64));
2589    result.push(record);
2590    RuntimeQueryResult {
2591        query: raw_query.to_string(),
2592        mode: crate::storage::query::modes::QueryMode::Sql,
2593        statement,
2594        engine: "vault",
2595        result,
2596        affected_rows,
2597        statement_type,
2598        bookmark: None,
2599        notice: None,
2600    }
2601}
2602
2603fn vault_history_result(collection: &str, key: &str, versions: &[VaultEntry]) -> UnifiedResult {
2604    let mut result = UnifiedResult::with_columns(vec![
2605        "collection".into(),
2606        "key".into(),
2607        "version".into(),
2608        "fingerprint".into(),
2609        "tags".into(),
2610        "created_at".into(),
2611        "updated_at".into(),
2612        "status".into(),
2613        "tombstone".into(),
2614        "op".into(),
2615    ]);
2616    for entry in versions {
2617        push_vault_metadata_record(&mut result, collection, key, entry);
2618    }
2619    result
2620}
2621
2622fn push_vault_metadata_record(
2623    result: &mut UnifiedResult,
2624    collection: &str,
2625    key: &str,
2626    entry: &VaultEntry,
2627) {
2628    let mut record = UnifiedRecord::new();
2629    record.set("collection", Value::text(collection.to_string()));
2630    record.set("key", Value::text(key.to_string()));
2631    record.set("version", Value::Integer(entry.version));
2632    if entry.tombstone {
2633        record.set("fingerprint", Value::Null);
2634        record.set("status", Value::text("deleted"));
2635    } else {
2636        record.set("fingerprint", Value::text(vault_fingerprint(&entry.value)));
2637        record.set("status", Value::text("sealed"));
2638    }
2639    record.set("tags", vault_tags_value(&entry.metadata));
2640    record.set("created_at", Value::TimestampMs(entry.created_at as i64));
2641    record.set("updated_at", Value::TimestampMs(entry.updated_at as i64));
2642    record.set("tombstone", Value::Boolean(entry.tombstone));
2643    record.set("op", Value::text(entry.op.clone()));
2644    result.push(record);
2645}
2646
2647fn vault_metadata_result(
2648    collection: &str,
2649    key: &str,
2650    entry: Option<&VaultEntry>,
2651    key_available: bool,
2652) -> UnifiedResult {
2653    let mut result = UnifiedResult::with_columns(vec![
2654        "collection".into(),
2655        "key".into(),
2656        "version".into(),
2657        "fingerprint".into(),
2658        "tags".into(),
2659        "created_at".into(),
2660        "updated_at".into(),
2661        "value".into(),
2662        "status".into(),
2663        "tombstone".into(),
2664        "op".into(),
2665    ]);
2666    let mut record = UnifiedRecord::new();
2667    record.set("collection", Value::text(collection.to_string()));
2668    record.set("key", Value::text(key.to_string()));
2669    match entry {
2670        Some(entry) => {
2671            record.set("version", Value::Integer(entry.version));
2672            if entry.tombstone {
2673                record.set("fingerprint", Value::Null);
2674            } else {
2675                record.set("fingerprint", Value::text(vault_fingerprint(&entry.value)));
2676            }
2677            record.set("tags", vault_tags_value(&entry.metadata));
2678            record.set("created_at", Value::TimestampMs(entry.created_at as i64));
2679            record.set("updated_at", Value::TimestampMs(entry.updated_at as i64));
2680            record.set("value", Value::text("***"));
2681            record.set(
2682                "status",
2683                Value::text(if entry.tombstone {
2684                    "deleted"
2685                } else if key_available {
2686                    "sealed"
2687                } else {
2688                    "sealed_unavailable"
2689                }),
2690            );
2691            record.set("tombstone", Value::Boolean(entry.tombstone));
2692            record.set("op", Value::text(entry.op.clone()));
2693        }
2694        None => {
2695            record.set("version", Value::Null);
2696            record.set("fingerprint", Value::Null);
2697            record.set("tags", Value::Array(Vec::new()));
2698            record.set("created_at", Value::Null);
2699            record.set("updated_at", Value::Null);
2700            record.set("value", Value::text(""));
2701            record.set("status", Value::text("missing"));
2702            record.set("tombstone", Value::Boolean(false));
2703            record.set("op", Value::Null);
2704        }
2705    }
2706    result.push(record);
2707    result
2708}
2709
2710fn vault_fingerprint(value: &Value) -> String {
2711    match value {
2712        Value::Secret(payload) => crate::utils::to_hex(&crate::crypto::sha256::sha256(payload)),
2713        other => crate::utils::to_hex(&crate::crypto::sha256::sha256(&other.to_bytes())),
2714    }
2715}
2716
2717fn vault_entry_metadata_json(entry: &VaultEntry) -> crate::json::Value {
2718    let mut object = crate::json::Map::new();
2719    object.insert(
2720        "key".to_string(),
2721        crate::json::Value::String(entry.key.clone()),
2722    );
2723    object.insert(
2724        "version".to_string(),
2725        crate::json::Value::Number(entry.version as f64),
2726    );
2727    object.insert(
2728        "fingerprint".to_string(),
2729        if entry.tombstone {
2730            crate::json::Value::Null
2731        } else {
2732            crate::json::Value::String(vault_fingerprint(&entry.value))
2733        },
2734    );
2735    object.insert("tags".to_string(), vault_tags_json(&entry.metadata));
2736    object.insert(
2737        "actor".to_string(),
2738        crate::json::Value::String(RedDBRuntime::current_vault_actor()),
2739    );
2740    object.insert(
2741        "sequence_id".to_string(),
2742        crate::json::Value::Number(entry.sequence_id as f64),
2743    );
2744    object.insert(
2745        "tombstone".to_string(),
2746        crate::json::Value::Bool(entry.tombstone),
2747    );
2748    object.insert(
2749        "op".to_string(),
2750        crate::json::Value::String(entry.op.clone()),
2751    );
2752    crate::json::Value::Object(object)
2753}
2754
2755fn vault_tags_json(metadata: &Metadata) -> crate::json::Value {
2756    match vault_tags_value(metadata) {
2757        Value::Array(values) => crate::json::Value::Array(
2758            values
2759                .into_iter()
2760                .filter_map(|value| match value {
2761                    Value::Text(tag) => Some(crate::json::Value::String(tag.to_string())),
2762                    _ => None,
2763                })
2764                .collect(),
2765        ),
2766        _ => crate::json::Value::Array(Vec::new()),
2767    }
2768}
2769
2770fn vault_tags_metadata(tags: &[String]) -> std::collections::HashMap<String, MetadataValue> {
2771    [(
2772        "tags".to_string(),
2773        MetadataValue::Array(
2774            tags.iter()
2775                .map(|tag| MetadataValue::String(tag.clone()))
2776                .collect(),
2777        ),
2778    )]
2779    .into_iter()
2780    .collect()
2781}
2782
2783fn vault_filter_watch_event(
2784    mut event: crate::replication::cdc::KvWatchEvent,
2785) -> crate::replication::cdc::KvWatchEvent {
2786    event.before = event.before.and_then(vault_metadata_json_only);
2787    event.after = event.after.and_then(vault_metadata_json_only);
2788    event
2789}
2790
2791fn vault_metadata_json_only(value: crate::json::Value) -> Option<crate::json::Value> {
2792    let object = value.as_object()?;
2793    let mut out = crate::json::Map::new();
2794    for field in [
2795        "key",
2796        "version",
2797        "fingerprint",
2798        "tags",
2799        "actor",
2800        "sequence_id",
2801        "tombstone",
2802        "op",
2803    ] {
2804        if let Some(value) = object.get(field) {
2805            out.insert(field.to_string(), value.clone());
2806        }
2807    }
2808    Some(crate::json::Value::Object(out))
2809}
2810
2811fn vault_tags_value(metadata: &Metadata) -> Value {
2812    match metadata.get("tags") {
2813        Some(MetadataValue::Array(values)) => Value::Array(
2814            values
2815                .iter()
2816                .filter_map(|value| match value {
2817                    MetadataValue::String(tag) => Some(Value::text(tag.clone())),
2818                    _ => None,
2819                })
2820                .collect(),
2821        ),
2822        Some(MetadataValue::String(tag)) if !tag.is_empty() => {
2823            Value::Array(vec![Value::text(tag.clone())])
2824        }
2825        _ => Value::Array(Vec::new()),
2826    }
2827}
2828
2829fn decode_vault_key(hex_key: &str) -> RedDBResult<[u8; 32]> {
2830    let bytes = hex::decode(hex_key)
2831        .map_err(|_| RedDBError::Query("vault sealed_unavailable: bad key material".to_string()))?;
2832    let key: [u8; 32] = bytes.try_into().map_err(|_| {
2833        RedDBError::Query("vault sealed_unavailable: bad key material length".to_string())
2834    })?;
2835    Ok(key)
2836}
2837
2838fn kv_tags_metadata(tags: &[String]) -> Option<(String, MetadataValue)> {
2839    if tags.is_empty() {
2840        return None;
2841    }
2842    let values = tags
2843        .iter()
2844        .map(|tag| MetadataValue::String(tag.clone()))
2845        .collect();
2846    Some(("_kv_tags".to_string(), MetadataValue::Array(values)))
2847}
2848
2849fn kv_tags_value(tags: &[String]) -> Value {
2850    let json = crate::json::Value::Array(
2851        tags.iter()
2852            .map(|tag| crate::json::Value::String(tag.clone()))
2853            .collect(),
2854    );
2855    Value::Json(crate::json::to_vec(&json).unwrap_or_default())
2856}
2857
2858fn kv_value_from_entity(entity: &crate::storage::UnifiedEntity) -> Option<Value> {
2859    if let crate::storage::EntityData::Row(ref row) = entity.data {
2860        if let Some(ref named) = row.named {
2861            return named.get("value").cloned();
2862        }
2863    }
2864    None
2865}
2866
2867/// Whether a KV row entity carries the logical `key`.
2868fn kv_entity_has_key(entity: &crate::storage::UnifiedEntity, key: &str) -> bool {
2869    if let crate::storage::EntityData::Row(ref row) = entity.data {
2870        if let Some(ref named) = row.named {
2871            if let Some(crate::storage::schema::Value::Text(ref k)) = named.get("key") {
2872                return &**k == key;
2873            }
2874        }
2875    }
2876    false
2877}
2878
2879fn insert_kv_json_path(root: &mut crate::json::Value, path: &str, value: crate::json::Value) {
2880    let segments: Vec<&str> = path
2881        .split('.')
2882        .filter(|segment| !segment.is_empty())
2883        .collect();
2884    insert_kv_json_segments(root, &segments, value);
2885}
2886
2887fn insert_kv_json_segments(
2888    root: &mut crate::json::Value,
2889    segments: &[&str],
2890    value: crate::json::Value,
2891) {
2892    if segments.is_empty() {
2893        *root = value;
2894        return;
2895    }
2896
2897    if !matches!(root, crate::json::Value::Object(_)) {
2898        *root = crate::json::Value::Object(crate::json::Map::new());
2899    }
2900
2901    let crate::json::Value::Object(map) = root else {
2902        return;
2903    };
2904    if segments.len() == 1 {
2905        map.insert(segments[0].to_string(), value);
2906        return;
2907    }
2908    let entry = map
2909        .entry(segments[0].to_string())
2910        .or_insert_with(|| crate::json::Value::Object(crate::json::Map::new()));
2911    insert_kv_json_segments(entry, &segments[1..], value);
2912}
2913
2914fn kv_list_json_result(
2915    raw_query: &str,
2916    collection: &str,
2917    prefix: &Option<String>,
2918    value: crate::json::Value,
2919) -> RuntimeQueryResult {
2920    let mut result =
2921        UnifiedResult::with_columns(vec!["collection".into(), "prefix".into(), "value".into()]);
2922    let mut record = UnifiedRecord::new();
2923    record.set("collection", Value::text(collection.to_string()));
2924    record.set(
2925        "prefix",
2926        prefix
2927            .as_ref()
2928            .map(|prefix| Value::text(prefix.clone()))
2929            .unwrap_or(Value::Null),
2930    );
2931    record.set("value", Value::Json(value.to_string_compact().into_bytes()));
2932    result.push(record);
2933    RuntimeQueryResult {
2934        query: raw_query.to_string(),
2935        mode: crate::storage::query::modes::QueryMode::Sql,
2936        statement: "kv_list_json",
2937        engine: "kv",
2938        result,
2939        affected_rows: 0,
2940        statement_type: "select",
2941        bookmark: None,
2942        notice: None,
2943    }
2944}
2945
2946fn kv_collection_contract(name: &str) -> crate::physical::CollectionContract {
2947    let now = current_unix_ms();
2948    crate::physical::CollectionContract {
2949        name: name.to_string(),
2950        declared_model: crate::catalog::CollectionModel::Kv,
2951        schema_mode: crate::catalog::SchemaMode::Dynamic,
2952        origin: crate::physical::ContractOrigin::Implicit,
2953        version: 1,
2954        created_at_unix_ms: now,
2955        updated_at_unix_ms: now,
2956        default_ttl_ms: None,
2957        vector_dimension: None,
2958        vector_metric: None,
2959        context_index_fields: Vec::new(),
2960        declared_columns: Vec::new(),
2961        table_def: None,
2962        timestamps_enabled: false,
2963        context_index_enabled: false,
2964        metrics_raw_retention_ms: None,
2965        metrics_rollup_policies: Vec::new(),
2966        metrics_tenant_identity: None,
2967        metrics_namespace: None,
2968        append_only: false,
2969        subscriptions: Vec::new(),
2970        analytics_config: Vec::new(),
2971        session_key: None,
2972        session_gap_ms: None,
2973        retention_duration_ms: None,
2974        analytical_storage: None,
2975
2976        ai_policy: None,
2977    }
2978}
2979
2980fn current_unix_ms() -> u128 {
2981    std::time::SystemTime::now()
2982        .duration_since(std::time::UNIX_EPOCH)
2983        .unwrap_or_default()
2984        .as_millis()
2985}
2986
2987#[cfg(test)]
2988mod tests {
2989    use crate::api::RedDBOptions;
2990    use crate::catalog::CollectionModel;
2991    use crate::runtime::RedDBRuntime;
2992
2993    fn rt() -> RedDBRuntime {
2994        RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("in-memory runtime")
2995    }
2996
2997    #[test]
2998    fn incr_missing_key_initialises_at_by() {
2999        let r = rt();
3000        let ops = super::KvAtomicOps::new(&r);
3001        let v = ops
3002            .incr(CollectionModel::Kv, "kv_default", "missing", 5, None)
3003            .unwrap();
3004        assert_eq!(v, 5);
3005    }
3006
3007    #[test]
3008    fn kv_runtime_stats_count_public_ops() {
3009        let r = rt();
3010        let ops = super::KvAtomicOps::new(&r);
3011
3012        ops.set(
3013            CollectionModel::Kv,
3014            "kv_default",
3015            "profile",
3016            crate::storage::schema::Value::text("alice"),
3017            None,
3018            false,
3019        )
3020        .unwrap();
3021        ops.get(CollectionModel::Kv, "kv_default", "profile")
3022            .unwrap();
3023        ops.delete(CollectionModel::Kv, "kv_default", "profile")
3024            .unwrap();
3025        ops.incr(CollectionModel::Kv, "kv_default", "hits", 1, None)
3026            .unwrap();
3027        ops.cas(
3028            CollectionModel::Kv,
3029            "kv_default",
3030            "profile",
3031            None,
3032            crate::storage::schema::Value::text("created"),
3033            None,
3034        )
3035        .unwrap();
3036        ops.cas(
3037            CollectionModel::Kv,
3038            "kv_default",
3039            "profile",
3040            Some(&crate::storage::schema::Value::text("different")),
3041            crate::storage::schema::Value::text("ignored"),
3042            None,
3043        )
3044        .unwrap();
3045
3046        let stats = r.stats().kv;
3047        assert_eq!(stats.puts, 1);
3048        assert_eq!(stats.gets, 1);
3049        assert_eq!(stats.deletes, 1);
3050        assert_eq!(stats.incrs, 1);
3051        assert_eq!(stats.cas_success, 1);
3052        assert_eq!(stats.cas_conflict, 1);
3053    }
3054
3055    #[test]
3056    fn kv_invalidate_tags_removes_matching_entries_only() {
3057        let r = rt();
3058
3059        r.execute_query("KV PUT sessions.blob = 'payload' TAGS [user:42, org:7]")
3060            .unwrap();
3061
3062        let miss = r
3063            .execute_query("INVALIDATE TAGS [org:99] FROM sessions")
3064            .unwrap();
3065        assert_eq!(miss.affected_rows, 0);
3066        assert!(matches!(
3067            r.execute_query("KV GET sessions.blob")
3068                .unwrap()
3069                .result
3070                .records[0]
3071                .get("value"),
3072            Some(crate::storage::schema::Value::Text(value)) if &**value == "payload"
3073        ));
3074
3075        let hit = r
3076            .execute_query("INVALIDATE TAGS [user:42] FROM sessions")
3077            .unwrap();
3078        assert_eq!(hit.affected_rows, 1);
3079        assert!(matches!(
3080            r.execute_query("KV GET sessions.blob")
3081                .unwrap()
3082                .result
3083                .records[0]
3084                .get("value"),
3085            Some(crate::storage::schema::Value::Null)
3086        ));
3087    }
3088
3089    #[test]
3090    fn kv_runtime_stats_count_watch_streams_and_events() {
3091        let r = rt();
3092        let ops = super::KvAtomicOps::new(&r);
3093        assert_eq!(r.stats().kv.watch_streams_active, 0);
3094
3095        {
3096            let mut stream = r.kv_watch_subscribe("kv_default", "watched", None);
3097            assert_eq!(r.stats().kv.watch_streams_active, 1);
3098
3099            ops.set(
3100                CollectionModel::Kv,
3101                "kv_default",
3102                "watched",
3103                crate::storage::schema::Value::Integer(1),
3104                None,
3105                false,
3106            )
3107            .unwrap();
3108            let event = stream.poll_next().expect("watch event");
3109            assert_eq!(event.key, "watched");
3110            assert_eq!(r.stats().kv.watch_events_emitted, 1);
3111
3112            stream.record_drop_count(3);
3113            assert_eq!(r.stats().kv.watch_drops, 3);
3114        }
3115
3116        assert_eq!(r.stats().kv.watch_streams_active, 0);
3117    }
3118
3119    #[test]
3120    fn incr_existing_integer_accumulates() {
3121        let r = rt();
3122        let ops = super::KvAtomicOps::new(&r);
3123        ops.incr(CollectionModel::Kv, "kv_default", "ctr", 1, None)
3124            .unwrap();
3125        ops.incr(CollectionModel::Kv, "kv_default", "ctr", 1, None)
3126            .unwrap();
3127        let v = ops
3128            .incr(CollectionModel::Kv, "kv_default", "ctr", 1, None)
3129            .unwrap();
3130        assert_eq!(v, 3);
3131    }
3132
3133    #[test]
3134    fn decr_via_negative_by() {
3135        let r = rt();
3136        let ops = super::KvAtomicOps::new(&r);
3137        ops.incr(CollectionModel::Kv, "kv_default", "stock", 10, None)
3138            .unwrap();
3139        let v = ops
3140            .incr(CollectionModel::Kv, "kv_default", "stock", -3, None)
3141            .unwrap();
3142        assert_eq!(v, 7);
3143    }
3144
3145    #[test]
3146    fn concurrent_incr_single_key_is_atomic() {
3147        const THREADS: usize = 8;
3148        const ITERS: usize = 1000;
3149
3150        let runtime = std::sync::Arc::new(rt());
3151        let barrier = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
3152        let mut handles = Vec::new();
3153
3154        for _ in 0..THREADS {
3155            let runtime = std::sync::Arc::clone(&runtime);
3156            let barrier = std::sync::Arc::clone(&barrier);
3157            handles.push(std::thread::spawn(move || {
3158                let ops = super::KvAtomicOps::new(&runtime);
3159                barrier.wait();
3160                for _ in 0..ITERS {
3161                    ops.incr(CollectionModel::Kv, "kv_default", "counter", 1, None)
3162                        .unwrap();
3163                }
3164            }));
3165        }
3166
3167        for handle in handles {
3168            handle.join().expect("worker should finish");
3169        }
3170
3171        let ops = super::KvAtomicOps::new(&runtime);
3172        assert_eq!(
3173            ops.get(CollectionModel::Kv, "kv_default", "counter")
3174                .unwrap(),
3175            Some(crate::storage::schema::Value::Integer(
3176                (THREADS * ITERS) as i64
3177            ))
3178        );
3179    }
3180
3181    #[test]
3182    fn incr_on_string_value_returns_error() {
3183        let r = rt();
3184        let ops = super::KvAtomicOps::new(&r);
3185        ops.set(
3186            CollectionModel::Kv,
3187            "kv_default",
3188            "name",
3189            crate::storage::schema::Value::text("alice"),
3190            None,
3191            false,
3192        )
3193        .unwrap();
3194        let err = ops
3195            .incr(CollectionModel::Kv, "kv_default", "name", 1, None)
3196            .unwrap_err();
3197        assert!(err.to_string().contains("non-integer"));
3198    }
3199
3200    // --- CAS tests ---
3201
3202    #[test]
3203    fn cas_matching_value_succeeds() {
3204        let r = rt();
3205        let ops = super::KvAtomicOps::new(&r);
3206        ops.set(
3207            CollectionModel::Kv,
3208            "kv_default",
3209            "lock",
3210            crate::storage::schema::Value::text("free"),
3211            None,
3212            false,
3213        )
3214        .unwrap();
3215        let (ok, prev) = ops
3216            .cas(
3217                CollectionModel::Kv,
3218                "kv_default",
3219                "lock",
3220                Some(&crate::storage::schema::Value::text("free")),
3221                crate::storage::schema::Value::text("held"),
3222                None,
3223            )
3224            .unwrap();
3225        assert!(ok);
3226        assert_eq!(prev, Some(crate::storage::schema::Value::text("free")));
3227        // Value actually changed.
3228        assert_eq!(
3229            ops.get(CollectionModel::Kv, "kv_default", "lock").unwrap(),
3230            Some(crate::storage::schema::Value::text("held"))
3231        );
3232    }
3233
3234    #[test]
3235    fn concurrent_cas_allows_one_success_per_round() {
3236        const THREADS: usize = 8;
3237        const ROUNDS: usize = 100;
3238
3239        let runtime = std::sync::Arc::new(rt());
3240        let ops = super::KvAtomicOps::new(&runtime);
3241        ops.set(
3242            CollectionModel::Kv,
3243            "kv_default",
3244            "cas_counter",
3245            crate::storage::schema::Value::Integer(0),
3246            None,
3247            false,
3248        )
3249        .unwrap();
3250
3251        let start_round = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
3252        let finish_round = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
3253        let successes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
3254        let mut handles = Vec::new();
3255
3256        for _ in 0..THREADS {
3257            let runtime = std::sync::Arc::clone(&runtime);
3258            let start_round = std::sync::Arc::clone(&start_round);
3259            let finish_round = std::sync::Arc::clone(&finish_round);
3260            let successes = std::sync::Arc::clone(&successes);
3261            handles.push(std::thread::spawn(move || {
3262                let ops = super::KvAtomicOps::new(&runtime);
3263                for round in 0..ROUNDS {
3264                    start_round.wait();
3265                    let (ok, _) = ops
3266                        .cas(
3267                            CollectionModel::Kv,
3268                            "kv_default",
3269                            "cas_counter",
3270                            Some(&crate::storage::schema::Value::Integer(round as i64)),
3271                            crate::storage::schema::Value::Integer((round + 1) as i64),
3272                            None,
3273                        )
3274                        .unwrap();
3275                    if ok {
3276                        successes.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
3277                    }
3278                    finish_round.wait();
3279                }
3280            }));
3281        }
3282
3283        for handle in handles {
3284            handle.join().expect("worker should finish");
3285        }
3286
3287        assert_eq!(successes.load(std::sync::atomic::Ordering::SeqCst), ROUNDS);
3288        assert_eq!(
3289            ops.get(CollectionModel::Kv, "kv_default", "cas_counter")
3290                .unwrap(),
3291            Some(crate::storage::schema::Value::Integer(ROUNDS as i64))
3292        );
3293    }
3294
3295    #[test]
3296    fn cas_mismatching_value_fails() {
3297        let r = rt();
3298        let ops = super::KvAtomicOps::new(&r);
3299        ops.set(
3300            CollectionModel::Kv,
3301            "kv_default",
3302            "lock",
3303            crate::storage::schema::Value::text("free"),
3304            None,
3305            false,
3306        )
3307        .unwrap();
3308        let (ok, current) = ops
3309            .cas(
3310                CollectionModel::Kv,
3311                "kv_default",
3312                "lock",
3313                Some(&crate::storage::schema::Value::text("held")),
3314                crate::storage::schema::Value::text("worker-7"),
3315                None,
3316            )
3317            .unwrap();
3318        assert!(!ok);
3319        assert_eq!(current, Some(crate::storage::schema::Value::text("free")));
3320        // Value unchanged.
3321        assert_eq!(
3322            ops.get(CollectionModel::Kv, "kv_default", "lock").unwrap(),
3323            Some(crate::storage::schema::Value::text("free"))
3324        );
3325    }
3326
3327    #[test]
3328    fn cas_expect_null_on_missing_key_creates() {
3329        let r = rt();
3330        let ops = super::KvAtomicOps::new(&r);
3331        let (ok, prev) = ops
3332            .cas(
3333                CollectionModel::Kv,
3334                "kv_default",
3335                "new_key",
3336                None,
3337                crate::storage::schema::Value::text("created"),
3338                None,
3339            )
3340            .unwrap();
3341        assert!(ok);
3342        assert_eq!(prev, None);
3343        assert_eq!(
3344            ops.get(CollectionModel::Kv, "kv_default", "new_key")
3345                .unwrap(),
3346            Some(crate::storage::schema::Value::text("created"))
3347        );
3348    }
3349
3350    #[test]
3351    fn cas_expect_null_on_existing_key_fails() {
3352        let r = rt();
3353        let ops = super::KvAtomicOps::new(&r);
3354        ops.set(
3355            CollectionModel::Kv,
3356            "kv_default",
3357            "taken",
3358            crate::storage::schema::Value::text("worker-1"),
3359            None,
3360            false,
3361        )
3362        .unwrap();
3363        let (ok, current) = ops
3364            .cas(
3365                CollectionModel::Kv,
3366                "kv_default",
3367                "taken",
3368                None,
3369                crate::storage::schema::Value::text("worker-2"),
3370                None,
3371            )
3372            .unwrap();
3373        assert!(!ok);
3374        assert_eq!(
3375            current,
3376            Some(crate::storage::schema::Value::text("worker-1"))
3377        );
3378    }
3379
3380    #[test]
3381    fn cas_via_sql_roundtrip() {
3382        let r = rt();
3383        // Seed value.
3384        r.execute_query("KV PUT lock = 'free'").unwrap();
3385        // CAS: free → held — should succeed.
3386        let res = r
3387            .execute_query("KV CAS lock EXPECT 'free' SET 'held'")
3388            .unwrap();
3389        let row = &res.result.records[0];
3390        assert_eq!(
3391            row.get("ok"),
3392            Some(&crate::storage::schema::Value::Boolean(true))
3393        );
3394        // CAS: free → held again — should fail (value is now 'held').
3395        let res2 = r
3396            .execute_query("KV CAS lock EXPECT 'free' SET 'held'")
3397            .unwrap();
3398        let row2 = &res2.result.records[0];
3399        assert_eq!(
3400            row2.get("ok"),
3401            Some(&crate::storage::schema::Value::Boolean(false))
3402        );
3403    }
3404
3405    #[test]
3406    fn cas_expect_null_via_sql() {
3407        let r = rt();
3408        let res = r
3409            .execute_query("KV CAS singleton EXPECT NULL SET 'first'")
3410            .unwrap();
3411        let row = &res.result.records[0];
3412        assert_eq!(
3413            row.get("ok"),
3414            Some(&crate::storage::schema::Value::Boolean(true))
3415        );
3416        // Second call must fail.
3417        let res2 = r
3418            .execute_query("KV CAS singleton EXPECT NULL SET 'second'")
3419            .unwrap();
3420        let row2 = &res2.result.records[0];
3421        assert_eq!(
3422            row2.get("ok"),
3423            Some(&crate::storage::schema::Value::Boolean(false))
3424        );
3425    }
3426
3427    #[test]
3428    fn incr_via_sql_roundtrip() {
3429        let r = rt();
3430        let res = r.execute_query("KV INCR hits").unwrap();
3431        let row = &res.result.records[0];
3432        assert_eq!(
3433            row.get("value"),
3434            Some(&crate::storage::schema::Value::Integer(1))
3435        );
3436        let res2 = r.execute_query("KV INCR hits BY 4").unwrap();
3437        let row2 = &res2.result.records[0];
3438        assert_eq!(
3439            row2.get("value"),
3440            Some(&crate::storage::schema::Value::Integer(5))
3441        );
3442    }
3443
3444    #[test]
3445    fn concurrent_self_referential_update_is_atomic() {
3446        const THREADS: usize = 8;
3447        const ITERS: usize = 100;
3448
3449        let runtime = std::sync::Arc::new(rt());
3450        runtime
3451            .execute_query("CREATE TABLE counters (id INT, n INT)")
3452            .unwrap();
3453        runtime
3454            .execute_query("INSERT INTO counters (id, n) VALUES (1, 0)")
3455            .unwrap();
3456
3457        let barrier = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
3458        let mut handles = Vec::new();
3459        for _ in 0..THREADS {
3460            let runtime = std::sync::Arc::clone(&runtime);
3461            let barrier = std::sync::Arc::clone(&barrier);
3462            handles.push(std::thread::spawn(move || {
3463                barrier.wait();
3464                for _ in 0..ITERS {
3465                    runtime
3466                        .execute_query("UPDATE counters SET n = n + 1 WHERE id = 1")
3467                        .unwrap();
3468                }
3469            }));
3470        }
3471
3472        for handle in handles {
3473            handle.join().expect("worker should finish");
3474        }
3475
3476        let selected = runtime
3477            .execute_query("SELECT n FROM counters WHERE id = 1")
3478            .unwrap();
3479        assert_eq!(
3480            selected.result.records[0].get("n"),
3481            Some(&crate::storage::schema::Value::Integer(
3482                (THREADS * ITERS) as i64
3483            ))
3484        );
3485    }
3486
3487    #[test]
3488    fn watch_stream_delivers_key_events_in_lsn_order() {
3489        let r = rt();
3490        let ops = super::KvAtomicOps::new(&r);
3491        let mut stream = r.kv_watch_subscribe("kv_default", "seq", None);
3492
3493        ops.set(
3494            CollectionModel::Kv,
3495            "kv_default",
3496            "seq",
3497            crate::storage::schema::Value::Integer(1),
3498            None,
3499            false,
3500        )
3501        .unwrap();
3502        ops.incr(CollectionModel::Kv, "kv_default", "seq", 1, None)
3503            .unwrap();
3504        ops.delete(CollectionModel::Kv, "kv_default", "seq")
3505            .unwrap();
3506        ops.set(
3507            CollectionModel::Kv,
3508            "kv_default",
3509            "seq",
3510            crate::storage::schema::Value::Integer(9),
3511            None,
3512            false,
3513        )
3514        .unwrap();
3515
3516        let mut events = Vec::new();
3517        while let Some(event) = stream.poll_next() {
3518            events.push(event);
3519            if events.len() == 4 {
3520                break;
3521            }
3522        }
3523
3524        assert_eq!(events.len(), 4);
3525        assert_eq!(
3526            events[0].op,
3527            crate::replication::cdc::ChangeOperation::Insert
3528        );
3529        assert_eq!(
3530            events[1].op,
3531            crate::replication::cdc::ChangeOperation::Update
3532        );
3533        assert_eq!(
3534            events[2].op,
3535            crate::replication::cdc::ChangeOperation::Delete
3536        );
3537        assert_eq!(
3538            events[3].op,
3539            crate::replication::cdc::ChangeOperation::Insert
3540        );
3541        assert!(events.windows(2).all(|pair| pair[0].lsn < pair[1].lsn));
3542    }
3543
3544    #[test]
3545    fn watch_prefix_stream_delivers_matching_events_only() {
3546        let r = rt();
3547        let ops = super::KvAtomicOps::new(&r);
3548        let mut stream = r.kv_watch_subscribe_prefix("kv_default", "acct:", None);
3549
3550        ops.set(
3551            CollectionModel::Kv,
3552            "kv_default",
3553            "acct:1",
3554            crate::storage::schema::Value::Integer(1),
3555            None,
3556            false,
3557        )
3558        .unwrap();
3559        ops.set(
3560            CollectionModel::Kv,
3561            "kv_default",
3562            "session:1",
3563            crate::storage::schema::Value::Integer(2),
3564            None,
3565            false,
3566        )
3567        .unwrap();
3568        ops.set(
3569            CollectionModel::Kv,
3570            "kv_default",
3571            "acct:2",
3572            crate::storage::schema::Value::Integer(3),
3573            None,
3574            false,
3575        )
3576        .unwrap();
3577
3578        let first = stream.poll_next().expect("first prefix event");
3579        let second = stream.poll_next().expect("second prefix event");
3580        assert_eq!(first.key, "acct:1");
3581        assert_eq!(second.key, "acct:2");
3582        assert!(stream.poll_next().is_none());
3583    }
3584
3585    #[test]
3586    fn watch_stream_resume_from_lsn_delivers_missed_events_without_duplicates() {
3587        let r = rt();
3588        let ops = super::KvAtomicOps::new(&r);
3589        let mut stream = r.kv_watch_subscribe("kv_default", "resume", None);
3590
3591        let mut last_seen_lsn = 0;
3592        for value in 0..5 {
3593            ops.set(
3594                CollectionModel::Kv,
3595                "kv_default",
3596                "resume",
3597                crate::storage::schema::Value::Integer(value),
3598                None,
3599                false,
3600            )
3601            .unwrap();
3602            last_seen_lsn = stream.poll_next().expect("initial event").lsn;
3603        }
3604        drop(stream);
3605
3606        for value in 5..55 {
3607            ops.set(
3608                CollectionModel::Kv,
3609                "kv_default",
3610                "resume",
3611                crate::storage::schema::Value::Integer(value),
3612                None,
3613                false,
3614            )
3615            .unwrap();
3616        }
3617
3618        let expected_lsns: Vec<u64> = r
3619            .kv_watch_events_since("kv_default", "resume", last_seen_lsn, 200)
3620            .into_iter()
3621            .map(|event| event.lsn)
3622            .collect();
3623        assert!(!expected_lsns.is_empty());
3624
3625        let mut resumed = r.kv_watch_subscribe("kv_default", "resume", Some(last_seen_lsn));
3626        let mut lsns = Vec::new();
3627        while let Some(event) = resumed.poll_next() {
3628            lsns.push(event.lsn);
3629            if lsns.len() == expected_lsns.len() {
3630                break;
3631            }
3632        }
3633
3634        assert_eq!(lsns, expected_lsns);
3635        assert!(lsns.iter().all(|lsn| *lsn > last_seen_lsn));
3636        assert!(lsns.windows(2).all(|pair| pair[0] < pair[1]));
3637        assert!(resumed.poll_next().is_none());
3638    }
3639
3640    #[test]
3641    fn watch_stream_slow_consumer_drops_oldest_buffered_events() {
3642        let r = rt();
3643        let ops = super::KvAtomicOps::new(&r);
3644        let mut stream = r.kv_watch_subscribe("kv_default", "slow", None);
3645
3646        for value in 0..1_200 {
3647            ops.set(
3648                CollectionModel::Kv,
3649                "kv_default",
3650                "slow",
3651                crate::storage::schema::Value::Integer(value),
3652                None,
3653                false,
3654            )
3655            .unwrap();
3656        }
3657
3658        let event = stream.poll_next().expect("tail event after drops");
3659        assert!(event.lsn > 1);
3660        assert!(event.dropped_event_count > 0);
3661        assert_eq!(stream.dropped_event_count(), event.dropped_event_count);
3662        assert_eq!(r.stats().kv.watch_drops, event.dropped_event_count);
3663    }
3664
3665    #[test]
3666    fn watch_stream_idle_timeout_closes_subscription() {
3667        let r = rt();
3668        r.execute_query("SET CONFIG red.config.kv.watch.idle_timeout_ms = 1")
3669            .unwrap();
3670
3671        let mut stream = r.kv_watch_subscribe("kv_default", "idle", None);
3672        assert_eq!(r.stats().kv.watch_streams_active, 1);
3673        std::thread::sleep(std::time::Duration::from_millis(5));
3674
3675        assert!(stream.poll_next().is_none());
3676        assert_eq!(r.stats().kv.watch_streams_active, 0);
3677    }
3678
3679    #[test]
3680    fn watch_stream_does_not_emit_rolled_back_put() {
3681        let r = rt();
3682        let mut stream = r.kv_watch_subscribe("kv_default", "rollback_key", None);
3683
3684        r.execute_query("BEGIN").unwrap();
3685        r.execute_query("KV PUT rollback_key = 'dirty'").unwrap();
3686        r.execute_query("ROLLBACK").unwrap();
3687
3688        assert!(stream.poll_next().is_none());
3689    }
3690}