Skip to main content

reddb_server/runtime/
execution_context.rs

1//! Per-thread runtime execution context.
2//!
3//! This module owns connection identity, auth/tenant scope, statement-local
4//! config/secret resolvers, and MVCC snapshot visibility helpers.
5
6use 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    /// Current connection id for the executing statement. Set by the
15    /// per-connection wrapper (stdio/gRPC handlers) before dispatching
16    /// into `execute_query`; falls back to `0` for embedded callers.
17    static CURRENT_CONN_ID: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
18
19    /// Authenticated user + role for the executing statement (Phase 2.5.2
20    /// RLS enforcement). Set by the transport middleware after validating
21    /// credentials (password / cert / oauth); unset means "anonymous" /
22    /// "embedded" — RLS policies degrade to the role-agnostic subset.
23    ///
24    /// `None` skips RLS injection entirely; `Some((username, role))`
25    /// passes `role` to `matching_rls_policies(table, Some(role), action)`.
26    static CURRENT_AUTH_IDENTITY: std::cell::RefCell<Option<(String, crate::auth::Role)>> =
27        const { std::cell::RefCell::new(None) };
28
29    /// MVCC snapshot scoped to the currently-executing statement (Phase
30    /// 2.3.2d PG parity). `execute_query` captures it on entry and drops
31    /// it on exit; every scan consults it via
32    /// `entity_visible_under_current_snapshot` to hide tuples whose xmin
33    /// hasn't committed or whose xmax already has.
34    ///
35    /// `None` means "pre-MVCC semantics" — the read path returns every
36    /// tuple regardless of xmin/xmax. All embedded callers that bypass
37    /// `execute_query` see this default.
38    static CURRENT_SNAPSHOT: std::cell::RefCell<Option<SnapshotContext>> =
39        const { std::cell::RefCell::new(None) };
40
41    /// Cheap presence flag for `CURRENT_SNAPSHOT`. Scan hot paths
42    /// poll this instead of `borrow()`-ing the RefCell on every
43    /// row — the common case (autocommit / no MVCC session) reads
44    /// one atomic `Cell<bool>` and short-circuits, saving ~10ns × N
45    /// rows on aggregate_group / select_range scans.
46    static HAS_SNAPSHOT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
47
48    /// Session-scoped tenant id for the current connection (Phase 2.5.3
49    /// multi-tenancy). Populated by `SET TENANT 'id'` or by transport
50    /// middleware after resolving tenant from auth claims. Read by the
51    /// `CURRENT_TENANT()` scalar function — RLS policies typically
52    /// combine it as `USING (tenant_id = CURRENT_TENANT())` to scope
53    /// every query to one tenant.
54    ///
55    /// `None` means "no tenant bound" — `CURRENT_TENANT()` returns
56    /// NULL, and RLS policies that gate on it hide every row.
57    static CURRENT_TENANT_ID: std::cell::RefCell<Option<String>> =
58        const { std::cell::RefCell::new(None) };
59
60    /// Statement-local config resolver. SQL expressions materialize the
61    /// `red_config` snapshot lazily on the first `$config.*`/`CONFIG()`
62    /// access, keeping ordinary statements on the zero-scan path.
63    static CURRENT_CONFIG_RESOLVER: std::cell::RefCell<Option<ConfigResolver>> =
64        const { std::cell::RefCell::new(None) };
65
66    /// Statement-local secret resolver. SQL expressions materialize the
67    /// vault KV snapshot lazily on first `$secret.*` access, then use
68    /// lock-free map reads for the rest of the statement.
69    static CURRENT_SECRET_RESOLVER: std::cell::RefCell<Option<SecretResolver>> =
70        const { std::cell::RefCell::new(None) };
71
72    /// Statement-local plain-KV resolver (#1602). Mirrors the secret
73    /// resolver but reads the non-encrypted `plain_kv` store and gates on
74    /// `kv:read`. Materialized lazily on the first `$kv.*` access.
75    static CURRENT_KV_RESOLVER: std::cell::RefCell<Option<KvResolver>> =
76        const { std::cell::RefCell::new(None) };
77}
78
79/// Snapshot + manager pair used for read-path visibility checks.
80///
81/// The manager is needed in addition to the snapshot because `aborted`
82/// state mutates after the snapshot is captured — a ROLLBACK by a
83/// committed-at-capture-time writer must still hide its tuples. Keeping
84/// the Arc around is O(pointer) and the RwLock reads on `is_aborted`
85/// are cheap (HashSet lookup under a parking_lot read guard).
86///
87/// `own_xids` (Phase 2.3.2e) lists the xids belonging to the current
88/// connection's transaction — the parent xid plus open and released
89/// savepoint sub-xids. The visibility rule promotes rows stamped with
90/// these xids to "always visible (unless aborted)" so the writer sees
91/// its own nested-savepoint writes even though their xids exceed
92/// `snapshot.xid`.
93#[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
102/// Install a connection id on the current thread for the duration of a
103/// statement. Transaction state (`RuntimeInner::tx_contexts`) is keyed
104/// by this id so different connections can hold independent BEGINs.
105///
106/// Pub so transports (PG wire, gRPC, HTTP per-request spawners) and
107/// tests can emulate per-connection isolation. Call it once when
108/// binding the connection's worker thread; pair with
109/// `clear_current_connection_id` on teardown.
110pub fn set_current_connection_id(id: u64) {
111    CURRENT_CONN_ID.with(|c| c.set(id));
112}
113
114/// Reset the thread's connection id back to `0` (autocommit).
115pub fn clear_current_connection_id() {
116    CURRENT_CONN_ID.with(|c| c.set(0));
117}
118
119/// Read the connection id set by `set_current_connection_id`. Returns
120/// `0` when no wrapper installed one — auto-commit path.
121pub fn current_connection_id() -> u64 {
122    CURRENT_CONN_ID.with(|c| c.get())
123}
124
125/// Install the authenticated identity for the current thread (Phase 2.5.2
126/// RLS enforcement). Transport layers call this right after resolving
127/// auth so the query dispatch can fold RLS policies into the filter.
128pub 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
132/// Clear the thread-local auth identity. Transports call this after the
133/// statement completes so pooled threads don't leak identities across
134/// requests.
135pub fn clear_current_auth_identity() {
136    CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = None);
137}
138
139/// Read the current-thread auth identity. `None` when no transport
140/// installed one (embedded mode / anonymous access).
141pub(crate) fn current_auth_identity() -> Option<(String, crate::auth::Role)> {
142    CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone())
143}
144
145/// Public probe of the thread-local auth identity for callers outside
146/// the `runtime` module (e.g. the AI credential resolver, which audits
147/// who triggered a secret read on behalf of a query).
148pub fn current_auth_identity_for_audit() -> Option<(String, crate::auth::Role)> {
149    current_auth_identity()
150}
151
152/// Install the session tenant id for the current thread (Phase 2.5.3
153/// multi-tenancy). Called by `SET TENANT 'id'` dispatch and by
154/// transport middleware that resolves tenant from auth claims (e.g.
155/// JWT `tenant` claim, HTTP header, subdomain).
156pub fn set_current_tenant(tenant_id: String) {
157    CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = Some(tenant_id));
158}
159
160/// Clear the current-thread tenant — `CURRENT_TENANT()` will then
161/// return NULL and any RLS policy gated on it will hide every row.
162pub fn clear_current_tenant() {
163    CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = None);
164}
165
166/// Read the current-thread tenant id, applying overrides in priority order:
167///   1. `WITHIN TENANT '<id>' …` per-statement override (highest)
168///   2. `SET LOCAL TENANT '<id>'` transaction-local override (consulted
169///      only when the current connection has an open transaction)
170///   3. `SET TENANT '<id>'` session-level thread-local
171///   4. `None` (deny-default for RLS).
172///
173/// The transaction-local layer is read through the runtime; an embedded
174/// helper crate that has no `RedDBRuntime` access still gets correct
175/// behaviour for layers 1, 3, and 4.
176pub 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    /// Snapshot of the active connection's `tx_local_tenants` entry for
191    /// the current `execute_query` call. Outer `Some(_)` means "a
192    /// transaction-local tenant override is active for this call";
193    /// inner is the override's value (`Some(s)` overrides to `s`,
194    /// `None` overrides to NULL/cleared). Refreshed at the top of every
195    /// `execute_query` invocation and cleared by the RAII guard on
196    /// return so pooled connections cannot leak the override past the
197    /// statement that owns it.
198    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
206/// Recognise `SET LOCAL TENANT '<id>'` / `SET LOCAL TENANT NULL` —
207/// returns `Ok(Some(Some(id)))` for an explicit value, `Ok(Some(None))`
208/// for an explicit NULL clear, `Ok(None)` when the input is not a
209/// `SET LOCAL TENANT` statement at all, and `Err` when the prefix
210/// matches but the value is malformed.
211pub(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    /// Stack of `WITHIN ... <stmt>` overrides active on the current
268    /// thread. Every entry corresponds to one in-flight `execute_query`
269    /// call that started with a `WITHIN` prefix; the entry is pushed
270    /// before dispatch and popped before the call returns. The stack
271    /// shape supports nested invocations (e.g. a view body that itself
272    /// re-enters execute_query).
273    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
291/// Cheap probe: is any `WITHIN …` scope override active on this
292/// thread? The fast-path needs to know without paying for the full
293/// `.last().cloned()` allocation — just peek at stack length.
294pub(crate) fn has_scope_override_active() -> bool {
295    SCOPE_OVERRIDES.with(|cell| !cell.borrow().is_empty())
296}
297
298/// RAII guard pairing `push_scope_override` with the matching pop, so
299/// the stack stays balanced even when the inner `execute_query` returns
300/// early via `?`.
301pub(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
316/// Read the current-thread auth identity, honouring per-statement
317/// `WITHIN ... USER '<u>' AS ROLE '<r>'` overrides. The override only
318/// supplies projected strings — it never grants additional privilege —
319/// so callers that need to make authorisation decisions must read from
320/// the underlying `current_auth_identity()` directly.
321pub(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<String> {
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 resolver.values.is_none() {
347            resolver.values = resolver
348                .store
349                .as_ref()
350                .map(|store| store.vault_kv_snapshot());
351        }
352        let values = resolver.values.as_ref()?;
353        let found = values
354            .get(&key)
355            .map(|value| (key.as_str(), value))
356            .or_else(|| {
357                key.strip_prefix("red.vault/").and_then(|rest| {
358                    values.get(rest).map(|value| (rest, value)).or_else(|| {
359                        let red_secret_key = format!("red.secret.{rest}");
360                        values
361                            .get_key_value(&red_secret_key)
362                            .map(|(key, value)| (key.as_str(), value))
363                    })
364                })
365            })?;
366        if !resolver.can_read(found.0) {
367            return None;
368        }
369        Some(found.1.clone())
370    })
371}
372
373fn secret_value_from_snapshot(values: &HashMap<String, String>, key: &str) -> Option<String> {
374    if key.starts_with("red.secret.") {
375        return None;
376    }
377    values.get(key).cloned()
378}
379
380struct SecretResolver {
381    store: Option<Arc<crate::auth::store::AuthStore>>,
382    values: Option<HashMap<String, String>>,
383    identity: Option<(String, crate::auth::Role, Option<String>)>,
384}
385
386impl SecretResolver {
387    fn can_read(&self, key: &str) -> bool {
388        // `red.secret.*` is the internal system-secrets namespace. Never
389        // expose it via `$secret.X` regardless of IAM role — not even admin.
390        if key.starts_with("red.secret.") {
391            return false;
392        }
393        let Some(store) = &self.store else {
394            return true;
395        };
396        let Some((username, role, tenant)) = &self.identity else {
397            return true;
398        };
399        let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
400        let mut resource =
401            crate::auth::policies::ResourceRef::new("secret".to_string(), key.to_string());
402        if let Some(tenant) = tenant {
403            resource = resource.with_tenant(tenant.clone());
404        }
405        let ctx = crate::auth::policies::EvalContext {
406            principal_tenant: tenant.clone(),
407            current_tenant: tenant.clone(),
408            peer_ip: None,
409            mfa_present: false,
410            now_ms: crate::auth::now_ms(),
411            principal_is_admin_role: *role == crate::auth::Role::Admin,
412            principal_is_platform_scoped: tenant.is_none(),
413        };
414        store.check_policy_authz_with_role(&principal, "secret:read", &resource, &ctx, *role)
415    }
416}
417
418pub(crate) struct SecretStoreGuard {
419    previous: Option<SecretResolver>,
420}
421
422impl SecretStoreGuard {
423    pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
424        let previous = CURRENT_SECRET_RESOLVER.with(|cell| {
425            cell.replace(Some(SecretResolver {
426                store,
427                values: None,
428                identity: current_auth_identity().map(|(username, role)| {
429                    let tenant = current_tenant();
430                    (username, role, tenant)
431                }),
432            }))
433        });
434        Self { previous }
435    }
436}
437
438impl Drop for SecretStoreGuard {
439    fn drop(&mut self) {
440        let previous = self.previous.take();
441        CURRENT_SECRET_RESOLVER.with(|cell| {
442            cell.replace(previous);
443        });
444    }
445}
446
447/// Resolve `$kv.<path>` from the plain (non-encrypted) KV store (#1602).
448///
449/// The parser desugars `$kv.<path>` to `__KV_REF("red.kv/<path>")`; this
450/// strips the `red.kv/` prefix and looks up the bare key in the
451/// statement-local snapshot, gating each hit on `kv:read`. Returns `None`
452/// (→ SQL NULL) when the key is absent or the principal is denied — never
453/// an error, mirroring the secret resolver's deny-to-NULL behaviour.
454pub(crate) fn current_kv_value(path: &str) -> Option<String> {
455    let key = path.to_ascii_lowercase();
456    CURRENT_KV_RESOLVER.with(|cell| {
457        let mut resolver = cell.borrow_mut();
458        let resolver = resolver.as_mut()?;
459        if resolver.values.is_none() {
460            resolver.values = resolver
461                .store
462                .as_ref()
463                .map(|store| store.plain_kv_snapshot());
464        }
465        let values = resolver.values.as_ref()?;
466        // Accept both the namespaced `red.kv/<key>` form (from the
467        // `$kv.*` desugar) and the bare key (from `update_current_kv_value`
468        // after a `SET KV`).
469        let found = values
470            .get(&key)
471            .map(|value| (key.as_str(), value))
472            .or_else(|| {
473                key.strip_prefix("red.kv/")
474                    .and_then(|rest| values.get(rest).map(|value| (rest, value)))
475            })?;
476        if !resolver.can_read(found.0) {
477            return None;
478        }
479        Some(found.1.clone())
480    })
481}
482
483struct KvResolver {
484    store: Option<Arc<crate::auth::store::AuthStore>>,
485    values: Option<HashMap<String, String>>,
486    identity: Option<(String, crate::auth::Role, Option<String>)>,
487}
488
489impl KvResolver {
490    fn can_read(&self, key: &str) -> bool {
491        let Some(store) = &self.store else {
492            return true;
493        };
494        let Some((username, role, tenant)) = &self.identity else {
495            return true;
496        };
497        let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
498        let mut resource =
499            crate::auth::policies::ResourceRef::new("kv".to_string(), key.to_string());
500        if let Some(tenant) = tenant {
501            resource = resource.with_tenant(tenant.clone());
502        }
503        let ctx = crate::auth::policies::EvalContext {
504            principal_tenant: tenant.clone(),
505            current_tenant: tenant.clone(),
506            peer_ip: None,
507            mfa_present: false,
508            now_ms: crate::auth::now_ms(),
509            principal_is_admin_role: *role == crate::auth::Role::Admin,
510            principal_is_platform_scoped: tenant.is_none(),
511        };
512        store.check_policy_authz_with_role(&principal, "kv:read", &resource, &ctx, *role)
513    }
514}
515
516pub(crate) struct KvStoreGuard {
517    // Boxed to keep the stack footprint of nested guards small. Each
518    // StatementFrameGuards is live for the full recursive call depth of a
519    // materialized-view refresh chain; unboxed KvResolver (≈120 B) would
520    // push those deep paths past the 8 MB stack limit.
521    previous: Option<Box<KvResolver>>,
522}
523
524impl KvStoreGuard {
525    pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
526        let previous = CURRENT_KV_RESOLVER.with(|cell| {
527            cell.replace(Some(KvResolver {
528                store,
529                values: None,
530                identity: current_auth_identity().map(|(username, role)| {
531                    let tenant = current_tenant();
532                    (username, role, tenant)
533                }),
534            }))
535        });
536        Self {
537            previous: previous.map(Box::new),
538        }
539    }
540}
541
542impl Drop for KvStoreGuard {
543    fn drop(&mut self) {
544        let previous = self.previous.take().map(|b| *b);
545        CURRENT_KV_RESOLVER.with(|cell| {
546            cell.replace(previous);
547        });
548    }
549}
550
551/// Update the statement-local KV snapshot after a `SET KV` / `DELETE KV`
552/// so a subsequent `$kv.*` read in the same statement observes the write.
553pub(crate) fn update_current_kv_value(path: &str, value: Option<String>) {
554    let key = path.to_ascii_lowercase();
555    CURRENT_KV_RESOLVER.with(|cell| {
556        if let Some(resolver) = cell.borrow_mut().as_mut() {
557            let Some(values) = resolver.values.as_mut() else {
558                return;
559            };
560            match value {
561                Some(value) => {
562                    values.insert(key, value);
563                }
564                None => {
565                    values.remove(&key);
566                }
567            }
568        }
569    });
570}
571
572pub(crate) fn current_config_value(path: &str) -> Option<Value> {
573    let key = path.to_ascii_lowercase();
574    CURRENT_CONFIG_RESOLVER.with(|cell| {
575        let mut resolver = cell.borrow_mut();
576        let resolver = resolver.as_mut()?;
577        if resolver.values.is_none() {
578            resolver.values = Some(latest_config_snapshot(&resolver.db));
579        }
580        let values = resolver.values.as_ref()?;
581        // #1370 — `$config.<path>` desugars to `CONFIG("red.config/<path>")`,
582        // but `SET CONFIG <path>` stores under the bare key. Mirror the secret
583        // resolver: after the namespaced key, fall back to the stripped bare
584        // key, then the dotted `red.config.<rest>` legacy form.
585        values.get(&key).cloned().or_else(|| {
586            key.strip_prefix("red.config/").and_then(|rest| {
587                values
588                    .get(rest)
589                    .cloned()
590                    .or_else(|| values.get(&format!("red.config.{rest}")).cloned())
591            })
592        })
593    })
594}
595
596pub(crate) fn update_current_config_value(path: &str, value: Value) {
597    let key = path.to_ascii_lowercase();
598    CURRENT_CONFIG_RESOLVER.with(|cell| {
599        if let Some(resolver) = cell.borrow_mut().as_mut() {
600            if let Some(values) = resolver.values.as_mut() {
601                values.insert(key, value);
602            }
603        }
604    });
605}
606
607pub(crate) fn update_current_secret_value(path: &str, value: Option<String>) {
608    let key = path.to_ascii_lowercase();
609    CURRENT_SECRET_RESOLVER.with(|cell| {
610        if let Some(resolver) = cell.borrow_mut().as_mut() {
611            let Some(values) = resolver.values.as_mut() else {
612                return;
613            };
614            match value {
615                Some(value) => {
616                    values.insert(key, value);
617                }
618                None => {
619                    values.remove(&key);
620                }
621            }
622        }
623    });
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    fn with_secret_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
631        CURRENT_SECRET_RESOLVER.with(|cell| {
632            cell.replace(Some(SecretResolver {
633                store: None,
634                values: Some(values),
635                identity: None,
636            }));
637        });
638        let result = f();
639        CURRENT_SECRET_RESOLVER.with(|cell| {
640            cell.replace(None);
641        });
642        result
643    }
644
645    #[test]
646    fn current_secret_value_does_not_fall_back_to_reserved_red_secret_namespace() {
647        let values = HashMap::from([
648            (
649                "red.secret.aes_key".to_string(),
650                "vault-aes-key".to_string(),
651            ),
652            (
653                "red.secret.ai.anthropic.default.api_key".to_string(),
654                "provider-key".to_string(),
655            ),
656            ("acme.key".to_string(), "user-value".to_string()),
657        ]);
658
659        with_secret_values(values, || {
660            assert_eq!(current_secret_value("red.vault/aes_key"), None);
661            assert_eq!(current_secret_value("red.vault/red.secret.aes_key"), None);
662            assert_eq!(
663                current_secret_value("red.vault/ai.anthropic.default.api_key"),
664                None
665            );
666            assert_eq!(
667                current_secret_value("red.vault/acme.key").as_deref(),
668                Some("user-value")
669            );
670        });
671    }
672
673    fn with_kv_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
674        CURRENT_KV_RESOLVER.with(|cell| {
675            cell.replace(Some(KvResolver {
676                store: None,
677                values: Some(values),
678                identity: None,
679            }));
680        });
681        let result = f();
682        CURRENT_KV_RESOLVER.with(|cell| {
683            cell.replace(None);
684        });
685        result
686    }
687
688    #[test]
689    fn current_kv_value_resolves_namespaced_and_bare_keys() {
690        let values = HashMap::from([("acme.key".to_string(), "plain-value".to_string())]);
691
692        with_kv_values(values, || {
693            // `$kv.acme.key` desugars to `red.kv/acme.key`.
694            assert_eq!(
695                current_kv_value("red.kv/acme.key").as_deref(),
696                Some("plain-value")
697            );
698            // Bare-key lookup (post-`SET KV` snapshot update) also resolves.
699            assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
700            // Unknown keys resolve to None (→ SQL NULL), never an error.
701            assert_eq!(current_kv_value("red.kv/missing.key"), None);
702        });
703    }
704
705    #[test]
706    fn update_current_kv_value_reflects_in_resolver() {
707        with_kv_values(HashMap::new(), || {
708            assert_eq!(current_kv_value("red.kv/feature.flag"), None);
709            update_current_kv_value("feature.flag", Some("on".to_string()));
710            assert_eq!(
711                current_kv_value("red.kv/feature.flag").as_deref(),
712                Some("on")
713            );
714            update_current_kv_value("feature.flag", None);
715            assert_eq!(current_kv_value("red.kv/feature.flag"), None);
716        });
717    }
718}
719
720fn latest_config_snapshot(db: &RedDB) -> HashMap<String, Value> {
721    let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
722
723    if let Some(manager) = db.store().get_collection("red_config") {
724        manager.for_each_entity(|entity| {
725            let Some(row) = entity.data.as_row() else {
726                return true;
727            };
728            let Some(Value::Text(key)) = row.get_field("key") else {
729                return true;
730            };
731            let value = row.get_field("value").cloned().unwrap_or(Value::Null);
732            let id = entity.id.raw();
733            let key = key.to_ascii_lowercase();
734            insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
735            if let Some(rest) = key.strip_prefix("red.config.") {
736                insert_latest_config_value(&mut latest, format!("red.config/{rest}"), id, value);
737            }
738            true
739        });
740    }
741
742    if let Some(manager) = db.store().get_collection("red.config") {
743        manager.for_each_entity(|entity| {
744            let Some(row) = entity.data.as_row() else {
745                return true;
746            };
747            if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
748                return true;
749            }
750            let Some(Value::Text(key)) = row.get_field("key") else {
751                return true;
752            };
753            let value = row.get_field("value").cloned().unwrap_or(Value::Null);
754            insert_latest_config_value(
755                &mut latest,
756                format!("red.config/{}", key.to_ascii_lowercase()),
757                entity.id.raw(),
758                value,
759            );
760            true
761        });
762    }
763
764    latest
765        .into_iter()
766        .map(|(key, (_, value))| (key, value))
767        .collect()
768}
769
770fn insert_latest_config_value(
771    latest: &mut HashMap<String, (u64, Value)>,
772    key: String,
773    id: u64,
774    value: Value,
775) {
776    match latest.get(&key) {
777        Some((prev_id, _)) if *prev_id > id => {}
778        _ => {
779            latest.insert(key, (id, value));
780        }
781    }
782}
783
784struct ConfigResolver {
785    db: Arc<RedDB>,
786    values: Option<HashMap<String, Value>>,
787}
788
789pub(crate) struct ConfigSnapshotGuard {
790    previous: Option<ConfigResolver>,
791}
792
793impl ConfigSnapshotGuard {
794    pub(super) fn install(db: Arc<RedDB>) -> Self {
795        let previous = CURRENT_CONFIG_RESOLVER
796            .with(|cell| cell.replace(Some(ConfigResolver { db, values: None })));
797        Self { previous }
798    }
799}
800
801impl Drop for ConfigSnapshotGuard {
802    fn drop(&mut self) {
803        let previous = self.previous.take();
804        CURRENT_CONFIG_RESOLVER.with(|cell| {
805            cell.replace(previous);
806        });
807    }
808}
809
810/// Install the MVCC snapshot used by the current thread for the duration
811/// of one statement. Paired with `clear_current_snapshot()` — callers
812/// should prefer the `CurrentSnapshotGuard` RAII wrapper so early returns
813/// still clean up.
814pub fn set_current_snapshot(ctx: SnapshotContext) {
815    CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
816    HAS_SNAPSHOT.with(|c| c.set(true));
817}
818
819pub fn clear_current_snapshot() {
820    CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
821    HAS_SNAPSHOT.with(|c| c.set(false));
822}
823
824/// Drop-guard that restores the previous snapshot on scope exit. Safe to
825/// nest — each statement saves the caller's snapshot and puts it back
826/// instead of blindly clearing, so a top-level `execute_query` called
827/// from inside another statement dispatch (e.g. vector source subqueries)
828/// doesn't strip visibility from the outer scan.
829pub(crate) struct CurrentSnapshotGuard {
830    previous: Option<SnapshotContext>,
831}
832
833impl CurrentSnapshotGuard {
834    pub(crate) fn install(ctx: SnapshotContext) -> Self {
835        let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
836        set_current_snapshot(ctx);
837        Self { previous }
838    }
839}
840
841impl Drop for CurrentSnapshotGuard {
842    fn drop(&mut self) {
843        let prev = self.previous.take();
844        let has = prev.is_some();
845        CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
846        HAS_SNAPSHOT.with(|c| c.set(has));
847    }
848}
849
850/// Is this entity visible under the current thread's MVCC snapshot?
851///
852/// Returns `true` (no filtering) when no snapshot is installed — that
853/// path is used by embedded callers and by operations that intentionally
854/// bypass MVCC (VACUUM, snapshot export, admin introspection).
855///
856/// When a snapshot is installed the result is
857///   `snapshot.sees(xmin, xmax) && !mgr.is_aborted(xmin) && !xmax_half_abort`
858/// where `xmax_half_abort` re-grants visibility for tuples whose
859/// deleting transaction rolled back.
860#[inline]
861pub fn entity_visible_under_current_snapshot(
862    entity: &crate::storage::unified::entity::UnifiedEntity,
863) -> bool {
864    // Moderation visibility gate (#1274, ADR 0057). A row carrying the
865    // moderation status marker — quarantine-pending or rejected-tombstone
866    // — is hidden from every normal read, on top of MVCC visibility. The
867    // marker lives on the row itself, so the check is a single field probe
868    // and rides the existing per-row visibility chokepoint rather than
869    // adding a separate filter pass to each scan call-site.
870    if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
871        return false;
872    }
873    // Fast path — one `Cell<bool>` read, no RefCell borrow. Autocommit
874    // reads (no active MVCC transaction) still hide superseded physical
875    // versions while avoiding a full snapshot-context lookup.
876    // This runs on every row of every scan; the slow path only fires
877    // inside an explicit transaction.
878    if !HAS_SNAPSHOT.with(|c| c.get()) {
879        return entity.xmax == 0;
880    }
881    CURRENT_SNAPSHOT.with(|cell| {
882        let guard = cell.borrow();
883        let Some(ctx) = guard.as_ref() else {
884            return true;
885        };
886        let visible = visibility_check(ctx, entity.xmin, entity.xmax);
887        if visible {
888            record_serializable_read(ctx, entity);
889        }
890        visible
891    })
892}
893
894/// Direct visibility check from raw `(xmin, xmax)` — bypasses the
895/// entity borrow for callers that already decomposed the tuple (e.g.
896/// pre-materialized scan caches). Same semantics as
897/// `entity_visible_under_current_snapshot`.
898#[inline]
899pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
900    if !HAS_SNAPSHOT.with(|c| c.get()) {
901        return true;
902    }
903    CURRENT_SNAPSHOT.with(|cell| {
904        let guard = cell.borrow();
905        let Some(ctx) = guard.as_ref() else {
906            return true;
907        };
908        visibility_check(ctx, xmin, xmax)
909    })
910}
911
912/// Clone the current thread's snapshot context. Parallel scan paths
913/// (`query_all_zoned` with `std::thread::scope`) call this on the main
914/// thread *before* spawning workers so the captured `SnapshotContext`
915/// can be moved into every worker closure. Worker threads do not
916/// inherit thread-locals, so calling `entity_visible_under_current_snapshot`
917/// from inside a spawned closure would silently skip the filter.
918pub fn capture_current_snapshot() -> Option<SnapshotContext> {
919    CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
920}
921
922/// Whether the active read snapshot may need historical tuple versions
923/// that the current secondary indexes cannot prove. Index paths can still
924/// recheck visible candidates, but only a heap scan can discover versions
925/// whose indexed value was changed or deleted after this snapshot.
926pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
927    if !HAS_SNAPSHOT.with(|c| c.get()) {
928        return false;
929    }
930    CURRENT_SNAPSHOT.with(|cell| {
931        cell.borrow()
932            .as_ref()
933            .is_some_and(|ctx| ctx.requires_index_fallback)
934    })
935}
936
937/// Frozen MVCC + identity context for callers that need to reinstall
938/// the same view across thread-local boundaries — long-lived cursors,
939/// background batchers, anything that detaches from the dispatch path
940/// and re-enters later.
941///
942/// The bundle bakes in the three thread-locals every read path
943/// consults: `SnapshotContext` (MVCC visibility), the auth identity
944/// (RLS policy gate), and the tenant id (RLS scope). A FETCH that
945/// reinstalls the bundle sees exactly the same rows as the DECLARE
946/// would have, regardless of writes that landed in between.
947///
948/// Cheap to clone — `SnapshotContext` is a clone of three
949/// `Arc`-backed fields, identity is a `(String, Role)`, tenant is a
950/// `String`. None of these contend with the read path.
951#[derive(Clone, Default)]
952pub struct SnapshotBundle {
953    pub snapshot: Option<SnapshotContext>,
954    pub auth: Option<(String, crate::auth::Role)>,
955    pub tenant: Option<String>,
956}
957
958/// Capture the three read-path thread-locals into a `SnapshotBundle`.
959/// Pairs with `with_snapshot_bundle` for re-entry.
960pub fn snapshot_bundle() -> SnapshotBundle {
961    SnapshotBundle {
962        snapshot: capture_current_snapshot(),
963        auth: current_auth_identity(),
964        tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
965    }
966}
967
968/// Reinstall a captured `SnapshotBundle` for the duration of `f`.
969/// Restores the caller's previous thread-locals on exit (panic-safe via
970/// the explicit guard struct so a panic in `f` cannot leak the
971/// installed identity into the worker's next request).
972pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
973    struct Guard {
974        prev_snapshot: Option<SnapshotContext>,
975        prev_auth: Option<(String, crate::auth::Role)>,
976        prev_tenant: Option<String>,
977    }
978    impl Drop for Guard {
979        fn drop(&mut self) {
980            let snap = self.prev_snapshot.take();
981            let has = snap.is_some();
982            CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
983            HAS_SNAPSHOT.with(|c| c.set(has));
984            CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
985            CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
986        }
987    }
988
989    let _guard = {
990        let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
991        let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
992        let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
993
994        match bundle.snapshot.clone() {
995            Some(ctx) => set_current_snapshot(ctx),
996            None => clear_current_snapshot(),
997        }
998        CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
999        CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
1000
1001        Guard {
1002            prev_snapshot,
1003            prev_auth,
1004            prev_tenant,
1005        }
1006    };
1007    f()
1008}
1009
1010/// Apply the same visibility rules used by the thread-local helpers
1011/// against a caller-provided context. Intended for parallel workers
1012/// that captured the snapshot with `capture_current_snapshot()`.
1013#[inline]
1014pub fn entity_visible_with_context(
1015    ctx: Option<&SnapshotContext>,
1016    entity: &crate::storage::unified::entity::UnifiedEntity,
1017) -> bool {
1018    // Same moderation visibility gate as the thread-local path (#1274):
1019    // parallel scan workers capture the snapshot context but must apply
1020    // the moderation marker check identically.
1021    if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1022        return false;
1023    }
1024    match ctx {
1025        Some(ctx) => {
1026            let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1027            if visible {
1028                record_serializable_read(ctx, entity);
1029            }
1030            visible
1031        }
1032        None => true,
1033    }
1034}
1035
1036fn record_serializable_read(
1037    ctx: &SnapshotContext,
1038    entity: &crate::storage::unified::entity::UnifiedEntity,
1039) {
1040    let Some(reader) = ctx.serializable_reader else {
1041        return;
1042    };
1043    if !matches!(
1044        &entity.kind,
1045        crate::storage::unified::entity::EntityKind::TableRow { .. }
1046    ) {
1047        return;
1048    }
1049    ctx.manager
1050        .record_serializable_read(reader, entity.kind.collection(), entity.logical_id());
1051}
1052
1053#[inline]
1054fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
1055    // Writer aborted → tuple never existed from any future reader's view.
1056    // Checked *before* the own-xids fast path so an aborted own-sub-xid
1057    // (rolled-back savepoint) stays hidden from the parent.
1058    if xmin != 0 && ctx.manager.is_aborted(xmin) {
1059        return false;
1060    }
1061    // Deleter aborted → treat xmax as unset; fall back to xmin-only check.
1062    let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
1063        0
1064    } else {
1065        xmax
1066    };
1067    // Phase 2.3.2e: own-tx writes are always visible to the connection
1068    // that stamped them, even when xmin/xmax exceed `snapshot.xid` (as
1069    // happens for sub-xids allocated by SAVEPOINT after BEGIN).
1070    let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
1071    let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
1072    if own_xmax {
1073        // This connection deleted the row via this xid — hide it from self.
1074        return false;
1075    }
1076    if own_xmin {
1077        return true;
1078    }
1079    ctx.snapshot.sees(xmin, effective_xmax)
1080}