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