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