1use crate::error::{Result, SQLError};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum FTSTokenType {
40 Term,
41 Phrase,
42 Vector,
43 And,
44 Or,
45 Not,
46 LParen,
47 RParen,
48 Colon,
49 Eof,
50}
51
52#[derive(Debug, Clone)]
53pub struct FTSToken {
54 pub kind: FTSTokenType,
55 pub value: String,
56 pub pos: usize,
57}
58
59pub fn tokenize(source: &str) -> Result<Vec<FTSToken>> {
60 let mut tokens: Vec<FTSToken> = Vec::new();
61 let bytes: Vec<char> = source.chars().collect();
62 let n = bytes.len();
63 let mut i = 0_usize;
64
65 while i < n {
66 let ch = bytes[i];
67 if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
68 i += 1;
69 continue;
70 }
71 match ch {
72 '(' => {
73 tokens.push(FTSToken {
74 kind: FTSTokenType::LParen,
75 value: "(".into(),
76 pos: i,
77 });
78 i += 1;
79 continue;
80 }
81 ')' => {
82 tokens.push(FTSToken {
83 kind: FTSTokenType::RParen,
84 value: ")".into(),
85 pos: i,
86 });
87 i += 1;
88 continue;
89 }
90 ':' => {
91 tokens.push(FTSToken {
92 kind: FTSTokenType::Colon,
93 value: ":".into(),
94 pos: i,
95 });
96 i += 1;
97 continue;
98 }
99 '"' => {
100 let start = i;
101 i += 1;
102 let body_start = i;
103 while i < n && bytes[i] != '"' {
104 i += 1;
105 }
106 if i >= n {
107 return Err(SQLError::TypeMismatch(format!(
108 "Unterminated quoted phrase starting at position {start}"
109 )));
110 }
111 let phrase: String = bytes[body_start..i].iter().collect();
112 tokens.push(FTSToken {
113 kind: FTSTokenType::Phrase,
114 value: phrase,
115 pos: start,
116 });
117 i += 1;
118 continue;
119 }
120 '[' => {
121 let start = i;
122 i += 1;
123 let body_start = i;
124 while i < n && bytes[i] != ']' {
125 i += 1;
126 }
127 if i >= n {
128 return Err(SQLError::TypeMismatch(format!(
129 "Unterminated vector literal starting at position {start}"
130 )));
131 }
132 let content: String = bytes[body_start..i].iter().collect();
133 tokens.push(FTSToken {
134 kind: FTSTokenType::Vector,
135 value: content,
136 pos: start,
137 });
138 i += 1;
139 continue;
140 }
141 _ => {}
142 }
143 if is_word_char(ch) {
144 let start = i;
145 while i < n && is_word_char(bytes[i]) {
146 i += 1;
147 }
148 let word: String = bytes[start..i].iter().collect();
149 let lower = word.to_ascii_lowercase();
150 let kind = match lower.as_str() {
151 "and" => FTSTokenType::And,
152 "or" => FTSTokenType::Or,
153 "not" => FTSTokenType::Not,
154 _ => FTSTokenType::Term,
155 };
156 tokens.push(FTSToken {
157 kind,
158 value: word,
159 pos: start,
160 });
161 continue;
162 }
163 return Err(SQLError::TypeMismatch(format!(
164 "Unexpected character {ch:?} at position {i}"
165 )));
166 }
167
168 tokens.push(FTSToken {
169 kind: FTSTokenType::Eof,
170 value: String::new(),
171 pos: n,
172 });
173 Ok(tokens)
174}
175
176fn is_word_char(ch: char) -> bool {
177 !matches!(
178 ch,
179 ' ' | '\t' | '\n' | '\r' | '(' | ')' | ':' | '"' | '[' | ']'
180 )
181}
182
183#[derive(Debug, Clone)]
188pub enum FTSNode {
189 Term {
190 field: Option<String>,
191 term: String,
192 },
193 Phrase {
194 field: Option<String>,
195 phrase: String,
196 },
197 Vector {
198 field: Option<String>,
199 values: Vec<f32>,
200 },
201 And(Box<FTSNode>, Box<FTSNode>),
202 Or(Box<FTSNode>, Box<FTSNode>),
203 Not(Box<FTSNode>),
204}
205
206pub struct FTSParser {
211 tokens: Vec<FTSToken>,
212 pos: usize,
213}
214
215impl FTSParser {
216 pub fn new(tokens: Vec<FTSToken>) -> Self {
217 Self { tokens, pos: 0 }
218 }
219
220 pub fn parse(mut self) -> Result<FTSNode> {
221 if self.peek().kind == FTSTokenType::Eof {
222 return Err(SQLError::TypeMismatch("Empty query".into()));
223 }
224 let node = self.or_expr()?;
225 if self.peek().kind != FTSTokenType::Eof {
226 let tok = self.peek();
227 return Err(SQLError::TypeMismatch(format!(
228 "Unexpected token {:?} at position {}",
229 tok.value, tok.pos
230 )));
231 }
232 Ok(node)
233 }
234
235 fn peek(&self) -> &FTSToken {
236 &self.tokens[self.pos]
237 }
238
239 fn advance(&mut self) -> FTSToken {
240 let tok = self.tokens[self.pos].clone();
241 self.pos += 1;
242 tok
243 }
244
245 fn expect(&mut self, kind: FTSTokenType) -> Result<FTSToken> {
246 let tok = self.advance();
247 if tok.kind != kind {
248 return Err(SQLError::TypeMismatch(format!(
249 "Expected {:?}, got {:?} ({:?}) at position {}",
250 kind, tok.kind, tok.value, tok.pos
251 )));
252 }
253 Ok(tok)
254 }
255
256 fn or_expr(&mut self) -> Result<FTSNode> {
257 let mut left = self.and_expr()?;
258 while self.peek().kind == FTSTokenType::Or {
259 self.advance();
260 let right = self.and_expr()?;
261 left = FTSNode::Or(Box::new(left), Box::new(right));
262 }
263 Ok(left)
264 }
265
266 fn and_expr(&mut self) -> Result<FTSNode> {
267 let mut left = self.unary()?;
268 loop {
269 let kind = self.peek().kind;
270 if kind == FTSTokenType::And {
271 self.advance();
272 let right = self.unary()?;
273 left = FTSNode::And(Box::new(left), Box::new(right));
274 } else if matches!(
275 kind,
276 FTSTokenType::Term
277 | FTSTokenType::Phrase
278 | FTSTokenType::Vector
279 | FTSTokenType::LParen
280 | FTSTokenType::Not
281 ) {
282 let right = self.unary()?;
283 left = FTSNode::And(Box::new(left), Box::new(right));
284 } else {
285 break;
286 }
287 }
288 Ok(left)
289 }
290
291 fn unary(&mut self) -> Result<FTSNode> {
292 if self.peek().kind == FTSTokenType::Not {
293 self.advance();
294 let operand = self.unary()?;
295 return Ok(FTSNode::Not(Box::new(operand)));
296 }
297 self.primary()
298 }
299
300 fn primary(&mut self) -> Result<FTSNode> {
301 let kind = self.peek().kind;
302 if kind == FTSTokenType::LParen {
303 self.advance();
304 let node = self.or_expr()?;
305 self.expect(FTSTokenType::RParen)?;
306 return Ok(node);
307 }
308 if kind == FTSTokenType::Phrase {
309 let tok = self.advance();
310 return Ok(FTSNode::Phrase {
311 field: None,
312 phrase: tok.value,
313 });
314 }
315 if kind == FTSTokenType::Vector {
316 let tok = self.advance();
317 return Ok(FTSNode::Vector {
318 field: None,
319 values: parse_vector_literal(&tok.value)?,
320 });
321 }
322 if kind == FTSTokenType::Term {
323 let tok = self.advance();
324 if self.peek().kind == FTSTokenType::Colon {
325 self.advance();
326 let next = self.peek().clone();
327 match next.kind {
328 FTSTokenType::Phrase => {
329 self.advance();
330 return Ok(FTSNode::Phrase {
331 field: Some(tok.value),
332 phrase: next.value,
333 });
334 }
335 FTSTokenType::Vector => {
336 self.advance();
337 return Ok(FTSNode::Vector {
338 field: Some(tok.value),
339 values: parse_vector_literal(&next.value)?,
340 });
341 }
342 FTSTokenType::Term => {
343 self.advance();
344 return Ok(FTSNode::Term {
345 field: Some(tok.value),
346 term: next.value,
347 });
348 }
349 other => {
350 return Err(SQLError::TypeMismatch(format!(
351 "Expected term, phrase, or vector after ':', got {other:?} at position {}",
352 next.pos
353 )));
354 }
355 }
356 }
357 return Ok(FTSNode::Term {
358 field: None,
359 term: tok.value,
360 });
361 }
362 let tok = self.peek().clone();
363 Err(SQLError::TypeMismatch(format!(
364 "Unexpected token {:?} ({:?}) at position {}",
365 tok.kind, tok.value, tok.pos
366 )))
367 }
368}
369
370fn parse_vector_literal(content: &str) -> Result<Vec<f32>> {
371 let trimmed = content.trim();
372 if trimmed.is_empty() {
373 return Err(SQLError::TypeMismatch("Empty vector literal".into()));
374 }
375 trimmed
376 .split(',')
377 .map(|s| {
378 s.trim()
379 .parse::<f32>()
380 .map_err(|e| SQLError::TypeMismatch(format!("Malformed vector literal: {e}")))
381 })
382 .collect()
383}
384
385pub fn parse_query_string(query_string: &str) -> Result<FTSNode> {
388 let tokens = tokenize(query_string)?;
389 FTSParser::new(tokens).parse()
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn tokenize_terms_and_phrase() {
398 let tokens = tokenize(r#"hello "world today" foo"#).unwrap();
399 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
400 assert_eq!(
401 kinds,
402 vec![
403 FTSTokenType::Term,
404 FTSTokenType::Phrase,
405 FTSTokenType::Term,
406 FTSTokenType::Eof,
407 ]
408 );
409 assert_eq!(tokens[1].value, "world today");
410 }
411
412 #[test]
413 fn tokenize_vector() {
414 let tokens = tokenize("[0.1, 0.2, 0.3]").unwrap();
415 assert_eq!(tokens[0].kind, FTSTokenType::Vector);
416 assert_eq!(tokens[0].value, "0.1, 0.2, 0.3");
417 }
418
419 #[test]
420 fn parse_implicit_and() {
421 let tokens = tokenize("rust ferris").unwrap();
422 let ast = FTSParser::new(tokens).parse().unwrap();
423 assert!(matches!(ast, FTSNode::And(_, _)));
424 }
425
426 #[test]
427 fn parse_field_qualified_term() {
428 let tokens = tokenize("title:rust").unwrap();
429 let ast = FTSParser::new(tokens).parse().unwrap();
430 match ast {
431 FTSNode::Term { field, term } => {
432 assert_eq!(field.as_deref(), Some("title"));
433 assert_eq!(term, "rust");
434 }
435 _ => panic!("expected Term"),
436 }
437 }
438
439 #[test]
440 fn parse_not_precedence() {
441 let tokens = tokenize("NOT foo OR bar").unwrap();
442 let ast = FTSParser::new(tokens).parse().unwrap();
443 match ast {
445 FTSNode::Or(left, _) => match *left {
446 FTSNode::Not(_) => {}
447 _ => panic!("expected NOT on left"),
448 },
449 _ => panic!("expected OR"),
450 }
451 }
452
453 #[test]
454 fn parse_query_string_stops_at_syntax_ast() {
455 let ast = parse_query_string("body:rust OR embedding:[1, 0]").unwrap();
456 assert!(matches!(ast, FTSNode::Or(_, _)));
457 }
458}