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