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
998 .runtime
999 .config_bool("red.config.kv.default_collection", true);
1000 if !auto_create {
1001 return Err(RedDBError::NotFound(format!(
1002 "kv collection '{collection}' does not exist and auto-create is disabled \
1003 (red.config.kv.default_collection = false)"
1004 )));
1005 }
1006 store
1007 .create_collection(collection)
1008 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1009 self.runtime
1010 .inner
1011 .db
1012 .save_collection_contract(kv_collection_contract(collection))
1013 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1014 Ok(())
1015 }
1016
1017 fn get_entry(
1018 &self,
1019 model: crate::catalog::CollectionModel,
1020 collection: &str,
1021 key: &str,
1022 ) -> RedDBResult<Option<(reddb_types::Value, crate::storage::EntityId)>> {
1023 let resolver =
1024 super::kv_read::KvReadResolver::for_keyed_model(self.runtime, model, collection)?;
1025 Ok(resolver
1026 .record(key)?
1027 .and_then(|record| record.value.map(|value| (value, record.id))))
1028 }
1029
1030 fn is_versioned_collection(
1037 &self,
1038 model: crate::catalog::CollectionModel,
1039 collection: &str,
1040 ) -> RedDBResult<bool> {
1041 super::kv_read::is_versioned(self.runtime, model, collection)
1044 }
1045
1046 fn get_entity(
1047 &self,
1048 model: crate::catalog::CollectionModel,
1049 collection: &str,
1050 key: &str,
1051 ) -> RedDBResult<Option<crate::storage::UnifiedEntity>> {
1052 super::kv_read::KvReadResolver::for_keyed_model(self.runtime, model, collection)?
1053 .owned_entity(key)
1054 }
1055
1056 fn latest_kv_entries(
1057 &self,
1058 collection: &str,
1059 prefix: Option<&str>,
1060 entries_max: usize,
1061 ) -> RedDBResult<Vec<(String, crate::storage::UnifiedEntity)>> {
1062 self.ensure_declared_model(crate::catalog::CollectionModel::Kv, collection)?;
1063 let store = self.runtime.inner.db.store();
1064 let Some(manager) = store.get_collection(collection) else {
1065 return Ok(Vec::new());
1066 };
1067 let _publication = manager
1068 .has_backing()
1069 .then(|| store.vault_publication_lock.lock());
1070 let versioned =
1075 self.is_versioned_collection(crate::catalog::CollectionModel::Kv, collection)?;
1076 let snapshot = crate::runtime::impl_core::capture_current_snapshot();
1077 let mut entries: std::collections::BTreeMap<String, crate::storage::UnifiedEntity> =
1078 std::collections::BTreeMap::new();
1079 super::function_budget::try_scan(
1080 |visit| {
1081 if versioned {
1082 manager.try_scan_for_each(snapshot.as_ref(), usize::MAX, visit)
1083 } else {
1084 manager.try_for_each_entity(usize::MAX, visit)
1085 }
1086 },
1087 |entity| {
1088 let Some(reddb_types::Value::Text(key)) =
1089 entity.data.as_row().and_then(|row| row.get_field("key"))
1090 else {
1091 return true;
1092 };
1093 if prefix.is_some_and(|prefix| !key.starts_with(prefix)) {
1094 return true;
1095 }
1096 if entries_max == 0
1099 || (entries.len() >= entries_max
1100 && entries
1101 .last_key_value()
1102 .is_some_and(|(last, _)| key.as_ref() > last.as_str()))
1103 {
1104 return true;
1105 }
1106 let should_replace = match entries.get(key.as_ref()) {
1107 Some(existing) if versioned => entity.xmin >= existing.xmin,
1108 Some(existing) => entity.id.raw() >= existing.id.raw(),
1109 None => true,
1110 };
1111 if should_replace {
1112 entries.insert(key.to_string(), entity.clone());
1113 if entries.len() > entries_max {
1114 entries.pop_last();
1115 }
1116 }
1117 true
1118 },
1119 )?;
1120 Ok(entries.into_iter().collect())
1121 }
1122
1123 fn get_vault_entry(&self, collection: &str, key: &str) -> RedDBResult<Option<VaultEntry>> {
1124 self.record_vault_read(collection, Some(key));
1125 let mut pending = None;
1126 self.visit_pending_vault_entries(collection, Some(key), |entry| pending = Some(entry));
1127 if let Some(entry) = pending {
1128 return Ok(Some(entry));
1129 }
1130 let mut latest: Option<VaultEntry> = None;
1131 self.visit_vault_versions(collection, key, |entry| {
1132 super::keyed_spine::select_latest_version(&mut latest, entry);
1133 })?;
1134 Ok(latest)
1135 }
1136
1137 fn get_vault_entry_version(
1138 &self,
1139 collection: &str,
1140 key: &str,
1141 version: i64,
1142 ) -> RedDBResult<Option<VaultEntry>> {
1143 let mut selected = None;
1144 self.visit_vault_versions(collection, key, |entry| {
1145 if entry.version == version {
1146 selected = Some(entry);
1147 }
1148 })?;
1149 Ok(selected)
1150 }
1151
1152 fn vault_versions(&self, collection: &str, key: &str) -> RedDBResult<Vec<VaultEntry>> {
1153 let mut versions = Vec::new();
1154 self.visit_vault_versions(collection, key, |entry| versions.push(entry))?;
1155 Ok(versions)
1156 }
1157
1158 fn visit_vault_versions(
1161 &self,
1162 collection: &str,
1163 key: &str,
1164 mut visit: impl FnMut(VaultEntry),
1165 ) -> RedDBResult<()> {
1166 self.record_vault_read(collection, Some(key));
1167 self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
1168 let store = self.runtime.inner.db.store();
1169 let manager = store.get_collection(collection);
1170 let snapshot = self.runtime.vault_read_snapshot();
1171 let _publication = manager
1172 .as_ref()
1173 .filter(|manager| manager.has_backing())
1174 .map(|_| store.vault_publication_lock.lock());
1175 let records = manager.as_ref().map(|manager| {
1176 super::keyed_spine::records_with_metadata(manager, Some(snapshot), Some(key), |entity| {
1177 entity.data.as_row().is_some_and(|row| self.vault_row_in_scope(row)
1178 && matches!(row.get_field("key"), Some(Value::Text(stored)) if stored.as_ref() == key))
1179 })
1180 }).transpose()?.into_iter().flatten();
1181 let mut found = false;
1182 for record in records {
1183 let (entity, metadata) = record?;
1184 let crate::storage::EntityData::Row(row) = entity.data else {
1185 continue;
1186 };
1187 if !self.vault_row_in_scope(&row) {
1188 continue;
1189 }
1190 let Some(version) =
1191 super::keyed_spine::row_version_owned(entity.id, row, entity.sequence_id as i64)
1192 else {
1193 continue;
1194 };
1195 if version.key != key {
1196 continue;
1197 }
1198 let metadata = metadata.unwrap_or_default();
1199 found = true;
1200 visit(VaultEntry::from_keyed_row(
1201 version,
1202 metadata,
1203 entity.created_at,
1204 entity.updated_at,
1205 entity.sequence_id,
1206 self.tenant.clone(),
1207 ));
1208 }
1209 if !found {
1210 if let Some(entry) = self.legacy_vault_entry(collection, key)? {
1211 visit(entry);
1212 }
1213 }
1214 self.visit_pending_vault_entries(collection, Some(key), visit);
1215 Ok(())
1216 }
1217
1218 fn latest_vault_entries(
1219 &self,
1220 collection: &str,
1221 prefix: Option<&str>,
1222 ) -> RedDBResult<Vec<VaultEntry>> {
1223 self.record_vault_read(collection, None);
1225 self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
1226 let store = self.runtime.inner.db.store();
1227 let manager = store.get_collection(collection);
1228 let snapshot = self.runtime.vault_read_snapshot();
1229 let _publication = manager
1230 .as_ref()
1231 .filter(|manager| manager.has_backing())
1232 .map(|_| store.vault_publication_lock.lock());
1233 let records = manager
1234 .as_ref()
1235 .map(|manager| {
1236 super::keyed_spine::records_with_metadata(manager, Some(snapshot), None, |entity| {
1237 entity
1238 .data
1239 .as_row()
1240 .is_some_and(|row| self.vault_row_in_scope(row)
1241 && prefix.is_none_or(|prefix| matches!(row.get_field("key"), Some(Value::Text(key)) if key.starts_with(prefix))))
1242 })
1243 })
1244 .transpose()?
1245 .into_iter()
1246 .flatten();
1247 let mut latest = std::collections::BTreeMap::new();
1248 for record in records {
1249 let (entity, metadata) = record?;
1250 let crate::storage::EntityData::Row(row) = entity.data else {
1251 continue;
1252 };
1253 if !self.vault_row_in_scope(&row) {
1254 continue;
1255 }
1256 let Some(version) =
1257 super::keyed_spine::row_version_owned(entity.id, row, entity.sequence_id as i64)
1258 else {
1259 continue;
1260 };
1261 let metadata = metadata.unwrap_or_default();
1262 let entry = VaultEntry::from_keyed_row(
1263 version,
1264 metadata,
1265 entity.created_at,
1266 entity.updated_at,
1267 entity.sequence_id,
1268 self.tenant.clone(),
1269 );
1270 super::keyed_spine::retain_latest_version(&mut latest, entry);
1271 }
1272 if collection == "red.vault" && self.tenant.is_none() {
1273 if let Some(auth) = self.runtime.inner.auth_store.read().clone() {
1274 for key in auth.vault_kv_keys() {
1275 if prefix.is_some_and(|prefix| !key.starts_with(prefix)) {
1276 continue;
1277 }
1278 if !latest.contains_key(&key) {
1279 if let Some(entry) = self.legacy_vault_entry(collection, &key)? {
1280 super::keyed_spine::retain_latest_version(&mut latest, entry);
1281 }
1282 }
1283 }
1284 }
1285 }
1286 let mut pending_keys = std::collections::HashSet::new();
1287 self.visit_pending_vault_entries(collection, None, |entry| {
1288 if prefix.is_some_and(|prefix| !entry.key.starts_with(prefix)) {
1289 return;
1290 }
1291 if pending_keys.insert(entry.key.clone()) {
1294 latest.remove(&entry.key);
1295 }
1296 super::keyed_spine::retain_latest_version(&mut latest, entry);
1297 });
1298 Ok(latest
1299 .into_values()
1300 .filter(|entry| prefix.is_none_or(|prefix| entry.key.starts_with(prefix)))
1301 .collect())
1302 }
1303
1304 fn legacy_vault_entry(&self, collection: &str, key: &str) -> RedDBResult<Option<VaultEntry>> {
1305 if collection != "red.vault" || self.tenant.is_some() || !show_secrets_allows_key(key) {
1306 return Ok(None);
1307 }
1308 let Some(value) = self.runtime.vault_kv_get(key) else {
1309 return Ok(None);
1310 };
1311 self.runtime.provision_default_vault_key()?;
1312 Ok(Some(VaultEntry {
1313 id: crate::storage::EntityId::new(0),
1314 key: key.into(),
1315 value: self.runtime.seal_vault_value_for_tenant(
1316 collection,
1317 Value::text(value),
1318 None,
1319 )?,
1320 metadata: Metadata::default(),
1321 created_at: 0,
1322 updated_at: 0,
1323 sequence_id: 0,
1324 version: 0,
1325 tombstone: false,
1326 op: "legacy".into(),
1327 tenant: None,
1328 }))
1329 }
1330
1331 fn append_vault_version(
1332 &self,
1333 collection: &str,
1334 key: &str,
1335 value: reddb_types::Value,
1336 op: &str,
1337 tombstone: bool,
1338 tags: &[String],
1339 ) -> RedDBResult<VaultEntry> {
1340 let lock_key = format!("vault/{}/{}", self.tenant.as_deref().unwrap_or(""), key);
1341 let lock = self.runtime.inner.rmw_locks.lock_for(collection, &lock_key);
1342 let _guard = lock.lock();
1343 let vault_store = self.runtime.inner.db.store();
1344 let _vault_guard = vault_store.vault_publication_lock.lock();
1345 let _snapshot_guard = super::execution_context::CurrentSnapshotGuard::install(
1346 self.runtime.vault_write_snapshot(),
1347 );
1348 self.append_vault_version_unlocked(collection, key, value, op, tombstone, tags)
1349 }
1350
1351 fn append_vault_version_unlocked(
1352 &self,
1353 collection: &str,
1354 key: &str,
1355 value: reddb_types::Value,
1356 op: &str,
1357 tombstone: bool,
1358 tags: &[String],
1359 ) -> RedDBResult<VaultEntry> {
1360 self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
1361 let previous = self.get_vault_entry(collection, key)?;
1362 let version = previous
1363 .as_ref()
1364 .map(|entry| entry.version)
1365 .unwrap_or(0)
1366 .checked_add(1)
1367 .ok_or_else(|| RedDBError::Query("vault version overflow".into()))?;
1368 let stored_value = if tombstone {
1369 reddb_types::Value::Null
1370 } else {
1371 self.runtime
1372 .seal_vault_value_for_tenant(collection, value, self.tenant.as_deref())?
1373 };
1374 let now = current_unix_ms() as i64;
1375 let mut fields = vec![
1376 ("key".to_string(), reddb_types::Value::text(key.to_string())),
1377 ("value".to_string(), stored_value),
1378 ("version".to_string(), reddb_types::Value::Integer(version)),
1379 (
1380 "tombstone".to_string(),
1381 reddb_types::Value::Boolean(tombstone),
1382 ),
1383 ("op".to_string(), reddb_types::Value::text(op.to_string())),
1384 (
1385 "created_at_ms".to_string(),
1386 reddb_types::Value::Integer(now),
1387 ),
1388 ];
1389 if let Some(tenant) = &self.tenant {
1390 fields.push(("_vault_tenant".into(), Value::text(tenant.clone())));
1391 }
1392 let mut row = crate::storage::RowData::new(Vec::new());
1393 row.named = Some(fields.into_iter().collect());
1394 let entity = crate::storage::UnifiedEntity::new(
1395 self.runtime.inner.db.store().next_entity_id(),
1396 crate::storage::EntityKind::TableRow {
1397 table: std::sync::Arc::from(collection),
1398 row_id: 0,
1399 },
1400 crate::storage::EntityData::Row(row),
1401 );
1402 self.stage_or_store_vault_version(
1403 collection,
1404 key,
1405 entity,
1406 Metadata::with_fields(vault_tags_metadata(tags)),
1407 previous.as_ref().map(|entry| entry.id),
1408 )
1409 }
1410
1411 fn purge_vault_versions(&self, collection: &str, key: &str) -> RedDBResult<usize> {
1412 self.runtime.reject_vault_transaction()?;
1413 let lock_key = format!("vault/{}/{}", self.tenant.as_deref().unwrap_or(""), key);
1414 let lock = self.runtime.inner.rmw_locks.lock_for(collection, &lock_key);
1415 let _guard = lock.lock();
1416 let vault_store = self.runtime.inner.db.store();
1417 let _vault_guard = vault_store.vault_publication_lock.lock();
1418 let _snapshot_guard = super::execution_context::CurrentSnapshotGuard::install(
1419 self.runtime.vault_write_snapshot(),
1420 );
1421 self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
1422 let versions = self.vault_versions(collection, key)?;
1423 let store = self.runtime.inner.db.store();
1424 let mut purged = 0usize;
1425 for entry in versions {
1426 if store
1427 .delete(collection, entry.id)
1428 .map_err(|err| RedDBError::Internal(err.to_string()))?
1429 {
1430 store.context_index().remove_entity(entry.id);
1431 purged += 1;
1432 }
1433 }
1434 if collection == "red.vault" && self.tenant.is_none() && show_secrets_allows_key(key) {
1435 if let Some(auth) = self.runtime.inner.auth_store.read().clone() {
1436 if auth
1437 .vault_kv_try_delete(key)
1438 .map_err(|err| RedDBError::Query(err.to_string()))?
1439 {
1440 purged += 1;
1441 }
1442 }
1443 }
1444 Ok(purged)
1445 }
1446
1447 fn ensure_declared_model(
1448 &self,
1449 model: crate::catalog::CollectionModel,
1450 collection: &str,
1451 ) -> RedDBResult<()> {
1452 super::kv_read::KvReadResolver::for_keyed_model(self.runtime, model, collection).map(|_| ())
1453 }
1454}
1455
1456impl RedDBRuntime {
1457 pub fn export_vault_collection_records(&self, collection: &str) -> RedDBResult<Vec<String>> {
1460 let vault_store = self.inner.db.store();
1461 let _vault_guard = vault_store.vault_publication_lock.lock();
1462 let store = self.inner.db.store();
1463 let contract = self
1464 .inner
1465 .db
1466 .collection_contract(collection)
1467 .ok_or_else(|| RedDBError::NotFound(collection.into()))?;
1468 if contract.declared_model != CollectionModel::Vault {
1469 return Err(RedDBError::Query(
1470 "native vault export requires a vault collection".into(),
1471 ));
1472 }
1473 let manager = store
1474 .get_collection(collection)
1475 .ok_or_else(|| RedDBError::NotFound(collection.into()))?;
1476 let mut records = Vec::new();
1477 for record in super::keyed_spine::records_with_metadata(&manager, None, None, |_| true)? {
1478 let (entity, metadata) = record?;
1479 records.push((
1480 entity.id,
1481 hex::encode(crate::storage::UnifiedStore::serialize_entity_record(
1482 &entity,
1483 metadata.as_ref(),
1484 store.format_version(),
1485 )),
1486 ));
1487 }
1488 records.sort_unstable_by_key(|(id, _)| id.raw());
1489 Ok(records.into_iter().map(|(_, encoded)| encoded).collect())
1490 }
1491
1492 pub fn import_vault_collection_records(
1495 &self,
1496 collection: &str,
1497 records: &[String],
1498 format_version: u32,
1499 ) -> RedDBResult<usize> {
1500 self.reject_vault_transaction()?;
1501 let vault_store = self.inner.db.store();
1502 let _vault_guard = vault_store.vault_publication_lock.lock();
1503 let store = self.inner.db.store();
1504 if collection.is_empty() || (collection.starts_with("red.") && collection != "red.vault") {
1505 return Err(RedDBError::Query("invalid vault import collection".into()));
1506 }
1507 if let Some(contract) = self.inner.db.collection_contract(collection) {
1508 if contract.declared_model != CollectionModel::Vault {
1509 return Err(RedDBError::Query(
1510 "cannot restore vault records into another collection model".into(),
1511 ));
1512 }
1513 }
1514 if store
1515 .get_collection(collection)
1516 .is_some_and(|manager| manager.count() != 0)
1517 {
1518 return Err(RedDBError::Query(
1519 "vault restore requires an empty collection".into(),
1520 ));
1521 }
1522 let auth = self.inner.auth_store.read().clone().ok_or_else(|| {
1523 RedDBError::Query("vault restore requires imported vault keys".into())
1524 })?;
1525 if !auth.is_vault_backed() || auth.vault_secret_key().is_none() {
1526 return Err(RedDBError::Query(
1527 "vault restore requires imported vault keys".into(),
1528 ));
1529 }
1530 if format_version > store.format_version() {
1531 return Err(RedDBError::Query(
1532 "vault dump uses a newer storage format".into(),
1533 ));
1534 }
1535 let mut decoded = Vec::with_capacity(records.len());
1536 for record in records {
1537 let bytes = hex::decode(record)
1538 .map_err(|_| RedDBError::Query("invalid native vault dump record".into()))?;
1539 let (mut entity, metadata) =
1540 crate::storage::UnifiedStore::deserialize_vault_entity_record(
1541 &bytes,
1542 format_version,
1543 )
1544 .map_err(|error| RedDBError::Query(error.to_string()))?;
1545 let crate::storage::EntityData::Row(row) = &entity.data else {
1546 return Err(RedDBError::Query("vault dump requires rows".into()));
1547 };
1548 let Some(version) =
1549 super::keyed_spine::row_version(entity.id, row, entity.sequence_id as i64)
1550 else {
1551 return Err(RedDBError::Query("invalid vault version record".into()));
1552 };
1553 if !version.tombstone {
1554 let tenant = row
1555 .get_field("_vault_tenant")
1556 .and_then(|value| match value {
1557 Value::Text(tenant) => Some(tenant.as_ref()),
1558 _ => None,
1559 });
1560 self.unseal_vault_value_for_tenant(collection, &version.value, tenant)?;
1561 }
1562 entity.id = crate::storage::EntityId::new(0);
1563 entity.set_xmin(0);
1566 entity.set_xmax(0);
1567 entity.kind = crate::storage::EntityKind::TableRow {
1568 table: std::sync::Arc::from(collection),
1569 row_id: 0,
1570 };
1571 decoded.push((entity, metadata));
1572 }
1573 if store.get_collection(collection).is_none() {
1574 store
1575 .create_collection(collection)
1576 .map_err(|error| RedDBError::Internal(error.to_string()))?;
1577 }
1578 let mut contract = kv_collection_contract(collection);
1579 contract.declared_model = CollectionModel::Vault;
1580 self.inner
1581 .db
1582 .save_collection_contract(contract)
1583 .map_err(|error| RedDBError::Internal(error.to_string()))?;
1584 self.inner
1585 .db
1586 .persist_metadata()
1587 .map_err(|error| RedDBError::Internal(error.to_string()))?;
1588 let count = decoded.len();
1589 for (entity, metadata) in decoded {
1590 let id = store
1591 .insert(collection, entity)
1592 .map_err(|error| RedDBError::Internal(error.to_string()))?;
1593 if let Some(metadata) = metadata {
1594 store
1595 .set_metadata(collection, id, metadata)
1596 .map_err(|error| RedDBError::Internal(error.to_string()))?;
1597 }
1598 }
1599 self.invalidate_result_cache();
1600 Ok(count)
1601 }
1602
1603 pub(crate) fn reject_vault_transaction(&self) -> RedDBResult<()> {
1604 if self
1605 .inner
1606 .transaction_state
1607 .in_transaction(current_connection_id())
1608 {
1609 return Err(RedDBError::Query(
1610 "vault administration cannot run inside a transaction; execute after COMMIT or ROLLBACK"
1611 .into(),
1612 ));
1613 }
1614 Ok(())
1615 }
1616
1617 fn ensure_default_vault(&self) -> RedDBResult<()> {
1618 self.provision_default_vault_key()?;
1619 let lock = self.inner.rmw_locks.lock_for("red.vault", "initialize");
1620 let _guard = lock.lock();
1621 let store = self.inner.db.store();
1622 if store.get_collection("red.vault").is_none() {
1623 store
1624 .create_collection("red.vault")
1625 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1626 let mut contract = kv_collection_contract("red.vault");
1627 contract.declared_model = CollectionModel::Vault;
1628 self.inner
1629 .db
1630 .save_collection_contract(contract)
1631 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1632 self.inner
1633 .db
1634 .persist_metadata()
1635 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1636 }
1637 Ok(())
1638 }
1639
1640 fn provision_default_vault_key(&self) -> RedDBResult<()> {
1641 let auth = self.inner.auth_store.read().clone().ok_or_else(|| {
1642 RedDBError::Query("secret storage requires an enabled, unsealed vault".into())
1643 })?;
1644 if !auth.is_vault_backed() {
1645 return Err(RedDBError::Query(
1646 "secret storage requires an enabled, unsealed vault".into(),
1647 ));
1648 }
1649 auth.ensure_vault_secret_key_result()
1650 .map_err(|err| RedDBError::Query(err.to_string()))?;
1651 Ok(())
1652 }
1653
1654 pub(crate) fn set_user_secret(&self, key: &str, value: Option<String>) -> RedDBResult<()> {
1655 self.check_vault_capability("vault:write", "red.vault", key)
1656 .map_err(RedDBError::Query)?;
1657 self.ensure_default_vault()?;
1658 let command = match value {
1659 Some(value) => reddb_rql::ast::KvCommand::Put {
1660 model: CollectionModel::Vault,
1661 collection: "red.vault".into(),
1662 key: key.into(),
1663 value: Value::text(value),
1664 ttl_ms: None,
1665 tags: Vec::new(),
1666 if_not_exists: false,
1667 },
1668 None => reddb_rql::ast::KvCommand::Delete {
1669 model: CollectionModel::Vault,
1670 collection: "red.vault".into(),
1671 key: key.into(),
1672 },
1673 };
1674 self.execute_kv_command("", &command)?;
1675 self.invalidate_result_cache();
1676 Ok(())
1677 }
1678
1679 pub(crate) fn query_secret_value(&self, path: &str) -> RedDBResult<Option<Value>> {
1682 let (collection, key) = path.split_once('/').unwrap_or(("red.vault", path));
1683 let (collection, key, owner_tenant) = if collection == "platform" {
1684 let (collection, key) = key.split_once('.').ok_or_else(|| {
1685 RedDBError::Query("platform secret expects platform.<vault>.<path>".into())
1686 })?;
1687 (
1688 if collection == "default" {
1689 "red.vault"
1690 } else {
1691 collection
1692 },
1693 key,
1694 None,
1695 )
1696 } else {
1697 (collection, key, current_tenant())
1698 };
1699 if collection == "red.vault" && !show_secrets_allows_key(key) {
1700 return Ok(None);
1701 }
1702 let ops = KvAtomicOps::for_tenant(self, owner_tenant.clone());
1703 if let Err(reason) = self.check_vault_capability_in_tenant(
1704 "vault:use",
1705 collection,
1706 key,
1707 owner_tenant.as_deref(),
1708 ) {
1709 self.audit_vault_lifecycle(
1710 "use",
1711 collection,
1712 key,
1713 crate::runtime::audit_log::Outcome::Denied,
1714 &reason,
1715 None,
1716 );
1717 self.emit_vault_control_event(
1718 crate::runtime::control_events::EventKind::VaultUse,
1719 crate::runtime::control_events::Outcome::Denied,
1720 "vault:use",
1721 collection,
1722 key,
1723 &reason,
1724 None,
1725 Vec::new(),
1726 )?;
1727 return Ok(None);
1730 }
1731 let entry = ops.get_vault_entry(collection, key)?;
1732 let value = match entry.as_ref() {
1733 Some(entry) if entry.tombstone => None,
1734 Some(_)
1738 if !self
1739 .inner
1740 .auth_store
1741 .read()
1742 .as_ref()
1743 .is_some_and(|auth| auth.is_vault_backed()) =>
1744 {
1745 None
1746 }
1747 Some(entry) => Some(self.unseal_vault_value_for_tenant(
1748 collection,
1749 &entry.value,
1750 owner_tenant.as_deref(),
1751 )?),
1752 None if collection == "red.vault" && owner_tenant.is_none() => {
1753 self.vault_kv_get(key).map(Value::text)
1754 }
1755 None => None,
1756 };
1757 self.audit_vault_lifecycle(
1758 "use",
1759 collection,
1760 key,
1761 crate::runtime::audit_log::Outcome::Success,
1762 "ok",
1763 entry.as_ref(),
1764 );
1765 self.emit_vault_control_event(
1766 crate::runtime::control_events::EventKind::VaultUse,
1767 crate::runtime::control_events::Outcome::Allowed,
1768 "vault:use",
1769 collection,
1770 key,
1771 "ok",
1772 entry.as_ref(),
1773 Vec::new(),
1774 )?;
1775 Ok(value)
1776 }
1777
1778 pub(crate) fn show_user_secrets(&self, prefix: Option<&str>) -> RedDBResult<UnifiedResult> {
1779 self.provision_default_vault_key()?;
1780 let ops = KvAtomicOps::new(self);
1781 let mut result = UnifiedResult::with_columns(vec![
1782 "collection".into(),
1783 "key".into(),
1784 "value".into(),
1785 "status".into(),
1786 ]);
1787 let mut default_keys = std::collections::HashSet::new();
1788 for collection in self.inner.db.store().list_collections() {
1789 let model = self
1790 .inner
1791 .db
1792 .collection_contract(&collection)
1793 .map(|contract| contract.declared_model);
1794 if model != Some(CollectionModel::Vault) {
1795 continue;
1796 }
1797 for entry in ops.latest_vault_entries(&collection, None)? {
1798 if collection == "red.vault" {
1799 default_keys.insert(entry.key.clone());
1800 }
1801 let display_key = if collection == "red.vault" {
1802 entry.key.clone()
1803 } else {
1804 format!("{collection}.{}", entry.key)
1805 };
1806 if entry.tombstone
1807 || !show_secrets_allows_key(&entry.key)
1808 || prefix.is_some_and(|prefix| !display_key.starts_with(prefix))
1809 || self
1810 .check_vault_capability("vault:read_metadata", &collection, &entry.key)
1811 .is_err()
1812 {
1813 continue;
1814 }
1815 let mut record = UnifiedRecord::new();
1816 record.set("collection", Value::text(collection.clone()));
1817 record.set("key", Value::text(display_key));
1818 record.set("value", Value::text("***"));
1819 record.set("status", Value::text("active"));
1820 result.push(record);
1821 }
1822 }
1823 if current_tenant().is_none() {
1824 if let Some(auth) = self.inner.auth_store.read().clone() {
1825 for key in auth.vault_kv_keys() {
1826 if default_keys.contains(&key)
1827 || !show_secrets_allows_key(&key)
1828 || prefix.is_some_and(|prefix| !key.starts_with(prefix))
1829 || self
1830 .check_vault_capability("vault:read_metadata", "red.vault", &key)
1831 .is_err()
1832 {
1833 continue;
1834 }
1835 let mut record = UnifiedRecord::new();
1836 record.set("collection", Value::text("red.vault"));
1837 record.set("key", Value::text(key));
1838 record.set("value", Value::text("***"));
1839 record.set("status", Value::text("active"));
1840 result.push(record);
1841 }
1842 }
1843 }
1844 result
1845 .records
1846 .sort_by_key(|record| record.get("key").map(Value::display_string));
1847 Ok(result)
1848 }
1849 pub(crate) fn seal_vault_value(
1850 &self,
1851 collection: &str,
1852 value: reddb_types::Value,
1853 ) -> RedDBResult<reddb_types::Value> {
1854 self.seal_vault_value_for_tenant(collection, value, current_tenant().as_deref())
1855 }
1856
1857 fn seal_vault_value_for_tenant(
1858 &self,
1859 collection: &str,
1860 value: Value,
1861 tenant: Option<&str>,
1862 ) -> RedDBResult<Value> {
1863 let key = self.vault_encryption_key(collection)?;
1864 let plaintext = zeroize::Zeroizing::new(value.to_bytes());
1865 let nonce_bytes = crate::auth::store::random_bytes(12);
1866 let mut nonce = [0u8; 12];
1867 nonce.copy_from_slice(&nonce_bytes[..12]);
1868 let aad = vault_value_aad(collection, tenant);
1869 let ciphertext =
1870 crate::crypto::aes_gcm::aes256_gcm_encrypt(&key, &nonce, aad.as_bytes(), &plaintext);
1871 let mut payload = Vec::with_capacity(12 + ciphertext.len());
1872 payload.extend_from_slice(&nonce);
1873 payload.extend_from_slice(&ciphertext);
1874 Ok(reddb_types::Value::Secret(payload))
1875 }
1876
1877 fn vault_key_available(&self, collection: &str) -> bool {
1878 self.vault_encryption_key(collection).is_ok()
1879 }
1880
1881 fn vault_encryption_key(&self, collection: &str) -> RedDBResult<[u8; 32]> {
1882 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1883 RedDBError::Query("vault sealed_unavailable: no key provider is configured".to_string())
1884 })?;
1885 if !auth_store.is_vault_backed() {
1886 return Err(RedDBError::Query(
1887 "vault sealed_unavailable: key provider is sealed".to_string(),
1888 ));
1889 }
1890
1891 if let Some(hex_key) = auth_store.vault_kv_get(&vault_master_key_ref(collection)) {
1892 return decode_vault_key(&hex_key);
1893 }
1894 auth_store.vault_secret_key().ok_or_else(|| {
1895 RedDBError::Query("vault sealed_unavailable: cluster vault key is missing".to_string())
1896 })
1897 }
1898
1899 fn unseal_vault_value(
1900 &self,
1901 collection: &str,
1902 sealed: &reddb_types::Value,
1903 ) -> RedDBResult<reddb_types::Value> {
1904 self.unseal_vault_value_for_tenant(collection, sealed, current_tenant().as_deref())
1905 }
1906
1907 fn unseal_vault_value_for_tenant(
1908 &self,
1909 collection: &str,
1910 sealed: &Value,
1911 tenant: Option<&str>,
1912 ) -> RedDBResult<Value> {
1913 let reddb_types::Value::Secret(payload) = sealed else {
1914 return Err(RedDBError::Query(
1915 "vault unseal failed: stored value is not sealed".to_string(),
1916 ));
1917 };
1918 if payload.len() < 12 {
1919 return Err(RedDBError::Query(
1920 "vault unseal failed: sealed payload is truncated".to_string(),
1921 ));
1922 }
1923 let engine_limit =
1924 usize::try_from(self.memory_accounting().budget().resolved_bytes).unwrap_or(usize::MAX);
1925 let limit_bytes = self
1926 .inner
1927 .db
1928 .store()
1929 .get_collection(collection)
1930 .and_then(|manager| manager.backing_read_bytes_max())
1931 .map_or(engine_limit, |limit| limit.min(engine_limit));
1932 let limit_error = |required_bytes| {
1933 RedDBError::from(crate::storage::unified::StoreError::ReadLimitExceeded {
1934 required_bytes,
1935 limit_bytes,
1936 })
1937 };
1938 let cipher_bytes = payload.len().saturating_mul(2);
1942 let crypto_bytes = cipher_bytes.saturating_add(payload.len());
1943 if crypto_bytes > limit_bytes {
1944 return Err(limit_error(crypto_bytes));
1945 }
1946 let key = self.vault_encryption_key(collection)?;
1947 let mut nonce = [0u8; 12];
1948 nonce.copy_from_slice(&payload[..12]);
1949 let aad = vault_value_aad(collection, tenant);
1950 let plaintext = zeroize::Zeroizing::new(
1951 crate::crypto::aes_gcm::aes256_gcm_decrypt(
1952 &key,
1953 &nonce,
1954 aad.as_bytes(),
1955 &payload[12..],
1956 )
1957 .map_err(|_| RedDBError::Query("vault unseal failed: decryption failed".to_string()))?,
1958 );
1959 let (value, consumed) =
1960 reddb_types::Value::from_bytes_with_limit(&plaintext, limit_bytes - cipher_bytes)
1961 .map_err(|error| match error {
1962 reddb_types::ValueError::ReadLimitExceeded { required_bytes, .. } => {
1963 limit_error(required_bytes.saturating_add(cipher_bytes))
1964 }
1965 other => RedDBError::Query(format!(
1966 "vault unseal failed: bad plaintext value: {other}"
1967 )),
1968 })?;
1969 if consumed != plaintext.len() {
1970 return Err(RedDBError::Query(
1971 "vault unseal failed: trailing plaintext bytes".to_string(),
1972 ));
1973 }
1974 Ok(value)
1975 }
1976
1977 pub(crate) fn peek_vault_unsealed(
1983 &self,
1984 collection: &str,
1985 key: &str,
1986 ) -> RedDBResult<Option<reddb_types::Value>> {
1987 let ops = KvAtomicOps::new(self);
1988 let Some(entry) = ops.get_vault_entry(collection, key)? else {
1989 return Ok(None);
1990 };
1991 if entry.tombstone {
1992 return Ok(None);
1993 }
1994 Ok(self.unseal_vault_value(collection, &entry.value).ok())
1995 }
1996
1997 fn vault_target_resource(collection: &str, key: &str) -> String {
1998 if collection == "red.vault" {
1999 return format!("red.vault/{}", key.to_ascii_lowercase());
2000 }
2001 format!("{collection}.{key}")
2002 }
2003
2004 fn current_vault_actor() -> String {
2005 current_auth_identity()
2006 .map(|(principal, _)| principal)
2007 .unwrap_or_else(|| "anonymous".to_string())
2008 }
2009
2010 fn vault_request_id() -> String {
2011 let conn_id = current_connection_id();
2012 if conn_id == 0 {
2013 "embedded".to_string()
2014 } else {
2015 format!("conn-{conn_id}")
2016 }
2017 }
2018
2019 pub(crate) fn check_vault_capability(
2020 &self,
2021 action: &str,
2022 collection: &str,
2023 key: &str,
2024 ) -> Result<(), String> {
2025 self.check_vault_capability_in_tenant(action, collection, key, current_tenant().as_deref())
2026 }
2027
2028 fn check_vault_capability_in_tenant(
2029 &self,
2030 action: &str,
2031 collection: &str,
2032 key: &str,
2033 owner_tenant: Option<&str>,
2034 ) -> Result<(), String> {
2035 let Some(auth_store) = self.inner.auth_store.read().clone() else {
2036 return Ok(());
2037 };
2038 let Some((principal, role)) = current_auth_identity() else {
2039 if !auth_store.iam_authorization_enabled() {
2040 return Ok(());
2041 }
2042 return Err(
2043 "IAM authorization is enabled; vault capability check requires an authenticated principal"
2044 .to_string(),
2045 );
2046 };
2047 let tenant = current_tenant();
2048 let principal_id = crate::auth::UserId::from_parts(tenant.as_deref(), &principal);
2049 let mut resource = crate::auth::policies::ResourceRef::new(
2050 "vault",
2051 Self::vault_target_resource(collection, key),
2052 );
2053 if let Some(owner_tenant) = owner_tenant {
2054 resource = resource.with_tenant(owner_tenant.to_string());
2055 } else if tenant.is_some() {
2056 resource.name = format!("tenant/platform/{}", resource.name);
2057 }
2058 let ctx = crate::auth::policies::EvalContext {
2059 principal_tenant: tenant.clone(),
2060 current_tenant: tenant,
2061 peer_ip: None,
2062 mfa_present: false,
2063 now_ms: crate::utils::now_unix_millis() as u128,
2064 principal_is_admin_role: role == crate::auth::Role::Admin,
2065 principal_is_platform_scoped: principal_id.tenant.is_none(),
2066 };
2067 let allowed = if owner_tenant != ctx.principal_tenant.as_deref() {
2068 auth_store.check_policy_authz(&principal_id, action, &resource, &ctx)
2071 } else {
2072 auth_store.check_policy_authz_with_role(&principal_id, action, &resource, &ctx, role)
2073 };
2074 if allowed {
2075 Ok(())
2076 } else {
2077 Err(format!(
2078 "principal=`{}` action=`{}` resource=`vault:{}` denied by IAM policy",
2079 principal,
2080 action,
2081 Self::vault_target_resource(collection, key)
2082 ))
2083 }
2084 }
2085
2086 fn check_system_vault_capability(
2087 &self,
2088 action: &str,
2089 collection: &str,
2090 key: &str,
2091 ) -> Result<(), String> {
2092 self.check_vault_capability(action, collection, key)
2093 }
2094
2095 fn audit_vault_unseal(
2096 &self,
2097 collection: &str,
2098 key: &str,
2099 outcome: crate::runtime::audit_log::Outcome,
2100 reason: &str,
2101 entry: Option<&VaultEntry>,
2102 ) {
2103 let actor = Self::current_vault_actor();
2104 let request_id = Self::vault_request_id();
2105 let mut builder = crate::runtime::audit_log::AuditEvent::builder("vault/reveal")
2106 .principal(actor.clone())
2107 .source(crate::runtime::audit_log::AuditAuthSource::Password)
2108 .resource(format!(
2109 "vault:{}",
2110 Self::vault_target_resource(collection, key)
2111 ))
2112 .outcome(outcome)
2113 .correlation_id(request_id.clone())
2114 .fields([
2115 crate::runtime::audit_log::AuditFieldEscaper::field("actor", actor),
2116 crate::runtime::audit_log::AuditFieldEscaper::field("collection", collection),
2117 crate::runtime::audit_log::AuditFieldEscaper::field("key", key),
2118 crate::runtime::audit_log::AuditFieldEscaper::field(
2119 "target",
2120 Self::vault_target_resource(collection, key),
2121 ),
2122 crate::runtime::audit_log::AuditFieldEscaper::field("reason", reason),
2123 crate::runtime::audit_log::AuditFieldEscaper::field("request_id", request_id),
2124 crate::runtime::audit_log::AuditFieldEscaper::field(
2125 "connection_id",
2126 current_connection_id(),
2127 ),
2128 ]);
2129 if let Some(tenant) = current_tenant() {
2130 builder = builder.tenant(tenant);
2131 }
2132 if let Some(entry) = entry {
2133 builder = builder.fields([
2134 crate::runtime::audit_log::AuditFieldEscaper::field("entity_id", entry.id.raw()),
2135 crate::runtime::audit_log::AuditFieldEscaper::field(
2136 "sequence_id",
2137 entry.sequence_id,
2138 ),
2139 ]);
2140 }
2141 self.audit_log().record_event(builder.build());
2142 }
2143
2144 fn audit_vault_lifecycle(
2145 &self,
2146 operation: &str,
2147 collection: &str,
2148 key: &str,
2149 outcome: crate::runtime::audit_log::Outcome,
2150 reason: &str,
2151 entry: Option<&VaultEntry>,
2152 ) {
2153 let actor = Self::current_vault_actor();
2154 let request_id = Self::vault_request_id();
2155 let mut builder =
2156 crate::runtime::audit_log::AuditEvent::builder(format!("vault/{operation}"))
2157 .principal(actor.clone())
2158 .source(crate::runtime::audit_log::AuditAuthSource::Password)
2159 .resource(format!(
2160 "vault:{}",
2161 Self::vault_target_resource(collection, key)
2162 ))
2163 .outcome(outcome)
2164 .correlation_id(request_id.clone())
2165 .fields([
2166 crate::runtime::audit_log::AuditFieldEscaper::field("actor", actor),
2167 crate::runtime::audit_log::AuditFieldEscaper::field("collection", collection),
2168 crate::runtime::audit_log::AuditFieldEscaper::field("key", key),
2169 crate::runtime::audit_log::AuditFieldEscaper::field(
2170 "target",
2171 Self::vault_target_resource(collection, key),
2172 ),
2173 crate::runtime::audit_log::AuditFieldEscaper::field("reason", reason),
2174 crate::runtime::audit_log::AuditFieldEscaper::field("request_id", request_id),
2175 crate::runtime::audit_log::AuditFieldEscaper::field(
2176 "connection_id",
2177 current_connection_id(),
2178 ),
2179 ]);
2180 if let Some(tenant) = current_tenant() {
2181 builder = builder.tenant(tenant);
2182 }
2183 if let Some(entry) = entry {
2184 builder = builder.fields([
2185 crate::runtime::audit_log::AuditFieldEscaper::field("entity_id", entry.id.raw()),
2186 crate::runtime::audit_log::AuditFieldEscaper::field("version", entry.version),
2187 crate::runtime::audit_log::AuditFieldEscaper::field(
2188 "sequence_id",
2189 entry.sequence_id,
2190 ),
2191 ]);
2192 }
2193 self.audit_log().record_event(builder.build());
2194 }
2195
2196 fn emit_vault_control_event(
2197 &self,
2198 kind: crate::runtime::control_events::EventKind,
2199 outcome: crate::runtime::control_events::Outcome,
2200 action: &'static str,
2201 collection: &str,
2202 key: &str,
2203 reason: &str,
2204 entry: Option<&VaultEntry>,
2205 extra_fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
2206 ) -> RedDBResult<()> {
2207 use crate::runtime::control_events::{
2208 ActorRef, ControlEvent, ControlEventCtx, ControlEventLedger, Sensitivity,
2209 };
2210 use std::borrow::Cow;
2211
2212 let tenant = current_tenant();
2213 let principal = current_auth_identity().map(|(principal, _)| principal);
2214 let actor_user = principal
2215 .as_ref()
2216 .map(|principal| crate::auth::UserId::from_parts(tenant.as_deref(), principal));
2217 let request_id = Self::vault_request_id();
2218 let actor = actor_user
2219 .as_ref()
2220 .map(ActorRef::User)
2221 .unwrap_or(ActorRef::Anonymous);
2222 let ctx = ControlEventCtx {
2223 actor,
2224 scope: tenant.as_ref().map(|scope| Cow::Borrowed(scope.as_str())),
2225 request_id: Some(Cow::Borrowed(request_id.as_str())),
2226 trace_id: None,
2227 };
2228
2229 let target = Self::vault_target_resource(collection, key);
2230 let mut fields = std::collections::HashMap::new();
2231 fields.insert("path".to_string(), Sensitivity::raw(target.clone()));
2232 fields.insert("collection".to_string(), Sensitivity::raw(collection));
2233 fields.insert("key".to_string(), Sensitivity::raw(key));
2234 fields.insert(
2235 "connection_id".to_string(),
2236 Sensitivity::raw(current_connection_id().to_string()),
2237 );
2238 if let Some(entry) = entry {
2239 fields.insert(
2240 "entity_id".to_string(),
2241 Sensitivity::raw(entry.id.raw().to_string()),
2242 );
2243 fields.insert(
2244 "sequence_id".to_string(),
2245 Sensitivity::raw(entry.sequence_id.to_string()),
2246 );
2247 fields.insert(
2248 "version".to_string(),
2249 Sensitivity::raw(entry.version.to_string()),
2250 );
2251 fields.insert("op".to_string(), Sensitivity::raw(entry.op.clone()));
2252 fields.insert(
2253 "tombstone".to_string(),
2254 Sensitivity::raw(entry.tombstone.to_string()),
2255 );
2256 if !entry.tombstone {
2257 fields.insert(
2258 "fingerprint".to_string(),
2259 Sensitivity::raw(vault_fingerprint(&entry.value)),
2260 );
2261 }
2262 fields.insert(
2263 "tags".to_string(),
2264 Sensitivity::raw(format!("{:?}", vault_tags_value(&entry.metadata))),
2265 );
2266 }
2267 for (key, value) in extra_fields {
2268 fields.insert(key, value);
2269 }
2270
2271 let event = ControlEvent {
2272 kind,
2273 outcome,
2274 action: Cow::Borrowed(action),
2275 resource: Some(format!("vault:{target}")),
2276 reason: Some(reason.to_string()),
2277 matched_policy_id: None,
2278 fields,
2279 };
2280 let ledger = self.inner.control_event_ledger.read();
2281 match ledger.emit(&ctx, event) {
2282 Ok(_) => Ok(()),
2283 Err(err) if self.inner.control_event_config.require_persistence() => {
2284 Err(RedDBError::Internal(err.to_string()))
2285 }
2286 Err(_) => Ok(()),
2287 }
2288 }
2289
2290 pub(crate) fn resolve_vault_secret_value(
2291 &self,
2292 collection: &str,
2293 key: &str,
2294 ) -> RedDBResult<Value> {
2295 let ops = KvAtomicOps::new(self);
2296 let entry = ops.get_vault_entry(collection, key)?;
2297 if let Err(reason) = self.check_vault_capability("vault:reveal", collection, key) {
2298 self.audit_vault_unseal(
2299 collection,
2300 key,
2301 crate::runtime::audit_log::Outcome::Denied,
2302 &reason,
2303 entry.as_ref(),
2304 );
2305 self.emit_vault_control_event(
2306 crate::runtime::control_events::EventKind::VaultReveal,
2307 crate::runtime::control_events::Outcome::Denied,
2308 "vault:reveal",
2309 collection,
2310 key,
2311 &reason,
2312 entry.as_ref(),
2313 Vec::new(),
2314 )?;
2315 return Err(RedDBError::Query(reason));
2316 }
2317 let Some(entry) = entry else {
2318 let reason = "not_found";
2319 self.audit_vault_unseal(
2320 collection,
2321 key,
2322 crate::runtime::audit_log::Outcome::Denied,
2323 reason,
2324 None,
2325 );
2326 self.emit_vault_control_event(
2327 crate::runtime::control_events::EventKind::VaultReveal,
2328 crate::runtime::control_events::Outcome::Denied,
2329 "vault:reveal",
2330 collection,
2331 key,
2332 reason,
2333 None,
2334 Vec::new(),
2335 )?;
2336 return Err(RedDBError::NotFound(format!(
2337 "vault secret '{}.{}' not found",
2338 collection, key
2339 )));
2340 };
2341 if entry.tombstone {
2342 let reason = "deleted";
2343 self.audit_vault_unseal(
2344 collection,
2345 key,
2346 crate::runtime::audit_log::Outcome::Denied,
2347 reason,
2348 Some(&entry),
2349 );
2350 self.emit_vault_control_event(
2351 crate::runtime::control_events::EventKind::VaultReveal,
2352 crate::runtime::control_events::Outcome::Denied,
2353 "vault:reveal",
2354 collection,
2355 key,
2356 reason,
2357 Some(&entry),
2358 Vec::new(),
2359 )?;
2360 return Err(RedDBError::NotFound(format!(
2361 "vault secret '{}.{}' is deleted",
2362 collection, key
2363 )));
2364 }
2365 match self.unseal_vault_value(collection, &entry.value) {
2366 Ok(value) => {
2367 self.audit_vault_unseal(
2368 collection,
2369 key,
2370 crate::runtime::audit_log::Outcome::Success,
2371 "ok",
2372 Some(&entry),
2373 );
2374 self.emit_vault_control_event(
2375 crate::runtime::control_events::EventKind::VaultReveal,
2376 crate::runtime::control_events::Outcome::Allowed,
2377 "vault:reveal",
2378 collection,
2379 key,
2380 "ok",
2381 Some(&entry),
2382 Vec::new(),
2383 )?;
2384 Ok(value)
2385 }
2386 Err(err) => {
2387 let reason = err.to_string();
2388 self.audit_vault_unseal(
2389 collection,
2390 key,
2391 crate::runtime::audit_log::Outcome::Error,
2392 &reason,
2393 Some(&entry),
2394 );
2395 self.emit_vault_control_event(
2396 crate::runtime::control_events::EventKind::VaultReveal,
2397 crate::runtime::control_events::Outcome::Error,
2398 "vault:reveal",
2399 collection,
2400 key,
2401 &reason,
2402 Some(&entry),
2403 Vec::new(),
2404 )?;
2405 Err(err)
2406 }
2407 }
2408 }
2409
2410 pub fn execute_kv_command(
2412 &self,
2413 raw_query: &str,
2414 cmd: &reddb_rql::ast::KvCommand,
2415 ) -> RedDBResult<RuntimeQueryResult> {
2416 use reddb_rql::ast::KvCommand;
2417
2418 let ops = KvAtomicOps::new(self);
2419
2420 match cmd {
2421 KvCommand::Put {
2422 model,
2423 collection,
2424 key,
2425 value,
2426 ttl_ms,
2427 tags,
2428 if_not_exists,
2429 } => {
2430 if *model == crate::catalog::CollectionModel::Vault {
2431 self.check_system_vault_capability("vault:write", collection, key)
2432 .map_err(RedDBError::Query)?;
2433 }
2434 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2435 let (created, id) = ops.set_with_tags_for_model(
2436 *model,
2437 collection,
2438 key,
2439 value.clone(),
2440 *ttl_ms,
2441 tags,
2442 *if_not_exists,
2443 )?;
2444
2445 let mut result = UnifiedResult::with_columns(vec![
2446 "ok".into(),
2447 "collection".into(),
2448 "key".into(),
2449 "id".into(),
2450 "created".into(),
2451 "tags".into(),
2452 ]);
2453 let mut record = UnifiedRecord::new();
2454 record.set("ok", Value::Boolean(true));
2455 record.set("collection", Value::text(collection.clone()));
2456 record.set("key", Value::text(key.clone()));
2457 record.set("id", Value::Integer(id.raw() as i64));
2458 record.set("created", Value::Boolean(created));
2459 record.set("tags", kv_tags_value(tags));
2460 result.push(record);
2461
2462 Ok(RuntimeQueryResult {
2463 query: raw_query.to_string(),
2464 mode: reddb_rql::modes::QueryMode::Sql,
2465 statement: if *model == crate::catalog::CollectionModel::Vault {
2466 "vault_put"
2467 } else {
2468 "kv_put"
2469 },
2470 engine: if *model == crate::catalog::CollectionModel::Vault {
2471 "vault"
2472 } else {
2473 "kv"
2474 },
2475 result,
2476 affected_rows: 1,
2477 statement_type: if created { "insert" } else { "update" },
2478 bookmark: None,
2479 notice: None,
2480 })
2481 }
2482 KvCommand::InvalidateTags { collection, tags } => {
2483 let invalidated = ops.invalidate_tags(collection, tags)?;
2484
2485 let mut result = UnifiedResult::with_columns(vec![
2486 "ok".into(),
2487 "collection".into(),
2488 "invalidated".into(),
2489 "tags".into(),
2490 ]);
2491 let mut record = UnifiedRecord::new();
2492 record.set("ok", Value::Boolean(true));
2493 record.set("collection", Value::text(collection.clone()));
2494 record.set("invalidated", Value::Integer(invalidated as i64));
2495 record.set("tags", kv_tags_value(tags));
2496 result.push(record);
2497
2498 Ok(RuntimeQueryResult {
2499 query: raw_query.to_string(),
2500 mode: reddb_rql::modes::QueryMode::Sql,
2501 statement: "kv_invalidate_tags",
2502 engine: "kv",
2503 result,
2504 affected_rows: invalidated as u64,
2505 statement_type: "delete",
2506 bookmark: None,
2507 notice: None,
2508 })
2509 }
2510
2511 KvCommand::Rotate {
2512 collection,
2513 key,
2514 value,
2515 tags,
2516 } => {
2517 self.check_system_vault_capability("vault:write", collection, key)
2518 .map_err(RedDBError::Query)?;
2519 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2520 let entry = ops.append_vault_version(
2521 collection,
2522 key,
2523 value.clone(),
2524 "rotate",
2525 false,
2526 tags,
2527 )?;
2528 self.record_kv_watch_event(
2529 crate::replication::cdc::ChangeOperation::Update,
2530 collection,
2531 key,
2532 entry.id.raw(),
2533 None,
2534 Some(vault_entry_metadata_json(&entry)),
2535 );
2536 self.audit_vault_lifecycle(
2537 "rotate",
2538 collection,
2539 key,
2540 crate::runtime::audit_log::Outcome::Success,
2541 "ok",
2542 Some(&entry),
2543 );
2544 self.emit_vault_control_event(
2545 crate::runtime::control_events::EventKind::VaultRotate,
2546 crate::runtime::control_events::Outcome::Allowed,
2547 "vault:rotate",
2548 collection,
2549 key,
2550 "ok",
2551 Some(&entry),
2552 Vec::new(),
2553 )?;
2554 Ok(vault_write_result(
2555 raw_query,
2556 "vault_rotate",
2557 "update",
2558 collection,
2559 key,
2560 &entry,
2561 1,
2562 ))
2563 }
2564
2565 KvCommand::List {
2566 model,
2567 collection,
2568 prefix,
2569 limit,
2570 offset,
2571 as_json,
2572 } => {
2573 if *model == crate::catalog::CollectionModel::Vault {
2574 if *as_json {
2575 return Err(RedDBError::Query(
2576 "LIST VAULT AS JSON is not supported; vault list only exposes metadata"
2577 .to_string(),
2578 ));
2579 }
2580 let mut entries = ops.latest_vault_entries(collection, prefix.as_deref())?;
2581 entries.sort_by(|left, right| left.key.cmp(&right.key));
2582 let mut result = UnifiedResult::with_columns(vec![
2583 "collection".into(),
2584 "key".into(),
2585 "version".into(),
2586 "fingerprint".into(),
2587 "tags".into(),
2588 "created_at".into(),
2589 "updated_at".into(),
2590 "status".into(),
2591 "tombstone".into(),
2592 "op".into(),
2593 ]);
2594 let mut visible = Vec::new();
2595 for entry in entries {
2596 match self.check_vault_capability(
2597 "vault:read_metadata",
2598 collection,
2599 &entry.key,
2600 ) {
2601 Ok(()) => {
2602 self.emit_vault_control_event(
2603 crate::runtime::control_events::EventKind::VaultMetadataRead,
2604 crate::runtime::control_events::Outcome::Allowed,
2605 "vault:read_metadata",
2606 collection,
2607 &entry.key,
2608 "ok",
2609 Some(&entry),
2610 Vec::new(),
2611 )?;
2612 visible.push(entry);
2613 }
2614 Err(reason) => {
2615 self.emit_vault_control_event(
2616 crate::runtime::control_events::EventKind::VaultMetadataRead,
2617 crate::runtime::control_events::Outcome::Denied,
2618 "vault:read_metadata",
2619 collection,
2620 &entry.key,
2621 &reason,
2622 Some(&entry),
2623 Vec::new(),
2624 )?;
2625 }
2626 }
2627 }
2628 for entry in visible
2629 .into_iter()
2630 .skip(*offset)
2631 .take(limit.unwrap_or(usize::MAX))
2632 {
2633 push_vault_metadata_record(&mut result, collection, &entry.key, &entry);
2634 }
2635 Ok(RuntimeQueryResult {
2636 query: raw_query.to_string(),
2637 mode: reddb_rql::modes::QueryMode::Sql,
2638 statement: "vault_list",
2639 engine: "vault",
2640 result,
2641 affected_rows: 0,
2642 statement_type: "select",
2643 bookmark: None,
2644 notice: None,
2645 })
2646 } else {
2647 let mut result = UnifiedResult::with_columns(vec![
2648 "rid".into(),
2649 "collection".into(),
2650 "kind".into(),
2651 "tenant".into(),
2652 "created_at".into(),
2653 "updated_at".into(),
2654 "key".into(),
2655 "value".into(),
2656 "tags".into(),
2657 ]);
2658 let entries_max = match limit {
2659 Some(0) => 0,
2660 Some(limit) => offset.saturating_add(*limit),
2661 None => usize::MAX,
2662 };
2663 let entries =
2664 ops.latest_kv_entries(collection, prefix.as_deref(), entries_max)?;
2665 if *as_json {
2666 let mut tree = crate::json::Value::Object(crate::json::Map::new());
2667 for (key, entity) in entries
2668 .into_iter()
2669 .skip(*offset)
2670 .take(limit.unwrap_or(usize::MAX))
2671 {
2672 let Some(value) = kv_value_from_entity(&entity) else {
2673 continue;
2674 };
2675 let relative = match prefix {
2676 Some(pfx) if key == *pfx => "",
2677 Some(pfx) => key
2678 .strip_prefix(pfx.as_str())
2679 .map(|tail| tail.strip_prefix('.').unwrap_or(tail))
2680 .unwrap_or(key.as_str()),
2681 None => key.as_str(),
2682 };
2683 insert_kv_json_path(
2684 &mut tree,
2685 relative,
2686 reddb_types::Value::to_json(&value),
2687 );
2688 }
2689 Ok(kv_list_json_result(raw_query, collection, prefix, tree))
2690 } else {
2691 for (key, entity) in entries
2692 .into_iter()
2693 .skip(*offset)
2694 .take(limit.unwrap_or(usize::MAX))
2695 {
2696 let mut record = UnifiedRecord::new();
2697 record.set("rid", Value::UnsignedInteger(entity.id.raw()));
2698 record.set("collection", Value::text(collection.clone()));
2699 record.set("kind", Value::text("kv"));
2700 record.set("tenant", Value::Null);
2701 record.set("created_at", Value::UnsignedInteger(entity.created_at));
2702 record.set("updated_at", Value::UnsignedInteger(entity.updated_at));
2703 record.set("key", Value::text(key.clone()));
2704 record.set(
2705 "value",
2706 kv_value_from_entity(&entity).unwrap_or(reddb_types::Value::Null),
2707 );
2708 record.set(
2709 "tags",
2710 kv_tags_value(&ops.try_tags_for_key(collection, &key)?),
2711 );
2712 result.push(record);
2713 }
2714 Ok(RuntimeQueryResult {
2715 query: raw_query.to_string(),
2716 mode: reddb_rql::modes::QueryMode::Sql,
2717 statement: "kv_list",
2718 engine: "kv",
2719 result,
2720 affected_rows: 0,
2721 statement_type: "select",
2722 bookmark: None,
2723 notice: None,
2724 })
2725 }
2726 }
2727 }
2728
2729 KvCommand::History { collection, key } => {
2730 let latest = ops.get_vault_entry(collection, key)?;
2731 if let Err(reason) =
2732 self.check_vault_capability("vault:read_metadata", collection, key)
2733 {
2734 self.emit_vault_control_event(
2735 crate::runtime::control_events::EventKind::VaultMetadataRead,
2736 crate::runtime::control_events::Outcome::Denied,
2737 "vault:read_metadata",
2738 collection,
2739 key,
2740 &reason,
2741 latest.as_ref(),
2742 Vec::new(),
2743 )?;
2744 return Err(RedDBError::Query(reason));
2745 }
2746 let versions =
2747 super::keyed_spine::history_versions(ops.vault_versions(collection, key)?);
2748 let result = vault_history_result(collection, key, &versions);
2749 self.emit_vault_control_event(
2750 crate::runtime::control_events::EventKind::VaultMetadataRead,
2751 crate::runtime::control_events::Outcome::Allowed,
2752 "vault:read_metadata",
2753 collection,
2754 key,
2755 "ok",
2756 latest.as_ref(),
2757 Vec::new(),
2758 )?;
2759 Ok(RuntimeQueryResult {
2760 query: raw_query.to_string(),
2761 mode: reddb_rql::modes::QueryMode::Sql,
2762 statement: "vault_history",
2763 engine: "vault",
2764 result,
2765 affected_rows: 0,
2766 statement_type: "select",
2767 bookmark: None,
2768 notice: None,
2769 })
2770 }
2771
2772 KvCommand::Purge { collection, key } => {
2773 let entry = ops.get_vault_entry(collection, key)?;
2774 if let Err(reason) = self.check_vault_capability("vault:purge", collection, key) {
2775 self.audit_vault_lifecycle(
2776 "purge",
2777 collection,
2778 key,
2779 crate::runtime::audit_log::Outcome::Denied,
2780 &reason,
2781 entry.as_ref(),
2782 );
2783 self.emit_vault_control_event(
2784 crate::runtime::control_events::EventKind::VaultPurge,
2785 crate::runtime::control_events::Outcome::Denied,
2786 "vault:purge",
2787 collection,
2788 key,
2789 &reason,
2790 entry.as_ref(),
2791 Vec::new(),
2792 )?;
2793 return Err(RedDBError::Query(reason));
2794 }
2795 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2796 let purged = ops.purge_vault_versions(collection, key)?;
2797 self.audit_vault_lifecycle(
2798 "purge",
2799 collection,
2800 key,
2801 crate::runtime::audit_log::Outcome::Success,
2802 "ok",
2803 entry.as_ref(),
2804 );
2805 self.emit_vault_control_event(
2806 crate::runtime::control_events::EventKind::VaultPurge,
2807 crate::runtime::control_events::Outcome::Allowed,
2808 "vault:purge",
2809 collection,
2810 key,
2811 "ok",
2812 entry.as_ref(),
2813 vec![(
2814 "purged".to_string(),
2815 crate::runtime::control_events::Sensitivity::raw(purged.to_string()),
2816 )],
2817 )?;
2818 let mut result = UnifiedResult::with_columns(vec![
2819 "ok".into(),
2820 "collection".into(),
2821 "key".into(),
2822 "purged".into(),
2823 ]);
2824 let mut record = UnifiedRecord::new();
2825 record.set("ok", Value::Boolean(true));
2826 record.set("collection", Value::text(collection.clone()));
2827 record.set("key", Value::text(key.clone()));
2828 record.set("purged", Value::Integer(purged as i64));
2829 result.push(record);
2830 Ok(RuntimeQueryResult {
2831 query: raw_query.to_string(),
2832 mode: reddb_rql::modes::QueryMode::Sql,
2833 statement: "vault_purge",
2834 engine: "vault",
2835 result,
2836 affected_rows: purged as u64,
2837 statement_type: "delete",
2838 bookmark: None,
2839 notice: None,
2840 })
2841 }
2842
2843 KvCommand::Get {
2844 model,
2845 collection,
2846 key,
2847 } => {
2848 if *model == crate::catalog::CollectionModel::Vault {
2849 let entry = ops.get_vault_entry(collection, key)?;
2850 if let Err(reason) =
2851 self.check_vault_capability("vault:read_metadata", collection, key)
2852 {
2853 self.emit_vault_control_event(
2854 crate::runtime::control_events::EventKind::VaultMetadataRead,
2855 crate::runtime::control_events::Outcome::Denied,
2856 "vault:read_metadata",
2857 collection,
2858 key,
2859 &reason,
2860 entry.as_ref(),
2861 Vec::new(),
2862 )?;
2863 return Err(RedDBError::Query(reason));
2864 }
2865 let key_available = self.vault_key_available(collection);
2866 let result =
2867 vault_metadata_result(collection, key, entry.as_ref(), key_available);
2868 self.emit_vault_control_event(
2869 crate::runtime::control_events::EventKind::VaultMetadataRead,
2870 crate::runtime::control_events::Outcome::Allowed,
2871 "vault:read_metadata",
2872 collection,
2873 key,
2874 "ok",
2875 entry.as_ref(),
2876 Vec::new(),
2877 )?;
2878 return Ok(RuntimeQueryResult {
2879 query: raw_query.to_string(),
2880 mode: reddb_rql::modes::QueryMode::Sql,
2881 statement: "vault_get",
2882 engine: "vault",
2883 result,
2884 affected_rows: 0,
2885 statement_type: "select",
2886 bookmark: None,
2887 notice: None,
2888 });
2889 }
2890
2891 let entry =
2892 super::kv_read::KvReadResolver::for_keyed_model(self, *model, collection)?
2893 .record(key)?;
2894 if *model == crate::catalog::CollectionModel::Kv {
2895 self.inner.kv_stats.incr_gets();
2896 }
2897 let mut result = UnifiedResult::with_columns(vec![
2898 "rid".into(),
2899 "collection".into(),
2900 "kind".into(),
2901 "tenant".into(),
2902 "created_at".into(),
2903 "updated_at".into(),
2904 "key".into(),
2905 "value".into(),
2906 "tags".into(),
2907 ]);
2908 let mut record = UnifiedRecord::new();
2909 if let Some(entry) = entry.as_ref() {
2910 record.set("rid", Value::UnsignedInteger(entry.id.raw()));
2911 record.set("created_at", Value::UnsignedInteger(entry.created_at));
2912 record.set("updated_at", Value::UnsignedInteger(entry.updated_at));
2913 } else {
2914 record.set("rid", Value::Null);
2915 record.set("created_at", Value::Null);
2916 record.set("updated_at", Value::Null);
2917 }
2918 record.set("collection", Value::text(collection.clone()));
2919 record.set("kind", Value::text(keyed_model_name(*model).to_string()));
2920 record.set("tenant", Value::Null);
2921 record.set("key", Value::text(key.clone()));
2922 record.set(
2923 "value",
2924 entry.and_then(|entry| entry.value).unwrap_or(Value::Null),
2925 );
2926 let tags = if *model == CollectionModel::Kv {
2927 ops.try_tags_for_key(collection, key)?
2928 } else {
2929 ops.tags_for_key(collection, key)
2930 };
2931 record.set("tags", kv_tags_value(&tags));
2932 result.push(record);
2933
2934 Ok(RuntimeQueryResult {
2935 query: raw_query.to_string(),
2936 mode: reddb_rql::modes::QueryMode::Sql,
2937 statement: "kv_get",
2938 engine: "kv",
2939 result,
2940 affected_rows: 0,
2941 statement_type: "select",
2942 bookmark: None,
2943 notice: None,
2944 })
2945 }
2946 KvCommand::Watch {
2947 model,
2948 collection,
2949 key,
2950 prefix,
2951 from_lsn,
2952 } => {
2953 let watch_key = if *prefix {
2954 format!("{key}.*")
2955 } else {
2956 key.clone()
2957 };
2958 let endpoint = match from_lsn {
2959 Some(lsn) => format!(
2960 "/collections/{collection}/{}/{watch_key}/watch?since_lsn={lsn}",
2961 keyed_model_name(*model)
2962 ),
2963 None => format!(
2964 "/collections/{collection}/{}/{watch_key}/watch",
2965 keyed_model_name(*model)
2966 ),
2967 };
2968 let mut result = UnifiedResult::with_columns(vec![
2969 "collection".into(),
2970 "key".into(),
2971 "prefix".into(),
2972 "from_lsn".into(),
2973 "watch_url".into(),
2974 "streaming".into(),
2975 ]);
2976 let mut record = UnifiedRecord::new();
2977 record.set("collection", Value::text(collection.clone()));
2978 record.set("key", Value::text(watch_key));
2979 record.set("prefix", Value::Boolean(*prefix));
2980 record.set(
2981 "from_lsn",
2982 from_lsn
2983 .map(Value::UnsignedInteger)
2984 .unwrap_or(reddb_types::Value::Null),
2985 );
2986 record.set("watch_url", Value::text(endpoint));
2987 record.set("streaming", Value::Boolean(true));
2988 result.push(record);
2989
2990 Ok(RuntimeQueryResult {
2991 query: raw_query.to_string(),
2992 mode: reddb_rql::modes::QueryMode::Sql,
2993 statement: "kv_watch",
2994 engine: keyed_model_name(*model),
2995 result,
2996 affected_rows: 0,
2997 statement_type: "stream",
2998 bookmark: None,
2999 notice: None,
3000 })
3001 }
3002
3003 KvCommand::Reveal {
3004 collection,
3005 key,
3006 version,
3007 } => {
3008 let latest = ops.get_vault_entry(collection, key)?;
3009 let entry = match version {
3010 Some(version) => ops.get_vault_entry_version(collection, key, *version)?,
3011 None => latest.clone(),
3012 };
3013 let action = match (version, latest.as_ref()) {
3014 (Some(requested), Some(latest)) if *requested == latest.version => {
3015 "vault:reveal"
3016 }
3017 (Some(_), _) => "vault:reveal_history",
3018 _ => "vault:reveal",
3019 };
3020 let event_kind = crate::runtime::control_events::EventKind::VaultReveal;
3021 if let Err(reason) = self.check_vault_capability(action, collection, key) {
3022 self.audit_vault_unseal(
3023 collection,
3024 key,
3025 crate::runtime::audit_log::Outcome::Denied,
3026 &reason,
3027 entry.as_ref(),
3028 );
3029 self.emit_vault_control_event(
3030 event_kind,
3031 crate::runtime::control_events::Outcome::Denied,
3032 action,
3033 collection,
3034 key,
3035 &reason,
3036 entry.as_ref(),
3037 Vec::new(),
3038 )?;
3039 return Err(RedDBError::Query(reason));
3040 }
3041 let Some(entry) = entry else {
3042 let reason = "not_found";
3043 self.audit_vault_unseal(
3044 collection,
3045 key,
3046 crate::runtime::audit_log::Outcome::Denied,
3047 reason,
3048 None,
3049 );
3050 self.emit_vault_control_event(
3051 event_kind,
3052 crate::runtime::control_events::Outcome::Denied,
3053 action,
3054 collection,
3055 key,
3056 reason,
3057 None,
3058 Vec::new(),
3059 )?;
3060 return Err(RedDBError::NotFound(format!(
3061 "vault secret '{}.{}' not found",
3062 collection, key
3063 )));
3064 };
3065 if entry.tombstone {
3066 let reason = "deleted";
3067 self.audit_vault_unseal(
3068 collection,
3069 key,
3070 crate::runtime::audit_log::Outcome::Denied,
3071 reason,
3072 Some(&entry),
3073 );
3074 self.emit_vault_control_event(
3075 event_kind,
3076 crate::runtime::control_events::Outcome::Denied,
3077 action,
3078 collection,
3079 key,
3080 reason,
3081 Some(&entry),
3082 Vec::new(),
3083 )?;
3084 return Err(RedDBError::NotFound(format!(
3085 "vault secret '{}.{}' is deleted",
3086 collection, key
3087 )));
3088 }
3089 match self.unseal_vault_value(collection, &entry.value) {
3090 Ok(value) => {
3091 self.audit_vault_unseal(
3092 collection,
3093 key,
3094 crate::runtime::audit_log::Outcome::Success,
3095 "ok",
3096 Some(&entry),
3097 );
3098 self.emit_vault_control_event(
3099 event_kind,
3100 crate::runtime::control_events::Outcome::Allowed,
3101 action,
3102 collection,
3103 key,
3104 "ok",
3105 Some(&entry),
3106 Vec::new(),
3107 )?;
3108 let mut result = UnifiedResult::with_columns(vec![
3109 "collection".into(),
3110 "key".into(),
3111 "value".into(),
3112 ]);
3113 let mut record = UnifiedRecord::new();
3114 record.set("collection", Value::text(collection.clone()));
3115 record.set("key", Value::text(key.clone()));
3116 record.set("value", value);
3117 result.push(record);
3118 Ok(RuntimeQueryResult {
3119 query: raw_query.to_string(),
3120 mode: reddb_rql::modes::QueryMode::Sql,
3121 statement: "vault_reveal",
3122 engine: "vault",
3123 result,
3124 affected_rows: 0,
3125 statement_type: "select",
3126 bookmark: None,
3127 notice: None,
3128 })
3129 }
3130 Err(err) => {
3131 let reason = err.to_string();
3132 self.audit_vault_unseal(
3133 collection,
3134 key,
3135 crate::runtime::audit_log::Outcome::Error,
3136 &reason,
3137 Some(&entry),
3138 );
3139 self.emit_vault_control_event(
3140 event_kind,
3141 crate::runtime::control_events::Outcome::Error,
3142 action,
3143 collection,
3144 key,
3145 &reason,
3146 Some(&entry),
3147 Vec::new(),
3148 )?;
3149 Err(err)
3150 }
3151 }
3152 }
3153
3154 KvCommand::Incr {
3155 model,
3156 collection,
3157 key,
3158 by,
3159 ttl_ms,
3160 } => {
3161 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3162 let new_value = ops.incr(*model, collection, key, *by, *ttl_ms)?;
3163
3164 let mut result = UnifiedResult::with_columns(vec![
3165 "ok".into(),
3166 "collection".into(),
3167 "key".into(),
3168 "value".into(),
3169 ]);
3170 let mut record = UnifiedRecord::new();
3171 record.set("ok", Value::Boolean(true));
3172 record.set("collection", Value::text(collection.clone()));
3173 record.set("key", Value::text(key.clone()));
3174 record.set("value", Value::Integer(new_value));
3175 result.push(record);
3176
3177 Ok(RuntimeQueryResult {
3178 query: raw_query.to_string(),
3179 mode: reddb_rql::modes::QueryMode::Sql,
3180 statement: "kv_incr",
3181 engine: "kv",
3182 result,
3183 affected_rows: 1,
3184 statement_type: "update",
3185 bookmark: None,
3186 notice: None,
3187 })
3188 }
3189
3190 KvCommand::Cas {
3191 model,
3192 collection,
3193 key,
3194 expected,
3195 new_value,
3196 ttl_ms,
3197 } => {
3198 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3199 let (ok, current) = ops.cas(
3200 *model,
3201 collection,
3202 key,
3203 expected.as_ref(),
3204 new_value.clone(),
3205 *ttl_ms,
3206 )?;
3207
3208 let mut result = UnifiedResult::with_columns(vec![
3209 "ok".into(),
3210 "collection".into(),
3211 "key".into(),
3212 "current".into(),
3213 ]);
3214 let mut record = UnifiedRecord::new();
3215 record.set("ok", Value::Boolean(ok));
3216 record.set("collection", Value::text(collection.clone()));
3217 record.set("key", Value::text(key.clone()));
3218 record.set("current", current.unwrap_or(reddb_types::Value::Null));
3219 result.push(record);
3220
3221 Ok(RuntimeQueryResult {
3222 query: raw_query.to_string(),
3223 mode: reddb_rql::modes::QueryMode::Sql,
3224 statement: "kv_cas",
3225 engine: "kv",
3226 result,
3227 affected_rows: if ok { 1 } else { 0 },
3228 statement_type: "update",
3229 bookmark: None,
3230 notice: None,
3231 })
3232 }
3233
3234 KvCommand::Delete {
3235 model,
3236 collection,
3237 key,
3238 } => {
3239 if *model == crate::catalog::CollectionModel::Vault {
3240 self.check_system_vault_capability("vault:write", collection, key)
3241 .map_err(RedDBError::Query)?;
3242 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3243 let entry = ops.append_vault_version(
3244 collection,
3245 key,
3246 Value::Null,
3247 "delete",
3248 true,
3249 &[],
3250 )?;
3251 self.record_kv_watch_event(
3252 crate::replication::cdc::ChangeOperation::Delete,
3253 collection,
3254 key,
3255 entry.id.raw(),
3256 None,
3257 Some(vault_entry_metadata_json(&entry)),
3258 );
3259 self.audit_vault_lifecycle(
3260 "delete",
3261 collection,
3262 key,
3263 crate::runtime::audit_log::Outcome::Success,
3264 "ok",
3265 Some(&entry),
3266 );
3267 return Ok(vault_write_result(
3268 raw_query,
3269 "vault_delete",
3270 "delete",
3271 collection,
3272 key,
3273 &entry,
3274 1,
3275 ));
3276 }
3277 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3278 let deleted = ops.delete(*model, collection, key)?;
3279
3280 let mut result = UnifiedResult::with_columns(vec![
3281 "ok".into(),
3282 "collection".into(),
3283 "key".into(),
3284 "deleted".into(),
3285 ]);
3286 let mut record = UnifiedRecord::new();
3287 record.set("ok", Value::Boolean(true));
3288 record.set("collection", Value::text(collection.clone()));
3289 record.set("key", Value::text(key.clone()));
3290 record.set("deleted", Value::Boolean(deleted));
3291 result.push(record);
3292
3293 Ok(RuntimeQueryResult {
3294 query: raw_query.to_string(),
3295 mode: reddb_rql::modes::QueryMode::Sql,
3296 statement: "kv_delete",
3297 engine: "kv",
3298 result,
3299 affected_rows: if deleted { 1 } else { 0 },
3300 statement_type: "delete",
3301 bookmark: None,
3302 notice: None,
3303 })
3304 }
3305 }
3306 }
3307
3308 pub fn vault_watch_events_since(
3309 &self,
3310 collection: &str,
3311 key: &str,
3312 since_lsn: u64,
3313 max_count: usize,
3314 ) -> Vec<crate::replication::cdc::KvWatchEvent> {
3315 self.kv_watch_events_since(collection, key, since_lsn, max_count)
3316 .into_iter()
3317 .filter(|event| {
3318 vault_watch_in_scope(event, current_tenant().as_deref())
3319 && self
3320 .check_vault_capability(
3321 "vault:read_metadata",
3322 &event.collection,
3323 &event.key,
3324 )
3325 .is_ok()
3326 })
3327 .map(vault_filter_watch_event)
3328 .collect()
3329 }
3330
3331 pub fn vault_watch_events_since_prefix(
3332 &self,
3333 collection: &str,
3334 prefix: &str,
3335 since_lsn: u64,
3336 max_count: usize,
3337 ) -> Vec<crate::replication::cdc::KvWatchEvent> {
3338 self.kv_watch_events_since_prefix(collection, prefix, since_lsn, max_count)
3339 .into_iter()
3340 .filter(|event| {
3341 vault_watch_in_scope(event, current_tenant().as_deref())
3342 && self
3343 .check_vault_capability(
3344 "vault:read_metadata",
3345 &event.collection,
3346 &event.key,
3347 )
3348 .is_ok()
3349 })
3350 .map(vault_filter_watch_event)
3351 .collect()
3352 }
3353
3354 fn check_kv_invalidate_policy(&self, collection: &str) -> RedDBResult<()> {
3355 let auth_store = match self.inner.auth_store.read().clone() {
3356 Some(store) => store,
3357 None => return Ok(()),
3358 };
3359 let (username, role) = match crate::runtime::impl_core::current_auth_identity() {
3360 Some(identity) => identity,
3361 None => return Ok(()),
3362 };
3363 if role < crate::auth::Role::Write {
3364 return Err(RedDBError::Query(format!(
3365 "principal=`{username}` role=`{role:?}` cannot invalidate KV tags"
3366 )));
3367 }
3368 if !auth_store.iam_authorization_enabled() {
3369 return Ok(());
3370 }
3371
3372 let tenant = crate::runtime::impl_core::current_tenant();
3373 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
3374 let mut resource =
3375 crate::auth::policies::ResourceRef::new("kv".to_string(), collection.to_string());
3376 if let Some(tenant) = tenant.clone() {
3377 resource = resource.with_tenant(tenant);
3378 }
3379 let ctx = crate::auth::policies::EvalContext {
3380 principal_tenant: tenant.clone(),
3381 current_tenant: tenant,
3382 peer_ip: None,
3383 mfa_present: false,
3384 now_ms: current_unix_ms(),
3385 principal_is_admin_role: role == crate::auth::Role::Admin,
3386 principal_is_platform_scoped: principal.tenant.is_none(),
3387 };
3388 if auth_store.check_policy_authz_with_role(
3389 &principal,
3390 "kv:invalidate",
3391 &resource,
3392 &ctx,
3393 role,
3394 ) {
3395 Ok(())
3396 } else {
3397 Err(RedDBError::Query(format!(
3398 "principal=`{username}` action=`kv:invalidate` resource=`kv:{collection}` denied by IAM policy"
3399 )))
3400 }
3401 }
3402}
3403
3404fn ttl_metadata(ttl_ms: Option<u64>) -> Option<Metadata> {
3405 let ttl_ms = ttl_ms?;
3406 Some(Metadata::with_fields(
3407 [(
3408 "_ttl_ms".to_string(),
3409 if ttl_ms <= i64::MAX as u64 {
3410 MetadataValue::Int(ttl_ms as i64)
3411 } else {
3412 MetadataValue::Timestamp(ttl_ms)
3413 },
3414 )]
3415 .into_iter()
3416 .collect(),
3417 ))
3418}
3419
3420fn vault_write_result(
3421 raw_query: &str,
3422 statement: &'static str,
3423 statement_type: &'static str,
3424 collection: &str,
3425 key: &str,
3426 entry: &VaultEntry,
3427 affected_rows: u64,
3428) -> RuntimeQueryResult {
3429 let mut result = UnifiedResult::with_columns(vec![
3430 "ok".into(),
3431 "collection".into(),
3432 "key".into(),
3433 "version".into(),
3434 "fingerprint".into(),
3435 "tombstone".into(),
3436 "op".into(),
3437 "id".into(),
3438 ]);
3439 let mut record = UnifiedRecord::new();
3440 record.set("ok", Value::Boolean(true));
3441 record.set("collection", Value::text(collection.to_string()));
3442 record.set("key", Value::text(key.to_string()));
3443 record.set("version", Value::Integer(entry.version));
3444 if entry.tombstone {
3445 record.set("fingerprint", Value::Null);
3446 } else {
3447 record.set("fingerprint", Value::text(vault_fingerprint(&entry.value)));
3448 }
3449 record.set("tombstone", Value::Boolean(entry.tombstone));
3450 record.set("op", Value::text(entry.op.clone()));
3451 record.set("id", Value::Integer(entry.id.raw() as i64));
3452 result.push(record);
3453 RuntimeQueryResult {
3454 query: raw_query.to_string(),
3455 mode: reddb_rql::modes::QueryMode::Sql,
3456 statement,
3457 engine: "vault",
3458 result,
3459 affected_rows,
3460 statement_type,
3461 bookmark: None,
3462 notice: None,
3463 }
3464}
3465
3466fn vault_history_result(collection: &str, key: &str, versions: &[VaultEntry]) -> UnifiedResult {
3467 let mut result = UnifiedResult::with_columns(vec![
3468 "collection".into(),
3469 "key".into(),
3470 "version".into(),
3471 "fingerprint".into(),
3472 "tags".into(),
3473 "created_at".into(),
3474 "updated_at".into(),
3475 "status".into(),
3476 "tombstone".into(),
3477 "op".into(),
3478 ]);
3479 for entry in versions {
3480 push_vault_metadata_record(&mut result, collection, key, entry);
3481 }
3482 result
3483}
3484
3485fn push_vault_metadata_record(
3486 result: &mut UnifiedResult,
3487 collection: &str,
3488 key: &str,
3489 entry: &VaultEntry,
3490) {
3491 let mut record = UnifiedRecord::new();
3492 record.set("collection", Value::text(collection.to_string()));
3493 record.set("key", Value::text(key.to_string()));
3494 record.set("version", Value::Integer(entry.version));
3495 if entry.tombstone {
3496 record.set("fingerprint", Value::Null);
3497 record.set("status", Value::text("deleted"));
3498 } else {
3499 record.set("fingerprint", Value::text(vault_fingerprint(&entry.value)));
3500 record.set("status", Value::text("sealed"));
3501 }
3502 record.set("tags", vault_tags_value(&entry.metadata));
3503 record.set("created_at", Value::TimestampMs(entry.created_at as i64));
3504 record.set("updated_at", Value::TimestampMs(entry.updated_at as i64));
3505 record.set("tombstone", Value::Boolean(entry.tombstone));
3506 record.set("op", Value::text(entry.op.clone()));
3507 result.push(record);
3508}
3509
3510fn vault_metadata_result(
3511 collection: &str,
3512 key: &str,
3513 entry: Option<&VaultEntry>,
3514 key_available: bool,
3515) -> UnifiedResult {
3516 let mut result = UnifiedResult::with_columns(vec![
3517 "collection".into(),
3518 "key".into(),
3519 "version".into(),
3520 "fingerprint".into(),
3521 "tags".into(),
3522 "created_at".into(),
3523 "updated_at".into(),
3524 "value".into(),
3525 "status".into(),
3526 "tombstone".into(),
3527 "op".into(),
3528 ]);
3529 let mut record = UnifiedRecord::new();
3530 record.set("collection", Value::text(collection.to_string()));
3531 record.set("key", Value::text(key.to_string()));
3532 match entry {
3533 Some(entry) => {
3534 record.set("version", Value::Integer(entry.version));
3535 if entry.tombstone {
3536 record.set("fingerprint", Value::Null);
3537 } else {
3538 record.set("fingerprint", Value::text(vault_fingerprint(&entry.value)));
3539 }
3540 record.set("tags", vault_tags_value(&entry.metadata));
3541 record.set("created_at", Value::TimestampMs(entry.created_at as i64));
3542 record.set("updated_at", Value::TimestampMs(entry.updated_at as i64));
3543 record.set("value", Value::text("***"));
3544 record.set(
3545 "status",
3546 Value::text(if entry.tombstone {
3547 "deleted"
3548 } else if key_available {
3549 "sealed"
3550 } else {
3551 "sealed_unavailable"
3552 }),
3553 );
3554 record.set("tombstone", Value::Boolean(entry.tombstone));
3555 record.set("op", Value::text(entry.op.clone()));
3556 }
3557 None => {
3558 record.set("version", Value::Null);
3559 record.set("fingerprint", Value::Null);
3560 record.set("tags", Value::Array(Vec::new()));
3561 record.set("created_at", Value::Null);
3562 record.set("updated_at", Value::Null);
3563 record.set("value", Value::text(""));
3564 record.set("status", Value::text("missing"));
3565 record.set("tombstone", Value::Boolean(false));
3566 record.set("op", Value::Null);
3567 }
3568 }
3569 result.push(record);
3570 result
3571}
3572
3573fn vault_fingerprint(value: &Value) -> String {
3574 match value {
3575 Value::Secret(payload) => crate::utils::to_hex(&crate::crypto::sha256::sha256(payload)),
3576 other => crate::utils::to_hex(&crate::crypto::sha256::sha256(&other.to_bytes())),
3577 }
3578}
3579
3580fn vault_entry_metadata_json(entry: &VaultEntry) -> crate::json::Value {
3581 let mut object = crate::json::Map::new();
3582 object.insert(
3583 "key".to_string(),
3584 crate::json::Value::String(entry.key.clone()),
3585 );
3586 object.insert(
3587 "version".to_string(),
3588 crate::json::Value::Number(entry.version as f64),
3589 );
3590 object.insert(
3591 "fingerprint".to_string(),
3592 if entry.tombstone {
3593 crate::json::Value::Null
3594 } else {
3595 crate::json::Value::String(vault_fingerprint(&entry.value))
3596 },
3597 );
3598 object.insert(
3599 "tenant".to_string(),
3600 entry
3601 .tenant
3602 .as_ref()
3603 .map(|tenant| crate::json::Value::String(tenant.clone()))
3604 .unwrap_or(crate::json::Value::Null),
3605 );
3606 object.insert("tags".to_string(), vault_tags_json(&entry.metadata));
3607 object.insert(
3608 "actor".to_string(),
3609 crate::json::Value::String(RedDBRuntime::current_vault_actor()),
3610 );
3611 object.insert(
3612 "sequence_id".to_string(),
3613 crate::json::Value::Number(entry.sequence_id as f64),
3614 );
3615 object.insert(
3616 "tombstone".to_string(),
3617 crate::json::Value::Bool(entry.tombstone),
3618 );
3619 object.insert(
3620 "op".to_string(),
3621 crate::json::Value::String(entry.op.clone()),
3622 );
3623 crate::json::Value::Object(object)
3624}
3625
3626fn vault_tags_json(metadata: &Metadata) -> crate::json::Value {
3627 match vault_tags_value(metadata) {
3628 Value::Array(values) => crate::json::Value::Array(
3629 values
3630 .into_iter()
3631 .filter_map(|value| match value {
3632 Value::Text(tag) => Some(crate::json::Value::String(tag.to_string())),
3633 _ => None,
3634 })
3635 .collect(),
3636 ),
3637 _ => crate::json::Value::Array(Vec::new()),
3638 }
3639}
3640
3641fn vault_tags_metadata(tags: &[String]) -> std::collections::HashMap<String, MetadataValue> {
3642 [(
3643 "tags".to_string(),
3644 MetadataValue::Array(
3645 tags.iter()
3646 .map(|tag| MetadataValue::String(tag.clone()))
3647 .collect(),
3648 ),
3649 )]
3650 .into_iter()
3651 .collect()
3652}
3653
3654fn vault_filter_watch_event(
3655 mut event: crate::replication::cdc::KvWatchEvent,
3656) -> crate::replication::cdc::KvWatchEvent {
3657 event.before = event.before.and_then(vault_metadata_json_only);
3658 event.after = event.after.and_then(vault_metadata_json_only);
3659 event
3660}
3661
3662fn vault_metadata_json_only(value: crate::json::Value) -> Option<crate::json::Value> {
3663 let object = value.as_object()?;
3664 let mut out = crate::json::Map::new();
3665 for field in [
3666 "key",
3667 "version",
3668 "fingerprint",
3669 "tags",
3670 "actor",
3671 "sequence_id",
3672 "tombstone",
3673 "op",
3674 ] {
3675 if let Some(value) = object.get(field) {
3676 out.insert(field.to_string(), value.clone());
3677 }
3678 }
3679 Some(crate::json::Value::Object(out))
3680}
3681
3682fn vault_tags_value(metadata: &Metadata) -> Value {
3683 match metadata.get("tags") {
3684 Some(MetadataValue::Array(values)) => Value::Array(
3685 values
3686 .iter()
3687 .filter_map(|value| match value {
3688 MetadataValue::String(tag) => Some(Value::text(tag.clone())),
3689 _ => None,
3690 })
3691 .collect(),
3692 ),
3693 Some(MetadataValue::String(tag)) if !tag.is_empty() => {
3694 Value::Array(vec![Value::text(tag.clone())])
3695 }
3696 _ => Value::Array(Vec::new()),
3697 }
3698}
3699
3700fn decode_vault_key(hex_key: &str) -> RedDBResult<[u8; 32]> {
3701 let bytes = hex::decode(hex_key)
3702 .map_err(|_| RedDBError::Query("vault sealed_unavailable: bad key material".to_string()))?;
3703 let key: [u8; 32] = bytes.try_into().map_err(|_| {
3704 RedDBError::Query("vault sealed_unavailable: bad key material length".to_string())
3705 })?;
3706 Ok(key)
3707}
3708
3709fn kv_tags_metadata(tags: &[String]) -> Option<(String, MetadataValue)> {
3710 if tags.is_empty() {
3711 return None;
3712 }
3713 let values = tags
3714 .iter()
3715 .map(|tag| MetadataValue::String(tag.clone()))
3716 .collect();
3717 Some(("_kv_tags".to_string(), MetadataValue::Array(values)))
3718}
3719
3720impl RedDBRuntime {
3721 pub(crate) fn rehydrate_kv_tags(&self) -> RedDBResult<()> {
3722 let db = self.db();
3723 let store = db.store();
3724 for contract in db.collection_contracts() {
3725 if contract.declared_model != CollectionModel::Kv {
3726 continue;
3727 }
3728 let Some(manager) = store.get_collection(&contract.name) else {
3729 continue;
3730 };
3731 let resolver = super::kv_read::KvReadResolver::for_keyed_model(
3732 self,
3733 CollectionModel::Kv,
3734 &contract.name,
3735 )?;
3736 for entity in manager.scan(None, |_| true) {
3737 let Some(reddb_types::Value::Text(key)) =
3738 entity.data.as_row().and_then(|row| row.get_field("key"))
3739 else {
3740 continue;
3741 };
3742 let Some(metadata) = manager.get_metadata(entity.id) else {
3743 continue;
3744 };
3745 let Some(MetadataValue::Array(tags)) = metadata.fields.get("_kv_tags") else {
3746 continue;
3747 };
3748 if resolver
3749 .record(key)?
3750 .is_none_or(|record| record.id != entity.id)
3751 {
3752 continue;
3753 }
3754 let tags: Vec<_> = tags
3755 .iter()
3756 .filter_map(|tag| match tag {
3757 MetadataValue::String(tag) => Some(tag.clone()),
3758 _ => None,
3759 })
3760 .collect();
3761 self.inner
3762 .kv_tag_index
3763 .replace(&contract.name, key, entity.id, &tags);
3764 }
3765 }
3766 Ok(())
3767 }
3768}
3769
3770fn kv_tags_value(tags: &[String]) -> Value {
3771 let json = crate::json::Value::Array(
3772 tags.iter()
3773 .map(|tag| crate::json::Value::String(tag.clone()))
3774 .collect(),
3775 );
3776 Value::Json(crate::json::to_vec(&json).unwrap_or_default())
3777}
3778
3779pub(super) fn kv_value_from_entity(entity: &crate::storage::UnifiedEntity) -> Option<Value> {
3780 if let crate::storage::EntityData::Row(ref row) = entity.data {
3781 return row.get_field("value").cloned();
3782 }
3783 None
3784}
3785
3786fn kv_entity_has_key(entity: &crate::storage::UnifiedEntity, key: &str) -> bool {
3788 if let crate::storage::EntityData::Row(ref row) = entity.data {
3789 if let Some(reddb_types::Value::Text(ref k)) = row.get_field("key") {
3790 return &**k == key;
3791 }
3792 }
3793 false
3794}
3795
3796fn insert_kv_json_path(root: &mut crate::json::Value, path: &str, value: crate::json::Value) {
3797 let segments: Vec<&str> = path
3798 .split('.')
3799 .filter(|segment| !segment.is_empty())
3800 .collect();
3801 insert_kv_json_segments(root, &segments, value);
3802}
3803
3804fn insert_kv_json_segments(
3805 root: &mut crate::json::Value,
3806 segments: &[&str],
3807 value: crate::json::Value,
3808) {
3809 if segments.is_empty() {
3810 *root = value;
3811 return;
3812 }
3813
3814 if !matches!(root, crate::json::Value::Object(_)) {
3815 *root = crate::json::Value::Object(crate::json::Map::new());
3816 }
3817
3818 let crate::json::Value::Object(map) = root else {
3819 return;
3820 };
3821 if segments.len() == 1 {
3822 map.insert(segments[0].to_string(), value);
3823 return;
3824 }
3825 let entry = map
3826 .entry(segments[0].to_string())
3827 .or_insert_with(|| crate::json::Value::Object(crate::json::Map::new()));
3828 insert_kv_json_segments(entry, &segments[1..], value);
3829}
3830
3831fn kv_list_json_result(
3832 raw_query: &str,
3833 collection: &str,
3834 prefix: &Option<String>,
3835 value: crate::json::Value,
3836) -> RuntimeQueryResult {
3837 let mut result =
3838 UnifiedResult::with_columns(vec!["collection".into(), "prefix".into(), "value".into()]);
3839 let mut record = UnifiedRecord::new();
3840 record.set("collection", Value::text(collection.to_string()));
3841 record.set(
3842 "prefix",
3843 prefix
3844 .as_ref()
3845 .map(|prefix| Value::text(prefix.clone()))
3846 .unwrap_or(Value::Null),
3847 );
3848 record.set("value", Value::Json(value.to_string_compact().into_bytes()));
3849 result.push(record);
3850 RuntimeQueryResult {
3851 query: raw_query.to_string(),
3852 mode: reddb_rql::modes::QueryMode::Sql,
3853 statement: "kv_list_json",
3854 engine: "kv",
3855 result,
3856 affected_rows: 0,
3857 statement_type: "select",
3858 bookmark: None,
3859 notice: None,
3860 }
3861}
3862
3863fn kv_collection_contract(name: &str) -> crate::physical::CollectionContract {
3864 let now = current_unix_ms();
3865 crate::physical::CollectionContract {
3866 name: name.to_string(),
3867 declared_model: crate::catalog::CollectionModel::Kv,
3868 schema_mode: crate::catalog::SchemaMode::Dynamic,
3869 origin: crate::physical::ContractOrigin::Implicit,
3870 version: 1,
3871 created_at_unix_ms: now,
3872 updated_at_unix_ms: now,
3873 default_ttl_ms: None,
3874 vector_dimension: None,
3875 vector_metric: None,
3876 context_index_fields: Vec::new(),
3877 declared_columns: Vec::new(),
3878 table_def: None,
3879 timestamps_enabled: false,
3880 context_index_enabled: false,
3881 metrics_raw_retention_ms: None,
3882 metrics_rollup_policies: Vec::new(),
3883 metrics_tenant_identity: None,
3884 metrics_namespace: None,
3885 append_only: false,
3886 subscriptions: Vec::new(),
3887 analytics_config: Vec::new(),
3888 session_key: None,
3889 session_gap_ms: None,
3890 retention_duration_ms: None,
3891 analytical_storage: None,
3892
3893 ai_policy: None,
3894 }
3895}
3896
3897fn current_unix_ms() -> u128 {
3898 std::time::SystemTime::now()
3899 .duration_since(std::time::UNIX_EPOCH)
3900 .unwrap_or_default()
3901 .as_millis()
3902}
3903
3904#[cfg(test)]
3905mod tests {
3906 use crate::api::RedDBOptions;
3907 use crate::application::ports::RuntimeEntityPort;
3908 use crate::auth::Role;
3909 use crate::catalog::CollectionModel;
3910 use crate::runtime::execution_context::{with_snapshot_bundle, SnapshotBundle};
3911 use crate::runtime::RedDBRuntime;
3912 use reddb_types::Value;
3913
3914 fn rt() -> RedDBRuntime {
3915 RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("in-memory runtime")
3916 }
3917
3918 #[test]
3919 fn limited_kv_lists_match_full_version_selection_across_segments() {
3920 use crate::storage::{EntityData, EntityId, RowData, UnifiedEntity};
3921 let runtime = rt();
3922 runtime
3923 .execute_query("CREATE KV bounded_keys")
3924 .expect("collection");
3925 let manager = runtime
3926 .db()
3927 .store()
3928 .get_collection("bounded_keys")
3929 .expect("manager");
3930 for (index, key) in ["z", "app.z", "a", "m", "a", "c", "app.a", "b"]
3931 .iter()
3932 .enumerate()
3933 {
3934 let mut entity = UnifiedEntity::table_row(
3935 EntityId::new(1_000_000 + index as u64),
3936 "bounded_keys",
3937 index as u64,
3938 vec![],
3939 );
3940 entity.data = EntityData::Row(RowData::with_names(
3941 vec![Value::text(*key), Value::Integer(index as i64)],
3942 vec!["key".into(), "value".into()],
3943 ));
3944 manager.insert(entity).expect("version");
3945 if index == 3 {
3946 manager.force_seal().expect("seal");
3947 }
3948 }
3949 for versioned in [false, true] {
3950 runtime
3951 .vcs_set_versioned("bounded_keys", versioned)
3952 .expect("versioning");
3953 let ops = super::KvAtomicOps::new(&runtime);
3954 for prefix in [None, Some("app."), Some("missing")] {
3955 let summarize = |entries: Vec<(String, UnifiedEntity)>| {
3956 entries
3957 .into_iter()
3958 .map(|(key, entity)| (key, entity.id))
3959 .collect::<Vec<_>>()
3960 };
3961 let expected = summarize(
3962 ops.latest_kv_entries("bounded_keys", prefix, usize::MAX)
3963 .expect("full view"),
3964 );
3965 for count in [0, 1, 3, 100] {
3966 let actual = summarize(
3967 ops.latest_kv_entries("bounded_keys", prefix, count)
3968 .expect("bounded view"),
3969 );
3970 assert_eq!(
3971 actual,
3972 expected.iter().take(count).cloned().collect::<Vec<_>>()
3973 );
3974 }
3975 }
3976 }
3977 let full = runtime
3978 .execute_query("KV LIST bounded_keys")
3979 .expect("SQL full");
3980 let page = runtime
3981 .execute_query("KV LIST bounded_keys LIMIT 2 OFFSET 1")
3982 .expect("SQL page");
3983 let keys = |rows: &[crate::storage::query::unified::UnifiedRecord]| {
3984 rows.iter()
3985 .map(|row| row.get("key").cloned())
3986 .collect::<Vec<_>>()
3987 };
3988 assert_eq!(keys(&page.result.records), keys(&full.result.records[1..3]));
3989 }
3990
3991 #[test]
3992 fn limited_kv_lists_do_not_copy_payloads_beyond_the_key_window() {
3993 use crate::storage::no_malloc_hot_paths::measure_allocations;
3994 use crate::storage::{EntityData, EntityId, RowData, UnifiedEntity};
3995 let runtime = rt();
3996 runtime
3997 .execute_query("CREATE KV payload_keys")
3998 .expect("collection");
3999 let manager = runtime
4000 .db()
4001 .store()
4002 .get_collection("payload_keys")
4003 .expect("manager");
4004 for id in 0..128 {
4005 let mut entity =
4006 UnifiedEntity::table_row(EntityId::new(1_000_000 + id), "payload_keys", id, vec![]);
4007 entity.data = EntityData::Row(RowData {
4008 columns: vec![],
4009 schema: None,
4010 named: Some(std::collections::HashMap::from([
4011 ("key".into(), Value::text(format!("key{id:04}"))),
4012 ("value".into(), Value::Blob(vec![7; 32 * 1024])),
4013 ])),
4014 });
4015 manager.insert(entity).expect("key");
4016 }
4017 let ops = super::KvAtomicOps::new(&runtime);
4018 let (full, all_allocations) = measure_allocations(|| {
4019 ops.latest_kv_entries("payload_keys", None, usize::MAX)
4020 .expect("full list")
4021 });
4022 let (bounded, allocations) = measure_allocations(|| {
4023 ops.latest_kv_entries("payload_keys", None, 3)
4024 .expect("bounded list")
4025 });
4026 assert_eq!(
4027 bounded
4028 .iter()
4029 .map(|(key, entity)| (key, entity.id))
4030 .collect::<Vec<_>>(),
4031 full.iter()
4032 .take(3)
4033 .map(|(key, entity)| (key, entity.id))
4034 .collect::<Vec<_>>()
4035 );
4036 assert!(
4037 allocations.peak_live_bytes < all_allocations.peak_live_bytes / 8,
4038 "{allocations:?} / {all_allocations:?}"
4039 );
4040 eprintln!(
4041 "KV list temporary peak bytes, full/3 keys: {} / {}",
4042 all_allocations.peak_live_bytes, allocations.peak_live_bytes
4043 );
4044 }
4045
4046 #[cfg(unix)]
4047 #[test]
4048 fn paged_replacements_batch_value_ttl_and_tags_across_cold_boots() {
4049 use crate::api::{DurabilityMode, RedDBOptions};
4050 use crate::storage::profile::StorageDeployPreset;
4051
4052 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
4053 let directory = tempfile::tempdir().expect("directory");
4054 let options = RedDBOptions::persistent(directory.path().join("keys.rdb"))
4055 .with_durability_mode(mode)
4056 .with_memory_budget(64 * 1024 * 1024)
4057 .with_storage_profile(StorageDeployPreset::PrimaryReplicaDev.selection())
4058 .expect("paged profile");
4059 for generation in 0..3 {
4060 let runtime = RedDBRuntime::with_options(options.clone()).expect("runtime");
4061 let ops = super::KvAtomicOps::new(&runtime);
4062 if generation > 0 {
4063 assert_eq!(
4064 ops.get(CollectionModel::Kv, "keys", "item")
4065 .expect("cold read"),
4066 Some(Value::Integer(generation - 1))
4067 );
4068 assert_eq!(
4069 ops.existing_ttl_ms("keys", "item").expect("TTL"),
4070 Some(600_000)
4071 );
4072 assert_eq!(
4073 runtime.inner.kv_tag_index.tags_for_key("keys", "item"),
4074 vec!["daily"]
4075 );
4076 }
4077 let batches = || {
4078 crate::storage::wal::WalReader::open(reddb_file::unified_wal_path(
4079 options.data_path.as_ref().expect("persistent path"),
4080 ))
4081 .expect("WAL")
4082 .iter()
4083 .filter(|record| {
4084 matches!(
4085 &record.as_ref().expect("record").1,
4086 crate::storage::wal::WalRecord::TxCommitBatch { .. }
4087 )
4088 })
4089 .count()
4090 };
4091 let before = batches();
4092 ops.set_with_tags(
4093 "keys",
4094 "item",
4095 Value::Integer(generation),
4096 Some(600_000),
4097 &["daily".into()],
4098 false,
4099 )
4100 .expect("replacement");
4101 if generation > 0 {
4102 assert_eq!(
4103 batches() - before,
4104 1,
4105 "value, deletion and metadata share one WAL batch"
4106 );
4107 }
4108 assert!(runtime.db().store().pager().is_some());
4109 assert!(super::super::kv_write::atomic_available(&runtime, "keys")
4110 .expect("atomic path"));
4111 assert_eq!(
4112 ops.get(CollectionModel::Kv, "keys", "item").expect("read"),
4113 Some(Value::Integer(generation))
4114 );
4115 runtime.checkpoint().expect("checkpoint");
4116 }
4117 }
4118 }
4119
4120 #[test]
4121 fn keyed_overwrite_and_metadata_share_one_durable_publication() {
4122 let directory = tempfile::tempdir().expect("directory");
4123 let path = directory.path().join("atomic.rdb");
4124 {
4125 let runtime = RedDBRuntime::with_options(
4126 RedDBOptions::persistent(&path)
4127 .with_durability_mode(crate::api::DurabilityMode::Strict),
4128 )
4129 .expect("runtime");
4130 let ops = super::KvAtomicOps::new(&runtime);
4131 ops.set(
4132 CollectionModel::Kv,
4133 "keys",
4134 "item",
4135 Value::Integer(1),
4136 None,
4137 false,
4138 )
4139 .expect("seed");
4140 let before = crate::storage::EmbeddedRdbArtifact::open(&path).expect("before");
4141 ops.set_with_tags(
4142 "keys",
4143 "item",
4144 Value::Integer(2),
4145 Some(600_000),
4146 &["daily".into()],
4147 false,
4148 )
4149 .expect("replace");
4150 let after = crate::storage::EmbeddedRdbArtifact::open(&path).expect("after");
4151 assert_eq!(
4152 after.selected_superblock.generation,
4153 before.selected_superblock.generation + 1
4154 );
4155 assert_eq!(
4156 runtime.db().store().get_collection("keys").unwrap().count(),
4157 1
4158 );
4159 }
4160 let runtime = RedDBRuntime::with_options(RedDBOptions::persistent(&path)).expect("reopen");
4161 let ops = super::KvAtomicOps::new(&runtime);
4162 assert_eq!(
4163 ops.get(CollectionModel::Kv, "keys", "item").expect("read"),
4164 Some(Value::Integer(2))
4165 );
4166 assert_eq!(
4167 ops.existing_ttl_ms("keys", "item").expect("TTL read"),
4168 Some(600_000)
4169 );
4170 assert_eq!(
4171 runtime.inner.kv_tag_index.tags_for_key("keys", "item"),
4172 vec!["daily".to_string()]
4173 );
4174 }
4175
4176 #[test]
4177 fn kv_get_projection_preserves_metadata_tags_and_missing_envelope() {
4178 let runtime = rt();
4179 let ops = super::KvAtomicOps::new(&runtime);
4180 let value = reddb_types::Value::Array(vec![reddb_types::Value::Integer(7)]);
4181 let (_, id) = ops
4182 .set_with_tags(
4183 "projected",
4184 "item",
4185 value.clone(),
4186 None,
4187 &["daily".into()],
4188 false,
4189 )
4190 .expect("seed");
4191 let db = runtime.db();
4192 let store = db.store();
4193 let manager = store.get_collection("projected").expect("collection");
4194 let entity = manager.get(id).expect("stored row");
4195 manager.force_seal().expect("seal row");
4196 let result = runtime.execute_query("KV GET projected.item").expect("GET");
4197 let record = &result.result.records[0];
4198 assert_eq!(record.iter_fields().count(), 9);
4199 for (field, expected) in [
4200 ("rid", reddb_types::Value::UnsignedInteger(id.raw())),
4201 (
4202 "created_at",
4203 reddb_types::Value::UnsignedInteger(entity.created_at),
4204 ),
4205 (
4206 "updated_at",
4207 reddb_types::Value::UnsignedInteger(entity.updated_at),
4208 ),
4209 ("collection", reddb_types::Value::text("projected")),
4210 ("kind", reddb_types::Value::text("kv")),
4211 ("tenant", reddb_types::Value::Null),
4212 ("key", reddb_types::Value::text("item")),
4213 ("value", value),
4214 ("tags", super::kv_tags_value(&["daily".into()])),
4215 ] {
4216 assert_eq!(record.get(field), Some(&expected), "{field}");
4217 }
4218 let missing = runtime
4219 .execute_query("KV GET projected.absent")
4220 .expect("missing GET");
4221 let record = &missing.result.records[0];
4222 for field in ["rid", "created_at", "updated_at", "value", "tenant"] {
4223 assert_eq!(
4224 record.get(field),
4225 Some(&reddb_types::Value::Null),
4226 "{field}"
4227 );
4228 }
4229 assert_eq!(record.get("tags"), Some(&super::kv_tags_value(&[])));
4230 }
4231
4232 #[test]
4233 fn kv_key_lookup_rebuilds_after_reopen() {
4234 let directory = tempfile::tempdir().expect("temporary database");
4235 let path = directory.path().join("keys.rdb");
4236 {
4237 let runtime =
4238 RedDBRuntime::with_options(RedDBOptions::persistent(&path)).expect("open");
4239 for statement in [
4240 "KV PUT indexed.alpha = 'first'",
4241 "KV PUT indexed.beta = 'second'",
4242 "KV PUT indexed.alpha = 'updated'",
4243 "KV DELETE indexed.beta",
4244 "KV PUT indexed.gamma = 'third'",
4245 ] {
4246 runtime.execute_query(statement).expect("KV mutation");
4247 }
4248 }
4249 let runtime = RedDBRuntime::with_options(RedDBOptions::persistent(&path)).expect("reopen");
4250 let ops = super::KvAtomicOps::new(&runtime);
4251 for (key, expected) in [
4252 ("alpha", Some("updated")),
4253 ("beta", None),
4254 ("gamma", Some("third")),
4255 ("absent", None),
4256 ] {
4257 assert_eq!(
4258 ops.get(CollectionModel::Kv, "indexed", key).expect("read"),
4259 expected.map(reddb_types::Value::text)
4260 );
4261 }
4262 ops.set(
4263 CollectionModel::Kv,
4264 "indexed",
4265 "beta",
4266 reddb_types::Value::text("reused"),
4267 None,
4268 false,
4269 )
4270 .expect("reuse deleted key");
4271 assert_eq!(
4272 ops.get(CollectionModel::Kv, "indexed", "beta")
4273 .expect("read reused key"),
4274 Some(reddb_types::Value::text("reused"))
4275 );
4276 }
4277
4278 #[test]
4279 fn concurrent_put_if_not_exists_has_exactly_one_winner() {
4280 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 red.config.kv.watch.idle_timeout_ms = 1")
5132 .unwrap();
5133
5134 let mut stream = r.kv_watch_subscribe("kv_default", "idle", None);
5135 assert_eq!(r.stats().kv.watch_streams_active, 1);
5136 std::thread::sleep(std::time::Duration::from_millis(5));
5137
5138 assert!(stream.poll_next().is_none());
5139 assert_eq!(r.stats().kv.watch_streams_active, 0);
5140 }
5141
5142 #[test]
5143 fn watch_stream_does_not_emit_rolled_back_put() {
5144 let r = rt();
5145 let mut stream = r.kv_watch_subscribe("kv_default", "rollback_key", None);
5146
5147 r.execute_query("BEGIN").unwrap();
5148 r.execute_query("KV PUT rollback_key = 'dirty'").unwrap();
5149 r.execute_query("ROLLBACK").unwrap();
5150
5151 assert!(stream.poll_next().is_none());
5152 }
5153}
5154
5155fn vault_value_aad(collection: &str, tenant: Option<&str>) -> String {
5156 match tenant {
5157 Some(tenant) => format!("reddb.vault.{collection}.tenant/{}", hex::encode(tenant)),
5158 None => format!("reddb.vault.{collection}"),
5159 }
5160}
5161
5162fn vault_watch_in_scope(
5163 event: &crate::replication::cdc::KvWatchEvent,
5164 tenant: Option<&str>,
5165) -> bool {
5166 let stored_tenant = event
5167 .after
5168 .as_ref()
5169 .or(event.before.as_ref())
5170 .and_then(|metadata| metadata.get("tenant"))
5171 .and_then(|value| value.as_str());
5172 stored_tenant == tenant
5173}