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 let (vec_arg, weight_arg, only) = match args {
242 [v, w] => (v, w, None),
243 [v, w, l] => (v, w, Some(l)),
244 _ => {
245 return Err(EvalError::TypeMismatch {
246 detail: alloc::format!("setweight expects 2 or 3 arguments, got {}", args.len()),
247 });
248 }
249 };
250 if matches!(vec_arg, Value::Null) || matches!(weight_arg, Value::Null) {
251 return Ok(Value::Null);
252 }
253 let Value::TsVector(lexemes) = vec_arg else {
254 return Err(EvalError::TypeMismatch {
255 detail: alloc::format!(
256 "setweight expects a tsvector, got {}",
257 crate::conversions::pg_type_name_for_error_opt(vec_arg.data_type())
258 ),
259 });
260 };
261 let Value::Text(w) = weight_arg else {
262 return Err(EvalError::TypeMismatch {
263 detail: alloc::format!(
264 "setweight expects a weight letter, got {}",
265 crate::conversions::pg_type_name_for_error_opt(weight_arg.data_type())
266 ),
267 });
268 };
269 let weight = match w.to_ascii_uppercase().as_str() {
270 "A" => 3,
271 "B" => 2,
272 "C" => 1,
273 "D" => 0,
274 other => {
275 return Err(EvalError::TypeMismatch {
276 detail: alloc::format!("unrecognized weight: {other:?} (expected A, B, C or D)"),
277 });
278 }
279 };
280 let selected: Option<alloc::vec::Vec<String>> = match only {
283 None => None,
284 Some(Value::Null) => return Ok(Value::Null),
285 Some(v) => {
286 let t = crate::eval::value_to_text(v);
287 let inner = t.trim().trim_start_matches('{').trim_end_matches('}');
288 Some(
289 inner
290 .split(',')
291 .map(|x| x.trim().trim_matches('"').to_string())
292 .filter(|x| !x.is_empty())
293 .collect(),
294 )
295 }
296 };
297 let mut out = lexemes.clone();
298 for lex in &mut out {
299 let hit = selected
300 .as_ref()
301 .is_none_or(|names| names.iter().any(|n| *n == lex.word));
302 if hit {
303 lex.weight = weight;
304 }
305 }
306 Ok(Value::TsVector(out))
307}
308
309pub(super) fn fts_plainto_tsquery(
310 args: &[Value<'_>],
311 ctx: &EvalContext<'_>,
312) -> Result<Value<'static>, EvalError> {
313 let (config, text) = parse_fts_args("plainto_tsquery", args, ctx)?;
314 match text {
315 None => Ok(Value::Null),
316 Some(t) => Ok(Value::TsQuery(crate::fts::plainto_tsquery(config, &t))),
317 }
318}
319
320pub(super) fn fts_phraseto_tsquery(
321 args: &[Value<'_>],
322 ctx: &EvalContext<'_>,
323) -> Result<Value<'static>, EvalError> {
324 let (config, text) = parse_fts_args("phraseto_tsquery", args, ctx)?;
325 match text {
326 None => Ok(Value::Null),
327 Some(t) => Ok(Value::TsQuery(crate::fts::phraseto_tsquery(config, &t))),
328 }
329}
330
331pub(super) fn fts_websearch_to_tsquery(
332 args: &[Value<'_>],
333 ctx: &EvalContext<'_>,
334) -> Result<Value<'static>, EvalError> {
335 let (config, text) = parse_fts_args("websearch_to_tsquery", args, ctx)?;
336 match text {
337 None => Ok(Value::Null),
338 Some(t) => Ok(Value::TsQuery(crate::fts::websearch_to_tsquery(config, &t))),
339 }
340}
341
342pub(super) fn fts_to_tsquery(
343 args: &[Value<'_>],
344 ctx: &EvalContext<'_>,
345) -> Result<Value<'static>, EvalError> {
346 let (config, text) = parse_fts_args("to_tsquery", args, ctx)?;
347 match text {
348 None => Ok(Value::Null),
349 Some(t) => Ok(Value::TsQuery(crate::fts::to_tsquery(config, &t)?)),
350 }
351}
352
353fn parse_fts_args(
358 name: &str,
359 args: &[Value<'_>],
360 ctx: &EvalContext<'_>,
361) -> Result<(crate::fts::TsConfig, Option<String>), EvalError> {
362 let (config_arg, text_arg) = match args {
363 [t] => (None, t),
364 [c, t] => (Some(c), t),
365 _ => {
366 return Err(EvalError::TypeMismatch {
367 detail: format!("{name}() takes 1 or 2 args, got {}", args.len()),
368 });
369 }
370 };
371 let config = match config_arg {
372 None => match ctx.default_text_search_config {
373 Some(name_str) => crate::fts::TsConfig::from_name(name_str).ok_or_else(|| {
374 EvalError::TypeMismatch {
375 detail: format!(
376 "text search config not implemented: {name_str:?} (supported: simple, english)"
377 ),
378 }
379 })?,
380 None => crate::fts::TsConfig::English,
384 },
385 Some(Value::Null) => return Ok((crate::fts::TsConfig::Simple, None)),
386 Some(Value::Text(name_str)) => crate::fts::TsConfig::from_name(name_str).ok_or_else(|| {
387 EvalError::TypeMismatch {
388 detail: format!(
389 "text search config not implemented: {name_str:?} (supported: simple, english)"
390 ),
391 }
392 })?,
393 Some(other) => {
394 return Err(EvalError::TypeMismatch {
395 detail: format!(
396 "{name}() config arg must be text, got {}",
397 crate::conversions::pg_type_name_for_error_opt(other.data_type())
398 ),
399 });
400 }
401 };
402 let text = match text_arg {
403 Value::Null => None,
404 Value::Text(s) => Some(s.to_string()),
405 other => {
406 return Err(EvalError::TypeMismatch {
407 detail: format!(
408 "{name}() text arg must be text, got {}",
409 crate::conversions::pg_type_name_for_error_opt(other.data_type())
410 ),
411 });
412 }
413 };
414 Ok((config, text))
415}
416
417pub fn format_tsvector(lexs: &[TsLexeme]) -> String {
423 let mut out = String::with_capacity(lexs.len() * 12);
424 for (i, l) in lexs.iter().enumerate() {
425 if i > 0 {
426 out.push(' ');
427 }
428 out.push('\'');
429 for c in l.word.chars() {
430 if c == '\'' {
431 out.push('\'');
432 }
433 out.push(c);
434 }
435 out.push('\'');
436 if !l.positions.is_empty() {
437 for (pi, p) in l.positions.iter().enumerate() {
438 out.push(if pi == 0 { ':' } else { ',' });
439 out.push_str(&p.to_string());
440 }
441 match l.weight {
446 3 => out.push('A'),
447 2 => out.push('B'),
448 1 => out.push('C'),
449 _ => {}
450 }
451 }
452 }
453 out
454}
455
456pub fn format_tsquery(ast: &TsQueryAst) -> String {
459 fn go(ast: &TsQueryAst, parent_prec: u8, out: &mut String) {
460 let (own_prec, write_self): (u8, &dyn Fn(&mut String)) = match ast {
462 TsQueryAst::Or(_, _) => (1, &|_| {}),
463 TsQueryAst::And(_, _) | TsQueryAst::Phrase { .. } => (2, &|_| {}),
464 TsQueryAst::Not(_) => (3, &|_| {}),
465 TsQueryAst::Term { .. } => (4, &|_| {}),
466 };
467 let need_parens = own_prec < parent_prec;
468 if need_parens {
469 out.push_str("( ");
471 }
472 match ast {
473 TsQueryAst::Term { word, weight_mask } => {
474 out.push('\'');
475 for c in word.chars() {
476 if c == '\'' {
477 out.push('\'');
478 }
479 out.push(c);
480 }
481 out.push('\'');
482 if *weight_mask != 0 {
485 out.push(':');
486 if weight_mask & 0x10 != 0 {
487 out.push('*');
488 }
489 for (bit, ch) in [(3u8, 'A'), (2, 'B'), (1, 'C'), (0, 'D')] {
490 if weight_mask & (1 << bit) != 0 {
491 out.push(ch);
492 }
493 }
494 }
495 }
496 TsQueryAst::And(a, b) => {
497 go(a, own_prec, out);
498 out.push_str(" & ");
499 go(b, own_prec, out);
500 }
501 TsQueryAst::Or(a, b) => {
502 go(a, own_prec, out);
503 out.push_str(" | ");
504 go(b, own_prec, out);
505 }
506 TsQueryAst::Not(x) => {
507 out.push('!');
508 go(x, own_prec, out);
509 }
510 TsQueryAst::Phrase {
511 left,
512 right,
513 distance,
514 } => {
515 go(left, own_prec, out);
516 if *distance == 1 {
519 out.push_str(" <-> ");
520 } else {
521 out.push_str(&alloc::format!(" <{distance}> "));
522 }
523 go(right, own_prec, out);
524 }
525 }
526 write_self(out);
527 if need_parens {
528 out.push_str(" )");
529 }
530 }
531 let mut out = String::new();
532 go(ast, 0, &mut out);
533 out
534}
535
536pub fn decode_tsvector_external(s: &str) -> Result<Vec<TsLexeme>, EvalError> {
545 let mut out: Vec<TsLexeme> = Vec::new();
546 let mut i = 0;
547 let bytes = s.as_bytes();
548 while i < bytes.len() {
549 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
550 i += 1;
551 }
552 if i >= bytes.len() {
553 break;
554 }
555 let word = if bytes[i] == b'\'' {
558 i += 1;
559 let mut w = String::new();
560 loop {
561 if i >= bytes.len() {
562 return Err(EvalError::TypeMismatch {
563 detail: "tsvector literal: unterminated quoted lexeme".into(),
564 });
565 }
566 let b = bytes[i];
567 if b == b'\'' {
568 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
569 w.push('\'');
570 i += 2;
571 } else {
572 i += 1;
573 break;
574 }
575 } else {
576 w.push(b as char);
577 i += 1;
578 }
579 }
580 w
581 } else {
582 let start = i;
584 while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b':' {
585 i += 1;
586 }
587 core::str::from_utf8(&bytes[start..i])
588 .map_err(|_| EvalError::TypeMismatch {
589 detail: "tsvector literal: non-UTF-8 lexeme".into(),
590 })?
591 .to_string()
592 };
593 if word.is_empty() {
594 return Err(EvalError::TypeMismatch {
595 detail: "tsvector literal: empty lexeme".into(),
596 });
597 }
598 let mut positions: Vec<u16> = Vec::new();
601 let mut weight: u8 = 0;
602 if i < bytes.len() && bytes[i] == b':' {
603 i += 1;
604 loop {
605 let start = i;
606 while i < bytes.len() && bytes[i].is_ascii_digit() {
607 i += 1;
608 }
609 if start == i {
610 return Err(EvalError::TypeMismatch {
611 detail: "tsvector literal: expected digit after ':'".into(),
612 });
613 }
614 let num: u16 = core::str::from_utf8(&bytes[start..i])
615 .expect("ascii digits")
616 .parse()
617 .map_err(|_| EvalError::TypeMismatch {
618 detail: alloc::format!(
619 "tsvector literal: position {} overflows u16",
620 core::str::from_utf8(&bytes[start..i]).unwrap_or("?")
621 ),
622 })?;
623 positions.push(num);
624 if i < bytes.len() {
625 let w = bytes[i];
626 if matches!(w, b'A' | b'B' | b'C' | b'D') {
627 weight = match w {
628 b'A' => 3,
629 b'B' => 2,
630 b'C' => 1,
631 _ => 0,
632 };
633 i += 1;
634 }
635 }
636 if i < bytes.len() && bytes[i] == b',' {
637 i += 1;
638 continue;
639 }
640 break;
641 }
642 }
643 positions.sort_unstable();
644 positions.dedup();
645 match out.binary_search_by(|l| l.word.as_str().cmp(word.as_str())) {
648 Ok(idx) => {
649 for p in positions {
650 if !out[idx].positions.contains(&p) {
651 out[idx].positions.push(p);
652 }
653 }
654 out[idx].positions.sort_unstable();
655 if weight != 0 {
656 out[idx].weight = weight;
657 }
658 }
659 Err(idx) => {
660 out.insert(
661 idx,
662 TsLexeme {
663 word,
664 positions,
665 weight,
666 },
667 );
668 }
669 }
670 }
671 Ok(out)
672}
673
674pub fn decode_tsquery_external(s: &str) -> Result<TsQueryAst, EvalError> {
680 let mut p = TsQueryParser {
681 bytes: s.as_bytes(),
682 pos: 0,
683 };
684 p.skip_ws();
685 if p.pos >= p.bytes.len() {
686 return Err(EvalError::TypeMismatch {
687 detail: "tsquery literal: empty".into(),
688 });
689 }
690 let ast = p.parse_or()?;
691 p.skip_ws();
692 if p.pos < p.bytes.len() {
693 return Err(EvalError::TypeMismatch {
694 detail: alloc::format!("tsquery literal: trailing garbage at offset {}", p.pos),
695 });
696 }
697 Ok(ast)
698}
699
700struct TsQueryParser<'a> {
701 bytes: &'a [u8],
702 pos: usize,
703}
704
705impl<'a> TsQueryParser<'a> {
706 fn skip_ws(&mut self) {
707 while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_whitespace() {
708 self.pos += 1;
709 }
710 }
711 fn peek(&self) -> Option<u8> {
712 self.bytes.get(self.pos).copied()
713 }
714 fn parse_or(&mut self) -> Result<TsQueryAst, EvalError> {
715 let mut lhs = self.parse_and()?;
716 loop {
717 self.skip_ws();
718 if self.peek() != Some(b'|') {
719 return Ok(lhs);
720 }
721 self.pos += 1;
722 let rhs = self.parse_and()?;
723 lhs = TsQueryAst::Or(Box::new(lhs), Box::new(rhs));
724 }
725 }
726 fn parse_and(&mut self) -> Result<TsQueryAst, EvalError> {
727 let mut lhs = self.parse_unary()?;
728 loop {
729 self.skip_ws();
730 match self.peek() {
731 Some(b'&') => {
732 self.pos += 1;
733 let rhs = self.parse_unary()?;
734 lhs = TsQueryAst::And(Box::new(lhs), Box::new(rhs));
735 }
736 Some(b'<') => {
737 self.pos += 1;
740 let n: u16 = if self.peek() == Some(b'-')
741 && self.bytes.get(self.pos + 1) == Some(&b'>')
742 {
743 self.pos += 2; 1
745 } else {
746 let start = self.pos;
747 while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_digit() {
748 self.pos += 1;
749 }
750 if start == self.pos || self.peek() != Some(b'>') {
751 return Err(EvalError::TypeMismatch {
752 detail: "tsquery literal: malformed <N> / <-> phrase operator"
753 .into(),
754 });
755 }
756 let val = core::str::from_utf8(&self.bytes[start..self.pos])
757 .expect("ascii digits")
758 .parse()
759 .map_err(|_| EvalError::TypeMismatch {
760 detail: "tsquery literal: phrase distance overflows u16".into(),
761 })?;
762 self.pos += 1; val
764 };
765 let rhs = self.parse_unary()?;
766 lhs = TsQueryAst::Phrase {
767 left: Box::new(lhs),
768 right: Box::new(rhs),
769 distance: n,
770 };
771 }
772 _ => return Ok(lhs),
773 }
774 }
775 }
776 fn parse_unary(&mut self) -> Result<TsQueryAst, EvalError> {
777 self.skip_ws();
778 if self.peek() == Some(b'!') {
779 self.pos += 1;
780 let inner = self.parse_unary()?;
781 return Ok(TsQueryAst::Not(Box::new(inner)));
782 }
783 self.parse_atom()
784 }
785 fn parse_atom(&mut self) -> Result<TsQueryAst, EvalError> {
786 self.skip_ws();
787 match self.peek() {
788 Some(b'(') => {
789 self.pos += 1;
790 let inner = self.parse_or()?;
791 self.skip_ws();
792 if self.peek() != Some(b')') {
793 return Err(EvalError::TypeMismatch {
794 detail: "tsquery literal: missing ')'".into(),
795 });
796 }
797 self.pos += 1;
798 Ok(inner)
799 }
800 Some(b'\'') => {
801 self.pos += 1;
802 let mut w = String::new();
803 loop {
804 match self.peek() {
805 None => {
806 return Err(EvalError::TypeMismatch {
807 detail: "tsquery literal: unterminated quoted lexeme".into(),
808 });
809 }
810 Some(b'\'') => {
811 if self.bytes.get(self.pos + 1) == Some(&b'\'') {
812 w.push('\'');
813 self.pos += 2;
814 } else {
815 self.pos += 1;
816 break;
817 }
818 }
819 Some(b) => {
820 w.push(b as char);
821 self.pos += 1;
822 }
823 }
824 }
825 let weight_mask = self.skip_weight_suffix();
828 Ok(TsQueryAst::Term {
829 word: w,
830 weight_mask,
831 })
832 }
833 Some(b) if b.is_ascii_alphanumeric() || b == b'_' => {
834 let start = self.pos;
835 while self.pos < self.bytes.len() {
836 let c = self.bytes[self.pos];
837 if c.is_ascii_alphanumeric() || c == b'_' {
838 self.pos += 1;
839 } else {
840 break;
841 }
842 }
843 let w = core::str::from_utf8(&self.bytes[start..self.pos])
844 .map_err(|_| EvalError::TypeMismatch {
845 detail: "tsquery literal: non-UTF-8 lexeme".into(),
846 })?
847 .to_string();
848 let weight_mask = self.skip_weight_suffix();
849 Ok(TsQueryAst::Term {
850 word: w,
851 weight_mask,
852 })
853 }
854 Some(b) => Err(EvalError::TypeMismatch {
855 detail: alloc::format!(
856 "tsquery literal: unexpected byte {:?} at offset {}",
857 b as char,
858 self.pos
859 ),
860 }),
861 None => Err(EvalError::TypeMismatch {
862 detail: "tsquery literal: expected term".into(),
863 }),
864 }
865 }
866 fn skip_weight_suffix(&mut self) -> u8 {
871 if self.peek() != Some(b':') {
872 return 0;
873 }
874 self.pos += 1;
875 let mut mask: u8 = 0;
876 while let Some(b) = self.peek() {
877 match b {
878 b'A' | b'a' => mask |= 1 << 3,
879 b'B' | b'b' => mask |= 1 << 2,
880 b'C' | b'c' => mask |= 1 << 1,
881 b'D' | b'd' => mask |= 1,
882 b'*' => mask |= 0x10,
883 _ if b.is_ascii_digit() => {}
884 _ => break,
885 }
886 self.pos += 1;
887 }
888 mask
889 }
890}
891
892pub(super) fn tsvector_concat(
893 l: &[spg_storage::TsLexeme],
894 r: &[spg_storage::TsLexeme],
895) -> Value<'static> {
896 let shift = l
897 .iter()
898 .flat_map(|x| x.positions.iter().copied())
899 .max()
900 .unwrap_or(0);
901 let mut out: Vec<spg_storage::TsLexeme> = l.to_vec();
902 for lex in r {
903 let shifted: Vec<u16> = lex
904 .positions
905 .iter()
906 .map(|p| p.saturating_add(shift))
907 .collect();
908 if let Some(existing) = out.iter_mut().find(|x| x.word == lex.word) {
909 existing.positions.extend(shifted);
910 existing.positions.sort_unstable();
911 existing.weight = existing.weight.max(lex.weight);
912 } else {
913 out.push(spg_storage::TsLexeme {
914 word: lex.word.clone(),
915 positions: shifted,
916 weight: lex.weight,
917 });
918 }
919 }
920 out.sort_by(|a, b| a.word.cmp(&b.word));
921 Value::TsVector(out)
922}
923
924pub(super) fn fts_ts_headline(
933 args: &[Value<'_>],
934 ctx: &EvalContext<'_>,
935) -> Result<Value<'static>, EvalError> {
936 let is_queryish = |v: &Value<'_>| matches!(v, Value::TsQuery(_));
938 let (config_arg, doc_arg, query_arg, opts_arg) = match args {
939 [d, q] => (None, d, q, None),
940 [d, q, o] if is_queryish(q) => (None, d, q, Some(o)),
941 [c, d, q] => (Some(c), d, q, None),
942 [c, d, q, o] => (Some(c), d, q, Some(o)),
943 _ => {
944 return Err(EvalError::TypeMismatch {
945 detail: format!("ts_headline() takes 2 to 4 args, got {}", args.len()),
946 });
947 }
948 };
949 if matches!(doc_arg, Value::Null) || matches!(query_arg, Value::Null) {
950 return Ok(Value::Null);
951 }
952 let config = match config_arg {
953 None => match ctx.default_text_search_config {
954 Some(name_str) => crate::fts::TsConfig::from_name(name_str).ok_or_else(|| {
955 EvalError::TypeMismatch {
956 detail: format!(
957 "text search config not implemented: {name_str:?} (supported: simple, english)"
958 ),
959 }
960 })?,
961 None => crate::fts::TsConfig::English,
962 },
963 Some(Value::Text(name_str)) => {
964 crate::fts::TsConfig::from_name(name_str).ok_or_else(|| EvalError::TypeMismatch {
965 detail: format!(
966 "text search config not implemented: {name_str:?} (supported: simple, english)"
967 ),
968 })?
969 }
970 Some(other) => {
971 return Err(EvalError::TypeMismatch {
972 detail: format!(
973 "ts_headline() config must be text, got {}",
974 crate::conversions::pg_type_name_for_error_opt(other.data_type())
975 ),
976 });
977 }
978 };
979 let doc = match doc_arg {
980 Value::Text(s) => s.as_ref(),
981 other => {
982 return Err(EvalError::TypeMismatch {
983 detail: format!(
984 "ts_headline() document must be text, got {}",
985 crate::conversions::pg_type_name_for_error_opt(other.data_type())
986 ),
987 });
988 }
989 };
990 let query = match query_arg {
991 Value::TsQuery(q) => q.clone(),
992 Value::Text(s) => crate::fts::to_tsquery(config, s)?,
995 other => {
996 return Err(EvalError::TypeMismatch {
997 detail: format!(
998 "ts_headline() query must be tsquery, got {}",
999 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1000 ),
1001 });
1002 }
1003 };
1004 let mut start_sel = String::from("<b>");
1008 let mut stop_sel = String::from("</b>");
1009 let mut max_words: usize = 35;
1010 let mut min_words: usize = 15;
1011 let mut max_fragments: usize = 0;
1012 let mut frag_delim = String::from(" ... ");
1013 let mut highlight_all = false;
1014 let mut short_word: usize = 3;
1015 if let Some(opts_v) = opts_arg {
1016 let opts = match opts_v {
1017 Value::Null => "",
1018 Value::Text(s) => s.as_ref(),
1019 other => {
1020 return Err(EvalError::TypeMismatch {
1021 detail: format!(
1022 "ts_headline() options must be text, got {}",
1023 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1024 ),
1025 });
1026 }
1027 };
1028 let parse_int = |v: &str| -> Result<i64, EvalError> {
1033 v.parse::<i64>().map_err(|_| EvalError::TypeMismatch {
1034 detail: alloc::format!("invalid input syntax for type integer: {v:?}"),
1035 })
1036 };
1037 let mut short_word_i: i64 = short_word as i64;
1038 let mut max_fragments_i: i64 = 0;
1039 let mut min_words_i: i64 = min_words as i64;
1040 let mut max_words_i: i64 = max_words as i64;
1041 for pair in opts.split(',') {
1042 if pair.trim().is_empty() {
1043 continue;
1044 }
1045 let Some((k, v)) = pair.split_once('=') else {
1046 return Err(EvalError::TypeMismatch {
1047 detail: alloc::format!("invalid parameter list format: {:?}", pair.trim()),
1048 });
1049 };
1050 let v = v.trim().trim_matches('"');
1051 if v.is_empty() {
1052 return Err(EvalError::TypeMismatch {
1053 detail: alloc::format!("invalid parameter list format: {:?}", pair.trim()),
1054 });
1055 }
1056 match k.trim().to_ascii_lowercase().as_str() {
1057 "startsel" => start_sel = v.to_string(),
1058 "stopsel" => stop_sel = v.to_string(),
1059 "maxwords" => max_words_i = parse_int(v)?,
1060 "minwords" => min_words_i = parse_int(v)?,
1061 "maxfragments" => max_fragments_i = parse_int(v)?,
1062 "shortword" => short_word_i = parse_int(v)?,
1063 "fragmentdelimiter" => frag_delim = v.to_string(),
1064 "highlightall" => {
1067 highlight_all = matches!(
1068 v.to_ascii_lowercase().as_str(),
1069 "1" | "on" | "t" | "true" | "y" | "yes"
1070 );
1071 }
1072 _ => {
1073 return Err(EvalError::TypeMismatch {
1074 detail: alloc::format!("unrecognized headline parameter: {:?}", k.trim()),
1075 });
1076 }
1077 }
1078 }
1079 if min_words_i >= max_words_i {
1082 return Err(EvalError::TypeMismatch {
1083 detail: "MinWords must be less than MaxWords".into(),
1084 });
1085 }
1086 if min_words_i <= 0 {
1087 return Err(EvalError::TypeMismatch {
1088 detail: "MinWords must be positive".into(),
1089 });
1090 }
1091 if short_word_i < 0 {
1092 return Err(EvalError::TypeMismatch {
1093 detail: "ShortWord must be >= 0".into(),
1094 });
1095 }
1096 if max_fragments_i < 0 {
1097 return Err(EvalError::TypeMismatch {
1098 detail: "MaxFragments must be >= 0".into(),
1099 });
1100 }
1101 max_words = max_words_i as usize;
1102 min_words = min_words_i as usize;
1103 short_word = short_word_i as usize;
1104 max_fragments = max_fragments_i as usize;
1105 }
1106 fn collect_positive(ast: &spg_storage::TsQueryAst, out: &mut Vec<String>) {
1108 match ast {
1109 spg_storage::TsQueryAst::Term { word, .. } => {
1110 if !word.is_empty() {
1111 out.push(word.clone());
1112 }
1113 }
1114 spg_storage::TsQueryAst::And(l, r) | spg_storage::TsQueryAst::Or(l, r) => {
1115 collect_positive(l, out);
1116 collect_positive(r, out);
1117 }
1118 spg_storage::TsQueryAst::Not(_) => {}
1119 spg_storage::TsQueryAst::Phrase { left, right, .. } => {
1120 collect_positive(left, out);
1121 collect_positive(right, out);
1122 }
1123 }
1124 }
1125 let mut terms: Vec<String> = Vec::new();
1126 collect_positive(&query, &mut terms);
1127 struct HlToken {
1132 word: String,
1133 lex: String,
1134 sep_after: String,
1135 is_match: bool,
1136 }
1137 let mut tokens: Vec<HlToken> = Vec::new();
1138 let mut leading_sep = String::new();
1139 let mut word = String::new();
1140 let mut push_word = |word: &mut String, tokens: &mut Vec<HlToken>| {
1141 if word.is_empty() {
1142 return;
1143 }
1144 let lowered: String = word.chars().flat_map(|c| c.to_lowercase()).collect();
1145 let lex = match config {
1146 crate::fts::TsConfig::Simple => lowered,
1147 crate::fts::TsConfig::English => crate::fts::porter_stem(&lowered),
1148 crate::fts::TsConfig::Spanish => crate::fts_es::stem_es(&lowered),
1149 crate::fts::TsConfig::French => crate::fts_fr::stem_fr(&lowered),
1150 crate::fts::TsConfig::German => crate::fts_de::stem_de(&lowered),
1151 };
1152 let is_match = terms.iter().any(|t| *t == lex);
1153 tokens.push(HlToken {
1154 word: core::mem::take(word),
1155 lex,
1156 sep_after: String::new(),
1157 is_match,
1158 });
1159 };
1160 for c in doc.chars() {
1161 if c.is_alphanumeric() || c == '_' {
1162 word.push(c);
1163 } else {
1164 push_word(&mut word, &mut tokens);
1165 match tokens.last_mut() {
1166 Some(t) => t.sep_after.push(c),
1167 None => leading_sep.push(c),
1168 }
1169 }
1170 }
1171 push_word(&mut word, &mut tokens);
1172 let render = |lo: usize, hi: usize| -> String {
1176 let mut out = String::new();
1177 for (i, t) in tokens[lo..hi].iter().enumerate() {
1178 if t.is_match {
1179 out.push_str(&start_sel);
1180 out.push_str(&t.word);
1181 out.push_str(&stop_sel);
1182 } else {
1183 out.push_str(&t.word);
1184 }
1185 if lo + i + 1 < hi {
1186 out.push_str(&t.sep_after);
1187 }
1188 }
1189 out
1190 };
1191 let n = tokens.len();
1192 let match_pos: Vec<usize> = tokens
1193 .iter()
1194 .enumerate()
1195 .filter_map(|(i, t)| t.is_match.then_some(i))
1196 .collect();
1197 if highlight_all || n <= min_words.max(1) {
1200 let mut out = leading_sep;
1201 out.push_str(&render(0, n));
1202 if let Some(t) = tokens.last() {
1203 out.push_str(&t.sep_after);
1204 }
1205 return Ok(Value::text(out));
1206 }
1207 if match_pos.is_empty() {
1211 return Ok(Value::text(render(0, min_words.max(1).min(n))));
1212 }
1213 if max_fragments > 0 {
1214 let interesting: Vec<bool> = tokens.iter().map(|t| t.is_match).collect();
1237 let is_bad_endpoint = |i: usize| -> bool {
1238 if interesting[i] {
1239 return false;
1240 }
1241 let w = &tokens[i].word;
1242 w.chars().count() <= short_word || w.chars().all(|c| c.is_ascii_digit())
1243 };
1244 fn and_groups(ast: &spg_storage::TsQueryAst, out: &mut Vec<Vec<String>>) {
1247 match ast {
1248 spg_storage::TsQueryAst::And(l, r) => {
1249 and_groups(l, out);
1250 and_groups(r, out);
1251 }
1252 spg_storage::TsQueryAst::Not(_) => {}
1253 other => {
1254 let mut g = Vec::new();
1255 fn collect(ast: &spg_storage::TsQueryAst, out: &mut Vec<String>) {
1257 match ast {
1258 spg_storage::TsQueryAst::Term { word, .. } => {
1259 if !word.is_empty() {
1260 out.push(word.clone());
1261 }
1262 }
1263 spg_storage::TsQueryAst::And(l, r)
1264 | spg_storage::TsQueryAst::Or(l, r) => {
1265 collect(l, out);
1266 collect(r, out);
1267 }
1268 spg_storage::TsQueryAst::Not(_) => {}
1269 spg_storage::TsQueryAst::Phrase { left, right, .. } => {
1270 collect(left, out);
1271 collect(right, out);
1272 }
1273 }
1274 }
1275 collect(other, &mut g);
1276 if !g.is_empty() {
1277 out.push(g);
1278 }
1279 }
1280 }
1281 }
1282 let mut groups: Vec<Vec<String>> = Vec::new();
1283 and_groups(&query, &mut groups);
1284 let group_pos: Vec<Vec<usize>> = groups
1285 .iter()
1286 .map(|g| {
1287 tokens
1288 .iter()
1289 .enumerate()
1290 .filter(|(_, t)| g.iter().any(|term| *term == t.lex))
1291 .map(|(i, _)| i)
1292 .collect()
1293 })
1294 .collect();
1295 struct Cand {
1297 st: usize,
1298 en: usize,
1299 curlen: usize,
1300 poslen: usize,
1301 chosen: bool,
1302 excluded: bool,
1303 }
1304 let mut cands: Vec<Cand> = Vec::new();
1305 if !group_pos.is_empty() && group_pos.iter().all(|ps| !ps.is_empty()) {
1306 let mut nextpos = 0usize;
1307 loop {
1308 let mut pose = 0usize;
1311 let mut dead = false;
1312 for ps in &group_pos {
1313 match ps.iter().find(|&&p| p >= nextpos) {
1314 Some(&p) => pose = pose.max(p),
1315 None => {
1316 dead = true;
1317 break;
1318 }
1319 }
1320 }
1321 if dead {
1322 break;
1323 }
1324 let mut posb = usize::MAX;
1325 for ps in &group_pos {
1326 if let Some(&p) = ps.iter().rev().find(|&&p| p <= pose) {
1327 posb = posb.min(p);
1328 }
1329 }
1330 let posb = posb.max(nextpos);
1331 let (mut st, en_cover) = (posb, pose);
1334 while st <= en_cover {
1335 let mut i = st;
1337 while i < en_cover && !interesting[i] {
1338 i += 1;
1339 }
1340 st = i;
1341 let mut curlen = 0usize;
1342 let mut poslen = 0usize;
1343 i = st;
1344 while i <= en_cover && curlen < max_words.max(1) {
1345 curlen += 1;
1346 if interesting[i] {
1347 poslen += 1;
1348 }
1349 i += 1;
1350 }
1351 let mut en = i - 1;
1353 if en < en_cover {
1354 while en > st && !interesting[en] {
1355 curlen -= 1;
1356 en -= 1;
1357 }
1358 }
1359 cands.push(Cand {
1360 st,
1361 en,
1362 curlen,
1363 poslen,
1364 chosen: false,
1365 excluded: false,
1366 });
1367 st = en + 1;
1368 }
1369 nextpos = posb + 1;
1370 }
1371 }
1372 let mut in_frag: Vec<bool> = alloc::vec![false; n];
1374 let mut picked = 0usize;
1375 for _ in 0..max_fragments {
1376 let mut best: Option<usize> = None;
1377 for (i, c) in cands.iter().enumerate() {
1378 if c.chosen || c.excluded {
1379 continue;
1380 }
1381 let better = match best {
1382 None => true,
1383 Some(b) => {
1384 c.poslen > cands[b].poslen
1385 || (c.poslen == cands[b].poslen && c.curlen < cands[b].curlen)
1386 }
1387 };
1388 if better {
1389 best = Some(i);
1390 }
1391 }
1392 let Some(bi) = best else { break };
1393 let (mut st, mut en, mut curlen) = (cands[bi].st, cands[bi].en, cands[bi].curlen);
1394 if curlen < max_words {
1395 let maxstretch = (max_words - curlen) / 2;
1398 let mut stretch = 0usize;
1399 let mut posmarker = st;
1400 let mut i = st;
1401 while i > 0 && stretch < maxstretch && !in_frag[i - 1] {
1402 i -= 1;
1403 curlen += 1;
1404 stretch += 1;
1405 posmarker = i;
1406 }
1407 let mut i = posmarker;
1409 while i < st && is_bad_endpoint(i) {
1410 curlen -= 1;
1411 i += 1;
1412 }
1413 st = i;
1414 let mut posmarker = en;
1416 let mut i = en + 1;
1417 while i < n && curlen < max_words && !in_frag[i] {
1418 curlen += 1;
1419 posmarker = i;
1420 i += 1;
1421 }
1422 let mut i = posmarker;
1424 while i > en && is_bad_endpoint(i) {
1425 curlen -= 1;
1426 i -= 1;
1427 }
1428 en = i;
1429 }
1430 cands[bi].st = st;
1431 cands[bi].en = en;
1432 cands[bi].curlen = curlen;
1433 cands[bi].chosen = true;
1434 for k in st..=en {
1435 in_frag[k] = true;
1436 }
1437 picked += 1;
1438 for (i, c) in cands.iter_mut().enumerate() {
1439 if i != bi
1440 && ((c.st >= st && c.st <= en)
1441 || (c.en >= st && c.en <= en)
1442 || (c.st < st && c.en > en))
1443 {
1444 c.excluded = true;
1445 }
1446 }
1447 }
1448 if picked == 0 {
1449 let hi = min_words.max(1).min(n);
1450 return Ok(Value::text(render(0, hi)));
1451 }
1452 let mut chosen: Vec<(usize, usize)> = cands
1453 .iter()
1454 .filter(|c| c.chosen)
1455 .map(|c| (c.st, c.en))
1456 .collect();
1457 chosen.sort_unstable();
1458 let parts: Vec<String> = chosen.iter().map(|&(st, en)| render(st, en + 1)).collect();
1459 return Ok(Value::text(parts.join(&frag_delim)));
1460 }
1461 let first = match_pos[0];
1466 let last = *match_pos.last().expect("non-empty");
1467 let mut lo = first;
1468 let mut hi = (last + 1).min(lo + max_words.max(1)).min(n);
1469 while hi - lo < min_words.max(1) && hi < n {
1470 hi += 1;
1471 }
1472 while hi - lo < min_words.max(1) && lo > 0 {
1473 lo -= 1;
1474 }
1475 Ok(Value::text(render(lo, hi)))
1476}
1477
1478pub(super) fn fts_ts_rewrite(
1486 args: &[Value<'_>],
1487 ctx: &EvalContext<'_>,
1488) -> Result<Value<'static>, EvalError> {
1489 if args.len() != 3 {
1490 return Err(EvalError::TypeMismatch {
1491 detail: format!(
1492 "ts_rewrite() takes 3 args (query, target, substitute), got {}",
1493 args.len()
1494 ),
1495 });
1496 }
1497 if args.iter().any(|a| matches!(a, Value::Null)) {
1498 return Ok(Value::Null);
1499 }
1500 let config = match ctx.default_text_search_config {
1501 Some(name_str) => {
1502 crate::fts::TsConfig::from_name(name_str).unwrap_or(crate::fts::TsConfig::English)
1503 }
1504 None => crate::fts::TsConfig::English,
1505 };
1506 let as_query = |v: &Value<'_>, which: &str| -> Result<spg_storage::TsQueryAst, EvalError> {
1507 match v {
1508 Value::TsQuery(q) => Ok(q.clone()),
1509 Value::Text(s) => crate::fts::to_tsquery(config, s),
1512 other => Err(EvalError::TypeMismatch {
1513 detail: format!(
1514 "ts_rewrite() {which} must be tsquery, got {}",
1515 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1516 ),
1517 }),
1518 }
1519 };
1520 let query = as_query(&args[0], "query")?;
1521 let target = as_query(&args[1], "target")?;
1522 let substitute = as_query(&args[2], "substitute")?;
1523 fn rewrite(
1524 node: &spg_storage::TsQueryAst,
1525 target: &spg_storage::TsQueryAst,
1526 substitute: &spg_storage::TsQueryAst,
1527 ) -> spg_storage::TsQueryAst {
1528 if node == target {
1529 return substitute.clone();
1530 }
1531 use spg_storage::TsQueryAst as A;
1532 match node {
1533 A::Term { .. } => node.clone(),
1534 A::And(l, r) => A::And(
1535 Box::new(rewrite(l, target, substitute)),
1536 Box::new(rewrite(r, target, substitute)),
1537 ),
1538 A::Or(l, r) => A::Or(
1539 Box::new(rewrite(l, target, substitute)),
1540 Box::new(rewrite(r, target, substitute)),
1541 ),
1542 A::Not(x) => A::Not(Box::new(rewrite(x, target, substitute))),
1543 A::Phrase {
1544 left,
1545 right,
1546 distance,
1547 } => A::Phrase {
1548 left: Box::new(rewrite(left, target, substitute)),
1549 right: Box::new(rewrite(right, target, substitute)),
1550 distance: *distance,
1551 },
1552 }
1553 }
1554 Ok(Value::TsQuery(rewrite(&query, &target, &substitute)))
1555}
1556
1557pub(super) fn fts_tsquery_bool(
1563 args: &[Value<'_>],
1564 ctx: &EvalContext<'_>,
1565 op: &str,
1566) -> Result<Value<'static>, EvalError> {
1567 let arity = if op == "not" { 1 } else { 2 };
1568 if args.len() != arity {
1569 return Err(EvalError::TypeMismatch {
1570 detail: format!("tsquery_{op}() takes {arity} arg(s), got {}", args.len()),
1571 });
1572 }
1573 if args.iter().any(|a| matches!(a, Value::Null)) {
1574 return Ok(Value::Null);
1575 }
1576 let config = match ctx.default_text_search_config {
1577 Some(name_str) => {
1578 crate::fts::TsConfig::from_name(name_str).unwrap_or(crate::fts::TsConfig::English)
1579 }
1580 None => crate::fts::TsConfig::English,
1581 };
1582 let as_query = |v: &Value<'_>| -> Result<spg_storage::TsQueryAst, EvalError> {
1583 match v {
1584 Value::TsQuery(q) => Ok(q.clone()),
1585 Value::Text(s) => crate::fts::to_tsquery(config, s),
1586 other => Err(EvalError::TypeMismatch {
1587 detail: format!(
1588 "tsquery_{op}() arguments must be tsquery, got {}",
1589 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1590 ),
1591 }),
1592 }
1593 };
1594 use spg_storage::TsQueryAst as A;
1595 let out = match op {
1596 "and" => A::And(Box::new(as_query(&args[0])?), Box::new(as_query(&args[1])?)),
1597 "or" => A::Or(Box::new(as_query(&args[0])?), Box::new(as_query(&args[1])?)),
1598 _ => A::Not(Box::new(as_query(&args[0])?)),
1599 };
1600 Ok(Value::TsQuery(out))
1601}