qql-core 0.4.1

Parser, typed AST, validation, and transformations for the Qdrant Query Language
Documentation
use super::AstLowerer;
use crate::ast::{CountStmt, FacetStmt, OrderDirection, ScrollOrderBy, ScrollStmt, Stmt};
use crate::error::QqlError;
use crate::token::TokenKind;
use alloc::boxed::Box;

impl<'a> AstLowerer<'a> {
    pub fn parse_scroll(&mut self) -> Result<Stmt, QqlError> {
        self.expect(TokenKind::Scroll)?;
        self.expect(TokenKind::From)?;
        let collection = self.parse_identifier()?;
        let filter = if self.peek()?.kind == TokenKind::Where {
            self.advance()?;
            Some(Box::new(self.parse_filter_expr()?))
        } else {
            None
        };
        let after = if self.peek()?.kind == TokenKind::After {
            self.advance()?;
            Some(self.parse_point_id("SCROLL AFTER")?)
        } else {
            None
        };
        // Optional: ORDER BY <key> [ASC|DESC] [START FROM <value>].
        let order_by = if self.peek()?.kind == TokenKind::Order {
            self.advance()?;
            self.expect(TokenKind::By)?;
            let field = self.parse_field_path()?;
            let direction = match self.peek()?.kind {
                TokenKind::Desc => {
                    self.advance()?;
                    OrderDirection::Desc
                }
                TokenKind::Asc => {
                    self.advance()?;
                    OrderDirection::Asc
                }
                _ => OrderDirection::Asc,
            };
            let start_from = self.parse_optional_order_start()?;
            Some(ScrollOrderBy {
                field,
                direction,
                start_from,
            })
        } else {
            None
        };
        let shard_key = if self.peek()?.kind == TokenKind::Shard {
            self.advance()?;
            Some(self.parse_shard_key_atom()?)
        } else {
            None
        };
        // Optional: WITH PAYLOAD [true|false|INCLUDE (...)|EXCLUDE (...)].
        // Bare `WITH PAYLOAD` without a selector means all payload.
        let with_payload = if self.peek()?.kind == TokenKind::With
            && self.peek_nth(1).kind == TokenKind::Payload
        {
            self.advance()?;
            self.advance()?;
            Some(self.parse_payload_selector()?)
        } else {
            None
        };
        // Optional: WITH VECTOR [true|false|(names)] — bare form means all vectors.
        let with_vector =
            if self.peek()?.kind == TokenKind::With && self.peek_nth(1).kind == TokenKind::Vector {
                self.advance()?;
                self.advance()?;
                Some(self.parse_vector_selector()?)
            } else {
                None
            };
        self.expect(TokenKind::Limit)?;
        let (limit, limit_param, limit_span) =
            if let Some((param, span)) = self.parse_placeholder_param()? {
                (10, Some(param), Some(span))
            } else {
                (self.parse_positive_u64("SCROLL LIMIT")?, None, None)
            };
        Ok(Stmt::Scroll(Box::new(ScrollStmt {
            collection,
            limit,
            filter,
            after,
            order_by,
            shard_key,
            with_payload,
            with_vector,
            limit_param,
            limit_span,
        })))
    }

    pub fn parse_count(&mut self) -> Result<Stmt, QqlError> {
        self.expect(TokenKind::Count)?;
        self.expect(TokenKind::From)?;
        let collection = crate::ast::QueryCollection::Explicit(self.parse_identifier()?);
        // Grammar order (grammar.pest `count`): WHERE → SHARD → WITH, each at
        // most once. The runtime previously accepted any order/repeats.
        let filter = if self.peek()?.kind == TokenKind::Where {
            self.advance()?;
            Some(Box::new(self.parse_filter_expr()?))
        } else {
            None
        };
        let shard_key = if self.peek()?.kind == TokenKind::Shard {
            self.advance()?;
            Some(self.parse_shard_key_atom()?)
        } else {
            None
        };
        let exact = if self.peek()?.kind == TokenKind::With {
            let with_span = self.peek()?.span;
            self.advance()?;
            let opts = self.parse_config_block()?;
            let mut exact = None;
            for (key, value) in &opts {
                if !key.eq_ignore_ascii_case("exact") {
                    return Err(QqlError::parse(
                        "QQL-PARSE-COUNT-CONFIG",
                        alloc::format!("unknown COUNT parameter '{key}'. Expected: exact"),
                        with_span,
                    ));
                }
                match value {
                    crate::ast::Value::Bool(b) => exact = Some(*b),
                    _ => {
                        return Err(QqlError::parse(
                            "QQL-PARSE-COUNT-CONFIG",
                            "COUNT 'exact' must be true or false",
                            with_span,
                        ));
                    }
                }
            }
            exact
        } else {
            None
        };
        if matches!(
            self.peek()?.kind,
            TokenKind::Where | TokenKind::Shard | TokenKind::With
        ) {
            return Err(QqlError::parse(
                "QQL-PARSE-CLAUSE-ORDER",
                "duplicate or out-of-order COUNT clause (grammar order: WHERE, SHARD, WITH)",
                self.peek()?.span,
            ));
        }
        Ok(Stmt::Count(Box::new(CountStmt {
            collection,
            filter,
            shard_key,
            exact,
        })))
    }

    pub fn parse_facet(&mut self) -> Result<Stmt, QqlError> {
        self.expect(TokenKind::Facet)?;
        let (key, collection) = if self.peek()?.kind == TokenKind::From {
            self.advance()?;
            let coll = crate::ast::QueryCollection::Explicit(self.parse_identifier()?);
            if self.peek()?.kind == TokenKind::Key || self.peek_word("KEY")? {
                self.advance()?;
            }
            let k = self.parse_identifier()?;
            (k, coll)
        } else {
            let k = self.parse_identifier()?;
            self.expect(TokenKind::From)?;
            let coll = crate::ast::QueryCollection::Explicit(self.parse_identifier()?);
            (k, coll)
        };

        let mut filter = None;
        let mut limit = None;
        let mut limit_param = None;
        let mut limit_span = None;
        let mut exact = None;
        let mut shard_key = None;

        while self.peek()?.kind != TokenKind::Eof && self.peek()?.kind != TokenKind::Semicolon {
            match self.peek()?.kind {
                TokenKind::Where if filter.is_none() => {
                    self.advance()?;
                    filter = Some(Box::new(self.parse_filter_expr()?));
                }
                TokenKind::Limit if limit.is_none() && limit_param.is_none() => {
                    self.advance()?;
                    if let Some((param, span)) = self.parse_placeholder_param()? {
                        limit_param = Some(param);
                        limit_span = Some(span);
                    } else {
                        limit = Some(self.parse_positive_u64("FACET LIMIT")?);
                    }
                }
                TokenKind::Limit => {
                    return Err(QqlError::parse(
                        "QQL-PARSE-DUPLICATE-CLAUSE",
                        "duplicate FACET LIMIT clause",
                        self.peek()?.span,
                    ));
                }
                TokenKind::Exact if exact.is_none() => {
                    self.advance()?;
                    match self.peek()?.kind {
                        TokenKind::True => {
                            self.advance()?;
                            exact = Some(true);
                        }
                        TokenKind::False => {
                            self.advance()?;
                            exact = Some(false);
                        }
                        _ => exact = Some(true),
                    }
                }
                TokenKind::Exact => {
                    return Err(QqlError::parse(
                        "QQL-PARSE-DUPLICATE-CLAUSE",
                        "duplicate FACET EXACT clause",
                        self.peek()?.span,
                    ));
                }
                TokenKind::Shard if shard_key.is_none() => {
                    self.advance()?;
                    shard_key = Some(self.parse_shard_key_atom()?);
                }
                TokenKind::With => {
                    let with_span = self.peek()?.span;
                    self.advance()?;
                    let opts = self.parse_config_block()?;
                    for (k, v) in &opts {
                        if k.eq_ignore_ascii_case("exact") {
                            if exact.is_some() {
                                return Err(QqlError::parse(
                                    "QQL-PARSE-DUPLICATE-CLAUSE",
                                    "duplicate FACET EXACT clause",
                                    with_span,
                                ));
                            }
                            match v {
                                crate::ast::Value::Bool(b) => exact = Some(*b),
                                _ => {
                                    return Err(QqlError::parse(
                                        "QQL-PARSE-FACET-CONFIG",
                                        "FACET 'exact' must be true or false",
                                        with_span,
                                    ));
                                }
                            }
                        } else if k.eq_ignore_ascii_case("limit") {
                            if limit.is_some() || limit_param.is_some() {
                                return Err(QqlError::parse(
                                    "QQL-PARSE-DUPLICATE-CLAUSE",
                                    "duplicate FACET LIMIT clause",
                                    with_span,
                                ));
                            }
                            match v {
                                crate::ast::Value::Int(i) if *i > 0 => {
                                    limit = Some(*i as u64);
                                }
                                _ => {
                                    return Err(QqlError::parse(
                                        "QQL-PARSE-FACET-CONFIG",
                                        "FACET limit must be a positive integer",
                                        with_span,
                                    ));
                                }
                            }
                        } else {
                            return Err(QqlError::parse(
                                "QQL-PARSE-FACET-CONFIG",
                                alloc::format!(
                                    "unknown FACET parameter '{k}'. Expected: exact, limit"
                                ),
                                with_span,
                            ));
                        }
                    }
                }
                _ => break,
            }
        }

        Ok(Stmt::Facet(Box::new(FacetStmt {
            key,
            collection,
            filter,
            limit,
            exact,
            shard_key,
            limit_param,
            limit_span,
        })))
    }
}