Skip to main content

reddb_server/runtime/
impl_kv.rs

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