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}
100
101/// Install a connection id on the current thread for the duration of a
102/// statement. Transaction state (`RuntimeInner::tx_contexts`) is keyed
103/// by this id so different connections can hold independent BEGINs.
104///
105/// Pub so transports (PG wire, gRPC, HTTP per-request spawners) and
106/// tests can emulate per-connection isolation. Call it once when
107/// binding the connection's worker thread; pair with
108/// `clear_current_connection_id` on teardown.
109pub fn set_current_connection_id(id: u64) {
110    CURRENT_CONN_ID.with(|c| c.set(id));
111}
112
113/// Reset the thread's connection id back to `0` (autocommit).
114pub fn clear_current_connection_id() {
115    CURRENT_CONN_ID.with(|c| c.set(0));
116}
117
118/// Read the connection id set by `set_current_connection_id`. Returns
119/// `0` when no wrapper installed one — auto-commit path.
120pub fn current_connection_id() -> u64 {
121    CURRENT_CONN_ID.with(|c| c.get())
122}
123
124/// Install the authenticated identity for the current thread (Phase 2.5.2
125/// RLS enforcement). Transport layers call this right after resolving
126/// auth so the query dispatch can fold RLS policies into the filter.
127pub 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
131/// Clear the thread-local auth identity. Transports call this after the
132/// statement completes so pooled threads don't leak identities across
133/// requests.
134pub fn clear_current_auth_identity() {
135    CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = None);
136}
137
138/// Read the current-thread auth identity. `None` when no transport
139/// installed one (embedded mode / anonymous access).
140pub(crate) fn current_auth_identity() -> Option<(String, crate::auth::Role)> {
141    CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone())
142}
143
144/// Public probe of the thread-local auth identity for callers outside
145/// the `runtime` module (e.g. the AI credential resolver, which audits
146/// who triggered a secret read on behalf of a query).
147pub fn current_auth_identity_for_audit() -> Option<(String, crate::auth::Role)> {
148    current_auth_identity()
149}
150
151/// Install the session tenant id for the current thread (Phase 2.5.3
152/// multi-tenancy). Called by `SET TENANT 'id'` dispatch and by
153/// transport middleware that resolves tenant from auth claims (e.g.
154/// JWT `tenant` claim, HTTP header, subdomain).
155pub fn set_current_tenant(tenant_id: String) {
156    CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = Some(tenant_id));
157}
158
159/// Clear the current-thread tenant — `CURRENT_TENANT()` will then
160/// return NULL and any RLS policy gated on it will hide every row.
161pub fn clear_current_tenant() {
162    CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = None);
163}
164
165/// Read the current-thread tenant id, applying overrides in priority order:
166///   1. `WITHIN TENANT '<id>' …` per-statement override (highest)
167///   2. `SET LOCAL TENANT '<id>'` transaction-local override (consulted
168///      only when the current connection has an open transaction)
169///   3. `SET TENANT '<id>'` session-level thread-local
170///   4. `None` (deny-default for RLS).
171///
172/// The transaction-local layer is read through the runtime; an embedded
173/// helper crate that has no `RedDBRuntime` access still gets correct
174/// behaviour for layers 1, 3, and 4.
175pub 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    /// Snapshot of the active connection's `tx_local_tenants` entry for
190    /// the current `execute_query` call. Outer `Some(_)` means "a
191    /// transaction-local tenant override is active for this call";
192    /// inner is the override's value (`Some(s)` overrides to `s`,
193    /// `None` overrides to NULL/cleared). Refreshed at the top of every
194    /// `execute_query` invocation and cleared by the RAII guard on
195    /// return so pooled connections cannot leak the override past the
196    /// statement that owns it.
197    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
205/// Recognise `SET LOCAL TENANT '<id>'` / `SET LOCAL TENANT NULL` —
206/// returns `Ok(Some(Some(id)))` for an explicit value, `Ok(Some(None))`
207/// for an explicit NULL clear, `Ok(None)` when the input is not a
208/// `SET LOCAL TENANT` statement at all, and `Err` when the prefix
209/// matches but the value is malformed.
210pub(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    /// Stack of `WITHIN ... <stmt>` overrides active on the current
267    /// thread. Every entry corresponds to one in-flight `execute_query`
268    /// call that started with a `WITHIN` prefix; the entry is pushed
269    /// before dispatch and popped before the call returns. The stack
270    /// shape supports nested invocations (e.g. a view body that itself
271    /// re-enters execute_query).
272    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
290/// Cheap probe: is any `WITHIN …` scope override active on this
291/// thread? The fast-path needs to know without paying for the full
292/// `.last().cloned()` allocation — just peek at stack length.
293pub(crate) fn has_scope_override_active() -> bool {
294    SCOPE_OVERRIDES.with(|cell| !cell.borrow().is_empty())
295}
296
297/// RAII guard pairing `push_scope_override` with the matching pop, so
298/// the stack stays balanced even when the inner `execute_query` returns
299/// early via `?`.
300pub(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
315/// Read the current-thread auth identity, honouring per-statement
316/// `WITHIN ... USER '<u>' AS ROLE '<r>'` overrides. The override only
317/// supplies projected strings — it never grants additional privilege —
318/// so callers that need to make authorisation decisions must read from
319/// the underlying `current_auth_identity()` directly.
320pub(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        // `red.secret.*` is the internal system-secrets namespace. Never
388        // expose it via `$secret.X` regardless of IAM role — not even admin.
389        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
446/// Resolve `$kv.<path>` from the plain (non-encrypted) KV store (#1602).
447///
448/// The parser desugars `$kv.<path>` to `__KV_REF("red.kv/<path>")`; this
449/// strips the `red.kv/` prefix and looks up the bare key in the
450/// statement-local snapshot, gating each hit on `kv:read`. Returns `None`
451/// (→ SQL NULL) when the key is absent or the principal is denied — never
452/// an error, mirroring the secret resolver's deny-to-NULL behaviour.
453pub(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        // Accept both the namespaced `red.kv/<key>` form (from the
466        // `$kv.*` desugar) and the bare key (from `update_current_kv_value`
467        // after a `SET KV`).
468        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    // Boxed to keep the stack footprint of nested guards small. Each
517    // StatementFrameGuards is live for the full recursive call depth of a
518    // materialized-view refresh chain; unboxed KvResolver (≈120 B) would
519    // push those deep paths past the 8 MB stack limit.
520    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
550/// Update the statement-local KV snapshot after a `SET KV` / `DELETE KV`
551/// so a subsequent `$kv.*` read in the same statement observes the write.
552pub(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        // #1370 — `$config.<path>` desugars to `CONFIG("red.config/<path>")`,
581        // but `SET CONFIG <path>` stores under the bare key. Mirror the secret
582        // resolver: after the namespaced key, fall back to the stripped bare
583        // key, then the dotted `red.config.<rest>` legacy form.
584        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            // `$kv.acme.key` desugars to `red.kv/acme.key`.
693            assert_eq!(
694                current_kv_value("red.kv/acme.key").as_deref(),
695                Some("plain-value")
696            );
697            // Bare-key lookup (post-`SET KV` snapshot update) also resolves.
698            assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
699            // Unknown keys resolve to None (→ SQL NULL), never an error.
700            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
809/// Install the MVCC snapshot used by the current thread for the duration
810/// of one statement. Paired with `clear_current_snapshot()` — callers
811/// should prefer the `CurrentSnapshotGuard` RAII wrapper so early returns
812/// still clean up.
813pub 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
823/// Drop-guard that restores the previous snapshot on scope exit. Safe to
824/// nest — each statement saves the caller's snapshot and puts it back
825/// instead of blindly clearing, so a top-level `execute_query` called
826/// from inside another statement dispatch (e.g. vector source subqueries)
827/// doesn't strip visibility from the outer scan.
828pub(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/// Is this entity visible under the current thread's MVCC snapshot?
850///
851/// Returns `true` (no filtering) when no snapshot is installed — that
852/// path is used by embedded callers and by operations that intentionally
853/// bypass MVCC (VACUUM, snapshot export, admin introspection).
854///
855/// When a snapshot is installed the result is
856///   `snapshot.sees(xmin, xmax) && !mgr.is_aborted(xmin) && !xmax_half_abort`
857/// where `xmax_half_abort` re-grants visibility for tuples whose
858/// deleting transaction rolled back.
859#[inline]
860pub fn entity_visible_under_current_snapshot(
861    entity: &crate::storage::unified::entity::UnifiedEntity,
862) -> bool {
863    // Moderation visibility gate (#1274, ADR 0057). A row carrying the
864    // moderation status marker — quarantine-pending or rejected-tombstone
865    // — is hidden from every normal read, on top of MVCC visibility. The
866    // marker lives on the row itself, so the check is a single field probe
867    // and rides the existing per-row visibility chokepoint rather than
868    // adding a separate filter pass to each scan call-site.
869    if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
870        return false;
871    }
872    // Fast path — one `Cell<bool>` read, no RefCell borrow. Autocommit
873    // reads (no active MVCC transaction) still hide superseded physical
874    // versions while avoiding a full snapshot-context lookup.
875    // This runs on every row of every scan; the slow path only fires
876    // inside an explicit transaction.
877    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/// Direct visibility check from raw `(xmin, xmax)` — bypasses the
890/// entity borrow for callers that already decomposed the tuple (e.g.
891/// pre-materialized scan caches). Same semantics as
892/// `entity_visible_under_current_snapshot`.
893#[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
907/// Clone the current thread's snapshot context. Parallel scan paths
908/// (`query_all_zoned` with `std::thread::scope`) call this on the main
909/// thread *before* spawning workers so the captured `SnapshotContext`
910/// can be moved into every worker closure. Worker threads do not
911/// inherit thread-locals, so calling `entity_visible_under_current_snapshot`
912/// from inside a spawned closure would silently skip the filter.
913pub fn capture_current_snapshot() -> Option<SnapshotContext> {
914    CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
915}
916
917/// Whether the active read snapshot may need historical tuple versions
918/// that the current secondary indexes cannot prove. Index paths can still
919/// recheck visible candidates, but only a heap scan can discover versions
920/// whose indexed value was changed or deleted after this snapshot.
921pub(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/// Frozen MVCC + identity context for callers that need to reinstall
933/// the same view across thread-local boundaries — long-lived cursors,
934/// background batchers, anything that detaches from the dispatch path
935/// and re-enters later.
936///
937/// The bundle bakes in the three thread-locals every read path
938/// consults: `SnapshotContext` (MVCC visibility), the auth identity
939/// (RLS policy gate), and the tenant id (RLS scope). A FETCH that
940/// reinstalls the bundle sees exactly the same rows as the DECLARE
941/// would have, regardless of writes that landed in between.
942///
943/// Cheap to clone — `SnapshotContext` is a clone of three
944/// `Arc`-backed fields, identity is a `(String, Role)`, tenant is a
945/// `String`. None of these contend with the read path.
946#[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
953/// Capture the three read-path thread-locals into a `SnapshotBundle`.
954/// Pairs with `with_snapshot_bundle` for re-entry.
955pub 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
963/// Reinstall a captured `SnapshotBundle` for the duration of `f`.
964/// Restores the caller's previous thread-locals on exit (panic-safe via
965/// the explicit guard struct so a panic in `f` cannot leak the
966/// installed identity into the worker's next request).
967pub 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/// Apply the same visibility rules used by the thread-local helpers
1006/// against a caller-provided context. Intended for parallel workers
1007/// that captured the snapshot with `capture_current_snapshot()`.
1008#[inline]
1009pub fn entity_visible_with_context(
1010    ctx: Option<&SnapshotContext>,
1011    entity: &crate::storage::unified::entity::UnifiedEntity,
1012) -> bool {
1013    // Same moderation visibility gate as the thread-local path (#1274):
1014    // parallel scan workers capture the snapshot context but must apply
1015    // the moderation marker check identically.
1016    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    // Writer aborted → tuple never existed from any future reader's view.
1028    // Checked *before* the own-xids fast path so an aborted own-sub-xid
1029    // (rolled-back savepoint) stays hidden from the parent.
1030    if xmin != 0 && ctx.manager.is_aborted(xmin) {
1031        return false;
1032    }
1033    // Deleter aborted → treat xmax as unset; fall back to xmin-only check.
1034    let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
1035        0
1036    } else {
1037        xmax
1038    };
1039    // Phase 2.3.2e: own-tx writes are always visible to the connection
1040    // that stamped them, even when xmin/xmax exceed `snapshot.xid` (as
1041    // happens for sub-xids allocated by SAVEPOINT after BEGIN).
1042    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        // This connection deleted the row via this xid — hide it from self.
1046        return false;
1047    }
1048    if own_xmin {
1049        return true;
1050    }
1051    ctx.snapshot.sees(xmin, effective_xmax)
1052}