Skip to main content

radixdb_executor/access/
handle.rs

1//! Query-local transaction and table handles.
2
3use std::sync::{Arc, Mutex};
4
5use radixdb_core::{Error, Result};
6use radixdb_storage::mvcc::engine::MVCCEngine;
7use radixdb_storage::traits::{Engine, Table, Transaction};
8
9use crate::mutation::host::ActiveTransaction;
10
11/// Owns the physical table handle and, for an implicit statement, the
12/// transaction whose snapshot keeps that handle valid.
13pub struct QueryTableHandle {
14    pub table: Box<dyn Table>,
15    pub in_explicit_transaction: bool,
16    _statement_transaction: Option<Box<dyn Transaction>>,
17}
18
19pub struct QueryTablePairHandle {
20    pub left: Box<dyn Table>,
21    pub right: Box<dyn Table>,
22    pub in_explicit_transaction: bool,
23    pub statement_transaction: Option<Box<dyn Transaction>>,
24}
25
26impl QueryTableHandle {
27    pub fn into_parts(self) -> (Box<dyn Table>, Option<Box<dyn Transaction>>, bool) {
28        (
29            self.table,
30            self._statement_transaction,
31            self.in_explicit_transaction,
32        )
33    }
34}
35
36pub enum QuerySourceHandle {
37    Current(QueryTableHandle),
38    Temporal(Box<dyn Transaction>),
39}
40
41/// Open a table against the explicit transaction snapshot when present, or
42/// create one statement-local snapshot otherwise.
43pub fn open_query_table(
44    engine: &Arc<MVCCEngine>,
45    active_transaction: &Mutex<Option<ActiveTransaction>>,
46    table_name: &str,
47) -> Result<QueryTableHandle> {
48    let active = active_transaction.lock().unwrap();
49    if let Some(state) = active.as_ref() {
50        let table = map_table_not_found(state.transaction.get_table(table_name), table_name)?;
51        drop(active);
52        return Ok(QueryTableHandle {
53            table,
54            in_explicit_transaction: true,
55            _statement_transaction: None,
56        });
57    }
58    drop(active);
59
60    let transaction = engine.begin_transaction()?;
61    let table = map_table_not_found(transaction.get_table(table_name), table_name)?;
62    Ok(QueryTableHandle {
63        table,
64        in_explicit_transaction: false,
65        _statement_transaction: Some(transaction),
66    })
67}
68
69/// Open a table without translating the storage error. Internal JOIN paths
70/// retain their established error contract while sharing snapshot ownership.
71pub fn open_query_table_raw(
72    engine: &Arc<MVCCEngine>,
73    active_transaction: &Mutex<Option<ActiveTransaction>>,
74    table_name: &str,
75) -> Result<QueryTableHandle> {
76    let active = active_transaction.lock().unwrap();
77    if let Some(state) = active.as_ref() {
78        let table = state.transaction.get_table(table_name)?;
79        drop(active);
80        return Ok(QueryTableHandle {
81            table,
82            in_explicit_transaction: true,
83            _statement_transaction: None,
84        });
85    }
86    drop(active);
87    let transaction = engine.begin_transaction()?;
88    let table = transaction.get_table(table_name)?;
89    Ok(QueryTableHandle {
90        table,
91        in_explicit_transaction: false,
92        _statement_transaction: Some(transaction),
93    })
94}
95
96/// Open two tables from exactly one query snapshot.
97pub fn open_query_table_pair(
98    engine: &Arc<MVCCEngine>,
99    active_transaction: &Mutex<Option<ActiveTransaction>>,
100    left_name: &str,
101    right_name: &str,
102) -> Result<QueryTablePairHandle> {
103    let active = active_transaction.lock().unwrap();
104    if let Some(state) = active.as_ref() {
105        let left = state.transaction.get_table(left_name)?;
106        let right = state.transaction.get_table(right_name)?;
107        drop(active);
108        return Ok(QueryTablePairHandle {
109            left,
110            right,
111            in_explicit_transaction: true,
112            statement_transaction: None,
113        });
114    }
115    drop(active);
116    let transaction = engine.begin_transaction()?;
117    let left = transaction.get_table(left_name)?;
118    let right = transaction.get_table(right_name)?;
119    Ok(QueryTablePairHandle {
120        left,
121        right,
122        in_explicit_transaction: false,
123        statement_transaction: Some(transaction),
124    })
125}
126
127/// Metadata-only table probe with query-snapshot semantics.
128pub fn query_table_has_cold_segments(
129    engine: &Arc<MVCCEngine>,
130    active_transaction: &Mutex<Option<ActiveTransaction>>,
131    table_name: &str,
132) -> bool {
133    open_query_table_raw(engine, active_transaction, table_name)
134        .is_ok_and(|handle| handle.table.has_cold_segments())
135}
136
137/// Open the physical source once while preserving the legacy rule that an
138/// explicit transaction owns the snapshot and therefore suppresses AS OF.
139pub fn open_query_source(
140    engine: &Arc<MVCCEngine>,
141    active_transaction: &Mutex<Option<ActiveTransaction>>,
142    table_name: &str,
143    temporal_requested: bool,
144) -> Result<QuerySourceHandle> {
145    let active = active_transaction.lock().unwrap();
146    if let Some(state) = active.as_ref() {
147        let table = map_table_not_found(state.transaction.get_table(table_name), table_name)?;
148        drop(active);
149        return Ok(QuerySourceHandle::Current(QueryTableHandle {
150            table,
151            in_explicit_transaction: true,
152            _statement_transaction: None,
153        }));
154    }
155    drop(active);
156
157    let transaction = engine.begin_transaction()?;
158    if temporal_requested {
159        return Ok(QuerySourceHandle::Temporal(transaction));
160    }
161    let table = map_table_not_found(transaction.get_table(table_name), table_name)?;
162    Ok(QuerySourceHandle::Current(QueryTableHandle {
163        table,
164        in_explicit_transaction: false,
165        _statement_transaction: Some(transaction),
166    }))
167}
168
169/// Begin the statement-local snapshot used by temporal reads.
170pub fn begin_query_transaction(engine: &Arc<MVCCEngine>) -> Result<Box<dyn Transaction>> {
171    engine.begin_transaction()
172}
173
174fn map_table_not_found<T>(result: Result<T>, table_name: &str) -> Result<T> {
175    result.map_err(|error| {
176        if matches!(error, Error::TableNotFound(_)) {
177            Error::TableOrViewNotFound(table_name.to_string())
178        } else {
179            error
180        }
181    })
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use radixdb_storage::Config;
188
189    #[test]
190    fn missing_table_uses_public_table_or_view_error() {
191        let engine = Arc::new(MVCCEngine::new(Config::in_memory()));
192        engine.open_engine().unwrap();
193        let active = Mutex::new(None);
194        let error = match open_query_table(&engine, &active, "missing") {
195            Ok(_) => panic!("missing table unexpectedly opened"),
196            Err(error) => error,
197        };
198        assert!(
199            matches!(error, Error::TableOrViewNotFound(ref name) if name == "missing"),
200            "unexpected error: {error:?}"
201        );
202    }
203}