salesforce_client/
query_builder.rs

1//! Type-safe SOQL query builder
2//!
3//! Provides a fluent API for constructing SOQL queries with compile-time guarantees.
4
5use std::marker::PhantomData;
6
7/// Type-safe SOQL query builder
8///
9/// # Example
10/// ```
11/// use salesforce_client::QueryBuilder;
12///
13/// let query = QueryBuilder::select(&["Id", "Name"])
14///     .from("Account")
15///     .where_clause("AnnualRevenue > 1000000")
16///     .order_by("Name")
17///     .limit(10)
18///     .build();
19///
20/// assert_eq!(query, "SELECT Id, Name FROM Account WHERE AnnualRevenue > 1000000 ORDER BY Name LIMIT 10");
21/// ```
22#[derive(Debug, Clone)]
23pub struct QueryBuilder<State = NeedsFrom> {
24    fields: Vec<String>,
25    from: Option<String>,
26    where_clauses: Vec<String>,
27    order_by: Option<String>,
28    limit: Option<u32>,
29    offset: Option<u32>,
30    _state: PhantomData<State>,
31}
32
33// Type states for compile-time query validation
34#[derive(Debug, Clone)]
35pub struct NeedsFrom;
36
37#[derive(Debug, Clone)]
38pub struct Complete;
39
40impl QueryBuilder<NeedsFrom> {
41    /// Start building a query with SELECT fields
42    pub fn select(fields: &[&str]) -> Self {
43        Self {
44            fields: fields.iter().map(|s| s.to_string()).collect(),
45            from: None,
46            where_clauses: Vec::new(),
47            order_by: None,
48            limit: None,
49            offset: None,
50            _state: PhantomData,
51        }
52    }
53
54    /// Specify the FROM clause (required)
55    pub fn from(mut self, sobject: impl Into<String>) -> QueryBuilder<Complete> {
56        self.from = Some(sobject.into());
57        QueryBuilder {
58            fields: self.fields,
59            from: self.from,
60            where_clauses: self.where_clauses,
61            order_by: self.order_by,
62            limit: self.limit,
63            offset: self.offset,
64            _state: PhantomData,
65        }
66    }
67}
68
69impl QueryBuilder<Complete> {
70    /// Add a WHERE clause
71    pub fn where_clause(mut self, condition: impl Into<String>) -> Self {
72        self.where_clauses.push(condition.into());
73        self
74    }
75
76    /// Add an AND condition to WHERE clause
77    pub fn and(mut self, condition: impl Into<String>) -> Self {
78        self.where_clauses.push(condition.into());
79        self
80    }
81
82    /// Add an ORDER BY clause
83    pub fn order_by(mut self, field: impl Into<String>) -> Self {
84        self.order_by = Some(field.into());
85        self
86    }
87
88    /// Add ORDER BY with direction
89    pub fn order_by_asc(mut self, field: impl Into<String>) -> Self {
90        self.order_by = Some(format!("{} ASC", field.into()));
91        self
92    }
93
94    /// Add ORDER BY descending
95    pub fn order_by_desc(mut self, field: impl Into<String>) -> Self {
96        self.order_by = Some(format!("{} DESC", field.into()));
97        self
98    }
99
100    /// Add a LIMIT clause
101    pub fn limit(mut self, limit: u32) -> Self {
102        self.limit = Some(limit);
103        self
104    }
105
106    /// Add an OFFSET clause
107    pub fn offset(mut self, offset: u32) -> Self {
108        self.offset = Some(offset);
109        self
110    }
111
112    /// Build the final SOQL query string
113    pub fn build(self) -> String {
114        let mut query = format!(
115            "SELECT {} FROM {}",
116            self.fields.join(", "),
117            self.from.unwrap() // Safe because Complete state guarantees from is set
118        );
119
120        if !self.where_clauses.is_empty() {
121            query.push_str(" WHERE ");
122            query.push_str(&self.where_clauses.join(" AND "));
123        }
124
125        if let Some(order) = self.order_by {
126            query.push_str(" ORDER BY ");
127            query.push_str(&order);
128        }
129
130        if let Some(limit) = self.limit {
131            query.push_str(&format!(" LIMIT {}", limit));
132        }
133
134        if let Some(offset) = self.offset {
135            query.push_str(&format!(" OFFSET {}", offset));
136        }
137
138        query
139    }
140}
141
142/// Fluent API for building COUNT queries
143pub struct CountQueryBuilder {
144    from: String,
145    where_clauses: Vec<String>,
146}
147
148impl CountQueryBuilder {
149    /// Start building a COUNT query
150    pub fn count_from(sobject: impl Into<String>) -> Self {
151        Self {
152            from: sobject.into(),
153            where_clauses: Vec::new(),
154        }
155    }
156
157    /// Add a WHERE clause
158    pub fn where_clause(mut self, condition: impl Into<String>) -> Self {
159        self.where_clauses.push(condition.into());
160        self
161    }
162
163    /// Build the query
164    pub fn build(self) -> String {
165        let mut query = format!("SELECT COUNT() FROM {}", self.from);
166
167        if !self.where_clauses.is_empty() {
168            query.push_str(" WHERE ");
169            query.push_str(&self.where_clauses.join(" AND "));
170        }
171
172        query
173    }
174}
175
176/// Helper for building subqueries
177pub struct SubqueryBuilder {
178    fields: Vec<String>,
179    relationship: String,
180    where_clauses: Vec<String>,
181    order_by: Option<String>,
182    limit: Option<u32>,
183}
184
185impl SubqueryBuilder {
186    /// Create a new subquery builder
187    pub fn new(relationship: impl Into<String>, fields: &[&str]) -> Self {
188        Self {
189            fields: fields.iter().map(|s| s.to_string()).collect(),
190            relationship: relationship.into(),
191            where_clauses: Vec::new(),
192            order_by: None,
193            limit: None,
194        }
195    }
196
197    /// Add a WHERE clause
198    pub fn where_clause(mut self, condition: impl Into<String>) -> Self {
199        self.where_clauses.push(condition.into());
200        self
201    }
202
203    /// Add an ORDER BY clause
204    pub fn order_by(mut self, field: impl Into<String>) -> Self {
205        self.order_by = Some(field.into());
206        self
207    }
208
209    /// Add a LIMIT clause
210    pub fn limit(mut self, limit: u32) -> Self {
211        self.limit = Some(limit);
212        self
213    }
214
215    /// Build the subquery string (for use in parent query)
216    pub fn build(self) -> String {
217        let mut query = format!(
218            "(SELECT {} FROM {}",
219            self.fields.join(", "),
220            self.relationship
221        );
222
223        if !self.where_clauses.is_empty() {
224            query.push_str(" WHERE ");
225            query.push_str(&self.where_clauses.join(" AND "));
226        }
227
228        if let Some(order) = self.order_by {
229            query.push_str(" ORDER BY ");
230            query.push_str(&order);
231        }
232
233        if let Some(limit) = self.limit {
234            query.push_str(&format!(" LIMIT {}", limit));
235        }
236
237        query.push(')');
238        query
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_basic_query() {
248        let query = QueryBuilder::select(&["Id", "Name"])
249            .from("Account")
250            .build();
251
252        assert_eq!(query, "SELECT Id, Name FROM Account");
253    }
254
255    #[test]
256    fn test_query_with_where() {
257        let query = QueryBuilder::select(&["Id", "Name"])
258            .from("Account")
259            .where_clause("AnnualRevenue > 1000000")
260            .build();
261
262        assert_eq!(
263            query,
264            "SELECT Id, Name FROM Account WHERE AnnualRevenue > 1000000"
265        );
266    }
267
268    #[test]
269    fn test_query_with_multiple_conditions() {
270        let query = QueryBuilder::select(&["Id", "Name"])
271            .from("Account")
272            .where_clause("AnnualRevenue > 1000000")
273            .and("Industry = 'Technology'")
274            .build();
275
276        assert_eq!(
277            query,
278            "SELECT Id, Name FROM Account WHERE AnnualRevenue > 1000000 AND Industry = 'Technology'"
279        );
280    }
281
282    #[test]
283    fn test_query_with_order_and_limit() {
284        let query = QueryBuilder::select(&["Id", "Name"])
285            .from("Account")
286            .order_by("Name")
287            .limit(10)
288            .build();
289
290        assert_eq!(query, "SELECT Id, Name FROM Account ORDER BY Name LIMIT 10");
291    }
292
293    #[test]
294    fn test_query_with_all_clauses() {
295        let query = QueryBuilder::select(&["Id", "Name", "AnnualRevenue"])
296            .from("Account")
297            .where_clause("AnnualRevenue > 1000000")
298            .and("Industry = 'Technology'")
299            .order_by_desc("AnnualRevenue")
300            .limit(10)
301            .offset(5)
302            .build();
303
304        assert_eq!(
305            query,
306            "SELECT Id, Name, AnnualRevenue FROM Account WHERE AnnualRevenue > 1000000 AND Industry = 'Technology' ORDER BY AnnualRevenue DESC LIMIT 10 OFFSET 5"
307        );
308    }
309
310    #[test]
311    fn test_count_query() {
312        let query = CountQueryBuilder::count_from("Account")
313            .where_clause("AnnualRevenue > 1000000")
314            .build();
315
316        assert_eq!(
317            query,
318            "SELECT COUNT() FROM Account WHERE AnnualRevenue > 1000000"
319        );
320    }
321
322    #[test]
323    fn test_subquery() {
324        let subquery = SubqueryBuilder::new("Contacts", &["Id", "Email"])
325            .where_clause("Email != null")
326            .limit(5)
327            .build();
328
329        assert_eq!(
330            subquery,
331            "(SELECT Id, Email FROM Contacts WHERE Email != null LIMIT 5)"
332        );
333    }
334}