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