1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use crate::api::{RedDBError, RedDBResult};
10use crate::storage::RedDB;
11use reddb_types::Value;
12
13thread_local! {
14 static CURRENT_CONN_ID: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
18
19 static CURRENT_AUTH_IDENTITY: std::cell::RefCell<Option<(String, crate::auth::Role)>> =
27 const { std::cell::RefCell::new(None) };
28
29 static CURRENT_SNAPSHOT: std::cell::RefCell<Option<SnapshotContext>> =
39 const { std::cell::RefCell::new(None) };
40
41 static HAS_SNAPSHOT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
47
48 static CURRENT_TENANT_ID: std::cell::RefCell<Option<String>> =
58 const { std::cell::RefCell::new(None) };
59
60 static CURRENT_CONFIG_RESOLVER: std::cell::RefCell<Option<ConfigResolver>> =
64 const { std::cell::RefCell::new(None) };
65
66 static CURRENT_SECRET_RESOLVER: std::cell::RefCell<Option<SecretResolver>> =
70 const { std::cell::RefCell::new(None) };
71
72 static CURRENT_KV_RESOLVER: std::cell::RefCell<Option<KvResolver>> =
76 const { std::cell::RefCell::new(None) };
77}
78
79#[derive(Clone)]
94pub struct SnapshotContext {
95 pub snapshot: crate::storage::transaction::snapshot::Snapshot,
96 pub manager: Arc<crate::storage::transaction::snapshot::SnapshotManager>,
97 pub own_xids: std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
98 pub requires_index_fallback: bool,
99 pub serializable_reader: Option<crate::storage::transaction::snapshot::Xid>,
100}
101
102pub fn set_current_connection_id(id: u64) {
111 CURRENT_CONN_ID.with(|c| c.set(id));
112}
113
114pub fn clear_current_connection_id() {
116 CURRENT_CONN_ID.with(|c| c.set(0));
117}
118
119pub fn current_connection_id() -> u64 {
122 CURRENT_CONN_ID.with(|c| c.get())
123}
124
125pub fn set_current_auth_identity(username: String, role: crate::auth::Role) {
129 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = Some((username, role)));
130}
131
132pub fn clear_current_auth_identity() {
136 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = None);
137}
138
139pub(crate) fn current_auth_identity() -> Option<(String, crate::auth::Role)> {
142 CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone())
143}
144
145pub fn current_auth_identity_for_audit() -> Option<(String, crate::auth::Role)> {
149 current_auth_identity()
150}
151
152pub fn set_current_tenant(tenant_id: String) {
157 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = Some(tenant_id));
158}
159
160pub fn clear_current_tenant() {
163 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = None);
164}
165
166pub fn current_tenant() -> Option<String> {
177 let inherited = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
178 if let Some(over) = current_scope_override() {
179 if over.tenant.is_active() {
180 return over.tenant.resolve(inherited);
181 }
182 }
183 if let Some(tx_local) = current_tx_local_tenant() {
184 return tx_local;
185 }
186 inherited
187}
188
189thread_local! {
190 static TX_LOCAL_TENANT: std::cell::RefCell<Option<Option<String>>> =
199 const { std::cell::RefCell::new(None) };
200}
201
202fn current_tx_local_tenant() -> Option<Option<String>> {
203 TX_LOCAL_TENANT.with(|cell| cell.borrow().clone())
204}
205
206pub(crate) fn parse_set_local_tenant(query: &str) -> RedDBResult<Option<Option<String>>> {
212 let mut tokens = query.split_ascii_whitespace();
213 let Some(w1) = tokens.next() else {
214 return Ok(None);
215 };
216 if !w1.eq_ignore_ascii_case("SET") {
217 return Ok(None);
218 }
219 let Some(w2) = tokens.next() else {
220 return Ok(None);
221 };
222 if !w2.eq_ignore_ascii_case("LOCAL") {
223 return Ok(None);
224 }
225 let Some(w3) = tokens.next() else {
226 return Ok(None);
227 };
228 if !w3.eq_ignore_ascii_case("TENANT") {
229 return Ok(None);
230 }
231 let rest: String = tokens.collect::<Vec<_>>().join(" ");
232 let rest = rest.trim().trim_end_matches(';').trim();
233 let value_str = rest.strip_prefix('=').map(|s| s.trim()).unwrap_or(rest);
234 if value_str.is_empty() {
235 return Err(RedDBError::Query(
236 "SET LOCAL TENANT expects a string literal or NULL".to_string(),
237 ));
238 }
239 if value_str.eq_ignore_ascii_case("NULL") {
240 return Ok(Some(None));
241 }
242 if value_str.starts_with('\'') && value_str.ends_with('\'') && value_str.len() >= 2 {
243 let inner = &value_str[1..value_str.len() - 1];
244 return Ok(Some(Some(inner.to_string())));
245 }
246 Err(RedDBError::Query(format!(
247 "SET LOCAL TENANT expects a string literal or NULL, got `{value_str}`"
248 )))
249}
250
251pub(crate) struct TxLocalTenantGuard;
252
253impl TxLocalTenantGuard {
254 pub fn install(value: Option<Option<String>>) -> Self {
255 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = value);
256 Self
257 }
258}
259
260impl Drop for TxLocalTenantGuard {
261 fn drop(&mut self) {
262 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = None);
263 }
264}
265
266thread_local! {
267 static SCOPE_OVERRIDES: std::cell::RefCell<Vec<crate::runtime::within_clause::ScopeOverride>> =
274 const { std::cell::RefCell::new(Vec::new()) };
275}
276
277pub(crate) fn push_scope_override(over: crate::runtime::within_clause::ScopeOverride) {
278 SCOPE_OVERRIDES.with(|cell| cell.borrow_mut().push(over));
279}
280
281pub(crate) fn pop_scope_override() {
282 SCOPE_OVERRIDES.with(|cell| {
283 cell.borrow_mut().pop();
284 });
285}
286
287pub(crate) fn current_scope_override() -> Option<crate::runtime::within_clause::ScopeOverride> {
288 SCOPE_OVERRIDES.with(|cell| cell.borrow().last().cloned())
289}
290
291pub(crate) fn has_scope_override_active() -> bool {
295 SCOPE_OVERRIDES.with(|cell| !cell.borrow().is_empty())
296}
297
298pub(crate) struct ScopeOverrideGuard;
302
303impl ScopeOverrideGuard {
304 pub fn install(over: crate::runtime::within_clause::ScopeOverride) -> Self {
305 push_scope_override(over);
306 Self
307 }
308}
309
310impl Drop for ScopeOverrideGuard {
311 fn drop(&mut self) {
312 pop_scope_override();
313 }
314}
315
316pub(crate) fn current_user_projected() -> Option<String> {
322 let inherited = current_auth_identity().map(|(u, _)| u);
323 if let Some(over) = current_scope_override() {
324 if over.user.is_active() {
325 return over.user.resolve(inherited);
326 }
327 }
328 inherited
329}
330
331pub(crate) fn current_role_projected() -> Option<String> {
332 let inherited = current_auth_identity().map(|(_, r)| format!("{r:?}").to_lowercase());
333 if let Some(over) = current_scope_override() {
334 if over.role.is_active() {
335 return over.role.resolve(inherited);
336 }
337 }
338 inherited
339}
340
341pub(crate) fn current_secret_value(path: &str) -> Option<Value> {
342 let key = path.to_ascii_lowercase();
343 CURRENT_SECRET_RESOLVER.with(|cell| {
344 let mut resolver = cell.borrow_mut();
345 let resolver = resolver.as_mut()?;
346 if let Some(value) = resolver.values.get(&key) {
347 return value.clone();
348 }
349 let value = resolver.runtime.query_secret_value(&key).ok().flatten();
350 resolver.values.insert(key, value.clone());
351 value
352 })
353}
354
355pub(crate) fn secret_query_input(value: Value) -> Option<Value> {
358 if !matches!(value, Value::Secret(_)) {
359 return Some(value);
360 }
361 CURRENT_SECRET_RESOLVER.with(|cell| {
362 let resolver = cell.borrow();
363 resolver.as_ref()?.runtime.secret_query_input(value)
364 })
365}
366
367pub(crate) fn secret_query_output(value: Value) -> crate::RedDBResult<Value> {
368 if value.is_null() || matches!(value, Value::Secret(_)) {
369 return Ok(value);
370 }
371 CURRENT_SECRET_RESOLVER.with(|cell| {
372 let resolver = cell.borrow();
373 let resolver = resolver.as_ref().ok_or_else(|| {
374 RedDBError::Query("secret expression requires an active statement".into())
375 })?;
376 resolver.runtime.secret_query_output(value)
377 })
378}
379
380#[cfg(test)]
381fn secret_value_from_snapshot(values: &HashMap<String, String>, key: &str) -> Option<String> {
382 if key.starts_with("red.secret.") {
383 return None;
384 }
385 values.get(key).cloned()
386}
387
388struct SecretResolver {
389 runtime: super::RedDBRuntime,
390 values: HashMap<String, Option<Value>>,
391}
392
393pub(crate) struct SecretStoreGuard {
394 previous: Option<SecretResolver>,
395}
396
397impl SecretStoreGuard {
398 pub(super) fn install(runtime: &super::RedDBRuntime) -> Self {
399 let previous = CURRENT_SECRET_RESOLVER.with(|cell| {
400 cell.replace(Some(SecretResolver {
401 runtime: runtime.clone(),
402 values: HashMap::new(),
403 }))
404 });
405 Self { previous }
406 }
407}
408
409impl Drop for SecretStoreGuard {
410 fn drop(&mut self) {
411 let previous = self.previous.take();
412 CURRENT_SECRET_RESOLVER.with(|cell| {
413 cell.replace(previous);
414 });
415 }
416}
417
418pub(crate) fn current_kv_value(path: &str) -> Option<String> {
426 let key = path.to_ascii_lowercase();
427 CURRENT_KV_RESOLVER.with(|cell| {
428 let mut resolver = cell.borrow_mut();
429 let resolver = resolver.as_mut()?;
430 if resolver.values.is_none() {
431 resolver.values = resolver
432 .store
433 .as_ref()
434 .map(|store| store.plain_kv_snapshot());
435 }
436 let values = resolver.values.as_ref()?;
437 let found = values
441 .get(&key)
442 .map(|value| (key.as_str(), value))
443 .or_else(|| {
444 key.strip_prefix("red.kv/")
445 .and_then(|rest| values.get(rest).map(|value| (rest, value)))
446 })?;
447 if !resolver.can_read(found.0) {
448 return None;
449 }
450 Some(found.1.clone())
451 })
452}
453
454struct KvResolver {
455 store: Option<Arc<crate::auth::store::AuthStore>>,
456 values: Option<HashMap<String, String>>,
457 identity: Option<(String, crate::auth::Role, Option<String>)>,
458}
459
460impl KvResolver {
461 fn can_read(&self, key: &str) -> bool {
462 let Some(store) = &self.store else {
463 return true;
464 };
465 let Some((username, role, tenant)) = &self.identity else {
466 return true;
467 };
468 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
469 let mut resource =
470 crate::auth::policies::ResourceRef::new("kv".to_string(), key.to_string());
471 if let Some(tenant) = tenant {
472 resource = resource.with_tenant(tenant.clone());
473 }
474 let ctx = crate::auth::policies::EvalContext {
475 principal_tenant: tenant.clone(),
476 current_tenant: tenant.clone(),
477 peer_ip: None,
478 mfa_present: false,
479 now_ms: crate::auth::now_ms(),
480 principal_is_admin_role: *role == crate::auth::Role::Admin,
481 principal_is_platform_scoped: tenant.is_none(),
482 };
483 store.check_policy_authz_with_role(&principal, "kv:read", &resource, &ctx, *role)
484 }
485}
486
487pub(crate) struct KvStoreGuard {
488 previous: Option<Box<KvResolver>>,
493}
494
495impl KvStoreGuard {
496 pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
497 let previous = CURRENT_KV_RESOLVER.with(|cell| {
498 cell.replace(Some(KvResolver {
499 store,
500 values: None,
501 identity: current_auth_identity().map(|(username, role)| {
502 let tenant = current_tenant();
503 (username, role, tenant)
504 }),
505 }))
506 });
507 Self {
508 previous: previous.map(Box::new),
509 }
510 }
511}
512
513impl Drop for KvStoreGuard {
514 fn drop(&mut self) {
515 let previous = self.previous.take().map(|b| *b);
516 CURRENT_KV_RESOLVER.with(|cell| {
517 cell.replace(previous);
518 });
519 }
520}
521
522pub(crate) fn update_current_kv_value(path: &str, value: Option<String>) {
525 let key = path.to_ascii_lowercase();
526 CURRENT_KV_RESOLVER.with(|cell| {
527 if let Some(resolver) = cell.borrow_mut().as_mut() {
528 let Some(values) = resolver.values.as_mut() else {
529 return;
530 };
531 match value {
532 Some(value) => {
533 values.insert(key, value);
534 }
535 None => {
536 values.remove(&key);
537 }
538 }
539 }
540 });
541}
542
543pub(crate) fn current_config_value(path: &str) -> Option<Value> {
544 let key = path.to_ascii_lowercase();
545 CURRENT_CONFIG_RESOLVER.with(|cell| {
546 let mut resolver = cell.borrow_mut();
547 let resolver = resolver.as_mut()?;
548 if resolver.values.is_none() {
549 resolver.values = Some(latest_config_snapshot(&resolver.db));
550 }
551 let found = {
557 let values = resolver.values.as_ref()?;
558 values
559 .get(&key)
560 .map(|value| (key.clone(), value.clone()))
561 .or_else(|| {
562 key.strip_prefix("red.config/").and_then(|rest| {
563 values
564 .get(rest)
565 .map(|value| (rest.to_string(), value.clone()))
566 .or_else(|| {
567 let legacy = format!("red.config.{rest}");
568 values.get(&legacy).map(|value| (legacy, value.clone()))
569 })
570 })
571 })
572 }?;
573 if !resolver.can_read(&found.0) {
577 return None;
578 }
579 Some(found.1)
580 })
581}
582
583pub(crate) fn update_current_config_value(path: &str, value: Value) {
584 let key = path.to_ascii_lowercase();
585 CURRENT_CONFIG_RESOLVER.with(|cell| {
586 if let Some(resolver) = cell.borrow_mut().as_mut() {
587 if let Some(values) = resolver.values.as_mut() {
588 values.insert(key, value);
589 }
590 }
591 });
592}
593
594pub(crate) fn update_current_secret_value(_path: &str, _value: Option<String>) {
595 CURRENT_SECRET_RESOLVER.with(|cell| {
596 if let Some(resolver) = cell.borrow_mut().as_mut() {
597 resolver.values.clear();
598 }
599 });
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
613 fn snapshot_thread_local_stays_private_to_the_statement_frame() {
614 let src = include_str!("execution_context.rs");
615
616 for name in ["CURRENT_SNAPSHOT", "HAS_SNAPSHOT"] {
617 assert!(
618 !src.contains(&format!("pub static {name}"))
619 && !src.contains(&format!("pub(crate) static {name}")),
620 "{name} must stay private to execution_context; expose an explicit \
621 Option<SnapshotContext> via capture_current_snapshot instead"
622 );
623 }
624
625 assert!(
626 src.contains("the only sanctioned ambient read"),
627 "capture_current_snapshot must keep documenting that it is the single \
628 sanctioned ambient read of the snapshot thread-local"
629 );
630 }
631
632 #[test]
633 fn secret_snapshot_hard_blocks_system_keys() {
634 let values = HashMap::from([
635 ("red.secret.aes_key".into(), "internal".into()),
636 ("acme.key".into(), "user-value".into()),
637 ]);
638 assert_eq!(
639 secret_value_from_snapshot(&values, "red.secret.aes_key"),
640 None
641 );
642 assert_eq!(
643 secret_value_from_snapshot(&values, "acme.key").as_deref(),
644 Some("user-value")
645 );
646 }
647
648 fn with_kv_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
649 CURRENT_KV_RESOLVER.with(|cell| {
650 cell.replace(Some(KvResolver {
651 store: None,
652 values: Some(values),
653 identity: None,
654 }));
655 });
656 let result = f();
657 CURRENT_KV_RESOLVER.with(|cell| {
658 cell.replace(None);
659 });
660 result
661 }
662
663 #[test]
664 fn current_kv_value_resolves_namespaced_and_bare_keys() {
665 let values = HashMap::from([("acme.key".to_string(), "plain-value".to_string())]);
666
667 with_kv_values(values, || {
668 assert_eq!(
670 current_kv_value("red.kv/acme.key").as_deref(),
671 Some("plain-value")
672 );
673 assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
675 assert_eq!(current_kv_value("red.kv/missing.key"), None);
677 });
678 }
679
680 #[test]
681 fn config_key_read_allowed_hard_blocks_reserved_namespace() {
682 assert!(!config_key_read_allowed(
685 None,
686 None,
687 "red.config/red.config.aes_key"
688 ));
689 assert!(!config_key_read_allowed(None, None, "red.config.aes_key"));
690 assert!(config_key_read_allowed(
692 None,
693 None,
694 "red.config/ai.openrouter.default.key"
695 ));
696 assert!(config_key_read_allowed(None, None, "acme.flags.beta"));
697 }
698
699 #[test]
700 fn is_config_collection_matches_system_config_stores() {
701 assert!(is_config_collection("red_config"));
702 assert!(is_config_collection("RED_CONFIG"));
703 assert!(is_config_collection("red.config"));
704 assert!(!is_config_collection("users"));
705 assert!(!is_config_collection("red_kv"));
706 }
707
708 #[test]
709 fn update_current_kv_value_reflects_in_resolver() {
710 with_kv_values(HashMap::new(), || {
711 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
712 update_current_kv_value("feature.flag", Some("on".to_string()));
713 assert_eq!(
714 current_kv_value("red.kv/feature.flag").as_deref(),
715 Some("on")
716 );
717 update_current_kv_value("feature.flag", None);
718 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
719 });
720 }
721}
722
723fn latest_config_snapshot(db: &RedDB) -> HashMap<String, Value> {
724 let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
725
726 if let Some(manager) = db.store().get_collection("red_config") {
727 manager.for_each_entity(|entity| {
728 let Some(row) = entity.data.as_row() else {
729 return true;
730 };
731 let Some(Value::Text(key)) = row.get_field("key") else {
732 return true;
733 };
734 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
735 let id = entity.id.raw();
736 let key = key.to_ascii_lowercase();
737 insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
738 if let Some(rest) = key.strip_prefix("red.config.") {
739 insert_latest_config_value(&mut latest, format!("red.config/{rest}"), id, value);
740 }
741 true
742 });
743 }
744
745 if let Some(manager) = db.store().get_collection("red.config") {
746 manager.for_each_entity(|entity| {
747 let Some(row) = entity.data.as_row() else {
748 return true;
749 };
750 if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
751 return true;
752 }
753 let Some(Value::Text(key)) = row.get_field("key") else {
754 return true;
755 };
756 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
757 insert_latest_config_value(
758 &mut latest,
759 format!("red.config/{}", key.to_ascii_lowercase()),
760 entity.id.raw(),
761 value,
762 );
763 true
764 });
765 }
766
767 latest
768 .into_iter()
769 .map(|(key, (_, value))| (key, value))
770 .collect()
771}
772
773fn insert_latest_config_value(
774 latest: &mut HashMap<String, (u64, Value)>,
775 key: String,
776 id: u64,
777 value: Value,
778) {
779 match latest.get(&key) {
780 Some((prev_id, _)) if *prev_id > id => {}
781 _ => {
782 latest.insert(key, (id, value));
783 }
784 }
785}
786
787struct ConfigResolver {
788 db: Arc<RedDB>,
789 store: Option<Arc<crate::auth::store::AuthStore>>,
790 identity: Option<(String, crate::auth::Role, Option<String>)>,
791 values: Option<HashMap<String, Value>>,
792}
793
794impl ConfigResolver {
795 fn can_read(&self, key: &str) -> bool {
796 config_key_read_allowed(self.store.as_ref(), self.identity.as_ref(), key)
797 }
798}
799
800fn is_reserved_config_key(key: &str) -> bool {
804 let key = key.strip_prefix("red.config/").unwrap_or(key);
805 key.starts_with("red.config.")
806 || key == "red.config"
807 || key == super::function_catalog::REGISTRY_KEY
808}
809
810fn is_config_collection(collection: &str) -> bool {
814 matches!(
815 collection.to_ascii_lowercase().as_str(),
816 "red_config" | "red.config"
817 )
818}
819
820fn config_resource_target(key: &str) -> String {
821 let bare = key.strip_prefix("red.config/").unwrap_or(key);
822 format!("red.config/{bare}")
823}
824
825fn config_key_read_allowed(
831 store: Option<&Arc<crate::auth::store::AuthStore>>,
832 identity: Option<&(String, crate::auth::Role, Option<String>)>,
833 key: &str,
834) -> bool {
835 if is_reserved_config_key(key) {
836 return false;
837 }
838 let Some(store) = store else {
839 return true;
840 };
841 if !store.iam_authorization_enabled() {
842 return true;
843 }
844 let Some((username, role, tenant)) = identity else {
845 return true;
846 };
847 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
848 let mut resource =
849 crate::auth::policies::ResourceRef::new("config".to_string(), config_resource_target(key));
850 if let Some(tenant) = tenant {
851 resource = resource.with_tenant(tenant.clone());
852 }
853 let ctx = crate::auth::policies::EvalContext {
854 principal_tenant: tenant.clone(),
855 current_tenant: tenant.clone(),
856 peer_ip: None,
857 mfa_present: false,
858 now_ms: crate::auth::now_ms(),
859 principal_is_admin_role: *role == crate::auth::Role::Admin,
860 principal_is_platform_scoped: tenant.is_none(),
861 };
862 store.check_policy_authz_with_role(&principal, "config:read", &resource, &ctx, *role)
863}
864
865pub(crate) fn config_read_permitted(key: &str) -> bool {
869 let key = key.to_ascii_lowercase();
870 CURRENT_CONFIG_RESOLVER.with(|cell| match cell.borrow().as_ref() {
871 Some(resolver) => resolver.can_read(&key),
872 None => !is_reserved_config_key(&key),
873 })
874}
875
876pub(crate) fn kv_read_permitted(collection: &str, key: &str) -> bool {
879 if is_config_collection(collection) {
880 config_read_permitted(key)
881 } else {
882 true
883 }
884}
885
886pub(crate) struct ConfigSnapshotGuard {
887 previous: Option<ConfigResolver>,
888}
889
890impl ConfigSnapshotGuard {
891 pub(super) fn install(
892 db: Arc<RedDB>,
893 store: Option<Arc<crate::auth::store::AuthStore>>,
894 ) -> Self {
895 let identity = current_auth_identity().map(|(username, role)| {
896 let tenant = current_tenant();
897 (username, role, tenant)
898 });
899 let previous = CURRENT_CONFIG_RESOLVER.with(|cell| {
900 cell.replace(Some(ConfigResolver {
901 db,
902 store,
903 identity,
904 values: None,
905 }))
906 });
907 Self { previous }
908 }
909}
910
911impl Drop for ConfigSnapshotGuard {
912 fn drop(&mut self) {
913 let previous = self.previous.take();
914 CURRENT_CONFIG_RESOLVER.with(|cell| {
915 cell.replace(previous);
916 });
917 }
918}
919
920pub fn set_current_snapshot(ctx: SnapshotContext) {
925 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
926 HAS_SNAPSHOT.with(|c| c.set(true));
927}
928
929pub fn clear_current_snapshot() {
930 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
931 HAS_SNAPSHOT.with(|c| c.set(false));
932}
933
934pub(crate) struct CurrentSnapshotGuard {
940 previous: Option<SnapshotContext>,
941}
942
943impl CurrentSnapshotGuard {
944 pub(crate) fn install(ctx: SnapshotContext) -> Self {
945 let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
946 set_current_snapshot(ctx);
947 Self { previous }
948 }
949}
950
951impl Drop for CurrentSnapshotGuard {
952 fn drop(&mut self) {
953 let prev = self.previous.take();
954 let has = prev.is_some();
955 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
956 HAS_SNAPSHOT.with(|c| c.set(has));
957 }
958}
959
960#[inline]
971pub fn entity_visible_under_current_snapshot(
972 entity: &crate::storage::unified::entity::UnifiedEntity,
973) -> bool {
974 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
981 return false;
982 }
983 if !HAS_SNAPSHOT.with(|c| c.get()) {
989 return entity.xmax == 0;
990 }
991 CURRENT_SNAPSHOT.with(|cell| {
992 let guard = cell.borrow();
993 let Some(ctx) = guard.as_ref() else {
994 return true;
995 };
996 let visible = visibility_check(ctx, entity.xmin, entity.xmax);
997 if visible {
998 record_serializable_read(ctx, entity);
999 }
1000 visible
1001 })
1002}
1003
1004#[inline]
1009pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
1010 if !HAS_SNAPSHOT.with(|c| c.get()) {
1011 return true;
1012 }
1013 CURRENT_SNAPSHOT.with(|cell| {
1014 let guard = cell.borrow();
1015 let Some(ctx) = guard.as_ref() else {
1016 return true;
1017 };
1018 visibility_check(ctx, xmin, xmax)
1019 })
1020}
1021
1022pub fn capture_current_snapshot() -> Option<SnapshotContext> {
1047 CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
1048}
1049
1050pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
1055 if !HAS_SNAPSHOT.with(|c| c.get()) {
1056 return false;
1057 }
1058 CURRENT_SNAPSHOT.with(|cell| {
1059 cell.borrow()
1060 .as_ref()
1061 .is_some_and(|ctx| ctx.requires_index_fallback)
1062 })
1063}
1064
1065#[derive(Clone, Default)]
1080pub struct SnapshotBundle {
1081 pub snapshot: Option<SnapshotContext>,
1082 pub auth: Option<(String, crate::auth::Role)>,
1083 pub tenant: Option<String>,
1084}
1085
1086pub fn snapshot_bundle() -> SnapshotBundle {
1089 SnapshotBundle {
1090 snapshot: capture_current_snapshot(),
1091 auth: current_auth_identity(),
1092 tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
1093 }
1094}
1095
1096pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
1101 struct Guard {
1102 prev_snapshot: Option<SnapshotContext>,
1103 prev_auth: Option<(String, crate::auth::Role)>,
1104 prev_tenant: Option<String>,
1105 }
1106 impl Drop for Guard {
1107 fn drop(&mut self) {
1108 let snap = self.prev_snapshot.take();
1109 let has = snap.is_some();
1110 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
1111 HAS_SNAPSHOT.with(|c| c.set(has));
1112 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
1113 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
1114 }
1115 }
1116
1117 let _guard = {
1118 let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1119 let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
1120 let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
1121
1122 match bundle.snapshot.clone() {
1123 Some(ctx) => set_current_snapshot(ctx),
1124 None => clear_current_snapshot(),
1125 }
1126 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
1127 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
1128
1129 Guard {
1130 prev_snapshot,
1131 prev_auth,
1132 prev_tenant,
1133 }
1134 };
1135 f()
1136}
1137
1138#[inline]
1142pub fn entity_visible_with_context(
1143 ctx: Option<&SnapshotContext>,
1144 entity: &crate::storage::unified::entity::UnifiedEntity,
1145) -> bool {
1146 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1150 return false;
1151 }
1152 match ctx {
1153 Some(ctx) => {
1154 let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1155 if visible {
1156 record_serializable_read(ctx, entity);
1157 }
1158 visible
1159 }
1160 None => true,
1161 }
1162}
1163
1164fn record_serializable_read(
1165 ctx: &SnapshotContext,
1166 entity: &crate::storage::unified::entity::UnifiedEntity,
1167) {
1168 let Some(reader) = ctx.serializable_reader else {
1169 return;
1170 };
1171 if !matches!(
1172 &entity.kind,
1173 crate::storage::unified::entity::EntityKind::TableRow { .. }
1174 ) {
1175 return;
1176 }
1177 ctx.manager
1178 .record_serializable_read(reader, entity.kind.collection(), entity.logical_id());
1179}
1180
1181#[inline]
1182fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
1183 if xmin != 0 && ctx.manager.is_aborted(xmin) {
1187 return false;
1188 }
1189 let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
1191 0
1192 } else {
1193 xmax
1194 };
1195 let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
1199 let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
1200 if own_xmax {
1201 return false;
1203 }
1204 if own_xmin {
1205 return true;
1206 }
1207 ctx.snapshot.sees(xmin, effective_xmax)
1208}