1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use crate::api::{RedDBError, RedDBResult};
10use crate::storage::schema::Value;
11use crate::storage::RedDB;
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}
100
101pub fn set_current_connection_id(id: u64) {
110 CURRENT_CONN_ID.with(|c| c.set(id));
111}
112
113pub fn clear_current_connection_id() {
115 CURRENT_CONN_ID.with(|c| c.set(0));
116}
117
118pub fn current_connection_id() -> u64 {
121 CURRENT_CONN_ID.with(|c| c.get())
122}
123
124pub fn set_current_auth_identity(username: String, role: crate::auth::Role) {
128 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = Some((username, role)));
129}
130
131pub fn clear_current_auth_identity() {
135 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = None);
136}
137
138pub(crate) fn current_auth_identity() -> Option<(String, crate::auth::Role)> {
141 CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone())
142}
143
144pub fn current_auth_identity_for_audit() -> Option<(String, crate::auth::Role)> {
148 current_auth_identity()
149}
150
151pub fn set_current_tenant(tenant_id: String) {
156 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = Some(tenant_id));
157}
158
159pub fn clear_current_tenant() {
162 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = None);
163}
164
165pub fn current_tenant() -> Option<String> {
176 let inherited = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
177 if let Some(over) = current_scope_override() {
178 if over.tenant.is_active() {
179 return over.tenant.resolve(inherited);
180 }
181 }
182 if let Some(tx_local) = current_tx_local_tenant() {
183 return tx_local;
184 }
185 inherited
186}
187
188thread_local! {
189 static TX_LOCAL_TENANT: std::cell::RefCell<Option<Option<String>>> =
198 const { std::cell::RefCell::new(None) };
199}
200
201fn current_tx_local_tenant() -> Option<Option<String>> {
202 TX_LOCAL_TENANT.with(|cell| cell.borrow().clone())
203}
204
205pub(crate) fn parse_set_local_tenant(query: &str) -> RedDBResult<Option<Option<String>>> {
211 let mut tokens = query.split_ascii_whitespace();
212 let Some(w1) = tokens.next() else {
213 return Ok(None);
214 };
215 if !w1.eq_ignore_ascii_case("SET") {
216 return Ok(None);
217 }
218 let Some(w2) = tokens.next() else {
219 return Ok(None);
220 };
221 if !w2.eq_ignore_ascii_case("LOCAL") {
222 return Ok(None);
223 }
224 let Some(w3) = tokens.next() else {
225 return Ok(None);
226 };
227 if !w3.eq_ignore_ascii_case("TENANT") {
228 return Ok(None);
229 }
230 let rest: String = tokens.collect::<Vec<_>>().join(" ");
231 let rest = rest.trim().trim_end_matches(';').trim();
232 let value_str = rest.strip_prefix('=').map(|s| s.trim()).unwrap_or(rest);
233 if value_str.is_empty() {
234 return Err(RedDBError::Query(
235 "SET LOCAL TENANT expects a string literal or NULL".to_string(),
236 ));
237 }
238 if value_str.eq_ignore_ascii_case("NULL") {
239 return Ok(Some(None));
240 }
241 if value_str.starts_with('\'') && value_str.ends_with('\'') && value_str.len() >= 2 {
242 let inner = &value_str[1..value_str.len() - 1];
243 return Ok(Some(Some(inner.to_string())));
244 }
245 Err(RedDBError::Query(format!(
246 "SET LOCAL TENANT expects a string literal or NULL, got `{value_str}`"
247 )))
248}
249
250pub(crate) struct TxLocalTenantGuard;
251
252impl TxLocalTenantGuard {
253 pub fn install(value: Option<Option<String>>) -> Self {
254 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = value);
255 Self
256 }
257}
258
259impl Drop for TxLocalTenantGuard {
260 fn drop(&mut self) {
261 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = None);
262 }
263}
264
265thread_local! {
266 static SCOPE_OVERRIDES: std::cell::RefCell<Vec<crate::runtime::within_clause::ScopeOverride>> =
273 const { std::cell::RefCell::new(Vec::new()) };
274}
275
276pub(crate) fn push_scope_override(over: crate::runtime::within_clause::ScopeOverride) {
277 SCOPE_OVERRIDES.with(|cell| cell.borrow_mut().push(over));
278}
279
280pub(crate) fn pop_scope_override() {
281 SCOPE_OVERRIDES.with(|cell| {
282 cell.borrow_mut().pop();
283 });
284}
285
286pub(crate) fn current_scope_override() -> Option<crate::runtime::within_clause::ScopeOverride> {
287 SCOPE_OVERRIDES.with(|cell| cell.borrow().last().cloned())
288}
289
290pub(crate) fn has_scope_override_active() -> bool {
294 SCOPE_OVERRIDES.with(|cell| !cell.borrow().is_empty())
295}
296
297pub(crate) struct ScopeOverrideGuard;
301
302impl ScopeOverrideGuard {
303 pub fn install(over: crate::runtime::within_clause::ScopeOverride) -> Self {
304 push_scope_override(over);
305 Self
306 }
307}
308
309impl Drop for ScopeOverrideGuard {
310 fn drop(&mut self) {
311 pop_scope_override();
312 }
313}
314
315pub(crate) fn current_user_projected() -> Option<String> {
321 let inherited = current_auth_identity().map(|(u, _)| u);
322 if let Some(over) = current_scope_override() {
323 if over.user.is_active() {
324 return over.user.resolve(inherited);
325 }
326 }
327 inherited
328}
329
330pub(crate) fn current_role_projected() -> Option<String> {
331 let inherited = current_auth_identity().map(|(_, r)| format!("{r:?}").to_lowercase());
332 if let Some(over) = current_scope_override() {
333 if over.role.is_active() {
334 return over.role.resolve(inherited);
335 }
336 }
337 inherited
338}
339
340pub(crate) fn current_secret_value(path: &str) -> Option<String> {
341 let key = path.to_ascii_lowercase();
342 CURRENT_SECRET_RESOLVER.with(|cell| {
343 let mut resolver = cell.borrow_mut();
344 let resolver = resolver.as_mut()?;
345 if resolver.values.is_none() {
346 resolver.values = resolver
347 .store
348 .as_ref()
349 .map(|store| store.vault_kv_snapshot());
350 }
351 let values = resolver.values.as_ref()?;
352 let found = values
353 .get(&key)
354 .map(|value| (key.as_str(), value))
355 .or_else(|| {
356 key.strip_prefix("red.vault/").and_then(|rest| {
357 values.get(rest).map(|value| (rest, value)).or_else(|| {
358 let red_secret_key = format!("red.secret.{rest}");
359 values
360 .get_key_value(&red_secret_key)
361 .map(|(key, value)| (key.as_str(), value))
362 })
363 })
364 })?;
365 if !resolver.can_read(found.0) {
366 return None;
367 }
368 Some(found.1.clone())
369 })
370}
371
372fn secret_value_from_snapshot(values: &HashMap<String, String>, key: &str) -> Option<String> {
373 if key.starts_with("red.secret.") {
374 return None;
375 }
376 values.get(key).cloned()
377}
378
379struct SecretResolver {
380 store: Option<Arc<crate::auth::store::AuthStore>>,
381 values: Option<HashMap<String, String>>,
382 identity: Option<(String, crate::auth::Role, Option<String>)>,
383}
384
385impl SecretResolver {
386 fn can_read(&self, key: &str) -> bool {
387 if key.starts_with("red.secret.") {
390 return false;
391 }
392 let Some(store) = &self.store else {
393 return true;
394 };
395 let Some((username, role, tenant)) = &self.identity else {
396 return true;
397 };
398 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
399 let mut resource =
400 crate::auth::policies::ResourceRef::new("secret".to_string(), key.to_string());
401 if let Some(tenant) = tenant {
402 resource = resource.with_tenant(tenant.clone());
403 }
404 let ctx = crate::auth::policies::EvalContext {
405 principal_tenant: tenant.clone(),
406 current_tenant: tenant.clone(),
407 peer_ip: None,
408 mfa_present: false,
409 now_ms: crate::auth::now_ms(),
410 principal_is_admin_role: *role == crate::auth::Role::Admin,
411 principal_is_platform_scoped: tenant.is_none(),
412 };
413 store.check_policy_authz_with_role(&principal, "secret:read", &resource, &ctx, *role)
414 }
415}
416
417pub(crate) struct SecretStoreGuard {
418 previous: Option<SecretResolver>,
419}
420
421impl SecretStoreGuard {
422 pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
423 let previous = CURRENT_SECRET_RESOLVER.with(|cell| {
424 cell.replace(Some(SecretResolver {
425 store,
426 values: None,
427 identity: current_auth_identity().map(|(username, role)| {
428 let tenant = current_tenant();
429 (username, role, tenant)
430 }),
431 }))
432 });
433 Self { previous }
434 }
435}
436
437impl Drop for SecretStoreGuard {
438 fn drop(&mut self) {
439 let previous = self.previous.take();
440 CURRENT_SECRET_RESOLVER.with(|cell| {
441 cell.replace(previous);
442 });
443 }
444}
445
446pub(crate) fn current_kv_value(path: &str) -> Option<String> {
454 let key = path.to_ascii_lowercase();
455 CURRENT_KV_RESOLVER.with(|cell| {
456 let mut resolver = cell.borrow_mut();
457 let resolver = resolver.as_mut()?;
458 if resolver.values.is_none() {
459 resolver.values = resolver
460 .store
461 .as_ref()
462 .map(|store| store.plain_kv_snapshot());
463 }
464 let values = resolver.values.as_ref()?;
465 let found = values
469 .get(&key)
470 .map(|value| (key.as_str(), value))
471 .or_else(|| {
472 key.strip_prefix("red.kv/")
473 .and_then(|rest| values.get(rest).map(|value| (rest, value)))
474 })?;
475 if !resolver.can_read(found.0) {
476 return None;
477 }
478 Some(found.1.clone())
479 })
480}
481
482struct KvResolver {
483 store: Option<Arc<crate::auth::store::AuthStore>>,
484 values: Option<HashMap<String, String>>,
485 identity: Option<(String, crate::auth::Role, Option<String>)>,
486}
487
488impl KvResolver {
489 fn can_read(&self, key: &str) -> bool {
490 let Some(store) = &self.store else {
491 return true;
492 };
493 let Some((username, role, tenant)) = &self.identity else {
494 return true;
495 };
496 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
497 let mut resource =
498 crate::auth::policies::ResourceRef::new("kv".to_string(), key.to_string());
499 if let Some(tenant) = tenant {
500 resource = resource.with_tenant(tenant.clone());
501 }
502 let ctx = crate::auth::policies::EvalContext {
503 principal_tenant: tenant.clone(),
504 current_tenant: tenant.clone(),
505 peer_ip: None,
506 mfa_present: false,
507 now_ms: crate::auth::now_ms(),
508 principal_is_admin_role: *role == crate::auth::Role::Admin,
509 principal_is_platform_scoped: tenant.is_none(),
510 };
511 store.check_policy_authz_with_role(&principal, "kv:read", &resource, &ctx, *role)
512 }
513}
514
515pub(crate) struct KvStoreGuard {
516 previous: Option<Box<KvResolver>>,
521}
522
523impl KvStoreGuard {
524 pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
525 let previous = CURRENT_KV_RESOLVER.with(|cell| {
526 cell.replace(Some(KvResolver {
527 store,
528 values: None,
529 identity: current_auth_identity().map(|(username, role)| {
530 let tenant = current_tenant();
531 (username, role, tenant)
532 }),
533 }))
534 });
535 Self {
536 previous: previous.map(Box::new),
537 }
538 }
539}
540
541impl Drop for KvStoreGuard {
542 fn drop(&mut self) {
543 let previous = self.previous.take().map(|b| *b);
544 CURRENT_KV_RESOLVER.with(|cell| {
545 cell.replace(previous);
546 });
547 }
548}
549
550pub(crate) fn update_current_kv_value(path: &str, value: Option<String>) {
553 let key = path.to_ascii_lowercase();
554 CURRENT_KV_RESOLVER.with(|cell| {
555 if let Some(resolver) = cell.borrow_mut().as_mut() {
556 let Some(values) = resolver.values.as_mut() else {
557 return;
558 };
559 match value {
560 Some(value) => {
561 values.insert(key, value);
562 }
563 None => {
564 values.remove(&key);
565 }
566 }
567 }
568 });
569}
570
571pub(crate) fn current_config_value(path: &str) -> Option<Value> {
572 let key = path.to_ascii_lowercase();
573 CURRENT_CONFIG_RESOLVER.with(|cell| {
574 let mut resolver = cell.borrow_mut();
575 let resolver = resolver.as_mut()?;
576 if resolver.values.is_none() {
577 resolver.values = Some(latest_config_snapshot(&resolver.db));
578 }
579 let values = resolver.values.as_ref()?;
580 values.get(&key).cloned().or_else(|| {
585 key.strip_prefix("red.config/").and_then(|rest| {
586 values
587 .get(rest)
588 .cloned()
589 .or_else(|| values.get(&format!("red.config.{rest}")).cloned())
590 })
591 })
592 })
593}
594
595pub(crate) fn update_current_config_value(path: &str, value: Value) {
596 let key = path.to_ascii_lowercase();
597 CURRENT_CONFIG_RESOLVER.with(|cell| {
598 if let Some(resolver) = cell.borrow_mut().as_mut() {
599 if let Some(values) = resolver.values.as_mut() {
600 values.insert(key, value);
601 }
602 }
603 });
604}
605
606pub(crate) fn update_current_secret_value(path: &str, value: Option<String>) {
607 let key = path.to_ascii_lowercase();
608 CURRENT_SECRET_RESOLVER.with(|cell| {
609 if let Some(resolver) = cell.borrow_mut().as_mut() {
610 let Some(values) = resolver.values.as_mut() else {
611 return;
612 };
613 match value {
614 Some(value) => {
615 values.insert(key, value);
616 }
617 None => {
618 values.remove(&key);
619 }
620 }
621 }
622 });
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 fn with_secret_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
630 CURRENT_SECRET_RESOLVER.with(|cell| {
631 cell.replace(Some(SecretResolver {
632 store: None,
633 values: Some(values),
634 identity: None,
635 }));
636 });
637 let result = f();
638 CURRENT_SECRET_RESOLVER.with(|cell| {
639 cell.replace(None);
640 });
641 result
642 }
643
644 #[test]
645 fn current_secret_value_does_not_fall_back_to_reserved_red_secret_namespace() {
646 let values = HashMap::from([
647 (
648 "red.secret.aes_key".to_string(),
649 "vault-aes-key".to_string(),
650 ),
651 (
652 "red.secret.ai.anthropic.default.api_key".to_string(),
653 "provider-key".to_string(),
654 ),
655 ("acme.key".to_string(), "user-value".to_string()),
656 ]);
657
658 with_secret_values(values, || {
659 assert_eq!(current_secret_value("red.vault/aes_key"), None);
660 assert_eq!(current_secret_value("red.vault/red.secret.aes_key"), None);
661 assert_eq!(
662 current_secret_value("red.vault/ai.anthropic.default.api_key"),
663 None
664 );
665 assert_eq!(
666 current_secret_value("red.vault/acme.key").as_deref(),
667 Some("user-value")
668 );
669 });
670 }
671
672 fn with_kv_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
673 CURRENT_KV_RESOLVER.with(|cell| {
674 cell.replace(Some(KvResolver {
675 store: None,
676 values: Some(values),
677 identity: None,
678 }));
679 });
680 let result = f();
681 CURRENT_KV_RESOLVER.with(|cell| {
682 cell.replace(None);
683 });
684 result
685 }
686
687 #[test]
688 fn current_kv_value_resolves_namespaced_and_bare_keys() {
689 let values = HashMap::from([("acme.key".to_string(), "plain-value".to_string())]);
690
691 with_kv_values(values, || {
692 assert_eq!(
694 current_kv_value("red.kv/acme.key").as_deref(),
695 Some("plain-value")
696 );
697 assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
699 assert_eq!(current_kv_value("red.kv/missing.key"), None);
701 });
702 }
703
704 #[test]
705 fn update_current_kv_value_reflects_in_resolver() {
706 with_kv_values(HashMap::new(), || {
707 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
708 update_current_kv_value("feature.flag", Some("on".to_string()));
709 assert_eq!(
710 current_kv_value("red.kv/feature.flag").as_deref(),
711 Some("on")
712 );
713 update_current_kv_value("feature.flag", None);
714 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
715 });
716 }
717}
718
719fn latest_config_snapshot(db: &RedDB) -> HashMap<String, Value> {
720 let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
721
722 if let Some(manager) = db.store().get_collection("red_config") {
723 manager.for_each_entity(|entity| {
724 let Some(row) = entity.data.as_row() else {
725 return true;
726 };
727 let Some(Value::Text(key)) = row.get_field("key") else {
728 return true;
729 };
730 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
731 let id = entity.id.raw();
732 let key = key.to_ascii_lowercase();
733 insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
734 if let Some(rest) = key.strip_prefix("red.config.") {
735 insert_latest_config_value(&mut latest, format!("red.config/{rest}"), id, value);
736 }
737 true
738 });
739 }
740
741 if let Some(manager) = db.store().get_collection("red.config") {
742 manager.for_each_entity(|entity| {
743 let Some(row) = entity.data.as_row() else {
744 return true;
745 };
746 if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
747 return true;
748 }
749 let Some(Value::Text(key)) = row.get_field("key") else {
750 return true;
751 };
752 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
753 insert_latest_config_value(
754 &mut latest,
755 format!("red.config/{}", key.to_ascii_lowercase()),
756 entity.id.raw(),
757 value,
758 );
759 true
760 });
761 }
762
763 latest
764 .into_iter()
765 .map(|(key, (_, value))| (key, value))
766 .collect()
767}
768
769fn insert_latest_config_value(
770 latest: &mut HashMap<String, (u64, Value)>,
771 key: String,
772 id: u64,
773 value: Value,
774) {
775 match latest.get(&key) {
776 Some((prev_id, _)) if *prev_id > id => {}
777 _ => {
778 latest.insert(key, (id, value));
779 }
780 }
781}
782
783struct ConfigResolver {
784 db: Arc<RedDB>,
785 values: Option<HashMap<String, Value>>,
786}
787
788pub(crate) struct ConfigSnapshotGuard {
789 previous: Option<ConfigResolver>,
790}
791
792impl ConfigSnapshotGuard {
793 pub(super) fn install(db: Arc<RedDB>) -> Self {
794 let previous = CURRENT_CONFIG_RESOLVER
795 .with(|cell| cell.replace(Some(ConfigResolver { db, values: None })));
796 Self { previous }
797 }
798}
799
800impl Drop for ConfigSnapshotGuard {
801 fn drop(&mut self) {
802 let previous = self.previous.take();
803 CURRENT_CONFIG_RESOLVER.with(|cell| {
804 cell.replace(previous);
805 });
806 }
807}
808
809pub fn set_current_snapshot(ctx: SnapshotContext) {
814 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
815 HAS_SNAPSHOT.with(|c| c.set(true));
816}
817
818pub fn clear_current_snapshot() {
819 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
820 HAS_SNAPSHOT.with(|c| c.set(false));
821}
822
823pub(crate) struct CurrentSnapshotGuard {
829 previous: Option<SnapshotContext>,
830}
831
832impl CurrentSnapshotGuard {
833 pub(crate) fn install(ctx: SnapshotContext) -> Self {
834 let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
835 set_current_snapshot(ctx);
836 Self { previous }
837 }
838}
839
840impl Drop for CurrentSnapshotGuard {
841 fn drop(&mut self) {
842 let prev = self.previous.take();
843 let has = prev.is_some();
844 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
845 HAS_SNAPSHOT.with(|c| c.set(has));
846 }
847}
848
849#[inline]
860pub fn entity_visible_under_current_snapshot(
861 entity: &crate::storage::unified::entity::UnifiedEntity,
862) -> bool {
863 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
870 return false;
871 }
872 if !HAS_SNAPSHOT.with(|c| c.get()) {
878 return entity.xmax == 0;
879 }
880 CURRENT_SNAPSHOT.with(|cell| {
881 let guard = cell.borrow();
882 let Some(ctx) = guard.as_ref() else {
883 return true;
884 };
885 visibility_check(ctx, entity.xmin, entity.xmax)
886 })
887}
888
889#[inline]
894pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
895 if !HAS_SNAPSHOT.with(|c| c.get()) {
896 return true;
897 }
898 CURRENT_SNAPSHOT.with(|cell| {
899 let guard = cell.borrow();
900 let Some(ctx) = guard.as_ref() else {
901 return true;
902 };
903 visibility_check(ctx, xmin, xmax)
904 })
905}
906
907pub fn capture_current_snapshot() -> Option<SnapshotContext> {
914 CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
915}
916
917pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
922 if !HAS_SNAPSHOT.with(|c| c.get()) {
923 return false;
924 }
925 CURRENT_SNAPSHOT.with(|cell| {
926 cell.borrow()
927 .as_ref()
928 .is_some_and(|ctx| ctx.requires_index_fallback)
929 })
930}
931
932#[derive(Clone, Default)]
947pub struct SnapshotBundle {
948 pub snapshot: Option<SnapshotContext>,
949 pub auth: Option<(String, crate::auth::Role)>,
950 pub tenant: Option<String>,
951}
952
953pub fn snapshot_bundle() -> SnapshotBundle {
956 SnapshotBundle {
957 snapshot: capture_current_snapshot(),
958 auth: current_auth_identity(),
959 tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
960 }
961}
962
963pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
968 struct Guard {
969 prev_snapshot: Option<SnapshotContext>,
970 prev_auth: Option<(String, crate::auth::Role)>,
971 prev_tenant: Option<String>,
972 }
973 impl Drop for Guard {
974 fn drop(&mut self) {
975 let snap = self.prev_snapshot.take();
976 let has = snap.is_some();
977 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
978 HAS_SNAPSHOT.with(|c| c.set(has));
979 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
980 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
981 }
982 }
983
984 let _guard = {
985 let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
986 let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
987 let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
988
989 match bundle.snapshot.clone() {
990 Some(ctx) => set_current_snapshot(ctx),
991 None => clear_current_snapshot(),
992 }
993 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
994 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
995
996 Guard {
997 prev_snapshot,
998 prev_auth,
999 prev_tenant,
1000 }
1001 };
1002 f()
1003}
1004
1005#[inline]
1009pub fn entity_visible_with_context(
1010 ctx: Option<&SnapshotContext>,
1011 entity: &crate::storage::unified::entity::UnifiedEntity,
1012) -> bool {
1013 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1017 return false;
1018 }
1019 match ctx {
1020 Some(ctx) => visibility_check(ctx, entity.xmin, entity.xmax),
1021 None => true,
1022 }
1023}
1024
1025#[inline]
1026fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
1027 if xmin != 0 && ctx.manager.is_aborted(xmin) {
1031 return false;
1032 }
1033 let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
1035 0
1036 } else {
1037 xmax
1038 };
1039 let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
1043 let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
1044 if own_xmax {
1045 return false;
1047 }
1048 if own_xmin {
1049 return true;
1050 }
1051 ctx.snapshot.sees(xmin, effective_xmax)
1052}