1use std::fmt;
33
34#[derive(Debug, Clone)]
40pub enum SparqlBuilderError {
41 EmptyWhereClause,
43 InvalidVariableName(String),
45 ConflictingModifiers(String),
47}
48
49impl fmt::Display for SparqlBuilderError {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 SparqlBuilderError::EmptyWhereClause => {
53 write!(f, "SPARQL builder error: WHERE clause is empty")
54 }
55 SparqlBuilderError::InvalidVariableName(name) => {
56 write!(
57 f,
58 "SPARQL builder error: invalid variable name '{name}' \
59 (must start with a letter or '_')"
60 )
61 }
62 SparqlBuilderError::ConflictingModifiers(msg) => {
63 write!(f, "SPARQL builder error: conflicting modifiers — {msg}")
64 }
65 }
66 }
67}
68
69impl std::error::Error for SparqlBuilderError {}
70
71pub fn validate_variable_name(name: &str) -> bool {
80 let mut chars = name.chars();
81 match chars.next() {
82 Some(c) if c.is_alphabetic() || c == '_' => {}
83 _ => return false,
84 }
85 chars.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
86}
87
88#[derive(Debug, Clone, PartialEq)]
94pub struct SparqlLiteral {
95 pub value: String,
97 pub datatype: Option<String>,
99 pub lang_tag: Option<String>,
101}
102
103#[derive(Debug, Clone, PartialEq)]
109pub enum SparqlTerm {
110 Iri(String),
112 PrefixedName(String, String),
114 Literal(SparqlLiteral),
116 BlankNode(String),
118 Variable(String),
120}
121
122impl SparqlTerm {
123 pub fn iri(s: impl Into<String>) -> Self {
126 SparqlTerm::Iri(s.into())
127 }
128
129 pub fn var(name: impl Into<String>) -> Self {
131 SparqlTerm::Variable(name.into())
132 }
133
134 pub fn literal(value: impl Into<String>) -> Self {
136 SparqlTerm::Literal(SparqlLiteral {
137 value: value.into(),
138 datatype: None,
139 lang_tag: None,
140 })
141 }
142
143 pub fn typed_literal(value: impl Into<String>, datatype: impl Into<String>) -> Self {
145 SparqlTerm::Literal(SparqlLiteral {
146 value: value.into(),
147 datatype: Some(datatype.into()),
148 lang_tag: None,
149 })
150 }
151
152 pub fn lang_literal(value: impl Into<String>, lang: impl Into<String>) -> Self {
154 SparqlTerm::Literal(SparqlLiteral {
155 value: value.into(),
156 datatype: None,
157 lang_tag: Some(lang.into()),
158 })
159 }
160
161 pub fn blank(id: impl Into<String>) -> Self {
163 SparqlTerm::BlankNode(id.into())
164 }
165
166 pub fn prefixed(prefix: impl Into<String>, local: impl Into<String>) -> Self {
168 SparqlTerm::PrefixedName(prefix.into(), local.into())
169 }
170
171 pub fn to_sparql(&self) -> String {
173 match self {
174 SparqlTerm::Variable(name) => format!("?{name}"),
175 SparqlTerm::Iri(iri) => {
176 if iri.starts_with('<') && iri.ends_with('>') {
177 iri.clone()
178 } else {
179 format!("<{iri}>")
180 }
181 }
182 SparqlTerm::PrefixedName(prefix, local) => format!("{prefix}:{local}"),
183 SparqlTerm::BlankNode(id) => format!("_:{id}"),
184 SparqlTerm::Literal(lit) => render_literal(lit),
185 }
186 }
187}
188
189fn render_literal(lit: &SparqlLiteral) -> String {
191 let escaped = lit.value.replace('\\', "\\\\").replace('"', "\\\"");
193 let base = format!("\"{escaped}\"");
194 if let Some(lang) = &lit.lang_tag {
195 format!("{base}@{lang}")
196 } else if let Some(dt) = &lit.datatype {
197 format!("{base}^^{dt}")
198 } else {
199 base
200 }
201}
202
203#[derive(Debug, Clone)]
209pub struct BuilderTriplePattern {
210 pub subject: SparqlTerm,
212 pub predicate: SparqlTerm,
214 pub object: SparqlTerm,
216}
217
218impl BuilderTriplePattern {
219 pub fn new(s: SparqlTerm, p: SparqlTerm, o: SparqlTerm) -> Self {
221 BuilderTriplePattern {
222 subject: s,
223 predicate: p,
224 object: o,
225 }
226 }
227
228 pub fn to_sparql(&self) -> String {
230 format!(
231 "{} {} {} .",
232 self.subject.to_sparql(),
233 self.predicate.to_sparql(),
234 self.object.to_sparql()
235 )
236 }
237}
238
239#[derive(Debug, Clone)]
245pub struct SparqlFilter {
246 pub expression: String,
248}
249
250impl SparqlFilter {
251 pub fn new(expr: impl Into<String>) -> Self {
253 SparqlFilter {
254 expression: expr.into(),
255 }
256 }
257
258 pub fn gt(var: &str, value: f64) -> Self {
260 SparqlFilter::new(format!("?{var} > {value}"))
261 }
262
263 pub fn lt(var: &str, value: f64) -> Self {
265 SparqlFilter::new(format!("?{var} < {value}"))
266 }
267
268 pub fn eq(var: &str, other: &str) -> Self {
270 SparqlFilter::new(format!("?{var} = {other}"))
271 }
272
273 pub fn lang_matches(var: &str, lang: &str) -> Self {
275 SparqlFilter::new(format!("langMatches(lang(?{var}), \"{lang}\")"))
276 }
277
278 pub fn regex(var: &str, pattern: &str) -> Self {
280 SparqlFilter::new(format!("regex(?{var}, \"{pattern}\")"))
281 }
282
283 pub fn and(left: SparqlFilter, right: SparqlFilter) -> Self {
285 SparqlFilter::new(format!("({}) && ({})", left.expression, right.expression))
286 }
287
288 pub fn or(left: SparqlFilter, right: SparqlFilter) -> Self {
290 SparqlFilter::new(format!("({}) || ({})", left.expression, right.expression))
291 }
292
293 pub fn negate(inner: SparqlFilter) -> Self {
295 SparqlFilter::new(format!("!({})", inner.expression))
296 }
297
298 pub fn to_sparql(&self) -> String {
300 format!("FILTER ({})", self.expression)
301 }
302}
303
304impl std::ops::Not for SparqlFilter {
305 type Output = SparqlFilter;
306
307 fn not(self) -> Self::Output {
308 SparqlFilter::negate(self)
309 }
310}
311
312#[derive(Debug, Clone)]
318pub enum OrderDirection {
319 Asc(String),
321 Desc(String),
323}
324
325impl OrderDirection {
326 pub fn to_sparql(&self) -> String {
328 match self {
329 OrderDirection::Asc(var) => format!("ASC(?{var})"),
330 OrderDirection::Desc(var) => format!("DESC(?{var})"),
331 }
332 }
333}
334
335#[derive(Debug, Clone)]
341pub enum WhereClauseItem {
342 Triple(BuilderTriplePattern),
344 Filter(SparqlFilter),
346 Optional(WhereClause),
348 Union(WhereClause, WhereClause),
350 Bind(String, String),
352 Values(String, Vec<SparqlTerm>),
354 ValuesMulti(Vec<String>, Vec<Vec<SparqlTerm>>),
356}
357
358#[derive(Debug, Clone, Default)]
361pub struct WhereClause {
362 patterns: Vec<WhereClauseItem>,
363}
364
365impl WhereClause {
366 pub fn new() -> Self {
368 WhereClause::default()
369 }
370
371 pub fn triple(mut self, s: SparqlTerm, p: SparqlTerm, o: SparqlTerm) -> Self {
373 self.patterns
374 .push(WhereClauseItem::Triple(BuilderTriplePattern::new(s, p, o)));
375 self
376 }
377
378 pub fn filter(mut self, f: SparqlFilter) -> Self {
380 self.patterns.push(WhereClauseItem::Filter(f));
381 self
382 }
383
384 pub fn optional(mut self, inner: WhereClause) -> Self {
386 self.patterns.push(WhereClauseItem::Optional(inner));
387 self
388 }
389
390 pub fn union(mut self, left: WhereClause, right: WhereClause) -> Self {
392 self.patterns.push(WhereClauseItem::Union(left, right));
393 self
394 }
395
396 pub fn bind(mut self, expr: impl Into<String>, var: impl Into<String>) -> Self {
398 self.patterns
399 .push(WhereClauseItem::Bind(expr.into(), var.into()));
400 self
401 }
402
403 pub fn values(mut self, var: impl Into<String>, vals: Vec<SparqlTerm>) -> Self {
405 self.patterns
406 .push(WhereClauseItem::Values(var.into(), vals));
407 self
408 }
409
410 pub fn values_multi(
412 mut self,
413 vars: Vec<impl Into<String>>,
414 rows: Vec<Vec<SparqlTerm>>,
415 ) -> Self {
416 let vars: Vec<String> = vars.into_iter().map(Into::into).collect();
417 self.patterns.push(WhereClauseItem::ValuesMulti(vars, rows));
418 self
419 }
420
421 pub fn is_empty(&self) -> bool {
423 self.patterns.is_empty()
424 }
425
426 pub fn pattern_count(&self) -> usize {
428 self.patterns.len()
429 }
430
431 pub fn to_sparql(&self) -> String {
433 let mut out = String::from("WHERE {\n");
434 for item in &self.patterns {
435 render_where_item(&mut out, item, 1);
436 }
437 out.push('}');
438 out
439 }
440
441 fn render_inner(&self, depth: usize) -> String {
444 let mut out = String::from("{\n");
445 for item in &self.patterns {
446 render_where_item(&mut out, item, depth + 1);
447 }
448 push_indent(&mut out, depth);
449 out.push('}');
450 out
451 }
452}
453
454fn push_indent(buf: &mut String, depth: usize) {
456 for _ in 0..depth * 2 {
457 buf.push(' ');
458 }
459}
460
461fn render_where_item(buf: &mut String, item: &WhereClauseItem, depth: usize) {
464 match item {
465 WhereClauseItem::Triple(tp) => {
466 push_indent(buf, depth);
467 buf.push_str(&tp.to_sparql());
468 buf.push('\n');
469 }
470 WhereClauseItem::Filter(f) => {
471 push_indent(buf, depth);
472 buf.push_str(&f.to_sparql());
473 buf.push('\n');
474 }
475 WhereClauseItem::Optional(inner) => {
476 push_indent(buf, depth);
477 buf.push_str("OPTIONAL ");
478 buf.push_str(&inner.render_inner(depth));
479 buf.push('\n');
480 }
481 WhereClauseItem::Union(left, right) => {
482 push_indent(buf, depth);
483 buf.push_str(&left.render_inner(depth));
484 buf.push_str(" UNION ");
485 buf.push_str(&right.render_inner(depth));
486 buf.push('\n');
487 }
488 WhereClauseItem::Bind(expr, var) => {
489 push_indent(buf, depth);
490 buf.push_str(&format!("BIND ({expr} AS ?{var})\n"));
491 }
492 WhereClauseItem::Values(var, vals) => {
493 push_indent(buf, depth);
494 let vals_str: Vec<String> = vals.iter().map(|v| v.to_sparql()).collect();
495 buf.push_str(&format!("VALUES ?{var} {{ {} }}\n", vals_str.join(" ")));
496 }
497 WhereClauseItem::ValuesMulti(vars, rows) => {
498 push_indent(buf, depth);
499 let var_list: Vec<String> = vars.iter().map(|v| format!("?{v}")).collect();
500 let row_strs: Vec<String> = rows
501 .iter()
502 .map(|row| {
503 let terms: Vec<String> = row.iter().map(|t| t.to_sparql()).collect();
504 format!("({})", terms.join(" "))
505 })
506 .collect();
507 buf.push_str(&format!(
508 "VALUES ({}) {{ {} }}\n",
509 var_list.join(" "),
510 row_strs.join(" ")
511 ));
512 }
513 }
514}
515
516#[derive(Debug, Clone)]
522pub struct SelectQuery {
523 pub prefixes: Vec<(String, String)>,
525 pub projection: Vec<String>,
527 pub distinct: bool,
529 pub reduced: bool,
531 pub where_clause: WhereClause,
533 pub order_by: Vec<OrderDirection>,
535 pub group_by: Vec<String>,
537 pub having: Option<SparqlFilter>,
539 pub limit: Option<usize>,
541 pub offset: Option<usize>,
543}
544
545impl SelectQuery {
546 pub fn new() -> Self {
548 SelectQuery {
549 prefixes: Vec::new(),
550 projection: Vec::new(),
551 distinct: false,
552 reduced: false,
553 where_clause: WhereClause::new(),
554 order_by: Vec::new(),
555 group_by: Vec::new(),
556 having: None,
557 limit: None,
558 offset: None,
559 }
560 }
561
562 pub fn prefix(mut self, prefix: impl Into<String>, iri: impl Into<String>) -> Self {
564 self.prefixes.push((prefix.into(), iri.into()));
565 self
566 }
567
568 pub fn select(mut self, var: impl Into<String>) -> Self {
570 self.projection.push(var.into());
571 self
572 }
573
574 pub fn select_all(mut self) -> Self {
576 self.projection.clear();
577 self
578 }
579
580 pub fn distinct(mut self) -> Self {
582 self.distinct = true;
583 self
584 }
585
586 pub fn reduced(mut self) -> Self {
588 self.reduced = true;
589 self
590 }
591
592 pub fn where_clause(mut self, clause: WhereClause) -> Self {
594 self.where_clause = clause;
595 self
596 }
597
598 pub fn order_by_asc(mut self, var: impl Into<String>) -> Self {
600 self.order_by.push(OrderDirection::Asc(var.into()));
601 self
602 }
603
604 pub fn order_by_desc(mut self, var: impl Into<String>) -> Self {
606 self.order_by.push(OrderDirection::Desc(var.into()));
607 self
608 }
609
610 pub fn group_by(mut self, var: impl Into<String>) -> Self {
612 self.group_by.push(var.into());
613 self
614 }
615
616 pub fn having(mut self, filter: SparqlFilter) -> Self {
618 self.having = Some(filter);
619 self
620 }
621
622 pub fn limit(mut self, n: usize) -> Self {
624 self.limit = Some(n);
625 self
626 }
627
628 pub fn offset(mut self, n: usize) -> Self {
630 self.offset = Some(n);
631 self
632 }
633
634 pub fn build(&self) -> Result<String, SparqlBuilderError> {
636 if self.where_clause.is_empty() {
638 return Err(SparqlBuilderError::EmptyWhereClause);
639 }
640
641 if self.distinct && self.reduced {
643 return Err(SparqlBuilderError::ConflictingModifiers(
644 "DISTINCT and REDUCED cannot both be set".to_string(),
645 ));
646 }
647
648 for var in &self.projection {
650 if !validate_variable_name(var) {
651 return Err(SparqlBuilderError::InvalidVariableName(var.clone()));
652 }
653 }
654
655 Ok(self.build_unchecked())
656 }
657
658 pub fn build_unchecked(&self) -> String {
660 let mut out = String::new();
661
662 for (prefix, iri) in &self.prefixes {
664 let iri_str = if iri.starts_with('<') && iri.ends_with('>') {
665 iri.clone()
666 } else {
667 format!("<{iri}>")
668 };
669 out.push_str(&format!("PREFIX {prefix}: {iri_str}\n"));
670 }
671
672 if !self.prefixes.is_empty() {
673 out.push('\n');
674 }
675
676 out.push_str("SELECT");
678 if self.distinct {
679 out.push_str(" DISTINCT");
680 } else if self.reduced {
681 out.push_str(" REDUCED");
682 }
683
684 if self.projection.is_empty() {
685 out.push_str(" *");
686 } else {
687 for var in &self.projection {
688 out.push_str(&format!(" ?{var}"));
689 }
690 }
691 out.push('\n');
692
693 out.push_str(&self.where_clause.to_sparql());
695 out.push('\n');
696
697 if !self.group_by.is_empty() {
699 let vars: Vec<String> = self.group_by.iter().map(|v| format!("?{v}")).collect();
700 out.push_str(&format!("GROUP BY {}\n", vars.join(" ")));
701 }
702
703 if let Some(having) = &self.having {
705 out.push_str(&format!("HAVING ({})\n", having.expression));
706 }
707
708 if !self.order_by.is_empty() {
710 let terms: Vec<String> = self.order_by.iter().map(|o| o.to_sparql()).collect();
711 out.push_str(&format!("ORDER BY {}\n", terms.join(" ")));
712 }
713
714 if let Some(limit) = self.limit {
716 out.push_str(&format!("LIMIT {limit}\n"));
717 }
718
719 if let Some(offset) = self.offset {
721 out.push_str(&format!("OFFSET {offset}\n"));
722 }
723
724 out
725 }
726}
727
728impl Default for SelectQuery {
729 fn default() -> Self {
730 Self::new()
731 }
732}
733
734#[derive(Debug, Clone)]
740pub struct AskQuery {
741 pub prefixes: Vec<(String, String)>,
743 pub where_clause: WhereClause,
745}
746
747impl AskQuery {
748 pub fn new() -> Self {
750 AskQuery {
751 prefixes: Vec::new(),
752 where_clause: WhereClause::new(),
753 }
754 }
755
756 pub fn prefix(mut self, prefix: impl Into<String>, iri: impl Into<String>) -> Self {
758 self.prefixes.push((prefix.into(), iri.into()));
759 self
760 }
761
762 pub fn where_clause(mut self, clause: WhereClause) -> Self {
764 self.where_clause = clause;
765 self
766 }
767
768 pub fn build(&self) -> String {
770 let mut out = String::new();
771
772 for (prefix, iri) in &self.prefixes {
773 let iri_str = if iri.starts_with('<') && iri.ends_with('>') {
774 iri.clone()
775 } else {
776 format!("<{iri}>")
777 };
778 out.push_str(&format!("PREFIX {prefix}: {iri_str}\n"));
779 }
780
781 if !self.prefixes.is_empty() {
782 out.push('\n');
783 }
784
785 out.push_str("ASK\n");
786 out.push_str(&self.where_clause.to_sparql());
787 out.push('\n');
788 out
789 }
790}
791
792impl Default for AskQuery {
793 fn default() -> Self {
794 Self::new()
795 }
796}
797
798#[cfg(test)]
803mod tests {
804 use super::*;
805
806 #[test]
809 fn test_sparql_term_variable_render() {
810 assert_eq!(SparqlTerm::var("x").to_sparql(), "?x");
811 }
812
813 #[test]
814 fn test_sparql_term_iri_render_bare() {
815 assert_eq!(
817 SparqlTerm::iri("http://example.org/foo").to_sparql(),
818 "<http://example.org/foo>"
819 );
820 }
821
822 #[test]
823 fn test_sparql_term_iri_render_already_wrapped() {
824 assert_eq!(
826 SparqlTerm::iri("<http://example.org/foo>").to_sparql(),
827 "<http://example.org/foo>"
828 );
829 }
830
831 #[test]
832 fn test_sparql_term_literal_render() {
833 assert_eq!(SparqlTerm::literal("hello").to_sparql(), "\"hello\"");
834 }
835
836 #[test]
837 fn test_sparql_term_typed_literal_render() {
838 assert_eq!(
839 SparqlTerm::typed_literal("42", "xsd:integer").to_sparql(),
840 "\"42\"^^xsd:integer"
841 );
842 }
843
844 #[test]
845 fn test_sparql_term_lang_literal_render() {
846 assert_eq!(
847 SparqlTerm::lang_literal("hello", "en").to_sparql(),
848 "\"hello\"@en"
849 );
850 }
851
852 #[test]
853 fn test_sparql_term_prefixed_render() {
854 assert_eq!(SparqlTerm::prefixed("rdf", "type").to_sparql(), "rdf:type");
855 }
856
857 #[test]
858 fn test_sparql_term_blank_node_render() {
859 assert_eq!(SparqlTerm::blank("b0").to_sparql(), "_:b0");
860 }
861
862 #[test]
865 fn test_triple_pattern_to_sparql() {
866 let tp = BuilderTriplePattern::new(
867 SparqlTerm::var("s"),
868 SparqlTerm::prefixed("rdf", "type"),
869 SparqlTerm::var("o"),
870 );
871 assert_eq!(tp.to_sparql(), "?s rdf:type ?o .");
872 }
873
874 #[test]
877 fn test_filter_gt() {
878 let f = SparqlFilter::gt("x", 18.0);
879 assert_eq!(f.to_sparql(), "FILTER (?x > 18)");
880 }
881
882 #[test]
883 fn test_filter_lt() {
884 let f = SparqlFilter::lt("age", 65.0);
885 assert_eq!(f.to_sparql(), "FILTER (?age < 65)");
886 }
887
888 #[test]
889 fn test_filter_and() {
890 let left = SparqlFilter::gt("x", 0.0);
891 let right = SparqlFilter::lt("x", 100.0);
892 let combined = SparqlFilter::and(left, right);
893 assert!(combined.to_sparql().contains("&&"));
894 }
895
896 #[test]
897 fn test_filter_or() {
898 let left = SparqlFilter::gt("x", 0.0);
899 let right = SparqlFilter::lt("x", 100.0);
900 let combined = SparqlFilter::or(left, right);
901 assert!(combined.to_sparql().contains("||"));
902 }
903
904 #[test]
905 fn test_filter_not() {
906 let inner = SparqlFilter::gt("x", 18.0);
907 let negated = SparqlFilter::negate(inner);
908 assert!(negated.to_sparql().contains("!("));
909 }
910
911 #[test]
912 fn test_filter_lang_matches() {
913 let f = SparqlFilter::lang_matches("label", "en");
914 assert!(f.to_sparql().contains("langMatches"));
915 }
916
917 #[test]
918 fn test_filter_regex() {
919 let f = SparqlFilter::regex("name", "^Alice");
920 assert!(f.to_sparql().contains("regex"));
921 assert!(f.to_sparql().contains("^Alice"));
922 }
923
924 #[test]
927 fn test_where_clause_empty() {
928 let wc = WhereClause::new();
929 assert!(wc.is_empty());
930 assert_eq!(wc.pattern_count(), 0);
931 }
932
933 #[test]
934 fn test_where_clause_triple() {
935 let wc = WhereClause::new().triple(
936 SparqlTerm::var("s"),
937 SparqlTerm::prefixed("rdf", "type"),
938 SparqlTerm::var("o"),
939 );
940 assert!(!wc.is_empty());
941 assert_eq!(wc.pattern_count(), 1);
942 }
943
944 #[test]
945 fn test_where_clause_to_sparql_contains_triple() {
946 let wc = WhereClause::new().triple(
947 SparqlTerm::var("s"),
948 SparqlTerm::prefixed("rdf", "type"),
949 SparqlTerm::var("o"),
950 );
951 let rendered = wc.to_sparql();
952 assert!(rendered.contains("?s rdf:type ?o ."));
953 }
954
955 #[test]
956 fn test_where_clause_optional() {
957 let inner = WhereClause::new().triple(
958 SparqlTerm::var("s"),
959 SparqlTerm::prefixed("ex", "email"),
960 SparqlTerm::var("email"),
961 );
962 let wc = WhereClause::new()
963 .triple(
964 SparqlTerm::var("s"),
965 SparqlTerm::prefixed("rdf", "type"),
966 SparqlTerm::prefixed("ex", "Person"),
967 )
968 .optional(inner);
969 let rendered = wc.to_sparql();
970 assert!(rendered.contains("OPTIONAL"));
971 }
972
973 #[test]
974 fn test_where_clause_union() {
975 let left = WhereClause::new().triple(
976 SparqlTerm::var("s"),
977 SparqlTerm::prefixed("ex", "name"),
978 SparqlTerm::var("name"),
979 );
980 let right = WhereClause::new().triple(
981 SparqlTerm::var("s"),
982 SparqlTerm::prefixed("ex", "label"),
983 SparqlTerm::var("name"),
984 );
985 let wc = WhereClause::new().union(left, right);
986 let rendered = wc.to_sparql();
987 assert!(rendered.contains("UNION"));
988 }
989
990 #[test]
991 fn test_where_clause_filter() {
992 let wc = WhereClause::new()
993 .triple(
994 SparqlTerm::var("s"),
995 SparqlTerm::prefixed("ex", "age"),
996 SparqlTerm::var("age"),
997 )
998 .filter(SparqlFilter::gt("age", 18.0));
999 let rendered = wc.to_sparql();
1000 assert!(rendered.contains("FILTER"));
1001 }
1002
1003 #[test]
1006 fn test_select_query_build_basic() {
1007 let wc = WhereClause::new().triple(
1008 SparqlTerm::var("x"),
1009 SparqlTerm::prefixed("rdf", "type"),
1010 SparqlTerm::var("type"),
1011 );
1012 let query = SelectQuery::new()
1013 .select("x")
1014 .where_clause(wc)
1015 .build()
1016 .expect("should build successfully");
1017
1018 assert!(query.contains("SELECT"));
1019 assert!(query.contains("?x"));
1020 assert!(query.contains("WHERE"));
1021 assert!(query.contains("rdf:type"));
1022 }
1023
1024 #[test]
1025 fn test_select_query_build_distinct() {
1026 let wc = WhereClause::new().triple(
1027 SparqlTerm::var("x"),
1028 SparqlTerm::prefixed("rdf", "type"),
1029 SparqlTerm::var("t"),
1030 );
1031 let query = SelectQuery::new()
1032 .select("x")
1033 .distinct()
1034 .where_clause(wc)
1035 .build()
1036 .expect("should build successfully");
1037
1038 assert!(query.contains("DISTINCT"));
1039 }
1040
1041 #[test]
1042 fn test_select_query_build_limit_offset() {
1043 let wc = WhereClause::new().triple(
1044 SparqlTerm::var("x"),
1045 SparqlTerm::prefixed("rdf", "type"),
1046 SparqlTerm::var("t"),
1047 );
1048 let query = SelectQuery::new()
1049 .select("x")
1050 .where_clause(wc)
1051 .limit(10)
1052 .offset(5)
1053 .build()
1054 .expect("should build successfully");
1055
1056 assert!(query.contains("LIMIT 10"));
1057 assert!(query.contains("OFFSET 5"));
1058 }
1059
1060 #[test]
1061 fn test_select_query_build_order_by() {
1062 let wc = WhereClause::new().triple(
1063 SparqlTerm::var("x"),
1064 SparqlTerm::prefixed("rdf", "type"),
1065 SparqlTerm::var("t"),
1066 );
1067 let query = SelectQuery::new()
1068 .select("x")
1069 .where_clause(wc)
1070 .order_by_asc("x")
1071 .build()
1072 .expect("should build successfully");
1073
1074 assert!(query.contains("ORDER BY ASC(?x)"));
1075 }
1076
1077 #[test]
1078 fn test_select_query_empty_where_error() {
1079 let result = SelectQuery::new().select("x").build();
1080 assert!(matches!(result, Err(SparqlBuilderError::EmptyWhereClause)));
1081 }
1082
1083 #[test]
1084 fn test_select_query_distinct_and_reduced_conflict() {
1085 let wc = WhereClause::new().triple(
1086 SparqlTerm::var("x"),
1087 SparqlTerm::prefixed("rdf", "type"),
1088 SparqlTerm::var("t"),
1089 );
1090 let result = SelectQuery::new()
1091 .select("x")
1092 .distinct()
1093 .reduced()
1094 .where_clause(wc)
1095 .build();
1096 assert!(matches!(
1097 result,
1098 Err(SparqlBuilderError::ConflictingModifiers(_))
1099 ));
1100 }
1101
1102 #[test]
1103 fn test_select_query_with_prefix() {
1104 let wc = WhereClause::new().triple(
1105 SparqlTerm::var("x"),
1106 SparqlTerm::prefixed("rdf", "type"),
1107 SparqlTerm::prefixed("ex", "Person"),
1108 );
1109 let query = SelectQuery::new()
1110 .prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
1111 .prefix("ex", "http://example.org/")
1112 .select("x")
1113 .where_clause(wc)
1114 .build()
1115 .expect("should build successfully");
1116
1117 assert!(query.contains("PREFIX rdf:"));
1118 assert!(query.contains("PREFIX ex:"));
1119 }
1120
1121 #[test]
1122 fn test_select_query_build_star() {
1123 let wc = WhereClause::new().triple(
1124 SparqlTerm::var("s"),
1125 SparqlTerm::var("p"),
1126 SparqlTerm::var("o"),
1127 );
1128 let query = SelectQuery::new()
1129 .select_all()
1130 .where_clause(wc)
1131 .build()
1132 .expect("should build successfully");
1133
1134 assert!(query.contains("SELECT *"));
1135 }
1136
1137 #[test]
1140 fn test_ask_query_build() {
1141 let wc = WhereClause::new().triple(
1142 SparqlTerm::var("s"),
1143 SparqlTerm::prefixed("rdf", "type"),
1144 SparqlTerm::prefixed("ex", "Person"),
1145 );
1146 let query = AskQuery::new().where_clause(wc).build();
1147 assert!(query.starts_with("ASK"));
1148 }
1149
1150 #[test]
1151 fn test_ask_query_with_prefix() {
1152 let wc = WhereClause::new().triple(
1153 SparqlTerm::var("s"),
1154 SparqlTerm::prefixed("rdf", "type"),
1155 SparqlTerm::prefixed("ex", "Thing"),
1156 );
1157 let query = AskQuery::new()
1158 .prefix("ex", "http://example.org/")
1159 .where_clause(wc)
1160 .build();
1161 assert!(query.contains("PREFIX ex:"));
1162 assert!(query.contains("ASK"));
1163 }
1164
1165 #[test]
1168 fn test_validate_variable_name_valid() {
1169 assert!(validate_variable_name("x"));
1170 assert!(validate_variable_name("myVar"));
1171 assert!(validate_variable_name("_private"));
1172 assert!(validate_variable_name("foo_bar"));
1173 }
1174
1175 #[test]
1176 fn test_validate_variable_name_invalid() {
1177 assert!(!validate_variable_name(""));
1178 assert!(!validate_variable_name("123abc"));
1179 assert!(!validate_variable_name("?x"));
1180 }
1181
1182 #[test]
1185 fn test_order_direction_asc() {
1186 assert_eq!(OrderDirection::Asc("x".to_string()).to_sparql(), "ASC(?x)");
1187 }
1188
1189 #[test]
1190 fn test_order_direction_desc() {
1191 assert_eq!(
1192 OrderDirection::Desc("score".to_string()).to_sparql(),
1193 "DESC(?score)"
1194 );
1195 }
1196
1197 #[test]
1200 fn test_error_display() {
1201 let e = SparqlBuilderError::EmptyWhereClause;
1202 let s = e.to_string();
1203 assert!(s.contains("empty"));
1204
1205 let e2 = SparqlBuilderError::InvalidVariableName("123bad".to_string());
1206 let s2 = e2.to_string();
1207 assert!(s2.contains("123bad"));
1208
1209 let e3 = SparqlBuilderError::ConflictingModifiers("test".to_string());
1210 let s3 = e3.to_string();
1211 assert!(s3.contains("test"));
1212 }
1213}