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