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 };
1149 let is_match = terms.iter().any(|t| *t == lex);
1150 tokens.push(HlToken {
1151 word: core::mem::take(word),
1152 lex,
1153 sep_after: String::new(),
1154 is_match,
1155 });
1156 };
1157 for c in doc.chars() {
1158 if c.is_alphanumeric() || c == '_' {
1159 word.push(c);
1160 } else {
1161 push_word(&mut word, &mut tokens);
1162 match tokens.last_mut() {
1163 Some(t) => t.sep_after.push(c),
1164 None => leading_sep.push(c),
1165 }
1166 }
1167 }
1168 push_word(&mut word, &mut tokens);
1169 let render = |lo: usize, hi: usize| -> String {
1173 let mut out = String::new();
1174 for (i, t) in tokens[lo..hi].iter().enumerate() {
1175 if t.is_match {
1176 out.push_str(&start_sel);
1177 out.push_str(&t.word);
1178 out.push_str(&stop_sel);
1179 } else {
1180 out.push_str(&t.word);
1181 }
1182 if lo + i + 1 < hi {
1183 out.push_str(&t.sep_after);
1184 }
1185 }
1186 out
1187 };
1188 let n = tokens.len();
1189 let match_pos: Vec<usize> = tokens
1190 .iter()
1191 .enumerate()
1192 .filter_map(|(i, t)| t.is_match.then_some(i))
1193 .collect();
1194 if highlight_all || n <= min_words.max(1) {
1197 let mut out = leading_sep;
1198 out.push_str(&render(0, n));
1199 if let Some(t) = tokens.last() {
1200 out.push_str(&t.sep_after);
1201 }
1202 return Ok(Value::text(out));
1203 }
1204 if match_pos.is_empty() {
1208 return Ok(Value::text(render(0, min_words.max(1).min(n))));
1209 }
1210 if max_fragments > 0 {
1211 let interesting: Vec<bool> = tokens.iter().map(|t| t.is_match).collect();
1234 let is_bad_endpoint = |i: usize| -> bool {
1235 if interesting[i] {
1236 return false;
1237 }
1238 let w = &tokens[i].word;
1239 w.chars().count() <= short_word || w.chars().all(|c| c.is_ascii_digit())
1240 };
1241 fn and_groups(ast: &spg_storage::TsQueryAst, out: &mut Vec<Vec<String>>) {
1244 match ast {
1245 spg_storage::TsQueryAst::And(l, r) => {
1246 and_groups(l, out);
1247 and_groups(r, out);
1248 }
1249 spg_storage::TsQueryAst::Not(_) => {}
1250 other => {
1251 let mut g = Vec::new();
1252 fn collect(ast: &spg_storage::TsQueryAst, out: &mut Vec<String>) {
1254 match ast {
1255 spg_storage::TsQueryAst::Term { word, .. } => {
1256 if !word.is_empty() {
1257 out.push(word.clone());
1258 }
1259 }
1260 spg_storage::TsQueryAst::And(l, r)
1261 | spg_storage::TsQueryAst::Or(l, r) => {
1262 collect(l, out);
1263 collect(r, out);
1264 }
1265 spg_storage::TsQueryAst::Not(_) => {}
1266 spg_storage::TsQueryAst::Phrase { left, right, .. } => {
1267 collect(left, out);
1268 collect(right, out);
1269 }
1270 }
1271 }
1272 collect(other, &mut g);
1273 if !g.is_empty() {
1274 out.push(g);
1275 }
1276 }
1277 }
1278 }
1279 let mut groups: Vec<Vec<String>> = Vec::new();
1280 and_groups(&query, &mut groups);
1281 let group_pos: Vec<Vec<usize>> = groups
1282 .iter()
1283 .map(|g| {
1284 tokens
1285 .iter()
1286 .enumerate()
1287 .filter(|(_, t)| g.iter().any(|term| *term == t.lex))
1288 .map(|(i, _)| i)
1289 .collect()
1290 })
1291 .collect();
1292 struct Cand {
1294 st: usize,
1295 en: usize,
1296 curlen: usize,
1297 poslen: usize,
1298 chosen: bool,
1299 excluded: bool,
1300 }
1301 let mut cands: Vec<Cand> = Vec::new();
1302 if !group_pos.is_empty() && group_pos.iter().all(|ps| !ps.is_empty()) {
1303 let mut nextpos = 0usize;
1304 loop {
1305 let mut pose = 0usize;
1308 let mut dead = false;
1309 for ps in &group_pos {
1310 match ps.iter().find(|&&p| p >= nextpos) {
1311 Some(&p) => pose = pose.max(p),
1312 None => {
1313 dead = true;
1314 break;
1315 }
1316 }
1317 }
1318 if dead {
1319 break;
1320 }
1321 let mut posb = usize::MAX;
1322 for ps in &group_pos {
1323 if let Some(&p) = ps.iter().rev().find(|&&p| p <= pose) {
1324 posb = posb.min(p);
1325 }
1326 }
1327 let posb = posb.max(nextpos);
1328 let (mut st, en_cover) = (posb, pose);
1331 while st <= en_cover {
1332 let mut i = st;
1334 while i < en_cover && !interesting[i] {
1335 i += 1;
1336 }
1337 st = i;
1338 let mut curlen = 0usize;
1339 let mut poslen = 0usize;
1340 i = st;
1341 while i <= en_cover && curlen < max_words.max(1) {
1342 curlen += 1;
1343 if interesting[i] {
1344 poslen += 1;
1345 }
1346 i += 1;
1347 }
1348 let mut en = i - 1;
1350 if en < en_cover {
1351 while en > st && !interesting[en] {
1352 curlen -= 1;
1353 en -= 1;
1354 }
1355 }
1356 cands.push(Cand {
1357 st,
1358 en,
1359 curlen,
1360 poslen,
1361 chosen: false,
1362 excluded: false,
1363 });
1364 st = en + 1;
1365 }
1366 nextpos = posb + 1;
1367 }
1368 }
1369 let mut in_frag: Vec<bool> = alloc::vec![false; n];
1371 let mut picked = 0usize;
1372 for _ in 0..max_fragments {
1373 let mut best: Option<usize> = None;
1374 for (i, c) in cands.iter().enumerate() {
1375 if c.chosen || c.excluded {
1376 continue;
1377 }
1378 let better = match best {
1379 None => true,
1380 Some(b) => {
1381 c.poslen > cands[b].poslen
1382 || (c.poslen == cands[b].poslen && c.curlen < cands[b].curlen)
1383 }
1384 };
1385 if better {
1386 best = Some(i);
1387 }
1388 }
1389 let Some(bi) = best else { break };
1390 let (mut st, mut en, mut curlen) = (cands[bi].st, cands[bi].en, cands[bi].curlen);
1391 if curlen < max_words {
1392 let maxstretch = (max_words - curlen) / 2;
1395 let mut stretch = 0usize;
1396 let mut posmarker = st;
1397 let mut i = st;
1398 while i > 0 && stretch < maxstretch && !in_frag[i - 1] {
1399 i -= 1;
1400 curlen += 1;
1401 stretch += 1;
1402 posmarker = i;
1403 }
1404 let mut i = posmarker;
1406 while i < st && is_bad_endpoint(i) {
1407 curlen -= 1;
1408 i += 1;
1409 }
1410 st = i;
1411 let mut posmarker = en;
1413 let mut i = en + 1;
1414 while i < n && curlen < max_words && !in_frag[i] {
1415 curlen += 1;
1416 posmarker = i;
1417 i += 1;
1418 }
1419 let mut i = posmarker;
1421 while i > en && is_bad_endpoint(i) {
1422 curlen -= 1;
1423 i -= 1;
1424 }
1425 en = i;
1426 }
1427 cands[bi].st = st;
1428 cands[bi].en = en;
1429 cands[bi].curlen = curlen;
1430 cands[bi].chosen = true;
1431 for k in st..=en {
1432 in_frag[k] = true;
1433 }
1434 picked += 1;
1435 for (i, c) in cands.iter_mut().enumerate() {
1436 if i != bi
1437 && ((c.st >= st && c.st <= en)
1438 || (c.en >= st && c.en <= en)
1439 || (c.st < st && c.en > en))
1440 {
1441 c.excluded = true;
1442 }
1443 }
1444 }
1445 if picked == 0 {
1446 let hi = min_words.max(1).min(n);
1447 return Ok(Value::text(render(0, hi)));
1448 }
1449 let mut chosen: Vec<(usize, usize)> = cands
1450 .iter()
1451 .filter(|c| c.chosen)
1452 .map(|c| (c.st, c.en))
1453 .collect();
1454 chosen.sort_unstable();
1455 let parts: Vec<String> = chosen.iter().map(|&(st, en)| render(st, en + 1)).collect();
1456 return Ok(Value::text(parts.join(&frag_delim)));
1457 }
1458 let first = match_pos[0];
1463 let last = *match_pos.last().expect("non-empty");
1464 let mut lo = first;
1465 let mut hi = (last + 1).min(lo + max_words.max(1)).min(n);
1466 while hi - lo < min_words.max(1) && hi < n {
1467 hi += 1;
1468 }
1469 while hi - lo < min_words.max(1) && lo > 0 {
1470 lo -= 1;
1471 }
1472 Ok(Value::text(render(lo, hi)))
1473}
1474
1475pub(super) fn fts_ts_rewrite(
1483 args: &[Value<'_>],
1484 ctx: &EvalContext<'_>,
1485) -> Result<Value<'static>, EvalError> {
1486 if args.len() != 3 {
1487 return Err(EvalError::TypeMismatch {
1488 detail: format!(
1489 "ts_rewrite() takes 3 args (query, target, substitute), got {}",
1490 args.len()
1491 ),
1492 });
1493 }
1494 if args.iter().any(|a| matches!(a, Value::Null)) {
1495 return Ok(Value::Null);
1496 }
1497 let config = match ctx.default_text_search_config {
1498 Some(name_str) => {
1499 crate::fts::TsConfig::from_name(name_str).unwrap_or(crate::fts::TsConfig::English)
1500 }
1501 None => crate::fts::TsConfig::English,
1502 };
1503 let as_query = |v: &Value<'_>, which: &str| -> Result<spg_storage::TsQueryAst, EvalError> {
1504 match v {
1505 Value::TsQuery(q) => Ok(q.clone()),
1506 Value::Text(s) => crate::fts::to_tsquery(config, s),
1509 other => Err(EvalError::TypeMismatch {
1510 detail: format!(
1511 "ts_rewrite() {which} must be tsquery, got {}",
1512 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1513 ),
1514 }),
1515 }
1516 };
1517 let query = as_query(&args[0], "query")?;
1518 let target = as_query(&args[1], "target")?;
1519 let substitute = as_query(&args[2], "substitute")?;
1520 fn rewrite(
1521 node: &spg_storage::TsQueryAst,
1522 target: &spg_storage::TsQueryAst,
1523 substitute: &spg_storage::TsQueryAst,
1524 ) -> spg_storage::TsQueryAst {
1525 if node == target {
1526 return substitute.clone();
1527 }
1528 use spg_storage::TsQueryAst as A;
1529 match node {
1530 A::Term { .. } => node.clone(),
1531 A::And(l, r) => A::And(
1532 Box::new(rewrite(l, target, substitute)),
1533 Box::new(rewrite(r, target, substitute)),
1534 ),
1535 A::Or(l, r) => A::Or(
1536 Box::new(rewrite(l, target, substitute)),
1537 Box::new(rewrite(r, target, substitute)),
1538 ),
1539 A::Not(x) => A::Not(Box::new(rewrite(x, target, substitute))),
1540 A::Phrase {
1541 left,
1542 right,
1543 distance,
1544 } => A::Phrase {
1545 left: Box::new(rewrite(left, target, substitute)),
1546 right: Box::new(rewrite(right, target, substitute)),
1547 distance: *distance,
1548 },
1549 }
1550 }
1551 Ok(Value::TsQuery(rewrite(&query, &target, &substitute)))
1552}
1553
1554pub(super) fn fts_tsquery_bool(
1560 args: &[Value<'_>],
1561 ctx: &EvalContext<'_>,
1562 op: &str,
1563) -> Result<Value<'static>, EvalError> {
1564 let arity = if op == "not" { 1 } else { 2 };
1565 if args.len() != arity {
1566 return Err(EvalError::TypeMismatch {
1567 detail: format!("tsquery_{op}() takes {arity} arg(s), got {}", args.len()),
1568 });
1569 }
1570 if args.iter().any(|a| matches!(a, Value::Null)) {
1571 return Ok(Value::Null);
1572 }
1573 let config = match ctx.default_text_search_config {
1574 Some(name_str) => {
1575 crate::fts::TsConfig::from_name(name_str).unwrap_or(crate::fts::TsConfig::English)
1576 }
1577 None => crate::fts::TsConfig::English,
1578 };
1579 let as_query = |v: &Value<'_>| -> Result<spg_storage::TsQueryAst, EvalError> {
1580 match v {
1581 Value::TsQuery(q) => Ok(q.clone()),
1582 Value::Text(s) => crate::fts::to_tsquery(config, s),
1583 other => Err(EvalError::TypeMismatch {
1584 detail: format!(
1585 "tsquery_{op}() arguments must be tsquery, got {}",
1586 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1587 ),
1588 }),
1589 }
1590 };
1591 use spg_storage::TsQueryAst as A;
1592 let out = match op {
1593 "and" => A::And(Box::new(as_query(&args[0])?), Box::new(as_query(&args[1])?)),
1594 "or" => A::Or(Box::new(as_query(&args[0])?), Box::new(as_query(&args[1])?)),
1595 _ => A::Not(Box::new(as_query(&args[0])?)),
1596 };
1597 Ok(Value::TsQuery(out))
1598}