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