1use std::cell::RefCell;
20
21use super::{ArithOp, Builtin, FExpr, Op, QueryOutput};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub(crate) enum Site {
28 Fn(Builtin),
29 Op(&'static str),
31}
32
33impl From<Builtin> for Site {
34 fn from(f: Builtin) -> Self {
35 Site::Fn(f)
36 }
37}
38
39impl Site {
40 pub(crate) fn arith(op: ArithOp) -> Site {
41 Site::Op(match op {
42 ArithOp::Add => "+",
43 ArithOp::Sub => "-",
44 ArithOp::Mul => "*",
45 ArithOp::Div => "/",
46 })
47 }
48
49 pub(crate) fn compare(op: Op) -> Site {
50 Site::Op(match op {
51 Op::Eq => "=",
52 Op::Ne => "!=",
53 Op::Lt => "<",
54 Op::Le => "<=",
55 Op::Gt => ">",
56 Op::Ge => ">=",
57 })
58 }
59
60 fn name(self) -> &'static str {
61 match self {
62 Site::Fn(f) => fn_name(f),
63 Site::Op(sym) => sym,
64 }
65 }
66
67 fn family(self) -> Family {
69 match self {
70 Site::Fn(Builtin::Abs | Builtin::Ceil | Builtin::Floor | Builtin::Round) => {
71 Family::Numeric
72 }
73 Site::Op("+" | "-" | "*" | "/") => Family::Numeric,
74 Site::Op(_) => Family::Compare,
75 Site::Fn(Builtin::Lang | Builtin::Datatype) => Family::LiteralAccessor,
76 Site::Fn(Builtin::StrDt | Builtin::StrLang) => Family::Constructor,
77 Site::Fn(_) => Family::Other,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83enum Family {
84 Numeric,
85 Compare,
86 LiteralAccessor,
87 Constructor,
88 Other,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[non_exhaustive]
94pub enum WarningSeverity {
95 TypeError,
99 Hint,
102}
103
104impl WarningSeverity {
105 pub fn as_str(self) -> &'static str {
107 match self {
108 WarningSeverity::TypeError => "type-error",
109 WarningSeverity::Hint => "hint",
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
117#[non_exhaustive]
118pub struct QueryWarning {
119 pub severity: WarningSeverity,
120 pub function: String,
124 pub argument: usize,
127 pub kind: String,
135 pub count: u64,
138 pub sample: Option<String>,
140 pub hint: String,
142 pub message: String,
144}
145
146const SAMPLE_CHARS: usize = 80;
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub(crate) enum ArgKind {
151 Iri,
152 Blank,
153 QuotedTriple,
154 Numeric,
155 TypedLiteral,
156 LangMismatch,
157 Unbound,
158 InvalidRegex,
159 NotDatetime,
160 StringLit,
163 LangTagged,
165 InvalidNumber,
167 DivByZero,
169 Incomparable,
173 StringVsNumber,
176}
177
178impl ArgKind {
179 fn as_str(self) -> &'static str {
180 match self {
181 ArgKind::Iri => "iri",
182 ArgKind::Blank => "blank-node",
183 ArgKind::QuotedTriple => "quoted-triple",
184 ArgKind::Numeric => "numeric",
185 ArgKind::TypedLiteral => "typed-literal",
186 ArgKind::LangMismatch => "language-mismatch",
187 ArgKind::Unbound => "unbound",
188 ArgKind::InvalidRegex => "invalid-regex",
189 ArgKind::NotDatetime => "not-a-datetime",
190 ArgKind::StringLit => "string",
191 ArgKind::LangTagged => "language-tagged",
192 ArgKind::InvalidNumber => "invalid-number",
193 ArgKind::DivByZero => "division-by-zero",
194 ArgKind::Incomparable => "incomparable",
195 ArgKind::StringVsNumber => "string-vs-number",
196 }
197 }
198}
199
200struct Entry {
201 site: Site,
202 pos: usize,
203 kind: ArgKind,
204 count: u64,
205 sample: String,
206 detail: String,
208}
209
210pub(crate) const MISS_CONTAINS: u8 = 1;
212pub(crate) const MISS_STRSTARTS: u8 = 2;
213pub(crate) const MISS_STRENDS: u8 = 4;
214pub(crate) const MISS_REGEX: u8 = 8;
215
216#[derive(Default)]
217struct Sink {
218 entries: Vec<Entry>,
219 misses: Vec<(Builtin, String)>,
222}
223
224thread_local! {
225 static SINK: RefCell<Sink> = RefCell::new(Sink::default());
226}
227
228pub(crate) fn begin() {
230 SINK.with(|s| {
231 let mut s = s.borrow_mut();
232 s.entries.clear();
233 s.misses.clear();
234 });
235}
236
237#[cold]
240#[inline(never)]
241pub(crate) fn record(
242 site: impl Into<Site>,
243 pos: usize,
244 kind: ArgKind,
245 describe: impl FnOnce() -> (String, String),
246) {
247 let site = site.into();
248 SINK.with(|s| {
249 let mut s = s.borrow_mut();
250 if let Some(e) = s
251 .entries
252 .iter_mut()
253 .find(|e| e.site == site && e.pos == pos && e.kind == kind)
254 {
255 e.count += 1;
256 return;
257 }
258 let (sample, detail) = describe();
259 s.entries.push(Entry {
260 site,
261 pos,
262 kind,
263 count: 1,
264 sample: truncate(&sample),
265 detail,
266 });
267 });
268}
269
270#[cold]
274#[inline(never)]
275pub(crate) fn note_no_match(func: Builtin, needle: &FExpr) {
276 if let FExpr::Const(c) = needle {
277 let lex = crate::terms::literal_lexical(c).unwrap_or_else(|| c.clone());
278 SINK.with(|s| s.borrow_mut().misses.push((func, lex)));
279 }
280}
281
282#[cold]
284#[inline(never)]
285pub(crate) fn bad_arg(func: Builtin, pos: usize, token: &str) {
286 record(func, pos, kind_of(token), || {
287 (token.to_string(), detail_of(token))
288 });
289}
290
291#[cold]
294#[inline(never)]
295pub(crate) fn lang_mismatch(func: Builtin, text: &str, pat: &str) {
296 record(func, 2, ArgKind::LangMismatch, || {
297 let tag = |t: &str| match crate::terms::lang_tag(t).filter(|l| !l.is_empty()) {
298 Some(l) => format!("@{l}"),
299 None => "no tag".to_string(),
300 };
301 (pat.to_string(), format!("{} vs {}", tag(pat), tag(text)))
302 });
303}
304
305#[cold]
307#[inline(never)]
308pub(crate) fn unbound(site: impl Into<Site>, pos: usize, var: &str) {
309 record(site, pos, ArgKind::Unbound, || {
310 (format!("?{var}"), String::new())
311 });
312}
313
314#[cold]
316#[inline(never)]
317pub(crate) fn invalid_regex(func: Builtin, pattern: &str, flags: &str) {
318 record(func, 2, ArgKind::InvalidRegex, || {
319 (
320 pattern.to_string(),
321 crate::row::Resolver::regex_error(pattern, flags),
322 )
323 });
324}
325
326#[cold]
328#[inline(never)]
329pub(crate) fn bad_datetime(func: Builtin, token: &str) {
330 record(func, 1, ArgKind::NotDatetime, || {
331 let detail = match crate::terms::literal_datatype(token) {
332 Some(dt) if token.starts_with('"') => short_dt(&dt),
333 _ => kind_of(token).as_str().to_string(),
334 };
335 (token.to_string(), detail)
336 });
337}
338
339#[cold]
343#[inline(never)]
344pub(crate) fn not_a_number(site: impl Into<Site>, pos: usize, token: &str) {
345 let kind = match term_kind(token) {
346 ArgKind::Numeric => ArgKind::InvalidNumber,
347 k => k,
348 };
349 record(site, pos, kind, || {
350 (token.to_string(), detail_of_any(token))
351 });
352}
353
354#[cold]
357#[inline(never)]
358pub(crate) fn incomparable(site: Site, a: &str, b: &str) {
359 record(site, 0, ArgKind::Incomparable, || {
360 (
361 format!("{a} vs {b}"),
362 format!("{} vs {}", operand_kind(a), operand_kind(b)),
363 )
364 });
365}
366
367pub(crate) fn string_vs_number(site: Site, a: &str, b: &str) {
371 let (s, n) = if a.starts_with('"') && super::value::numeric(a).is_none() {
372 (a, b)
373 } else {
374 (b, a)
375 };
376 let (Some(text), Some(num)) = (crate::terms::as_number(s), super::value::numeric(n)) else {
377 return;
378 };
379 if text == num {
380 record(site, 0, ArgKind::StringVsNumber, || {
381 (format!("{a} vs {b}"), String::new())
382 });
383 }
384}
385
386fn operand_kind(token: &str) -> String {
388 use super::value::{classify, Class};
389 match classify(token) {
390 Class::Blank => "a blank node".into(),
391 Class::Iri => "an IRI".into(),
392 Class::Triple => "a quoted triple".into(),
393 Class::Str => "a string".into(),
394 Class::Lang => match crate::terms::lang_tag(token) {
395 Some(l) if !l.is_empty() => format!("a language-tagged literal (@{l})"),
396 _ => "a language-tagged literal".into(),
397 },
398 Class::Other => match crate::terms::literal_datatype(token) {
399 Some(dt) if super::value::compared_by_value(&dt) => {
400 format!("an invalid {} literal", short_dt(&dt))
401 }
402 Some(dt) => format!("a literal of datatype {}", short_dt(&dt)),
403 None => "a literal".into(),
404 },
405 _ => match crate::terms::literal_datatype(token) {
406 Some(dt) => short_dt(&dt),
407 None => "a number".into(),
408 },
409 }
410}
411
412#[cold]
414#[inline(never)]
415pub(crate) fn div_by_zero(token: &str) {
416 record(Site::Op("/"), 2, ArgKind::DivByZero, || {
417 (token.to_string(), detail_of(token))
418 });
419}
420
421#[cold]
423#[inline(never)]
424pub(crate) fn not_a_literal(func: Builtin, token: &str) {
425 record(func, 1, kind_of(token), || {
426 (token.to_string(), String::new())
427 });
428}
429
430#[cold]
433#[inline(never)]
434pub(crate) fn bad_constructor_arg(func: Builtin, pos: usize, token: &str) {
435 record(func, pos, term_kind(token), || {
436 (token.to_string(), detail_of_any(token))
437 });
438}
439
440fn term_kind(token: &str) -> ArgKind {
443 if !token.starts_with('"') {
444 return kind_of(token);
445 }
446 if crate::terms::lang_tag(token).is_some_and(|l| !l.is_empty()) {
447 return ArgKind::LangTagged;
448 }
449 match crate::terms::literal_datatype(token) {
450 Some(dt) if dt == "http://www.w3.org/2001/XMLSchema#string" => ArgKind::StringLit,
451 None => ArgKind::StringLit,
452 Some(dt) if is_numeric_datatype(&dt) => ArgKind::Numeric,
453 Some(_) => ArgKind::TypedLiteral,
454 }
455}
456
457fn detail_of_any(token: &str) -> String {
459 match crate::terms::lang_tag(token).filter(|l| !l.is_empty()) {
460 Some(l) if token.starts_with('"') => format!("@{l}"),
461 _ => detail_of(token),
462 }
463}
464
465fn kind_of(token: &str) -> ArgKind {
468 if token.starts_with("<<") {
469 ArgKind::QuotedTriple
470 } else if token.starts_with('<') {
471 ArgKind::Iri
472 } else if token.starts_with("_:") {
473 ArgKind::Blank
474 } else if crate::terms::literal_datatype(token).is_some_and(|dt| is_numeric_datatype(&dt)) {
475 ArgKind::Numeric
476 } else {
477 ArgKind::TypedLiteral
478 }
479}
480
481fn detail_of(token: &str) -> String {
483 match crate::terms::literal_datatype(token) {
484 Some(dt) if token.starts_with('"') => short_dt(&dt),
485 _ => String::new(),
486 }
487}
488
489fn is_numeric_datatype(dt: &str) -> bool {
490 dt.strip_prefix("http://www.w3.org/2001/XMLSchema#")
491 .is_some_and(|local| {
492 matches!(
493 local,
494 "integer"
495 | "decimal"
496 | "float"
497 | "double"
498 | "int"
499 | "long"
500 | "short"
501 | "byte"
502 | "nonNegativeInteger"
503 | "nonPositiveInteger"
504 | "negativeInteger"
505 | "positiveInteger"
506 | "unsignedInt"
507 | "unsignedLong"
508 | "unsignedShort"
509 | "unsignedByte"
510 )
511 })
512}
513
514fn short_dt(dt: &str) -> String {
515 match dt.strip_prefix("http://www.w3.org/2001/XMLSchema#") {
516 Some(local) => format!("xsd:{local}"),
517 None => format!("<{dt}>"),
518 }
519}
520
521fn truncate(s: &str) -> String {
522 match s.char_indices().nth(SAMPLE_CHARS) {
523 Some((i, _)) => format!("{}…", &s[..i]),
524 None => s.to_string(),
525 }
526}
527
528pub(crate) fn fn_name(f: Builtin) -> &'static str {
530 match f {
531 Builtin::Str => "STR",
532 Builtin::StrLen => "STRLEN",
533 Builtin::UCase => "UCASE",
534 Builtin::LCase => "LCASE",
535 Builtin::Concat => "CONCAT",
536 Builtin::SubStr => "SUBSTR",
537 Builtin::StrBefore => "STRBEFORE",
538 Builtin::StrAfter => "STRAFTER",
539 Builtin::Contains => "CONTAINS",
540 Builtin::StrStarts => "STRSTARTS",
541 Builtin::StrEnds => "STRENDS",
542 Builtin::Regex => "REGEX",
543 Builtin::Replace => "REPLACE",
544 Builtin::EncodeForUri => "ENCODE_FOR_URI",
545 Builtin::Md5 => "MD5",
546 Builtin::Sha1 => "SHA1",
547 Builtin::Sha256 => "SHA256",
548 Builtin::Sha384 => "SHA384",
549 Builtin::Sha512 => "SHA512",
550 Builtin::Year => "YEAR",
551 Builtin::Month => "MONTH",
552 Builtin::Day => "DAY",
553 Builtin::Hours => "HOURS",
554 Builtin::Minutes => "MINUTES",
555 Builtin::Seconds => "SECONDS",
556 Builtin::Timezone => "TIMEZONE",
557 Builtin::Tz => "TZ",
558 Builtin::Abs => "ABS",
559 Builtin::Ceil => "CEIL",
560 Builtin::Floor => "FLOOR",
561 Builtin::Round => "ROUND",
562 Builtin::Lang => "LANG",
563 Builtin::Datatype => "DATATYPE",
564 Builtin::StrDt => "STRDT",
565 Builtin::StrLang => "STRLANG",
566 _ => "function",
567 }
568}
569
570fn ordinal(site: Site, pos: usize) -> String {
571 match (site, pos) {
572 (Site::Op(_), 1) => "the left operand".to_string(),
573 (Site::Op(_), 2) => "the right operand".to_string(),
574 _ => format!("argument {pos}"),
575 }
576}
577
578fn rows(n: u64) -> String {
579 if n == 1 {
580 "1 row".to_string()
581 } else {
582 format!("{n} rows")
583 }
584}
585
586fn family_hint(family: Family, pos: usize, kind: ArgKind) -> Option<&'static str> {
589 use ArgKind::*;
590 Some(match (family, kind) {
591 (
592 _,
593 Unbound | InvalidRegex | NotDatetime | LangMismatch | DivByZero | Incomparable
594 | StringVsNumber,
595 ) => return None,
596 (Family::Numeric, StringLit) => {
597 "a string is not a number; cast one that holds a number first, e.g. \
598 xsd:decimal(?x), or restrict the variable with isNumeric()"
599 }
600 (Family::Numeric, InvalidNumber) => {
601 "its text is not a valid number for its datatype; restrict the variable with \
602 isNumeric(), which checks the lexical form"
603 }
604 (Family::Numeric, _) => "it is not a number; restrict the variable with isNumeric()",
605 (Family::LiteralAccessor, _) => {
606 "LANG() and DATATYPE() take a literal; restrict the variable with isLiteral()"
607 }
608 (Family::Constructor, Blank | QuotedTriple) if pos == 1 => {
609 "argument 1 must be a simple literal; restrict the variable with isLiteral()"
610 }
611 (Family::Constructor, _) if pos == 1 => {
612 "argument 1 must be a simple literal (no language tag, no datatype); use STR() on \
613 it to take its lexical form"
614 }
615 (Family::Constructor, _) => {
616 "argument 2 of STRDT must be a datatype IRI, e.g. xsd:integer; IRI(?x) turns a \
617 string into one"
618 }
619 (Family::Compare | Family::Other, _) => return None,
620 })
621}
622
623fn string_and_number(detail: &str) -> bool {
625 let numeric = |k: &str| {
626 k.strip_prefix("xsd:")
627 .is_some_and(|l| is_numeric_datatype(&format!("http://www.w3.org/2001/XMLSchema#{l}")))
628 || k == "a number"
629 };
630 match detail.split_once(" vs ") {
631 Some((l, r)) => (l == "a string" && numeric(r)) || (numeric(l) && r == "a string"),
632 None => false,
633 }
634}
635
636impl Entry {
637 fn into_comparison_warning(self) -> QueryWarning {
640 let name = self.site.name();
641 let n = rows(self.count);
642 let sample = (!self.sample.is_empty()).then(|| format!("; e.g. {}", self.sample));
643 let (severity, hint, message) = if self.kind == ArgKind::StringVsNumber {
644 let hint = "SPARQL never reads a string as a number; to compare values cast \
645 the string, e.g. xsd:integer(?x) or xsd:decimal(?x), or to compare \
646 text use STR() on the number"
647 .to_string();
648 let message = format!(
649 "Operator {name} compared a string with a number in {n} and found them \
650 unequal although the string's text is that number{}. Hint: {hint}.",
651 sample.unwrap_or_default()
652 );
653 (WarningSeverity::Hint, hint, message)
654 } else {
655 let hint = if string_and_number(&self.detail) {
656 "a string is never compared as a number; cast it first, e.g. \
657 xsd:integer(?x) or xsd:decimal(?x), or compare the text of both sides \
658 with STR()"
659 } else if self.detail.contains("an invalid ") {
660 "a value whose text is not valid for its datatype (an ill-typed literal, \
661 such as a year of fewer than four digits typed xsd:gYear) compares with \
662 nothing; fix the data, or compare the text of both sides with STR()"
663 } else {
664 "SPARQL orders only two numbers, two strings (with the same language tag), \
665 two booleans or two date/time values of one type, and = cannot tell two \
666 different literals of an unrecognised datatype apart; cast both sides to \
667 one type, or compare their text with STR()"
668 }
669 .to_string();
670 let message = format!(
671 "Operator {name} compared {} in {n}: a SPARQL type error, which makes a \
672 FILTER drop the row, even under ! (BIND leaves the variable unbound){}. \
673 Hint: {hint}.",
674 self.detail.replacen(" vs ", " with ", 1),
675 sample.unwrap_or_default()
676 );
677 (WarningSeverity::TypeError, hint, message)
678 };
679 QueryWarning {
680 severity,
681 function: name.to_string(),
682 argument: 0,
683 kind: self.kind.as_str().to_string(),
684 count: self.count,
685 sample: (!self.sample.is_empty()).then_some(self.sample),
686 hint,
687 message,
688 }
689 }
690
691 fn into_warning(self) -> QueryWarning {
692 if matches!(self.kind, ArgKind::Incomparable | ArgKind::StringVsNumber) {
693 return self.into_comparison_warning();
694 }
695 let name = self.site.name();
696 let arg = ordinal(self.site, self.pos);
697 let n = rows(self.count);
698 let dt = |what: &str| {
699 if self.detail.is_empty() {
700 what.to_string()
701 } else {
702 format!("{what} ({})", self.detail)
703 }
704 };
705 let (what, hint) = match self.kind {
706 ArgKind::Iri => (
707 "an IRI".to_string(),
708 "wrap it in STR() to match the IRI's text".to_string(),
709 ),
710 ArgKind::Blank => (
711 "a blank node".to_string(),
712 "blank nodes are not strings; restrict the variable with isLiteral()".to_string(),
713 ),
714 ArgKind::QuotedTriple => (
715 "a quoted triple".to_string(),
716 "quoted triples are not strings; restrict the variable with isLiteral()"
717 .to_string(),
718 ),
719 ArgKind::Numeric => (
720 dt("a number"),
721 "wrap it in STR() to match its lexical form".to_string(),
722 ),
723 ArgKind::TypedLiteral => (
724 dt("a non-string typed literal"),
725 "wrap it in STR() to match its lexical form".to_string(),
726 ),
727 ArgKind::StringLit => (
728 "a string".to_string(),
729 "wrap it in STR() to match its lexical form".to_string(),
730 ),
731 ArgKind::LangTagged => (
732 dt("a language-tagged literal"),
733 "use STR() on it to drop the language tag".to_string(),
734 ),
735 ArgKind::InvalidNumber => (
736 dt("a literal whose text is not a valid number for its datatype"),
737 String::new(),
738 ),
739 ArgKind::LangMismatch => (
740 format!(
741 "a literal whose language tag does not match argument 1 ({})",
742 self.detail
743 ),
744 "the language tags differ; use STR() on both arguments".to_string(),
745 ),
746 ArgKind::Unbound => (
747 format!("an unbound variable ({})", self.sample),
748 "the variable has no value in those rows (an OPTIONAL that did not match, \
749 or a misspelled variable); use BOUND() or COALESCE() if that is intended"
750 .to_string(),
751 ),
752 ArgKind::InvalidRegex => (
753 "an invalid regex pattern".to_string(),
754 format!(
755 "invalid regex pattern ({}); rete uses Rust regex syntax, which has no \
756 look-around or back-references",
757 self.detail
758 ),
759 ),
760 ArgKind::NotDatetime => (
761 dt("a value that is not an xsd:dateTime"),
762 "this function needs an xsd:dateTime; for a year in a plain or xsd:date value \
763 use SUBSTR(STR(?x), 1, 4)"
764 .to_string(),
765 ),
766 ArgKind::DivByZero => (
767 "zero".to_string(),
768 "guard the divisor, e.g. IF(?d != 0, ?n / ?d, 0), or FILTER(?d != 0) first"
769 .to_string(),
770 ),
771 ArgKind::Incomparable | ArgKind::StringVsNumber => {
772 unreachable!("handled by into_comparison_warning")
773 }
774 };
775 let hint = match family_hint(self.site.family(), self.pos, self.kind) {
776 Some(h) => h.to_string(),
777 None => hint,
778 };
779 let mut message = match (self.kind, self.site) {
780 (ArgKind::DivByZero, _) => format!("{name} divided by zero in {n}: a SPARQL error"),
781 (_, Site::Op(_)) => {
782 format!("Operator {name} received {what} as {arg} in {n}: a SPARQL type error")
783 }
784 _ => format!("{name} received {what} as {arg} in {n}: a SPARQL type error"),
785 };
786 message.push_str(
787 ", which makes a FILTER drop the row, even under ! (BIND leaves the variable unbound)",
788 );
789 if !matches!(self.kind, ArgKind::Unbound | ArgKind::DivByZero) && !self.sample.is_empty() {
790 message.push_str(&format!("; e.g. {}", self.sample));
791 }
792 message.push_str(&format!(". Hint: {hint}."));
793 QueryWarning {
794 severity: WarningSeverity::TypeError,
795 function: name.to_string(),
796 argument: self.pos,
797 kind: self.kind.as_str().to_string(),
798 count: self.count,
799 sample: (!self.sample.is_empty()).then_some(self.sample),
800 hint,
801 message,
802 }
803 }
804}
805
806fn is_empty_result(out: &QueryOutput) -> bool {
807 match out {
808 QueryOutput::Select(_, rows) => rows.is_empty(),
809 QueryOutput::Ask(b) => !*b,
810 QueryOutput::Construct(t) => t.is_empty(),
811 }
812}
813
814pub(crate) fn finish(out: &QueryOutput) -> Vec<QueryWarning> {
816 let (entries, misses) = SINK.with(|s| {
817 let mut s = s.borrow_mut();
818 (
819 std::mem::take(&mut s.entries),
820 std::mem::take(&mut s.misses),
821 )
822 });
823 let errored = !entries.is_empty();
824 let mut out_w: Vec<QueryWarning> = entries.into_iter().map(Entry::into_warning).collect();
825 if !errored && is_empty_result(out) {
830 let mut seen: Vec<(Builtin, String)> = Vec::new();
831 for (func, needle) in misses {
832 if needle.to_lowercase() == needle.to_uppercase()
833 || seen.iter().any(|(f, n)| *f == func && *n == needle)
834 {
835 continue;
836 }
837 seen.push((func, needle.clone()));
838 let name = fn_name(func);
839 let hint = if func == Builtin::Regex {
840 format!("add the \"i\" flag: REGEX(?x, \"{needle}\", \"i\")")
841 } else {
842 format!(
843 "compare case-folded: {name}(LCASE(?x), \"{}\")",
844 needle.to_lowercase()
845 )
846 };
847 out_w.push(QueryWarning {
848 severity: WarningSeverity::Hint,
849 function: name.to_string(),
850 argument: 2,
851 kind: "case-sensitive".to_string(),
852 count: 0,
853 sample: Some(truncate(&needle)),
854 message: format!(
855 "No results. {name} ran on string values but never matched \"{}\"; \
856 string matching is case-sensitive. If you expected matches, {hint}.",
857 truncate(&needle)
858 ),
859 hint,
860 });
861 }
862 }
863 out_w
864}
865
866pub fn warnings_json(warnings: &[QueryWarning]) -> String {
869 use crate::results::push_json_string;
870 let mut s = String::from("[");
871 for (i, w) in warnings.iter().enumerate() {
872 if i > 0 {
873 s.push(',');
874 }
875 s.push_str(r#"{"severity":"#);
876 push_json_string(&mut s, w.severity.as_str());
877 s.push_str(r#","function":"#);
878 push_json_string(&mut s, &w.function);
879 s.push_str(&format!(r#","argument":{}"#, w.argument));
880 s.push_str(r#","argKind":"#);
881 push_json_string(&mut s, &w.kind);
882 s.push_str(&format!(r#","count":{}"#, w.count));
883 s.push_str(r#","sample":"#);
884 match &w.sample {
885 Some(v) => push_json_string(&mut s, v),
886 None => s.push_str("null"),
887 }
888 s.push_str(r#","hint":"#);
889 push_json_string(&mut s, &w.hint);
890 s.push_str(r#","message":"#);
891 push_json_string(&mut s, &w.message);
892 s.push('}');
893 }
894 s.push(']');
895 s
896}