Skip to main content

linkrs_query/executor/streaming/
runtime.rs

1//! Per-query execution runtime.
2//!
3//! Split by concern:
4//! - [`columnar_stats`] — columnar fast-path counters and PROFILE snapshots
5//! - [`profile`] — per-operator profiling ([`OperatorProfile`],
6//!   [`ProfileBoard`], [`ProfileCollector`])
7//! - [`resources`] — cleanup ownership ([`ResourceOwner`]) and the
8//!   [`QueryFinishGuard`] RAII lifecycle guard
9//! - this module — [`ExecutionRuntime`], the per-query composition root
10
11pub mod columnar_stats;
12pub mod profile;
13pub mod resources;
14
15pub use columnar_stats::{
16    ColumnarStats, ColumnarStatsSnapshot, D1_EVAL_THRESHOLD, D1_TYPED_RATE_THRESHOLD,
17    SELECTION_BOUNDARY_OPS,
18};
19pub use profile::{
20    OperatorProfile, OperatorProfileKey, ProfileBoard, ProfileCollector, ProfileEntry,
21};
22pub use resources::{QueryFinishGuard, ResourceOwner};
23
24use std::collections::HashMap;
25use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering};
26use std::sync::Arc;
27use std::time::Instant;
28
29use parking_lot::Mutex;
30
31use parking_lot::RwLock;
32
33use super::query_registry::{CancelToken, QueryId, QueryRegistry};
34use super::spill::SpillManager;
35use super::state::StateArenaSet;
36use super::transaction_scope::{CancelReason, SessionTransactionController, TransactionScope};
37use crate::executor::base::MemoryBudget;
38use crate::executor::streaming::pool::TaskScheduler;
39use crate::optimizer::stats::feedback::history::QueryFeedbackHistory;
40use crate::query_manager::QueryManager;
41use crate::storage::QueryStorage;
42use linkrs_core::error::QueryError;
43use linkrs_core::Arena;
44use linkrs_core::Value;
45
46/// Query identity information
47#[derive(Debug, Clone, Default)]
48pub struct QueryIdentity {
49    pub query_id: u64,
50    pub session_id: Option<String>,
51    pub space_name: Option<String>,
52}
53
54/// Per-query execution runtime shared across all operators.
55///
56/// Centralises cancellation, memory tracking, profiling, resource
57/// lifecycle, and query-registration so that operators do not each
58/// carry ad-hoc context.
59///
60/// unified cancellation via [`CancelToken`]
61/// has been removed; operators check [`CancelToken::is_cancelled`].
62#[derive(Debug)]
63pub struct ExecutionRuntime {
64    /// Query identity (behind Mutex for write-once from the API layer).
65    query_id: parking_lot::Mutex<QueryIdentity>,
66    /// typed cancellation token with reason tracking (single source).
67    /// Behind a `Mutex` because the token is adopted after the runtime is
68    /// shared with the executor tree (`Arc::get_mut` is unusable once the
69    /// tree holds clones); `&self` mutation keeps the registry-None path
70    /// wired without `&mut` access.
71    cancel_token_v2: parking_lot::Mutex<CancelToken>,
72    /// Optional deadline; the query is cancelled after this instant.
73    deadline: Option<Instant>,
74    /// Per-query memory budget for blocking operators.
75    pub memory_budget: MemoryBudget,
76    /// Profile board with atomic counters for lock-free hot-path recording.
77    profile: Arc<ProfileBoard>,
78    /// Resource owner for cleanup of cursors, temp files, etc.
79    resource_owner: Arc<Mutex<ResourceOwner>>,
80    /// Optional reference to the global QueryManager for KILL QUERY, finish
81    /// tracking, and progress forwarding. Behind a `Mutex` for interior
82    /// mutability so the assembly can attach it after the runtime is shared
83    /// with the executor tree (same pattern as `query_registry`).
84    query_manager: parking_lot::Mutex<Option<Arc<QueryManager>>>,
85    /// Row cadence for query-progress notifications. Zero disables emission.
86    /// Checked with a single relaxed atomic load on the row-recording path,
87    /// so the default zero keeps the hot path allocation- and lock-free.
88    progress_rows_interval: AtomicU64,
89    /// Highest processed-row watermark already reported to progress
90    /// observers. Prevents duplicate notifications when several chunks land
91    /// inside the same interval bucket.
92    progress_last_emitted: AtomicU64,
93    /// Session id reported in progress notifications. Set by the assembly
94    /// that attaches the `QueryManager`; defaults to zero.
95    progress_session_id: AtomicI64,
96    /// Query id (in the `QueryManager` id space) reported in progress
97    /// notifications. Negative means unset and suppresses emission.
98    progress_query_id: AtomicI64,
99    /// Session-level transaction controller for transaction commands.
100    /// Behind a RwLock for interior mutability (set after runtime is shared).
101    session_controller: parking_lot::RwLock<Option<Arc<SessionTransactionController>>>,
102    /// Transaction scope for this execution (set by bindings).
103    transaction_scope: Option<TransactionScope>,
104    /// Optional reference to the [`QueryRegistry`] for KILL QUERY.
105    /// Behind a `Mutex` for the same interior-mutability reason as the
106    /// cancel token (the registry is attached after the executor tree clones
107    /// the runtime `Arc`).
108    query_registry: parking_lot::Mutex<Option<Arc<QueryRegistry>>>,
109    /// Query ID allocated by the registry.
110    registry_query_id: parking_lot::Mutex<Option<QueryId>>,
111    /// Engine-level shared scheduler for dynamic partition execution.
112    /// When set, all queries share the same worker pool instead of creating
113    /// per-query threads.  Falls back to serial if neither this nor the
114    /// per-query `worker_pool` is set.
115    /// Behind a `parking_lot::Mutex` because it's written via `&self` (internal
116    /// mutability pattern used throughout [`ExecutionRuntime`]).
117    shared_scheduler: parking_lot::Mutex<Option<Arc<super::pool::SharedScheduler>>>,
118    /// Query-level morsel worker pool for dynamic partition execution
119    /// Kept for backward compatibility.
120    /// Created when `max_workers > 1` and no `shared_scheduler` is set;
121    /// `None` means serial fallback.
122    /// Behind a Mutex so the engine can set the pool after construction.
123    pub worker_pool: Arc<parking_lot::Mutex<Option<Arc<dyn TaskScheduler>>>>,
124    /// Per-partition output channel capacity for parallel exchange/gather.
125    pub max_buffered_chunks: AtomicUsize,
126    /// Spill manager for offloading operator data to disk.
127    pub spill_manager: Arc<parking_lot::Mutex<Option<Arc<SpillManager>>>>,
128    /// Storage client for this query execution.
129    /// Moved here from OperatorSpec so that the physical plan tree is
130    /// truly immutable and cacheable without sharing storage handles.
131    pub storage: Option<Arc<RwLock<dyn QueryStorage>>>,
132    pub search: crate::executor::base::SearchContext,
133    /// Per-partition operator state arenas.
134    ///
135    /// Indexed by `partition_id` so parallel workers do not contend on a
136    /// single lock.  `state_arenas[0]` serves global / non-partitioned
137    /// operators.
138    ///
139    /// Operators create/read/update their typed state during
140    /// `open()` / `next()` / `close()`, indexed by [`PhysicalOperatorId`]
141    /// stored in [`OperatorBase::physical_operator_id`](super::operators::base::OperatorBase).
142    pub state_arenas: Vec<Mutex<StateArenaSet>>,
143
144    /// Runtime parameter name→value map, bound at materialization time.
145    /// Operators read this to resolve `Expression::Parameter` references.
146    pub parameter_values: Option<Arc<HashMap<String, Value>>>,
147
148    /// Runtime session variable name→value snapshot, bound at materialization
149    /// time. Operators read this to resolve `Expression::SessionVariable`
150    /// references.
151    pub session_variable_values: Option<Arc<HashMap<String, Value>>>,
152
153    /// Per-query bumpalo arena for executor temporary allocations.
154    pub arena: Option<Arc<Mutex<Arena>>>,
155    /// Columnar fast-path hit/miss counters shared with produced chunks (T5).
156    columnar_stats: Arc<ColumnarStats>,
157    /// Cross-query adaptive policy for the typed columnar chunk layout.
158    ///
159    /// Injected by the materializer from the query bindings (owned by the
160    /// optimizer engine).  The policy is read at chunk-production time by
161    /// the scan gates; when the runtime finishes, the per-query columnar
162    /// stats are merged back into the policy so later queries can adapt.
163    columnar_policy: Option<Arc<super::chunk::ColumnarPolicy>>,
164    /// Per-query override for the shared columnar policy decision.
165    ///
166    /// Defaults to inherit; internal callers may pin one query without
167    /// touching the shared counters.
168    columnar_override: super::chunk::QueryColumnarOverride,
169    /// Shared query feedback history for collecting execution statistics.
170    ///
171    /// Injected by the materializer from the query bindings; when set, the
172    /// execution instance records estimated-vs-actual operator feedback here
173    /// after execution completes (stats feedback loop).
174    pub feedback_history: Option<Arc<QueryFeedbackHistory>>,
175    /// Macro catalog manager (user-defined macros), shared engine-wide.
176    ///
177    /// Injected by the materializer from the query bindings; read by DDL
178    /// operators executing `CREATE/DROP MACRO` and `SHOW MACROS`.
179    pub macro_manager: Option<Arc<linkrs_core::metadata::MacroManager>>,
180    /// Type-alias catalog manager (user-defined types), shared engine-wide.
181    ///
182    /// Injected by the materializer from the query bindings; read by DDL
183    /// operators executing `CREATE/DROP TYPE`.
184    pub type_alias_manager: Option<Arc<linkrs_core::metadata::TypeAliasManager>>,
185    /// Sequence catalog manager (named sequences), shared engine-wide.
186    ///
187    /// Injected by the materializer from the query bindings; read by DDL
188    /// operators executing `CREATE/ALTER/DROP SEQUENCE` and by the
189    /// `curr_val` / `next_val` expression functions.
190    pub sequence_manager: Option<Arc<linkrs_core::metadata::SequenceManager>>,
191    /// Working tables of enclosing recursive-CTE fixpoints, keyed by the
192    /// mangled CTE tag (`crate::cte::mangle_cte_name`).
193    ///
194    /// Written by the fixpoint operator before each step iteration and
195    /// cleared afterwards; read by CTE scan sources. Serial execution only
196    /// (fixpoint fragments are never partitioned).
197    pub cte_tables:
198        Arc<parking_lot::Mutex<std::collections::HashMap<String, Vec<Vec<linkrs_core::Value>>>>>,
199}
200
201impl ExecutionRuntime {
202    /// Create a new execution runtime with the given query identity, memory budget,
203    /// and optional storage client.
204    pub fn new(
205        query_id: QueryIdentity,
206        memory_budget: MemoryBudget,
207        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
208        search: crate::executor::base::SearchContext,
209    ) -> Self {
210        Self {
211            query_id: parking_lot::Mutex::new(query_id),
212            cancel_token_v2: parking_lot::Mutex::new(CancelToken::new()),
213            deadline: None,
214            memory_budget,
215            profile: Arc::new(ProfileBoard::new()),
216            resource_owner: Arc::new(Mutex::new(ResourceOwner::new())),
217            query_manager: parking_lot::Mutex::new(None),
218            progress_rows_interval: AtomicU64::new(0),
219            progress_last_emitted: AtomicU64::new(0),
220            progress_session_id: AtomicI64::new(0),
221            progress_query_id: AtomicI64::new(-1),
222            session_controller: parking_lot::RwLock::new(None),
223            transaction_scope: None,
224            query_registry: parking_lot::Mutex::new(None),
225            registry_query_id: parking_lot::Mutex::new(None),
226            shared_scheduler: parking_lot::Mutex::new(None),
227            worker_pool: Arc::new(parking_lot::Mutex::new(None)),
228            max_buffered_chunks: AtomicUsize::new(10),
229            spill_manager: Arc::new(parking_lot::Mutex::new(None)),
230            storage,
231            search,
232            state_arenas: vec![Mutex::new(StateArenaSet::new())],
233            parameter_values: None,
234            session_variable_values: None,
235            arena: Some(Arc::new(Mutex::new(Arena::new()))),
236            columnar_stats: Arc::new(ColumnarStats::new()),
237            columnar_policy: None,
238            columnar_override: super::chunk::QueryColumnarOverride::inherit(),
239            feedback_history: None,
240            macro_manager: None,
241            type_alias_manager: None,
242            sequence_manager: None,
243            cte_tables: Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())),
244        }
245    }
246
247    /// Create a runtime with default settings (query_id = 0, default memory budget, no storage).
248    pub fn default_budget() -> Self {
249        Self::new(
250            QueryIdentity::default(),
251            MemoryBudget::default_budget(),
252            None,
253            crate::executor::base::SearchContext::default(),
254        )
255    }
256
257    // ── Query identity ──
258
259    pub fn query_id(&self) -> QueryIdentity {
260        self.query_id.lock().clone()
261    }
262
263    /// Override the query ID number after construction.
264    ///
265    /// The factory initialises `query_id.query_id` to 0; the API layer assigns the
266    /// real server-side ID before the handle is returned to the caller.
267    pub fn assign_query_id(&self, id: u64) {
268        self.query_id.lock().query_id = id;
269    }
270
271    #[cfg(feature = "fulltext")]
272    pub fn set_fulltext_manager(
273        &mut self,
274        manager: Option<Arc<linkrs_fulltext::manager::FulltextIndexManager>>,
275    ) {
276        self.search.fulltext_manager = manager;
277    }
278
279    #[cfg(feature = "vector")]
280    pub fn set_vector_coordinator(
281        &mut self,
282        coordinator: Option<Arc<linkrs_sync::VectorSyncCoordinator>>,
283    ) {
284        self.search.vector_coordinator = coordinator;
285    }
286
287    /// Attach a QueryManager so that KILL QUERY, finish tracking, and progress
288    /// forwarding work.
289    ///
290    /// Uses interior mutability (`&self`) because the runtime is shared with
291    /// the executor tree before the assembly attaches the manager, so `&mut`
292    /// access is unavailable at that point.
293    pub fn set_query_manager(&self, qm: Arc<QueryManager>) {
294        *self.query_manager.lock() = Some(qm);
295    }
296
297    /// Register this query with the attached QueryManager and return a
298    /// [`QueryFinishGuard`] that marks it finished on drop.
299    ///
300    /// Returns `None` when no QueryManager is attached (non-fatal).
301    pub fn finish_guard(&self) -> Option<QueryFinishGuard> {
302        let qm = self.query_manager.lock().as_ref()?.clone();
303        let id = self.query_id();
304        Some(QueryFinishGuard::new(qm, id.query_id as i64))
305    }
306
307    // ── QueryRegistry integration ──
308
309    /// Attach a [`QueryRegistry`] and the allocated [`QueryId`].
310    ///
311    /// Interior mutability via `Mutex`: the runtime is shared with the
312    /// executor tree before registration, so `&mut` access is not available.
313    pub fn set_query_registry(&self, registry: Arc<QueryRegistry>, qid: QueryId) {
314        *self.query_registry.lock() = Some(registry);
315        *self.registry_query_id.lock() = Some(qid);
316    }
317
318    /// Return the registry-allocated query ID, if set.
319    pub fn registry_query_id(&self) -> Option<QueryId> {
320        *self.registry_query_id.lock()
321    }
322
323    /// Set the session-level transaction controller for transaction commands.
324    pub fn set_session_controller(&self, ctrl: Arc<SessionTransactionController>) {
325        *self.session_controller.write() = Some(ctrl);
326    }
327
328    /// Return the session-level transaction controller, if set.
329    pub fn session_controller(&self) -> Option<Arc<SessionTransactionController>> {
330        self.session_controller.read().clone()
331    }
332
333    /// Set the parameter name→value map for this execution instance.
334    pub fn set_parameter_values(&mut self, values: Arc<HashMap<String, Value>>) {
335        self.parameter_values = Some(values);
336    }
337
338    /// Return the parameter name→value map, if bound.
339    pub fn parameter_values(&self) -> Option<Arc<HashMap<String, Value>>> {
340        self.parameter_values.clone()
341    }
342
343    /// Set the session variable snapshot for this execution instance.
344    pub fn set_session_variable_values(&mut self, values: Arc<HashMap<String, Value>>) {
345        self.session_variable_values = Some(values);
346    }
347
348    /// Return the session variable snapshot, if bound.
349    pub fn session_variable_values(&self) -> Option<Arc<HashMap<String, Value>>> {
350        self.session_variable_values.clone()
351    }
352
353    /// Set the macro catalog manager for this execution instance.
354    pub fn set_macro_manager(&mut self, manager: Option<Arc<linkrs_core::metadata::MacroManager>>) {
355        self.macro_manager = manager;
356    }
357
358    /// Return the macro catalog manager, if bound.
359    pub fn macro_manager(&self) -> Option<Arc<linkrs_core::metadata::MacroManager>> {
360        self.macro_manager.clone()
361    }
362
363    /// Set the type-alias catalog manager for this execution instance.
364    pub fn set_type_alias_manager(
365        &mut self,
366        manager: Option<Arc<linkrs_core::metadata::TypeAliasManager>>,
367    ) {
368        self.type_alias_manager = manager;
369    }
370
371    /// Return the type-alias catalog manager, if bound.
372    pub fn type_alias_manager(&self) -> Option<Arc<linkrs_core::metadata::TypeAliasManager>> {
373        self.type_alias_manager.clone()
374    }
375
376    /// Set the sequence catalog manager for this execution instance.
377    pub fn set_sequence_manager(
378        &mut self,
379        manager: Option<Arc<linkrs_core::metadata::SequenceManager>>,
380    ) {
381        self.sequence_manager = manager;
382    }
383
384    /// Return the sequence catalog manager, if bound.
385    pub fn sequence_manager(&self) -> Option<Arc<linkrs_core::metadata::SequenceManager>> {
386        self.sequence_manager.clone()
387    }
388
389    /// Publish a recursive-CTE working table for the duration of one step
390    /// iteration. Overwrites any previous table under the same mangled tag.
391    pub fn set_cte_table(&self, mangled_tag: &str, rows: Vec<Vec<linkrs_core::Value>>) {
392        self.cte_tables.lock().insert(mangled_tag.to_string(), rows);
393    }
394
395    /// Read the current working table published under a mangled CTE tag.
396    pub fn cte_table(&self, mangled_tag: &str) -> Option<Vec<Vec<linkrs_core::Value>>> {
397        self.cte_tables.lock().get(mangled_tag).cloned()
398    }
399
400    /// Remove the working table published under a mangled CTE tag.
401    pub fn clear_cte_table(&self, mangled_tag: &str) {
402        self.cte_tables.lock().remove(mangled_tag);
403    }
404
405    /// Set the transaction scope for this execution.
406    pub fn set_transaction_scope(&mut self, scope: TransactionScope) {
407        self.transaction_scope = Some(scope);
408    }
409
410    /// Return the current transaction scope, if any.
411    pub fn transaction_scope(&self) -> Option<&TransactionScope> {
412        self.transaction_scope.as_ref()
413    }
414
415    /// Return the typed [`CancelToken`] for cooperative cancellation.
416    pub fn cancel_token_v2(&self) -> CancelToken {
417        self.cancel_token_v2.lock().clone()
418    }
419
420    /// Propagate a write-conflict failure of the current transaction.
421    ///
422    /// Called by write operators when a storage access fails with a
423    /// conflict-classified error (write-write conflict, rollback-only). The
424    /// session controller is marked rollback-only so a later COMMIT is
425    /// rejected, and the shared cancellation token is cancelled with the
426    /// typed [`CancelReason::TransactionConflict`] reason — terminating any
427    /// pipeline stages of the same transaction that have not started yet.
428    pub fn note_transaction_conflict(&self) {
429        if let Some(controller) = self.session_controller() {
430            controller.mark_rollback_only();
431        }
432        self.cancel_with_reason(CancelReason::TransactionConflict);
433    }
434
435    /// Adopt an externally-owned cancellation token.
436    ///
437    /// Called at instantiation so the runtime, the query registry, and the
438    /// request-scoped [`QueryContext`] share one token (single source for
439    /// KILL QUERY / cancellation).  Interior mutability via `Mutex` because
440    /// the executor tree holds an `Arc` clone of the runtime by the time
441    /// instantiation wires the token.
442    pub fn set_cancel_token(&self, token: CancelToken) {
443        *self.cancel_token_v2.lock() = token;
444    }
445
446    // ── Cancellation ──
447
448    /// Shared cancellation token for cooperative checks by operators and I/O.
449    pub fn cancel_token(&self) -> CancelToken {
450        self.cancel_token_v2.lock().clone()
451    }
452
453    /// Check whether the query has been cancelled (token or deadline).
454    pub fn is_cancelled(&self) -> bool {
455        self.cancel_token_v2.lock().is_cancelled()
456            || self.deadline.is_some_and(|d| Instant::now() >= d)
457    }
458
459    /// Return an error if the query has been cancelled.
460    pub fn ensure_not_cancelled(&self) -> Result<(), QueryError> {
461        if self.is_cancelled() {
462            let reason = self
463                .cancel_token_v2
464                .lock()
465                .reason()
466                .map(|r| r.to_string())
467                .unwrap_or_else(|| "Query cancelled".to_string());
468            Err(QueryError::execution(reason))
469        } else {
470            Ok(())
471        }
472    }
473
474    /// Cancel this query with a typed reason.
475    ///
476    /// Sets the [`CancelToken`], marks the query as Killed in the
477    /// attached QueryManager, and cancels the registry entry (if configured).
478    pub fn cancel_with_reason(&self, reason: CancelReason) {
479        self.cancel_token_v2.lock().cancel(reason.clone());
480        if let Some(qm) = self.query_manager.lock().as_ref() {
481            let id = self.query_id();
482            let _ = qm.kill_query(id.query_id as i64);
483        }
484        if let (Some(ref reg), Some(qid)) = (
485            &*self.query_registry.lock(),
486            self.registry_query_id.lock().as_ref().copied(),
487        ) {
488            reg.cancel(qid, reason);
489        }
490    }
491
492    /// Legacy cancel (no typed reason).  Delegates to [`cancel_with_reason`]
493    /// with [`CancelReason::UserKill`].
494    pub fn cancel(&self) {
495        self.cancel_with_reason(CancelReason::UserKill);
496    }
497
498    /// Enable deadline-based cancellation.
499    pub fn cancel_on_deadline(&self) {
500        if let Some(deadline) = self.deadline {
501            if Instant::now() >= deadline {
502                self.cancel_with_reason(CancelReason::Deadline);
503            }
504        }
505    }
506
507    /// Set or clear a deadline.
508    pub fn set_deadline(&mut self, deadline: Option<Instant>) {
509        self.deadline = deadline;
510    }
511
512    // ── Profile ──
513
514    pub fn profile(&self) -> &Arc<ProfileBoard> {
515        &self.profile
516    }
517
518    /// Register an operator profile entry (called during `open()`).
519    pub fn register_operator(&self, op_profile: &OperatorProfile) -> Arc<ProfileEntry> {
520        self.profile.register_operator(op_profile)
521    }
522
523    /// Return a [`StateArenaSet`] mutex for the given partition.
524    ///
525    /// Non-partitioned operators (`partition_id == None`) always map to
526    /// arena 0.  Partitioned operators map to `(partition_id + 1)`, with
527    /// fallback to the last arena when the partition count was under-estimated.
528    pub fn state_arena_for(&self, partition_id: Option<usize>) -> &Mutex<StateArenaSet> {
529        let idx = partition_id.map(|p| p + 1).unwrap_or(0);
530        let capped = idx.min(self.state_arenas.len().saturating_sub(1));
531        &self.state_arenas[capped]
532    }
533
534    /// Set the number of partition arenas (must be ≥ 1).
535    pub fn set_partition_count(&mut self, count: usize) {
536        let count = count.max(1);
537        self.state_arenas
538            .resize_with(count, || Mutex::new(StateArenaSet::new()));
539    }
540
541    /// Record that execution has started (profile timing).
542    pub fn profile_start(&self) {
543        self.profile.record_start();
544    }
545
546    /// Record that execution has ended.
547    pub fn profile_end(&self) {
548        self.profile.record_end();
549    }
550
551    /// Add rows to the profile counter.
552    pub fn profile_add_rows(&self, count: u64) {
553        let previous = self.profile.total_rows.fetch_add(count, Ordering::Relaxed);
554        self.maybe_emit_progress(previous, previous.saturating_add(count));
555    }
556
557    /// Row cadence for query-progress notifications (`0` disables).
558    ///
559    /// Set alongside `set_query_manager` by the assembly that owns both the
560    /// executor and the `QueryManager`. Zero by default: unconfigured
561    /// runtimes never emit and pay a single relaxed atomic load per batch.
562    pub fn set_progress_rows_interval(&self, rows_interval: u64) {
563        self.progress_rows_interval
564            .store(rows_interval, Ordering::Relaxed);
565    }
566
567    /// Identity reported in progress notifications, in the `QueryManager`
568    /// id space. A negative query id suppresses emission until the assembly
569    /// provides the real mapping between the two query identity schemes.
570    pub fn set_progress_identity(&self, session_id: i64, query_id: i64) {
571        self.progress_session_id
572            .store(session_id, Ordering::Relaxed);
573        self.progress_query_id.store(query_id, Ordering::Relaxed);
574        self.progress_last_emitted.store(0, Ordering::Relaxed);
575    }
576
577    /// Forward a row watermark to the attached `QueryManager` when it
578    /// crosses an unreported interval bucket. Cheap no-op unless a manager
579    /// is attached, an interval is configured, and the identity is set.
580    fn maybe_emit_progress(&self, previous_total: u64, new_total: u64) {
581        let interval = self.progress_rows_interval.load(Ordering::Relaxed);
582        if interval == 0 {
583            return;
584        }
585        let query_id = self.progress_query_id.load(Ordering::Relaxed);
586        if query_id < 0 {
587            return;
588        }
589        // Clone out of the guard so the lock is released before any callback
590        // runs (progress observers must never be invoked under the lock).
591        let query_manager = match self.query_manager.lock().as_ref() {
592            Some(query_manager) => Arc::clone(query_manager),
593            None => return,
594        };
595        if query_manager.progress_callback_count() == 0 {
596            return;
597        }
598        if new_total / interval == previous_total / interval {
599            return;
600        }
601        let session_id = self.progress_session_id.load(Ordering::Relaxed);
602        // Monotonic guard: concurrent workers may cross buckets out of
603        // order; only the highest watermark is reported.
604        let last = self.progress_last_emitted.load(Ordering::Relaxed);
605        if new_total > last
606            && self
607                .progress_last_emitted
608                .compare_exchange(last, new_total, Ordering::Relaxed, Ordering::Relaxed)
609                .is_ok()
610        {
611            query_manager.emit_progress(session_id, query_id, new_total);
612        }
613    }
614
615    // ── Resource ownership ──
616
617    pub fn resource_owner(&self) -> &Arc<Mutex<ResourceOwner>> {
618        &self.resource_owner
619    }
620
621    /// Register a cleanup callback.
622    pub fn on_cleanup<F>(&self, f: F)
623    where
624        F: FnOnce() + Send + 'static,
625    {
626        self.resource_owner.lock().add(Box::new(f));
627    }
628
629    /// Release all owned resources.
630    pub fn release_resources(&self) {
631        self.resource_owner.lock().release_all();
632    }
633
634    /// Set the morsel worker pool for this query.
635    ///
636    /// When a shared scheduler is available via
637    /// [`set_shared_scheduler`](Self::set_shared_scheduler), this call is
638    /// ignored — the shared scheduler's pool is used instead.  Legacy support:
639    /// creates a per-query pool when no shared scheduler is configured.
640    pub fn set_worker_pool(&self, pool: Option<super::pool::MorselWorkerPool>) {
641        if self.shared_scheduler.lock().is_some() {
642            return;
643        }
644        *self.worker_pool.lock() = pool.map(|p| Arc::new(p) as Arc<dyn TaskScheduler>);
645    }
646
647    /// Set the engine-level shared scheduler for this query.
648    ///
649    /// When set, all parallel execution uses the shared worker pool instead
650    /// of per-query threads.  The scheduler's `Arc<dyn TaskScheduler>` is
651    /// injected into `worker_pool` so existing consumer code paths
652    /// (Exchange / Gather operators) continue to work unchanged.
653    ///
654    /// Takes priority over any per-query pool that may have been set.
655    pub fn set_shared_scheduler(&self, scheduler: Option<Arc<super::pool::SharedScheduler>>) {
656        *self.shared_scheduler.lock() = scheduler.clone();
657        if let Some(ref ss) = scheduler {
658            ss.apply_to_runtime(self);
659        }
660    }
661
662    /// Raw injection — set the worker pool from an `Arc<dyn TaskScheduler>`.
663    /// Used internally by [`SharedScheduler::apply_to_runtime`].
664    pub(crate) fn set_shared_scheduler_raw(&self, pool: Option<Arc<dyn TaskScheduler>>) {
665        *self.worker_pool.lock() = pool;
666    }
667
668    /// Return the shared scheduler, if set.
669    pub fn get_shared_scheduler(&self) -> Option<Arc<super::pool::SharedScheduler>> {
670        self.shared_scheduler.lock().clone()
671    }
672
673    /// Return the effective worker pool — either from the shared scheduler,
674    /// or from the per-query pool, or `None` for serial fallback.
675    pub fn effective_worker_pool(&self) -> Option<Arc<dyn TaskScheduler>> {
676        self.worker_pool.lock().clone()
677    }
678
679    /// Set the spill manager for this query execution.
680    pub fn set_spill_manager(&self, manager: Option<Arc<SpillManager>>) {
681        if let Some(ref m) = manager {
682            m.register_cleanup(self);
683        }
684        *self.spill_manager.lock() = manager;
685    }
686
687    /// Access the spill manager.
688    pub fn get_spill_manager(&self) -> Option<Arc<SpillManager>> {
689        self.spill_manager.lock().clone()
690    }
691
692    /// Return a reference to the bumpalo arena, if configured.
693    pub fn arena(&self) -> Option<&Arc<Mutex<Arena>>> {
694        self.arena.as_ref()
695    }
696
697    /// Return the columnar fast-path counters shared with produced chunks.
698    pub fn columnar_stats(&self) -> Arc<ColumnarStats> {
699        Arc::clone(&self.columnar_stats)
700    }
701
702    /// Return the shared columnar layout policy, if injected.
703    pub fn columnar_policy(&self) -> Option<Arc<super::chunk::ColumnarPolicy>> {
704        self.columnar_policy.clone()
705    }
706
707    /// Inject the shared columnar layout policy (owned by the optimizer
708    /// engine; set by the materializer from the query bindings).
709    pub fn set_columnar_policy(&mut self, policy: Option<Arc<super::chunk::ColumnarPolicy>>) {
710        self.columnar_policy = policy;
711    }
712
713    /// Per-query columnar override for this runtime.
714    pub fn columnar_override(&self) -> super::chunk::QueryColumnarOverride {
715        self.columnar_override
716    }
717
718    /// Pin the columnar decision for this query only.
719    pub fn set_columnar_override(&mut self, query_override: super::chunk::QueryColumnarOverride) {
720        self.columnar_override = query_override;
721    }
722
723    /// Merge this query's columnar hit/miss counts into the shared policy.
724    ///
725    /// Called once when the query finishes (materialized, streaming, and
726    /// discard paths) so the adaptive gate learns across queries.
727    pub fn flush_columnar_stats_to_policy(&self) {
728        if let Some(policy) = &self.columnar_policy {
729            let snapshot = ColumnarStatsSnapshot::from_stats(&self.columnar_stats);
730            // B path hits also represent typed benefit; feed them into the
731            // policy together with the A path hits. Wasted builds stay out:
732            // they measure built-but-unconsumed layouts, not hits/misses.
733            let effective_hits = snapshot.columnar_hits + snapshot.columnar_b_path_hits;
734            policy.merge(effective_hits, snapshot.columnar_misses);
735        }
736    }
737
738    /// Reset the bumpalo arena, freeing all temporary allocations.
739    pub fn reset_arena(&self) {
740        if let Some(arena) = &self.arena {
741            arena.lock().reset();
742        }
743    }
744
745    /// Set the per-partition output channel capacity for parallel operators.
746    pub fn set_max_buffered_chunks(&self, chunks: usize) {
747        self.max_buffered_chunks
748            .store(chunks.max(1), Ordering::Relaxed);
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755
756    #[test]
757    fn test_runtime_default_budget() {
758        let rt = ExecutionRuntime::default_budget();
759        assert!(!rt.is_cancelled());
760        assert_eq!(rt.query_id().query_id, 0);
761    }
762
763    #[test]
764    fn test_cancel_token() {
765        let rt = ExecutionRuntime::default_budget();
766        assert!(!rt.is_cancelled());
767        rt.cancel();
768        assert!(rt.is_cancelled());
769        assert!(rt.ensure_not_cancelled().is_err());
770    }
771
772    #[test]
773    fn test_deadline() {
774        let mut rt = ExecutionRuntime::default_budget();
775        rt.set_deadline(Some(Instant::now()));
776        assert!(rt.is_cancelled());
777    }
778
779    #[test]
780    fn test_profile_add_rows() {
781        let rt = ExecutionRuntime::default_budget();
782        rt.profile_add_rows(10);
783        rt.profile_add_rows(20);
784        assert_eq!(
785            rt.profile()
786                .total_rows
787                .load(std::sync::atomic::Ordering::Relaxed),
788            30
789        );
790    }
791}