Skip to main content

linkrs_api/api_core/
query_api_builder.rs

1//! Builder / configuration surface for [`QueryApi`].
2//!
3//! The ~20 `with_*` constructors and accessor/configuration methods were split
4//! out of `query_api.rs` so the execution core stays readable. They all return
5//! `Self` (builder chaining) or expose a pipeline setting; pipeline internals
6//! (the `pipeline_manager` field) are reached through the parent module.
7
8use crate::api_core::cost_profile::cost_config_for_profile;
9use crate::api_core::cost_profile::resolve_space_cost_configs;
10use crate::api_core::error::{CoreError, CoreResult};
11use crate::api_core::QueryApi;
12use linkrs_config::{RuntimeConfig, StorageCostProfile};
13use linkrs_core::metadata::SchemaManager;
14use linkrs_metrics::StatsManager;
15use linkrs_query::executor::streaming::pool::SharedScheduler;
16use linkrs_query::executor::streaming::query_registry::QueryRegistry;
17use linkrs_query::experimental_pipeline_extensions::ExtensionRegistry;
18use linkrs_query::optimizer::CostModelConfig;
19use linkrs_query::query_manager::QueryManager;
20use linkrs_query::OptimizerEngine;
21use linkrs_storage::StorageClient;
22#[cfg(feature = "vector")]
23use linkrs_sync::backend::VectorBackend;
24use parking_lot::RwLock;
25use std::collections::HashMap;
26use std::sync::Arc;
27
28impl<S: StorageClient + Clone + 'static> QueryApi<S> {
29    /// Attach a schema manager to the pipeline.
30    pub fn with_schema_manager(mut self, schema_manager: Arc<SchemaManager>) -> Self {
31        self.pipeline_manager = self.pipeline_manager.with_schema_manager(schema_manager);
32        self
33    }
34
35    /// Attach a sync manager to the pipeline.
36    pub fn with_sync_manager(mut self, sync_manager: Arc<linkrs_sync::SyncManager>) -> Self {
37        self.pipeline_manager = self.pipeline_manager.with_sync_manager(sync_manager);
38        self
39    }
40
41    /// Create a new QueryApi using an externally configured optimizer engine,
42    /// so server-level settings (e.g. the `[parallel]` partitioning section)
43    /// reach the query pipeline. The engine instance is shared by value.
44    pub fn with_optimizer_engine(
45        storage: Arc<RwLock<S>>,
46        stats_manager: Arc<StatsManager>,
47        optimizer_engine: Arc<OptimizerEngine>,
48        schema_manager: Option<Arc<SchemaManager>>,
49    ) -> Self {
50        let pipeline = Self::pipeline_with_engine(storage, stats_manager, optimizer_engine);
51        let pipeline = match schema_manager {
52            Some(sm) => pipeline.with_schema_manager(sm),
53            None => pipeline,
54        };
55        Self {
56            pipeline_manager: pipeline,
57        }
58    }
59
60    /// Apply an explicit cost model configuration, replacing the default
61    /// engine built by the constructors.
62    ///
63    /// Assembly-time only: call before the first query. The replacement
64    /// engine is freshly built, exactly like the constructor default;
65    /// previously installed per-space overrides are carried over.
66    pub fn with_cost_config(mut self, config: CostModelConfig) -> Self {
67        let spaces = self
68            .pipeline_manager
69            .optimizer_engine()
70            .space_cost_configs();
71        let engine = OptimizerEngine::new(config);
72        engine.set_space_cost_configs(spaces);
73        self.pipeline_manager.set_optimizer_engine(Arc::new(engine));
74        self
75    }
76
77    /// Apply a storage cost profile with runtime knowledge.
78    ///
79    /// Memory-mode deployments short-circuit to the in-memory preset without
80    /// touching the filesystem; embedded in-memory users must call this (or
81    /// [`Self::with_cost_config`]), otherwise the constructor default prices
82    /// queries with mechanical-drive costs.
83    pub fn with_cost_profile(
84        mut self,
85        profile: StorageCostProfile,
86        runtime: &RuntimeConfig,
87    ) -> Self {
88        let resolved = profile.resolve_for_runtime(runtime);
89        self = self.with_cost_config(cost_config_for_profile(resolved));
90        self.pipeline_manager
91            .set_default_cost_profile_label(format!("{resolved:?}"));
92        self
93    }
94
95    /// Apply per-space storage cost profile overrides.
96    ///
97    /// Spaces absent from the map keep the global profile; memory-mode
98    /// deployments short-circuit every space to the in-memory preset.
99    /// Assembly-time only: call before the first query.
100    pub fn with_space_cost_profiles(
101        mut self,
102        profiles: &HashMap<String, StorageCostProfile>,
103        runtime: &RuntimeConfig,
104    ) -> Self {
105        let (configs, labels) = resolve_space_cost_configs(profiles, runtime);
106        self.pipeline_manager
107            .set_space_cost_configs(configs, labels);
108        self
109    }
110
111    /// Override the global cost profile display label (assembly-time only).
112    ///
113    /// Used by assemblies that build their own engine (e.g. the network
114    /// server with extra engine settings) so EXPLAIN still names the
115    /// effective profile.
116    pub fn with_default_cost_profile_label(mut self, label: String) -> Self {
117        self.pipeline_manager.set_default_cost_profile_label(label);
118        self
119    }
120
121    /// Effective cost model configuration currently in force.
122    pub fn cost_config(&self) -> CostModelConfig {
123        *self.pipeline_manager.optimizer_engine().cost_config()
124    }
125
126    /// Apply the server-configured statistics collection settings to the
127    /// pipeline (sampling window and automatic-refresh epoch delta).
128    ///
129    /// The pipeline keeps fallback defaults when this is never called, so
130    /// embedded users without server config are unaffected.
131    pub fn with_statistics_settings(mut self, sample_limit: usize, min_epoch_delta: u64) -> Self {
132        self.pipeline_manager = self
133            .pipeline_manager
134            .with_statistics_sample_limit(sample_limit)
135            .with_statistics_min_epoch_delta(min_epoch_delta);
136        self
137    }
138
139    /// Effective statistics sampling window currently in force.
140    pub fn statistics_sample_limit(&self) -> usize {
141        self.pipeline_manager.statistics_sample_limit()
142    }
143
144    /// Set the retained password-history depth applied by user-management
145    /// operations (assembly-time only).
146    pub fn with_password_history_depth(mut self, depth: usize) -> Self {
147        self.pipeline_manager = self.pipeline_manager.with_password_history_depth(depth);
148        self
149    }
150
151    /// Effective statistics refresh epoch delta currently in force.
152    pub fn statistics_min_epoch_delta(&self) -> u64 {
153        self.pipeline_manager.statistics_min_epoch_delta()
154    }
155
156    /// Install a shared scheduler and query registry into an existing
157    /// pipeline (used when the pipeline was built by another constructor).
158    pub fn install_shared_scheduler(
159        &mut self,
160        shared_scheduler: Arc<SharedScheduler>,
161        query_registry: Arc<QueryRegistry>,
162    ) {
163        self.pipeline_manager
164            .set_shared_scheduler(Some(shared_scheduler));
165        self.pipeline_manager
166            .set_query_registry(Some(query_registry));
167    }
168
169    /// Access the shared scheduler instance held by the pipeline, if any.
170    pub fn shared_scheduler(&self) -> Option<Arc<SharedScheduler>> {
171        self.pipeline_manager.shared_scheduler()
172    }
173
174    /// Access the query registry instance held by the pipeline, if any.
175    pub fn query_registry(&self) -> Option<Arc<QueryRegistry>> {
176        self.pipeline_manager.query_registry()
177    }
178
179    /// Install the global QueryManager so per-query runtimes forward KILL QUERY,
180    /// finish tracking, and progress events into one registry. Mirror of
181    /// [`install_shared_scheduler`]: called once at server startup.
182    pub fn install_query_manager(&mut self, query_manager: Arc<QueryManager>) {
183        self.pipeline_manager.set_query_manager(Some(query_manager));
184    }
185
186    /// Install a shared pipeline-extension registry (unstable experimental
187    /// API) into an existing pipeline.
188    pub fn set_extensions(&mut self, extensions: Arc<ExtensionRegistry>) {
189        self.pipeline_manager.set_extensions(extensions);
190    }
191
192    /// Pipeline-extension registry backing this API.
193    pub fn extensions(&self) -> Arc<ExtensionRegistry> {
194        self.pipeline_manager.extensions()
195    }
196
197    /// Collect (or serve cached) optimizer statistics for a space.
198    ///
199    /// `force` bypasses the schema-version gate so an explicit ANALYZE always
200    /// refreshes the statistics. Failures are returned as `Err`, never panic.
201    pub fn collect_statistics(&self, space: &str, force: bool) -> CoreResult<()> {
202        self.pipeline_manager
203            .collect_statistics(space, force)
204            .map_err(CoreError::Internal)
205            .map(|_| ())
206    }
207
208    /// Query-plan-cache hit rate across all executed statements.
209    pub fn plan_cache_hit_rate(&self) -> f64 {
210        self.pipeline_manager.plan_cache_metrics().hit_rate()
211    }
212
213    /// Number of entries currently held in the query plan cache.
214    pub fn plan_cache_len(&self) -> usize {
215        self.pipeline_manager.plan_cache().len()
216    }
217
218    /// Create a new QueryApi instance with an existing shared VectorBackend
219    #[cfg(feature = "vector")]
220    pub async fn with_vector_backend(
221        storage: Arc<RwLock<S>>,
222        stats_manager: Arc<StatsManager>,
223        backend: VectorBackend,
224        schema_manager: Option<Arc<SchemaManager>>,
225    ) -> CoreResult<Self> {
226        let optimizer_engine = Arc::new(OptimizerEngine::default());
227
228        // Create a VectorSyncCoordinator with the shared backend (no embedding service for query-only use)
229        let handle = tokio::runtime::Handle::current();
230        let vector_coordinator = Arc::new(
231            linkrs_sync::vector_sync::VectorSyncCoordinator::new_without_embedding(backend, handle),
232        );
233
234        // Create pipeline manager with vector coordinator and optional schema manager
235        let mut pipeline_manager =
236            Self::pipeline_with_engine(storage, stats_manager, optimizer_engine);
237
238        if let Some(sm) = schema_manager {
239            pipeline_manager = pipeline_manager
240                .with_schema_manager(sm)
241                .with_vector_coordinator(vector_coordinator);
242        } else {
243            pipeline_manager = pipeline_manager.with_vector_coordinator(vector_coordinator);
244        }
245
246        Ok(Self { pipeline_manager })
247    }
248}