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