Skip to main content

dbrest_core/api_request/
query_params.rs

1//! Query parameter parsing
2//!
3//! URL query parameter parsing for dbrest.
4//! Parses URL query strings into structured types for select, filter, order,
5//! logic trees, columns, and ranges.
6
7use compact_str::CompactString;
8use smallvec::SmallVec;
9use std::collections::{HashMap, HashSet};
10
11use crate::error::Error;
12
13use super::range::Range;
14use super::types::*;
15
16// ==========================================================================
17// QueryParams struct
18// ==========================================================================
19
20/// Parsed query parameters.
21///
22/// Parsed URL query parameters.
23#[derive(Debug, Clone)]
24pub struct QueryParams {
25    /// Canonical query string (sorted alphabetically)
26    pub canonical: String,
27    /// RPC parameters (key-value pairs without operators)
28    pub params: Vec<(CompactString, CompactString)>,
29    /// Ranges from &limit and &offset params
30    pub ranges: HashMap<CompactString, Range>,
31    /// Order parameters for each embed level
32    pub order: Vec<(EmbedPath, Vec<OrderTerm>)>,
33    /// Logic trees for &and and &or parameters
34    pub logic: Vec<(EmbedPath, LogicTree)>,
35    /// &columns parameter
36    pub columns: Option<HashSet<FieldName>>,
37    /// &select parameter parsed into a tree
38    pub select: Vec<SelectItem>,
39    /// All filters (embed_path, filter)
40    pub filters: Vec<(EmbedPath, Filter)>,
41    /// Filters on the root table only (no embed path)
42    pub filters_root: Vec<Filter>,
43    /// Filters on embedded tables (non-root)
44    pub filters_not_root: Vec<(EmbedPath, Filter)>,
45    /// Set of field names that have filters
46    pub filter_fields: HashSet<FieldName>,
47    /// &on_conflict parameter
48    pub on_conflict: Option<Vec<FieldName>>,
49}
50
51impl Default for QueryParams {
52    fn default() -> Self {
53        Self {
54            canonical: String::new(),
55            params: Vec::new(),
56            ranges: HashMap::new(),
57            order: Vec::new(),
58            logic: Vec::new(),
59            columns: None,
60            select: vec![SelectItem::Field {
61                field: ("*".into(), SmallVec::new()),
62                alias: None,
63                cast: None,
64                aggregate: None,
65                aggregate_cast: None,
66            }],
67            filters: Vec::new(),
68            filters_root: Vec::new(),
69            filters_not_root: Vec::new(),
70            filter_fields: HashSet::new(),
71            on_conflict: None,
72        }
73    }
74}
75
76/// Parse query parameters from a URL query string.
77///
78/// Parse query parameters from a URL query string.
79pub fn parse(is_rpc_read: bool, query_string: &str) -> Result<QueryParams, Error> {
80    let pairs: Vec<(String, String)> = form_urlencoded::parse(query_string.as_bytes())
81        .into_owned()
82        .collect();
83
84    // Build canonical representation (sorted)
85    let mut sorted_pairs = pairs.clone();
86    sorted_pairs.sort_by(|a, b| a.0.cmp(&b.0));
87    let canonical = sorted_pairs
88        .iter()
89        .map(|(k, v)| {
90            if v.is_empty() {
91                form_urlencoded::byte_serialize(k.as_bytes()).collect::<String>()
92            } else {
93                format!(
94                    "{}={}",
95                    form_urlencoded::byte_serialize(k.as_bytes()).collect::<String>(),
96                    form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>()
97                )
98            }
99        })
100        .collect::<Vec<_>>()
101        .join("&");
102
103    // Categorize parameters
104    let reserved = ["select", "columns", "on_conflict"];
105    let reserved_embeddable = ["order", "limit", "offset", "and", "or"];
106
107    let select_str = pairs
108        .iter()
109        .find(|(k, _)| k == "select")
110        .map(|(_, v)| v.as_str())
111        .unwrap_or("*");
112
113    let columns_str = pairs
114        .iter()
115        .find(|(k, _)| k == "columns")
116        .map(|(_, v)| v.as_str());
117
118    let on_conflict_str = pairs
119        .iter()
120        .find(|(k, _)| k == "on_conflict")
121        .map(|(_, v)| v.as_str());
122
123    // Parse select
124    let select = parse_select(select_str)?;
125
126    // Parse columns
127    let columns = match columns_str {
128        Some(s) => Some(parse_columns(s)?),
129        None => None,
130    };
131
132    // Parse on_conflict
133    let on_conflict = match on_conflict_str {
134        Some(s) => Some(parse_columns_list(s)?),
135        None => None,
136    };
137
138    // Separate order, limit, offset, logic, and filter params
139    let mut order_params = Vec::new();
140    let mut logic_params = Vec::new();
141    let mut limit_params = Vec::new();
142    let mut offset_params = Vec::new();
143    let mut filter_params = Vec::new();
144
145    for (k, v) in &pairs {
146        if v.is_empty() && reserved.contains(&k.as_str()) {
147            continue;
148        }
149        if reserved.contains(&k.as_str()) {
150            continue;
151        }
152
153        let last_word = k.rsplit('.').next().unwrap_or(k);
154        if last_word == "order" {
155            order_params.push((k.as_str(), v.as_str()));
156        } else if last_word == "limit" {
157            limit_params.push((k.as_str(), v.as_str()));
158        } else if last_word == "offset" {
159            offset_params.push((k.as_str(), v.as_str()));
160        } else if last_word == "and" || last_word == "or" {
161            logic_params.push((k.as_str(), v.as_str()));
162        } else if !reserved_embeddable.contains(&last_word) {
163            filter_params.push((k.as_str(), v.as_str()));
164        }
165    }
166
167    // Parse orders
168    let mut order = Vec::new();
169    for (k, v) in &order_params {
170        let (path, _) = parse_tree_path(k)?;
171        let terms = parse_order(v)?;
172        order.push((path, terms));
173    }
174
175    // Parse logic trees
176    let mut logic = Vec::new();
177    for (k, v) in &logic_params {
178        let (mut path, op) = parse_logic_path(k)?;
179        // Remove "not" from path, prepend to operator
180        let negated = path.contains(&CompactString::from("not"));
181        path.retain(|s| s.as_str() != "not");
182
183        let op_str = if negated {
184            format!("not.{}{}", op, v)
185        } else {
186            format!("{}{}", op, v)
187        };
188        let tree = parse_logic_tree(&op_str)?;
189        logic.push((path, tree));
190    }
191
192    // Parse ranges from limit/offset
193    let mut ranges: HashMap<CompactString, Range> = HashMap::new();
194
195    for (k, v) in &limit_params {
196        let embed_key = replace_last_segment(k, "limit");
197        if let Ok(limit) = v.parse::<i64>() {
198            let range = Range::all().restrict(Some(limit));
199            ranges.insert(CompactString::from(embed_key), range);
200        }
201    }
202
203    for (k, v) in &offset_params {
204        // Map offset key to limit key for merging
205        let embed_key = replace_last_segment(k, "limit");
206        if let Ok(offset) = v.parse::<i64>() {
207            let entry = ranges
208                .entry(CompactString::from(embed_key))
209                .or_insert_with(Range::all);
210            // Combine offset with existing limit
211            if let Some(limit) = entry.limit() {
212                *entry = Range::new(offset, offset + limit - 1);
213            } else {
214                *entry = Range::from_offset(offset);
215            }
216        }
217    }
218
219    // Parse filters
220    let mut all_filters = Vec::new();
221    let mut rpc_params = Vec::new();
222    let mut filter_fields = HashSet::new();
223
224    for (k, v) in &filter_params {
225        filter_fields.insert(CompactString::from(*k));
226
227        // Reject casting in filter field names (PostgREST restriction)
228        if k.contains("::") {
229            return Err(Error::InvalidQueryParam {
230                param: "filter".to_string(),
231                message: "casting not allowed in filters".to_string(),
232            });
233        }
234
235        let (path, field) = parse_tree_path(k)?;
236        let op_expr = parse_filter_value(v, is_rpc_read)?;
237
238        match &op_expr {
239            OpExpr::NoOp(val) => {
240                rpc_params.push((field.0.clone(), val.clone()));
241            }
242            _ => {
243                all_filters.push((path, Filter { field, op_expr }));
244            }
245        }
246    }
247
248    // Split into root and non-root filters
249    let mut filters_root = Vec::new();
250    let mut filters_not_root = Vec::new();
251    for (path, filter) in &all_filters {
252        if path.is_empty() {
253            filters_root.push(filter.clone());
254        } else {
255            filters_not_root.push((path.clone(), filter.clone()));
256        }
257    }
258
259    Ok(QueryParams {
260        canonical,
261        params: rpc_params,
262        ranges,
263        order,
264        logic,
265        columns,
266        select,
267        filters: all_filters,
268        filters_root,
269        filters_not_root,
270        filter_fields,
271        on_conflict,
272    })
273}
274
275// ==========================================================================
276// Select parser
277// ==========================================================================
278
279/// Parse the `select=` parameter into a list of SelectItems.
280///
281/// Handles: fields, aliases, casts, JSON paths, aggregates,
282/// relation embeddings, spread syntax, hints, join types.
283pub fn parse_select(input: &str) -> Result<Vec<SelectItem>, Error> {
284    if input.is_empty() {
285        return Ok(Vec::new());
286    }
287
288    let mut items = Vec::new();
289    let mut chars = input;
290
291    while !chars.is_empty() {
292        let (item, rest) = parse_select_item(chars)?;
293        items.push(item);
294
295        chars = rest.trim_start();
296        if chars.starts_with(',') {
297            chars = &chars[1..];
298            chars = chars.trim_start();
299        } else if !chars.is_empty() && !chars.starts_with(')') {
300            return Err(Error::ParseError {
301                location: "select".to_string(),
302                message: format!("unexpected character '{}' in select", &chars[..1]),
303            });
304        }
305    }
306
307    Ok(items)
308}
309
310fn parse_select_item(input: &str) -> Result<(SelectItem, &str), Error> {
311    let input = input.trim_start();
312
313    // Check for spread: ...relation(...)
314    if let Some(rest) = input.strip_prefix("...") {
315        return parse_spread_relation(rest);
316    }
317
318    // Try to parse as relation or field
319    // First, try to detect alias: look for "name:" where : is not followed by :
320    let (alias, after_alias) = try_parse_alias(input);
321
322    // Check if this is a relation (has parentheses for children)
323    // A relation is: name!hint!jointype(...) or name(...)
324    let after = after_alias;
325
326    // Try to parse relation first
327    if let Some((item, rest)) = try_parse_relation(after, alias.clone())? {
328        return Ok((item, rest));
329    }
330
331    // Otherwise parse as field
332    parse_field_select(after, alias)
333}
334
335fn parse_spread_relation(input: &str) -> Result<(SelectItem, &str), Error> {
336    // Parse: name!hint!jointype(children)
337    let (name, rest) = parse_field_name(input)?;
338
339    // Check that name is not "count"
340    let (hint, join_type, rest) = parse_embed_params(rest);
341
342    // Must have opening paren
343    if !rest.starts_with('(') {
344        return Err(Error::ParseError {
345            location: "select".to_string(),
346            message: format!("expected '(' after spread relation '{}'", name),
347        });
348    }
349
350    let (children, rest) = parse_children(&rest[1..])?;
351
352    Ok((
353        SelectItem::Spread {
354            relation: name,
355            hint,
356            join_type,
357            children,
358        },
359        rest,
360    ))
361}
362
363fn try_parse_relation(
364    input: &str,
365    alias: Option<Alias>,
366) -> Result<Option<(SelectItem, &str)>, Error> {
367    // Parse name
368    let (name, rest) = match parse_field_name(input) {
369        Ok(r) => r,
370        Err(_) => return Ok(None),
371    };
372
373    // "count" as a relation name is not allowed (it's the count() aggregate)
374    if name.as_str() == "count" {
375        return Ok(None);
376    }
377
378    let (hint, join_type, rest) = parse_embed_params(rest);
379
380    // Must have opening paren for a relation
381    if !rest.starts_with('(') {
382        return Ok(None);
383    }
384
385    let (children, rest) = parse_children(&rest[1..])?;
386
387    Ok(Some((
388        SelectItem::Relation {
389            relation: name,
390            alias,
391            hint,
392            join_type,
393            children,
394        },
395        rest,
396    )))
397}
398
399fn parse_field_select(input: &str, alias: Option<Alias>) -> Result<(SelectItem, &str), Error> {
400    // Handle star
401    if let Some(rest) = input.strip_prefix('*') {
402        // Star can't have anything after it except delimiter
403        return Ok((
404            SelectItem::Field {
405                field: ("*".into(), SmallVec::new()),
406                alias: None,
407                cast: None,
408                aggregate: None,
409                aggregate_cast: None,
410            },
411            rest,
412        ));
413    }
414
415    // Handle standalone count()
416    if let Some(after_count) = input.strip_prefix("count()") {
417        let (agg_cast, rest) = parse_optional_cast(after_count);
418        return Ok((
419            SelectItem::Field {
420                field: ("*".into(), SmallVec::new()),
421                alias,
422                cast: None,
423                aggregate: Some(AggregateFunction::Count),
424                aggregate_cast: agg_cast,
425            },
426            rest,
427        ));
428    }
429
430    // Parse field name and JSON path
431    let (name, rest) = parse_field_name(input)?;
432    let (json_path, rest) = parse_json_path(rest);
433
434    // Parse cast
435    let (cast, rest) = parse_optional_cast(rest);
436
437    // Parse aggregate
438    let (aggregate, rest) = parse_optional_aggregate(rest);
439    let (aggregate_cast, rest) = if aggregate.is_some() {
440        parse_optional_cast(rest)
441    } else {
442        (None, rest)
443    };
444
445    Ok((
446        SelectItem::Field {
447            field: (name, json_path),
448            alias,
449            cast,
450            aggregate,
451            aggregate_cast,
452        },
453        rest,
454    ))
455}
456
457fn parse_children(input: &str) -> Result<(Vec<SelectItem>, &str), Error> {
458    let items = parse_select_until_close(input)?;
459    Ok(items)
460}
461
462fn parse_select_until_close(input: &str) -> Result<(Vec<SelectItem>, &str), Error> {
463    let mut items = Vec::new();
464    let mut chars = input;
465
466    loop {
467        chars = chars.trim_start();
468        if let Some(after_close) = chars.strip_prefix(')') {
469            return Ok((items, after_close));
470        }
471        if chars.is_empty() {
472            return Err(Error::ParseError {
473                location: "select".to_string(),
474                message: "unclosed parenthesis in select".to_string(),
475            });
476        }
477
478        let (item, rest) = parse_select_item(chars)?;
479        items.push(item);
480        chars = rest.trim_start();
481
482        if let Some(after_comma) = chars.strip_prefix(',') {
483            chars = after_comma;
484        }
485    }
486}
487
488// ==========================================================================
489// Field name parser
490// ==========================================================================
491
492fn parse_field_name(input: &str) -> Result<(FieldName, &str), Error> {
493    // Handle quoted names
494    if input.starts_with('"') {
495        return parse_quoted_value(input).map(|(v, r)| (CompactString::from(v), r));
496    }
497
498    // Handle star
499    if let Some(rest) = input.strip_prefix('*') {
500        return Ok(("*".into(), rest));
501    }
502
503    // Regular field name: letters, digits, _, $, spaces (trimmed), dashes (not ->)
504    let mut end = 0;
505    let bytes = input.as_bytes();
506    let len = bytes.len();
507
508    while end < len {
509        let ch = bytes[end] as char;
510        if ch.is_alphanumeric() || ch == '_' || ch == '$' || ch == ' ' {
511            end += 1;
512        } else if ch == '-' && end + 1 < len && bytes[end + 1] != b'>' {
513            // Dash that's not part of -> or ->>
514            end += 1;
515        } else {
516            break;
517        }
518    }
519
520    if end == 0 {
521        return Err(Error::ParseError {
522            location: "field name".to_string(),
523            message: format!(
524                "expected field name, got '{}'",
525                &input[..input.len().min(10)]
526            ),
527        });
528    }
529
530    let name = input[..end].trim();
531    Ok((CompactString::from(name), &input[end..]))
532}
533
534fn parse_quoted_value(input: &str) -> Result<(&str, &str), Error> {
535    if !input.starts_with('"') {
536        return Err(Error::ParseError {
537            location: "quoted value".to_string(),
538            message: "expected opening quote".to_string(),
539        });
540    }
541
542    let bytes = input.as_bytes();
543    let mut i = 1;
544    while i < bytes.len() {
545        if bytes[i] == b'\\' && i + 1 < bytes.len() {
546            i += 2; // skip escaped char
547        } else if bytes[i] == b'"' {
548            return Ok((&input[1..i], &input[i + 1..]));
549        } else {
550            i += 1;
551        }
552    }
553
554    Err(Error::ParseError {
555        location: "quoted value".to_string(),
556        message: "unclosed quote".to_string(),
557    })
558}
559
560// ==========================================================================
561// JSON path parser
562// ==========================================================================
563
564fn parse_json_path(input: &str) -> (JsonPath, &str) {
565    let mut path = SmallVec::new();
566    let mut rest = input;
567
568    loop {
569        if let Some(after) = rest.strip_prefix("->>") {
570            match parse_json_operand(after) {
571                Ok((operand, r)) => {
572                    path.push(JsonOperation::Arrow2(operand));
573                    rest = r;
574                }
575                Err(_) => break,
576            }
577        } else if let Some(after) = rest.strip_prefix("->") {
578            match parse_json_operand(after) {
579                Ok((operand, r)) => {
580                    path.push(JsonOperation::Arrow(operand));
581                    rest = r;
582                }
583                Err(_) => break,
584            }
585        } else {
586            break;
587        }
588    }
589
590    (path, rest)
591}
592
593fn parse_json_operand(input: &str) -> Result<(JsonOperand, &str), Error> {
594    // Try index first (digits, possibly preceded by -)
595    let bytes = input.as_bytes();
596    if !bytes.is_empty() {
597        let (has_sign, start) = if bytes[0] == b'-' {
598            (true, 1)
599        } else {
600            (false, 0)
601        };
602
603        if start < bytes.len() && bytes[start].is_ascii_digit() {
604            let mut end = start + 1;
605            while end < bytes.len() && bytes[end].is_ascii_digit() {
606                end += 1;
607            }
608            // Check that next char is a delimiter
609            if end >= bytes.len()
610                || bytes[end] == b'-'
611                || bytes[end] == b':'
612                || bytes[end] == b'.'
613                || bytes[end] == b','
614                || bytes[end] == b')'
615            {
616                let sign = if has_sign { "-" } else { "+" };
617                let idx = format!("{}{}", sign, &input[start..end]);
618                return Ok((JsonOperand::Idx(CompactString::from(idx)), &input[end..]));
619            }
620        }
621    }
622
623    // Parse key
624    let mut end = 0;
625    while end < bytes.len() {
626        let ch = bytes[end] as char;
627        // Stop at delimiters
628        if ch == '(' || ch == '-' || ch == ':' || ch == '.' || ch == ',' || ch == '>' || ch == ')' {
629            break;
630        }
631        end += 1;
632    }
633
634    if end == 0 {
635        return Err(Error::ParseError {
636            location: "json operand".to_string(),
637            message: "empty json key".to_string(),
638        });
639    }
640
641    Ok((
642        JsonOperand::Key(CompactString::from(&input[..end])),
643        &input[end..],
644    ))
645}
646
647// ==========================================================================
648// Alias parser
649// ==========================================================================
650
651fn try_parse_alias(input: &str) -> (Option<Alias>, &str) {
652    // Look for "name:" where : is not followed by :
653    // Be careful: don't consume past special chars
654    let bytes = input.as_bytes();
655    let mut colon_pos = None;
656
657    for (i, &b) in bytes.iter().enumerate() {
658        if b == b':' {
659            if i + 1 < bytes.len() && bytes[i + 1] == b':' {
660                // This is :: (cast), not alias separator
661                break;
662            }
663            colon_pos = Some(i);
664            break;
665        }
666        // Stop at characters that can't be in a field name
667        if b == b'(' || b == b')' || b == b',' || b == b'!' || b == b'.' {
668            break;
669        }
670        if b == b'-' && i + 1 < bytes.len() && bytes[i + 1] == b'>' {
671            break;
672        }
673    }
674
675    match colon_pos {
676        Some(pos) if pos > 0 => {
677            let alias = input[..pos].trim();
678            (Some(CompactString::from(alias)), &input[pos + 1..])
679        }
680        _ => (None, input),
681    }
682}
683
684// ==========================================================================
685// Embed params (!hint, !inner, !left)
686// ==========================================================================
687
688fn parse_embed_params(input: &str) -> (Option<Hint>, Option<JoinType>, &str) {
689    let mut hint = None;
690    let mut join_type = None;
691    let mut rest = input;
692
693    // Parse up to 2 embed params
694    for _ in 0..2 {
695        if let Some(after_bang) = rest.strip_prefix('!') {
696            if let Some(r) = after_bang.strip_prefix("left")
697                && !r.starts_with(|c: char| c.is_alphanumeric() || c == '_')
698            {
699                join_type = join_type.or(Some(JoinType::Left));
700                rest = r;
701                continue;
702            }
703            if let Some(r) = after_bang.strip_prefix("inner")
704                && !r.starts_with(|c: char| c.is_alphanumeric() || c == '_')
705            {
706                join_type = join_type.or(Some(JoinType::Inner));
707                rest = r;
708                continue;
709            }
710            // It's a hint
711            if let Ok((name, r)) = parse_field_name(after_bang) {
712                hint = hint.or(Some(name));
713                rest = r;
714                continue;
715            }
716        }
717        break;
718    }
719
720    (hint, join_type, rest)
721}
722
723// ==========================================================================
724// Cast parser
725// ==========================================================================
726
727fn parse_optional_cast(input: &str) -> (Option<Cast>, &str) {
728    if let Some(rest) = input.strip_prefix("::") {
729        let mut end = 0;
730        let bytes = rest.as_bytes();
731        while end < bytes.len() {
732            let ch = bytes[end] as char;
733            if ch.is_alphanumeric() || ch == '_' || ch == ' ' {
734                end += 1;
735            } else {
736                break;
737            }
738        }
739        if end > 0 {
740            let cast = rest[..end].trim();
741            (Some(CompactString::from(cast)), &rest[end..])
742        } else {
743            (None, input) // :: but nothing after it
744        }
745    } else {
746        (None, input)
747    }
748}
749
750// ==========================================================================
751// Aggregate parser
752// ==========================================================================
753
754fn parse_optional_aggregate(input: &str) -> (Option<AggregateFunction>, &str) {
755    if !input.starts_with('.') {
756        return (None, input);
757    }
758
759    let rest = &input[1..];
760    let aggregates = [
761        ("count()", AggregateFunction::Count),
762        ("sum()", AggregateFunction::Sum),
763        ("avg()", AggregateFunction::Avg),
764        ("max()", AggregateFunction::Max),
765        ("min()", AggregateFunction::Min),
766    ];
767
768    for (prefix, agg) in &aggregates {
769        if let Some(after) = rest.strip_prefix(prefix) {
770            return (Some(*agg), after);
771        }
772    }
773
774    (None, input) // Not a recognized aggregate
775}
776
777// ==========================================================================
778// Filter parser
779// ==========================================================================
780
781/// Parse a filter value string (the RHS of `column=value`).
782fn parse_filter_value(value: &str, is_rpc_read: bool) -> Result<OpExpr, Error> {
783    match try_parse_op_expr(value) {
784        Ok(expr) => Ok(expr),
785        Err(_) if is_rpc_read => Ok(OpExpr::NoOp(CompactString::from(value))),
786        Err(e) => Err(e),
787    }
788}
789
790fn try_parse_op_expr(value: &str) -> Result<OpExpr, Error> {
791    // Check for "not." prefix
792    let (negated, rest) = if let Some(after) = value.strip_prefix("not.") {
793        (true, after)
794    } else {
795        (false, value)
796    };
797
798    let operation = parse_operation(rest)?;
799    Ok(OpExpr::Expr { negated, operation })
800}
801
802fn parse_operation(input: &str) -> Result<Operation, Error> {
803    // Try each operation type in order (by parser priority)
804
805    // in.(val1,val2)
806    if let Some(rest) = input.strip_prefix("in.") {
807        let list = parse_list_val(rest)?;
808        return Ok(Operation::In(list));
809    }
810
811    // is.null, is.true, is.false, is.unknown, is.not_null
812    if let Some(rest) = input.strip_prefix("is.") {
813        let val = parse_is_val(rest)?;
814        return Ok(Operation::Is(val));
815    }
816
817    // isdistinct.value
818    if let Some(rest) = input.strip_prefix("isdistinct.") {
819        return Ok(Operation::IsDistinctFrom(CompactString::from(rest)));
820    }
821
822    // FTS operators
823    if let Some(rest) = input.strip_prefix("fts") {
824        let (lang, val) = parse_fts_args(rest)?;
825        return Ok(Operation::Fts(FtsOperator::Fts, lang, val));
826    }
827    if let Some(rest) = input.strip_prefix("plfts") {
828        let (lang, val) = parse_fts_args(rest)?;
829        return Ok(Operation::Fts(FtsOperator::FtsPlain, lang, val));
830    }
831    if let Some(rest) = input.strip_prefix("phfts") {
832        let (lang, val) = parse_fts_args(rest)?;
833        return Ok(Operation::Fts(FtsOperator::FtsPhrase, lang, val));
834    }
835    if let Some(rest) = input.strip_prefix("wfts") {
836        let (lang, val) = parse_fts_args(rest)?;
837        return Ok(Operation::Fts(FtsOperator::FtsWebsearch, lang, val));
838    }
839
840    // Simple operators (must try before quant operators to avoid ambiguity)
841    let simple_ops = [
842        ("neq.", SimpleOperator::NotEqual),
843        ("cs.", SimpleOperator::Contains),
844        ("cd.", SimpleOperator::Contained),
845        ("ov.", SimpleOperator::Overlap),
846        ("sl.", SimpleOperator::StrictlyLeft),
847        ("sr.", SimpleOperator::StrictlyRight),
848        ("nxr.", SimpleOperator::NotExtendsRight),
849        ("nxl.", SimpleOperator::NotExtendsLeft),
850        ("adj.", SimpleOperator::Adjacent),
851    ];
852
853    for (prefix, op) in &simple_ops {
854        if let Some(rest) = input.strip_prefix(prefix) {
855            return Ok(Operation::Simple(*op, CompactString::from(rest)));
856        }
857    }
858
859    // Quant operators (with optional quantifier)
860    let quant_ops = [
861        ("eq", QuantOperator::Equal),
862        ("gte", QuantOperator::GreaterThanEqual),
863        ("gt", QuantOperator::GreaterThan),
864        ("lte", QuantOperator::LessThanEqual),
865        ("lt", QuantOperator::LessThan),
866        ("like", QuantOperator::Like),
867        ("ilike", QuantOperator::ILike),
868        ("match", QuantOperator::Match),
869        ("imatch", QuantOperator::IMatch),
870    ];
871
872    for (prefix, op) in &quant_ops {
873        if let Some(rest) = input.strip_prefix(prefix) {
874            // Check for quantifier: (any) or (all)
875            let (quant, rest) = if let Some(after_any) = rest.strip_prefix("(any)") {
876                (Some(OpQuantifier::Any), after_any)
877            } else if let Some(after_all) = rest.strip_prefix("(all)") {
878                (Some(OpQuantifier::All), after_all)
879            } else {
880                (None, rest)
881            };
882
883            if let Some(val) = rest.strip_prefix('.') {
884                return Ok(Operation::Quant(*op, quant, CompactString::from(val)));
885            }
886        }
887    }
888
889    Err(Error::ParseError {
890        location: "filter".to_string(),
891        message: format!("unknown operator in '{}'", input),
892    })
893}
894
895fn parse_is_val(input: &str) -> Result<IsValue, Error> {
896    let lower = input.to_lowercase();
897    match lower.as_str() {
898        "null" => Ok(IsValue::Null),
899        "not_null" => Ok(IsValue::NotNull),
900        "true" => Ok(IsValue::True),
901        "false" => Ok(IsValue::False),
902        "unknown" => Ok(IsValue::Unknown),
903        _ => Err(Error::ParseError {
904            location: "is value".to_string(),
905            message: format!(
906                "expected null, not_null, true, false, or unknown, got '{}'",
907                input
908            ),
909        }),
910    }
911}
912
913fn parse_fts_args(input: &str) -> Result<(Option<Language>, SingleVal), Error> {
914    // fts(lang).value or fts.value
915    if input.starts_with('(') {
916        if let Some(close) = input.find(')') {
917            let lang = &input[1..close];
918            let rest = &input[close + 1..];
919            if let Some(val) = rest.strip_prefix('.') {
920                return Ok((Some(CompactString::from(lang)), CompactString::from(val)));
921            }
922        }
923        return Err(Error::ParseError {
924            location: "fts".to_string(),
925            message: "malformed FTS expression".to_string(),
926        });
927    }
928
929    if let Some(val) = input.strip_prefix('.') {
930        Ok((None, CompactString::from(val)))
931    } else {
932        Err(Error::ParseError {
933            location: "fts".to_string(),
934            message: format!("expected '.' after FTS operator, got '{}'", input),
935        })
936    }
937}
938
939fn parse_list_val(input: &str) -> Result<ListVal, Error> {
940    // Expect (val1,val2,val3)
941    let input = input.trim();
942    if !input.starts_with('(') {
943        return Err(Error::ParseError {
944            location: "list value".to_string(),
945            message: "expected '(' for in list".to_string(),
946        });
947    }
948
949    let inner = &input[1..];
950    let close = find_matching_paren(inner).ok_or_else(|| Error::ParseError {
951        location: "list value".to_string(),
952        message: "unclosed '(' in list".to_string(),
953    })?;
954
955    let content = &inner[..close];
956    let values = split_list_elements(content);
957
958    Ok(values
959        .into_iter()
960        .map(|s| CompactString::from(s.as_str()))
961        .collect())
962}
963
964fn find_matching_paren(input: &str) -> Option<usize> {
965    let mut depth = 0;
966    let mut in_quote = false;
967    for (i, ch) in input.chars().enumerate() {
968        if ch == '"' {
969            in_quote = !in_quote;
970        } else if !in_quote {
971            if ch == '(' {
972                depth += 1;
973            } else if ch == ')' {
974                if depth == 0 {
975                    return Some(i);
976                }
977                depth -= 1;
978            }
979        }
980    }
981    None
982}
983
984fn split_list_elements(input: &str) -> Vec<String> {
985    let mut elements = Vec::new();
986    let mut current = String::new();
987    let chars = input.chars();
988    let mut escape_next = false;
989    let mut in_quote = false;
990
991    for ch in chars {
992        if escape_next {
993            current.push(ch);
994            escape_next = false;
995            continue;
996        }
997
998        if in_quote {
999            if ch == '\\' {
1000                escape_next = true;
1001            } else if ch == '"' {
1002                in_quote = false;
1003            } else {
1004                current.push(ch);
1005            }
1006        } else if ch == '"' {
1007            in_quote = true;
1008        } else if ch == ',' {
1009            elements.push(current.clone());
1010            current.clear();
1011        } else {
1012            current.push(ch);
1013        }
1014    }
1015    elements.push(current);
1016    elements
1017}
1018
1019// ==========================================================================
1020// Order parser
1021// ==========================================================================
1022
1023/// Parse the `order=` parameter value.
1024pub fn parse_order(input: &str) -> Result<Vec<OrderTerm>, Error> {
1025    let mut terms = Vec::new();
1026
1027    for part in input.split(',') {
1028        let part = part.trim();
1029        if part.is_empty() {
1030            continue;
1031        }
1032        terms.push(parse_order_term(part)?);
1033    }
1034
1035    Ok(terms)
1036}
1037
1038fn parse_order_term(input: &str) -> Result<OrderTerm, Error> {
1039    // Check for relation order: relation(field).dir.nulls
1040    if let Some(paren_pos) = input.find('(')
1041        && let Some(close_pos) = input.find(')')
1042    {
1043        let relation = CompactString::from(input[..paren_pos].trim());
1044        let field_str = &input[paren_pos + 1..close_pos];
1045        let (name, rest_in_paren) = parse_field_name(field_str)?;
1046        let (json_path, _) = parse_json_path(rest_in_paren);
1047
1048        let after_paren = &input[close_pos + 1..];
1049        let (direction, nulls) = parse_order_modifiers(after_paren);
1050
1051        return Ok(OrderTerm::RelationTerm {
1052            relation,
1053            field: (name, json_path),
1054            direction,
1055            nulls,
1056        });
1057    }
1058
1059    // Regular order: field.dir.nulls
1060    let (name, rest) = parse_field_name(input)?;
1061    let (json_path, rest) = parse_json_path(rest);
1062    let (direction, nulls) = parse_order_modifiers(rest);
1063
1064    Ok(OrderTerm::Term {
1065        field: (name, json_path),
1066        direction,
1067        nulls,
1068    })
1069}
1070
1071fn parse_order_modifiers(input: &str) -> (Option<OrderDirection>, Option<OrderNulls>) {
1072    let mut direction = None;
1073    let mut nulls = None;
1074    let mut rest = input;
1075
1076    // Parse direction
1077    if let Some(r) = rest.strip_prefix(".asc") {
1078        if !r.starts_with(|c: char| c.is_alphanumeric()) {
1079            direction = Some(OrderDirection::Asc);
1080            rest = r;
1081        }
1082    } else if let Some(r) = rest.strip_prefix(".desc")
1083        && !r.starts_with(|c: char| c.is_alphanumeric())
1084    {
1085        direction = Some(OrderDirection::Desc);
1086        rest = r;
1087    }
1088
1089    // Parse nulls
1090    if let Some(r) = rest.strip_prefix(".nullsfirst") {
1091        if !r.starts_with(|c: char| c.is_alphanumeric()) {
1092            nulls = Some(OrderNulls::First);
1093        }
1094    } else if let Some(r) = rest.strip_prefix(".nullslast")
1095        && !r.starts_with(|c: char| c.is_alphanumeric())
1096    {
1097        nulls = Some(OrderNulls::Last);
1098    }
1099
1100    (direction, nulls)
1101}
1102
1103// ==========================================================================
1104// Logic tree parser
1105// ==========================================================================
1106
1107/// Parse a logic tree expression like "and(col.eq.1,col.gt.5)" or
1108/// "or(col.eq.1,not.and(a.eq.1,b.eq.2))"
1109pub fn parse_logic_tree(input: &str) -> Result<LogicTree, Error> {
1110    let input = input.trim();
1111
1112    // Check for negation
1113    let (negated, rest) = if let Some(after) = input.strip_prefix("not.") {
1114        (true, after)
1115    } else {
1116        (false, input)
1117    };
1118
1119    // Check for logic operator
1120    let (operator, rest) = if let Some(rest) = rest.strip_prefix("and") {
1121        (Some(LogicOperator::And), rest)
1122    } else if let Some(rest) = rest.strip_prefix("or") {
1123        (Some(LogicOperator::Or), rest)
1124    } else {
1125        (None, rest)
1126    };
1127
1128    match operator {
1129        Some(op) => {
1130            // Must have parentheses
1131            let rest = rest.trim();
1132            if !rest.starts_with('(') {
1133                return Err(Error::ParseError {
1134                    location: "logic tree".to_string(),
1135                    message: format!("expected '(' after logic operator, got '{}'", rest),
1136                });
1137            }
1138
1139            let inner = &rest[1..];
1140            let close = find_matching_paren(inner).ok_or_else(|| Error::ParseError {
1141                location: "logic tree".to_string(),
1142                message: "unclosed '(' in logic tree".to_string(),
1143            })?;
1144
1145            let content = &inner[..close];
1146            let children = parse_logic_children(content)?;
1147
1148            Ok(LogicTree::Expr {
1149                negated,
1150                operator: op,
1151                children,
1152            })
1153        }
1154        None => {
1155            // Must be a filter statement
1156            let filter = parse_logic_filter(rest)?;
1157            Ok(LogicTree::Stmnt(filter))
1158        }
1159    }
1160}
1161
1162fn parse_logic_children(input: &str) -> Result<Vec<LogicTree>, Error> {
1163    let parts = split_logic_args(input);
1164    let mut children = Vec::new();
1165
1166    for part in parts {
1167        let part = part.trim();
1168        if part.is_empty() {
1169            continue;
1170        }
1171        children.push(parse_logic_tree(part)?);
1172    }
1173
1174    if children.is_empty() {
1175        return Err(Error::ParseError {
1176            location: "logic tree".to_string(),
1177            message: "empty logic expression".to_string(),
1178        });
1179    }
1180
1181    Ok(children)
1182}
1183
1184fn split_logic_args(input: &str) -> Vec<&str> {
1185    let mut parts = Vec::new();
1186    let mut depth = 0;
1187    let mut start = 0;
1188
1189    for (i, ch) in input.char_indices() {
1190        match ch {
1191            '(' => depth += 1,
1192            ')' => depth -= 1,
1193            ',' if depth == 0 => {
1194                parts.push(&input[start..i]);
1195                start = i + ch.len_utf8();
1196            }
1197            _ => {}
1198        }
1199    }
1200    parts.push(&input[start..]);
1201    parts
1202}
1203
1204fn parse_logic_filter(input: &str) -> Result<Filter, Error> {
1205    // field.op.value
1206    let (name, rest) = parse_field_name(input)?;
1207    let (json_path, rest) = parse_json_path(rest);
1208
1209    let rest = rest.strip_prefix('.').ok_or_else(|| Error::ParseError {
1210        location: "logic filter".to_string(),
1211        message: format!("expected '.' after field name '{}'", name),
1212    })?;
1213
1214    let op_expr = try_parse_op_expr(rest)?;
1215
1216    Ok(Filter {
1217        field: (name, json_path),
1218        op_expr,
1219    })
1220}
1221
1222// ==========================================================================
1223// Tree path parser
1224// ==========================================================================
1225
1226/// Parse a dot-separated tree path with a final field name.
1227///
1228/// E.g., "clients.projects.name" -> (["clients", "projects"], ("name", []))
1229fn parse_tree_path(key: &str) -> Result<(EmbedPath, Field), Error> {
1230    let parts: Vec<&str> = key.split('.').collect();
1231    if parts.is_empty() {
1232        return Err(Error::ParseError {
1233            location: "tree path".to_string(),
1234            message: "empty path".to_string(),
1235        });
1236    }
1237
1238    let path: Vec<FieldName> = parts[..parts.len() - 1]
1239        .iter()
1240        .map(|s| CompactString::from(*s))
1241        .collect();
1242
1243    let last = *parts.last().unwrap();
1244    let (name, rest) = parse_field_name(last)?;
1245    let (json_path, _) = parse_json_path(rest);
1246
1247    Ok((path, (name, json_path)))
1248}
1249
1250/// Parse a logic path: "clients.projects.and" -> (["clients", "projects"], "and")
1251fn parse_logic_path(key: &str) -> Result<(EmbedPath, String), Error> {
1252    let parts: Vec<&str> = key.split('.').collect();
1253    if parts.is_empty() {
1254        return Err(Error::ParseError {
1255            location: "logic path".to_string(),
1256            message: "empty path".to_string(),
1257        });
1258    }
1259
1260    let op = parts.last().unwrap().to_string();
1261    let path: Vec<FieldName> = parts[..parts.len() - 1]
1262        .iter()
1263        .map(|s| CompactString::from(*s))
1264        .collect();
1265
1266    Ok((path, op))
1267}
1268
1269// ==========================================================================
1270// Columns parser
1271// ==========================================================================
1272
1273fn parse_columns(input: &str) -> Result<HashSet<FieldName>, Error> {
1274    Ok(parse_columns_list(input)?.into_iter().collect())
1275}
1276
1277fn parse_columns_list(input: &str) -> Result<Vec<FieldName>, Error> {
1278    Ok(input
1279        .split(',')
1280        .map(|s| CompactString::from(s.trim()))
1281        .filter(|s| !s.is_empty())
1282        .collect())
1283}
1284
1285// ==========================================================================
1286// Helpers
1287// ==========================================================================
1288
1289fn replace_last_segment(key: &str, _replacement: &str) -> String {
1290    let parts: Vec<&str> = key.split('.').collect();
1291    if parts.len() <= 1 {
1292        return "limit".to_string();
1293    }
1294    let mut result: Vec<&str> = parts[..parts.len() - 1].to_vec();
1295    result.push("limit");
1296    result.join(".")
1297}
1298
1299// ==========================================================================
1300// Tests
1301// ==========================================================================
1302
1303#[cfg(test)]
1304mod tests {
1305    use super::*;
1306
1307    // ---------- Select parser tests ----------
1308
1309    #[test]
1310    fn test_parse_select_star() {
1311        let items = parse_select("*").unwrap();
1312        assert_eq!(items.len(), 1);
1313        assert!(matches!(&items[0], SelectItem::Field { field, .. } if field.0 == "*"));
1314    }
1315
1316    #[test]
1317    fn test_parse_select_simple_fields() {
1318        let items = parse_select("id,name").unwrap();
1319        assert_eq!(items.len(), 2);
1320        assert!(matches!(&items[0], SelectItem::Field { field, .. } if field.0 == "id"));
1321        assert!(matches!(&items[1], SelectItem::Field { field, .. } if field.0 == "name"));
1322    }
1323
1324    #[test]
1325    fn test_parse_select_with_alias() {
1326        let items = parse_select("my_id:id,my_name:name").unwrap();
1327        assert_eq!(items.len(), 2);
1328        if let SelectItem::Field { alias, field, .. } = &items[0] {
1329            assert_eq!(alias.as_deref(), Some("my_id"));
1330            assert_eq!(field.0.as_str(), "id");
1331        }
1332    }
1333
1334    #[test]
1335    fn test_parse_select_with_cast() {
1336        let items = parse_select("id::text").unwrap();
1337        assert_eq!(items.len(), 1);
1338        if let SelectItem::Field { cast, .. } = &items[0] {
1339            assert_eq!(cast.as_deref(), Some("text"));
1340        }
1341    }
1342
1343    #[test]
1344    fn test_parse_select_with_json_path() {
1345        let items = parse_select("data->key->>value").unwrap();
1346        assert_eq!(items.len(), 1);
1347        if let SelectItem::Field { field, .. } = &items[0] {
1348            assert_eq!(field.0.as_str(), "data");
1349            assert_eq!(field.1.len(), 2);
1350            assert!(matches!(&field.1[0], JsonOperation::Arrow(JsonOperand::Key(k)) if k == "key"));
1351            assert!(
1352                matches!(&field.1[1], JsonOperation::Arrow2(JsonOperand::Key(k)) if k == "value")
1353            );
1354        }
1355    }
1356
1357    #[test]
1358    fn test_parse_select_with_aggregate() {
1359        let items = parse_select("amount.sum()").unwrap();
1360        assert_eq!(items.len(), 1);
1361        if let SelectItem::Field {
1362            field, aggregate, ..
1363        } = &items[0]
1364        {
1365            assert_eq!(field.0.as_str(), "amount");
1366            assert_eq!(*aggregate, Some(AggregateFunction::Sum));
1367        }
1368    }
1369
1370    #[test]
1371    fn test_parse_select_count() {
1372        let items = parse_select("count()").unwrap();
1373        assert_eq!(items.len(), 1);
1374        if let SelectItem::Field {
1375            field, aggregate, ..
1376        } = &items[0]
1377        {
1378            assert_eq!(field.0.as_str(), "*");
1379            assert_eq!(*aggregate, Some(AggregateFunction::Count));
1380        }
1381    }
1382
1383    #[test]
1384    fn test_parse_select_relation() {
1385        let items = parse_select("posts(id,title)").unwrap();
1386        assert_eq!(items.len(), 1);
1387        if let SelectItem::Relation {
1388            relation, children, ..
1389        } = &items[0]
1390        {
1391            assert_eq!(relation.as_str(), "posts");
1392            assert_eq!(children.len(), 2);
1393        }
1394    }
1395
1396    #[test]
1397    fn test_parse_select_relation_with_alias_and_hint() {
1398        let items = parse_select("my_posts:posts!fk_author(*)").unwrap();
1399        assert_eq!(items.len(), 1);
1400        if let SelectItem::Relation {
1401            relation,
1402            alias,
1403            hint,
1404            children,
1405            ..
1406        } = &items[0]
1407        {
1408            assert_eq!(relation.as_str(), "posts");
1409            assert_eq!(alias.as_deref(), Some("my_posts"));
1410            assert_eq!(hint.as_deref(), Some("fk_author"));
1411            assert_eq!(children.len(), 1);
1412        }
1413    }
1414
1415    #[test]
1416    fn test_parse_select_relation_with_join_type() {
1417        let items = parse_select("posts!inner(*)").unwrap();
1418        assert_eq!(items.len(), 1);
1419        if let SelectItem::Relation {
1420            relation,
1421            join_type,
1422            ..
1423        } = &items[0]
1424        {
1425            assert_eq!(relation.as_str(), "posts");
1426            assert_eq!(*join_type, Some(JoinType::Inner));
1427        }
1428    }
1429
1430    #[test]
1431    fn test_parse_select_spread() {
1432        let items = parse_select("...details(*)").unwrap();
1433        assert_eq!(items.len(), 1);
1434        if let SelectItem::Spread {
1435            relation, children, ..
1436        } = &items[0]
1437        {
1438            assert_eq!(relation.as_str(), "details");
1439            assert_eq!(children.len(), 1);
1440        }
1441    }
1442
1443    #[test]
1444    fn test_parse_select_nested() {
1445        let items = parse_select("*,clients(*,projects(*))").unwrap();
1446        assert_eq!(items.len(), 2);
1447        if let SelectItem::Relation { children, .. } = &items[1] {
1448            assert_eq!(children.len(), 2);
1449            assert!(
1450                matches!(&children[1], SelectItem::Relation { relation, .. } if relation == "projects")
1451            );
1452        }
1453    }
1454
1455    #[test]
1456    fn test_parse_select_empty() {
1457        let items = parse_select("").unwrap();
1458        assert!(items.is_empty());
1459    }
1460
1461    #[test]
1462    fn test_parse_select_complex() {
1463        let items =
1464            parse_select("alias:name->key::cast,posts!hint!inner(id,author(*)),...spread(*)")
1465                .unwrap();
1466        assert_eq!(items.len(), 3);
1467    }
1468
1469    // ---------- Filter parser tests ----------
1470
1471    #[test]
1472    fn test_parse_filter_eq() {
1473        let expr = try_parse_op_expr("eq.5").unwrap();
1474        assert_eq!(
1475            expr,
1476            OpExpr::Expr {
1477                negated: false,
1478                operation: Operation::Quant(QuantOperator::Equal, None, "5".into())
1479            }
1480        );
1481    }
1482
1483    #[test]
1484    fn test_parse_filter_not_eq() {
1485        let expr = try_parse_op_expr("not.eq.5").unwrap();
1486        assert_eq!(
1487            expr,
1488            OpExpr::Expr {
1489                negated: true,
1490                operation: Operation::Quant(QuantOperator::Equal, None, "5".into())
1491            }
1492        );
1493    }
1494
1495    #[test]
1496    fn test_parse_filter_neq() {
1497        let expr = try_parse_op_expr("neq.5").unwrap();
1498        assert_eq!(
1499            expr,
1500            OpExpr::Expr {
1501                negated: false,
1502                operation: Operation::Simple(SimpleOperator::NotEqual, "5".into())
1503            }
1504        );
1505    }
1506
1507    #[test]
1508    fn test_parse_filter_gt_lt() {
1509        let gt = try_parse_op_expr("gt.10").unwrap();
1510        assert!(matches!(
1511            gt,
1512            OpExpr::Expr {
1513                operation: Operation::Quant(QuantOperator::GreaterThan, None, _),
1514                ..
1515            }
1516        ));
1517
1518        let lt = try_parse_op_expr("lt.5").unwrap();
1519        assert!(matches!(
1520            lt,
1521            OpExpr::Expr {
1522                operation: Operation::Quant(QuantOperator::LessThan, None, _),
1523                ..
1524            }
1525        ));
1526    }
1527
1528    #[test]
1529    fn test_parse_filter_in() {
1530        let expr = try_parse_op_expr("in.(1,2,3)").unwrap();
1531        if let OpExpr::Expr {
1532            operation: Operation::In(vals),
1533            ..
1534        } = &expr
1535        {
1536            assert_eq!(vals.len(), 3);
1537            assert_eq!(vals[0].as_str(), "1");
1538            assert_eq!(vals[1].as_str(), "2");
1539            assert_eq!(vals[2].as_str(), "3");
1540        } else {
1541            panic!("expected In operation");
1542        }
1543    }
1544
1545    #[test]
1546    fn test_parse_filter_is_null() {
1547        let expr = try_parse_op_expr("is.null").unwrap();
1548        assert_eq!(
1549            expr,
1550            OpExpr::Expr {
1551                negated: false,
1552                operation: Operation::Is(IsValue::Null)
1553            }
1554        );
1555    }
1556
1557    #[test]
1558    fn test_parse_filter_is_true() {
1559        let expr = try_parse_op_expr("is.true").unwrap();
1560        assert_eq!(
1561            expr,
1562            OpExpr::Expr {
1563                negated: false,
1564                operation: Operation::Is(IsValue::True)
1565            }
1566        );
1567    }
1568
1569    #[test]
1570    fn test_parse_filter_fts() {
1571        let expr = try_parse_op_expr("fts.search_term").unwrap();
1572        if let OpExpr::Expr {
1573            operation: Operation::Fts(op, lang, val),
1574            ..
1575        } = &expr
1576        {
1577            assert_eq!(*op, FtsOperator::Fts);
1578            assert!(lang.is_none());
1579            assert_eq!(val.as_str(), "search_term");
1580        }
1581    }
1582
1583    #[test]
1584    fn test_parse_filter_fts_with_lang() {
1585        let expr = try_parse_op_expr("fts(english).search_term").unwrap();
1586        if let OpExpr::Expr {
1587            operation: Operation::Fts(_, lang, _),
1588            ..
1589        } = &expr
1590        {
1591            assert_eq!(lang.as_deref(), Some("english"));
1592        }
1593    }
1594
1595    #[test]
1596    fn test_parse_filter_like() {
1597        let expr = try_parse_op_expr("like.*john*").unwrap();
1598        assert!(matches!(
1599            expr,
1600            OpExpr::Expr {
1601                operation: Operation::Quant(QuantOperator::Like, None, _),
1602                ..
1603            }
1604        ));
1605    }
1606
1607    #[test]
1608    fn test_parse_filter_quant_any() {
1609        let expr = try_parse_op_expr("eq(any).5").unwrap();
1610        assert_eq!(
1611            expr,
1612            OpExpr::Expr {
1613                negated: false,
1614                operation: Operation::Quant(
1615                    QuantOperator::Equal,
1616                    Some(OpQuantifier::Any),
1617                    "5".into()
1618                )
1619            }
1620        );
1621    }
1622
1623    #[test]
1624    fn test_parse_filter_quant_all() {
1625        let expr = try_parse_op_expr("eq(all).5").unwrap();
1626        assert_eq!(
1627            expr,
1628            OpExpr::Expr {
1629                negated: false,
1630                operation: Operation::Quant(
1631                    QuantOperator::Equal,
1632                    Some(OpQuantifier::All),
1633                    "5".into()
1634                )
1635            }
1636        );
1637    }
1638
1639    #[test]
1640    fn test_parse_filter_isdistinct() {
1641        let expr = try_parse_op_expr("isdistinct.value").unwrap();
1642        assert_eq!(
1643            expr,
1644            OpExpr::Expr {
1645                negated: false,
1646                operation: Operation::IsDistinctFrom("value".into())
1647            }
1648        );
1649    }
1650
1651    #[test]
1652    fn test_parse_filter_cs_cd() {
1653        let cs = try_parse_op_expr("cs.{1,2,3}").unwrap();
1654        assert!(matches!(
1655            cs,
1656            OpExpr::Expr {
1657                operation: Operation::Simple(SimpleOperator::Contains, _),
1658                ..
1659            }
1660        ));
1661
1662        let cd = try_parse_op_expr("cd.{1,2}").unwrap();
1663        assert!(matches!(
1664            cd,
1665            OpExpr::Expr {
1666                operation: Operation::Simple(SimpleOperator::Contained, _),
1667                ..
1668            }
1669        ));
1670    }
1671
1672    #[test]
1673    fn test_parse_filter_rpc_no_op() {
1674        let expr = parse_filter_value("plain_value", true).unwrap();
1675        assert_eq!(expr, OpExpr::NoOp("plain_value".into()));
1676    }
1677
1678    #[test]
1679    fn test_parse_filter_rpc_with_op() {
1680        let expr = parse_filter_value("eq.5", true).unwrap();
1681        assert!(matches!(expr, OpExpr::Expr { .. }));
1682    }
1683
1684    #[test]
1685    fn test_parse_filter_non_rpc_requires_op() {
1686        let result = parse_filter_value("plain_value", false);
1687        assert!(result.is_err());
1688    }
1689
1690    // ---------- Order parser tests ----------
1691
1692    #[test]
1693    fn test_parse_order_simple() {
1694        let terms = parse_order("name").unwrap();
1695        assert_eq!(terms.len(), 1);
1696        if let OrderTerm::Term {
1697            field,
1698            direction,
1699            nulls,
1700        } = &terms[0]
1701        {
1702            assert_eq!(field.0.as_str(), "name");
1703            assert!(direction.is_none());
1704            assert!(nulls.is_none());
1705        }
1706    }
1707
1708    #[test]
1709    fn test_parse_order_desc_nullsfirst() {
1710        let terms = parse_order("name.desc.nullsfirst").unwrap();
1711        assert_eq!(terms.len(), 1);
1712        if let OrderTerm::Term {
1713            direction, nulls, ..
1714        } = &terms[0]
1715        {
1716            assert_eq!(*direction, Some(OrderDirection::Desc));
1717            assert_eq!(*nulls, Some(OrderNulls::First));
1718        }
1719    }
1720
1721    #[test]
1722    fn test_parse_order_multiple() {
1723        let terms = parse_order("name.asc,id.desc").unwrap();
1724        assert_eq!(terms.len(), 2);
1725    }
1726
1727    #[test]
1728    fn test_parse_order_json() {
1729        let terms = parse_order("json_col->key.asc.nullslast").unwrap();
1730        assert_eq!(terms.len(), 1);
1731        if let OrderTerm::Term { field, .. } = &terms[0] {
1732            assert_eq!(field.0.as_str(), "json_col");
1733            assert_eq!(field.1.len(), 1);
1734        }
1735    }
1736
1737    #[test]
1738    fn test_parse_order_relation() {
1739        let terms = parse_order("clients(name).desc.nullsfirst").unwrap();
1740        assert_eq!(terms.len(), 1);
1741        assert!(
1742            matches!(&terms[0], OrderTerm::RelationTerm { relation, .. } if relation == "clients")
1743        );
1744    }
1745
1746    // ---------- Logic tree tests ----------
1747
1748    #[test]
1749    fn test_parse_logic_tree_simple() {
1750        let tree = parse_logic_tree("and(name.eq.John,age.gt.18)").unwrap();
1751        if let LogicTree::Expr {
1752            negated,
1753            operator,
1754            children,
1755        } = &tree
1756        {
1757            assert!(!negated);
1758            assert_eq!(*operator, LogicOperator::And);
1759            assert_eq!(children.len(), 2);
1760        }
1761    }
1762
1763    #[test]
1764    fn test_parse_logic_tree_or() {
1765        let tree = parse_logic_tree("or(id.eq.1,id.eq.2)").unwrap();
1766        if let LogicTree::Expr { operator, .. } = &tree {
1767            assert_eq!(*operator, LogicOperator::Or);
1768        }
1769    }
1770
1771    #[test]
1772    fn test_parse_logic_tree_nested() {
1773        let tree = parse_logic_tree("and(name.eq.John,or(id.eq.1,id.eq.2))").unwrap();
1774        if let LogicTree::Expr { children, .. } = &tree {
1775            assert_eq!(children.len(), 2);
1776            assert!(matches!(
1777                &children[1],
1778                LogicTree::Expr {
1779                    operator: LogicOperator::Or,
1780                    ..
1781                }
1782            ));
1783        }
1784    }
1785
1786    #[test]
1787    fn test_parse_logic_tree_negated() {
1788        let tree = parse_logic_tree("not.and(a.eq.1,b.eq.2)").unwrap();
1789        if let LogicTree::Expr { negated, .. } = &tree {
1790            assert!(negated);
1791        }
1792    }
1793
1794    // ---------- Full parse tests ----------
1795
1796    #[test]
1797    fn test_parse_full_query() {
1798        let qp = parse(false, "select=id,name&id=eq.1&order=name.asc").unwrap();
1799        assert_eq!(qp.select.len(), 2);
1800        assert_eq!(qp.filters_root.len(), 1);
1801        assert_eq!(qp.order.len(), 1);
1802    }
1803
1804    #[test]
1805    fn test_parse_with_limit_offset() {
1806        let qp = parse(false, "select=*&limit=25&offset=50").unwrap();
1807        let range = qp.ranges.get("limit").unwrap();
1808        assert_eq!(range.offset, 50);
1809        assert_eq!(range.limit(), Some(25));
1810    }
1811
1812    #[test]
1813    fn test_parse_canonical() {
1814        let qp = parse(false, "b=eq.2&a=eq.1").unwrap();
1815        // Canonical should be sorted
1816        assert!(qp.canonical.starts_with("a="));
1817    }
1818
1819    #[test]
1820    fn test_parse_columns() {
1821        let qp = parse(false, "select=*&columns=id,name").unwrap();
1822        let cols = qp.columns.as_ref().unwrap();
1823        assert!(cols.contains("id"));
1824        assert!(cols.contains("name"));
1825    }
1826
1827    #[test]
1828    fn test_parse_on_conflict() {
1829        let qp = parse(false, "select=*&on_conflict=id,email").unwrap();
1830        let oc = qp.on_conflict.as_ref().unwrap();
1831        assert_eq!(oc.len(), 2);
1832    }
1833
1834    #[test]
1835    fn test_parse_rpc_params() {
1836        let qp = parse(true, "id=5&name=john").unwrap();
1837        assert_eq!(qp.params.len(), 2);
1838    }
1839
1840    #[test]
1841    fn test_parse_embedded_filter() {
1842        let qp = parse(false, "select=*,posts(*)&posts.status=eq.published").unwrap();
1843        assert_eq!(qp.filters_not_root.len(), 1);
1844        assert_eq!(qp.filters_not_root[0].0, vec![CompactString::from("posts")]);
1845    }
1846
1847    #[test]
1848    fn test_parse_embedded_order() {
1849        let qp = parse(false, "select=*,posts(*)&posts.order=created_at.desc").unwrap();
1850        assert_eq!(qp.order.len(), 1);
1851        assert_eq!(qp.order[0].0, vec![CompactString::from("posts")]);
1852    }
1853
1854    #[test]
1855    fn test_parse_logic() {
1856        let qp = parse(false, "select=*&or=(id.eq.1,id.eq.2)").unwrap();
1857        assert_eq!(qp.logic.len(), 1);
1858    }
1859
1860    #[test]
1861    fn test_parse_default_select() {
1862        let qp = parse(false, "id=eq.1").unwrap();
1863        assert_eq!(qp.select.len(), 1);
1864        assert!(matches!(&qp.select[0], SelectItem::Field { field, .. } if field.0 == "*"));
1865    }
1866
1867    #[test]
1868    fn test_parse_all_simple_operators() {
1869        for op in ["neq", "cs", "cd", "ov", "sl", "sr", "nxr", "nxl", "adj"] {
1870            let expr = try_parse_op_expr(&format!("{}.value", op)).unwrap();
1871            assert!(
1872                matches!(
1873                    expr,
1874                    OpExpr::Expr {
1875                        operation: Operation::Simple(..),
1876                        ..
1877                    }
1878                ),
1879                "operator {} should parse as Simple",
1880                op
1881            );
1882        }
1883    }
1884
1885    #[test]
1886    fn test_parse_all_quant_operators() {
1887        for op in [
1888            "eq", "gte", "gt", "lte", "lt", "like", "ilike", "match", "imatch",
1889        ] {
1890            let expr = try_parse_op_expr(&format!("{}.value", op)).unwrap();
1891            assert!(
1892                matches!(
1893                    expr,
1894                    OpExpr::Expr {
1895                        operation: Operation::Quant(..),
1896                        ..
1897                    }
1898                ),
1899                "operator {} should parse as Quant",
1900                op
1901            );
1902        }
1903    }
1904
1905    #[test]
1906    fn test_parse_all_fts_operators() {
1907        for op in ["fts", "plfts", "phfts", "wfts"] {
1908            let expr = try_parse_op_expr(&format!("{}.term", op)).unwrap();
1909            assert!(
1910                matches!(
1911                    expr,
1912                    OpExpr::Expr {
1913                        operation: Operation::Fts(..),
1914                        ..
1915                    }
1916                ),
1917                "operator {} should parse as Fts",
1918                op
1919            );
1920        }
1921    }
1922
1923    #[test]
1924    fn test_parse_is_values() {
1925        for (val, expected) in [
1926            ("is.null", IsValue::Null),
1927            ("is.not_null", IsValue::NotNull),
1928            ("is.true", IsValue::True),
1929            ("is.false", IsValue::False),
1930            ("is.unknown", IsValue::Unknown),
1931        ] {
1932            let expr = try_parse_op_expr(val).unwrap();
1933            assert_eq!(
1934                expr,
1935                OpExpr::Expr {
1936                    negated: false,
1937                    operation: Operation::Is(expected)
1938                },
1939                "is value {} should parse",
1940                val
1941            );
1942        }
1943    }
1944}