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<'_>]) -> Result<Value<'static>, EvalError> {
22 if args.iter().any(|a| matches!(a, Value::Null)) {
28 return Ok(Value::Null);
29 }
30 let (weights, vec, query, norm) = parse_rank_args("ts_rank", args)?;
31 match (vec, query) {
32 (None, _) | (_, None) => Ok(Value::Null),
33 (Some(v), Some(q)) => {
34 let r = crate::fts::apply_rank_norm(crate::fts::ts_rank(&weights, &v, &q), norm, &v);
37 Ok(Value::Real(r))
40 }
41 }
42}
43
44pub(super) fn fts_ts_rank_cd(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
45 if args.iter().any(|a| matches!(a, Value::Null)) {
47 return Ok(Value::Null);
48 }
49 let (weights, vec, query, norm) = parse_rank_args("ts_rank_cd", args)?;
50 if norm & 4 != 0 {
51 return Err(EvalError::TypeMismatch {
52 detail:
53 "ts_rank_cd(): normalization flag 4 (cover-extent distance) is not yet supported"
54 .into(),
55 });
56 }
57 match (vec, query) {
58 (None, _) | (_, None) => Ok(Value::Null),
59 (Some(v), Some(q)) => {
60 let r = crate::fts::apply_rank_norm(crate::fts::ts_rank_cd(&weights, &v, &q), norm, &v);
61 Ok(Value::Real(r))
62 }
63 }
64}
65type RankArgs = (
68 crate::fts::RankWeights,
69 Option<Vec<spg_storage::TsLexeme>>,
70 Option<spg_storage::TsQueryAst>,
71 i64,
72);
73
74fn parse_rank_args(name: &str, args: &[Value<'_>]) -> Result<RankArgs, EvalError> {
80 let mut rest = args;
82 let mut weights = crate::fts::DEFAULT_RANK_WEIGHTS;
83 if matches!(
84 rest.first(),
85 Some(
86 Value::FloatArray(_)
87 | Value::NumericArray(_)
88 | Value::IntArray(_)
89 | Value::SmallIntArray(_)
90 )
91 ) {
92 weights = parse_weight_array(name, &rest[0])?;
93 rest = &rest[1..];
94 } else if args.len() >= 3
95 && let Some(Value::Text(s)) = rest.first()
96 && s.trim_start().starts_with('{')
97 {
98 let inner = s.trim().trim_start_matches('{').trim_end_matches('}');
101 let parsed: Result<Vec<f64>, _> =
102 inner.split(',').map(|x| x.trim().parse::<f64>()).collect();
103 let vals = parsed.map_err(|_| EvalError::TypeMismatch {
104 detail: format!("{name}(): invalid weight array literal {s:?}"),
105 })?;
106 weights = parse_weight_array(
107 name,
108 &Value::FloatArray(vals.into_iter().map(Some).collect()),
109 )?;
110 rest = &rest[1..];
111 }
112 let norm = match rest.last() {
114 Some(Value::Int(n)) => Some(i64::from(*n)),
115 Some(Value::BigInt(n)) => Some(*n),
116 _ => None,
117 };
118 if norm.is_some() {
119 rest = &rest[..rest.len() - 1];
120 }
121 let norm = norm.unwrap_or(0);
122 if norm & !0x3F != 0 {
123 return Err(EvalError::TypeMismatch {
124 detail: format!("{name}(): unknown normalization flag bits in {norm}"),
125 });
126 }
127 if rest.len() != 2 {
128 return Err(EvalError::TypeMismatch {
129 detail: format!(
130 "{name}() takes (vec, query) optionally wrapped by a weight array and a norm flag"
131 ),
132 });
133 }
134 let vec = match &rest[0] {
135 Value::Null => None,
136 Value::TsVector(v) => Some(v.clone()),
137 other => {
138 return Err(EvalError::TypeMismatch {
139 detail: format!(
140 "{name}() vector arg must be tsvector, got {}",
141 crate::conversions::pg_type_name_for_error_opt(other.data_type())
142 ),
143 });
144 }
145 };
146 let query = match &rest[1] {
147 Value::Null => None,
148 Value::TsQuery(q) => Some(q.clone()),
149 other => {
150 return Err(EvalError::TypeMismatch {
151 detail: format!(
152 "{name}() query arg must be tsquery, got {}",
153 crate::conversions::pg_type_name_for_error_opt(other.data_type())
154 ),
155 });
156 }
157 };
158 Ok((weights, vec, query, norm))
159}
160
161fn parse_weight_array(name: &str, v: &Value<'_>) -> Result<crate::fts::RankWeights, EvalError> {
163 let vals: Vec<f32> = match v {
164 Value::FloatArray(a) => a.iter().map(|o| o.unwrap_or(0.0) as f32).collect(),
165 Value::IntArray(a) => a.iter().map(|o| o.unwrap_or(0) as f32).collect(),
166 Value::SmallIntArray(a) => a.iter().map(|o| f32::from(o.unwrap_or(0))).collect(),
167 Value::NumericArray(a) => a
168 .iter()
169 .map(|o| o.map_or(0.0, |(m, s)| (m as f64 / 10f64.powi(i32::from(s))) as f32))
170 .collect(),
171 _ => {
172 return Err(EvalError::TypeMismatch {
173 detail: format!("{name}() weight argument must be a numeric array"),
174 });
175 }
176 };
177 if vals.len() != 4 {
178 return Err(EvalError::TypeMismatch {
179 detail: format!(
180 "{name}() weight array must have 4 elements [D, C, B, A], got {}",
181 vals.len()
182 ),
183 });
184 }
185 Ok([vals[0], vals[1], vals[2], vals[3]])
186}
187
188pub(super) fn ts_match(l: Value, r: Value) -> Result<Value<'static>, EvalError> {
193 let (vec, query) = match (l, r) {
194 (Value::Null, _) | (_, Value::Null) => return Ok(Value::Null),
195 (Value::TsVector(v), Value::TsQuery(q)) => (v, q),
196 (Value::TsQuery(q), Value::TsVector(v)) => (v, q),
197 (Value::TsVector(v), Value::Text(q)) => {
202 (v, crate::eval::decode_tsquery_external(q.as_ref())?)
203 }
204 (Value::Text(q), Value::TsVector(v)) => {
205 (v, crate::eval::decode_tsquery_external(q.as_ref())?)
206 }
207 (l, r) => {
208 return Err(EvalError::TypeMismatch {
209 detail: format!(
210 "@@ requires (tsvector, tsquery), got ({:?}, {:?})",
211 l.data_type(),
212 r.data_type()
213 ),
214 });
215 }
216 };
217 Ok(Value::Bool(crate::fts::ts_query_matches(&vec, &query)))
218}
219
220pub(super) fn fts_to_tsvector(
225 args: &[Value<'_>],
226 ctx: &EvalContext<'_>,
227) -> Result<Value<'static>, EvalError> {
228 let (config, text) = parse_fts_args("to_tsvector", args, ctx)?;
229 match text {
230 None => Ok(Value::Null),
231 Some(t) => Ok(Value::TsVector(crate::fts::to_tsvector(config, &t))),
232 }
233}
234
235pub(super) fn fts_setweight(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
238 if args.len() != 2 && args.len() != 3 {
239 return Err(crate::eval::functions::wrong_arity("setweight", args));
240 }
241 let (vec_arg, weight_arg, only) = match args {
245 [v, w] => (v, w, None),
246 [v, w, l] => (v, w, Some(l)),
247 _ => {
248 return Err(EvalError::TypeMismatch {
249 detail: alloc::format!("setweight expects 2 or 3 arguments, got {}", args.len()),
250 });
251 }
252 };
253 if matches!(vec_arg, Value::Null) || matches!(weight_arg, Value::Null) {
254 return Ok(Value::Null);
255 }
256 let Value::TsVector(lexemes) = vec_arg else {
257 return Err(EvalError::TypeMismatch {
258 detail: alloc::format!(
259 "setweight expects a tsvector, got {}",
260 crate::conversions::pg_type_name_for_error_opt(vec_arg.data_type())
261 ),
262 });
263 };
264 let Value::Text(w) = weight_arg else {
265 return Err(EvalError::TypeMismatch {
266 detail: alloc::format!(
267 "setweight expects a weight letter, got {}",
268 crate::conversions::pg_type_name_for_error_opt(weight_arg.data_type())
269 ),
270 });
271 };
272 let weight = match w.to_ascii_uppercase().as_str() {
273 "A" => 3,
274 "B" => 2,
275 "C" => 1,
276 "D" => 0,
277 other => {
278 return Err(EvalError::TypeMismatch {
279 detail: alloc::format!("unrecognized weight: {other:?} (expected A, B, C or D)"),
280 });
281 }
282 };
283 let selected: Option<alloc::vec::Vec<String>> = match only {
286 None => None,
287 Some(Value::Null) => return Ok(Value::Null),
288 Some(v) => {
289 let t = crate::eval::value_to_text(v);
290 let inner = t.trim().trim_start_matches('{').trim_end_matches('}');
291 Some(
292 inner
293 .split(',')
294 .map(|x| x.trim().trim_matches('"').to_string())
295 .filter(|x| !x.is_empty())
296 .collect(),
297 )
298 }
299 };
300 let mut out = lexemes.clone();
301 for lex in &mut out {
302 let hit = selected
303 .as_ref()
304 .is_none_or(|names| names.iter().any(|n| *n == lex.word));
305 if hit {
306 lex.weight = weight;
307 }
308 }
309 Ok(Value::TsVector(out))
310}
311
312pub(super) fn fts_plainto_tsquery(
313 args: &[Value<'_>],
314 ctx: &EvalContext<'_>,
315) -> Result<Value<'static>, EvalError> {
316 let (config, text) = parse_fts_args("plainto_tsquery", args, ctx)?;
317 match text {
318 None => Ok(Value::Null),
319 Some(t) => Ok(Value::TsQuery(crate::fts::plainto_tsquery(config, &t))),
320 }
321}
322
323pub(super) fn fts_phraseto_tsquery(
324 args: &[Value<'_>],
325 ctx: &EvalContext<'_>,
326) -> Result<Value<'static>, EvalError> {
327 let (config, text) = parse_fts_args("phraseto_tsquery", args, ctx)?;
328 match text {
329 None => Ok(Value::Null),
330 Some(t) => Ok(Value::TsQuery(crate::fts::phraseto_tsquery(config, &t))),
331 }
332}
333
334pub(super) fn fts_websearch_to_tsquery(
335 args: &[Value<'_>],
336 ctx: &EvalContext<'_>,
337) -> Result<Value<'static>, EvalError> {
338 let (config, text) = parse_fts_args("websearch_to_tsquery", args, ctx)?;
339 match text {
340 None => Ok(Value::Null),
341 Some(t) => Ok(Value::TsQuery(crate::fts::websearch_to_tsquery(config, &t))),
342 }
343}
344
345pub(super) fn fts_to_tsquery(
346 args: &[Value<'_>],
347 ctx: &EvalContext<'_>,
348) -> Result<Value<'static>, EvalError> {
349 let (config, text) = parse_fts_args("to_tsquery", args, ctx)?;
350 match text {
351 None => Ok(Value::Null),
352 Some(t) => Ok(Value::TsQuery(crate::fts::to_tsquery(config, &t)?)),
353 }
354}
355
356fn parse_fts_args(
361 name: &str,
362 args: &[Value<'_>],
363 ctx: &EvalContext<'_>,
364) -> Result<(crate::fts::TsConfig, Option<String>), EvalError> {
365 if args.len() != 1 && args.len() != 2 {
366 return Err(EvalError::WrongArity {
367 name: alloc::string::String::from(name),
368 types: crate::eval::functions::arg_type_list(args),
369 });
370 }
371 let (config_arg, text_arg) = match args {
372 [t] => (None, t),
373 [c, t] => (Some(c), t),
374 _ => {
375 return Err(EvalError::TypeMismatch {
376 detail: format!("{name}() takes 1 or 2 args, got {}", args.len()),
377 });
378 }
379 };
380 let config = match config_arg {
381 None => match ctx.default_text_search_config {
382 Some(name_str) => crate::fts::TsConfig::from_name(name_str).ok_or_else(|| {
383 EvalError::TypeMismatch {
384 detail: format!(
385 "text search config not implemented: {name_str:?} (supported: simple, english)"
386 ),
387 }
388 })?,
389 None => crate::fts::TsConfig::English,
393 },
394 Some(Value::Null) => return Ok((crate::fts::TsConfig::Simple, None)),
395 Some(Value::Text(name_str)) => crate::fts::TsConfig::from_name(name_str).ok_or_else(|| {
396 EvalError::TypeMismatch {
397 detail: format!(
398 "text search config not implemented: {name_str:?} (supported: simple, english)"
399 ),
400 }
401 })?,
402 Some(other) => {
403 return Err(EvalError::TypeMismatch {
404 detail: format!(
405 "{name}() config arg must be text, got {}",
406 crate::conversions::pg_type_name_for_error_opt(other.data_type())
407 ),
408 });
409 }
410 };
411 let text = match text_arg {
412 Value::Null => None,
413 Value::Text(s) => Some(s.to_string()),
414 other => {
415 return Err(EvalError::TypeMismatch {
416 detail: format!(
417 "{name}() text arg must be text, got {}",
418 crate::conversions::pg_type_name_for_error_opt(other.data_type())
419 ),
420 });
421 }
422 };
423 Ok((config, text))
424}
425
426pub fn format_tsvector(lexs: &[TsLexeme]) -> String {
432 let mut out = String::with_capacity(lexs.len() * 12);
433 for (i, l) in lexs.iter().enumerate() {
434 if i > 0 {
435 out.push(' ');
436 }
437 out.push('\'');
438 for c in l.word.chars() {
439 if c == '\'' {
440 out.push('\'');
441 }
442 out.push(c);
443 }
444 out.push('\'');
445 if !l.positions.is_empty() {
446 for (pi, p) in l.positions.iter().enumerate() {
447 out.push(if pi == 0 { ':' } else { ',' });
448 out.push_str(&p.to_string());
449 }
450 match l.weight {
455 3 => out.push('A'),
456 2 => out.push('B'),
457 1 => out.push('C'),
458 _ => {}
459 }
460 }
461 }
462 out
463}
464
465pub fn format_tsquery(ast: &TsQueryAst) -> String {
468 fn go(ast: &TsQueryAst, parent_prec: u8, out: &mut String) {
469 let (own_prec, write_self): (u8, &dyn Fn(&mut String)) = match ast {
471 TsQueryAst::Or(_, _) => (1, &|_| {}),
472 TsQueryAst::And(_, _) | TsQueryAst::Phrase { .. } => (2, &|_| {}),
473 TsQueryAst::Not(_) => (3, &|_| {}),
474 TsQueryAst::Term { .. } => (4, &|_| {}),
475 };
476 let need_parens = own_prec < parent_prec;
477 if need_parens {
478 out.push_str("( ");
480 }
481 match ast {
482 TsQueryAst::Term { word, weight_mask } => {
483 out.push('\'');
484 for c in word.chars() {
485 if c == '\'' {
486 out.push('\'');
487 }
488 out.push(c);
489 }
490 out.push('\'');
491 if *weight_mask != 0 {
494 out.push(':');
495 if weight_mask & 0x10 != 0 {
496 out.push('*');
497 }
498 for (bit, ch) in [(3u8, 'A'), (2, 'B'), (1, 'C'), (0, 'D')] {
499 if weight_mask & (1 << bit) != 0 {
500 out.push(ch);
501 }
502 }
503 }
504 }
505 TsQueryAst::And(a, b) => {
506 go(a, own_prec, out);
507 out.push_str(" & ");
508 go(b, own_prec, out);
509 }
510 TsQueryAst::Or(a, b) => {
511 go(a, own_prec, out);
512 out.push_str(" | ");
513 go(b, own_prec, out);
514 }
515 TsQueryAst::Not(x) => {
516 out.push('!');
517 go(x, own_prec, out);
518 }
519 TsQueryAst::Phrase {
520 left,
521 right,
522 distance,
523 } => {
524 go(left, own_prec, out);
525 if *distance == 1 {
528 out.push_str(" <-> ");
529 } else {
530 out.push_str(&alloc::format!(" <{distance}> "));
531 }
532 go(right, own_prec, out);
533 }
534 }
535 write_self(out);
536 if need_parens {
537 out.push_str(" )");
538 }
539 }
540 let mut out = String::new();
541 go(ast, 0, &mut out);
542 out
543}
544
545pub fn decode_tsvector_external(s: &str) -> Result<Vec<TsLexeme>, EvalError> {
554 let mut out: Vec<TsLexeme> = Vec::new();
555 let mut i = 0;
556 let bytes = s.as_bytes();
557 while i < bytes.len() {
558 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
559 i += 1;
560 }
561 if i >= bytes.len() {
562 break;
563 }
564 let word = if bytes[i] == b'\'' {
567 i += 1;
568 let mut w = String::new();
569 loop {
570 if i >= bytes.len() {
571 return Err(EvalError::TypeMismatch {
572 detail: "tsvector literal: unterminated quoted lexeme".into(),
573 });
574 }
575 let b = bytes[i];
576 if b == b'\'' {
577 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
578 w.push('\'');
579 i += 2;
580 } else {
581 i += 1;
582 break;
583 }
584 } else {
585 w.push(b as char);
586 i += 1;
587 }
588 }
589 w
590 } else {
591 let start = i;
593 while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b':' {
594 i += 1;
595 }
596 core::str::from_utf8(&bytes[start..i])
597 .map_err(|_| EvalError::TypeMismatch {
598 detail: "tsvector literal: non-UTF-8 lexeme".into(),
599 })?
600 .to_string()
601 };
602 if word.is_empty() {
603 return Err(EvalError::TypeMismatch {
604 detail: "tsvector literal: empty lexeme".into(),
605 });
606 }
607 let mut positions: Vec<u16> = Vec::new();
610 let mut weight: u8 = 0;
611 if i < bytes.len() && bytes[i] == b':' {
612 i += 1;
613 loop {
614 let start = i;
615 while i < bytes.len() && bytes[i].is_ascii_digit() {
616 i += 1;
617 }
618 if start == i {
619 return Err(EvalError::TypeMismatch {
620 detail: "tsvector literal: expected digit after ':'".into(),
621 });
622 }
623 let num: u16 = core::str::from_utf8(&bytes[start..i])
624 .expect("ascii digits")
625 .parse()
626 .map_err(|_| EvalError::TypeMismatch {
627 detail: alloc::format!(
628 "tsvector literal: position {} overflows u16",
629 core::str::from_utf8(&bytes[start..i]).unwrap_or("?")
630 ),
631 })?;
632 positions.push(num);
633 if i < bytes.len() {
634 let w = bytes[i];
635 if matches!(w, b'A' | b'B' | b'C' | b'D') {
636 weight = match w {
637 b'A' => 3,
638 b'B' => 2,
639 b'C' => 1,
640 _ => 0,
641 };
642 i += 1;
643 }
644 }
645 if i < bytes.len() && bytes[i] == b',' {
646 i += 1;
647 continue;
648 }
649 break;
650 }
651 }
652 positions.sort_unstable();
653 positions.dedup();
654 match out.binary_search_by(|l| l.word.as_str().cmp(word.as_str())) {
657 Ok(idx) => {
658 for p in positions {
659 if !out[idx].positions.contains(&p) {
660 out[idx].positions.push(p);
661 }
662 }
663 out[idx].positions.sort_unstable();
664 if weight != 0 {
665 out[idx].weight = weight;
666 }
667 }
668 Err(idx) => {
669 out.insert(
670 idx,
671 TsLexeme {
672 word,
673 positions,
674 weight,
675 },
676 );
677 }
678 }
679 }
680 Ok(out)
681}
682
683pub fn decode_tsquery_external(s: &str) -> Result<TsQueryAst, EvalError> {
689 let mut p = TsQueryParser {
690 bytes: s.as_bytes(),
691 pos: 0,
692 };
693 p.skip_ws();
694 if p.pos >= p.bytes.len() {
695 return Err(EvalError::TypeMismatch {
696 detail: "tsquery literal: empty".into(),
697 });
698 }
699 let ast = p.parse_or()?;
700 p.skip_ws();
701 if p.pos < p.bytes.len() {
702 return Err(EvalError::TypeMismatch {
703 detail: alloc::format!("tsquery literal: trailing garbage at offset {}", p.pos),
704 });
705 }
706 Ok(ast)
707}
708
709struct TsQueryParser<'a> {
710 bytes: &'a [u8],
711 pos: usize,
712}
713
714impl<'a> TsQueryParser<'a> {
715 fn skip_ws(&mut self) {
716 while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_whitespace() {
717 self.pos += 1;
718 }
719 }
720 fn peek(&self) -> Option<u8> {
721 self.bytes.get(self.pos).copied()
722 }
723 fn parse_or(&mut self) -> Result<TsQueryAst, EvalError> {
724 let mut lhs = self.parse_and()?;
725 loop {
726 self.skip_ws();
727 if self.peek() != Some(b'|') {
728 return Ok(lhs);
729 }
730 self.pos += 1;
731 let rhs = self.parse_and()?;
732 lhs = TsQueryAst::Or(Box::new(lhs), Box::new(rhs));
733 }
734 }
735 fn parse_and(&mut self) -> Result<TsQueryAst, EvalError> {
736 let mut lhs = self.parse_unary()?;
737 loop {
738 self.skip_ws();
739 match self.peek() {
740 Some(b'&') => {
741 self.pos += 1;
742 let rhs = self.parse_unary()?;
743 lhs = TsQueryAst::And(Box::new(lhs), Box::new(rhs));
744 }
745 Some(b'<') => {
746 self.pos += 1;
749 let n: u16 = if self.peek() == Some(b'-')
750 && self.bytes.get(self.pos + 1) == Some(&b'>')
751 {
752 self.pos += 2; 1
754 } else {
755 let start = self.pos;
756 while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_digit() {
757 self.pos += 1;
758 }
759 if start == self.pos || self.peek() != Some(b'>') {
760 return Err(EvalError::TypeMismatch {
761 detail: "tsquery literal: malformed <N> / <-> phrase operator"
762 .into(),
763 });
764 }
765 let val = core::str::from_utf8(&self.bytes[start..self.pos])
766 .expect("ascii digits")
767 .parse()
768 .map_err(|_| EvalError::TypeMismatch {
769 detail: "tsquery literal: phrase distance overflows u16".into(),
770 })?;
771 self.pos += 1; val
773 };
774 let rhs = self.parse_unary()?;
775 lhs = TsQueryAst::Phrase {
776 left: Box::new(lhs),
777 right: Box::new(rhs),
778 distance: n,
779 };
780 }
781 _ => return Ok(lhs),
782 }
783 }
784 }
785 fn parse_unary(&mut self) -> Result<TsQueryAst, EvalError> {
786 self.skip_ws();
787 if self.peek() == Some(b'!') {
788 self.pos += 1;
789 let inner = self.parse_unary()?;
790 return Ok(TsQueryAst::Not(Box::new(inner)));
791 }
792 self.parse_atom()
793 }
794 fn parse_atom(&mut self) -> Result<TsQueryAst, EvalError> {
795 self.skip_ws();
796 match self.peek() {
797 Some(b'(') => {
798 self.pos += 1;
799 let inner = self.parse_or()?;
800 self.skip_ws();
801 if self.peek() != Some(b')') {
802 return Err(EvalError::TypeMismatch {
803 detail: "tsquery literal: missing ')'".into(),
804 });
805 }
806 self.pos += 1;
807 Ok(inner)
808 }
809 Some(b'\'') => {
810 self.pos += 1;
811 let mut w = String::new();
812 loop {
813 match self.peek() {
814 None => {
815 return Err(EvalError::TypeMismatch {
816 detail: "tsquery literal: unterminated quoted lexeme".into(),
817 });
818 }
819 Some(b'\'') => {
820 if self.bytes.get(self.pos + 1) == Some(&b'\'') {
821 w.push('\'');
822 self.pos += 2;
823 } else {
824 self.pos += 1;
825 break;
826 }
827 }
828 Some(b) => {
829 w.push(b as char);
830 self.pos += 1;
831 }
832 }
833 }
834 let weight_mask = self.skip_weight_suffix();
837 Ok(TsQueryAst::Term {
838 word: w,
839 weight_mask,
840 })
841 }
842 Some(b) if b.is_ascii_alphanumeric() || b == b'_' => {
843 let start = self.pos;
844 while self.pos < self.bytes.len() {
845 let c = self.bytes[self.pos];
846 if c.is_ascii_alphanumeric() || c == b'_' {
847 self.pos += 1;
848 } else {
849 break;
850 }
851 }
852 let w = core::str::from_utf8(&self.bytes[start..self.pos])
853 .map_err(|_| EvalError::TypeMismatch {
854 detail: "tsquery literal: non-UTF-8 lexeme".into(),
855 })?
856 .to_string();
857 let weight_mask = self.skip_weight_suffix();
858 Ok(TsQueryAst::Term {
859 word: w,
860 weight_mask,
861 })
862 }
863 Some(b) => Err(EvalError::TypeMismatch {
864 detail: alloc::format!(
865 "tsquery literal: unexpected byte {:?} at offset {}",
866 b as char,
867 self.pos
868 ),
869 }),
870 None => Err(EvalError::TypeMismatch {
871 detail: "tsquery literal: expected term".into(),
872 }),
873 }
874 }
875 fn skip_weight_suffix(&mut self) -> u8 {
880 if self.peek() != Some(b':') {
881 return 0;
882 }
883 self.pos += 1;
884 let mut mask: u8 = 0;
885 while let Some(b) = self.peek() {
886 match b {
887 b'A' | b'a' => mask |= 1 << 3,
888 b'B' | b'b' => mask |= 1 << 2,
889 b'C' | b'c' => mask |= 1 << 1,
890 b'D' | b'd' => mask |= 1,
891 b'*' => mask |= 0x10,
892 _ if b.is_ascii_digit() => {}
893 _ => break,
894 }
895 self.pos += 1;
896 }
897 mask
898 }
899}
900
901pub(super) fn tsvector_concat(
902 l: &[spg_storage::TsLexeme],
903 r: &[spg_storage::TsLexeme],
904) -> Value<'static> {
905 let shift = l
906 .iter()
907 .flat_map(|x| x.positions.iter().copied())
908 .max()
909 .unwrap_or(0);
910 let mut out: Vec<spg_storage::TsLexeme> = l.to_vec();
911 for lex in r {
912 let shifted: Vec<u16> = lex
913 .positions
914 .iter()
915 .map(|p| p.saturating_add(shift))
916 .collect();
917 if let Some(existing) = out.iter_mut().find(|x| x.word == lex.word) {
918 existing.positions.extend(shifted);
919 existing.positions.sort_unstable();
920 existing.weight = existing.weight.max(lex.weight);
921 } else {
922 out.push(spg_storage::TsLexeme {
923 word: lex.word.clone(),
924 positions: shifted,
925 weight: lex.weight,
926 });
927 }
928 }
929 out.sort_by(|a, b| a.word.cmp(&b.word));
930 Value::TsVector(out)
931}
932
933pub(super) fn fts_ts_headline(
942 args: &[Value<'_>],
943 ctx: &EvalContext<'_>,
944) -> Result<Value<'static>, EvalError> {
945 if args.len() < 2 || args.len() > 4 {
946 return Err(crate::eval::functions::wrong_arity("ts_headline", args));
947 }
948 let is_queryish = |v: &Value<'_>| matches!(v, Value::TsQuery(_));
950 let (config_arg, doc_arg, query_arg, opts_arg) = match args {
951 [d, q] => (None, d, q, None),
952 [d, q, o] if is_queryish(q) => (None, d, q, Some(o)),
953 [c, d, q] => (Some(c), d, q, None),
954 [c, d, q, o] => (Some(c), d, q, Some(o)),
955 _ => {
956 return Err(EvalError::TypeMismatch {
957 detail: format!("ts_headline() takes 2 to 4 args, got {}", args.len()),
958 });
959 }
960 };
961 if matches!(doc_arg, Value::Null) || matches!(query_arg, Value::Null) {
962 return Ok(Value::Null);
963 }
964 let config = match config_arg {
965 None => match ctx.default_text_search_config {
966 Some(name_str) => crate::fts::TsConfig::from_name(name_str).ok_or_else(|| {
967 EvalError::TypeMismatch {
968 detail: format!(
969 "text search config not implemented: {name_str:?} (supported: simple, english)"
970 ),
971 }
972 })?,
973 None => crate::fts::TsConfig::English,
974 },
975 Some(Value::Text(name_str)) => {
976 crate::fts::TsConfig::from_name(name_str).ok_or_else(|| EvalError::TypeMismatch {
977 detail: format!(
978 "text search config not implemented: {name_str:?} (supported: simple, english)"
979 ),
980 })?
981 }
982 Some(other) => {
983 return Err(EvalError::TypeMismatch {
984 detail: format!(
985 "ts_headline() config must be text, got {}",
986 crate::conversions::pg_type_name_for_error_opt(other.data_type())
987 ),
988 });
989 }
990 };
991 let doc = match doc_arg {
992 Value::Text(s) => s.as_ref(),
993 other => {
994 return Err(EvalError::TypeMismatch {
995 detail: format!(
996 "ts_headline() document must be text, got {}",
997 crate::conversions::pg_type_name_for_error_opt(other.data_type())
998 ),
999 });
1000 }
1001 };
1002 let query = match query_arg {
1003 Value::TsQuery(q) => q.clone(),
1004 Value::Text(s) => crate::fts::to_tsquery(config, s)?,
1007 other => {
1008 return Err(EvalError::TypeMismatch {
1009 detail: format!(
1010 "ts_headline() query must be tsquery, got {}",
1011 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1012 ),
1013 });
1014 }
1015 };
1016 let mut start_sel = String::from("<b>");
1020 let mut stop_sel = String::from("</b>");
1021 let mut max_words: usize = 35;
1022 let mut min_words: usize = 15;
1023 let mut max_fragments: usize = 0;
1024 let mut frag_delim = String::from(" ... ");
1025 let mut highlight_all = false;
1026 let mut short_word: usize = 3;
1027 if let Some(opts_v) = opts_arg {
1028 let opts = match opts_v {
1029 Value::Null => "",
1030 Value::Text(s) => s.as_ref(),
1031 other => {
1032 return Err(EvalError::TypeMismatch {
1033 detail: format!(
1034 "ts_headline() options must be text, got {}",
1035 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1036 ),
1037 });
1038 }
1039 };
1040 let parse_int = |v: &str| -> Result<i64, EvalError> {
1045 v.parse::<i64>().map_err(|_| EvalError::TypeMismatch {
1046 detail: alloc::format!("invalid input syntax for type integer: {v:?}"),
1047 })
1048 };
1049 let mut short_word_i: i64 = short_word as i64;
1050 let mut max_fragments_i: i64 = 0;
1051 let mut min_words_i: i64 = min_words as i64;
1052 let mut max_words_i: i64 = max_words as i64;
1053 for pair in opts.split(',') {
1054 if pair.trim().is_empty() {
1055 continue;
1056 }
1057 let Some((k, v)) = pair.split_once('=') else {
1058 return Err(EvalError::TypeMismatch {
1059 detail: alloc::format!("invalid parameter list format: {:?}", pair.trim()),
1060 });
1061 };
1062 let v = v.trim().trim_matches('"');
1063 if v.is_empty() {
1064 return Err(EvalError::TypeMismatch {
1065 detail: alloc::format!("invalid parameter list format: {:?}", pair.trim()),
1066 });
1067 }
1068 match k.trim().to_ascii_lowercase().as_str() {
1069 "startsel" => start_sel = v.to_string(),
1070 "stopsel" => stop_sel = v.to_string(),
1071 "maxwords" => max_words_i = parse_int(v)?,
1072 "minwords" => min_words_i = parse_int(v)?,
1073 "maxfragments" => max_fragments_i = parse_int(v)?,
1074 "shortword" => short_word_i = parse_int(v)?,
1075 "fragmentdelimiter" => frag_delim = v.to_string(),
1076 "highlightall" => {
1079 highlight_all = matches!(
1080 v.to_ascii_lowercase().as_str(),
1081 "1" | "on" | "t" | "true" | "y" | "yes"
1082 );
1083 }
1084 _ => {
1085 return Err(EvalError::TypeMismatch {
1086 detail: alloc::format!("unrecognized headline parameter: {:?}", k.trim()),
1087 });
1088 }
1089 }
1090 }
1091 if min_words_i >= max_words_i {
1094 return Err(EvalError::TypeMismatch {
1095 detail: "MinWords must be less than MaxWords".into(),
1096 });
1097 }
1098 if min_words_i <= 0 {
1099 return Err(EvalError::TypeMismatch {
1100 detail: "MinWords must be positive".into(),
1101 });
1102 }
1103 if short_word_i < 0 {
1104 return Err(EvalError::TypeMismatch {
1105 detail: "ShortWord must be >= 0".into(),
1106 });
1107 }
1108 if max_fragments_i < 0 {
1109 return Err(EvalError::TypeMismatch {
1110 detail: "MaxFragments must be >= 0".into(),
1111 });
1112 }
1113 max_words = max_words_i as usize;
1114 min_words = min_words_i as usize;
1115 short_word = short_word_i as usize;
1116 max_fragments = max_fragments_i as usize;
1117 }
1118 fn collect_positive(ast: &spg_storage::TsQueryAst, out: &mut Vec<String>) {
1120 match ast {
1121 spg_storage::TsQueryAst::Term { word, .. } => {
1122 if !word.is_empty() {
1123 out.push(word.clone());
1124 }
1125 }
1126 spg_storage::TsQueryAst::And(l, r) | spg_storage::TsQueryAst::Or(l, r) => {
1127 collect_positive(l, out);
1128 collect_positive(r, out);
1129 }
1130 spg_storage::TsQueryAst::Not(_) => {}
1131 spg_storage::TsQueryAst::Phrase { left, right, .. } => {
1132 collect_positive(left, out);
1133 collect_positive(right, out);
1134 }
1135 }
1136 }
1137 let mut terms: Vec<String> = Vec::new();
1138 collect_positive(&query, &mut terms);
1139 struct HlToken {
1144 word: String,
1145 lex: String,
1146 sep_after: String,
1147 is_match: bool,
1148 }
1149 let mut tokens: Vec<HlToken> = Vec::new();
1150 let mut leading_sep = String::new();
1151 let mut word = String::new();
1152 let mut push_word = |word: &mut String, tokens: &mut Vec<HlToken>| {
1153 if word.is_empty() {
1154 return;
1155 }
1156 let lowered: String = word.chars().flat_map(|c| c.to_lowercase()).collect();
1157 let lex = match config {
1158 crate::fts::TsConfig::Simple => lowered,
1159 crate::fts::TsConfig::English => crate::fts::porter_stem(&lowered),
1160 crate::fts::TsConfig::Spanish => crate::fts_es::stem_es(&lowered),
1161 crate::fts::TsConfig::French => crate::fts_fr::stem_fr(&lowered),
1162 crate::fts::TsConfig::German => crate::fts_de::stem_de(&lowered),
1163 };
1164 let is_match = terms.iter().any(|t| *t == lex);
1165 tokens.push(HlToken {
1166 word: core::mem::take(word),
1167 lex,
1168 sep_after: String::new(),
1169 is_match,
1170 });
1171 };
1172 for c in doc.chars() {
1173 if c.is_alphanumeric() || c == '_' {
1174 word.push(c);
1175 } else {
1176 push_word(&mut word, &mut tokens);
1177 match tokens.last_mut() {
1178 Some(t) => t.sep_after.push(c),
1179 None => leading_sep.push(c),
1180 }
1181 }
1182 }
1183 push_word(&mut word, &mut tokens);
1184 let render = |lo: usize, hi: usize| -> String {
1188 let mut out = String::new();
1189 for (i, t) in tokens[lo..hi].iter().enumerate() {
1190 if t.is_match {
1191 out.push_str(&start_sel);
1192 out.push_str(&t.word);
1193 out.push_str(&stop_sel);
1194 } else {
1195 out.push_str(&t.word);
1196 }
1197 if lo + i + 1 < hi {
1198 out.push_str(&t.sep_after);
1199 }
1200 }
1201 out
1202 };
1203 let n = tokens.len();
1204 let match_pos: Vec<usize> = tokens
1205 .iter()
1206 .enumerate()
1207 .filter_map(|(i, t)| t.is_match.then_some(i))
1208 .collect();
1209 if highlight_all || n <= min_words.max(1) {
1212 let mut out = leading_sep;
1213 out.push_str(&render(0, n));
1214 if let Some(t) = tokens.last() {
1215 out.push_str(&t.sep_after);
1216 }
1217 return Ok(Value::text(out));
1218 }
1219 if match_pos.is_empty() {
1223 return Ok(Value::text(render(0, min_words.max(1).min(n))));
1224 }
1225 if max_fragments > 0 {
1226 let interesting: Vec<bool> = tokens.iter().map(|t| t.is_match).collect();
1249 let is_bad_endpoint = |i: usize| -> bool {
1250 if interesting[i] {
1251 return false;
1252 }
1253 let w = &tokens[i].word;
1254 w.chars().count() <= short_word || w.chars().all(|c| c.is_ascii_digit())
1255 };
1256 fn and_groups(ast: &spg_storage::TsQueryAst, out: &mut Vec<Vec<String>>) {
1259 match ast {
1260 spg_storage::TsQueryAst::And(l, r) => {
1261 and_groups(l, out);
1262 and_groups(r, out);
1263 }
1264 spg_storage::TsQueryAst::Not(_) => {}
1265 other => {
1266 let mut g = Vec::new();
1267 fn collect(ast: &spg_storage::TsQueryAst, out: &mut Vec<String>) {
1269 match ast {
1270 spg_storage::TsQueryAst::Term { word, .. } => {
1271 if !word.is_empty() {
1272 out.push(word.clone());
1273 }
1274 }
1275 spg_storage::TsQueryAst::And(l, r)
1276 | spg_storage::TsQueryAst::Or(l, r) => {
1277 collect(l, out);
1278 collect(r, out);
1279 }
1280 spg_storage::TsQueryAst::Not(_) => {}
1281 spg_storage::TsQueryAst::Phrase { left, right, .. } => {
1282 collect(left, out);
1283 collect(right, out);
1284 }
1285 }
1286 }
1287 collect(other, &mut g);
1288 if !g.is_empty() {
1289 out.push(g);
1290 }
1291 }
1292 }
1293 }
1294 let mut groups: Vec<Vec<String>> = Vec::new();
1295 and_groups(&query, &mut groups);
1296 let group_pos: Vec<Vec<usize>> = groups
1297 .iter()
1298 .map(|g| {
1299 tokens
1300 .iter()
1301 .enumerate()
1302 .filter(|(_, t)| g.iter().any(|term| *term == t.lex))
1303 .map(|(i, _)| i)
1304 .collect()
1305 })
1306 .collect();
1307 struct Cand {
1309 st: usize,
1310 en: usize,
1311 curlen: usize,
1312 poslen: usize,
1313 chosen: bool,
1314 excluded: bool,
1315 }
1316 let mut cands: Vec<Cand> = Vec::new();
1317 if !group_pos.is_empty() && group_pos.iter().all(|ps| !ps.is_empty()) {
1318 let mut nextpos = 0usize;
1319 loop {
1320 let mut pose = 0usize;
1323 let mut dead = false;
1324 for ps in &group_pos {
1325 match ps.iter().find(|&&p| p >= nextpos) {
1326 Some(&p) => pose = pose.max(p),
1327 None => {
1328 dead = true;
1329 break;
1330 }
1331 }
1332 }
1333 if dead {
1334 break;
1335 }
1336 let mut posb = usize::MAX;
1337 for ps in &group_pos {
1338 if let Some(&p) = ps.iter().rev().find(|&&p| p <= pose) {
1339 posb = posb.min(p);
1340 }
1341 }
1342 let posb = posb.max(nextpos);
1343 let (mut st, en_cover) = (posb, pose);
1346 while st <= en_cover {
1347 let mut i = st;
1349 while i < en_cover && !interesting[i] {
1350 i += 1;
1351 }
1352 st = i;
1353 let mut curlen = 0usize;
1354 let mut poslen = 0usize;
1355 i = st;
1356 while i <= en_cover && curlen < max_words.max(1) {
1357 curlen += 1;
1358 if interesting[i] {
1359 poslen += 1;
1360 }
1361 i += 1;
1362 }
1363 let mut en = i - 1;
1365 if en < en_cover {
1366 while en > st && !interesting[en] {
1367 curlen -= 1;
1368 en -= 1;
1369 }
1370 }
1371 cands.push(Cand {
1372 st,
1373 en,
1374 curlen,
1375 poslen,
1376 chosen: false,
1377 excluded: false,
1378 });
1379 st = en + 1;
1380 }
1381 nextpos = posb + 1;
1382 }
1383 }
1384 let mut in_frag: Vec<bool> = alloc::vec![false; n];
1386 let mut picked = 0usize;
1387 for _ in 0..max_fragments {
1388 let mut best: Option<usize> = None;
1389 for (i, c) in cands.iter().enumerate() {
1390 if c.chosen || c.excluded {
1391 continue;
1392 }
1393 let better = match best {
1394 None => true,
1395 Some(b) => {
1396 c.poslen > cands[b].poslen
1397 || (c.poslen == cands[b].poslen && c.curlen < cands[b].curlen)
1398 }
1399 };
1400 if better {
1401 best = Some(i);
1402 }
1403 }
1404 let Some(bi) = best else { break };
1405 let (mut st, mut en, mut curlen) = (cands[bi].st, cands[bi].en, cands[bi].curlen);
1406 if curlen < max_words {
1407 let maxstretch = (max_words - curlen) / 2;
1410 let mut stretch = 0usize;
1411 let mut posmarker = st;
1412 let mut i = st;
1413 while i > 0 && stretch < maxstretch && !in_frag[i - 1] {
1414 i -= 1;
1415 curlen += 1;
1416 stretch += 1;
1417 posmarker = i;
1418 }
1419 let mut i = posmarker;
1421 while i < st && is_bad_endpoint(i) {
1422 curlen -= 1;
1423 i += 1;
1424 }
1425 st = i;
1426 let mut posmarker = en;
1428 let mut i = en + 1;
1429 while i < n && curlen < max_words && !in_frag[i] {
1430 curlen += 1;
1431 posmarker = i;
1432 i += 1;
1433 }
1434 let mut i = posmarker;
1436 while i > en && is_bad_endpoint(i) {
1437 curlen -= 1;
1438 i -= 1;
1439 }
1440 en = i;
1441 }
1442 cands[bi].st = st;
1443 cands[bi].en = en;
1444 cands[bi].curlen = curlen;
1445 cands[bi].chosen = true;
1446 for k in st..=en {
1447 in_frag[k] = true;
1448 }
1449 picked += 1;
1450 for (i, c) in cands.iter_mut().enumerate() {
1451 if i != bi
1452 && ((c.st >= st && c.st <= en)
1453 || (c.en >= st && c.en <= en)
1454 || (c.st < st && c.en > en))
1455 {
1456 c.excluded = true;
1457 }
1458 }
1459 }
1460 if picked == 0 {
1461 let hi = min_words.max(1).min(n);
1462 return Ok(Value::text(render(0, hi)));
1463 }
1464 let mut chosen: Vec<(usize, usize)> = cands
1465 .iter()
1466 .filter(|c| c.chosen)
1467 .map(|c| (c.st, c.en))
1468 .collect();
1469 chosen.sort_unstable();
1470 let parts: Vec<String> = chosen.iter().map(|&(st, en)| render(st, en + 1)).collect();
1471 return Ok(Value::text(parts.join(&frag_delim)));
1472 }
1473 let first = match_pos[0];
1478 let last = *match_pos.last().expect("non-empty");
1479 let mut lo = first;
1480 let mut hi = (last + 1).min(lo + max_words.max(1)).min(n);
1481 while hi - lo < min_words.max(1) && hi < n {
1482 hi += 1;
1483 }
1484 while hi - lo < min_words.max(1) && lo > 0 {
1485 lo -= 1;
1486 }
1487 Ok(Value::text(render(lo, hi)))
1488}
1489
1490pub(super) fn fts_ts_rewrite(
1498 args: &[Value<'_>],
1499 ctx: &EvalContext<'_>,
1500) -> Result<Value<'static>, EvalError> {
1501 if args.len() != 3 {
1502 return Err(EvalError::WrongArity {
1503 name: alloc::string::String::from("ts_rewrite"),
1504 types: crate::eval::functions::arg_type_list(args),
1505 });
1506 }
1507 if args.iter().any(|a| matches!(a, Value::Null)) {
1508 return Ok(Value::Null);
1509 }
1510 let config = match ctx.default_text_search_config {
1511 Some(name_str) => {
1512 crate::fts::TsConfig::from_name(name_str).unwrap_or(crate::fts::TsConfig::English)
1513 }
1514 None => crate::fts::TsConfig::English,
1515 };
1516 let as_query = |v: &Value<'_>, which: &str| -> Result<spg_storage::TsQueryAst, EvalError> {
1517 match v {
1518 Value::TsQuery(q) => Ok(q.clone()),
1519 Value::Text(s) => crate::fts::to_tsquery(config, s),
1522 other => Err(EvalError::TypeMismatch {
1523 detail: format!(
1524 "ts_rewrite() {which} must be tsquery, got {}",
1525 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1526 ),
1527 }),
1528 }
1529 };
1530 let query = as_query(&args[0], "query")?;
1531 let target = as_query(&args[1], "target")?;
1532 let substitute = as_query(&args[2], "substitute")?;
1533 fn rewrite(
1534 node: &spg_storage::TsQueryAst,
1535 target: &spg_storage::TsQueryAst,
1536 substitute: &spg_storage::TsQueryAst,
1537 ) -> spg_storage::TsQueryAst {
1538 if node == target {
1539 return substitute.clone();
1540 }
1541 use spg_storage::TsQueryAst as A;
1542 match node {
1543 A::Term { .. } => node.clone(),
1544 A::And(l, r) => A::And(
1545 Box::new(rewrite(l, target, substitute)),
1546 Box::new(rewrite(r, target, substitute)),
1547 ),
1548 A::Or(l, r) => A::Or(
1549 Box::new(rewrite(l, target, substitute)),
1550 Box::new(rewrite(r, target, substitute)),
1551 ),
1552 A::Not(x) => A::Not(Box::new(rewrite(x, target, substitute))),
1553 A::Phrase {
1554 left,
1555 right,
1556 distance,
1557 } => A::Phrase {
1558 left: Box::new(rewrite(left, target, substitute)),
1559 right: Box::new(rewrite(right, target, substitute)),
1560 distance: *distance,
1561 },
1562 }
1563 }
1564 Ok(Value::TsQuery(rewrite(&query, &target, &substitute)))
1565}
1566
1567pub(super) fn fts_tsquery_bool(
1573 args: &[Value<'_>],
1574 ctx: &EvalContext<'_>,
1575 op: &str,
1576) -> Result<Value<'static>, EvalError> {
1577 let arity = if op == "not" { 1 } else { 2 };
1578 if args.len() != arity {
1579 return Err(EvalError::WrongArity {
1580 name: alloc::format!("tsquery_{op}"),
1581 types: crate::eval::functions::arg_type_list(args),
1582 });
1583 }
1584 if args.iter().any(|a| matches!(a, Value::Null)) {
1585 return Ok(Value::Null);
1586 }
1587 let config = match ctx.default_text_search_config {
1588 Some(name_str) => {
1589 crate::fts::TsConfig::from_name(name_str).unwrap_or(crate::fts::TsConfig::English)
1590 }
1591 None => crate::fts::TsConfig::English,
1592 };
1593 let as_query = |v: &Value<'_>| -> Result<spg_storage::TsQueryAst, EvalError> {
1594 match v {
1595 Value::TsQuery(q) => Ok(q.clone()),
1596 Value::Text(s) => crate::fts::to_tsquery(config, s),
1597 other => Err(EvalError::TypeMismatch {
1598 detail: format!(
1599 "tsquery_{op}() arguments must be tsquery, got {}",
1600 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1601 ),
1602 }),
1603 }
1604 };
1605 use spg_storage::TsQueryAst as A;
1606 let out = match op {
1607 "and" => A::And(Box::new(as_query(&args[0])?), Box::new(as_query(&args[1])?)),
1608 "or" => A::Or(Box::new(as_query(&args[0])?), Box::new(as_query(&args[1])?)),
1609 _ => A::Not(Box::new(as_query(&args[0])?)),
1610 };
1611 Ok(Value::TsQuery(out))
1612}