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