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::RedDB;
11use reddb_types::Value;
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. The Transaction State module 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 transaction-local tenant 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        // #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. Track the key
584        // that matched so the `config:read` gate authorizes the real target.
585        let found = {
586            let values = resolver.values.as_ref()?;
587            values
588                .get(&key)
589                .map(|value| (key.clone(), value.clone()))
590                .or_else(|| {
591                    key.strip_prefix("red.config/").and_then(|rest| {
592                        values
593                            .get(rest)
594                            .map(|value| (rest.to_string(), value.clone()))
595                            .or_else(|| {
596                                let legacy = format!("red.config.{rest}");
597                                values.get(&legacy).map(|value| (legacy, value.clone()))
598                            })
599                    })
600                })
601        }?;
602        // #1743 — hard-block the reserved `red.config.*` namespace and gate the
603        // read on `config:read`, mirroring the `$secret` sibling. Denied reads
604        // resolve to `None` (→ SQL NULL), never an error.
605        if !resolver.can_read(&found.0) {
606            return None;
607        }
608        Some(found.1)
609    })
610}
611
612pub(crate) fn update_current_config_value(path: &str, value: Value) {
613    let key = path.to_ascii_lowercase();
614    CURRENT_CONFIG_RESOLVER.with(|cell| {
615        if let Some(resolver) = cell.borrow_mut().as_mut() {
616            if let Some(values) = resolver.values.as_mut() {
617                values.insert(key, value);
618            }
619        }
620    });
621}
622
623pub(crate) fn update_current_secret_value(path: &str, value: Option<String>) {
624    let key = path.to_ascii_lowercase();
625    CURRENT_SECRET_RESOLVER.with(|cell| {
626        if let Some(resolver) = cell.borrow_mut().as_mut() {
627            let Some(values) = resolver.values.as_mut() else {
628                return;
629            };
630            match value {
631                Some(value) => {
632                    values.insert(key, value);
633                }
634                None => {
635                    values.remove(&key);
636                }
637            }
638        }
639    });
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    /// Companion to `raw_iteration_confined_to_maintenance_allowlist`:
647    /// the snapshot thread-local is an implementation detail of the
648    /// statement frame (issue #2138). It must stay private to this module,
649    /// so the only way any other module can observe ambient snapshot state
650    /// is `capture_current_snapshot`, which hands back an owned `Option`
651    /// the caller then passes explicitly into the scan API.
652    #[test]
653    fn snapshot_thread_local_stays_private_to_the_statement_frame() {
654        let src = include_str!("execution_context.rs");
655
656        for name in ["CURRENT_SNAPSHOT", "HAS_SNAPSHOT"] {
657            assert!(
658                !src.contains(&format!("pub static {name}"))
659                    && !src.contains(&format!("pub(crate) static {name}")),
660                "{name} must stay private to execution_context; expose an explicit \
661                 Option<SnapshotContext> via capture_current_snapshot instead"
662            );
663        }
664
665        assert!(
666            src.contains("the only sanctioned ambient read"),
667            "capture_current_snapshot must keep documenting that it is the single \
668             sanctioned ambient read of the snapshot thread-local"
669        );
670    }
671
672    fn with_secret_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
673        CURRENT_SECRET_RESOLVER.with(|cell| {
674            cell.replace(Some(SecretResolver {
675                store: None,
676                values: Some(values),
677                identity: None,
678            }));
679        });
680        let result = f();
681        CURRENT_SECRET_RESOLVER.with(|cell| {
682            cell.replace(None);
683        });
684        result
685    }
686
687    #[test]
688    fn current_secret_value_does_not_fall_back_to_reserved_red_secret_namespace() {
689        let values = HashMap::from([
690            (
691                "red.secret.aes_key".to_string(),
692                "vault-aes-key".to_string(),
693            ),
694            (
695                "red.secret.ai.providers.anthropic.tokens.default".to_string(),
696                "provider-key".to_string(),
697            ),
698            ("acme.key".to_string(), "user-value".to_string()),
699        ]);
700
701        with_secret_values(values, || {
702            assert_eq!(current_secret_value("red.vault/aes_key"), None);
703            assert_eq!(current_secret_value("red.vault/red.secret.aes_key"), None);
704            // The AI provider-token namespace is hard-blocked from `$secret`
705            // regardless of role, on the new path shape (#1745).
706            assert_eq!(
707                current_secret_value("red.vault/ai.providers.anthropic.tokens.default"),
708                None
709            );
710            assert_eq!(
711                current_secret_value("red.vault/red.secret.ai.providers.anthropic.tokens.default"),
712                None
713            );
714            assert_eq!(
715                current_secret_value("red.vault/acme.key").as_deref(),
716                Some("user-value")
717            );
718        });
719    }
720
721    fn with_kv_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
722        CURRENT_KV_RESOLVER.with(|cell| {
723            cell.replace(Some(KvResolver {
724                store: None,
725                values: Some(values),
726                identity: None,
727            }));
728        });
729        let result = f();
730        CURRENT_KV_RESOLVER.with(|cell| {
731            cell.replace(None);
732        });
733        result
734    }
735
736    #[test]
737    fn current_kv_value_resolves_namespaced_and_bare_keys() {
738        let values = HashMap::from([("acme.key".to_string(), "plain-value".to_string())]);
739
740        with_kv_values(values, || {
741            // `$kv.acme.key` desugars to `red.kv/acme.key`.
742            assert_eq!(
743                current_kv_value("red.kv/acme.key").as_deref(),
744                Some("plain-value")
745            );
746            // Bare-key lookup (post-`SET KV` snapshot update) also resolves.
747            assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
748            // Unknown keys resolve to None (→ SQL NULL), never an error.
749            assert_eq!(current_kv_value("red.kv/missing.key"), None);
750        });
751    }
752
753    #[test]
754    fn config_key_read_allowed_hard_blocks_reserved_namespace() {
755        // The reserved `red.config.*` system namespace is denied role- and
756        // store-independently, mirroring the `red.secret.*` block (#1743).
757        assert!(!config_key_read_allowed(
758            None,
759            None,
760            "red.config/red.config.aes_key"
761        ));
762        assert!(!config_key_read_allowed(None, None, "red.config.aes_key"));
763        // Ordinary config keys pass when no store is installed (non-IAM path).
764        assert!(config_key_read_allowed(
765            None,
766            None,
767            "red.config/ai.openrouter.default.key"
768        ));
769        assert!(config_key_read_allowed(None, None, "acme.flags.beta"));
770    }
771
772    #[test]
773    fn is_config_collection_matches_system_config_stores() {
774        assert!(is_config_collection("red_config"));
775        assert!(is_config_collection("RED_CONFIG"));
776        assert!(is_config_collection("red.config"));
777        assert!(!is_config_collection("users"));
778        assert!(!is_config_collection("red_kv"));
779    }
780
781    #[test]
782    fn update_current_kv_value_reflects_in_resolver() {
783        with_kv_values(HashMap::new(), || {
784            assert_eq!(current_kv_value("red.kv/feature.flag"), None);
785            update_current_kv_value("feature.flag", Some("on".to_string()));
786            assert_eq!(
787                current_kv_value("red.kv/feature.flag").as_deref(),
788                Some("on")
789            );
790            update_current_kv_value("feature.flag", None);
791            assert_eq!(current_kv_value("red.kv/feature.flag"), None);
792        });
793    }
794}
795
796fn latest_config_snapshot(db: &RedDB) -> HashMap<String, Value> {
797    let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
798
799    if let Some(manager) = db.store().get_collection("red_config") {
800        manager.for_each_entity(|entity| {
801            let Some(row) = entity.data.as_row() else {
802                return true;
803            };
804            let Some(Value::Text(key)) = row.get_field("key") else {
805                return true;
806            };
807            let value = row.get_field("value").cloned().unwrap_or(Value::Null);
808            let id = entity.id.raw();
809            let key = key.to_ascii_lowercase();
810            insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
811            if let Some(rest) = key.strip_prefix("red.config.") {
812                insert_latest_config_value(&mut latest, format!("red.config/{rest}"), id, value);
813            }
814            true
815        });
816    }
817
818    if let Some(manager) = db.store().get_collection("red.config") {
819        manager.for_each_entity(|entity| {
820            let Some(row) = entity.data.as_row() else {
821                return true;
822            };
823            if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
824                return true;
825            }
826            let Some(Value::Text(key)) = row.get_field("key") else {
827                return true;
828            };
829            let value = row.get_field("value").cloned().unwrap_or(Value::Null);
830            insert_latest_config_value(
831                &mut latest,
832                format!("red.config/{}", key.to_ascii_lowercase()),
833                entity.id.raw(),
834                value,
835            );
836            true
837        });
838    }
839
840    latest
841        .into_iter()
842        .map(|(key, (_, value))| (key, value))
843        .collect()
844}
845
846fn insert_latest_config_value(
847    latest: &mut HashMap<String, (u64, Value)>,
848    key: String,
849    id: u64,
850    value: Value,
851) {
852    match latest.get(&key) {
853        Some((prev_id, _)) if *prev_id > id => {}
854        _ => {
855            latest.insert(key, (id, value));
856        }
857    }
858}
859
860struct ConfigResolver {
861    db: Arc<RedDB>,
862    store: Option<Arc<crate::auth::store::AuthStore>>,
863    identity: Option<(String, crate::auth::Role, Option<String>)>,
864    values: Option<HashMap<String, Value>>,
865}
866
867impl ConfigResolver {
868    fn can_read(&self, key: &str) -> bool {
869        config_key_read_allowed(self.store.as_ref(), self.identity.as_ref(), key)
870    }
871}
872
873/// The reserved `red.config.*` system-config namespace. Never expose it via
874/// the inline `$config`/`CONFIG()`/`KV()` resolver regardless of IAM role,
875/// mirroring the role-independent `red.secret.*` hard-block (#1743).
876fn is_reserved_config_key(key: &str) -> bool {
877    let key = key.strip_prefix("red.config/").unwrap_or(key);
878    key.starts_with("red.config.")
879        || key == "red.config"
880        || key == super::function_catalog::REGISTRY_KEY
881}
882
883/// The system config-store collections (`red_config` / `red.config`) reached
884/// by the inline resolver. A `KV(<collection>, key)` read of one of these is
885/// gated exactly like `$config`; any other collection is unaffected (#1743).
886fn is_config_collection(collection: &str) -> bool {
887    matches!(
888        collection.to_ascii_lowercase().as_str(),
889        "red_config" | "red.config"
890    )
891}
892
893fn config_resource_target(key: &str) -> String {
894    let bare = key.strip_prefix("red.config/").unwrap_or(key);
895    format!("red.config/{bare}")
896}
897
898/// Shared inline-config read gate: a role-independent hard-block on the
899/// reserved `red.config.*` namespace, then a `config:read` capability check
900/// mirroring `SHOW CONFIG` / the `$secret` sibling (#1743). Absent a store or
901/// with IAM authorization disabled, only the hard-block applies so existing
902/// non-IAM `$config` reads keep working.
903fn config_key_read_allowed(
904    store: Option<&Arc<crate::auth::store::AuthStore>>,
905    identity: Option<&(String, crate::auth::Role, Option<String>)>,
906    key: &str,
907) -> bool {
908    if is_reserved_config_key(key) {
909        return false;
910    }
911    let Some(store) = store else {
912        return true;
913    };
914    if !store.iam_authorization_enabled() {
915        return true;
916    }
917    let Some((username, role, tenant)) = identity else {
918        return true;
919    };
920    let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
921    let mut resource =
922        crate::auth::policies::ResourceRef::new("config".to_string(), config_resource_target(key));
923    if let Some(tenant) = tenant {
924        resource = resource.with_tenant(tenant.clone());
925    }
926    let ctx = crate::auth::policies::EvalContext {
927        principal_tenant: tenant.clone(),
928        current_tenant: tenant.clone(),
929        peer_ip: None,
930        mfa_present: false,
931        now_ms: crate::auth::now_ms(),
932        principal_is_admin_role: *role == crate::auth::Role::Admin,
933        principal_is_platform_scoped: tenant.is_none(),
934    };
935    store.check_policy_authz_with_role(&principal, "config:read", &resource, &ctx, *role)
936}
937
938/// Whether the current principal may read config `key` through the inline
939/// `$config`/`CONFIG()`/`KV('red_config', …)` resolver. Used by the db-fallback
940/// lookup paths that bypass [`current_config_value`] (#1743).
941pub(crate) fn config_read_permitted(key: &str) -> bool {
942    let key = key.to_ascii_lowercase();
943    CURRENT_CONFIG_RESOLVER.with(|cell| match cell.borrow().as_ref() {
944        Some(resolver) => resolver.can_read(&key),
945        None => !is_reserved_config_key(&key),
946    })
947}
948
949/// Gate a raw `KV(collection, key)` read: config system collections go through
950/// the `$config` gate; every other collection is unaffected (#1743).
951pub(crate) fn kv_read_permitted(collection: &str, key: &str) -> bool {
952    if is_config_collection(collection) {
953        config_read_permitted(key)
954    } else {
955        true
956    }
957}
958
959pub(crate) struct ConfigSnapshotGuard {
960    previous: Option<ConfigResolver>,
961}
962
963impl ConfigSnapshotGuard {
964    pub(super) fn install(
965        db: Arc<RedDB>,
966        store: Option<Arc<crate::auth::store::AuthStore>>,
967    ) -> Self {
968        let identity = current_auth_identity().map(|(username, role)| {
969            let tenant = current_tenant();
970            (username, role, tenant)
971        });
972        let previous = CURRENT_CONFIG_RESOLVER.with(|cell| {
973            cell.replace(Some(ConfigResolver {
974                db,
975                store,
976                identity,
977                values: None,
978            }))
979        });
980        Self { previous }
981    }
982}
983
984impl Drop for ConfigSnapshotGuard {
985    fn drop(&mut self) {
986        let previous = self.previous.take();
987        CURRENT_CONFIG_RESOLVER.with(|cell| {
988            cell.replace(previous);
989        });
990    }
991}
992
993/// Install the MVCC snapshot used by the current thread for the duration
994/// of one statement. Paired with `clear_current_snapshot()` — callers
995/// should prefer the `CurrentSnapshotGuard` RAII wrapper so early returns
996/// still clean up.
997pub fn set_current_snapshot(ctx: SnapshotContext) {
998    CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
999    HAS_SNAPSHOT.with(|c| c.set(true));
1000}
1001
1002pub fn clear_current_snapshot() {
1003    CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
1004    HAS_SNAPSHOT.with(|c| c.set(false));
1005}
1006
1007/// Drop-guard that restores the previous snapshot on scope exit. Safe to
1008/// nest — each statement saves the caller's snapshot and puts it back
1009/// instead of blindly clearing, so a top-level `execute_query` called
1010/// from inside another statement dispatch (e.g. vector source subqueries)
1011/// doesn't strip visibility from the outer scan.
1012pub(crate) struct CurrentSnapshotGuard {
1013    previous: Option<SnapshotContext>,
1014}
1015
1016impl CurrentSnapshotGuard {
1017    pub(crate) fn install(ctx: SnapshotContext) -> Self {
1018        let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1019        set_current_snapshot(ctx);
1020        Self { previous }
1021    }
1022}
1023
1024impl Drop for CurrentSnapshotGuard {
1025    fn drop(&mut self) {
1026        let prev = self.previous.take();
1027        let has = prev.is_some();
1028        CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
1029        HAS_SNAPSHOT.with(|c| c.set(has));
1030    }
1031}
1032
1033/// Is this entity visible under the current thread's MVCC snapshot?
1034///
1035/// Returns `true` (no filtering) when no snapshot is installed — that
1036/// path is used by embedded callers and by operations that intentionally
1037/// bypass MVCC (VACUUM, snapshot export, admin introspection).
1038///
1039/// When a snapshot is installed the result is
1040///   `snapshot.sees(xmin, xmax) && !mgr.is_aborted(xmin) && !xmax_half_abort`
1041/// where `xmax_half_abort` re-grants visibility for tuples whose
1042/// deleting transaction rolled back.
1043#[inline]
1044pub fn entity_visible_under_current_snapshot(
1045    entity: &crate::storage::unified::entity::UnifiedEntity,
1046) -> bool {
1047    // Moderation visibility gate (#1274, ADR 0057). A row carrying the
1048    // moderation status marker — quarantine-pending or rejected-tombstone
1049    // — is hidden from every normal read, on top of MVCC visibility. The
1050    // marker lives on the row itself, so the check is a single field probe
1051    // and rides the existing per-row visibility chokepoint rather than
1052    // adding a separate filter pass to each scan call-site.
1053    if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1054        return false;
1055    }
1056    // Fast path — one `Cell<bool>` read, no RefCell borrow. Autocommit
1057    // reads (no active MVCC transaction) still hide superseded physical
1058    // versions while avoiding a full snapshot-context lookup.
1059    // This runs on every row of every scan; the slow path only fires
1060    // inside an explicit transaction.
1061    if !HAS_SNAPSHOT.with(|c| c.get()) {
1062        return entity.xmax == 0;
1063    }
1064    CURRENT_SNAPSHOT.with(|cell| {
1065        let guard = cell.borrow();
1066        let Some(ctx) = guard.as_ref() else {
1067            return true;
1068        };
1069        let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1070        if visible {
1071            record_serializable_read(ctx, entity);
1072        }
1073        visible
1074    })
1075}
1076
1077/// Direct visibility check from raw `(xmin, xmax)` — bypasses the
1078/// entity borrow for callers that already decomposed the tuple (e.g.
1079/// pre-materialized scan caches). Same semantics as
1080/// `entity_visible_under_current_snapshot`.
1081#[inline]
1082pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
1083    if !HAS_SNAPSHOT.with(|c| c.get()) {
1084        return true;
1085    }
1086    CURRENT_SNAPSHOT.with(|cell| {
1087        let guard = cell.borrow();
1088        let Some(ctx) = guard.as_ref() else {
1089            return true;
1090        };
1091        visibility_check(ctx, xmin, xmax)
1092    })
1093}
1094
1095/// Clone the current thread's snapshot context. Parallel scan paths
1096/// (`query_all_zoned` with `std::thread::scope`) call this on the main
1097/// thread *before* spawning workers so the captured `SnapshotContext`
1098/// can be moved into every worker closure. Worker threads do not
1099/// inherit thread-locals, so calling `entity_visible_under_current_snapshot`
1100/// from inside a spawned closure would silently skip the filter.
1101///
1102/// This is the only sanctioned ambient read of the snapshot thread-local
1103/// (issue #2138). `CURRENT_SNAPSHOT` and `HAS_SNAPSHOT` are private to this
1104/// module and stay that way: they exist for statement-frame entry and for
1105/// detecting a frameless read, nothing else. Everything downstream must
1106/// take the resulting `Option<SnapshotContext>` as an explicit argument —
1107/// that is what makes `SegmentManager::scan*` thread-independent.
1108///
1109/// Statement-frame build also reads it, so a nested sub-execution inside
1110/// one statement (CTE prelude, materialized-view refresh, inline graph
1111/// TVF) inherits the enclosing snapshot instead of minting a fresh one
1112/// (#2183).
1113///
1114/// The `Option` is load-bearing: `None` means no statement frame is
1115/// installed — the low-level callers that intentionally bypass MVCC
1116/// (VACUUM, snapshot export, admin introspection) — and the scan API
1117/// keeps a documented fallback for that case rather than assuming a frame
1118/// always exists.
1119pub fn capture_current_snapshot() -> Option<SnapshotContext> {
1120    CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
1121}
1122
1123/// Whether the active read snapshot may need historical tuple versions
1124/// that the current secondary indexes cannot prove. Index paths can still
1125/// recheck visible candidates, but only a heap scan can discover versions
1126/// whose indexed value was changed or deleted after this snapshot.
1127pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
1128    if !HAS_SNAPSHOT.with(|c| c.get()) {
1129        return false;
1130    }
1131    CURRENT_SNAPSHOT.with(|cell| {
1132        cell.borrow()
1133            .as_ref()
1134            .is_some_and(|ctx| ctx.requires_index_fallback)
1135    })
1136}
1137
1138/// Frozen MVCC + identity context for callers that need to reinstall
1139/// the same view across thread-local boundaries — long-lived cursors,
1140/// background batchers, anything that detaches from the dispatch path
1141/// and re-enters later.
1142///
1143/// The bundle bakes in the three thread-locals every read path
1144/// consults: `SnapshotContext` (MVCC visibility), the auth identity
1145/// (RLS policy gate), and the tenant id (RLS scope). A FETCH that
1146/// reinstalls the bundle sees exactly the same rows as the DECLARE
1147/// would have, regardless of writes that landed in between.
1148///
1149/// Cheap to clone — `SnapshotContext` is a clone of three
1150/// `Arc`-backed fields, identity is a `(String, Role)`, tenant is a
1151/// `String`. None of these contend with the read path.
1152#[derive(Clone, Default)]
1153pub struct SnapshotBundle {
1154    pub snapshot: Option<SnapshotContext>,
1155    pub auth: Option<(String, crate::auth::Role)>,
1156    pub tenant: Option<String>,
1157}
1158
1159/// Capture the three read-path thread-locals into a `SnapshotBundle`.
1160/// Pairs with `with_snapshot_bundle` for re-entry.
1161pub fn snapshot_bundle() -> SnapshotBundle {
1162    SnapshotBundle {
1163        snapshot: capture_current_snapshot(),
1164        auth: current_auth_identity(),
1165        tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
1166    }
1167}
1168
1169/// Reinstall a captured `SnapshotBundle` for the duration of `f`.
1170/// Restores the caller's previous thread-locals on exit (panic-safe via
1171/// the explicit guard struct so a panic in `f` cannot leak the
1172/// installed identity into the worker's next request).
1173pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
1174    struct Guard {
1175        prev_snapshot: Option<SnapshotContext>,
1176        prev_auth: Option<(String, crate::auth::Role)>,
1177        prev_tenant: Option<String>,
1178    }
1179    impl Drop for Guard {
1180        fn drop(&mut self) {
1181            let snap = self.prev_snapshot.take();
1182            let has = snap.is_some();
1183            CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
1184            HAS_SNAPSHOT.with(|c| c.set(has));
1185            CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
1186            CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
1187        }
1188    }
1189
1190    let _guard = {
1191        let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1192        let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
1193        let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
1194
1195        match bundle.snapshot.clone() {
1196            Some(ctx) => set_current_snapshot(ctx),
1197            None => clear_current_snapshot(),
1198        }
1199        CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
1200        CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
1201
1202        Guard {
1203            prev_snapshot,
1204            prev_auth,
1205            prev_tenant,
1206        }
1207    };
1208    f()
1209}
1210
1211/// Apply the same visibility rules used by the thread-local helpers
1212/// against a caller-provided context. Intended for parallel workers
1213/// that captured the snapshot with `capture_current_snapshot()`.
1214#[inline]
1215pub fn entity_visible_with_context(
1216    ctx: Option<&SnapshotContext>,
1217    entity: &crate::storage::unified::entity::UnifiedEntity,
1218) -> bool {
1219    // Same moderation visibility gate as the thread-local path (#1274):
1220    // parallel scan workers capture the snapshot context but must apply
1221    // the moderation marker check identically.
1222    if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1223        return false;
1224    }
1225    match ctx {
1226        Some(ctx) => {
1227            let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1228            if visible {
1229                record_serializable_read(ctx, entity);
1230            }
1231            visible
1232        }
1233        None => true,
1234    }
1235}
1236
1237fn record_serializable_read(
1238    ctx: &SnapshotContext,
1239    entity: &crate::storage::unified::entity::UnifiedEntity,
1240) {
1241    let Some(reader) = ctx.serializable_reader else {
1242        return;
1243    };
1244    if !matches!(
1245        &entity.kind,
1246        crate::storage::unified::entity::EntityKind::TableRow { .. }
1247    ) {
1248        return;
1249    }
1250    ctx.manager
1251        .record_serializable_read(reader, entity.kind.collection(), entity.logical_id());
1252}
1253
1254#[inline]
1255fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
1256    // Writer aborted → tuple never existed from any future reader's view.
1257    // Checked *before* the own-xids fast path so an aborted own-sub-xid
1258    // (rolled-back savepoint) stays hidden from the parent.
1259    if xmin != 0 && ctx.manager.is_aborted(xmin) {
1260        return false;
1261    }
1262    // Deleter aborted → treat xmax as unset; fall back to xmin-only check.
1263    let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
1264        0
1265    } else {
1266        xmax
1267    };
1268    // Phase 2.3.2e: own-tx writes are always visible to the connection
1269    // that stamped them, even when xmin/xmax exceed `snapshot.xid` (as
1270    // happens for sub-xids allocated by SAVEPOINT after BEGIN).
1271    let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
1272    let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
1273    if own_xmax {
1274        // This connection deleted the row via this xid — hide it from self.
1275        return false;
1276    }
1277    if own_xmin {
1278        return true;
1279    }
1280    ctx.snapshot.sees(xmin, effective_xmax)
1281}