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