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