Skip to main content

graphlite_sdk/
query.rs

1//! Query builder for fluent GQL query construction
2//!
3//! This module provides a builder API for constructing GQL queries in a
4//! type-safe and ergonomic way.
5
6use crate::connection::Session;
7use crate::error::{Error, Result};
8use graphlite::QueryResult;
9
10/// Fluent API for building GQL queries
11///
12/// QueryBuilder provides a convenient way to construct complex GQL queries
13/// without manually concatenating strings.
14///
15/// # Examples
16///
17/// ```no_run
18/// # use graphlite_sdk::GraphLite;
19/// # let db = GraphLite::open("./mydb")?;
20/// # let session = db.session("admin")?;
21/// // Using the builder
22/// let result = session.query_builder()
23///     .match_pattern("(p:Person)")
24///     .where_clause("p.age > 25")
25///     .return_clause("p.name, p.age")
26///     .execute()?;
27///
28/// // Equivalent to:
29/// // "MATCH (p:Person) WHERE p.age > 25 RETURN p.name, p.age"
30/// # Ok::<(), graphlite_sdk::Error>(())
31/// ```
32pub struct QueryBuilder<'session> {
33    session: &'session Session,
34    match_patterns: Vec<String>,
35    where_clauses: Vec<String>,
36    with_clauses: Vec<String>,
37    return_clause: Option<String>,
38    order_by: Option<String>,
39    skip: Option<usize>,
40    limit: Option<usize>,
41}
42
43impl<'session> QueryBuilder<'session> {
44    /// Create a new query builder
45    pub(crate) fn new(session: &'session Session) -> Self {
46        QueryBuilder {
47            session,
48            match_patterns: Vec::new(),
49            where_clauses: Vec::new(),
50            with_clauses: Vec::new(),
51            return_clause: None,
52            order_by: None,
53            skip: None,
54            limit: None,
55        }
56    }
57
58    /// Add a MATCH pattern
59    ///
60    /// Can be called multiple times to add multiple MATCH patterns.
61    ///
62    /// # Arguments
63    ///
64    /// * `pattern` - Graph pattern to match (without the MATCH keyword)
65    ///
66    /// # Examples
67    ///
68    /// ```no_run
69    /// # use graphlite_sdk::GraphLite;
70    /// # let db = GraphLite::open("./mydb")?;
71    /// # let session = db.session("admin")?;
72    /// session.query_builder()
73    ///     .match_pattern("(p:Person)")
74    ///     .match_pattern("(p)-[:KNOWS]->(f:Person)")
75    ///     .return_clause("p.name, f.name");
76    /// # Ok::<(), graphlite_sdk::Error>(())
77    /// ```
78    pub fn match_pattern(mut self, pattern: &str) -> Self {
79        self.match_patterns.push(pattern.to_string());
80        self
81    }
82
83    /// Add a WHERE clause condition
84    ///
85    /// Can be called multiple times - conditions are AND'ed together.
86    ///
87    /// # Arguments
88    ///
89    /// * `condition` - Condition to add (without the WHERE keyword)
90    ///
91    /// # Examples
92    ///
93    /// ```no_run
94    /// # use graphlite_sdk::GraphLite;
95    /// # let db = GraphLite::open("./mydb")?;
96    /// # let session = db.session("admin")?;
97    /// session.query_builder()
98    ///     .match_pattern("(p:Person)")
99    ///     .where_clause("p.age > 25")
100    ///     .where_clause("p.name STARTS WITH 'A'")
101    ///     .return_clause("p");
102    /// # Ok::<(), graphlite_sdk::Error>(())
103    /// ```
104    pub fn where_clause(mut self, condition: &str) -> Self {
105        self.where_clauses.push(condition.to_string());
106        self
107    }
108
109    /// Add a WITH clause
110    ///
111    /// WITH clauses are used for query chaining and intermediate results.
112    ///
113    /// # Arguments
114    ///
115    /// * `clause` - WITH clause content (without the WITH keyword)
116    ///
117    /// # Examples
118    ///
119    /// ```no_run
120    /// # use graphlite_sdk::GraphLite;
121    /// # let db = GraphLite::open("./mydb")?;
122    /// # let session = db.session("admin")?;
123    /// session.query_builder()
124    ///     .match_pattern("(p:Person)")
125    ///     .with_clause("p, p.age as age")
126    ///     .where_clause("age > 25")
127    ///     .return_clause("p.name");
128    /// # Ok::<(), graphlite_sdk::Error>(())
129    /// ```
130    pub fn with_clause(mut self, clause: &str) -> Self {
131        self.with_clauses.push(clause.to_string());
132        self
133    }
134
135    /// Set the RETURN clause
136    ///
137    /// Specifies what to return from the query. Required for MATCH queries.
138    ///
139    /// # Arguments
140    ///
141    /// * `clause` - Return clause content (without the RETURN keyword)
142    ///
143    /// # Examples
144    ///
145    /// ```no_run
146    /// # use graphlite_sdk::GraphLite;
147    /// # let db = GraphLite::open("./mydb")?;
148    /// # let session = db.session("admin")?;
149    /// session.query_builder()
150    ///     .match_pattern("(p:Person)")
151    ///     .return_clause("p.name, p.age");
152    /// # Ok::<(), graphlite_sdk::Error>(())
153    /// ```
154    pub fn return_clause(mut self, clause: &str) -> Self {
155        self.return_clause = Some(clause.to_string());
156        self
157    }
158
159    /// Set the ORDER BY clause
160    ///
161    /// # Arguments
162    ///
163    /// * `clause` - Order by clause (without the ORDER BY keywords)
164    ///
165    /// # Examples
166    ///
167    /// ```no_run
168    /// # use graphlite_sdk::GraphLite;
169    /// # let db = GraphLite::open("./mydb")?;
170    /// # let session = db.session("admin")?;
171    /// session.query_builder()
172    ///     .match_pattern("(p:Person)")
173    ///     .return_clause("p.name, p.age")
174    ///     .order_by("p.age DESC");
175    /// # Ok::<(), graphlite_sdk::Error>(())
176    /// ```
177    pub fn order_by(mut self, clause: &str) -> Self {
178        self.order_by = Some(clause.to_string());
179        self
180    }
181
182    /// Set the SKIP value
183    ///
184    /// Skips the first N results.
185    ///
186    /// # Arguments
187    ///
188    /// * `n` - Number of results to skip
189    ///
190    /// # Examples
191    ///
192    /// ```no_run
193    /// # use graphlite_sdk::GraphLite;
194    /// # let db = GraphLite::open("./mydb")?;
195    /// # let session = db.session("admin")?;
196    /// session.query_builder()
197    ///     .match_pattern("(p:Person)")
198    ///     .return_clause("p")
199    ///     .skip(10);  // Skip first 10 results
200    /// # Ok::<(), graphlite_sdk::Error>(())
201    /// ```
202    pub fn skip(mut self, n: usize) -> Self {
203        self.skip = Some(n);
204        self
205    }
206
207    /// Set the LIMIT value
208    ///
209    /// Limits the number of results returned.
210    ///
211    /// # Arguments
212    ///
213    /// * `n` - Maximum number of results to return
214    ///
215    /// # Examples
216    ///
217    /// ```no_run
218    /// # use graphlite_sdk::GraphLite;
219    /// # let db = GraphLite::open("./mydb")?;
220    /// # let session = db.session("admin")?;
221    /// session.query_builder()
222    ///     .match_pattern("(p:Person)")
223    ///     .return_clause("p")
224    ///     .limit(10);  // Return max 10 results
225    /// # Ok::<(), graphlite_sdk::Error>(())
226    /// ```
227    pub fn limit(mut self, n: usize) -> Self {
228        self.limit = Some(n);
229        self
230    }
231
232    /// Build the query string without executing
233    ///
234    /// Returns the constructed GQL query as a string.
235    ///
236    /// # Examples
237    ///
238    /// ```no_run
239    /// # use graphlite_sdk::GraphLite;
240    /// # let db = GraphLite::open("./mydb")?;
241    /// # let session = db.session("admin")?;
242    /// let query = session.query_builder()
243    ///     .match_pattern("(p:Person)")
244    ///     .where_clause("p.age > 25")
245    ///     .return_clause("p.name")
246    ///     .build()?;
247    ///
248    /// assert_eq!(query, "MATCH (p:Person) WHERE p.age > 25 RETURN p.name");
249    /// # Ok::<(), graphlite_sdk::Error>(())
250    /// ```
251    pub fn build(&self) -> Result<String> {
252        let mut query = String::new();
253
254        // MATCH clauses
255        if !self.match_patterns.is_empty() {
256            for pattern in &self.match_patterns {
257                if !query.is_empty() {
258                    query.push(' ');
259                }
260                query.push_str("MATCH ");
261                query.push_str(pattern);
262            }
263        }
264
265        // WHERE clause
266        if !self.where_clauses.is_empty() {
267            query.push_str(" WHERE ");
268            query.push_str(&self.where_clauses.join(" AND "));
269        }
270
271        // WITH clauses
272        for with_clause in &self.with_clauses {
273            query.push_str(" WITH ");
274            query.push_str(with_clause);
275        }
276
277        // RETURN clause
278        if let Some(ref return_clause) = self.return_clause {
279            query.push_str(" RETURN ");
280            query.push_str(return_clause);
281        } else if !self.match_patterns.is_empty() {
282            return Err(Error::InvalidOperation(
283                "MATCH query requires a RETURN clause".to_string(),
284            ));
285        }
286
287        // ORDER BY
288        if let Some(ref order_by) = self.order_by {
289            query.push_str(" ORDER BY ");
290            query.push_str(order_by);
291        }
292
293        // SKIP
294        if let Some(skip) = self.skip {
295            query.push_str(&format!(" SKIP {}", skip));
296        }
297
298        // LIMIT
299        if let Some(limit) = self.limit {
300            query.push_str(&format!(" LIMIT {}", limit));
301        }
302
303        Ok(query.trim().to_string())
304    }
305
306    /// Execute the query and return results
307    ///
308    /// Builds and executes the query in one step.
309    ///
310    /// # Examples
311    ///
312    /// ```no_run
313    /// # use graphlite_sdk::GraphLite;
314    /// # let db = GraphLite::open("./mydb")?;
315    /// # let session = db.session("admin")?;
316    /// let result = session.query_builder()
317    ///     .match_pattern("(p:Person)")
318    ///     .where_clause("p.age > 25")
319    ///     .return_clause("p.name, p.age")
320    ///     .limit(10)
321    ///     .execute()?;
322    ///
323    /// for row in result.rows() {
324    ///     println!("{:?}", row);
325    /// }
326    /// # Ok::<(), graphlite_sdk::Error>(())
327    /// ```
328    pub fn execute(&self) -> Result<QueryResult> {
329        let query = self.build()?;
330        self.session.query(&query)
331    }
332}
333
334impl<'session> Session {
335    /// Get a query builder for this session
336    ///
337    /// Convenience method for creating a query builder.
338    ///
339    /// # Examples
340    ///
341    /// ```no_run
342    /// # use graphlite_sdk::GraphLite;
343    /// # let db = GraphLite::open("./mydb")?;
344    /// let session = db.session("admin")?;
345    /// let result = session.query_builder()
346    ///     .match_pattern("(p:Person)")
347    ///     .return_clause("p")
348    ///     .execute()?;
349    /// # Ok::<(), graphlite_sdk::Error>(())
350    /// ```
351    pub fn query_builder(&self) -> QueryBuilder<'_> {
352        QueryBuilder::new(self)
353    }
354}
355
356#[cfg(test)]
357mod tests {
358
359    // Note: These are unit tests that test query building logic
360    // Integration tests would require a real database
361
362    #[test]
363    fn test_query_builder_types_compile() {
364        // Compilation test
365    }
366}