Skip to main content

postrust_core/api_request/
query_params.rs

1//! Query parameter parsing using nom.
2//!
3//! Parses URL query strings into structured filter, select, order, and range data.
4//! Mirrors PostgREST's QueryParams.hs parsing logic.
5
6use super::types::*;
7use crate::error::{Error, Result};
8use nom::{
9    branch::alt,
10    bytes::complete::{tag, take_while1},
11    character::complete::{char, digit1},
12    combinator::{map, opt, value},
13    multi::{many0, separated_list0},
14    sequence::preceded,
15    IResult,
16};
17use percent_encoding::percent_decode_str;
18
19/// Parse a query string into QueryParams.
20pub fn parse_query_params(query: &str) -> Result<QueryParams> {
21    let mut params = QueryParams::default();
22
23    if query.is_empty() {
24        return Ok(params);
25    }
26
27    // Sort parameters for canonical form
28    let mut pairs: Vec<(&str, &str)> = query
29        .split('&')
30        .filter_map(|pair| {
31            let mut parts = pair.splitn(2, '=');
32            Some((parts.next()?, parts.next().unwrap_or("")))
33        })
34        .collect();
35    pairs.sort_by_key(|(k, _)| *k);
36    params.canonical = pairs
37        .iter()
38        .map(|(k, v)| format!("{}={}", k, v))
39        .collect::<Vec<_>>()
40        .join("&");
41
42    for (key, value) in pairs {
43        let decoded_value = percent_decode_str(value)
44            .decode_utf8()
45            .map_err(|_| Error::InvalidQueryParam(key.into()))?
46            .to_string();
47
48        match key {
49            "select" => {
50                params.select = parse_select(&decoded_value)?;
51            }
52            "order" => {
53                let (path, terms) = parse_order_param(&decoded_value)?;
54                params.order.push((path, terms));
55            }
56            "limit" => {
57                let limit: i64 = decoded_value
58                    .parse()
59                    .map_err(|_| Error::InvalidQueryParam("limit".into()))?;
60                params.ranges.entry(String::new()).or_default().limit = Some(limit);
61            }
62            "offset" => {
63                let offset: i64 = decoded_value
64                    .parse()
65                    .map_err(|_| Error::InvalidQueryParam("offset".into()))?;
66                params.ranges.entry(String::new()).or_default().offset = offset;
67            }
68            "columns" => {
69                params.columns = Some(
70                    decoded_value
71                        .split(',')
72                        .map(|s| s.trim().to_string())
73                        .collect(),
74                );
75            }
76            "on_conflict" => {
77                params.on_conflict = Some(
78                    decoded_value
79                        .split(',')
80                        .map(|s| s.trim().to_string())
81                        .collect(),
82                );
83            }
84            "and" | "or" => {
85                let logic = parse_logic_param(key, &decoded_value)?;
86                params.logic.push((vec![], logic));
87            }
88            key if !key.starts_with('_') => {
89                // Filter parameter
90                let (path, filter) = parse_filter_param(key, &decoded_value)?;
91                if path.is_empty() {
92                    params.filter_fields.insert(filter.field.name.clone());
93                    params.filters_root.push(filter);
94                } else {
95                    params.filters.push((path, filter));
96                }
97            }
98            _ => {
99                // RPC parameters (anything else)
100                params.params.push((key.to_string(), decoded_value));
101            }
102        }
103    }
104
105    Ok(params)
106}
107
108// ============================================================================
109// Select Parsing
110// ============================================================================
111
112/// Parse the `select` parameter value.
113pub fn parse_select(input: &str) -> Result<Vec<SelectItem>> {
114    if input.is_empty() {
115        return Ok(vec![]);
116    }
117
118    match parse_select_items(input) {
119        Ok((_, items)) => Ok(items),
120        Err(_) => Err(Error::InvalidQueryParam("select".into())),
121    }
122}
123
124fn parse_select_items(input: &str) -> IResult<&str, Vec<SelectItem>> {
125    separated_list0(char(','), parse_select_item)(input)
126}
127
128fn parse_select_item(input: &str) -> IResult<&str, SelectItem> {
129    alt((
130        parse_spread_relation,
131        parse_relation_select,
132        parse_field_select,
133    ))(input)
134}
135
136/// Parse spread relation: `...relation`
137fn parse_spread_relation(input: &str) -> IResult<&str, SelectItem> {
138    let (input, _) = tag("...")(input)?;
139    let (input, relation) = parse_identifier(input)?;
140    let (input, hint) = opt(preceded(char('!'), parse_identifier))(input)?;
141    let (input, join_type) = opt(preceded(char('!'), parse_join_type))(input)?;
142
143    Ok((
144        input,
145        SelectItem::SpreadRelation {
146            relation: relation.to_string(),
147            hint: hint.map(|s| s.to_string()),
148            join_type,
149        },
150    ))
151}
152
153/// Parse the contents of a relation's parentheses.
154///
155/// Scans to the parenthesis that closes the group -- tracking nesting, so
156/// `books(chapters(id))` is not truncated at the first `)` -- then parses the
157/// contents as a select list.
158fn parse_nested_select(input: &str) -> IResult<&str, Vec<SelectItem>> {
159    let mut depth = 0usize;
160    let mut end = input.len();
161
162    for (idx, ch) in input.char_indices() {
163        match ch {
164            '(' => depth += 1,
165            ')' => {
166                if depth == 0 {
167                    end = idx;
168                    break;
169                }
170                depth -= 1;
171            }
172            _ => {}
173        }
174    }
175
176    let (body, rest) = input.split_at(end);
177
178    if body.is_empty() {
179        return Ok((rest, Vec::new()));
180    }
181
182    match parse_select_items(body) {
183        Ok(("", items)) => Ok((rest, items)),
184        _ => Err(nom::Err::Error(nom::error::Error::new(
185            input,
186            nom::error::ErrorKind::Verify,
187        ))),
188    }
189}
190
191/// Parse relation with embedded select: `relation(select_items)`
192fn parse_relation_select(input: &str) -> IResult<&str, SelectItem> {
193    let (input, name) = parse_identifier(input)?;
194    let (input, alias) = opt(preceded(char(':'), parse_identifier))(input)?;
195    let (input, hint) = opt(preceded(char('!'), parse_identifier))(input)?;
196    let (input, join_type) = opt(preceded(char('!'), parse_join_type))(input)?;
197    // The nested selection is parsed rather than skipped: it says which
198    // columns of the related resource to return, and may embed further
199    // relations of its own.
200    let (input, _) = char('(')(input)?;
201    let (input, nested) = parse_nested_select(input)?;
202    let (input, _) = char(')')(input)?;
203
204    Ok((
205        input,
206        SelectItem::Relation {
207            relation: name.to_string(),
208            alias: alias.map(|s| s.to_string()),
209            hint: hint.map(|s| s.to_string()),
210            join_type,
211            select: nested,
212        },
213    ))
214}
215
216/// Parse field select: `field`, `field::cast`, `field:alias`, `agg(field)`
217fn parse_field_select(input: &str) -> IResult<&str, SelectItem> {
218    // Check for aggregate function
219    let (input, aggregate) = opt(parse_aggregate_prefix)(input)?;
220
221    let (input, name) = parse_identifier(input)?;
222    let (input, json_path) = parse_json_path(input)?;
223
224    // Close aggregate if present
225    let (input, aggregate_cast) = if aggregate.is_some() {
226        let (input, _) = char(')')(input)?;
227        let (input, cast) = opt(preceded(tag("::"), parse_identifier))(input)?;
228        (input, cast.map(|s| s.to_string()))
229    } else {
230        (input, None)
231    };
232
233    let (input, cast) = if aggregate.is_none() {
234        opt(preceded(tag("::"), parse_identifier))(input)?
235    } else {
236        (input, None)
237    };
238
239    let (input, alias) = opt(preceded(char(':'), parse_identifier))(input)?;
240
241    Ok((
242        input,
243        SelectItem::Field {
244            field: Field {
245                name: name.to_string(),
246                json_path,
247            },
248            aggregate,
249            aggregate_cast,
250            cast: cast.map(|s| s.to_string()),
251            alias: alias.map(|s| s.to_string()),
252        },
253    ))
254}
255
256fn parse_aggregate_prefix(input: &str) -> IResult<&str, AggregateFunction> {
257    alt((
258        value(AggregateFunction::Sum, tag("sum(")),
259        value(AggregateFunction::Avg, tag("avg(")),
260        value(AggregateFunction::Max, tag("max(")),
261        value(AggregateFunction::Min, tag("min(")),
262        value(AggregateFunction::Count, tag("count(")),
263    ))(input)
264}
265
266fn parse_join_type(input: &str) -> IResult<&str, JoinType> {
267    alt((
268        value(JoinType::Inner, tag("inner")),
269        value(JoinType::Left, tag("left")),
270    ))(input)
271}
272
273// ============================================================================
274// Filter Parsing
275// ============================================================================
276
277/// Parse a filter parameter (key=value where key is a field name).
278fn parse_filter_param(key: &str, value: &str) -> Result<(EmbedPath, Filter)> {
279    // Parse the key for embedded path: rel.field or field
280    let (path, field_name) = parse_filter_key(key)?;
281
282    // Parse the value for operator and operand
283    let op_expr = parse_filter_value(value)?;
284
285    let filter = Filter::new(Field::simple(field_name), op_expr);
286    Ok((path, filter))
287}
288
289/// Parse a filter key into path and field name.
290fn parse_filter_key(key: &str) -> Result<(EmbedPath, String)> {
291    let parts: Vec<&str> = key.split('.').collect();
292    if parts.is_empty() {
293        return Err(Error::InvalidQueryParam(key.into()));
294    }
295
296    if parts.len() == 1 {
297        return Ok((vec![], parts[0].to_string()));
298    }
299
300    let path: Vec<String> = parts[..parts.len() - 1]
301        .iter()
302        .map(|s| s.to_string())
303        .collect();
304    let field = parts.last().unwrap().to_string();
305    Ok((path, field))
306}
307
308/// Parse filter value: `operator.value` or `not.operator.value`
309fn parse_filter_value(value: &str) -> Result<OpExpr> {
310    let (value, negated) = if let Some(rest) = value.strip_prefix("not.") {
311        (rest, true)
312    } else {
313        (value, false)
314    };
315
316    let operation = parse_operation(value)?;
317    Ok(OpExpr { negated, operation })
318}
319
320/// Parse an operation: `eq.value`, `in.(a,b,c)`, `is.null`, etc.
321fn parse_operation(value: &str) -> Result<Operation> {
322    // Try each operator pattern
323    if let Some(rest) = value.strip_prefix("eq.") {
324        return Ok(Operation::Quant {
325            op: QuantOperator::Equal,
326            quantifier: None,
327            value: rest.to_string(),
328        });
329    }
330    if let Some(rest) = value.strip_prefix("neq.") {
331        return Ok(Operation::Simple {
332            op: SimpleOperator::NotEqual,
333            value: rest.to_string(),
334        });
335    }
336    if let Some(rest) = value.strip_prefix("gt.") {
337        return Ok(Operation::Quant {
338            op: QuantOperator::GreaterThan,
339            quantifier: None,
340            value: rest.to_string(),
341        });
342    }
343    if let Some(rest) = value.strip_prefix("gte.") {
344        return Ok(Operation::Quant {
345            op: QuantOperator::GreaterThanEqual,
346            quantifier: None,
347            value: rest.to_string(),
348        });
349    }
350    if let Some(rest) = value.strip_prefix("lt.") {
351        return Ok(Operation::Quant {
352            op: QuantOperator::LessThan,
353            quantifier: None,
354            value: rest.to_string(),
355        });
356    }
357    if let Some(rest) = value.strip_prefix("lte.") {
358        return Ok(Operation::Quant {
359            op: QuantOperator::LessThanEqual,
360            quantifier: None,
361            value: rest.to_string(),
362        });
363    }
364    if let Some(rest) = value.strip_prefix("like.") {
365        return Ok(Operation::Quant {
366            op: QuantOperator::Like,
367            quantifier: None,
368            value: rest.to_string(),
369        });
370    }
371    if let Some(rest) = value.strip_prefix("ilike.") {
372        return Ok(Operation::Quant {
373            op: QuantOperator::ILike,
374            quantifier: None,
375            value: rest.to_string(),
376        });
377    }
378    if let Some(rest) = value.strip_prefix("match.") {
379        return Ok(Operation::Quant {
380            op: QuantOperator::Match,
381            quantifier: None,
382            value: rest.to_string(),
383        });
384    }
385    if let Some(rest) = value.strip_prefix("imatch.") {
386        return Ok(Operation::Quant {
387            op: QuantOperator::IMatch,
388            quantifier: None,
389            value: rest.to_string(),
390        });
391    }
392
393    // Array/Range operators
394    if let Some(rest) = value.strip_prefix("cs.") {
395        return Ok(Operation::Simple {
396            op: SimpleOperator::Contains,
397            value: rest.to_string(),
398        });
399    }
400    if let Some(rest) = value.strip_prefix("cd.") {
401        return Ok(Operation::Simple {
402            op: SimpleOperator::Contained,
403            value: rest.to_string(),
404        });
405    }
406    if let Some(rest) = value.strip_prefix("ov.") {
407        return Ok(Operation::Simple {
408            op: SimpleOperator::Overlap,
409            value: rest.to_string(),
410        });
411    }
412    if let Some(rest) = value.strip_prefix("sl.") {
413        return Ok(Operation::Simple {
414            op: SimpleOperator::StrictlyLeft,
415            value: rest.to_string(),
416        });
417    }
418    if let Some(rest) = value.strip_prefix("sr.") {
419        return Ok(Operation::Simple {
420            op: SimpleOperator::StrictlyRight,
421            value: rest.to_string(),
422        });
423    }
424    if let Some(rest) = value.strip_prefix("nxr.") {
425        return Ok(Operation::Simple {
426            op: SimpleOperator::NotExtendsRight,
427            value: rest.to_string(),
428        });
429    }
430    if let Some(rest) = value.strip_prefix("nxl.") {
431        return Ok(Operation::Simple {
432            op: SimpleOperator::NotExtendsLeft,
433            value: rest.to_string(),
434        });
435    }
436    if let Some(rest) = value.strip_prefix("adj.") {
437        return Ok(Operation::Simple {
438            op: SimpleOperator::Adjacent,
439            value: rest.to_string(),
440        });
441    }
442
443    // IN operator
444    if let Some(rest) = value.strip_prefix("in.") {
445        let values = parse_in_list(rest)?;
446        return Ok(Operation::In(values));
447    }
448
449    // IS operator
450    if let Some(rest) = value.strip_prefix("is.") {
451        let is_val = match rest {
452            "null" => IsValue::Null,
453            "true" => IsValue::True,
454            "false" => IsValue::False,
455            "unknown" => IsValue::Unknown,
456            _ => return Err(Error::InvalidQueryParam(format!("is.{}", rest))),
457        };
458        return Ok(Operation::Is(is_val));
459    }
460
461    // IS DISTINCT FROM
462    if let Some(rest) = value.strip_prefix("isdistinct.") {
463        return Ok(Operation::IsDistinctFrom(rest.to_string()));
464    }
465
466    // Full-text search
467    if let Some(rest) = value.strip_prefix("fts") {
468        return parse_fts(FtsOperator::Fts, rest);
469    }
470    if let Some(rest) = value.strip_prefix("plfts") {
471        return parse_fts(FtsOperator::Plain, rest);
472    }
473    if let Some(rest) = value.strip_prefix("phfts") {
474        return parse_fts(FtsOperator::Phrase, rest);
475    }
476    if let Some(rest) = value.strip_prefix("wfts") {
477        return parse_fts(FtsOperator::Websearch, rest);
478    }
479
480    Err(Error::InvalidQueryParam(value.into()))
481}
482
483/// Parse IN list: `(a,b,c)` -> vec!["a", "b", "c"]
484fn parse_in_list(value: &str) -> Result<Vec<String>> {
485    let value = value
486        .strip_prefix('(')
487        .and_then(|s| s.strip_suffix(')'))
488        .ok_or_else(|| Error::InvalidQueryParam(format!("in.{}", value)))?;
489
490    Ok(value.split(',').map(|s| s.trim().to_string()).collect())
491}
492
493/// Parse FTS operation: `(language).query` or `.query`
494fn parse_fts(op: FtsOperator, rest: &str) -> Result<Operation> {
495    if let Some(rest) = rest.strip_prefix('(') {
496        // Has language specifier
497        let (lang, query) = rest
498            .split_once(").")
499            .ok_or_else(|| Error::InvalidQueryParam(format!("fts{}", rest)))?;
500        return Ok(Operation::Fts {
501            op,
502            language: Some(lang.to_string()),
503            value: query.to_string(),
504        });
505    }
506
507    let query = rest
508        .strip_prefix('.')
509        .ok_or_else(|| Error::InvalidQueryParam(format!("fts{}", rest)))?;
510    Ok(Operation::Fts {
511        op,
512        language: None,
513        value: query.to_string(),
514    })
515}
516
517// ============================================================================
518// Order Parsing
519// ============================================================================
520
521/// Parse order parameter: `col.desc.nullsfirst,col2.asc`
522fn parse_order_param(value: &str) -> Result<(EmbedPath, Vec<OrderTerm>)> {
523    let terms: Vec<OrderTerm> = value
524        .split(',')
525        .map(|s| parse_order_term(s.trim()))
526        .collect::<Result<Vec<_>>>()?;
527    Ok((vec![], terms))
528}
529
530fn parse_order_term(value: &str) -> Result<OrderTerm> {
531    let parts: Vec<&str> = value.split('.').collect();
532    if parts.is_empty() {
533        return Err(Error::InvalidQueryParam("order".into()));
534    }
535
536    let field_name = parts[0];
537    let mut direction = None;
538    let mut nulls = None;
539
540    for part in &parts[1..] {
541        match *part {
542            "asc" => direction = Some(OrderDirection::Asc),
543            "desc" => direction = Some(OrderDirection::Desc),
544            "nullsfirst" => nulls = Some(OrderNulls::First),
545            "nullslast" => nulls = Some(OrderNulls::Last),
546            _ => {}
547        }
548    }
549
550    Ok(OrderTerm::Field {
551        field: Field::simple(field_name),
552        direction,
553        nulls,
554    })
555}
556
557// ============================================================================
558// Logic Tree Parsing
559// ============================================================================
560
561/// Parse `and` or `or` parameter: `(filter1,filter2)`
562fn parse_logic_param(op: &str, value: &str) -> Result<LogicTree> {
563    let logic_op = match op {
564        "and" => LogicOperator::And,
565        "or" => LogicOperator::Or,
566        _ => return Err(Error::InvalidQueryParam(op.into())),
567    };
568
569    // Parse nested filters: (field.op.value,field2.op.value)
570    let value = value
571        .strip_prefix('(')
572        .and_then(|s| s.strip_suffix(')'))
573        .ok_or_else(|| Error::InvalidQueryParam(format!("{}={}", op, value)))?;
574
575    let children: Vec<LogicTree> = value
576        .split(',')
577        .map(|s| {
578            let (key, val) = s
579                .split_once('.')
580                .ok_or_else(|| Error::InvalidQueryParam(s.into()))?;
581            let (_, filter) = parse_filter_param(key, val)?;
582            Ok(LogicTree::Stmt(filter))
583        })
584        .collect::<Result<Vec<_>>>()?;
585
586    Ok(LogicTree::Expr {
587        negated: false,
588        op: logic_op,
589        children,
590    })
591}
592
593// ============================================================================
594// Helper Parsers
595// ============================================================================
596
597fn parse_identifier(input: &str) -> IResult<&str, &str> {
598    take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
599}
600
601fn parse_json_path(input: &str) -> IResult<&str, JsonPath> {
602    many0(alt((parse_arrow, parse_double_arrow)))(input)
603}
604
605fn parse_arrow(input: &str) -> IResult<&str, JsonOperation> {
606    let (input, _) = tag("->")(input)?;
607    let (input, operand) = alt((
608        map(digit1, |s: &str| JsonOperand::Idx(s.parse().unwrap_or(0))),
609        map(parse_identifier, |s| JsonOperand::Key(s.to_string())),
610    ))(input)?;
611    Ok((input, JsonOperation::Arrow(operand)))
612}
613
614fn parse_double_arrow(input: &str) -> IResult<&str, JsonOperation> {
615    let (input, _) = tag("->>")(input)?;
616    let (input, operand) = alt((
617        map(digit1, |s: &str| JsonOperand::Idx(s.parse().unwrap_or(0))),
618        map(parse_identifier, |s| JsonOperand::Key(s.to_string())),
619    ))(input)?;
620    Ok((input, JsonOperation::DoubleArrow(operand)))
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626
627    #[test]
628    fn test_parse_simple_filter() {
629        let params = parse_query_params("name=eq.John").unwrap();
630        assert_eq!(params.filters_root.len(), 1);
631        assert_eq!(params.filters_root[0].field.name, "name");
632    }
633
634    #[test]
635    fn test_parse_negated_filter() {
636        let params = parse_query_params("status=not.eq.active").unwrap();
637        assert!(params.filters_root[0].op_expr.negated);
638    }
639
640    #[test]
641    fn test_parse_in_filter() {
642        let params = parse_query_params("id=in.(1,2,3)").unwrap();
643        match &params.filters_root[0].op_expr.operation {
644            Operation::In(values) => {
645                assert_eq!(values, &vec!["1", "2", "3"]);
646            }
647            _ => panic!("Expected In operation"),
648        }
649    }
650
651    #[test]
652    fn test_parse_is_null() {
653        let params = parse_query_params("deleted_at=is.null").unwrap();
654        match &params.filters_root[0].op_expr.operation {
655            Operation::Is(IsValue::Null) => {}
656            _ => panic!("Expected Is Null"),
657        }
658    }
659
660    #[test]
661    fn test_parse_order() {
662        let params = parse_query_params("order=name.asc,age.desc.nullslast").unwrap();
663        assert_eq!(params.order.len(), 1);
664        let (_, terms) = &params.order[0];
665        assert_eq!(terms.len(), 2);
666    }
667
668    #[test]
669    fn test_parse_limit_offset() {
670        let params = parse_query_params("limit=10&offset=20").unwrap();
671        let range = params.ranges.get("").unwrap();
672        assert_eq!(range.limit, Some(10));
673        assert_eq!(range.offset, 20);
674    }
675
676    #[test]
677    fn test_parse_select() {
678        let items = parse_select("id,name,orders(id,amount)").unwrap();
679        assert_eq!(items.len(), 3);
680    }
681
682    #[test]
683    fn test_parse_fts() {
684        let params = parse_query_params("content=fts(english).search+term").unwrap();
685        match &params.filters_root[0].op_expr.operation {
686            Operation::Fts {
687                op,
688                language,
689                value,
690            } => {
691                assert_eq!(*op, FtsOperator::Fts);
692                assert_eq!(language.as_deref(), Some("english"));
693                assert_eq!(value, "search+term");
694            }
695            _ => panic!("Expected FTS operation"),
696        }
697    }
698}