Skip to main content

linkrs_query/context/
query_context.rs

1//! Query context
2//!
3//! Manage the context information throughout the entire lifecycle of queries, from parsing and validation to planning and execution.
4//!
5//! ## Creation
6//!
7//! Use [`QueryContext::new`] for simple cases or [`QueryContext::builder`] for complex configuration:
8//!
9//! ```rust,ignore
10//! use linkrs::query::context::{QueryContext, QueryRequestContext};
11//! use std::sync::Arc;
12//!
13//! // Simple creation
14//! let rctx = Arc::new(QueryRequestContext::new("MATCH (n) RETURN n".to_string()));
15//! let ctx = QueryContext::new(rctx);
16//!
17//! // With builder (for complex configuration)
18//! let ctx = QueryContext::builder(rctx)
19//!     .with_space_info(space_info)
20//!     .with_arena()
21//!     .build();
22//! ```
23//!
24//! ## Arena Allocation
25//!
26//! For high-performance query execution with many temporary allocations,
27//! enable arena allocation via the builder's `with_arena()` method. This is beneficial for:
28//!
29//! - Complex queries with many intermediate results
30//! - Expression evaluation with temporary values
31//! - Graph traversal with path accumulation
32
33use std::sync::Arc;
34
35use crate::executor::streaming::query_registry::CancelToken;
36use crate::executor::streaming::transaction_scope::CancelReason;
37use linkrs_core::types::{CharsetInfo, SpaceInfo, Timestamp};
38use linkrs_core::IdGenerator;
39
40use super::QueryRequestContext;
41
42/// Query context
43///
44/// The context for each query request is created whenever the query request is received by the query engine.
45/// This context object is visible to the parser, planner, optimizer, and executor.
46///
47/// # Responsibilities
48///
49/// - Query request context (session information, request parameters)
50/// - Request-scoped cancellation token (shared with the execution runtime and
51///   the process-level query registry — one token for all cancellation)
52/// - ID generation for query execution
53/// - Space information management (space info, character set)
54/// - Optional arena allocator for high-performance temporary allocations
55///
56/// # Creation
57///
58/// Use [`QueryContext::new`] for simple cases or [`QueryContext::builder`] for complex configuration.
59pub struct QueryContext {
60    /// Query request context
61    rctx: Arc<QueryRequestContext>,
62    /// Request-scoped cancellation token.
63    ///
64    /// The execution pipeline threads this token into the query registry and
65    /// the execution runtime (`instantiate_plan`), so `mark_killed`, KILL
66    /// QUERY, and runtime cancel all flip the same underlying state.
67    cancel_token: CancelToken,
68    /// ID Generator for query execution
69    id_gen: IdGenerator,
70    /// Current space information
71    space_info: Option<SpaceInfo>,
72    /// Character set information
73    charset_info: Option<Box<CharsetInfo>>,
74    /// MVCC snapshot timestamp for consistent reads within explicit
75    /// transactions. `None` = auto-commit single statement (current-version
76    /// reads).
77    snapshot_ts: Option<Timestamp>,
78    /// Transaction isolation level injected by the API layer for queries
79    /// running inside an explicit transaction. `None` = auto-commit
80    /// statement-level snapshot semantics. Execution-time knob: the runtime
81    /// and operators can consult it instead of only the storage layer.
82    isolation_level: Option<linkrs_core::types::TransactionIsolationLevel>,
83    /// Whether arena allocation is enabled for this query's execution. The
84    /// concrete arena is owned by the execution context, not by this struct, so
85    /// the context stays `Send + Sync`.
86    arena_enabled: bool,
87    /// Retained password-history depth applied by user-management operations
88    /// in this query. Injected by the API layer from server configuration;
89    /// zero clears history on rotation.
90    password_history_depth: usize,
91}
92
93/// Builder-supplied context parameters, grouped so the internal constructor
94/// stays within a manageable arity.
95pub(super) struct ContextParams {
96    pub cancel_token: CancelToken,
97    pub id_gen: IdGenerator,
98    pub space_info: Option<SpaceInfo>,
99    pub charset_info: Option<Box<CharsetInfo>>,
100    pub snapshot_ts: Option<Timestamp>,
101    pub isolation_level: Option<linkrs_core::types::TransactionIsolationLevel>,
102    pub arena_enabled: bool,
103    pub password_history_depth: usize,
104}
105
106impl QueryContext {
107    /// Create a new query context with default configuration.
108    ///
109    /// For complex configuration (arena allocation, custom ID generator, etc.),
110    /// use [`QueryContext::builder`] instead.
111    pub fn new(rctx: Arc<QueryRequestContext>) -> Self {
112        Self {
113            rctx,
114            cancel_token: CancelToken::new(),
115            id_gen: IdGenerator::new(0),
116            space_info: None,
117            charset_info: None,
118            snapshot_ts: None,
119            isolation_level: None,
120            arena_enabled: false,
121            password_history_depth: 0,
122        }
123    }
124
125    /// Create a builder for complex configuration.
126    ///
127    /// # Example
128    ///
129    /// ```rust,ignore
130    /// let ctx = QueryContext::builder(rctx)
131    ///     .with_space_info(space_info)
132    ///     .with_charset_info(charset_info)
133    ///     .with_arena()
134    ///     .build();
135    /// ```
136    pub fn builder(rctx: Arc<QueryRequestContext>) -> super::QueryContextBuilder {
137        super::QueryContextBuilder::new(rctx)
138    }
139
140    /// Internal constructor for QueryContextBuilder.
141    /// Only visible within the query::context module.
142    pub(super) fn from_builder(rctx: Arc<QueryRequestContext>, params: ContextParams) -> Self {
143        Self {
144            rctx,
145            cancel_token: params.cancel_token,
146            id_gen: params.id_gen,
147            space_info: params.space_info,
148            charset_info: params.charset_info,
149            snapshot_ts: params.snapshot_ts,
150            isolation_level: params.isolation_level,
151            arena_enabled: params.arena_enabled,
152            password_history_depth: params.password_history_depth,
153        }
154    }
155
156    /// Obtain the context of the query request.
157    pub fn request_context(&self) -> &QueryRequestContext {
158        &self.rctx
159    }
160
161    /// The Arc reference that provides the context for the query request.
162    pub fn request_context_arc(&self) -> Arc<QueryRequestContext> {
163        self.rctx.clone()
164    }
165
166    /// Obtain the context of the query request (compatible with old code)
167    pub fn rctx(&self) -> &QueryRequestContext {
168        &self.rctx
169    }
170
171    /// The request-scoped cancellation token.
172    ///
173    /// Threaded into the execution runtime and query registry at instantiation
174    /// so all cancellation paths share one source of truth.
175    pub fn cancel_token(&self) -> CancelToken {
176        self.cancel_token.clone()
177    }
178
179    /// Obtaining character set information
180    pub fn charset_info(&self) -> Option<&CharsetInfo> {
181        self.charset_info.as_ref().map(|ci| ci.as_ref())
182    }
183
184    /// Setting character set information
185    pub fn set_charset_info(&mut self, charset_info: CharsetInfo) {
186        self.charset_info = Some(Box::new(charset_info));
187    }
188
189    /// The MVCC snapshot timestamp for consistent reads within explicit
190    /// transactions.
191    ///
192    /// `None` means auto-commit single-statement execution, which reads the
193    /// current version of the data.
194    pub fn snapshot_ts(&self) -> Option<Timestamp> {
195        self.snapshot_ts
196    }
197
198    /// The transaction isolation level for this execution, when running
199    /// inside an explicit transaction.
200    pub fn isolation_level(&self) -> Option<linkrs_core::types::TransactionIsolationLevel> {
201        self.isolation_level
202    }
203
204    /// The retained password-history depth for user-management operations
205    /// planned under this context.
206    pub fn password_history_depth(&self) -> usize {
207        self.password_history_depth
208    }
209
210    /// The authenticated user that issued this query, when the request came
211    /// from an authenticated session.
212    ///
213    /// Self-service password changes omit the username in the statement; the
214    /// planner backfills it from here so the execution chain targets the
215    /// caller instead of failing on an empty username.
216    pub fn authenticated_user(&self) -> Option<&str> {
217        self.rctx.user_name.as_deref()
218    }
219
220    /// Generate an ID.
221    pub fn gen_id(&self) -> i64 {
222        self.id_gen.id()
223    }
224
225    /// Retrieve the current ID value (without incrementing it).
226    pub fn current_id(&self) -> i64 {
227        self.id_gen.current_value()
228    }
229
230    /// Obtain the current spatial information
231    pub fn space_info(&self) -> Option<&SpaceInfo> {
232        self.space_info.as_ref()
233    }
234
235    /// Set the current space information
236    pub fn set_space_info(&mut self, space_info: SpaceInfo) {
237        self.space_info = Some(space_info);
238    }
239
240    /// Obtain the ID of the current space.
241    pub fn space_id(&self) -> Option<u64> {
242        self.space_info.as_ref().map(|s| s.space_id)
243    }
244
245    /// Get the name of the current space.
246    pub fn space_name(&self) -> Option<String> {
247        self.space_info.as_ref().map(|s| s.space_name.clone())
248    }
249
250    /// Mark this query as killed.
251    ///
252    /// Cancels the request-scoped token shared with the execution runtime and
253    /// the query registry, so running operators abort at the next cooperative
254    /// cancellation check and `KILL QUERY` observability reflects the state.
255    pub fn mark_killed(&self) {
256        self.cancel_token.cancel(CancelReason::UserKill);
257    }
258
259    /// Check whether this query has been killed/cancelled.
260    pub fn is_killed(&self) -> bool {
261        self.cancel_token.is_cancelled()
262    }
263
264    /// Check whether the parameters exist.
265    pub fn exist_parameter(&self, param: &str) -> bool {
266        self.rctx.get_parameter(param).is_some()
267    }
268
269    /// Obtain the query string
270    pub fn query(&self) -> &str {
271        &self.rctx.query
272    }
273
274    /// Obtain parameters
275    pub fn parameters(&self) -> &std::collections::HashMap<String, linkrs_core::Value> {
276        &self.rctx.parameters
277    }
278
279    /// Reset the query context
280    pub fn reset(&mut self) {
281        self.cancel_token.clear();
282        self.id_gen.reset(0);
283        self.space_info = None;
284        self.charset_info = None;
285        self.snapshot_ts = None;
286        self.isolation_level = None;
287        log::info!("Query context has been reset");
288    }
289
290    /// Check if arena allocation is enabled for this query's execution.
291    pub fn has_arena(&self) -> bool {
292        self.arena_enabled
293    }
294
295    // Note: resource_context() and space_context() methods have been removed
296    // as part of the optimization to inline these contexts into QueryContext.
297    // Use the direct accessor methods instead:
298    // - gen_id(), current_id() for resource operations
299    // - space_info(), space_id(), space_name(), charset_info() for space operations
300}
301
302impl std::fmt::Debug for QueryContext {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        f.debug_struct("QueryContext")
305            .field("rctx", &self.rctx)
306            .field("space_id", &self.space_id())
307            .field("snapshot_ts", &self.snapshot_ts)
308            .field("isolation_level", &self.isolation_level)
309            .field("killed", &self.is_killed())
310            .field("has_arena", &self.arena_enabled)
311            .finish()
312    }
313}
314
315impl Default for QueryContext {
316    fn default() -> Self {
317        Self::new(Arc::new(QueryRequestContext::default()))
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn query_context_is_send_and_sync() {
327        fn assert_send_sync<T: Send + Sync>() {}
328        assert_send_sync::<QueryContext>();
329        assert_send_sync::<Arc<QueryContext>>();
330    }
331
332    #[test]
333    fn mark_killed_cancels_shared_token() {
334        let rctx = Arc::new(QueryRequestContext::new("MATCH (n) RETURN n".to_string()));
335        let ctx = QueryContext::new(rctx);
336        assert!(!ctx.is_killed());
337
338        // A clone of the token is what the pipeline hands to the runtime.
339        let runtime_token = ctx.cancel_token();
340        assert!(!runtime_token.is_cancelled());
341
342        ctx.mark_killed();
343
344        assert!(ctx.is_killed());
345        assert!(runtime_token.is_cancelled());
346        assert_eq!(runtime_token.reason(), Some(CancelReason::UserKill));
347    }
348
349    #[test]
350    fn runtime_adopts_context_token() {
351        use crate::executor::streaming::runtime::ExecutionRuntime;
352        let rctx = Arc::new(QueryRequestContext::new("MATCH (n) RETURN n".to_string()));
353        let ctx = QueryContext::new(rctx);
354
355        let runtime = ExecutionRuntime::default_budget();
356        runtime.set_cancel_token(ctx.cancel_token());
357        assert!(!runtime.is_cancelled());
358
359        ctx.mark_killed();
360        assert!(runtime.is_cancelled());
361        assert!(runtime.ensure_not_cancelled().is_err());
362    }
363
364    #[test]
365    fn reset_clears_cancellation() {
366        let rctx = Arc::new(QueryRequestContext::new("MATCH (n) RETURN n".to_string()));
367        let mut ctx = QueryContext::new(rctx);
368        ctx.mark_killed();
369        assert!(ctx.is_killed());
370
371        ctx.reset();
372        assert!(!ctx.is_killed());
373    }
374}