Skip to main content

qql_core/parser/
mod.rs

1pub(crate) mod alter_drop_show;
2pub(crate) mod config_parsers;
3pub(crate) mod config_validation;
4pub(crate) mod create;
5pub(crate) mod filter;
6pub(crate) mod formula;
7pub(crate) mod helpers;
8pub(crate) mod point_ops;
9pub(crate) mod query;
10mod syntax;
11pub(crate) mod r#update;
12pub(crate) mod upsert;
13pub(crate) mod with_clause;
14
15use crate::ast::Stmt;
16use crate::error::QqlError;
17use crate::lexer::Lexer;
18use crate::token::{Token, TokenKind};
19use alloc::string::String;
20use alloc::vec::Vec;
21pub use config_validation::{
22    check_deleted_threshold, config_bool, config_float_range, config_has_key,
23    config_max_optimization_threads, config_non_negative_u64, config_positive_u64, config_value,
24    merge_collection_config, validate_hnsw_value, validate_index_options,
25    validate_optimizers_value, validate_params_value, validate_vectors_value,
26};
27
28/// Canonical QQL parser facade.
29///
30/// Syntax acceptance is generated from `language/v1/grammar.pest`. The
31/// private [`AstLowerer`] converts accepted source into the typed QQL AST.
32pub struct Parser;
33
34pub(crate) struct AstLowerer<'a> {
35    pub input: &'a str,
36    tokens: Vec<Token<'a>>,
37    index: usize,
38}
39
40/// Hard upper bound for one parsed script. Callers that need larger imports
41/// should split them into bounded batches before parsing.
42pub const MAX_STATEMENTS: usize = 256;
43
44pub fn ascii_equal(s: &str, upper: &str) -> bool {
45    if s.len() != upper.len() {
46        return false;
47    }
48    s.as_bytes()
49        .iter()
50        .zip(upper.as_bytes().iter())
51        .all(|(a, b)| a.to_ascii_uppercase() == *b)
52}
53
54pub fn ascii_equal_lower(s: &str, lower: &str) -> bool {
55    if s.len() != lower.len() {
56        return false;
57    }
58    s.as_bytes()
59        .iter()
60        .zip(lower.as_bytes().iter())
61        .all(|(a, b)| a.to_ascii_lowercase() == *b)
62}
63
64pub fn is_contextual_field_name(kind: TokenKind) -> bool {
65    matches!(
66        kind,
67        TokenKind::Offset
68            | TokenKind::Score
69            | TokenKind::Threshold
70            | TokenKind::Lookup
71            | TokenKind::Id
72            | TokenKind::Dense
73            | TokenKind::Sparse
74            | TokenKind::Vector
75            | TokenKind::By
76            | TokenKind::Count
77            | TokenKind::Clear
78            | TokenKind::Field
79            | TokenKind::Into
80    )
81}
82
83fn is_contextual_identifier(kind: TokenKind) -> bool {
84    matches!(
85        kind,
86        TokenKind::Offset
87            | TokenKind::Score
88            | TokenKind::Threshold
89            | TokenKind::Lookup
90            | TokenKind::Id
91            | TokenKind::Dense
92            | TokenKind::Sparse
93            | TokenKind::Vector
94            | TokenKind::Count
95            | TokenKind::Clear
96            | TokenKind::Field
97            | TokenKind::Into
98    )
99}
100
101impl Parser {
102    pub fn parse(input: &str) -> Result<Stmt, QqlError> {
103        let statement = AstLowerer::lower_statement(input)?;
104        syntax::validate_statement(input)?;
105        Ok(statement)
106    }
107
108    pub fn parse_all(input: &str) -> Result<Vec<Stmt>, QqlError> {
109        let statements = AstLowerer::lower_script(input)?;
110        syntax::validate_script(input)?;
111        Ok(statements)
112    }
113}
114
115impl<'a> AstLowerer<'a> {
116    fn new(input: &'a str, tokens: Vec<Token<'a>>) -> Self {
117        Self {
118            input,
119            tokens,
120            index: 0,
121        }
122    }
123
124    fn lower_statement(input: &'a str) -> Result<Stmt, QqlError> {
125        let tokens = Self::lex(input)?;
126        let mut parser = AstLowerer::new(input, tokens);
127        let stmt = parser.parse_stmt()?;
128        if parser.peek()?.kind == TokenKind::Semicolon {
129            parser.advance()?;
130        }
131        parser.expect_end()?;
132        Ok(stmt)
133    }
134
135    fn lower_script(input: &'a str) -> Result<Vec<Stmt>, QqlError> {
136        let tokens = Self::lex(input)?;
137        let mut parser = AstLowerer::new(input, tokens);
138        let mut statements = Vec::new();
139        if parser.peek()?.kind == TokenKind::Semicolon {
140            return Err(QqlError::parse(
141                "QQL-PARSE-EMPTY-STATEMENT",
142                "leading or empty statements are not allowed",
143                parser.peek()?.span,
144            ));
145        }
146
147        while parser.peek()?.kind != TokenKind::Eof {
148            if statements.len() >= MAX_STATEMENTS {
149                return Err(QqlError::parse(
150                    "QQL-PARSE-STATEMENT-LIMIT",
151                    alloc::format!("a script may contain at most {MAX_STATEMENTS} statements"),
152                    parser.peek()?.span,
153                ));
154            }
155            statements.push(parser.parse_stmt()?);
156            match parser.peek()?.kind {
157                TokenKind::Semicolon => {
158                    parser.advance()?;
159                    if parser.peek()?.kind == TokenKind::Semicolon {
160                        return Err(QqlError::parse(
161                            "QQL-PARSE-EMPTY-STATEMENT",
162                            "repeated semicolons are not allowed",
163                            parser.peek()?.span,
164                        ));
165                    }
166                }
167                TokenKind::Eof => break,
168                _ => {
169                    return Err(QqlError::parse(
170                        "QQL-PARSE-SEPARATOR",
171                        "multiple statements must be separated by a semicolon",
172                        parser.peek()?.span,
173                    ));
174                }
175            }
176        }
177        Ok(statements)
178    }
179
180    fn lex(input: &'a str) -> Result<Vec<Token<'a>>, QqlError> {
181        let lexer = Lexer::new(input);
182        let mut tokens = Vec::with_capacity(input.len() / 6 + 1);
183        for token_res in lexer {
184            tokens.push(token_res?);
185        }
186        Ok(tokens)
187    }
188
189    fn expect_end(&mut self) -> Result<(), QqlError> {
190        if self.index < self.tokens.len() {
191            let tok = self.tokens[self.index];
192            return Err(QqlError::parse(
193                "QQL-PARSE-TRAILING",
194                alloc::format!("unexpected trailing token '{}'", tok.text),
195                tok.span,
196            ));
197        }
198
199        Ok(())
200    }
201
202    pub fn parse_stmt(&mut self) -> Result<Stmt, QqlError> {
203        let tok = self.peek()?;
204        match tok.kind {
205            TokenKind::Create => self.parse_create(),
206            TokenKind::Alter => self.parse_alter(),
207            TokenKind::Drop => self.parse_drop(),
208            TokenKind::Show => self.parse_show(),
209            TokenKind::Upsert => self.parse_upsert(),
210            TokenKind::Scroll => self.parse_scroll(),
211            TokenKind::Query => self.parse_query(),
212            TokenKind::With => self.parse_query_with_cte(),
213            TokenKind::Delete => self.parse_delete(),
214            TokenKind::Clear => self.parse_clear(),
215            TokenKind::Update => self.parse_update(),
216            TokenKind::Count => self.parse_count(),
217            _ => Err(QqlError::parse(
218                "QQL-PARSE-STATEMENT",
219                alloc::format!("expected a QQL statement keyword, got '{}'", tok.text),
220                tok.span,
221            )),
222        }
223    }
224
225    // ── Token stream helpers ────────────────────────────────────
226
227    pub fn peek(&mut self) -> Result<Token<'a>, QqlError> {
228        if self.index < self.tokens.len() {
229            Ok(self.tokens[self.index])
230        } else {
231            Ok(Token::eof(self.input.len()))
232        }
233    }
234
235    pub fn peek_nth(&self, offset: usize) -> Token<'a> {
236        let idx = self.index + offset;
237        if idx < self.tokens.len() {
238            self.tokens[idx]
239        } else {
240            Token::eof(self.input.len())
241        }
242    }
243
244    pub fn advance(&mut self) -> Result<Token<'a>, QqlError> {
245        let tok = self.peek()?;
246        if self.index < self.tokens.len() {
247            self.index += 1;
248        }
249        Ok(tok)
250    }
251
252    pub fn expect(&mut self, kind: TokenKind) -> Result<Token<'a>, QqlError> {
253        let tok = self.peek()?;
254        if tok.kind != kind {
255            return Err(QqlError::parse(
256                "QQL-PARSE-EXPECTED",
257                alloc::format!("expected {} but got '{}'", kind, tok.text),
258                tok.span,
259            ));
260        }
261        self.advance()
262    }
263
264    // ── Identifier parsing ──────────────────────────────────────
265
266    pub fn parse_identifier_str(&mut self) -> Result<&'a str, QqlError> {
267        let tok = self.peek()?;
268        if tok.kind == TokenKind::Identifier
269            || tok.kind == TokenKind::String
270            || is_contextual_identifier(tok.kind)
271        {
272            self.advance()?;
273            Ok(tok.text)
274        } else {
275            Err(QqlError::parse(
276                "QQL-PARSE-IDENTIFIER",
277                alloc::format!("expected identifier or quoted name, got '{}'", tok.text),
278                tok.span,
279            ))
280        }
281    }
282
283    pub fn parse_identifier(&mut self) -> Result<String, QqlError> {
284        self.parse_identifier_str().map(String::from)
285    }
286
287    // ── Value parsing ───────────────────────────────────────────
288
289    pub fn parse_value(&mut self) -> Result<crate::ast::Value, QqlError> {
290        let tok = self.peek()?;
291        match tok.kind {
292            TokenKind::String => {
293                self.advance()?;
294                self.decode_string(tok).map(crate::ast::Value::Str)
295            }
296            TokenKind::Float => {
297                self.advance()?;
298                let v: f64 = tok.text.parse().map_err(|_| {
299                    QqlError::parse(
300                        "QQL-PARSE-FLOAT",
301                        alloc::format!("invalid float literal '{}'", tok.text),
302                        tok.span,
303                    )
304                })?;
305                Ok(crate::ast::Value::Float(v))
306            }
307            TokenKind::Integer => {
308                self.advance()?;
309                let v: i64 = tok.text.parse().map_err(|_| {
310                    QqlError::parse(
311                        "QQL-PARSE-INTEGER",
312                        alloc::format!("invalid integer literal '{}'", tok.text),
313                        tok.span,
314                    )
315                })?;
316                Ok(crate::ast::Value::Int(v))
317            }
318            TokenKind::Null => {
319                self.advance()?;
320                Ok(crate::ast::Value::Null)
321            }
322            TokenKind::Identifier => {
323                self.advance()?;
324                if ascii_equal(tok.text, "TRUE") {
325                    Ok(crate::ast::Value::Bool(true))
326                } else if ascii_equal(tok.text, "FALSE") {
327                    Ok(crate::ast::Value::Bool(false))
328                } else if ascii_equal(tok.text, "NULL") {
329                    Ok(crate::ast::Value::Null)
330                } else {
331                    Ok(crate::ast::Value::Str(tok.text.to_string()))
332                }
333            }
334            TokenKind::Lbrace => self
335                .parse_payload_dict()
336                .map(|items| crate::ast::Value::Dict(items.into_iter().collect())),
337            TokenKind::Lbracket => self.parse_list().map(crate::ast::Value::List),
338            _ => Err(QqlError::parse(
339                "QQL-PARSE-VALUE",
340                alloc::format!("unexpected value token '{}'", tok.text),
341                tok.span,
342            )),
343        }
344    }
345
346    fn decode_string(&self, token: Token<'a>) -> Result<String, QqlError> {
347        let first_byte = self
348            .input
349            .as_bytes()
350            .get(token.span.start)
351            .copied()
352            .unwrap_or(0);
353        let is_raw_or_backtick = first_byte == b'r' || first_byte == b'`';
354        if is_raw_or_backtick
355            || !(token.text.contains('\\') || first_byte == b'\'' && token.text.contains("''"))
356        {
357            return Ok(token.text.to_string());
358        }
359        let single_quoted = first_byte == b'\'';
360        let mut decoded = String::with_capacity(token.text.len());
361        let mut chars = token.text.chars().peekable();
362        while let Some(ch) = chars.next() {
363            if single_quoted && ch == '\'' && chars.peek() == Some(&'\'') {
364                chars.next();
365                decoded.push('\'');
366                continue;
367            }
368            if ch != '\\' {
369                decoded.push(ch);
370                continue;
371            }
372            let escaped = chars.next().ok_or_else(|| {
373                QqlError::parse(
374                    "QQL-PARSE-ESCAPE",
375                    "unterminated escape sequence",
376                    token.span,
377                )
378            })?;
379            decoded.push(match escaped {
380                'n' => '\n',
381                'r' => '\r',
382                't' => '\t',
383                '\\' => '\\',
384                '\'' => '\'',
385                '"' => '"',
386                '$' => '$',
387                _ => {
388                    return Err(QqlError::parse(
389                        "QQL-PARSE-ESCAPE",
390                        alloc::format!("unsupported escape sequence \\{}", escaped),
391                        token.span,
392                    ));
393                }
394            });
395        }
396        Ok(decoded)
397    }
398}