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