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