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