1use crate::application::ports::RuntimeEntityPort;
7use crate::storage::unified::{Metadata, MetadataValue};
8
9use super::impl_core::{current_auth_identity, current_connection_id, current_tenant};
10use super::*;
11
12pub const KV_DEFAULT_COLLECTION: &str = "kv_default";
14
15fn vault_master_key_ref(collection: &str) -> String {
16 format!("red.vault.{collection}.master_key")
17}
18
19fn keyed_model_name(model: crate::catalog::CollectionModel) -> &'static str {
20 match model {
21 crate::catalog::CollectionModel::Kv => "kv",
22 crate::catalog::CollectionModel::Vault => "vault",
23 crate::catalog::CollectionModel::Config => "config",
24 _ => "collection",
25 }
26}
27
28#[derive(Debug, Clone)]
29struct VaultEntry {
30 id: crate::storage::EntityId,
31 key: String,
32 value: reddb_types::Value,
33 metadata: Metadata,
34 created_at: u64,
35 updated_at: u64,
36 sequence_id: u64,
37 version: i64,
38 tombstone: bool,
39 op: String,
40}
41
42impl super::keyed_spine::KeyedVersion for VaultEntry {
43 fn key(&self) -> &str {
44 &self.key
45 }
46
47 fn version(&self) -> i64 {
48 self.version
49 }
50}
51
52impl VaultEntry {
53 fn from_keyed_row(
54 version: super::keyed_spine::KeyedRowVersion,
55 metadata: Metadata,
56 created_at: u64,
57 updated_at: u64,
58 sequence_id: u64,
59 ) -> Self {
60 Self {
61 id: version.id,
62 key: version.key,
63 value: version.value,
64 metadata,
65 created_at,
66 updated_at,
67 sequence_id,
68 version: version.version,
69 tombstone: version.tombstone,
70 op: version.op,
71 }
72 }
73}
74
75pub struct KvAtomicOps<'a> {
80 runtime: &'a RedDBRuntime,
81}
82
83impl<'a> KvAtomicOps<'a> {
84 pub fn new(runtime: &'a RedDBRuntime) -> Self {
85 Self { runtime }
86 }
87
88 pub fn set(
92 &self,
93 model: crate::catalog::CollectionModel,
94 collection: &str,
95 key: &str,
96 value: reddb_types::Value,
97 ttl_ms: Option<u64>,
98 if_not_exists: bool,
99 ) -> RedDBResult<(bool, crate::storage::EntityId)> {
100 self.set_with_tags_for_model(model, collection, key, value, ttl_ms, &[], if_not_exists)
101 }
102
103 pub fn set_with_tags(
104 &self,
105 collection: &str,
106 key: &str,
107 value: reddb_types::Value,
108 ttl_ms: Option<u64>,
109 tags: &[String],
110 if_not_exists: bool,
111 ) -> RedDBResult<(bool, crate::storage::EntityId)> {
112 self.set_with_tags_for_model(
113 crate::catalog::CollectionModel::Kv,
114 collection,
115 key,
116 value,
117 ttl_ms,
118 tags,
119 if_not_exists,
120 )
121 }
122
123 fn set_with_tags_for_model(
124 &self,
125 model: crate::catalog::CollectionModel,
126 collection: &str,
127 key: &str,
128 value: reddb_types::Value,
129 ttl_ms: Option<u64>,
130 tags: &[String],
131 if_not_exists: bool,
132 ) -> RedDBResult<(bool, crate::storage::EntityId)> {
133 self.set_with_tags_and_metadata_for_model(
134 model,
135 collection,
136 key,
137 value,
138 ttl_ms,
139 tags,
140 if_not_exists,
141 Vec::new(),
142 )
143 }
144
145 pub fn set_with_tags_and_metadata(
146 &self,
147 collection: &str,
148 key: &str,
149 value: reddb_types::Value,
150 ttl_ms: Option<u64>,
151 tags: &[String],
152 if_not_exists: bool,
153 metadata: Vec<(String, MetadataValue)>,
154 ) -> RedDBResult<(bool, crate::storage::EntityId)> {
155 self.set_with_tags_and_metadata_for_model(
156 crate::catalog::CollectionModel::Kv,
157 collection,
158 key,
159 value,
160 ttl_ms,
161 tags,
162 if_not_exists,
163 metadata,
164 )
165 }
166
167 fn set_with_tags_and_metadata_for_model(
168 &self,
169 model: crate::catalog::CollectionModel,
170 collection: &str,
171 key: &str,
172 value: reddb_types::Value,
173 ttl_ms: Option<u64>,
174 tags: &[String],
175 if_not_exists: bool,
176 mut metadata: Vec<(String, MetadataValue)>,
177 ) -> RedDBResult<(bool, crate::storage::EntityId)> {
178 let rmw_lock =
187 if_not_exists.then(|| self.runtime.inner.rmw_locks.lock_for(collection, key));
188 let _rmw_guard = rmw_lock.as_ref().map(|lock| lock.lock());
189
190 self.ensure_keyed_collection(model, collection)?;
191
192 if model == crate::catalog::CollectionModel::Vault {
193 let latest = self.get_vault_entry(collection, key)?;
194 let was_present = latest.as_ref().is_some_and(|entry| !entry.tombstone);
195 if if_not_exists && was_present {
196 return Ok((false, latest.expect("checked present").id));
197 }
198 let entry = self.append_vault_version(collection, key, value, "put", false, tags)?;
199 self.runtime.record_kv_watch_event(
200 if latest.is_some() {
201 crate::replication::cdc::ChangeOperation::Update
202 } else {
203 crate::replication::cdc::ChangeOperation::Insert
204 },
205 collection,
206 key,
207 entry.id.raw(),
208 latest.as_ref().map(vault_entry_metadata_json),
209 Some(vault_entry_metadata_json(&entry)),
210 );
211 return Ok((!was_present, entry.id));
212 }
213
214 let existing = self.get_entry(model, collection, key)?;
215 let was_present = existing.is_some();
216
217 if if_not_exists && was_present {
218 let (_, id) = existing.unwrap();
219 self.runtime.inner.kv_stats.incr_puts();
220 return Ok((false, id));
221 }
222
223 let before = existing
224 .as_ref()
225 .map(|(value, _)| reddb_types::Value::to_json(value));
226 let op = if was_present {
227 crate::replication::cdc::ChangeOperation::Update
228 } else {
229 crate::replication::cdc::ChangeOperation::Insert
230 };
231 let after = Some(reddb_types::Value::to_json(&value));
232
233 let versioned = self.is_versioned_collection(model, collection);
239 let prior_versioned_entity = if versioned && was_present {
244 self.get_entity(model, collection, key)?
245 } else {
246 None
247 };
248 if was_present && !versioned {
249 self.delete(model, collection, key)?;
250 }
251
252 if let Some(ttl_metadata) = ttl_metadata(ttl_ms) {
253 metadata.extend(ttl_metadata.fields);
254 }
255 if let Some(tags_metadata) = kv_tags_metadata(tags) {
256 metadata.push(tags_metadata);
257 }
258
259 let output = self
260 .runtime
261 .create_kv(crate::application::entity::CreateKvInput {
262 collection: collection.to_string(),
263 key: key.to_string(),
264 value,
265 metadata,
266 })?;
267
268 if versioned {
279 let version_xid = match self.runtime.current_xid() {
280 Some(xid) => xid,
281 None => {
282 let mgr = self.runtime.snapshot_manager();
283 let xid = mgr.begin();
284 mgr.commit(xid);
285 xid
286 }
287 };
288 if let Some(new_entity) = output.entity.clone() {
289 self.restamp_xmin(collection, new_entity, version_xid)?;
290 }
291 if let Some(prior) = prior_versioned_entity {
292 let previous_xmax = prior.xmax;
302 let old_id = prior.id;
303 self.tombstone_version(collection, prior, version_xid)?;
304 if self.runtime.current_xid().is_some() {
305 self.runtime.record_pending_versioned_update(
306 crate::runtime::impl_core::current_connection_id(),
307 collection,
308 old_id,
309 output.id,
310 version_xid,
311 previous_xmax,
312 );
313 }
314 }
315 }
316 if model == crate::catalog::CollectionModel::Kv {
317 self.runtime
318 .inner
319 .kv_tag_index
320 .replace(collection, key, output.id, tags);
321 }
322
323 if model == crate::catalog::CollectionModel::Kv {
324 self.runtime
325 .record_kv_watch_event(op, collection, key, output.id.raw(), before, after);
326 }
327
328 if model == crate::catalog::CollectionModel::Kv {
329 self.runtime.inner.kv_stats.incr_puts();
330 }
331 Ok((!was_present, output.id))
332 }
333
334 pub fn get(
336 &self,
337 model: crate::catalog::CollectionModel,
338 collection: &str,
339 key: &str,
340 ) -> RedDBResult<Option<reddb_types::Value>> {
341 let result = self.get_entry(model, collection, key)?;
342 if model == crate::catalog::CollectionModel::Kv {
343 self.runtime.inner.kv_stats.incr_gets();
344 }
345 Ok(result.map(|(v, _)| v))
346 }
347
348 pub fn delete(
350 &self,
351 model: crate::catalog::CollectionModel,
352 collection: &str,
353 key: &str,
354 ) -> RedDBResult<bool> {
355 self.ensure_declared_model(model, collection)?;
356
357 if self.is_versioned_collection(model, collection) {
361 let Some(entity) = self.get_entity(model, collection, key)? else {
362 return Ok(false);
363 };
364 let id = entity.id;
365 let value = kv_value_from_entity(&entity);
366 let xid = self.runtime.current_xid().unwrap_or_else(|| {
367 let mgr = self.runtime.snapshot_manager();
368 let xid = mgr.begin();
369 mgr.commit(xid);
370 xid
371 });
372 let previous_xmax = entity.xmax;
381 self.tombstone_version(collection, entity, xid)?;
382 if self.runtime.current_xid().is_some() {
383 self.runtime.record_pending_tombstone(
384 crate::runtime::impl_core::current_connection_id(),
385 collection,
386 id,
387 xid,
388 previous_xmax,
389 );
390 }
391 self.runtime.inner.kv_tag_index.remove(collection, key);
392 self.runtime.record_kv_watch_event(
393 crate::replication::cdc::ChangeOperation::Delete,
394 collection,
395 key,
396 id.raw(),
397 value.as_ref().map(reddb_types::Value::to_json),
398 None,
399 );
400 self.runtime.inner.kv_stats.incr_deletes();
401 return Ok(true);
402 }
403
404 let found = self.get_entry(model, collection, key)?;
405 if let Some((value, id)) = found {
406 let store = self.runtime.inner.db.store();
407 let deleted = store
408 .delete(collection, id)
409 .map_err(|err| RedDBError::Internal(err.to_string()))?;
410 if deleted {
411 store.context_index().remove_entity(id);
412 if model == crate::catalog::CollectionModel::Kv {
413 self.runtime.inner.kv_tag_index.remove(collection, key);
414 self.runtime.record_kv_watch_event(
415 crate::replication::cdc::ChangeOperation::Delete,
416 collection,
417 key,
418 id.raw(),
419 Some(reddb_types::Value::to_json(&value)),
420 None,
421 );
422 self.runtime.inner.kv_stats.incr_deletes();
423 }
424 }
425 Ok(deleted)
426 } else {
427 Ok(false)
428 }
429 }
430
431 fn tombstone_version(
437 &self,
438 collection: &str,
439 mut entity: crate::storage::UnifiedEntity,
440 xid: crate::storage::transaction::snapshot::Xid,
441 ) -> RedDBResult<()> {
442 let store = self.runtime.inner.db.store();
443 let Some(manager) = store.get_collection(collection) else {
444 return Ok(());
445 };
446 let id = entity.id;
447 entity.set_xmax(xid);
448 manager
449 .update(entity.clone())
450 .map_err(|err| RedDBError::Internal(format!("{err:?}")))?;
451 store
452 .persist_entities_to_pager(collection, std::slice::from_ref(&entity))
453 .map_err(|err| RedDBError::Internal(err.to_string()))?;
454 store.context_index().remove_entity(id);
457 Ok(())
458 }
459
460 fn restamp_xmin(
466 &self,
467 collection: &str,
468 mut entity: crate::storage::UnifiedEntity,
469 xid: crate::storage::transaction::snapshot::Xid,
470 ) -> RedDBResult<()> {
471 let store = self.runtime.inner.db.store();
472 let Some(manager) = store.get_collection(collection) else {
473 return Ok(());
474 };
475 entity.set_xmin(xid);
476 manager
477 .update(entity.clone())
478 .map_err(|err| RedDBError::Internal(format!("{err:?}")))?;
479 store
480 .persist_entities_to_pager(collection, std::slice::from_ref(&entity))
481 .map_err(|err| RedDBError::Internal(err.to_string()))?;
482 Ok(())
483 }
484
485 fn existing_ttl_ms(&self, collection: &str, key: &str) -> Option<u64> {
491 let entity = self
492 .get_entity(crate::catalog::CollectionModel::Kv, collection, key)
493 .ok()
494 .flatten()?;
495 let metadata = self
496 .runtime
497 .inner
498 .db
499 .store()
500 .get_metadata(collection, entity.id)?;
501 match metadata.get("_ttl_ms")? {
502 MetadataValue::Int(ms) if *ms >= 0 => Some(*ms as u64),
503 MetadataValue::Timestamp(ms) => Some(*ms),
504 _ => None,
505 }
506 }
507
508 pub fn incr(
513 &self,
514 model: crate::catalog::CollectionModel,
515 collection: &str,
516 key: &str,
517 by: i64,
518 ttl_ms: Option<u64>,
519 ) -> RedDBResult<i64> {
520 if model == crate::catalog::CollectionModel::Vault {
521 return Err(RedDBError::InvalidOperation(
522 "VAULT INCR is not supported for sealed secrets".to_string(),
523 ));
524 }
525 let rmw_lock = self.runtime.inner.rmw_locks.lock_for(collection, key);
526 let _rmw_guard = rmw_lock.lock();
527 self.ensure_kv_collection(collection)?;
528 let existing = self.runtime.get_kv(collection, key)?;
529 let current: i64 = match existing.as_ref() {
530 None => 0,
531 Some((reddb_types::Value::Integer(n), _)) => *n,
532 Some((reddb_types::Value::Float(f), _)) => *f as i64,
533 Some((other, _)) => {
534 return Err(RedDBError::Internal(format!(
535 "INCR on non-integer value: {:?}",
536 other
537 )));
538 }
539 };
540
541 let next = current
542 .checked_add(by)
543 .ok_or_else(|| RedDBError::Internal(format!("INCR overflow: {current} + {by}")))?;
544
545 let carried_tags = self
552 .runtime
553 .inner
554 .kv_tag_index
555 .tags_for_key(collection, key);
556 let carried_ttl_ms = if ttl_ms.is_none() {
557 self.existing_ttl_ms(collection, key)
558 } else {
559 None
560 };
561
562 if existing.is_some() {
564 self.runtime.delete_kv(collection, key)?;
565 }
566
567 let mut meta_vec: Vec<(String, crate::storage::unified::MetadataValue)> =
568 ttl_metadata(ttl_ms.or(carried_ttl_ms))
569 .map(|m| m.fields.into_iter().collect())
570 .unwrap_or_default();
571 if let Some(tags_metadata) = kv_tags_metadata(&carried_tags) {
572 meta_vec.push(tags_metadata);
573 }
574
575 let output = self
576 .runtime
577 .create_kv(crate::application::entity::CreateKvInput {
578 collection: collection.to_string(),
579 key: key.to_string(),
580 value: reddb_types::Value::Integer(next),
581 metadata: meta_vec,
582 })?;
583 self.runtime
584 .inner
585 .kv_tag_index
586 .replace(collection, key, output.id, &carried_tags);
587
588 self.runtime.record_kv_watch_event(
589 if existing.is_some() {
590 crate::replication::cdc::ChangeOperation::Update
591 } else {
592 crate::replication::cdc::ChangeOperation::Insert
593 },
594 collection,
595 key,
596 output.id.raw(),
597 existing
598 .as_ref()
599 .map(|(value, _)| reddb_types::Value::to_json(value)),
600 Some(reddb_types::Value::to_json(&reddb_types::Value::Integer(
601 next,
602 ))),
603 );
604
605 self.runtime.inner.kv_stats.incr_incrs();
606 Ok(next)
607 }
608
609 pub fn cas(
617 &self,
618 model: crate::catalog::CollectionModel,
619 collection: &str,
620 key: &str,
621 expected: Option<&reddb_types::Value>,
622 new_value: reddb_types::Value,
623 ttl_ms: Option<u64>,
624 ) -> RedDBResult<(bool, Option<reddb_types::Value>)> {
625 if model == crate::catalog::CollectionModel::Vault {
626 return Err(RedDBError::InvalidOperation(
627 "VAULT CAS is not supported for sealed secrets".to_string(),
628 ));
629 }
630 let rmw_lock = self.runtime.inner.rmw_locks.lock_for(collection, key);
631 let _rmw_guard = rmw_lock.lock();
632 self.ensure_kv_collection(collection)?;
633 let current = self.runtime.get_kv(collection, key)?.map(|(v, _)| v);
634
635 let matches = match (¤t, expected) {
636 (None, None) => true,
637 (Some(cur), Some(exp)) => cur == exp,
638 _ => false,
639 };
640
641 if !matches {
642 self.runtime.inner.kv_stats.incr_cas_conflict();
643 return Ok((false, current));
644 }
645
646 let carried_tags = self
651 .runtime
652 .inner
653 .kv_tag_index
654 .tags_for_key(collection, key);
655 let carried_ttl_ms = if ttl_ms.is_none() {
656 self.existing_ttl_ms(collection, key)
657 } else {
658 None
659 };
660
661 if current.is_some() {
663 self.runtime.delete_kv(collection, key)?;
664 }
665
666 let mut meta_vec: Vec<(String, crate::storage::unified::MetadataValue)> =
667 ttl_metadata(ttl_ms.or(carried_ttl_ms))
668 .map(|m| m.fields.into_iter().collect())
669 .unwrap_or_default();
670 if let Some(tags_metadata) = kv_tags_metadata(&carried_tags) {
671 meta_vec.push(tags_metadata);
672 }
673
674 let output = self
675 .runtime
676 .create_kv(crate::application::entity::CreateKvInput {
677 collection: collection.to_string(),
678 key: key.to_string(),
679 value: new_value.clone(),
680 metadata: meta_vec,
681 })?;
682 self.runtime
683 .inner
684 .kv_tag_index
685 .replace(collection, key, output.id, &carried_tags);
686
687 self.runtime.record_kv_watch_event(
688 if current.is_some() {
689 crate::replication::cdc::ChangeOperation::Update
690 } else {
691 crate::replication::cdc::ChangeOperation::Insert
692 },
693 collection,
694 key,
695 output.id.raw(),
696 current.as_ref().map(reddb_types::Value::to_json),
697 Some(reddb_types::Value::to_json(&new_value)),
698 );
699
700 self.runtime.inner.kv_stats.incr_cas_success();
701 Ok((true, current))
702 }
703
704 pub fn invalidate_tags(&self, collection: &str, tags: &[String]) -> RedDBResult<usize> {
705 self.runtime
706 .check_write(crate::runtime::write_gate::WriteKind::Dml)?;
707 self.runtime.check_kv_invalidate_policy(collection)?;
708 self.ensure_kv_collection(collection)?;
709 let entries = self
710 .runtime
711 .inner
712 .kv_tag_index
713 .entries_for_tags(collection, tags);
714 if entries.is_empty() {
715 return Ok(0);
716 }
717
718 let store = self.runtime.inner.db.store();
719 let mut removed = 0usize;
720 for (key, id) in entries {
721 let before = store
722 .get(collection, id)
723 .and_then(|entity| kv_value_from_entity(&entity));
724 let deleted = store
725 .delete(collection, id)
726 .map_err(|err| RedDBError::Internal(err.to_string()))?;
727 if deleted {
728 store.context_index().remove_entity(id);
729 self.runtime.inner.kv_tag_index.remove(collection, &key);
730 self.runtime.record_kv_watch_event(
731 crate::replication::cdc::ChangeOperation::Delete,
732 collection,
733 &key,
734 id.raw(),
735 before.as_ref().map(reddb_types::Value::to_json),
736 None,
737 );
738 removed += 1;
739 }
740 }
741 if removed > 0 {
742 self.runtime.inner.kv_stats.incr_deletes();
743 }
744 Ok(removed)
745 }
746
747 pub fn tags_for_key(&self, collection: &str, key: &str) -> Vec<String> {
748 self.runtime
749 .inner
750 .kv_tag_index
751 .tags_for_key(collection, key)
752 }
753
754 fn ensure_kv_collection(&self, collection: &str) -> RedDBResult<()> {
756 self.ensure_keyed_collection(crate::catalog::CollectionModel::Kv, collection)
757 }
758
759 fn ensure_keyed_collection(
760 &self,
761 model: crate::catalog::CollectionModel,
762 collection: &str,
763 ) -> RedDBResult<()> {
764 let store = self.runtime.inner.db.store();
765 if store.get_collection(collection).is_some() {
766 return self.ensure_declared_model(model, collection);
767 }
768 if model != crate::catalog::CollectionModel::Kv {
769 return Err(RedDBError::NotFound(format!(
770 "{} collection '{collection}' does not exist",
771 keyed_model_name(model)
772 )));
773 }
774 let auto_create = self
776 .runtime
777 .config_bool("red.config.kv.default_collection", true);
778 if !auto_create {
779 return Err(RedDBError::NotFound(format!(
780 "kv collection '{collection}' does not exist and auto-create is disabled \
781 (red.config.kv.default_collection = false)"
782 )));
783 }
784 store
785 .create_collection(collection)
786 .map_err(|err| RedDBError::Internal(err.to_string()))?;
787 self.runtime
788 .inner
789 .db
790 .save_collection_contract(kv_collection_contract(collection))
791 .map_err(|err| RedDBError::Internal(err.to_string()))?;
792 Ok(())
793 }
794
795 fn get_entry(
796 &self,
797 model: crate::catalog::CollectionModel,
798 collection: &str,
799 key: &str,
800 ) -> RedDBResult<Option<(reddb_types::Value, crate::storage::EntityId)>> {
801 let Some(entity) = self.get_entity(model, collection, key)? else {
802 return Ok(None);
803 };
804 Ok(kv_value_from_entity(&entity).map(|value| (value, entity.id)))
805 }
806
807 fn is_versioned_collection(
814 &self,
815 model: crate::catalog::CollectionModel,
816 collection: &str,
817 ) -> bool {
818 if model != crate::catalog::CollectionModel::Kv {
821 return false;
822 }
823 self.runtime.vcs_is_versioned(collection).unwrap_or(false)
824 }
825
826 fn get_entity(
827 &self,
828 model: crate::catalog::CollectionModel,
829 collection: &str,
830 key: &str,
831 ) -> RedDBResult<Option<crate::storage::UnifiedEntity>> {
832 self.ensure_declared_model(model, collection)?;
833 let store = self.runtime.inner.db.store();
834 let Some(manager) = store.get_collection(collection) else {
835 return Ok(None);
836 };
837
838 if self.is_versioned_collection(model, collection) {
843 let snapshot = crate::runtime::impl_core::capture_current_snapshot();
844 let mut best: Option<crate::storage::UnifiedEntity> = None;
845 for entity in manager.scan(snapshot.as_ref(), |entity| kv_entity_has_key(entity, key)) {
846 let better = match &best {
847 Some(current) => entity.xmin >= current.xmin,
848 None => true,
849 };
850 if better {
851 best = Some(entity);
852 }
853 }
854 return Ok(best);
855 }
856
857 let entities = manager.query_all(|_| true);
858 for entity in entities {
859 if kv_entity_has_key(&entity, key) {
860 return Ok(Some(entity));
861 }
862 }
863 Ok(None)
864 }
865
866 fn latest_kv_entries(
867 &self,
868 collection: &str,
869 prefix: Option<&str>,
870 ) -> RedDBResult<Vec<(String, crate::storage::UnifiedEntity)>> {
871 self.ensure_declared_model(crate::catalog::CollectionModel::Kv, collection)?;
872 let store = self.runtime.inner.db.store();
873 let Some(manager) = store.get_collection(collection) else {
874 return Ok(Vec::new());
875 };
876 let versioned =
881 self.is_versioned_collection(crate::catalog::CollectionModel::Kv, collection);
882 let snapshot = crate::runtime::impl_core::capture_current_snapshot();
883 let entities = if versioned {
884 manager.scan(snapshot.as_ref(), |_| true)
885 } else {
886 manager.query_all(|_| true)
887 };
888
889 let mut entries: std::collections::BTreeMap<String, crate::storage::UnifiedEntity> =
890 std::collections::BTreeMap::new();
891 for entity in entities {
892 let key = match &entity.data {
893 crate::storage::EntityData::Row(row) => row
894 .named
895 .as_ref()
896 .and_then(|named| named.get("key"))
897 .and_then(|value| match value {
898 reddb_types::Value::Text(key) => Some(key.to_string()),
899 _ => None,
900 }),
901 _ => None,
902 };
903 let Some(key) = key else {
904 continue;
905 };
906 if prefix.is_some_and(|prefix| !key.starts_with(prefix)) {
907 continue;
908 }
909 let should_replace = match entries.get(&key) {
910 Some(existing) if versioned => entity.xmin >= existing.xmin,
911 Some(existing) => entity.id.raw() >= existing.id.raw(),
912 None => true,
913 };
914 if should_replace {
915 entries.insert(key, entity);
916 }
917 }
918 Ok(entries.into_iter().collect())
919 }
920
921 fn get_vault_entry(&self, collection: &str, key: &str) -> RedDBResult<Option<VaultEntry>> {
922 self.vault_versions(collection, key)
923 .map(super::keyed_spine::latest_version)
924 }
925
926 fn get_vault_entry_version(
927 &self,
928 collection: &str,
929 key: &str,
930 version: i64,
931 ) -> RedDBResult<Option<VaultEntry>> {
932 Ok(self
933 .vault_versions(collection, key)?
934 .into_iter()
935 .find(|entry| entry.version == version))
936 }
937
938 fn vault_versions(&self, collection: &str, key: &str) -> RedDBResult<Vec<VaultEntry>> {
939 self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
940 let store = self.runtime.inner.db.store();
941 let Some(manager) = store.get_collection(collection) else {
942 return Ok(Vec::new());
943 };
944 let entities = manager.query_all(|_| true);
945 let mut versions = Vec::new();
946 for entity in entities {
947 let crate::storage::EntityData::Row(ref row) = entity.data else {
948 continue;
949 };
950 let Some(version) =
951 super::keyed_spine::row_version(entity.id, row, entity.sequence_id as i64)
952 else {
953 continue;
954 };
955 if version.key != key {
956 continue;
957 }
958 let metadata = manager.get_metadata(entity.id).unwrap_or_default();
959 versions.push(VaultEntry::from_keyed_row(
960 version,
961 metadata,
962 entity.created_at,
963 entity.updated_at,
964 entity.sequence_id,
965 ));
966 }
967 Ok(versions)
968 }
969
970 fn latest_vault_entries(
971 &self,
972 collection: &str,
973 prefix: Option<&str>,
974 ) -> RedDBResult<Vec<VaultEntry>> {
975 self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
976 let store = self.runtime.inner.db.store();
977 let Some(manager) = store.get_collection(collection) else {
978 return Ok(Vec::new());
979 };
980 let mut versions = Vec::new();
981 for entity in manager.query_all(|_| true) {
982 let crate::storage::EntityData::Row(ref row) = entity.data else {
983 continue;
984 };
985 let Some(version) =
986 super::keyed_spine::row_version(entity.id, row, entity.sequence_id as i64)
987 else {
988 continue;
989 };
990 let metadata = manager.get_metadata(entity.id).unwrap_or_default();
991 let entry = VaultEntry::from_keyed_row(
992 version,
993 metadata,
994 entity.created_at,
995 entity.updated_at,
996 entity.sequence_id,
997 );
998 versions.push(entry);
999 }
1000 Ok(super::keyed_spine::latest_versions(versions, prefix))
1001 }
1002
1003 fn append_vault_version(
1004 &self,
1005 collection: &str,
1006 key: &str,
1007 value: reddb_types::Value,
1008 op: &str,
1009 tombstone: bool,
1010 tags: &[String],
1011 ) -> RedDBResult<VaultEntry> {
1012 self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
1013 let version = self
1014 .get_vault_entry(collection, key)?
1015 .map(|entry| entry.version)
1016 .unwrap_or(0)
1017 + 1;
1018 let stored_value = if tombstone {
1019 reddb_types::Value::Null
1020 } else {
1021 self.runtime.seal_vault_value(collection, value)?
1022 };
1023 let now = current_unix_ms() as i64;
1024 let fields = vec![
1025 ("key".to_string(), reddb_types::Value::text(key.to_string())),
1026 ("value".to_string(), stored_value),
1027 ("version".to_string(), reddb_types::Value::Integer(version)),
1028 (
1029 "tombstone".to_string(),
1030 reddb_types::Value::Boolean(tombstone),
1031 ),
1032 ("op".to_string(), reddb_types::Value::text(op.to_string())),
1033 (
1034 "created_at_ms".to_string(),
1035 reddb_types::Value::Integer(now),
1036 ),
1037 ];
1038 let mut row = crate::storage::RowData::new(Vec::new());
1039 row.named = Some(fields.into_iter().collect());
1040 let entity = crate::storage::UnifiedEntity::new(
1041 crate::storage::EntityId::new(0),
1042 crate::storage::EntityKind::TableRow {
1043 table: std::sync::Arc::from(collection),
1044 row_id: 0,
1045 },
1046 crate::storage::EntityData::Row(row),
1047 );
1048 let id = self
1049 .runtime
1050 .inner
1051 .db
1052 .store()
1053 .insert(collection, entity)
1054 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1055 if !tags.is_empty() {
1056 self.runtime
1057 .inner
1058 .db
1059 .store()
1060 .set_metadata(
1061 collection,
1062 id,
1063 Metadata::with_fields(vault_tags_metadata(tags)),
1064 )
1065 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1066 self.runtime
1067 .inner
1068 .kv_tag_index
1069 .replace(collection, key, id, tags);
1070 }
1071 self.get_vault_entry_version(collection, key, version)?
1072 .ok_or_else(|| RedDBError::Internal(format!("vault version {id} was not readable")))
1073 }
1074
1075 fn purge_vault_versions(&self, collection: &str, key: &str) -> RedDBResult<usize> {
1076 self.ensure_declared_model(crate::catalog::CollectionModel::Vault, collection)?;
1077 let versions = self.vault_versions(collection, key)?;
1078 let store = self.runtime.inner.db.store();
1079 let mut purged = 0usize;
1080 for entry in versions {
1081 if store
1082 .delete(collection, entry.id)
1083 .map_err(|err| RedDBError::Internal(err.to_string()))?
1084 {
1085 store.context_index().remove_entity(entry.id);
1086 purged += 1;
1087 }
1088 }
1089 Ok(purged)
1090 }
1091
1092 fn ensure_declared_model(
1093 &self,
1094 model: crate::catalog::CollectionModel,
1095 collection: &str,
1096 ) -> RedDBResult<()> {
1097 let Some(contract) = self.runtime.inner.db.collection_contract(collection) else {
1098 return Ok(());
1099 };
1100 if contract.declared_model == model
1101 || contract.declared_model == crate::catalog::CollectionModel::Mixed
1102 {
1103 return Ok(());
1104 }
1105 Err(RedDBError::InvalidOperation(format!(
1106 "collection '{}' is declared as '{}' and does not allow '{}' operations",
1107 collection,
1108 keyed_model_name(contract.declared_model),
1109 keyed_model_name(model)
1110 )))
1111 }
1112}
1113
1114impl RedDBRuntime {
1115 pub(crate) fn seal_vault_value(
1116 &self,
1117 collection: &str,
1118 value: reddb_types::Value,
1119 ) -> RedDBResult<reddb_types::Value> {
1120 let key = self.vault_encryption_key(collection)?;
1121 let plaintext = value.to_bytes();
1122 let nonce_bytes = crate::auth::store::random_bytes(12);
1123 let mut nonce = [0u8; 12];
1124 nonce.copy_from_slice(&nonce_bytes[..12]);
1125 let aad = format!("reddb.vault.{collection}");
1126 let ciphertext =
1127 crate::crypto::aes_gcm::aes256_gcm_encrypt(&key, &nonce, aad.as_bytes(), &plaintext);
1128 let mut payload = Vec::with_capacity(12 + ciphertext.len());
1129 payload.extend_from_slice(&nonce);
1130 payload.extend_from_slice(&ciphertext);
1131 Ok(reddb_types::Value::Secret(payload))
1132 }
1133
1134 fn vault_key_available(&self, collection: &str) -> bool {
1135 self.vault_encryption_key(collection).is_ok()
1136 }
1137
1138 fn vault_encryption_key(&self, collection: &str) -> RedDBResult<[u8; 32]> {
1139 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1140 RedDBError::Query("vault sealed_unavailable: no key provider is configured".to_string())
1141 })?;
1142 if !auth_store.is_vault_backed() {
1143 return Err(RedDBError::Query(
1144 "vault sealed_unavailable: key provider is sealed".to_string(),
1145 ));
1146 }
1147
1148 if let Some(hex_key) = auth_store.vault_kv_get(&vault_master_key_ref(collection)) {
1149 return decode_vault_key(&hex_key);
1150 }
1151 auth_store.vault_secret_key().ok_or_else(|| {
1152 RedDBError::Query("vault sealed_unavailable: cluster vault key is missing".to_string())
1153 })
1154 }
1155
1156 fn unseal_vault_value(
1157 &self,
1158 collection: &str,
1159 sealed: &reddb_types::Value,
1160 ) -> RedDBResult<reddb_types::Value> {
1161 let reddb_types::Value::Secret(payload) = sealed else {
1162 return Err(RedDBError::Query(
1163 "vault unseal failed: stored value is not sealed".to_string(),
1164 ));
1165 };
1166 if payload.len() < 12 {
1167 return Err(RedDBError::Query(
1168 "vault unseal failed: sealed payload is truncated".to_string(),
1169 ));
1170 }
1171 let key = self.vault_encryption_key(collection)?;
1172 let mut nonce = [0u8; 12];
1173 nonce.copy_from_slice(&payload[..12]);
1174 let aad = format!("reddb.vault.{collection}");
1175 let plaintext = crate::crypto::aes_gcm::aes256_gcm_decrypt(
1176 &key,
1177 &nonce,
1178 aad.as_bytes(),
1179 &payload[12..],
1180 )
1181 .map_err(|_| RedDBError::Query("vault unseal failed: decryption failed".to_string()))?;
1182 let (value, consumed) = reddb_types::Value::from_bytes(&plaintext).map_err(|err| {
1183 RedDBError::Query(format!("vault unseal failed: bad plaintext value: {err}"))
1184 })?;
1185 if consumed != plaintext.len() {
1186 return Err(RedDBError::Query(
1187 "vault unseal failed: trailing plaintext bytes".to_string(),
1188 ));
1189 }
1190 Ok(value)
1191 }
1192
1193 pub(crate) fn peek_vault_unsealed(
1199 &self,
1200 collection: &str,
1201 key: &str,
1202 ) -> RedDBResult<Option<reddb_types::Value>> {
1203 let ops = KvAtomicOps::new(self);
1204 let Some(entry) = ops.get_vault_entry(collection, key)? else {
1205 return Ok(None);
1206 };
1207 if entry.tombstone {
1208 return Ok(None);
1209 }
1210 Ok(self.unseal_vault_value(collection, &entry.value).ok())
1211 }
1212
1213 fn vault_target_resource(collection: &str, key: &str) -> String {
1214 if collection == "red.vault" {
1215 return format!("red.vault/{}", key.to_ascii_lowercase());
1216 }
1217 format!("{collection}.{key}")
1218 }
1219
1220 fn current_vault_actor() -> String {
1221 current_auth_identity()
1222 .map(|(principal, _)| principal)
1223 .unwrap_or_else(|| "anonymous".to_string())
1224 }
1225
1226 fn vault_request_id() -> String {
1227 let conn_id = current_connection_id();
1228 if conn_id == 0 {
1229 "embedded".to_string()
1230 } else {
1231 format!("conn-{conn_id}")
1232 }
1233 }
1234
1235 fn check_vault_capability(
1236 &self,
1237 action: &str,
1238 collection: &str,
1239 key: &str,
1240 ) -> Result<(), String> {
1241 let Some(auth_store) = self.inner.auth_store.read().clone() else {
1242 return Ok(());
1243 };
1244 if !auth_store.iam_authorization_enabled() {
1245 return Ok(());
1246 }
1247 let Some((principal, role)) = current_auth_identity() else {
1248 return Err(
1249 "IAM authorization is enabled; vault capability check requires an authenticated principal"
1250 .to_string(),
1251 );
1252 };
1253 let tenant = current_tenant();
1254 let principal_id = crate::auth::UserId::from_parts(tenant.as_deref(), &principal);
1255 let mut resource = crate::auth::policies::ResourceRef::new(
1256 "vault",
1257 Self::vault_target_resource(collection, key),
1258 );
1259 if let Some(ref tenant) = tenant {
1260 resource = resource.with_tenant(tenant.clone());
1261 }
1262 let ctx = crate::auth::policies::EvalContext {
1263 principal_tenant: tenant.clone(),
1264 current_tenant: tenant,
1265 peer_ip: None,
1266 mfa_present: false,
1267 now_ms: crate::utils::now_unix_millis() as u128,
1268 principal_is_admin_role: role == crate::auth::Role::Admin,
1269 principal_is_platform_scoped: principal_id.tenant.is_none(),
1270 };
1271 if auth_store.check_policy_authz_with_role(&principal_id, action, &resource, &ctx, role) {
1272 Ok(())
1273 } else {
1274 Err(format!(
1275 "principal=`{}` action=`{}` resource=`vault:{}` denied by IAM policy",
1276 principal,
1277 action,
1278 Self::vault_target_resource(collection, key)
1279 ))
1280 }
1281 }
1282
1283 fn check_system_vault_capability(
1284 &self,
1285 action: &str,
1286 collection: &str,
1287 key: &str,
1288 ) -> Result<(), String> {
1289 if collection != "red.vault" {
1290 return Ok(());
1291 }
1292 self.check_vault_capability(action, collection, key)
1293 }
1294
1295 fn audit_vault_unseal(
1296 &self,
1297 collection: &str,
1298 key: &str,
1299 outcome: crate::runtime::audit_log::Outcome,
1300 reason: &str,
1301 entry: Option<&VaultEntry>,
1302 ) {
1303 let actor = Self::current_vault_actor();
1304 let request_id = Self::vault_request_id();
1305 let mut builder = crate::runtime::audit_log::AuditEvent::builder("vault/unseal")
1306 .principal(actor.clone())
1307 .source(crate::runtime::audit_log::AuditAuthSource::Password)
1308 .resource(format!(
1309 "vault:{}",
1310 Self::vault_target_resource(collection, key)
1311 ))
1312 .outcome(outcome)
1313 .correlation_id(request_id.clone())
1314 .fields([
1315 crate::runtime::audit_log::AuditFieldEscaper::field("actor", actor),
1316 crate::runtime::audit_log::AuditFieldEscaper::field("collection", collection),
1317 crate::runtime::audit_log::AuditFieldEscaper::field("key", key),
1318 crate::runtime::audit_log::AuditFieldEscaper::field(
1319 "target",
1320 Self::vault_target_resource(collection, key),
1321 ),
1322 crate::runtime::audit_log::AuditFieldEscaper::field("reason", reason),
1323 crate::runtime::audit_log::AuditFieldEscaper::field("request_id", request_id),
1324 crate::runtime::audit_log::AuditFieldEscaper::field(
1325 "connection_id",
1326 current_connection_id(),
1327 ),
1328 ]);
1329 if let Some(tenant) = current_tenant() {
1330 builder = builder.tenant(tenant);
1331 }
1332 if let Some(entry) = entry {
1333 builder = builder.fields([
1334 crate::runtime::audit_log::AuditFieldEscaper::field("entity_id", entry.id.raw()),
1335 crate::runtime::audit_log::AuditFieldEscaper::field(
1336 "sequence_id",
1337 entry.sequence_id,
1338 ),
1339 ]);
1340 }
1341 self.audit_log().record_event(builder.build());
1342 }
1343
1344 fn audit_vault_lifecycle(
1345 &self,
1346 operation: &str,
1347 collection: &str,
1348 key: &str,
1349 outcome: crate::runtime::audit_log::Outcome,
1350 reason: &str,
1351 entry: Option<&VaultEntry>,
1352 ) {
1353 let actor = Self::current_vault_actor();
1354 let request_id = Self::vault_request_id();
1355 let mut builder =
1356 crate::runtime::audit_log::AuditEvent::builder(format!("vault/{operation}"))
1357 .principal(actor.clone())
1358 .source(crate::runtime::audit_log::AuditAuthSource::Password)
1359 .resource(format!(
1360 "vault:{}",
1361 Self::vault_target_resource(collection, key)
1362 ))
1363 .outcome(outcome)
1364 .correlation_id(request_id.clone())
1365 .fields([
1366 crate::runtime::audit_log::AuditFieldEscaper::field("actor", actor),
1367 crate::runtime::audit_log::AuditFieldEscaper::field("collection", collection),
1368 crate::runtime::audit_log::AuditFieldEscaper::field("key", key),
1369 crate::runtime::audit_log::AuditFieldEscaper::field(
1370 "target",
1371 Self::vault_target_resource(collection, key),
1372 ),
1373 crate::runtime::audit_log::AuditFieldEscaper::field("reason", reason),
1374 crate::runtime::audit_log::AuditFieldEscaper::field("request_id", request_id),
1375 crate::runtime::audit_log::AuditFieldEscaper::field(
1376 "connection_id",
1377 current_connection_id(),
1378 ),
1379 ]);
1380 if let Some(tenant) = current_tenant() {
1381 builder = builder.tenant(tenant);
1382 }
1383 if let Some(entry) = entry {
1384 builder = builder.fields([
1385 crate::runtime::audit_log::AuditFieldEscaper::field("entity_id", entry.id.raw()),
1386 crate::runtime::audit_log::AuditFieldEscaper::field("version", entry.version),
1387 crate::runtime::audit_log::AuditFieldEscaper::field(
1388 "sequence_id",
1389 entry.sequence_id,
1390 ),
1391 ]);
1392 }
1393 self.audit_log().record_event(builder.build());
1394 }
1395
1396 fn emit_vault_control_event(
1397 &self,
1398 kind: crate::runtime::control_events::EventKind,
1399 outcome: crate::runtime::control_events::Outcome,
1400 action: &'static str,
1401 collection: &str,
1402 key: &str,
1403 reason: &str,
1404 entry: Option<&VaultEntry>,
1405 extra_fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
1406 ) -> RedDBResult<()> {
1407 use crate::runtime::control_events::{
1408 ActorRef, ControlEvent, ControlEventCtx, ControlEventLedger, Sensitivity,
1409 };
1410 use std::borrow::Cow;
1411
1412 let tenant = current_tenant();
1413 let principal = current_auth_identity().map(|(principal, _)| principal);
1414 let actor_user = principal
1415 .as_ref()
1416 .map(|principal| crate::auth::UserId::from_parts(tenant.as_deref(), principal));
1417 let request_id = Self::vault_request_id();
1418 let actor = actor_user
1419 .as_ref()
1420 .map(ActorRef::User)
1421 .unwrap_or(ActorRef::Anonymous);
1422 let ctx = ControlEventCtx {
1423 actor,
1424 scope: tenant.as_ref().map(|scope| Cow::Borrowed(scope.as_str())),
1425 request_id: Some(Cow::Borrowed(request_id.as_str())),
1426 trace_id: None,
1427 };
1428
1429 let target = Self::vault_target_resource(collection, key);
1430 let mut fields = std::collections::HashMap::new();
1431 fields.insert("path".to_string(), Sensitivity::raw(target.clone()));
1432 fields.insert("collection".to_string(), Sensitivity::raw(collection));
1433 fields.insert("key".to_string(), Sensitivity::raw(key));
1434 fields.insert(
1435 "connection_id".to_string(),
1436 Sensitivity::raw(current_connection_id().to_string()),
1437 );
1438 if let Some(entry) = entry {
1439 fields.insert(
1440 "entity_id".to_string(),
1441 Sensitivity::raw(entry.id.raw().to_string()),
1442 );
1443 fields.insert(
1444 "sequence_id".to_string(),
1445 Sensitivity::raw(entry.sequence_id.to_string()),
1446 );
1447 fields.insert(
1448 "version".to_string(),
1449 Sensitivity::raw(entry.version.to_string()),
1450 );
1451 fields.insert("op".to_string(), Sensitivity::raw(entry.op.clone()));
1452 fields.insert(
1453 "tombstone".to_string(),
1454 Sensitivity::raw(entry.tombstone.to_string()),
1455 );
1456 if !entry.tombstone {
1457 fields.insert(
1458 "fingerprint".to_string(),
1459 Sensitivity::raw(vault_fingerprint(&entry.value)),
1460 );
1461 }
1462 fields.insert(
1463 "tags".to_string(),
1464 Sensitivity::raw(format!("{:?}", vault_tags_value(&entry.metadata))),
1465 );
1466 }
1467 for (key, value) in extra_fields {
1468 fields.insert(key, value);
1469 }
1470
1471 let event = ControlEvent {
1472 kind,
1473 outcome,
1474 action: Cow::Borrowed(action),
1475 resource: Some(format!("vault:{target}")),
1476 reason: Some(reason.to_string()),
1477 matched_policy_id: None,
1478 fields,
1479 };
1480 let ledger = self.inner.control_event_ledger.read();
1481 match ledger.emit(&ctx, event) {
1482 Ok(_) => Ok(()),
1483 Err(err) if self.inner.control_event_config.require_persistence() => {
1484 Err(RedDBError::Internal(err.to_string()))
1485 }
1486 Err(_) => Ok(()),
1487 }
1488 }
1489
1490 pub(crate) fn resolve_vault_secret_value(
1491 &self,
1492 collection: &str,
1493 key: &str,
1494 ) -> RedDBResult<Value> {
1495 let ops = KvAtomicOps::new(self);
1496 let entry = ops.get_vault_entry(collection, key)?;
1497 if let Err(reason) = self.check_vault_capability("vault:read", collection, key) {
1498 self.audit_vault_unseal(
1499 collection,
1500 key,
1501 crate::runtime::audit_log::Outcome::Denied,
1502 &reason,
1503 entry.as_ref(),
1504 );
1505 self.emit_vault_control_event(
1506 crate::runtime::control_events::EventKind::VaultRead,
1507 crate::runtime::control_events::Outcome::Denied,
1508 "vault:read",
1509 collection,
1510 key,
1511 &reason,
1512 entry.as_ref(),
1513 Vec::new(),
1514 )?;
1515 return Err(RedDBError::Query(reason));
1516 }
1517 let Some(entry) = entry else {
1518 let reason = "not_found";
1519 self.audit_vault_unseal(
1520 collection,
1521 key,
1522 crate::runtime::audit_log::Outcome::Denied,
1523 reason,
1524 None,
1525 );
1526 self.emit_vault_control_event(
1527 crate::runtime::control_events::EventKind::VaultRead,
1528 crate::runtime::control_events::Outcome::Denied,
1529 "vault:read",
1530 collection,
1531 key,
1532 reason,
1533 None,
1534 Vec::new(),
1535 )?;
1536 return Err(RedDBError::NotFound(format!(
1537 "vault secret '{}.{}' not found",
1538 collection, key
1539 )));
1540 };
1541 if entry.tombstone {
1542 let reason = "deleted";
1543 self.audit_vault_unseal(
1544 collection,
1545 key,
1546 crate::runtime::audit_log::Outcome::Denied,
1547 reason,
1548 Some(&entry),
1549 );
1550 self.emit_vault_control_event(
1551 crate::runtime::control_events::EventKind::VaultRead,
1552 crate::runtime::control_events::Outcome::Denied,
1553 "vault:read",
1554 collection,
1555 key,
1556 reason,
1557 Some(&entry),
1558 Vec::new(),
1559 )?;
1560 return Err(RedDBError::NotFound(format!(
1561 "vault secret '{}.{}' is deleted",
1562 collection, key
1563 )));
1564 }
1565 match self.unseal_vault_value(collection, &entry.value) {
1566 Ok(value) => {
1567 self.audit_vault_unseal(
1568 collection,
1569 key,
1570 crate::runtime::audit_log::Outcome::Success,
1571 "ok",
1572 Some(&entry),
1573 );
1574 self.emit_vault_control_event(
1575 crate::runtime::control_events::EventKind::VaultRead,
1576 crate::runtime::control_events::Outcome::Allowed,
1577 "vault:read",
1578 collection,
1579 key,
1580 "ok",
1581 Some(&entry),
1582 Vec::new(),
1583 )?;
1584 Ok(value)
1585 }
1586 Err(err) => {
1587 let reason = err.to_string();
1588 self.audit_vault_unseal(
1589 collection,
1590 key,
1591 crate::runtime::audit_log::Outcome::Error,
1592 &reason,
1593 Some(&entry),
1594 );
1595 self.emit_vault_control_event(
1596 crate::runtime::control_events::EventKind::VaultRead,
1597 crate::runtime::control_events::Outcome::Error,
1598 "vault:read",
1599 collection,
1600 key,
1601 &reason,
1602 Some(&entry),
1603 Vec::new(),
1604 )?;
1605 Err(err)
1606 }
1607 }
1608 }
1609
1610 pub fn execute_kv_command(
1612 &self,
1613 raw_query: &str,
1614 cmd: &reddb_rql::ast::KvCommand,
1615 ) -> RedDBResult<RuntimeQueryResult> {
1616 use reddb_rql::ast::KvCommand;
1617
1618 let ops = KvAtomicOps::new(self);
1619
1620 match cmd {
1621 KvCommand::Put {
1622 model,
1623 collection,
1624 key,
1625 value,
1626 ttl_ms,
1627 tags,
1628 if_not_exists,
1629 } => {
1630 if *model == crate::catalog::CollectionModel::Vault {
1631 self.check_system_vault_capability("vault:write", collection, key)
1632 .map_err(RedDBError::Query)?;
1633 }
1634 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1635 let (created, id) = ops.set_with_tags_for_model(
1636 *model,
1637 collection,
1638 key,
1639 value.clone(),
1640 *ttl_ms,
1641 tags,
1642 *if_not_exists,
1643 )?;
1644
1645 let mut result = UnifiedResult::with_columns(vec![
1646 "ok".into(),
1647 "collection".into(),
1648 "key".into(),
1649 "id".into(),
1650 "created".into(),
1651 "tags".into(),
1652 ]);
1653 let mut record = UnifiedRecord::new();
1654 record.set("ok", Value::Boolean(true));
1655 record.set("collection", Value::text(collection.clone()));
1656 record.set("key", Value::text(key.clone()));
1657 record.set("id", Value::Integer(id.raw() as i64));
1658 record.set("created", Value::Boolean(created));
1659 record.set("tags", kv_tags_value(tags));
1660 result.push(record);
1661
1662 Ok(RuntimeQueryResult {
1663 query: raw_query.to_string(),
1664 mode: reddb_rql::modes::QueryMode::Sql,
1665 statement: if *model == crate::catalog::CollectionModel::Vault {
1666 "vault_put"
1667 } else {
1668 "kv_put"
1669 },
1670 engine: if *model == crate::catalog::CollectionModel::Vault {
1671 "vault"
1672 } else {
1673 "kv"
1674 },
1675 result,
1676 affected_rows: 1,
1677 statement_type: if created { "insert" } else { "update" },
1678 bookmark: None,
1679 notice: None,
1680 })
1681 }
1682 KvCommand::InvalidateTags { collection, tags } => {
1683 let invalidated = ops.invalidate_tags(collection, tags)?;
1684
1685 let mut result = UnifiedResult::with_columns(vec![
1686 "ok".into(),
1687 "collection".into(),
1688 "invalidated".into(),
1689 "tags".into(),
1690 ]);
1691 let mut record = UnifiedRecord::new();
1692 record.set("ok", Value::Boolean(true));
1693 record.set("collection", Value::text(collection.clone()));
1694 record.set("invalidated", Value::Integer(invalidated as i64));
1695 record.set("tags", kv_tags_value(tags));
1696 result.push(record);
1697
1698 Ok(RuntimeQueryResult {
1699 query: raw_query.to_string(),
1700 mode: reddb_rql::modes::QueryMode::Sql,
1701 statement: "kv_invalidate_tags",
1702 engine: "kv",
1703 result,
1704 affected_rows: invalidated as u64,
1705 statement_type: "delete",
1706 bookmark: None,
1707 notice: None,
1708 })
1709 }
1710
1711 KvCommand::Rotate {
1712 collection,
1713 key,
1714 value,
1715 tags,
1716 } => {
1717 self.check_system_vault_capability("vault:write", collection, key)
1718 .map_err(RedDBError::Query)?;
1719 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1720 let entry = ops.append_vault_version(
1721 collection,
1722 key,
1723 value.clone(),
1724 "rotate",
1725 false,
1726 tags,
1727 )?;
1728 self.record_kv_watch_event(
1729 crate::replication::cdc::ChangeOperation::Update,
1730 collection,
1731 key,
1732 entry.id.raw(),
1733 None,
1734 Some(vault_entry_metadata_json(&entry)),
1735 );
1736 self.audit_vault_lifecycle(
1737 "rotate",
1738 collection,
1739 key,
1740 crate::runtime::audit_log::Outcome::Success,
1741 "ok",
1742 Some(&entry),
1743 );
1744 self.emit_vault_control_event(
1745 crate::runtime::control_events::EventKind::VaultRotate,
1746 crate::runtime::control_events::Outcome::Allowed,
1747 "vault:rotate",
1748 collection,
1749 key,
1750 "ok",
1751 Some(&entry),
1752 Vec::new(),
1753 )?;
1754 Ok(vault_write_result(
1755 raw_query,
1756 "vault_rotate",
1757 "update",
1758 collection,
1759 key,
1760 &entry,
1761 1,
1762 ))
1763 }
1764
1765 KvCommand::List {
1766 model,
1767 collection,
1768 prefix,
1769 limit,
1770 offset,
1771 as_json,
1772 } => {
1773 if *model == crate::catalog::CollectionModel::Vault {
1774 if *as_json {
1775 return Err(RedDBError::Query(
1776 "LIST VAULT AS JSON is not supported; vault list only exposes metadata"
1777 .to_string(),
1778 ));
1779 }
1780 let mut entries = ops.latest_vault_entries(collection, prefix.as_deref())?;
1781 entries.sort_by(|left, right| left.key.cmp(&right.key));
1782 let mut result = UnifiedResult::with_columns(vec![
1783 "collection".into(),
1784 "key".into(),
1785 "version".into(),
1786 "fingerprint".into(),
1787 "tags".into(),
1788 "created_at".into(),
1789 "updated_at".into(),
1790 "status".into(),
1791 "tombstone".into(),
1792 "op".into(),
1793 ]);
1794 let mut visible = Vec::new();
1795 for entry in entries {
1796 match self.check_vault_capability(
1797 "vault:read_metadata",
1798 collection,
1799 &entry.key,
1800 ) {
1801 Ok(()) => {
1802 self.emit_vault_control_event(
1803 crate::runtime::control_events::EventKind::VaultMetadataRead,
1804 crate::runtime::control_events::Outcome::Allowed,
1805 "vault:read_metadata",
1806 collection,
1807 &entry.key,
1808 "ok",
1809 Some(&entry),
1810 Vec::new(),
1811 )?;
1812 visible.push(entry);
1813 }
1814 Err(reason) => {
1815 self.emit_vault_control_event(
1816 crate::runtime::control_events::EventKind::VaultMetadataRead,
1817 crate::runtime::control_events::Outcome::Denied,
1818 "vault:read_metadata",
1819 collection,
1820 &entry.key,
1821 &reason,
1822 Some(&entry),
1823 Vec::new(),
1824 )?;
1825 }
1826 }
1827 }
1828 for entry in visible
1829 .into_iter()
1830 .skip(*offset)
1831 .take(limit.unwrap_or(usize::MAX))
1832 {
1833 push_vault_metadata_record(&mut result, collection, &entry.key, &entry);
1834 }
1835 Ok(RuntimeQueryResult {
1836 query: raw_query.to_string(),
1837 mode: reddb_rql::modes::QueryMode::Sql,
1838 statement: "vault_list",
1839 engine: "vault",
1840 result,
1841 affected_rows: 0,
1842 statement_type: "select",
1843 bookmark: None,
1844 notice: None,
1845 })
1846 } else {
1847 let mut result = UnifiedResult::with_columns(vec![
1848 "rid".into(),
1849 "collection".into(),
1850 "kind".into(),
1851 "tenant".into(),
1852 "created_at".into(),
1853 "updated_at".into(),
1854 "key".into(),
1855 "value".into(),
1856 "tags".into(),
1857 ]);
1858 let entries = ops.latest_kv_entries(collection, prefix.as_deref())?;
1859 if *as_json {
1860 let mut tree = crate::json::Value::Object(crate::json::Map::new());
1861 for (key, entity) in entries
1862 .into_iter()
1863 .skip(*offset)
1864 .take(limit.unwrap_or(usize::MAX))
1865 {
1866 let Some(value) = kv_value_from_entity(&entity) else {
1867 continue;
1868 };
1869 let relative = match prefix {
1870 Some(pfx) if key == *pfx => "",
1871 Some(pfx) => key
1872 .strip_prefix(pfx.as_str())
1873 .map(|tail| tail.strip_prefix('.').unwrap_or(tail))
1874 .unwrap_or(key.as_str()),
1875 None => key.as_str(),
1876 };
1877 insert_kv_json_path(
1878 &mut tree,
1879 relative,
1880 reddb_types::Value::to_json(&value),
1881 );
1882 }
1883 Ok(kv_list_json_result(raw_query, collection, prefix, tree))
1884 } else {
1885 for (key, entity) in entries
1886 .into_iter()
1887 .skip(*offset)
1888 .take(limit.unwrap_or(usize::MAX))
1889 {
1890 let mut record = UnifiedRecord::new();
1891 record.set("rid", Value::UnsignedInteger(entity.id.raw()));
1892 record.set("collection", Value::text(collection.clone()));
1893 record.set("kind", Value::text("kv"));
1894 record.set("tenant", Value::Null);
1895 record.set("created_at", Value::UnsignedInteger(entity.created_at));
1896 record.set("updated_at", Value::UnsignedInteger(entity.updated_at));
1897 record.set("key", Value::text(key.clone()));
1898 record.set(
1899 "value",
1900 kv_value_from_entity(&entity).unwrap_or(reddb_types::Value::Null),
1901 );
1902 record.set("tags", kv_tags_value(&ops.tags_for_key(collection, &key)));
1903 result.push(record);
1904 }
1905 Ok(RuntimeQueryResult {
1906 query: raw_query.to_string(),
1907 mode: reddb_rql::modes::QueryMode::Sql,
1908 statement: "kv_list",
1909 engine: "kv",
1910 result,
1911 affected_rows: 0,
1912 statement_type: "select",
1913 bookmark: None,
1914 notice: None,
1915 })
1916 }
1917 }
1918 }
1919
1920 KvCommand::History { collection, key } => {
1921 let latest = ops.get_vault_entry(collection, key)?;
1922 if let Err(reason) =
1923 self.check_vault_capability("vault:read_metadata", collection, key)
1924 {
1925 self.emit_vault_control_event(
1926 crate::runtime::control_events::EventKind::VaultMetadataRead,
1927 crate::runtime::control_events::Outcome::Denied,
1928 "vault:read_metadata",
1929 collection,
1930 key,
1931 &reason,
1932 latest.as_ref(),
1933 Vec::new(),
1934 )?;
1935 return Err(RedDBError::Query(reason));
1936 }
1937 let versions =
1938 super::keyed_spine::history_versions(ops.vault_versions(collection, key)?);
1939 let result = vault_history_result(collection, key, &versions);
1940 self.emit_vault_control_event(
1941 crate::runtime::control_events::EventKind::VaultMetadataRead,
1942 crate::runtime::control_events::Outcome::Allowed,
1943 "vault:read_metadata",
1944 collection,
1945 key,
1946 "ok",
1947 latest.as_ref(),
1948 Vec::new(),
1949 )?;
1950 Ok(RuntimeQueryResult {
1951 query: raw_query.to_string(),
1952 mode: reddb_rql::modes::QueryMode::Sql,
1953 statement: "vault_history",
1954 engine: "vault",
1955 result,
1956 affected_rows: 0,
1957 statement_type: "select",
1958 bookmark: None,
1959 notice: None,
1960 })
1961 }
1962
1963 KvCommand::Purge { collection, key } => {
1964 let entry = ops.get_vault_entry(collection, key)?;
1965 if let Err(reason) = self.check_vault_capability("vault:purge", collection, key) {
1966 self.audit_vault_lifecycle(
1967 "purge",
1968 collection,
1969 key,
1970 crate::runtime::audit_log::Outcome::Denied,
1971 &reason,
1972 entry.as_ref(),
1973 );
1974 self.emit_vault_control_event(
1975 crate::runtime::control_events::EventKind::VaultPurge,
1976 crate::runtime::control_events::Outcome::Denied,
1977 "vault:purge",
1978 collection,
1979 key,
1980 &reason,
1981 entry.as_ref(),
1982 Vec::new(),
1983 )?;
1984 return Err(RedDBError::Query(reason));
1985 }
1986 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1987 let purged = ops.purge_vault_versions(collection, key)?;
1988 self.audit_vault_lifecycle(
1989 "purge",
1990 collection,
1991 key,
1992 crate::runtime::audit_log::Outcome::Success,
1993 "ok",
1994 entry.as_ref(),
1995 );
1996 self.emit_vault_control_event(
1997 crate::runtime::control_events::EventKind::VaultPurge,
1998 crate::runtime::control_events::Outcome::Allowed,
1999 "vault:purge",
2000 collection,
2001 key,
2002 "ok",
2003 entry.as_ref(),
2004 vec![(
2005 "purged".to_string(),
2006 crate::runtime::control_events::Sensitivity::raw(purged.to_string()),
2007 )],
2008 )?;
2009 let mut result = UnifiedResult::with_columns(vec![
2010 "ok".into(),
2011 "collection".into(),
2012 "key".into(),
2013 "purged".into(),
2014 ]);
2015 let mut record = UnifiedRecord::new();
2016 record.set("ok", Value::Boolean(true));
2017 record.set("collection", Value::text(collection.clone()));
2018 record.set("key", Value::text(key.clone()));
2019 record.set("purged", Value::Integer(purged as i64));
2020 result.push(record);
2021 Ok(RuntimeQueryResult {
2022 query: raw_query.to_string(),
2023 mode: reddb_rql::modes::QueryMode::Sql,
2024 statement: "vault_purge",
2025 engine: "vault",
2026 result,
2027 affected_rows: purged as u64,
2028 statement_type: "delete",
2029 bookmark: None,
2030 notice: None,
2031 })
2032 }
2033
2034 KvCommand::Get {
2035 model,
2036 collection,
2037 key,
2038 } => {
2039 if *model == crate::catalog::CollectionModel::Vault {
2040 let entry = ops.get_vault_entry(collection, key)?;
2041 if let Err(reason) =
2042 self.check_vault_capability("vault:read_metadata", collection, key)
2043 {
2044 self.emit_vault_control_event(
2045 crate::runtime::control_events::EventKind::VaultMetadataRead,
2046 crate::runtime::control_events::Outcome::Denied,
2047 "vault:read_metadata",
2048 collection,
2049 key,
2050 &reason,
2051 entry.as_ref(),
2052 Vec::new(),
2053 )?;
2054 return Err(RedDBError::Query(reason));
2055 }
2056 let key_available = self.vault_key_available(collection);
2057 let result =
2058 vault_metadata_result(collection, key, entry.as_ref(), key_available);
2059 self.emit_vault_control_event(
2060 crate::runtime::control_events::EventKind::VaultMetadataRead,
2061 crate::runtime::control_events::Outcome::Allowed,
2062 "vault:read_metadata",
2063 collection,
2064 key,
2065 "ok",
2066 entry.as_ref(),
2067 Vec::new(),
2068 )?;
2069 return Ok(RuntimeQueryResult {
2070 query: raw_query.to_string(),
2071 mode: reddb_rql::modes::QueryMode::Sql,
2072 statement: "vault_get",
2073 engine: "vault",
2074 result,
2075 affected_rows: 0,
2076 statement_type: "select",
2077 bookmark: None,
2078 notice: None,
2079 });
2080 }
2081
2082 let entity = ops.get_entity(*model, collection, key)?;
2083 let value = entity.as_ref().and_then(kv_value_from_entity);
2084 if *model == crate::catalog::CollectionModel::Kv {
2085 self.inner.kv_stats.incr_gets();
2086 }
2087 let mut result = UnifiedResult::with_columns(vec![
2088 "rid".into(),
2089 "collection".into(),
2090 "kind".into(),
2091 "tenant".into(),
2092 "created_at".into(),
2093 "updated_at".into(),
2094 "key".into(),
2095 "value".into(),
2096 "tags".into(),
2097 ]);
2098 let mut record = UnifiedRecord::new();
2099 if let Some(entity) = entity.as_ref() {
2100 record.set("rid", Value::UnsignedInteger(entity.id.raw()));
2101 record.set("created_at", Value::UnsignedInteger(entity.created_at));
2102 record.set("updated_at", Value::UnsignedInteger(entity.updated_at));
2103 } else {
2104 record.set("rid", Value::Null);
2105 record.set("created_at", Value::Null);
2106 record.set("updated_at", Value::Null);
2107 }
2108 record.set("collection", Value::text(collection.clone()));
2109 record.set("kind", Value::text(keyed_model_name(*model).to_string()));
2110 record.set("tenant", Value::Null);
2111 record.set("key", Value::text(key.clone()));
2112 record.set("value", value.unwrap_or(reddb_types::Value::Null));
2113 record.set("tags", kv_tags_value(&ops.tags_for_key(collection, key)));
2114 result.push(record);
2115
2116 Ok(RuntimeQueryResult {
2117 query: raw_query.to_string(),
2118 mode: reddb_rql::modes::QueryMode::Sql,
2119 statement: "kv_get",
2120 engine: "kv",
2121 result,
2122 affected_rows: 0,
2123 statement_type: "select",
2124 bookmark: None,
2125 notice: None,
2126 })
2127 }
2128 KvCommand::Watch {
2129 model,
2130 collection,
2131 key,
2132 prefix,
2133 from_lsn,
2134 } => {
2135 let watch_key = if *prefix {
2136 format!("{key}.*")
2137 } else {
2138 key.clone()
2139 };
2140 let endpoint = match from_lsn {
2141 Some(lsn) => format!(
2142 "/collections/{collection}/{}/{watch_key}/watch?since_lsn={lsn}",
2143 keyed_model_name(*model)
2144 ),
2145 None => format!(
2146 "/collections/{collection}/{}/{watch_key}/watch",
2147 keyed_model_name(*model)
2148 ),
2149 };
2150 let mut result = UnifiedResult::with_columns(vec![
2151 "collection".into(),
2152 "key".into(),
2153 "prefix".into(),
2154 "from_lsn".into(),
2155 "watch_url".into(),
2156 "streaming".into(),
2157 ]);
2158 let mut record = UnifiedRecord::new();
2159 record.set("collection", Value::text(collection.clone()));
2160 record.set("key", Value::text(watch_key));
2161 record.set("prefix", Value::Boolean(*prefix));
2162 record.set(
2163 "from_lsn",
2164 from_lsn
2165 .map(Value::UnsignedInteger)
2166 .unwrap_or(reddb_types::Value::Null),
2167 );
2168 record.set("watch_url", Value::text(endpoint));
2169 record.set("streaming", Value::Boolean(true));
2170 result.push(record);
2171
2172 Ok(RuntimeQueryResult {
2173 query: raw_query.to_string(),
2174 mode: reddb_rql::modes::QueryMode::Sql,
2175 statement: "kv_watch",
2176 engine: keyed_model_name(*model),
2177 result,
2178 affected_rows: 0,
2179 statement_type: "stream",
2180 bookmark: None,
2181 notice: None,
2182 })
2183 }
2184
2185 KvCommand::Unseal {
2186 collection,
2187 key,
2188 version,
2189 } => {
2190 let latest = ops.get_vault_entry(collection, key)?;
2191 let entry = match version {
2192 Some(version) => ops.get_vault_entry_version(collection, key, *version)?,
2193 None => latest.clone(),
2194 };
2195 let action = match (version, latest.as_ref()) {
2196 (Some(requested), Some(latest)) if *requested == latest.version => "vault:read",
2197 (Some(_), _) => "vault:unseal_history",
2198 _ => "vault:read",
2199 };
2200 let event_kind = if action == "vault:read" {
2201 crate::runtime::control_events::EventKind::VaultRead
2202 } else {
2203 crate::runtime::control_events::EventKind::VaultUnseal
2204 };
2205 if let Err(reason) = self.check_vault_capability(action, collection, key) {
2206 self.audit_vault_unseal(
2207 collection,
2208 key,
2209 crate::runtime::audit_log::Outcome::Denied,
2210 &reason,
2211 entry.as_ref(),
2212 );
2213 self.emit_vault_control_event(
2214 event_kind,
2215 crate::runtime::control_events::Outcome::Denied,
2216 action,
2217 collection,
2218 key,
2219 &reason,
2220 entry.as_ref(),
2221 Vec::new(),
2222 )?;
2223 return Err(RedDBError::Query(reason));
2224 }
2225 let Some(entry) = entry else {
2226 let reason = "not_found";
2227 self.audit_vault_unseal(
2228 collection,
2229 key,
2230 crate::runtime::audit_log::Outcome::Denied,
2231 reason,
2232 None,
2233 );
2234 self.emit_vault_control_event(
2235 event_kind,
2236 crate::runtime::control_events::Outcome::Denied,
2237 action,
2238 collection,
2239 key,
2240 reason,
2241 None,
2242 Vec::new(),
2243 )?;
2244 return Err(RedDBError::NotFound(format!(
2245 "vault secret '{}.{}' not found",
2246 collection, key
2247 )));
2248 };
2249 if entry.tombstone {
2250 let reason = "deleted";
2251 self.audit_vault_unseal(
2252 collection,
2253 key,
2254 crate::runtime::audit_log::Outcome::Denied,
2255 reason,
2256 Some(&entry),
2257 );
2258 self.emit_vault_control_event(
2259 event_kind,
2260 crate::runtime::control_events::Outcome::Denied,
2261 action,
2262 collection,
2263 key,
2264 reason,
2265 Some(&entry),
2266 Vec::new(),
2267 )?;
2268 return Err(RedDBError::NotFound(format!(
2269 "vault secret '{}.{}' is deleted",
2270 collection, key
2271 )));
2272 }
2273 match self.unseal_vault_value(collection, &entry.value) {
2274 Ok(value) => {
2275 self.audit_vault_unseal(
2276 collection,
2277 key,
2278 crate::runtime::audit_log::Outcome::Success,
2279 "ok",
2280 Some(&entry),
2281 );
2282 self.emit_vault_control_event(
2283 event_kind,
2284 crate::runtime::control_events::Outcome::Allowed,
2285 action,
2286 collection,
2287 key,
2288 "ok",
2289 Some(&entry),
2290 Vec::new(),
2291 )?;
2292 let mut result = UnifiedResult::with_columns(vec![
2293 "collection".into(),
2294 "key".into(),
2295 "value".into(),
2296 ]);
2297 let mut record = UnifiedRecord::new();
2298 record.set("collection", Value::text(collection.clone()));
2299 record.set("key", Value::text(key.clone()));
2300 record.set("value", value);
2301 result.push(record);
2302 Ok(RuntimeQueryResult {
2303 query: raw_query.to_string(),
2304 mode: reddb_rql::modes::QueryMode::Sql,
2305 statement: "vault_unseal",
2306 engine: "vault",
2307 result,
2308 affected_rows: 0,
2309 statement_type: "select",
2310 bookmark: None,
2311 notice: None,
2312 })
2313 }
2314 Err(err) => {
2315 let reason = err.to_string();
2316 self.audit_vault_unseal(
2317 collection,
2318 key,
2319 crate::runtime::audit_log::Outcome::Error,
2320 &reason,
2321 Some(&entry),
2322 );
2323 self.emit_vault_control_event(
2324 event_kind,
2325 crate::runtime::control_events::Outcome::Error,
2326 action,
2327 collection,
2328 key,
2329 &reason,
2330 Some(&entry),
2331 Vec::new(),
2332 )?;
2333 Err(err)
2334 }
2335 }
2336 }
2337
2338 KvCommand::Incr {
2339 model,
2340 collection,
2341 key,
2342 by,
2343 ttl_ms,
2344 } => {
2345 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2346 let new_value = ops.incr(*model, collection, key, *by, *ttl_ms)?;
2347
2348 let mut result = UnifiedResult::with_columns(vec![
2349 "ok".into(),
2350 "collection".into(),
2351 "key".into(),
2352 "value".into(),
2353 ]);
2354 let mut record = UnifiedRecord::new();
2355 record.set("ok", Value::Boolean(true));
2356 record.set("collection", Value::text(collection.clone()));
2357 record.set("key", Value::text(key.clone()));
2358 record.set("value", Value::Integer(new_value));
2359 result.push(record);
2360
2361 Ok(RuntimeQueryResult {
2362 query: raw_query.to_string(),
2363 mode: reddb_rql::modes::QueryMode::Sql,
2364 statement: "kv_incr",
2365 engine: "kv",
2366 result,
2367 affected_rows: 1,
2368 statement_type: "update",
2369 bookmark: None,
2370 notice: None,
2371 })
2372 }
2373
2374 KvCommand::Cas {
2375 model,
2376 collection,
2377 key,
2378 expected,
2379 new_value,
2380 ttl_ms,
2381 } => {
2382 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2383 let (ok, current) = ops.cas(
2384 *model,
2385 collection,
2386 key,
2387 expected.as_ref(),
2388 new_value.clone(),
2389 *ttl_ms,
2390 )?;
2391
2392 let mut result = UnifiedResult::with_columns(vec![
2393 "ok".into(),
2394 "collection".into(),
2395 "key".into(),
2396 "current".into(),
2397 ]);
2398 let mut record = UnifiedRecord::new();
2399 record.set("ok", Value::Boolean(ok));
2400 record.set("collection", Value::text(collection.clone()));
2401 record.set("key", Value::text(key.clone()));
2402 record.set("current", current.unwrap_or(reddb_types::Value::Null));
2403 result.push(record);
2404
2405 Ok(RuntimeQueryResult {
2406 query: raw_query.to_string(),
2407 mode: reddb_rql::modes::QueryMode::Sql,
2408 statement: "kv_cas",
2409 engine: "kv",
2410 result,
2411 affected_rows: if ok { 1 } else { 0 },
2412 statement_type: "update",
2413 bookmark: None,
2414 notice: None,
2415 })
2416 }
2417
2418 KvCommand::Delete {
2419 model,
2420 collection,
2421 key,
2422 } => {
2423 if *model == crate::catalog::CollectionModel::Vault {
2424 self.check_system_vault_capability("vault:write", collection, key)
2425 .map_err(RedDBError::Query)?;
2426 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2427 let entry = ops.append_vault_version(
2428 collection,
2429 key,
2430 Value::Null,
2431 "delete",
2432 true,
2433 &[],
2434 )?;
2435 self.record_kv_watch_event(
2436 crate::replication::cdc::ChangeOperation::Delete,
2437 collection,
2438 key,
2439 entry.id.raw(),
2440 None,
2441 Some(vault_entry_metadata_json(&entry)),
2442 );
2443 self.audit_vault_lifecycle(
2444 "delete",
2445 collection,
2446 key,
2447 crate::runtime::audit_log::Outcome::Success,
2448 "ok",
2449 Some(&entry),
2450 );
2451 return Ok(vault_write_result(
2452 raw_query,
2453 "vault_delete",
2454 "delete",
2455 collection,
2456 key,
2457 &entry,
2458 1,
2459 ));
2460 }
2461 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2462 let deleted = ops.delete(*model, collection, key)?;
2463
2464 let mut result = UnifiedResult::with_columns(vec![
2465 "ok".into(),
2466 "collection".into(),
2467 "key".into(),
2468 "deleted".into(),
2469 ]);
2470 let mut record = UnifiedRecord::new();
2471 record.set("ok", Value::Boolean(true));
2472 record.set("collection", Value::text(collection.clone()));
2473 record.set("key", Value::text(key.clone()));
2474 record.set("deleted", Value::Boolean(deleted));
2475 result.push(record);
2476
2477 Ok(RuntimeQueryResult {
2478 query: raw_query.to_string(),
2479 mode: reddb_rql::modes::QueryMode::Sql,
2480 statement: "kv_delete",
2481 engine: "kv",
2482 result,
2483 affected_rows: if deleted { 1 } else { 0 },
2484 statement_type: "delete",
2485 bookmark: None,
2486 notice: None,
2487 })
2488 }
2489 }
2490 }
2491
2492 pub fn vault_watch_events_since(
2493 &self,
2494 collection: &str,
2495 key: &str,
2496 since_lsn: u64,
2497 max_count: usize,
2498 ) -> Vec<crate::replication::cdc::KvWatchEvent> {
2499 self.kv_watch_events_since(collection, key, since_lsn, max_count)
2500 .into_iter()
2501 .filter(|event| {
2502 self.check_vault_capability("vault:read_metadata", &event.collection, &event.key)
2503 .is_ok()
2504 })
2505 .map(vault_filter_watch_event)
2506 .collect()
2507 }
2508
2509 pub fn vault_watch_events_since_prefix(
2510 &self,
2511 collection: &str,
2512 prefix: &str,
2513 since_lsn: u64,
2514 max_count: usize,
2515 ) -> Vec<crate::replication::cdc::KvWatchEvent> {
2516 self.kv_watch_events_since_prefix(collection, prefix, since_lsn, max_count)
2517 .into_iter()
2518 .filter(|event| {
2519 self.check_vault_capability("vault:read_metadata", &event.collection, &event.key)
2520 .is_ok()
2521 })
2522 .map(vault_filter_watch_event)
2523 .collect()
2524 }
2525
2526 fn check_kv_invalidate_policy(&self, collection: &str) -> RedDBResult<()> {
2527 let auth_store = match self.inner.auth_store.read().clone() {
2528 Some(store) => store,
2529 None => return Ok(()),
2530 };
2531 let (username, role) = match crate::runtime::impl_core::current_auth_identity() {
2532 Some(identity) => identity,
2533 None => return Ok(()),
2534 };
2535 if role < crate::auth::Role::Write {
2536 return Err(RedDBError::Query(format!(
2537 "principal=`{username}` role=`{role:?}` cannot invalidate KV tags"
2538 )));
2539 }
2540 if !auth_store.iam_authorization_enabled() {
2541 return Ok(());
2542 }
2543
2544 let tenant = crate::runtime::impl_core::current_tenant();
2545 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
2546 let mut resource =
2547 crate::auth::policies::ResourceRef::new("kv".to_string(), collection.to_string());
2548 if let Some(tenant) = tenant.clone() {
2549 resource = resource.with_tenant(tenant);
2550 }
2551 let ctx = crate::auth::policies::EvalContext {
2552 principal_tenant: tenant.clone(),
2553 current_tenant: tenant,
2554 peer_ip: None,
2555 mfa_present: false,
2556 now_ms: current_unix_ms(),
2557 principal_is_admin_role: role == crate::auth::Role::Admin,
2558 principal_is_platform_scoped: principal.tenant.is_none(),
2559 };
2560 if auth_store.check_policy_authz_with_role(
2561 &principal,
2562 "kv:invalidate",
2563 &resource,
2564 &ctx,
2565 role,
2566 ) {
2567 Ok(())
2568 } else {
2569 Err(RedDBError::Query(format!(
2570 "principal=`{username}` action=`kv:invalidate` resource=`kv:{collection}` denied by IAM policy"
2571 )))
2572 }
2573 }
2574}
2575
2576fn ttl_metadata(ttl_ms: Option<u64>) -> Option<Metadata> {
2577 let ttl_ms = ttl_ms?;
2578 Some(Metadata::with_fields(
2579 [(
2580 "_ttl_ms".to_string(),
2581 if ttl_ms <= i64::MAX as u64 {
2582 MetadataValue::Int(ttl_ms as i64)
2583 } else {
2584 MetadataValue::Timestamp(ttl_ms)
2585 },
2586 )]
2587 .into_iter()
2588 .collect(),
2589 ))
2590}
2591
2592fn vault_write_result(
2593 raw_query: &str,
2594 statement: &'static str,
2595 statement_type: &'static str,
2596 collection: &str,
2597 key: &str,
2598 entry: &VaultEntry,
2599 affected_rows: u64,
2600) -> RuntimeQueryResult {
2601 let mut result = UnifiedResult::with_columns(vec![
2602 "ok".into(),
2603 "collection".into(),
2604 "key".into(),
2605 "version".into(),
2606 "fingerprint".into(),
2607 "tombstone".into(),
2608 "op".into(),
2609 "id".into(),
2610 ]);
2611 let mut record = UnifiedRecord::new();
2612 record.set("ok", Value::Boolean(true));
2613 record.set("collection", Value::text(collection.to_string()));
2614 record.set("key", Value::text(key.to_string()));
2615 record.set("version", Value::Integer(entry.version));
2616 if entry.tombstone {
2617 record.set("fingerprint", Value::Null);
2618 } else {
2619 record.set("fingerprint", Value::text(vault_fingerprint(&entry.value)));
2620 }
2621 record.set("tombstone", Value::Boolean(entry.tombstone));
2622 record.set("op", Value::text(entry.op.clone()));
2623 record.set("id", Value::Integer(entry.id.raw() as i64));
2624 result.push(record);
2625 RuntimeQueryResult {
2626 query: raw_query.to_string(),
2627 mode: reddb_rql::modes::QueryMode::Sql,
2628 statement,
2629 engine: "vault",
2630 result,
2631 affected_rows,
2632 statement_type,
2633 bookmark: None,
2634 notice: None,
2635 }
2636}
2637
2638fn vault_history_result(collection: &str, key: &str, versions: &[VaultEntry]) -> UnifiedResult {
2639 let mut result = UnifiedResult::with_columns(vec![
2640 "collection".into(),
2641 "key".into(),
2642 "version".into(),
2643 "fingerprint".into(),
2644 "tags".into(),
2645 "created_at".into(),
2646 "updated_at".into(),
2647 "status".into(),
2648 "tombstone".into(),
2649 "op".into(),
2650 ]);
2651 for entry in versions {
2652 push_vault_metadata_record(&mut result, collection, key, entry);
2653 }
2654 result
2655}
2656
2657fn push_vault_metadata_record(
2658 result: &mut UnifiedResult,
2659 collection: &str,
2660 key: &str,
2661 entry: &VaultEntry,
2662) {
2663 let mut record = UnifiedRecord::new();
2664 record.set("collection", Value::text(collection.to_string()));
2665 record.set("key", Value::text(key.to_string()));
2666 record.set("version", Value::Integer(entry.version));
2667 if entry.tombstone {
2668 record.set("fingerprint", Value::Null);
2669 record.set("status", Value::text("deleted"));
2670 } else {
2671 record.set("fingerprint", Value::text(vault_fingerprint(&entry.value)));
2672 record.set("status", Value::text("sealed"));
2673 }
2674 record.set("tags", vault_tags_value(&entry.metadata));
2675 record.set("created_at", Value::TimestampMs(entry.created_at as i64));
2676 record.set("updated_at", Value::TimestampMs(entry.updated_at as i64));
2677 record.set("tombstone", Value::Boolean(entry.tombstone));
2678 record.set("op", Value::text(entry.op.clone()));
2679 result.push(record);
2680}
2681
2682fn vault_metadata_result(
2683 collection: &str,
2684 key: &str,
2685 entry: Option<&VaultEntry>,
2686 key_available: bool,
2687) -> UnifiedResult {
2688 let mut result = UnifiedResult::with_columns(vec![
2689 "collection".into(),
2690 "key".into(),
2691 "version".into(),
2692 "fingerprint".into(),
2693 "tags".into(),
2694 "created_at".into(),
2695 "updated_at".into(),
2696 "value".into(),
2697 "status".into(),
2698 "tombstone".into(),
2699 "op".into(),
2700 ]);
2701 let mut record = UnifiedRecord::new();
2702 record.set("collection", Value::text(collection.to_string()));
2703 record.set("key", Value::text(key.to_string()));
2704 match entry {
2705 Some(entry) => {
2706 record.set("version", Value::Integer(entry.version));
2707 if entry.tombstone {
2708 record.set("fingerprint", Value::Null);
2709 } else {
2710 record.set("fingerprint", Value::text(vault_fingerprint(&entry.value)));
2711 }
2712 record.set("tags", vault_tags_value(&entry.metadata));
2713 record.set("created_at", Value::TimestampMs(entry.created_at as i64));
2714 record.set("updated_at", Value::TimestampMs(entry.updated_at as i64));
2715 record.set("value", Value::text("***"));
2716 record.set(
2717 "status",
2718 Value::text(if entry.tombstone {
2719 "deleted"
2720 } else if key_available {
2721 "sealed"
2722 } else {
2723 "sealed_unavailable"
2724 }),
2725 );
2726 record.set("tombstone", Value::Boolean(entry.tombstone));
2727 record.set("op", Value::text(entry.op.clone()));
2728 }
2729 None => {
2730 record.set("version", Value::Null);
2731 record.set("fingerprint", Value::Null);
2732 record.set("tags", Value::Array(Vec::new()));
2733 record.set("created_at", Value::Null);
2734 record.set("updated_at", Value::Null);
2735 record.set("value", Value::text(""));
2736 record.set("status", Value::text("missing"));
2737 record.set("tombstone", Value::Boolean(false));
2738 record.set("op", Value::Null);
2739 }
2740 }
2741 result.push(record);
2742 result
2743}
2744
2745fn vault_fingerprint(value: &Value) -> String {
2746 match value {
2747 Value::Secret(payload) => crate::utils::to_hex(&crate::crypto::sha256::sha256(payload)),
2748 other => crate::utils::to_hex(&crate::crypto::sha256::sha256(&other.to_bytes())),
2749 }
2750}
2751
2752fn vault_entry_metadata_json(entry: &VaultEntry) -> crate::json::Value {
2753 let mut object = crate::json::Map::new();
2754 object.insert(
2755 "key".to_string(),
2756 crate::json::Value::String(entry.key.clone()),
2757 );
2758 object.insert(
2759 "version".to_string(),
2760 crate::json::Value::Number(entry.version as f64),
2761 );
2762 object.insert(
2763 "fingerprint".to_string(),
2764 if entry.tombstone {
2765 crate::json::Value::Null
2766 } else {
2767 crate::json::Value::String(vault_fingerprint(&entry.value))
2768 },
2769 );
2770 object.insert("tags".to_string(), vault_tags_json(&entry.metadata));
2771 object.insert(
2772 "actor".to_string(),
2773 crate::json::Value::String(RedDBRuntime::current_vault_actor()),
2774 );
2775 object.insert(
2776 "sequence_id".to_string(),
2777 crate::json::Value::Number(entry.sequence_id as f64),
2778 );
2779 object.insert(
2780 "tombstone".to_string(),
2781 crate::json::Value::Bool(entry.tombstone),
2782 );
2783 object.insert(
2784 "op".to_string(),
2785 crate::json::Value::String(entry.op.clone()),
2786 );
2787 crate::json::Value::Object(object)
2788}
2789
2790fn vault_tags_json(metadata: &Metadata) -> crate::json::Value {
2791 match vault_tags_value(metadata) {
2792 Value::Array(values) => crate::json::Value::Array(
2793 values
2794 .into_iter()
2795 .filter_map(|value| match value {
2796 Value::Text(tag) => Some(crate::json::Value::String(tag.to_string())),
2797 _ => None,
2798 })
2799 .collect(),
2800 ),
2801 _ => crate::json::Value::Array(Vec::new()),
2802 }
2803}
2804
2805fn vault_tags_metadata(tags: &[String]) -> std::collections::HashMap<String, MetadataValue> {
2806 [(
2807 "tags".to_string(),
2808 MetadataValue::Array(
2809 tags.iter()
2810 .map(|tag| MetadataValue::String(tag.clone()))
2811 .collect(),
2812 ),
2813 )]
2814 .into_iter()
2815 .collect()
2816}
2817
2818fn vault_filter_watch_event(
2819 mut event: crate::replication::cdc::KvWatchEvent,
2820) -> crate::replication::cdc::KvWatchEvent {
2821 event.before = event.before.and_then(vault_metadata_json_only);
2822 event.after = event.after.and_then(vault_metadata_json_only);
2823 event
2824}
2825
2826fn vault_metadata_json_only(value: crate::json::Value) -> Option<crate::json::Value> {
2827 let object = value.as_object()?;
2828 let mut out = crate::json::Map::new();
2829 for field in [
2830 "key",
2831 "version",
2832 "fingerprint",
2833 "tags",
2834 "actor",
2835 "sequence_id",
2836 "tombstone",
2837 "op",
2838 ] {
2839 if let Some(value) = object.get(field) {
2840 out.insert(field.to_string(), value.clone());
2841 }
2842 }
2843 Some(crate::json::Value::Object(out))
2844}
2845
2846fn vault_tags_value(metadata: &Metadata) -> Value {
2847 match metadata.get("tags") {
2848 Some(MetadataValue::Array(values)) => Value::Array(
2849 values
2850 .iter()
2851 .filter_map(|value| match value {
2852 MetadataValue::String(tag) => Some(Value::text(tag.clone())),
2853 _ => None,
2854 })
2855 .collect(),
2856 ),
2857 Some(MetadataValue::String(tag)) if !tag.is_empty() => {
2858 Value::Array(vec![Value::text(tag.clone())])
2859 }
2860 _ => Value::Array(Vec::new()),
2861 }
2862}
2863
2864fn decode_vault_key(hex_key: &str) -> RedDBResult<[u8; 32]> {
2865 let bytes = hex::decode(hex_key)
2866 .map_err(|_| RedDBError::Query("vault sealed_unavailable: bad key material".to_string()))?;
2867 let key: [u8; 32] = bytes.try_into().map_err(|_| {
2868 RedDBError::Query("vault sealed_unavailable: bad key material length".to_string())
2869 })?;
2870 Ok(key)
2871}
2872
2873fn kv_tags_metadata(tags: &[String]) -> Option<(String, MetadataValue)> {
2874 if tags.is_empty() {
2875 return None;
2876 }
2877 let values = tags
2878 .iter()
2879 .map(|tag| MetadataValue::String(tag.clone()))
2880 .collect();
2881 Some(("_kv_tags".to_string(), MetadataValue::Array(values)))
2882}
2883
2884fn kv_tags_value(tags: &[String]) -> Value {
2885 let json = crate::json::Value::Array(
2886 tags.iter()
2887 .map(|tag| crate::json::Value::String(tag.clone()))
2888 .collect(),
2889 );
2890 Value::Json(crate::json::to_vec(&json).unwrap_or_default())
2891}
2892
2893fn kv_value_from_entity(entity: &crate::storage::UnifiedEntity) -> Option<Value> {
2894 if let crate::storage::EntityData::Row(ref row) = entity.data {
2895 if let Some(ref named) = row.named {
2896 return named.get("value").cloned();
2897 }
2898 }
2899 None
2900}
2901
2902fn kv_entity_has_key(entity: &crate::storage::UnifiedEntity, key: &str) -> bool {
2904 if let crate::storage::EntityData::Row(ref row) = entity.data {
2905 if let Some(ref named) = row.named {
2906 if let Some(reddb_types::Value::Text(ref k)) = named.get("key") {
2907 return &**k == key;
2908 }
2909 }
2910 }
2911 false
2912}
2913
2914fn insert_kv_json_path(root: &mut crate::json::Value, path: &str, value: crate::json::Value) {
2915 let segments: Vec<&str> = path
2916 .split('.')
2917 .filter(|segment| !segment.is_empty())
2918 .collect();
2919 insert_kv_json_segments(root, &segments, value);
2920}
2921
2922fn insert_kv_json_segments(
2923 root: &mut crate::json::Value,
2924 segments: &[&str],
2925 value: crate::json::Value,
2926) {
2927 if segments.is_empty() {
2928 *root = value;
2929 return;
2930 }
2931
2932 if !matches!(root, crate::json::Value::Object(_)) {
2933 *root = crate::json::Value::Object(crate::json::Map::new());
2934 }
2935
2936 let crate::json::Value::Object(map) = root else {
2937 return;
2938 };
2939 if segments.len() == 1 {
2940 map.insert(segments[0].to_string(), value);
2941 return;
2942 }
2943 let entry = map
2944 .entry(segments[0].to_string())
2945 .or_insert_with(|| crate::json::Value::Object(crate::json::Map::new()));
2946 insert_kv_json_segments(entry, &segments[1..], value);
2947}
2948
2949fn kv_list_json_result(
2950 raw_query: &str,
2951 collection: &str,
2952 prefix: &Option<String>,
2953 value: crate::json::Value,
2954) -> RuntimeQueryResult {
2955 let mut result =
2956 UnifiedResult::with_columns(vec!["collection".into(), "prefix".into(), "value".into()]);
2957 let mut record = UnifiedRecord::new();
2958 record.set("collection", Value::text(collection.to_string()));
2959 record.set(
2960 "prefix",
2961 prefix
2962 .as_ref()
2963 .map(|prefix| Value::text(prefix.clone()))
2964 .unwrap_or(Value::Null),
2965 );
2966 record.set("value", Value::Json(value.to_string_compact().into_bytes()));
2967 result.push(record);
2968 RuntimeQueryResult {
2969 query: raw_query.to_string(),
2970 mode: reddb_rql::modes::QueryMode::Sql,
2971 statement: "kv_list_json",
2972 engine: "kv",
2973 result,
2974 affected_rows: 0,
2975 statement_type: "select",
2976 bookmark: None,
2977 notice: None,
2978 }
2979}
2980
2981fn kv_collection_contract(name: &str) -> crate::physical::CollectionContract {
2982 let now = current_unix_ms();
2983 crate::physical::CollectionContract {
2984 name: name.to_string(),
2985 declared_model: crate::catalog::CollectionModel::Kv,
2986 schema_mode: crate::catalog::SchemaMode::Dynamic,
2987 origin: crate::physical::ContractOrigin::Implicit,
2988 version: 1,
2989 created_at_unix_ms: now,
2990 updated_at_unix_ms: now,
2991 default_ttl_ms: None,
2992 vector_dimension: None,
2993 vector_metric: None,
2994 context_index_fields: Vec::new(),
2995 declared_columns: Vec::new(),
2996 table_def: None,
2997 timestamps_enabled: false,
2998 context_index_enabled: false,
2999 metrics_raw_retention_ms: None,
3000 metrics_rollup_policies: Vec::new(),
3001 metrics_tenant_identity: None,
3002 metrics_namespace: None,
3003 append_only: false,
3004 subscriptions: Vec::new(),
3005 analytics_config: Vec::new(),
3006 session_key: None,
3007 session_gap_ms: None,
3008 retention_duration_ms: None,
3009 analytical_storage: None,
3010
3011 ai_policy: None,
3012 }
3013}
3014
3015fn current_unix_ms() -> u128 {
3016 std::time::SystemTime::now()
3017 .duration_since(std::time::UNIX_EPOCH)
3018 .unwrap_or_default()
3019 .as_millis()
3020}
3021
3022#[cfg(test)]
3023mod tests {
3024 use crate::api::RedDBOptions;
3025 use crate::catalog::CollectionModel;
3026 use crate::runtime::RedDBRuntime;
3027
3028 fn rt() -> RedDBRuntime {
3029 RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("in-memory runtime")
3030 }
3031
3032 #[test]
3033 fn concurrent_put_if_not_exists_has_exactly_one_winner() {
3034 const THREADS: usize = 8;
3041
3042 let runtime = std::sync::Arc::new(rt());
3043 let barrier = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
3044 let winners = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
3045 let mut handles = Vec::new();
3046
3047 for i in 0..THREADS {
3048 let runtime = std::sync::Arc::clone(&runtime);
3049 let barrier = std::sync::Arc::clone(&barrier);
3050 let winners = std::sync::Arc::clone(&winners);
3051 handles.push(std::thread::spawn(move || {
3052 let ops = super::KvAtomicOps::new(&runtime);
3053 barrier.wait();
3054 let (inserted, _) = ops
3055 .set_with_tags_and_metadata_for_model(
3056 CollectionModel::Kv,
3057 "kv_default",
3058 "once",
3059 reddb_types::Value::Integer(i as i64),
3060 None,
3061 &[],
3062 true,
3063 Vec::new(),
3064 )
3065 .expect("put should succeed");
3066 if inserted {
3067 winners.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3068 }
3069 }));
3070 }
3071 for handle in handles {
3072 handle.join().expect("worker should finish");
3073 }
3074
3075 assert_eq!(
3076 winners.load(std::sync::atomic::Ordering::Relaxed),
3077 1,
3078 "exactly one racer may report having inserted the key"
3079 );
3080 }
3081
3082 #[test]
3083 fn incr_missing_key_initialises_at_by() {
3084 let r = rt();
3085 let ops = super::KvAtomicOps::new(&r);
3086 let v = ops
3087 .incr(CollectionModel::Kv, "kv_default", "missing", 5, None)
3088 .unwrap();
3089 assert_eq!(v, 5);
3090 }
3091
3092 #[test]
3093 fn kv_runtime_stats_count_public_ops() {
3094 let r = rt();
3095 let ops = super::KvAtomicOps::new(&r);
3096
3097 ops.set(
3098 CollectionModel::Kv,
3099 "kv_default",
3100 "profile",
3101 reddb_types::Value::text("alice"),
3102 None,
3103 false,
3104 )
3105 .unwrap();
3106 ops.get(CollectionModel::Kv, "kv_default", "profile")
3107 .unwrap();
3108 ops.delete(CollectionModel::Kv, "kv_default", "profile")
3109 .unwrap();
3110 ops.incr(CollectionModel::Kv, "kv_default", "hits", 1, None)
3111 .unwrap();
3112 ops.cas(
3113 CollectionModel::Kv,
3114 "kv_default",
3115 "profile",
3116 None,
3117 reddb_types::Value::text("created"),
3118 None,
3119 )
3120 .unwrap();
3121 ops.cas(
3122 CollectionModel::Kv,
3123 "kv_default",
3124 "profile",
3125 Some(&reddb_types::Value::text("different")),
3126 reddb_types::Value::text("ignored"),
3127 None,
3128 )
3129 .unwrap();
3130
3131 let stats = r.stats().kv;
3132 assert_eq!(stats.puts, 1);
3133 assert_eq!(stats.gets, 1);
3134 assert_eq!(stats.deletes, 1);
3135 assert_eq!(stats.incrs, 1);
3136 assert_eq!(stats.cas_success, 1);
3137 assert_eq!(stats.cas_conflict, 1);
3138 }
3139
3140 #[test]
3141 fn kv_invalidate_tags_removes_matching_entries_only() {
3142 let r = rt();
3143
3144 r.execute_query("KV PUT sessions.blob = 'payload' TAGS [user:42, org:7]")
3145 .unwrap();
3146
3147 let miss = r
3148 .execute_query("INVALIDATE TAGS [org:99] FROM sessions")
3149 .unwrap();
3150 assert_eq!(miss.affected_rows, 0);
3151 assert!(matches!(
3152 r.execute_query("KV GET sessions.blob")
3153 .unwrap()
3154 .result
3155 .records[0]
3156 .get("value"),
3157 Some(reddb_types::Value::Text(value)) if &**value == "payload"
3158 ));
3159
3160 let hit = r
3161 .execute_query("INVALIDATE TAGS [user:42] FROM sessions")
3162 .unwrap();
3163 assert_eq!(hit.affected_rows, 1);
3164 assert!(matches!(
3165 r.execute_query("KV GET sessions.blob")
3166 .unwrap()
3167 .result
3168 .records[0]
3169 .get("value"),
3170 Some(reddb_types::Value::Null)
3171 ));
3172 }
3173
3174 #[test]
3175 fn kv_runtime_stats_count_watch_streams_and_events() {
3176 let r = rt();
3177 let ops = super::KvAtomicOps::new(&r);
3178 assert_eq!(r.stats().kv.watch_streams_active, 0);
3179
3180 {
3181 let mut stream = r.kv_watch_subscribe("kv_default", "watched", None);
3182 assert_eq!(r.stats().kv.watch_streams_active, 1);
3183
3184 ops.set(
3185 CollectionModel::Kv,
3186 "kv_default",
3187 "watched",
3188 reddb_types::Value::Integer(1),
3189 None,
3190 false,
3191 )
3192 .unwrap();
3193 let event = stream.poll_next().expect("watch event");
3194 assert_eq!(event.key, "watched");
3195 assert_eq!(r.stats().kv.watch_events_emitted, 1);
3196
3197 stream.record_drop_count(3);
3198 assert_eq!(r.stats().kv.watch_drops, 3);
3199 }
3200
3201 assert_eq!(r.stats().kv.watch_streams_active, 0);
3202 }
3203
3204 #[test]
3205 fn incr_preserves_ttl_and_tags_it_was_not_asked_to_change() {
3206 let r = rt();
3212 let ops = super::KvAtomicOps::new(&r);
3213
3214 ops.set_with_tags_and_metadata_for_model(
3215 CollectionModel::Kv,
3216 "kv_default",
3217 "hits",
3218 reddb_types::Value::Integer(1),
3219 Some(60_000),
3220 &["daily".to_string()],
3221 false,
3222 Vec::new(),
3223 )
3224 .expect("seed put");
3225
3226 ops.incr(CollectionModel::Kv, "kv_default", "hits", 1, None)
3227 .expect("incr");
3228
3229 assert_eq!(
3230 r.inner.kv_tag_index.tags_for_key("kv_default", "hits"),
3231 vec!["daily".to_string()],
3232 "INCR must not drop the key's tags"
3233 );
3234 let entity = ops
3235 .get_entity(CollectionModel::Kv, "kv_default", "hits")
3236 .expect("lookup")
3237 .expect("key still present");
3238 let metadata = r
3239 .inner
3240 .db
3241 .store()
3242 .get_metadata("kv_default", entity.id)
3243 .expect("kv entry carries metadata");
3244 assert!(
3245 metadata.get("_ttl_ms").is_some(),
3246 "INCR without EXPIRE must not make an expiring key permanent; metadata was {:?}",
3247 metadata
3248 );
3249 }
3250
3251 #[test]
3252 fn cas_preserves_ttl_and_tags_it_was_not_asked_to_change() {
3253 let r = rt();
3257 let ops = super::KvAtomicOps::new(&r);
3258
3259 ops.set_with_tags_and_metadata_for_model(
3260 CollectionModel::Kv,
3261 "kv_default",
3262 "flag",
3263 reddb_types::Value::Integer(1),
3264 Some(60_000),
3265 &["daily".to_string()],
3266 false,
3267 Vec::new(),
3268 )
3269 .expect("seed put");
3270
3271 let (swapped, _) = ops
3272 .cas(
3273 CollectionModel::Kv,
3274 "kv_default",
3275 "flag",
3276 Some(&reddb_types::Value::Integer(1)),
3277 reddb_types::Value::Integer(2),
3278 None,
3279 )
3280 .expect("cas");
3281 assert!(swapped, "expected value matched, so the swap must apply");
3282
3283 assert_eq!(
3284 r.inner.kv_tag_index.tags_for_key("kv_default", "flag"),
3285 vec!["daily".to_string()],
3286 "CAS must not drop the key's tags"
3287 );
3288 let entity = ops
3289 .get_entity(CollectionModel::Kv, "kv_default", "flag")
3290 .expect("lookup")
3291 .expect("key still present");
3292 let metadata = r
3293 .inner
3294 .db
3295 .store()
3296 .get_metadata("kv_default", entity.id)
3297 .expect("kv entry carries metadata");
3298 assert!(
3299 metadata.get("_ttl_ms").is_some(),
3300 "CAS without EXPIRE must not make an expiring key permanent; metadata was {:?}",
3301 metadata
3302 );
3303 }
3304
3305 #[test]
3306 fn incr_existing_integer_accumulates() {
3307 let r = rt();
3308 let ops = super::KvAtomicOps::new(&r);
3309 ops.incr(CollectionModel::Kv, "kv_default", "ctr", 1, None)
3310 .unwrap();
3311 ops.incr(CollectionModel::Kv, "kv_default", "ctr", 1, None)
3312 .unwrap();
3313 let v = ops
3314 .incr(CollectionModel::Kv, "kv_default", "ctr", 1, None)
3315 .unwrap();
3316 assert_eq!(v, 3);
3317 }
3318
3319 #[test]
3320 fn decr_via_negative_by() {
3321 let r = rt();
3322 let ops = super::KvAtomicOps::new(&r);
3323 ops.incr(CollectionModel::Kv, "kv_default", "stock", 10, None)
3324 .unwrap();
3325 let v = ops
3326 .incr(CollectionModel::Kv, "kv_default", "stock", -3, None)
3327 .unwrap();
3328 assert_eq!(v, 7);
3329 }
3330
3331 #[test]
3332 fn concurrent_incr_single_key_is_atomic() {
3333 const THREADS: usize = 8;
3334 const ITERS: usize = 1000;
3335
3336 let runtime = std::sync::Arc::new(rt());
3337 let barrier = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
3338 let mut handles = Vec::new();
3339
3340 for _ in 0..THREADS {
3341 let runtime = std::sync::Arc::clone(&runtime);
3342 let barrier = std::sync::Arc::clone(&barrier);
3343 handles.push(std::thread::spawn(move || {
3344 let ops = super::KvAtomicOps::new(&runtime);
3345 barrier.wait();
3346 for _ in 0..ITERS {
3347 ops.incr(CollectionModel::Kv, "kv_default", "counter", 1, None)
3348 .unwrap();
3349 }
3350 }));
3351 }
3352
3353 for handle in handles {
3354 handle.join().expect("worker should finish");
3355 }
3356
3357 let ops = super::KvAtomicOps::new(&runtime);
3358 assert_eq!(
3359 ops.get(CollectionModel::Kv, "kv_default", "counter")
3360 .unwrap(),
3361 Some(reddb_types::Value::Integer((THREADS * ITERS) as i64))
3362 );
3363 }
3364
3365 #[test]
3366 fn incr_on_string_value_returns_error() {
3367 let r = rt();
3368 let ops = super::KvAtomicOps::new(&r);
3369 ops.set(
3370 CollectionModel::Kv,
3371 "kv_default",
3372 "name",
3373 reddb_types::Value::text("alice"),
3374 None,
3375 false,
3376 )
3377 .unwrap();
3378 let err = ops
3379 .incr(CollectionModel::Kv, "kv_default", "name", 1, None)
3380 .unwrap_err();
3381 assert!(err.to_string().contains("non-integer"));
3382 }
3383
3384 #[test]
3387 fn cas_matching_value_succeeds() {
3388 let r = rt();
3389 let ops = super::KvAtomicOps::new(&r);
3390 ops.set(
3391 CollectionModel::Kv,
3392 "kv_default",
3393 "lock",
3394 reddb_types::Value::text("free"),
3395 None,
3396 false,
3397 )
3398 .unwrap();
3399 let (ok, prev) = ops
3400 .cas(
3401 CollectionModel::Kv,
3402 "kv_default",
3403 "lock",
3404 Some(&reddb_types::Value::text("free")),
3405 reddb_types::Value::text("held"),
3406 None,
3407 )
3408 .unwrap();
3409 assert!(ok);
3410 assert_eq!(prev, Some(reddb_types::Value::text("free")));
3411 assert_eq!(
3413 ops.get(CollectionModel::Kv, "kv_default", "lock").unwrap(),
3414 Some(reddb_types::Value::text("held"))
3415 );
3416 }
3417
3418 #[test]
3419 fn concurrent_cas_allows_one_success_per_round() {
3420 const THREADS: usize = 8;
3421 const ROUNDS: usize = 100;
3422
3423 let runtime = std::sync::Arc::new(rt());
3424 let ops = super::KvAtomicOps::new(&runtime);
3425 ops.set(
3426 CollectionModel::Kv,
3427 "kv_default",
3428 "cas_counter",
3429 reddb_types::Value::Integer(0),
3430 None,
3431 false,
3432 )
3433 .unwrap();
3434
3435 let start_round = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
3436 let finish_round = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
3437 let successes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
3438 let mut handles = Vec::new();
3439
3440 for _ in 0..THREADS {
3441 let runtime = std::sync::Arc::clone(&runtime);
3442 let start_round = std::sync::Arc::clone(&start_round);
3443 let finish_round = std::sync::Arc::clone(&finish_round);
3444 let successes = std::sync::Arc::clone(&successes);
3445 handles.push(std::thread::spawn(move || {
3446 let ops = super::KvAtomicOps::new(&runtime);
3447 for round in 0..ROUNDS {
3448 start_round.wait();
3449 let (ok, _) = ops
3450 .cas(
3451 CollectionModel::Kv,
3452 "kv_default",
3453 "cas_counter",
3454 Some(&reddb_types::Value::Integer(round as i64)),
3455 reddb_types::Value::Integer((round + 1) as i64),
3456 None,
3457 )
3458 .unwrap();
3459 if ok {
3460 successes.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
3461 }
3462 finish_round.wait();
3463 }
3464 }));
3465 }
3466
3467 for handle in handles {
3468 handle.join().expect("worker should finish");
3469 }
3470
3471 assert_eq!(successes.load(std::sync::atomic::Ordering::SeqCst), ROUNDS);
3472 assert_eq!(
3473 ops.get(CollectionModel::Kv, "kv_default", "cas_counter")
3474 .unwrap(),
3475 Some(reddb_types::Value::Integer(ROUNDS as i64))
3476 );
3477 }
3478
3479 #[test]
3480 fn cas_mismatching_value_fails() {
3481 let r = rt();
3482 let ops = super::KvAtomicOps::new(&r);
3483 ops.set(
3484 CollectionModel::Kv,
3485 "kv_default",
3486 "lock",
3487 reddb_types::Value::text("free"),
3488 None,
3489 false,
3490 )
3491 .unwrap();
3492 let (ok, current) = ops
3493 .cas(
3494 CollectionModel::Kv,
3495 "kv_default",
3496 "lock",
3497 Some(&reddb_types::Value::text("held")),
3498 reddb_types::Value::text("worker-7"),
3499 None,
3500 )
3501 .unwrap();
3502 assert!(!ok);
3503 assert_eq!(current, Some(reddb_types::Value::text("free")));
3504 assert_eq!(
3506 ops.get(CollectionModel::Kv, "kv_default", "lock").unwrap(),
3507 Some(reddb_types::Value::text("free"))
3508 );
3509 }
3510
3511 #[test]
3512 fn cas_expect_null_on_missing_key_creates() {
3513 let r = rt();
3514 let ops = super::KvAtomicOps::new(&r);
3515 let (ok, prev) = ops
3516 .cas(
3517 CollectionModel::Kv,
3518 "kv_default",
3519 "new_key",
3520 None,
3521 reddb_types::Value::text("created"),
3522 None,
3523 )
3524 .unwrap();
3525 assert!(ok);
3526 assert_eq!(prev, None);
3527 assert_eq!(
3528 ops.get(CollectionModel::Kv, "kv_default", "new_key")
3529 .unwrap(),
3530 Some(reddb_types::Value::text("created"))
3531 );
3532 }
3533
3534 #[test]
3535 fn cas_expect_null_on_existing_key_fails() {
3536 let r = rt();
3537 let ops = super::KvAtomicOps::new(&r);
3538 ops.set(
3539 CollectionModel::Kv,
3540 "kv_default",
3541 "taken",
3542 reddb_types::Value::text("worker-1"),
3543 None,
3544 false,
3545 )
3546 .unwrap();
3547 let (ok, current) = ops
3548 .cas(
3549 CollectionModel::Kv,
3550 "kv_default",
3551 "taken",
3552 None,
3553 reddb_types::Value::text("worker-2"),
3554 None,
3555 )
3556 .unwrap();
3557 assert!(!ok);
3558 assert_eq!(current, Some(reddb_types::Value::text("worker-1")));
3559 }
3560
3561 #[test]
3562 fn cas_via_sql_roundtrip() {
3563 let r = rt();
3564 r.execute_query("KV PUT lock = 'free'").unwrap();
3566 let res = r
3568 .execute_query("KV CAS lock EXPECT 'free' SET 'held'")
3569 .unwrap();
3570 let row = &res.result.records[0];
3571 assert_eq!(row.get("ok"), Some(&reddb_types::Value::Boolean(true)));
3572 let res2 = r
3574 .execute_query("KV CAS lock EXPECT 'free' SET 'held'")
3575 .unwrap();
3576 let row2 = &res2.result.records[0];
3577 assert_eq!(row2.get("ok"), Some(&reddb_types::Value::Boolean(false)));
3578 }
3579
3580 #[test]
3581 fn cas_expect_null_via_sql() {
3582 let r = rt();
3583 let res = r
3584 .execute_query("KV CAS singleton EXPECT NULL SET 'first'")
3585 .unwrap();
3586 let row = &res.result.records[0];
3587 assert_eq!(row.get("ok"), Some(&reddb_types::Value::Boolean(true)));
3588 let res2 = r
3590 .execute_query("KV CAS singleton EXPECT NULL SET 'second'")
3591 .unwrap();
3592 let row2 = &res2.result.records[0];
3593 assert_eq!(row2.get("ok"), Some(&reddb_types::Value::Boolean(false)));
3594 }
3595
3596 #[test]
3597 fn incr_via_sql_roundtrip() {
3598 let r = rt();
3599 let res = r.execute_query("KV INCR hits").unwrap();
3600 let row = &res.result.records[0];
3601 assert_eq!(row.get("value"), Some(&reddb_types::Value::Integer(1)));
3602 let res2 = r.execute_query("KV INCR hits BY 4").unwrap();
3603 let row2 = &res2.result.records[0];
3604 assert_eq!(row2.get("value"), Some(&reddb_types::Value::Integer(5)));
3605 }
3606
3607 #[test]
3608 fn concurrent_self_referential_update_is_atomic() {
3609 const THREADS: usize = 8;
3610 const ITERS: usize = 100;
3611
3612 let runtime = std::sync::Arc::new(rt());
3613 runtime
3614 .execute_query("CREATE TABLE counters (id INT, n INT)")
3615 .unwrap();
3616 runtime
3617 .execute_query("INSERT INTO counters (id, n) VALUES (1, 0)")
3618 .unwrap();
3619
3620 let barrier = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
3621 let mut handles = Vec::new();
3622 for _ in 0..THREADS {
3623 let runtime = std::sync::Arc::clone(&runtime);
3624 let barrier = std::sync::Arc::clone(&barrier);
3625 handles.push(std::thread::spawn(move || {
3626 barrier.wait();
3627 for _ in 0..ITERS {
3628 runtime
3629 .execute_query("UPDATE counters SET n = n + 1 WHERE id = 1")
3630 .unwrap();
3631 }
3632 }));
3633 }
3634
3635 for handle in handles {
3636 handle.join().expect("worker should finish");
3637 }
3638
3639 let selected = runtime
3640 .execute_query("SELECT n FROM counters WHERE id = 1")
3641 .unwrap();
3642 assert_eq!(
3643 selected.result.records[0].get("n"),
3644 Some(&reddb_types::Value::Integer((THREADS * ITERS) as i64))
3645 );
3646 }
3647
3648 #[test]
3649 fn watch_stream_delivers_key_events_in_lsn_order() {
3650 let r = rt();
3651 let ops = super::KvAtomicOps::new(&r);
3652 let mut stream = r.kv_watch_subscribe("kv_default", "seq", None);
3653
3654 ops.set(
3655 CollectionModel::Kv,
3656 "kv_default",
3657 "seq",
3658 reddb_types::Value::Integer(1),
3659 None,
3660 false,
3661 )
3662 .unwrap();
3663 ops.incr(CollectionModel::Kv, "kv_default", "seq", 1, None)
3664 .unwrap();
3665 ops.delete(CollectionModel::Kv, "kv_default", "seq")
3666 .unwrap();
3667 ops.set(
3668 CollectionModel::Kv,
3669 "kv_default",
3670 "seq",
3671 reddb_types::Value::Integer(9),
3672 None,
3673 false,
3674 )
3675 .unwrap();
3676
3677 let mut events = Vec::new();
3678 while let Some(event) = stream.poll_next() {
3679 events.push(event);
3680 if events.len() == 4 {
3681 break;
3682 }
3683 }
3684
3685 assert_eq!(events.len(), 4);
3686 assert_eq!(
3687 events[0].op,
3688 crate::replication::cdc::ChangeOperation::Insert
3689 );
3690 assert_eq!(
3691 events[1].op,
3692 crate::replication::cdc::ChangeOperation::Update
3693 );
3694 assert_eq!(
3695 events[2].op,
3696 crate::replication::cdc::ChangeOperation::Delete
3697 );
3698 assert_eq!(
3699 events[3].op,
3700 crate::replication::cdc::ChangeOperation::Insert
3701 );
3702 assert!(events.windows(2).all(|pair| pair[0].lsn < pair[1].lsn));
3703 }
3704
3705 #[test]
3706 fn watch_prefix_stream_delivers_matching_events_only() {
3707 let r = rt();
3708 let ops = super::KvAtomicOps::new(&r);
3709 let mut stream = r.kv_watch_subscribe_prefix("kv_default", "acct:", None);
3710
3711 ops.set(
3712 CollectionModel::Kv,
3713 "kv_default",
3714 "acct:1",
3715 reddb_types::Value::Integer(1),
3716 None,
3717 false,
3718 )
3719 .unwrap();
3720 ops.set(
3721 CollectionModel::Kv,
3722 "kv_default",
3723 "session:1",
3724 reddb_types::Value::Integer(2),
3725 None,
3726 false,
3727 )
3728 .unwrap();
3729 ops.set(
3730 CollectionModel::Kv,
3731 "kv_default",
3732 "acct:2",
3733 reddb_types::Value::Integer(3),
3734 None,
3735 false,
3736 )
3737 .unwrap();
3738
3739 let first = stream.poll_next().expect("first prefix event");
3740 let second = stream.poll_next().expect("second prefix event");
3741 assert_eq!(first.key, "acct:1");
3742 assert_eq!(second.key, "acct:2");
3743 assert!(stream.poll_next().is_none());
3744 }
3745
3746 #[test]
3747 fn watch_stream_resume_from_lsn_delivers_missed_events_without_duplicates() {
3748 let r = rt();
3749 let ops = super::KvAtomicOps::new(&r);
3750 let mut stream = r.kv_watch_subscribe("kv_default", "resume", None);
3751
3752 let mut last_seen_lsn = 0;
3753 for value in 0..5 {
3754 ops.set(
3755 CollectionModel::Kv,
3756 "kv_default",
3757 "resume",
3758 reddb_types::Value::Integer(value),
3759 None,
3760 false,
3761 )
3762 .unwrap();
3763 last_seen_lsn = stream.poll_next().expect("initial event").lsn;
3764 }
3765 drop(stream);
3766
3767 for value in 5..55 {
3768 ops.set(
3769 CollectionModel::Kv,
3770 "kv_default",
3771 "resume",
3772 reddb_types::Value::Integer(value),
3773 None,
3774 false,
3775 )
3776 .unwrap();
3777 }
3778
3779 let expected_lsns: Vec<u64> = r
3780 .kv_watch_events_since("kv_default", "resume", last_seen_lsn, 200)
3781 .into_iter()
3782 .map(|event| event.lsn)
3783 .collect();
3784 assert!(!expected_lsns.is_empty());
3785
3786 let mut resumed = r.kv_watch_subscribe("kv_default", "resume", Some(last_seen_lsn));
3787 let mut lsns = Vec::new();
3788 while let Some(event) = resumed.poll_next() {
3789 lsns.push(event.lsn);
3790 if lsns.len() == expected_lsns.len() {
3791 break;
3792 }
3793 }
3794
3795 assert_eq!(lsns, expected_lsns);
3796 assert!(lsns.iter().all(|lsn| *lsn > last_seen_lsn));
3797 assert!(lsns.windows(2).all(|pair| pair[0] < pair[1]));
3798 assert!(resumed.poll_next().is_none());
3799 }
3800
3801 #[test]
3802 fn watch_stream_slow_consumer_drops_oldest_buffered_events() {
3803 let r = rt();
3804 let ops = super::KvAtomicOps::new(&r);
3805 let mut stream = r.kv_watch_subscribe("kv_default", "slow", None);
3806
3807 for value in 0..1_200 {
3808 ops.set(
3809 CollectionModel::Kv,
3810 "kv_default",
3811 "slow",
3812 reddb_types::Value::Integer(value),
3813 None,
3814 false,
3815 )
3816 .unwrap();
3817 }
3818
3819 let event = stream.poll_next().expect("tail event after drops");
3820 assert!(event.lsn > 1);
3821 assert!(event.dropped_event_count > 0);
3822 assert_eq!(stream.dropped_event_count(), event.dropped_event_count);
3823 assert_eq!(r.stats().kv.watch_drops, event.dropped_event_count);
3824 }
3825
3826 #[test]
3827 fn watch_stream_idle_timeout_closes_subscription() {
3828 let r = rt();
3829 r.execute_query("SET CONFIG red.config.kv.watch.idle_timeout_ms = 1")
3830 .unwrap();
3831
3832 let mut stream = r.kv_watch_subscribe("kv_default", "idle", None);
3833 assert_eq!(r.stats().kv.watch_streams_active, 1);
3834 std::thread::sleep(std::time::Duration::from_millis(5));
3835
3836 assert!(stream.poll_next().is_none());
3837 assert_eq!(r.stats().kv.watch_streams_active, 0);
3838 }
3839
3840 #[test]
3841 fn watch_stream_does_not_emit_rolled_back_put() {
3842 let r = rt();
3843 let mut stream = r.kv_watch_subscribe("kv_default", "rollback_key", None);
3844
3845 r.execute_query("BEGIN").unwrap();
3846 r.execute_query("KV PUT rollback_key = 'dirty'").unwrap();
3847 r.execute_query("ROLLBACK").unwrap();
3848
3849 assert!(stream.poll_next().is_none());
3850 }
3851}