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