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, 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 results = db.query_with("MATCH (n:Person) WHERE n.age > $min_age RETURN n")
19///     .param("min_age", 18)
20///     .timeout(std::time::Duration::from_secs(5))
21///     .fetch_all()
22///     .await?;
23/// # Ok(())
24/// # }
25/// ```
26#[must_use = "query builders do nothing until .fetch_all(), .execute(), or .query_cursor() is called"]
27pub struct QueryBuilder<'a> {
28    db: &'a Uni,
29    cypher: String,
30    params: HashMap<String, Value>,
31    timeout: Option<std::time::Duration>,
32    max_memory: Option<usize>,
33}
34
35impl<'a> QueryBuilder<'a> {
36    pub fn new(db: &'a Uni, cypher: &str) -> Self {
37        Self {
38            db,
39            cypher: cypher.to_string(),
40            params: HashMap::new(),
41            timeout: None,
42            max_memory: None,
43        }
44    }
45
46    /// Set maximum execution time for this query.
47    /// Overrides the default timeout in `UniConfig`.
48    pub fn timeout(mut self, duration: std::time::Duration) -> Self {
49        self.timeout = Some(duration);
50        self
51    }
52
53    /// Set maximum memory per query in bytes.
54    /// Overrides the default limit in `UniConfig`.
55    pub fn max_memory(mut self, bytes: usize) -> Self {
56        self.max_memory = Some(bytes);
57        self
58    }
59
60    /// Bind a parameter to the query.
61    ///
62    /// The parameter name should not include the `$` prefix.
63    pub fn param(mut self, name: &str, value: impl Into<Value>) -> Self {
64        self.params.insert(name.to_string(), value.into());
65        self
66    }
67
68    /// Bind multiple parameters from an iterator or collection.
69    pub fn params<'p>(mut self, params: impl IntoIterator<Item = (&'p str, Value)>) -> Self {
70        for (k, v) in params {
71            self.params.insert(k.to_string(), v);
72        }
73        self
74    }
75
76    /// Execute the query and fetch all results into memory.
77    pub async fn fetch_all(self) -> Result<QueryResult> {
78        let mut db_config = self.db.config().clone();
79        if let Some(t) = self.timeout {
80            db_config.query_timeout = t;
81        }
82        if let Some(m) = self.max_memory {
83            db_config.max_query_memory = m;
84        }
85
86        self.db
87            .execute_internal_with_config(&self.cypher, self.params, db_config)
88            .await
89    }
90
91    /// Execute a mutation (CREATE, SET, DELETE, etc.) and return affected row count.
92    pub async fn execute(self) -> Result<ExecuteResult> {
93        let db = self.db;
94        let before = db.get_mutation_count().await;
95        let result = self.fetch_all().await?;
96        let affected_rows = if result.is_empty() {
97            db.get_mutation_count().await.saturating_sub(before)
98        } else {
99            result.len()
100        };
101        Ok(ExecuteResult { affected_rows })
102    }
103
104    /// Execute the query and return a cursor for streaming results.
105    ///
106    /// Useful for large result sets to avoid loading everything into memory.
107    pub async fn query_cursor(self) -> Result<QueryCursor> {
108        let mut db_config = self.db.config().clone();
109        if let Some(t) = self.timeout {
110            db_config.query_timeout = t;
111        }
112        if let Some(m) = self.max_memory {
113            db_config.max_query_memory = m;
114        }
115
116        self.db
117            .execute_cursor_internal_with_config(&self.cypher, self.params, db_config)
118            .await
119    }
120}