Skip to main content

uni_db/api/
query_builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4use crate::api::Uni;
5use std::collections::HashMap;
6use uni_common::Result;
7use uni_query::{ExecuteResult, QueryCursor, QueryResult, Row, Value};
8
9/// Builder for constructing and executing Cypher queries.
10///
11/// Supports parameter binding, timeouts, and resource limits.
12///
13/// # Examples
14///
15/// ```no_run
16/// # use uni_db::{Uni, Value};
17/// # async fn example(db: &Uni) -> uni_db::Result<()> {
18/// let session = db.session();
19/// let results = session.query_with("MATCH (n:Person) WHERE n.age > $min_age RETURN n")
20///     .param("min_age", 18)
21///     .timeout(std::time::Duration::from_secs(5))
22///     .fetch_all()
23///     .await?;
24/// # Ok(())
25/// # }
26/// ```
27#[must_use = "query builders do nothing until .fetch_all(), .fetch_one(), or .query_cursor() is called"]
28pub struct QueryBuilder<'a> {
29    db: &'a Uni,
30    cypher: String,
31    params: HashMap<String, Value>,
32    timeout: Option<std::time::Duration>,
33    max_memory: Option<usize>,
34}
35
36impl<'a> QueryBuilder<'a> {
37    pub fn new(db: &'a Uni, cypher: &str) -> Self {
38        Self {
39            db,
40            cypher: cypher.to_string(),
41            params: HashMap::new(),
42            timeout: None,
43            max_memory: None,
44        }
45    }
46
47    /// Set maximum execution time for this query.
48    /// Overrides the default timeout in `UniConfig`.
49    pub fn timeout(mut self, duration: std::time::Duration) -> Self {
50        self.timeout = Some(duration);
51        self
52    }
53
54    /// Set maximum memory per query in bytes.
55    /// Overrides the default limit in `UniConfig`.
56    pub fn max_memory(mut self, bytes: usize) -> Self {
57        self.max_memory = Some(bytes);
58        self
59    }
60
61    /// Bind a parameter to the query.
62    ///
63    /// The parameter name should not include the `$` prefix.
64    pub fn param(mut self, name: &str, value: impl Into<Value>) -> Self {
65        self.params.insert(name.to_string(), value.into());
66        self
67    }
68
69    /// Bind multiple parameters from an iterator or collection.
70    pub fn params<'p>(mut self, params: impl IntoIterator<Item = (&'p str, Value)>) -> Self {
71        for (k, v) in params {
72            self.params.insert(k.to_string(), v);
73        }
74        self
75    }
76
77    /// Execute the query and fetch all results into memory.
78    pub async fn fetch_all(self) -> Result<QueryResult> {
79        let mut db_config = self.db.inner.config.clone();
80        if let Some(t) = self.timeout {
81            db_config.query_timeout = t;
82        }
83        if let Some(m) = self.max_memory {
84            db_config.max_query_memory = m;
85        }
86
87        self.db
88            .inner
89            .execute_internal_with_config(&self.cypher, self.params, db_config)
90            .await
91    }
92
93    /// Execute the query and return the first row, or `None` if empty.
94    pub async fn fetch_one(self) -> Result<Option<Row>> {
95        let result = self.fetch_all().await?;
96        Ok(result.into_rows().into_iter().next())
97    }
98
99    /// Execute a mutation (CREATE, SET, DELETE, etc.) and return affected row count.
100    #[deprecated(
101        since = "0.4.0",
102        note = "Use `session.execute_with(cypher).run()` for auto-committed writes, or `fetch_all()` for queries"
103    )]
104    pub async fn execute(self) -> Result<ExecuteResult> {
105        let inner = &self.db.inner;
106        let before = inner.get_mutation_count().await;
107        let result = self.fetch_all().await?;
108        let affected_rows = if result.is_empty() {
109            inner.get_mutation_count().await.saturating_sub(before)
110        } else {
111            result.len()
112        };
113        Ok(ExecuteResult::new(affected_rows))
114    }
115
116    /// Execute the query and return a cursor for streaming results.
117    ///
118    /// Useful for large result sets to avoid loading everything into memory.
119    pub async fn query_cursor(self) -> Result<QueryCursor> {
120        let mut db_config = self.db.inner.config.clone();
121        if let Some(t) = self.timeout {
122            db_config.query_timeout = t;
123        }
124        if let Some(m) = self.max_memory {
125            db_config.max_query_memory = m;
126        }
127
128        self.db
129            .inner
130            .execute_cursor_internal_with_config(&self.cypher, self.params, db_config)
131            .await
132    }
133}