Skip to main content

uni_db/api/
prepared.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Prepared statements for repeated query and Locy program execution.
5//!
6//! `PreparedQuery` caches the parsed AST and logical plan so that repeated
7//! executions skip the parse and plan phases. `PreparedLocy` caches the
8//! compiled program. Both transparently refresh if the schema changes.
9
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use crate::api::UniInner;
14use crate::api::impl_locy::LocyRuleRegistry;
15use crate::api::locy_result::LocyResult;
16use uni_common::{Result, UniError, Value};
17use uni_locy::LocyConfig;
18use uni_query::QueryResult;
19
20// ── PreparedQuery ─────────────────────────────────────────────────
21
22/// Interior state for schema-staleness detection.
23struct PreparedQueryInner {
24    ast: uni_query::CypherQuery,
25    plan: uni_query::LogicalPlan,
26    schema_version: u32,
27}
28
29/// A prepared Cypher query with a cached logical plan.
30///
31/// Created via [`Session::prepare()`](crate::api::session::Session::prepare).
32/// The plan is cached and reused across executions. If the database schema
33/// changes, the plan is automatically regenerated.
34///
35/// All methods take `&self`, so a `PreparedQuery` can be shared across
36/// threads via `Arc<PreparedQuery>`.
37pub struct PreparedQuery {
38    db: Arc<UniInner>,
39    query_text: String,
40    inner: std::sync::RwLock<PreparedQueryInner>,
41}
42
43impl PreparedQuery {
44    /// Create a new prepared query by parsing and planning the given Cypher.
45    pub(crate) async fn new(db: Arc<UniInner>, cypher: &str) -> Result<Self> {
46        let ast = uni_cypher::parse(cypher).map_err(|e| UniError::Parse {
47            message: e.to_string(),
48            position: None,
49            line: None,
50            column: None,
51            context: Some(cypher.to_string()),
52        })?;
53
54        let schema_version = db.schema.schema().schema_version;
55        let planner = uni_query::QueryPlanner::new(db.schema.schema().clone());
56        let plan = planner.plan(ast.clone()).map_err(|e| UniError::Query {
57            message: e.to_string(),
58            query: Some(cypher.to_string()),
59        })?;
60
61        Ok(Self {
62            db,
63            query_text: cypher.to_string(),
64            inner: std::sync::RwLock::new(PreparedQueryInner {
65                ast,
66                plan,
67                schema_version,
68            }),
69        })
70    }
71
72    /// Execute the prepared query with the given parameters.
73    ///
74    /// If the schema has changed since preparation, the query is
75    /// transparently re-planned before execution.
76    pub async fn execute(&self, params: &[(&str, Value)]) -> Result<QueryResult> {
77        self.ensure_plan_fresh()?;
78
79        let param_map: HashMap<String, Value> = params
80            .iter()
81            .map(|(k, v)| (k.to_string(), v.clone()))
82            .collect();
83
84        let plan = {
85            let inner = self.inner.read().unwrap();
86            inner.plan.clone()
87        };
88
89        self.db
90            .execute_plan_internal(
91                plan,
92                &self.query_text,
93                param_map,
94                self.db.config.clone(),
95                None,
96            )
97            .await
98    }
99
100    /// Fluent parameter builder for executing a prepared query.
101    pub fn bind(&self) -> PreparedQueryBinder<'_> {
102        PreparedQueryBinder {
103            prepared: self,
104            params: HashMap::new(),
105        }
106    }
107
108    /// The original query text.
109    pub fn query_text(&self) -> &str {
110        &self.query_text
111    }
112
113    /// Re-plan the query if the schema has changed.
114    fn ensure_plan_fresh(&self) -> Result<()> {
115        let current_version = self.db.schema.schema().schema_version;
116
117        // Fast path: read lock only
118        {
119            let inner = self.inner.read().unwrap();
120            if inner.schema_version == current_version {
121                return Ok(());
122            }
123        }
124
125        // Slow path: write lock with double-check
126        let mut inner = self.inner.write().unwrap();
127        if inner.schema_version == current_version {
128            return Ok(());
129        }
130
131        let planner = uni_query::QueryPlanner::new(self.db.schema.schema().clone());
132        inner.plan = planner
133            .plan(inner.ast.clone())
134            .map_err(|e| UniError::Query {
135                message: e.to_string(),
136                query: Some(self.query_text.clone()),
137            })?;
138        inner.schema_version = current_version;
139        Ok(())
140    }
141}
142
143/// Fluent parameter builder for executing a [`PreparedQuery`].
144pub struct PreparedQueryBinder<'a> {
145    prepared: &'a PreparedQuery,
146    params: HashMap<String, Value>,
147}
148
149impl<'a> PreparedQueryBinder<'a> {
150    /// Bind a named parameter.
151    pub fn param<K: Into<String>, V: Into<Value>>(mut self, key: K, value: V) -> Self {
152        self.params.insert(key.into(), value.into());
153        self
154    }
155
156    /// Execute with the bound parameters.
157    pub async fn execute(self) -> Result<QueryResult> {
158        self.prepared.ensure_plan_fresh()?;
159
160        let plan = {
161            let inner = self.prepared.inner.read().unwrap();
162            inner.plan.clone()
163        };
164
165        self.prepared
166            .db
167            .execute_plan_internal(
168                plan,
169                &self.prepared.query_text,
170                self.params,
171                self.prepared.db.config.clone(),
172                None,
173            )
174            .await
175    }
176}
177
178// ── PreparedLocy ──────────────────────────────────────────────────
179
180/// Interior state for schema-staleness detection.
181struct PreparedLocyInner {
182    compiled: uni_locy::CompiledProgram,
183    schema_version: u32,
184}
185
186/// A prepared Locy program with a cached compiled program.
187///
188/// Created via [`Session::prepare_locy()`](crate::api::session::Session::prepare_locy).
189/// The compiled program is cached and reused across executions. If the database
190/// schema changes, the program is automatically recompiled.
191///
192/// All methods take `&self`, so a `PreparedLocy` can be shared across
193/// threads via `Arc<PreparedLocy>`.
194pub struct PreparedLocy {
195    db: Arc<UniInner>,
196    rule_registry: Arc<std::sync::RwLock<LocyRuleRegistry>>,
197    program_text: String,
198    inner: std::sync::RwLock<PreparedLocyInner>,
199}
200
201impl PreparedLocy {
202    /// Create a new prepared Locy program.
203    pub(crate) fn new(
204        db: Arc<UniInner>,
205        rule_registry: Arc<std::sync::RwLock<LocyRuleRegistry>>,
206        program: &str,
207    ) -> Result<Self> {
208        let compiled = compile_locy_with_registry(program, &rule_registry)?;
209        let schema_version = db.schema.schema().schema_version;
210
211        Ok(Self {
212            db,
213            rule_registry,
214            program_text: program.to_string(),
215            inner: std::sync::RwLock::new(PreparedLocyInner {
216                compiled,
217                schema_version,
218            }),
219        })
220    }
221
222    /// Execute the prepared Locy program with the given parameters.
223    ///
224    /// Uses the cached compiled program. If the schema has changed since
225    /// preparation, the program is automatically recompiled before execution.
226    pub async fn execute(&self, params: &[(&str, Value)]) -> Result<LocyResult> {
227        let param_map: HashMap<String, Value> = params
228            .iter()
229            .map(|(k, v)| (k.to_string(), v.clone()))
230            .collect();
231        self.execute_internal(param_map).await
232    }
233
234    /// Fluent parameter builder for executing a prepared Locy program.
235    pub fn bind(&self) -> PreparedLocyBinder<'_> {
236        PreparedLocyBinder {
237            prepared: self,
238            params: HashMap::new(),
239        }
240    }
241
242    /// The original program text.
243    pub fn program_text(&self) -> &str {
244        &self.program_text
245    }
246
247    /// Internal execution with a parameter map.
248    async fn execute_internal(&self, params: HashMap<String, Value>) -> Result<LocyResult> {
249        self.ensure_compiled_fresh()?;
250
251        // Clone the compiled program and merge rules
252        let mut compiled = {
253            let inner = self.inner.read().unwrap();
254            inner.compiled.clone()
255        };
256
257        // Merge registered rules (same logic as evaluate_with_config)
258        {
259            let registry = self.rule_registry.read().unwrap();
260            if !registry.rules.is_empty() {
261                for (name, rule) in &registry.rules {
262                    compiled
263                        .rule_catalog
264                        .entry(name.clone())
265                        .or_insert_with(|| rule.clone());
266                }
267                let base_id = registry.strata.len();
268                for stratum in &mut compiled.strata {
269                    stratum.id += base_id;
270                    stratum.depends_on = stratum.depends_on.iter().map(|d| d + base_id).collect();
271                }
272                let mut merged_strata = registry.strata.clone();
273                merged_strata.append(&mut compiled.strata);
274                compiled.strata = merged_strata;
275            }
276        }
277
278        // Build config with params and session-level semantics
279        let config = LocyConfig {
280            params,
281            ..LocyConfig::default()
282        };
283
284        let engine = crate::api::impl_locy::LocyEngine {
285            db: &self.db,
286            tx_l0_override: None,
287            locy_l0: None,
288            collect_derive: true,
289        };
290        engine
291            .evaluate_compiled_with_config(compiled, &config)
292            .await
293    }
294
295    /// Re-compile if the schema has changed.
296    fn ensure_compiled_fresh(&self) -> Result<()> {
297        let current_version = self.db.schema.schema().schema_version;
298
299        // Fast path: read lock only
300        {
301            let inner = self.inner.read().unwrap();
302            if inner.schema_version == current_version {
303                return Ok(());
304            }
305        }
306
307        // Slow path: write lock with double-check
308        let mut inner = self.inner.write().unwrap();
309        if inner.schema_version == current_version {
310            return Ok(());
311        }
312
313        inner.compiled = compile_locy_with_registry(&self.program_text, &self.rule_registry)?;
314        inner.schema_version = current_version;
315        Ok(())
316    }
317}
318
319/// Parse and compile a Locy program using the given rule registry.
320///
321/// Shared between `PreparedLocy::new()` and `PreparedLocy::ensure_compiled_fresh()`.
322fn compile_locy_with_registry(
323    program: &str,
324    rule_registry: &std::sync::RwLock<LocyRuleRegistry>,
325) -> Result<uni_locy::CompiledProgram> {
326    let ast = uni_cypher::parse_locy(program).map_err(|e| UniError::Parse {
327        message: format!("LocyParseError: {e}"),
328        position: None,
329        line: None,
330        column: None,
331        context: None,
332    })?;
333
334    let registry = rule_registry.read().unwrap();
335    if registry.rules.is_empty() {
336        drop(registry);
337        uni_locy::compile(&ast).map_err(|e| UniError::Query {
338            message: format!("LocyCompileError: {e}"),
339            query: None,
340        })
341    } else {
342        let external_names: Vec<String> = registry.rules.keys().cloned().collect();
343        drop(registry);
344        uni_locy::compile_with_external_rules(&ast, &external_names).map_err(|e| UniError::Query {
345            message: format!("LocyCompileError: {e}"),
346            query: None,
347        })
348    }
349}
350
351/// Fluent parameter builder for executing a [`PreparedLocy`].
352pub struct PreparedLocyBinder<'a> {
353    prepared: &'a PreparedLocy,
354    params: HashMap<String, Value>,
355}
356
357impl<'a> PreparedLocyBinder<'a> {
358    /// Bind a named parameter.
359    pub fn param<K: Into<String>, V: Into<Value>>(mut self, key: K, value: V) -> Self {
360        self.params.insert(key.into(), value.into());
361        self
362    }
363
364    /// Execute with the bound parameters.
365    pub async fn execute(self) -> Result<LocyResult> {
366        self.prepared.execute_internal(self.params).await
367    }
368}