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