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