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}