Skip to main content

postgrest_parser/parser/
select.rs

1use crate::ast::{ItemType, JsonOp, SelectItem};
2use crate::error::ParseError;
3
4/// Parses a PostgREST select clause into a list of select items.
5///
6/// Supports column selection, renaming, JSON path navigation, type casting,
7/// and nested resource embedding (relations).
8///
9/// # Syntax
10///
11/// - Columns: `col1,col2,col3`
12/// - Wildcard: `*`
13/// - Rename: `alias:column` (note: alias comes first)
14/// - JSON path: `data->key` or `data->>key`
15/// - Type cast: `price::numeric`
16/// - Nested relations: `users(id,name,posts(title))`
17/// - Spread operator: `...foreign_table(col1,col2)`
18///
19/// # Examples
20///
21/// ```
22/// use postgrest_parser::parse_select;
23///
24/// // Simple columns
25/// let items = parse_select("id,name,email").unwrap();
26/// assert_eq!(items.len(), 3);
27///
28/// // With alias (alias:field_name syntax)
29/// let items = parse_select("full_name:name,user_email:email").unwrap();
30/// assert_eq!(items.len(), 2);
31/// assert_eq!(items[0].name, "name");
32/// assert_eq!(items[0].alias, Some("full_name".to_string()));
33///
34/// // Wildcard
35/// let items = parse_select("*").unwrap();
36/// assert_eq!(items.len(), 1);
37///
38/// // JSON path
39/// let items = parse_select("data->user->name,metadata->>key").unwrap();
40/// assert_eq!(items.len(), 2);
41///
42/// // Nested relation
43/// let items = parse_select("id,name,orders(id,total,items(product_id))").unwrap();
44/// assert_eq!(items.len(), 3);
45///
46/// // Type cast
47/// let items = parse_select("price::numeric,created_at::text").unwrap();
48/// assert_eq!(items.len(), 2);
49/// ```
50///
51/// # Errors
52///
53/// Returns `ParseError` if:
54/// - Parentheses are unclosed
55/// - Relation syntax is malformed
56/// - Field names are invalid
57pub fn parse_select(select_str: &str) -> Result<Vec<SelectItem>, ParseError> {
58    if select_str.is_empty() {
59        return Ok(Vec::new());
60    }
61
62    if select_str.trim() == "*" {
63        return Ok(vec![SelectItem::wildcard()]);
64    }
65
66    tokenize_and_parse(select_str)
67}
68
69fn tokenize_and_parse(select_str: &str) -> Result<Vec<SelectItem>, ParseError> {
70    let tokens = tokenize(select_str)?;
71    parse_items(&tokens)
72}
73
74fn tokenize(select_str: &str) -> Result<Vec<SelectToken>, ParseError> {
75    let mut tokens = Vec::new();
76    let mut current = String::new();
77    let mut depth = 0;
78
79    for c in select_str.chars() {
80        match c {
81            '(' => {
82                if !current.is_empty() {
83                    tokens.push(SelectToken::Text(current.clone()));
84                }
85                tokens.push(SelectToken::OpenParen);
86                current.clear();
87                depth += 1;
88            }
89            ')' => {
90                if !current.is_empty() {
91                    tokens.push(SelectToken::Text(current.clone()));
92                    current.clear();
93                }
94                tokens.push(SelectToken::CloseParen);
95                depth -= 1;
96            }
97            ',' => {
98                if !current.is_empty() {
99                    tokens.push(SelectToken::Text(current.clone()));
100                    current.clear();
101                }
102                tokens.push(SelectToken::Comma);
103            }
104            _ => {
105                current.push(c);
106            }
107        }
108    }
109
110    if !current.is_empty() {
111        tokens.push(SelectToken::Text(current));
112    }
113
114    if depth != 0 {
115        return Err(ParseError::UnclosedParenthesisInSelect);
116    }
117
118    Ok(tokens)
119}
120
121#[derive(Debug, Clone, PartialEq)]
122enum SelectToken {
123    Text(String),
124    OpenParen,
125    CloseParen,
126    Comma,
127}
128
129fn parse_items(tokens: &[SelectToken]) -> Result<Vec<SelectItem>, ParseError> {
130    let mut items = Vec::new();
131    let mut index = 0;
132
133    while index < tokens.len() {
134        match &tokens[index] {
135            SelectToken::Text(text) => {
136                let has_children =
137                    index + 1 < tokens.len() && matches!(tokens[index + 1], SelectToken::OpenParen);
138
139                let item = parse_item_text(text, has_children)?;
140
141                if matches!(item.item_type, ItemType::Relation | ItemType::Spread) {
142                    if !has_children {
143                        return Err(ParseError::ExpectedParenthesisAfterRelation);
144                    }
145
146                    let (children, next_index) = parse_nested_children(tokens, index + 2)?;
147                    let item_with_children = item.with_children(children);
148                    items.push(item_with_children);
149                    index = next_index;
150                } else {
151                    items.push(item);
152                    index += 1;
153                }
154            }
155            SelectToken::OpenParen => {
156                return Err(ParseError::UnexpectedToken("(".to_string()));
157            }
158            SelectToken::CloseParen => {
159                return Err(ParseError::UnexpectedClosingParenthesis);
160            }
161            SelectToken::Comma => {
162                index += 1;
163            }
164        }
165    }
166
167    Ok(items)
168}
169
170fn parse_nested_children(
171    tokens: &[SelectToken],
172    start: usize,
173) -> Result<(Vec<SelectItem>, usize), ParseError> {
174    let mut children = Vec::new();
175    let mut index = start;
176    let mut depth = 1;
177
178    while index < tokens.len() && depth > 0 {
179        match &tokens[index] {
180            SelectToken::Text(text) => {
181                let has_children =
182                    index + 1 < tokens.len() && matches!(tokens[index + 1], SelectToken::OpenParen);
183
184                let item = parse_item_text(text, has_children)?;
185
186                if matches!(item.item_type, ItemType::Relation | ItemType::Spread) {
187                    if !has_children {
188                        return Err(ParseError::ExpectedParenthesisAfterRelation);
189                    }
190
191                    let (nested_children, next_index) = parse_nested_children(tokens, index + 2)?;
192                    let item_with_children = item.with_children(nested_children);
193                    children.push(item_with_children);
194                    index = next_index;
195                } else {
196                    children.push(item);
197                    index += 1;
198                }
199            }
200            SelectToken::OpenParen => {
201                depth += 1;
202                index += 1;
203            }
204            SelectToken::CloseParen => {
205                depth -= 1;
206                if depth == 0 {
207                    index += 1;
208                    break;
209                }
210                index += 1;
211            }
212            SelectToken::Comma => {
213                index += 1;
214            }
215        }
216    }
217
218    Ok((children, index))
219}
220
221fn parse_item_text(text: &str, has_children: bool) -> Result<SelectItem, ParseError> {
222    let trimmed = text.trim();
223
224    if trimmed.is_empty() {
225        return Err(ParseError::EmptyFieldName);
226    }
227
228    let is_spread = trimmed.starts_with("...");
229    let (name_part, alias) = extract_alias(if is_spread { &trimmed[3..] } else { trimmed })?;
230
231    let (name, hint) = extract_hint(&name_part)?;
232
233    if name.is_empty() {
234        return Err(ParseError::EmptyFieldName);
235    }
236
237    let item_type = if is_spread {
238        ItemType::Spread
239    } else if has_children {
240        ItemType::Relation
241    } else {
242        ItemType::Field
243    };
244
245    let mut item = match item_type {
246        ItemType::Field => SelectItem::field(name.clone()),
247        ItemType::Relation => SelectItem::relation(name.clone()),
248        ItemType::Spread => SelectItem::spread(name.clone()),
249    };
250
251    if let Some(alias_name) = alias {
252        item = item.with_alias(alias_name);
253    }
254
255    if let Some(h) = hint {
256        item = item.with_hint(h);
257    }
258
259    Ok(item)
260}
261
262fn extract_alias(text: &str) -> Result<(String, Option<String>), ParseError> {
263    if text.contains(':') {
264        let parts: Vec<&str> = text.splitn(2, ':').collect();
265        if parts.len() == 2 {
266            Ok((
267                parts[1].trim().to_string(),
268                Some(parts[0].trim().to_string()),
269            ))
270        } else {
271            Ok((text.to_string(), None))
272        }
273    } else {
274        Ok((text.to_string(), None))
275    }
276}
277
278fn extract_hint(text: &str) -> Result<(String, Option<crate::ast::ItemHint>), ParseError> {
279    if let Some(pos) = text.find('!') {
280        let name = text[..pos].to_string();
281        let hint_str = text[pos + 1..].to_string();
282
283        let hint = parse_field_for_hint(&name, &hint_str)?;
284        Ok((name, Some(hint)))
285    } else {
286        Ok((text.to_string(), None))
287    }
288}
289
290fn parse_field_for_hint(name: &str, hint_str: &str) -> Result<crate::ast::ItemHint, ParseError> {
291    match crate::parser::common::field(name) {
292        Ok((_, field)) => {
293            let json_path_vec = || {
294                field
295                    .json_path
296                    .iter()
297                    .map(|op| match op {
298                        JsonOp::Arrow(s) | JsonOp::DoubleArrow(s) => s.clone(),
299                        JsonOp::ArrayIndex(i) => i.to_string(),
300                    })
301                    .collect()
302            };
303
304            match (field.json_path.is_empty(), field.cast) {
305                (true, None) => Ok(crate::ast::ItemHint::Inner(hint_str.to_string())),
306                (true, Some(cast)) => Ok(crate::ast::ItemHint::Cast(cast.to_string())),
307                (false, None) => Ok(crate::ast::ItemHint::JsonPath(json_path_vec())),
308                (false, Some(cast)) => Ok(crate::ast::ItemHint::JsonPathCast(
309                    json_path_vec(),
310                    cast.to_string(),
311                )),
312            }
313        }
314        Err(_) => Ok(crate::ast::ItemHint::Inner(hint_str.to_string())),
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_parse_select_simple() {
324        let result = parse_select("id,name,email");
325        assert!(result.is_ok());
326        let items = result.unwrap();
327        assert_eq!(items.len(), 3);
328        assert_eq!(items[0].name, "id");
329    }
330
331    #[test]
332    fn test_parse_select_wildcard() {
333        let result = parse_select("*");
334        assert!(result.is_ok());
335        let items = result.unwrap();
336        assert_eq!(items.len(), 1);
337        assert_eq!(items[0].name, "*");
338    }
339
340    #[test]
341    fn test_parse_select_with_alias() {
342        let result = parse_select("user_name:name,user_email:email");
343        assert!(result.is_ok());
344        let items = result.unwrap();
345        assert_eq!(items[0].alias, Some("user_name".to_string()));
346    }
347
348    #[test]
349    fn test_parse_select_with_relation() {
350        let result = parse_select("id,client(id,name)");
351        assert!(result.is_ok());
352        let items = result.unwrap();
353        assert_eq!(items.len(), 2);
354        assert_eq!(items[1].item_type, ItemType::Relation);
355        assert_eq!(items[1].children.as_ref().unwrap().len(), 2);
356    }
357
358    #[test]
359    fn test_parse_select_with_spread() {
360        let result = parse_select("id,...profile(name)");
361        assert!(result.is_ok());
362        let items = result.unwrap();
363        assert_eq!(items[1].item_type, ItemType::Spread);
364    }
365
366    #[test]
367    fn test_parse_select_with_hint() {
368        let result = parse_select("author!inner,client!left");
369        assert!(result.is_ok());
370        let items = result.unwrap();
371        assert!(items[0].hint.is_some());
372    }
373
374    #[test]
375    fn test_parse_select_nested_relations() {
376        let result = parse_select("id,client(id,orders(id,total))");
377        assert!(result.is_ok());
378        let items = result.unwrap();
379        assert_eq!(items.len(), 2);
380
381        let client_children = items[1].children.as_ref().unwrap();
382        assert_eq!(client_children.len(), 2);
383        assert_eq!(client_children[1].item_type, ItemType::Relation);
384    }
385
386    #[test]
387    fn test_parse_select_empty() {
388        let result = parse_select("");
389        assert!(result.is_ok());
390        assert!(result.unwrap().is_empty());
391    }
392
393    #[test]
394    fn test_parse_select_unclosed_parenthesis() {
395        let result = parse_select("client(id,name");
396        assert!(matches!(
397            result,
398            Err(ParseError::UnclosedParenthesisInSelect)
399        ));
400    }
401
402    // Resource embedding use cases (PostgREST select syntax)
403
404    #[test]
405    fn test_many_to_one_join_via_fk() {
406        // select("*, profiles(username, avatar_url)")
407        let items = parse_select("*, profiles(username, avatar_url)").unwrap();
408        assert_eq!(items.len(), 2);
409        assert_eq!(items[0].name, "*");
410        assert_eq!(items[0].item_type, ItemType::Field);
411        assert_eq!(items[1].name, "profiles");
412        assert_eq!(items[1].item_type, ItemType::Relation);
413        let children = items[1].children.as_ref().unwrap();
414        assert_eq!(children.len(), 2);
415        assert_eq!(children[0].name, "username");
416        assert_eq!(children[1].name, "avatar_url");
417    }
418
419    #[test]
420    fn test_one_to_many_join() {
421        // select("title, comments(id, body)")
422        let items = parse_select("title, comments(id, body)").unwrap();
423        assert_eq!(items.len(), 2);
424        assert_eq!(items[0].name, "title");
425        assert_eq!(items[1].name, "comments");
426        assert_eq!(items[1].item_type, ItemType::Relation);
427        let children = items[1].children.as_ref().unwrap();
428        assert_eq!(children.len(), 2);
429        assert_eq!(children[0].name, "id");
430        assert_eq!(children[1].name, "body");
431    }
432
433    #[test]
434    fn test_aliased_relation() {
435        // select("*, author:profiles(name)") — aliased relation
436        let items = parse_select("*, author:profiles(name)").unwrap();
437        assert_eq!(items.len(), 2);
438        assert_eq!(items[1].name, "profiles");
439        assert_eq!(items[1].alias, Some("author".to_string()));
440        assert_eq!(items[1].item_type, ItemType::Relation);
441        let children = items[1].children.as_ref().unwrap();
442        assert_eq!(children.len(), 1);
443        assert_eq!(children[0].name, "name");
444    }
445
446    #[test]
447    fn test_nested_embedding_with_alias() {
448        // select("*, comments(id, author:profiles(name))") — nested embedding with alias
449        let items = parse_select("*, comments(id, author:profiles(name))").unwrap();
450        assert_eq!(items.len(), 2);
451        assert_eq!(items[1].name, "comments");
452        assert_eq!(items[1].item_type, ItemType::Relation);
453
454        let children = items[1].children.as_ref().unwrap();
455        assert_eq!(children.len(), 2);
456        assert_eq!(children[0].name, "id");
457
458        // The nested aliased relation
459        assert_eq!(children[1].name, "profiles");
460        assert_eq!(children[1].alias, Some("author".to_string()));
461        assert_eq!(children[1].item_type, ItemType::Relation);
462        let nested = children[1].children.as_ref().unwrap();
463        assert_eq!(nested.len(), 1);
464        assert_eq!(nested[0].name, "name");
465    }
466
467    #[test]
468    fn test_fk_hint_on_relation() {
469        // select("*, author:profiles!author_id_fkey(name)") — FK hint for disambiguation
470        let items = parse_select("*, author:profiles!author_id_fkey(name)").unwrap();
471        assert_eq!(items.len(), 2);
472        assert_eq!(items[1].name, "profiles");
473        assert_eq!(items[1].alias, Some("author".to_string()));
474        assert_eq!(items[1].item_type, ItemType::Relation);
475        assert!(items[1].hint.is_some());
476        assert_eq!(
477            items[1].hint,
478            Some(crate::ast::ItemHint::Inner("author_id_fkey".to_string()))
479        );
480        let children = items[1].children.as_ref().unwrap();
481        assert_eq!(children.len(), 1);
482        assert_eq!(children[0].name, "name");
483    }
484
485    #[test]
486    fn test_fk_hint_without_alias() {
487        // select("*, profiles!author_id_fkey(name)") — FK hint without alias
488        let items = parse_select("*, profiles!author_id_fkey(name)").unwrap();
489        assert_eq!(items.len(), 2);
490        assert_eq!(items[1].name, "profiles");
491        assert_eq!(items[1].alias, None);
492        assert_eq!(items[1].item_type, ItemType::Relation);
493        assert!(items[1].hint.is_some());
494        assert_eq!(
495            items[1].hint,
496            Some(crate::ast::ItemHint::Inner("author_id_fkey".to_string()))
497        );
498    }
499
500    #[test]
501    fn test_multiple_relations() {
502        // select("id, author:profiles(name), comments(id, body)")
503        let items = parse_select("id, author:profiles(name), comments(id, body)").unwrap();
504        assert_eq!(items.len(), 3);
505        assert_eq!(items[0].name, "id");
506        assert_eq!(items[0].item_type, ItemType::Field);
507
508        assert_eq!(items[1].name, "profiles");
509        assert_eq!(items[1].alias, Some("author".to_string()));
510        assert_eq!(items[1].item_type, ItemType::Relation);
511
512        assert_eq!(items[2].name, "comments");
513        assert_eq!(items[2].item_type, ItemType::Relation);
514        assert_eq!(items[2].children.as_ref().unwrap().len(), 2);
515    }
516
517    #[test]
518    fn test_deeply_nested_relations() {
519        // select("*, posts(id, comments(id, author:profiles(name, avatar_url)))")
520        let items =
521            parse_select("*, posts(id, comments(id, author:profiles(name, avatar_url)))").unwrap();
522        assert_eq!(items.len(), 2);
523
524        let posts = &items[1];
525        assert_eq!(posts.name, "posts");
526        let post_children = posts.children.as_ref().unwrap();
527        assert_eq!(post_children.len(), 2);
528
529        let comments = &post_children[1];
530        assert_eq!(comments.name, "comments");
531        let comment_children = comments.children.as_ref().unwrap();
532        assert_eq!(comment_children.len(), 2);
533
534        let author = &comment_children[1];
535        assert_eq!(author.name, "profiles");
536        assert_eq!(author.alias, Some("author".to_string()));
537        let author_children = author.children.as_ref().unwrap();
538        assert_eq!(author_children.len(), 2);
539        assert_eq!(author_children[0].name, "name");
540        assert_eq!(author_children[1].name, "avatar_url");
541    }
542}