1use alloc::boxed::Box;
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12
13use spg_storage::{TsLexeme, TsQueryAst, Value};
14
15use super::{EvalContext, EvalError};
16
17pub(super) fn fts_ts_rank(args: &[Value<'static>]) -> Result<Value<'static>, EvalError> {
22 let (vec, query) = parse_rank_args("ts_rank", args)?;
23 match (vec, query) {
24 (None, _) | (_, None) => Ok(Value::Null),
25 (Some(v), Some(q)) => Ok(Value::Float(f64::from(crate::fts::ts_rank(&v, &q)))),
26 }
27}
28
29pub(super) fn fts_ts_rank_cd(args: &[Value<'static>]) -> Result<Value<'static>, EvalError> {
30 let (vec, query) = parse_rank_args("ts_rank_cd", args)?;
31 match (vec, query) {
32 (None, _) | (_, None) => Ok(Value::Null),
33 (Some(v), Some(q)) => Ok(Value::Float(f64::from(crate::fts::ts_rank_cd(&v, &q)))),
34 }
35}
36
37fn parse_rank_args(
38 name: &str,
39 args: &[Value<'static>],
40) -> Result<
41 (
42 Option<Vec<spg_storage::TsLexeme>>,
43 Option<spg_storage::TsQueryAst>,
44 ),
45 EvalError,
46> {
47 if args.len() != 2 {
48 return Err(EvalError::TypeMismatch {
49 detail: format!(
50 "{name}() takes 2 args in v7.12.2 (weights array + normalisation flag are v7.12.x carve-out), got {}",
51 args.len()
52 ),
53 });
54 }
55 let vec = match &args[0] {
56 Value::Null => None,
57 Value::TsVector(v) => Some(v.clone()),
58 other => {
59 return Err(EvalError::TypeMismatch {
60 detail: format!(
61 "{name}() first arg must be tsvector, got {:?}",
62 other.data_type()
63 ),
64 });
65 }
66 };
67 let query = match &args[1] {
68 Value::Null => None,
69 Value::TsQuery(q) => Some(q.clone()),
70 other => {
71 return Err(EvalError::TypeMismatch {
72 detail: format!(
73 "{name}() second arg must be tsquery, got {:?}",
74 other.data_type()
75 ),
76 });
77 }
78 };
79 Ok((vec, query))
80}
81
82pub(super) fn ts_match(l: Value, r: Value) -> Result<Value<'static>, EvalError> {
87 let (vec, query) = match (l, r) {
88 (Value::Null, _) | (_, Value::Null) => return Ok(Value::Null),
89 (Value::TsVector(v), Value::TsQuery(q)) => (v, q),
90 (Value::TsQuery(q), Value::TsVector(v)) => (v, q),
91 (l, r) => {
92 return Err(EvalError::TypeMismatch {
93 detail: format!(
94 "@@ requires (tsvector, tsquery), got ({:?}, {:?})",
95 l.data_type(),
96 r.data_type()
97 ),
98 });
99 }
100 };
101 Ok(Value::Bool(crate::fts::ts_query_matches(&vec, &query)))
102}
103
104pub(super) fn fts_to_tsvector(
109 args: &[Value<'static>],
110 ctx: &EvalContext<'_>,
111) -> Result<Value<'static>, EvalError> {
112 let (config, text) = parse_fts_args("to_tsvector", args, ctx)?;
113 match text {
114 None => Ok(Value::Null),
115 Some(t) => Ok(Value::TsVector(crate::fts::to_tsvector(config, &t))),
116 }
117}
118
119pub(super) fn fts_setweight(args: &[Value<'static>]) -> Result<Value<'static>, EvalError> {
122 let [vec_arg, weight_arg] = args else {
123 return Err(EvalError::TypeMismatch {
124 detail: alloc::format!("setweight expects 2 arguments, got {}", args.len()),
125 });
126 };
127 if matches!(vec_arg, Value::Null) || matches!(weight_arg, Value::Null) {
128 return Ok(Value::Null);
129 }
130 let Value::TsVector(lexemes) = vec_arg else {
131 return Err(EvalError::TypeMismatch {
132 detail: alloc::format!(
133 "setweight expects a tsvector, got {:?}",
134 vec_arg.data_type()
135 ),
136 });
137 };
138 let Value::Text(w) = weight_arg else {
139 return Err(EvalError::TypeMismatch {
140 detail: alloc::format!(
141 "setweight expects a weight letter, got {:?}",
142 weight_arg.data_type()
143 ),
144 });
145 };
146 let weight = match w.to_ascii_uppercase().as_str() {
147 "A" => 3,
148 "B" => 2,
149 "C" => 1,
150 "D" => 0,
151 other => {
152 return Err(EvalError::TypeMismatch {
153 detail: alloc::format!("unrecognized weight: {other:?} (expected A, B, C or D)"),
154 });
155 }
156 };
157 let mut out = lexemes.clone();
158 for lex in &mut out {
159 lex.weight = weight;
160 }
161 Ok(Value::TsVector(out))
162}
163
164pub(super) fn fts_plainto_tsquery(
165 args: &[Value<'static>],
166 ctx: &EvalContext<'_>,
167) -> Result<Value<'static>, EvalError> {
168 let (config, text) = parse_fts_args("plainto_tsquery", args, ctx)?;
169 match text {
170 None => Ok(Value::Null),
171 Some(t) => Ok(Value::TsQuery(crate::fts::plainto_tsquery(config, &t))),
172 }
173}
174
175pub(super) fn fts_phraseto_tsquery(
176 args: &[Value<'static>],
177 ctx: &EvalContext<'_>,
178) -> Result<Value<'static>, EvalError> {
179 let (config, text) = parse_fts_args("phraseto_tsquery", args, ctx)?;
180 match text {
181 None => Ok(Value::Null),
182 Some(t) => Ok(Value::TsQuery(crate::fts::phraseto_tsquery(config, &t))),
183 }
184}
185
186pub(super) fn fts_websearch_to_tsquery(
187 args: &[Value<'static>],
188 ctx: &EvalContext<'_>,
189) -> Result<Value<'static>, EvalError> {
190 let (config, text) = parse_fts_args("websearch_to_tsquery", args, ctx)?;
191 match text {
192 None => Ok(Value::Null),
193 Some(t) => Ok(Value::TsQuery(crate::fts::websearch_to_tsquery(config, &t))),
194 }
195}
196
197pub(super) fn fts_to_tsquery(
198 args: &[Value<'static>],
199 ctx: &EvalContext<'_>,
200) -> Result<Value<'static>, EvalError> {
201 let (config, text) = parse_fts_args("to_tsquery", args, ctx)?;
202 match text {
203 None => Ok(Value::Null),
204 Some(t) => Ok(Value::TsQuery(crate::fts::to_tsquery(config, &t)?)),
205 }
206}
207
208fn parse_fts_args(
213 name: &str,
214 args: &[Value<'static>],
215 ctx: &EvalContext<'_>,
216) -> Result<(crate::fts::TsConfig, Option<String>), EvalError> {
217 let (config_arg, text_arg) = match args {
218 [t] => (None, t),
219 [c, t] => (Some(c), t),
220 _ => {
221 return Err(EvalError::TypeMismatch {
222 detail: format!("{name}() takes 1 or 2 args, got {}", args.len()),
223 });
224 }
225 };
226 let config = match config_arg {
227 None => match ctx.default_text_search_config {
228 Some(name_str) => crate::fts::TsConfig::from_name(name_str).ok_or_else(|| {
229 EvalError::TypeMismatch {
230 detail: format!(
231 "text search config not implemented: {name_str:?} (supported: simple, english)"
232 ),
233 }
234 })?,
235 None => crate::fts::TsConfig::Simple,
236 },
237 Some(Value::Null) => return Ok((crate::fts::TsConfig::Simple, None)),
238 Some(Value::Text(name_str)) => crate::fts::TsConfig::from_name(name_str).ok_or_else(|| {
239 EvalError::TypeMismatch {
240 detail: format!(
241 "text search config not implemented: {name_str:?} (supported: simple, english)"
242 ),
243 }
244 })?,
245 Some(other) => {
246 return Err(EvalError::TypeMismatch {
247 detail: format!(
248 "{name}() config arg must be text, got {:?}",
249 other.data_type()
250 ),
251 });
252 }
253 };
254 let text = match text_arg {
255 Value::Null => None,
256 Value::Text(s) => Some(s.to_string()),
257 other => {
258 return Err(EvalError::TypeMismatch {
259 detail: format!(
260 "{name}() text arg must be text, got {:?}",
261 other.data_type()
262 ),
263 });
264 }
265 };
266 Ok((config, text))
267}
268
269pub fn format_tsvector(lexs: &[TsLexeme]) -> String {
275 let mut out = String::with_capacity(lexs.len() * 12);
276 for (i, l) in lexs.iter().enumerate() {
277 if i > 0 {
278 out.push(' ');
279 }
280 out.push('\'');
281 for c in l.word.chars() {
282 if c == '\'' {
283 out.push('\'');
284 }
285 out.push(c);
286 }
287 out.push('\'');
288 if !l.positions.is_empty() {
289 for (pi, p) in l.positions.iter().enumerate() {
290 out.push(if pi == 0 { ':' } else { ',' });
291 out.push_str(&p.to_string());
292 }
293 match l.weight {
298 3 => out.push('A'),
299 2 => out.push('B'),
300 1 => out.push('C'),
301 _ => {}
302 }
303 }
304 }
305 out
306}
307
308pub fn format_tsquery(ast: &TsQueryAst) -> String {
311 fn go(ast: &TsQueryAst, parent_prec: u8, out: &mut String) {
312 let (own_prec, write_self): (u8, &dyn Fn(&mut String)) = match ast {
314 TsQueryAst::Or(_, _) => (1, &|_| {}),
315 TsQueryAst::And(_, _) | TsQueryAst::Phrase { .. } => (2, &|_| {}),
316 TsQueryAst::Not(_) => (3, &|_| {}),
317 TsQueryAst::Term { .. } => (4, &|_| {}),
318 };
319 let need_parens = own_prec < parent_prec;
320 if need_parens {
321 out.push('(');
322 }
323 match ast {
324 TsQueryAst::Term { word, .. } => {
325 out.push('\'');
326 for c in word.chars() {
327 if c == '\'' {
328 out.push('\'');
329 }
330 out.push(c);
331 }
332 out.push('\'');
333 }
334 TsQueryAst::And(a, b) => {
335 go(a, own_prec, out);
336 out.push_str(" & ");
337 go(b, own_prec, out);
338 }
339 TsQueryAst::Or(a, b) => {
340 go(a, own_prec, out);
341 out.push_str(" | ");
342 go(b, own_prec, out);
343 }
344 TsQueryAst::Not(x) => {
345 out.push('!');
346 go(x, own_prec, out);
347 }
348 TsQueryAst::Phrase {
349 left,
350 right,
351 distance,
352 } => {
353 go(left, own_prec, out);
354 out.push_str(&alloc::format!(" <{distance}> "));
355 go(right, own_prec, out);
356 }
357 }
358 write_self(out);
359 if need_parens {
360 out.push(')');
361 }
362 }
363 let mut out = String::new();
364 go(ast, 0, &mut out);
365 out
366}
367
368pub fn decode_tsvector_external(s: &str) -> Result<Vec<TsLexeme>, EvalError> {
377 let mut out: Vec<TsLexeme> = Vec::new();
378 let mut i = 0;
379 let bytes = s.as_bytes();
380 while i < bytes.len() {
381 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
382 i += 1;
383 }
384 if i >= bytes.len() {
385 break;
386 }
387 let word = if bytes[i] == b'\'' {
390 i += 1;
391 let mut w = String::new();
392 loop {
393 if i >= bytes.len() {
394 return Err(EvalError::TypeMismatch {
395 detail: "tsvector literal: unterminated quoted lexeme".into(),
396 });
397 }
398 let b = bytes[i];
399 if b == b'\'' {
400 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
401 w.push('\'');
402 i += 2;
403 } else {
404 i += 1;
405 break;
406 }
407 } else {
408 w.push(b as char);
409 i += 1;
410 }
411 }
412 w
413 } else {
414 let start = i;
416 while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b':' {
417 i += 1;
418 }
419 core::str::from_utf8(&bytes[start..i])
420 .map_err(|_| EvalError::TypeMismatch {
421 detail: "tsvector literal: non-UTF-8 lexeme".into(),
422 })?
423 .to_string()
424 };
425 if word.is_empty() {
426 return Err(EvalError::TypeMismatch {
427 detail: "tsvector literal: empty lexeme".into(),
428 });
429 }
430 let mut positions: Vec<u16> = Vec::new();
433 let mut weight: u8 = 0;
434 if i < bytes.len() && bytes[i] == b':' {
435 i += 1;
436 loop {
437 let start = i;
438 while i < bytes.len() && bytes[i].is_ascii_digit() {
439 i += 1;
440 }
441 if start == i {
442 return Err(EvalError::TypeMismatch {
443 detail: "tsvector literal: expected digit after ':'".into(),
444 });
445 }
446 let num: u16 = core::str::from_utf8(&bytes[start..i])
447 .expect("ascii digits")
448 .parse()
449 .map_err(|_| EvalError::TypeMismatch {
450 detail: alloc::format!(
451 "tsvector literal: position {} overflows u16",
452 core::str::from_utf8(&bytes[start..i]).unwrap_or("?")
453 ),
454 })?;
455 positions.push(num);
456 if i < bytes.len() {
457 let w = bytes[i];
458 if matches!(w, b'A' | b'B' | b'C' | b'D') {
459 weight = match w {
460 b'A' => 3,
461 b'B' => 2,
462 b'C' => 1,
463 _ => 0,
464 };
465 i += 1;
466 }
467 }
468 if i < bytes.len() && bytes[i] == b',' {
469 i += 1;
470 continue;
471 }
472 break;
473 }
474 }
475 positions.sort_unstable();
476 positions.dedup();
477 match out.binary_search_by(|l| l.word.as_str().cmp(word.as_str())) {
480 Ok(idx) => {
481 for p in positions {
482 if !out[idx].positions.contains(&p) {
483 out[idx].positions.push(p);
484 }
485 }
486 out[idx].positions.sort_unstable();
487 if weight != 0 {
488 out[idx].weight = weight;
489 }
490 }
491 Err(idx) => {
492 out.insert(
493 idx,
494 TsLexeme {
495 word,
496 positions,
497 weight,
498 },
499 );
500 }
501 }
502 }
503 Ok(out)
504}
505
506pub fn decode_tsquery_external(s: &str) -> Result<TsQueryAst, EvalError> {
512 let mut p = TsQueryParser {
513 bytes: s.as_bytes(),
514 pos: 0,
515 };
516 p.skip_ws();
517 if p.pos >= p.bytes.len() {
518 return Err(EvalError::TypeMismatch {
519 detail: "tsquery literal: empty".into(),
520 });
521 }
522 let ast = p.parse_or()?;
523 p.skip_ws();
524 if p.pos < p.bytes.len() {
525 return Err(EvalError::TypeMismatch {
526 detail: alloc::format!("tsquery literal: trailing garbage at offset {}", p.pos),
527 });
528 }
529 Ok(ast)
530}
531
532struct TsQueryParser<'a> {
533 bytes: &'a [u8],
534 pos: usize,
535}
536
537impl<'a> TsQueryParser<'a> {
538 fn skip_ws(&mut self) {
539 while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_whitespace() {
540 self.pos += 1;
541 }
542 }
543 fn peek(&self) -> Option<u8> {
544 self.bytes.get(self.pos).copied()
545 }
546 fn parse_or(&mut self) -> Result<TsQueryAst, EvalError> {
547 let mut lhs = self.parse_and()?;
548 loop {
549 self.skip_ws();
550 if self.peek() != Some(b'|') {
551 return Ok(lhs);
552 }
553 self.pos += 1;
554 let rhs = self.parse_and()?;
555 lhs = TsQueryAst::Or(Box::new(lhs), Box::new(rhs));
556 }
557 }
558 fn parse_and(&mut self) -> Result<TsQueryAst, EvalError> {
559 let mut lhs = self.parse_unary()?;
560 loop {
561 self.skip_ws();
562 match self.peek() {
563 Some(b'&') => {
564 self.pos += 1;
565 let rhs = self.parse_unary()?;
566 lhs = TsQueryAst::And(Box::new(lhs), Box::new(rhs));
567 }
568 Some(b'<') => {
569 self.pos += 1;
571 let start = self.pos;
572 while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_digit() {
573 self.pos += 1;
574 }
575 if start == self.pos || self.peek() != Some(b'>') {
576 return Err(EvalError::TypeMismatch {
577 detail: "tsquery literal: malformed <N> phrase operator".into(),
578 });
579 }
580 let n: u16 = core::str::from_utf8(&self.bytes[start..self.pos])
581 .expect("ascii digits")
582 .parse()
583 .map_err(|_| EvalError::TypeMismatch {
584 detail: "tsquery literal: phrase distance overflows u16".into(),
585 })?;
586 self.pos += 1; let rhs = self.parse_unary()?;
588 lhs = TsQueryAst::Phrase {
589 left: Box::new(lhs),
590 right: Box::new(rhs),
591 distance: n,
592 };
593 }
594 _ => return Ok(lhs),
595 }
596 }
597 }
598 fn parse_unary(&mut self) -> Result<TsQueryAst, EvalError> {
599 self.skip_ws();
600 if self.peek() == Some(b'!') {
601 self.pos += 1;
602 let inner = self.parse_unary()?;
603 return Ok(TsQueryAst::Not(Box::new(inner)));
604 }
605 self.parse_atom()
606 }
607 fn parse_atom(&mut self) -> Result<TsQueryAst, EvalError> {
608 self.skip_ws();
609 match self.peek() {
610 Some(b'(') => {
611 self.pos += 1;
612 let inner = self.parse_or()?;
613 self.skip_ws();
614 if self.peek() != Some(b')') {
615 return Err(EvalError::TypeMismatch {
616 detail: "tsquery literal: missing ')'".into(),
617 });
618 }
619 self.pos += 1;
620 Ok(inner)
621 }
622 Some(b'\'') => {
623 self.pos += 1;
624 let mut w = String::new();
625 loop {
626 match self.peek() {
627 None => {
628 return Err(EvalError::TypeMismatch {
629 detail: "tsquery literal: unterminated quoted lexeme".into(),
630 });
631 }
632 Some(b'\'') => {
633 if self.bytes.get(self.pos + 1) == Some(&b'\'') {
634 w.push('\'');
635 self.pos += 2;
636 } else {
637 self.pos += 1;
638 break;
639 }
640 }
641 Some(b) => {
642 w.push(b as char);
643 self.pos += 1;
644 }
645 }
646 }
647 self.skip_weight_suffix();
650 Ok(TsQueryAst::Term {
651 word: w,
652 weight_mask: 0,
653 })
654 }
655 Some(b) if b.is_ascii_alphanumeric() || b == b'_' => {
656 let start = self.pos;
657 while self.pos < self.bytes.len() {
658 let c = self.bytes[self.pos];
659 if c.is_ascii_alphanumeric() || c == b'_' {
660 self.pos += 1;
661 } else {
662 break;
663 }
664 }
665 let w = core::str::from_utf8(&self.bytes[start..self.pos])
666 .map_err(|_| EvalError::TypeMismatch {
667 detail: "tsquery literal: non-UTF-8 lexeme".into(),
668 })?
669 .to_string();
670 self.skip_weight_suffix();
671 Ok(TsQueryAst::Term {
672 word: w,
673 weight_mask: 0,
674 })
675 }
676 Some(b) => Err(EvalError::TypeMismatch {
677 detail: alloc::format!(
678 "tsquery literal: unexpected byte {:?} at offset {}",
679 b as char,
680 self.pos
681 ),
682 }),
683 None => Err(EvalError::TypeMismatch {
684 detail: "tsquery literal: expected term".into(),
685 }),
686 }
687 }
688 fn skip_weight_suffix(&mut self) {
689 if self.peek() != Some(b':') {
690 return;
691 }
692 self.pos += 1;
693 while let Some(b) = self.peek() {
694 if matches!(
695 b,
696 b'A' | b'B' | b'C' | b'D' | b'a' | b'b' | b'c' | b'd' | b'*'
697 ) || b.is_ascii_digit()
698 {
699 self.pos += 1;
700 } else {
701 break;
702 }
703 }
704 }
705}
706
707pub(super) fn tsvector_concat(
708 l: &[spg_storage::TsLexeme],
709 r: &[spg_storage::TsLexeme],
710) -> Value<'static> {
711 let shift = l
712 .iter()
713 .flat_map(|x| x.positions.iter().copied())
714 .max()
715 .unwrap_or(0);
716 let mut out: Vec<spg_storage::TsLexeme> = l.to_vec();
717 for lex in r {
718 let shifted: Vec<u16> = lex
719 .positions
720 .iter()
721 .map(|p| p.saturating_add(shift))
722 .collect();
723 if let Some(existing) = out.iter_mut().find(|x| x.word == lex.word) {
724 existing.positions.extend(shifted);
725 existing.positions.sort_unstable();
726 existing.weight = existing.weight.max(lex.weight);
727 } else {
728 out.push(spg_storage::TsLexeme {
729 word: lex.word.clone(),
730 positions: shifted,
731 weight: lex.weight,
732 });
733 }
734 }
735 out.sort_by(|a, b| a.word.cmp(&b.word));
736 Value::TsVector(out)
737}