1use std::collections::{HashMap, HashSet};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub enum RdfTerm {
11 Iri(String),
13 Literal {
15 value: String,
16 datatype: String,
17 lang: Option<String>,
18 },
19 BlankNode(String),
21}
22
23impl RdfTerm {
24 pub fn iri(iri: impl Into<String>) -> Self {
26 RdfTerm::Iri(iri.into())
27 }
28
29 pub fn string_literal(value: impl Into<String>) -> Self {
31 RdfTerm::Literal {
32 value: value.into(),
33 datatype: "http://www.w3.org/2001/XMLSchema#string".to_string(),
34 lang: None,
35 }
36 }
37
38 pub fn lang_literal(value: impl Into<String>, lang: impl Into<String>) -> Self {
40 RdfTerm::Literal {
41 value: value.into(),
42 datatype: "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString".to_string(),
43 lang: Some(lang.into()),
44 }
45 }
46
47 pub fn blank(id: impl Into<String>) -> Self {
49 RdfTerm::BlankNode(id.into())
50 }
51}
52
53impl std::fmt::Display for RdfTerm {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 RdfTerm::Iri(iri) => write!(f, "<{}>", iri),
57 RdfTerm::Literal {
58 value,
59 lang: Some(lang),
60 ..
61 } => write!(f, "\"{}\"@{}", value, lang),
62 RdfTerm::Literal {
63 value,
64 datatype,
65 lang: None,
66 } => write!(f, "\"{}\"^^<{}>", value, datatype),
67 RdfTerm::BlankNode(id) => write!(f, "_:{}", id),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Default)]
77pub struct BindingSet {
78 bindings: Vec<HashMap<String, RdfTerm>>,
79}
80
81impl BindingSet {
82 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn from_vec(bindings: Vec<HashMap<String, RdfTerm>>) -> Self {
89 BindingSet { bindings }
90 }
91
92 pub fn add(&mut self, binding: HashMap<String, RdfTerm>) {
94 self.bindings.push(binding);
95 }
96
97 pub fn len(&self) -> usize {
99 self.bindings.len()
100 }
101
102 pub fn is_empty(&self) -> bool {
104 self.bindings.is_empty()
105 }
106
107 pub fn minus(&self, other: &BindingSet) -> BindingSet {
119 let kept = self
120 .bindings
121 .iter()
122 .filter(|row_self| {
123 !other.bindings.iter().any(|row_other| {
124 let shared: HashSet<&String> = row_self
125 .keys()
126 .collect::<HashSet<_>>()
127 .intersection(&row_other.keys().collect::<HashSet<_>>())
128 .copied()
129 .collect();
130 !shared.is_empty() && Self::is_compatible(row_self, row_other)
131 })
132 })
133 .cloned()
134 .collect();
135 BindingSet { bindings: kept }
136 }
137
138 pub fn exists_filter(&self, pattern: &BindingSet) -> BindingSet {
144 let kept = self
145 .bindings
146 .iter()
147 .filter(|row_self| {
148 pattern
149 .bindings
150 .iter()
151 .any(|row_pattern| Self::is_compatible(row_self, row_pattern))
152 })
153 .cloned()
154 .collect();
155 BindingSet { bindings: kept }
156 }
157
158 pub fn union(&self, other: &BindingSet) -> BindingSet {
160 let mut result = self.bindings.clone();
161 result.extend(other.bindings.iter().cloned());
162 BindingSet { bindings: result }
163 }
164
165 pub fn project(&self, vars: &[&str]) -> BindingSet {
170 let projected = self
171 .bindings
172 .iter()
173 .map(|row| {
174 vars.iter()
175 .filter_map(|v| row.get(*v).map(|term| (v.to_string(), term.clone())))
176 .collect::<HashMap<String, RdfTerm>>()
177 })
178 .collect();
179 BindingSet {
180 bindings: projected,
181 }
182 }
183
184 pub fn join(&self, other: &BindingSet) -> BindingSet {
187 let mut result = Vec::new();
188 for row_self in &self.bindings {
189 for row_other in &other.bindings {
190 if Self::is_compatible(row_self, row_other) {
191 result.push(Self::merge_rows(row_self, row_other));
192 }
193 }
194 }
195 BindingSet { bindings: result }
196 }
197
198 pub fn distinct(&self) -> BindingSet {
200 let mut seen: HashSet<Vec<(String, RdfTerm)>> = HashSet::new();
201 let unique = self
202 .bindings
203 .iter()
204 .filter(|row| {
205 let mut sorted: Vec<(String, RdfTerm)> =
206 row.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
207 sorted.sort_by(|a, b| a.0.cmp(&b.0));
208 seen.insert(sorted)
209 })
210 .cloned()
211 .collect();
212 BindingSet { bindings: unique }
213 }
214
215 pub fn iter(&self) -> impl Iterator<Item = &HashMap<String, RdfTerm>> {
217 self.bindings.iter()
218 }
219
220 pub fn is_compatible(a: &HashMap<String, RdfTerm>, b: &HashMap<String, RdfTerm>) -> bool {
225 a.iter()
226 .all(|(var, term_a)| b.get(var).map_or(true, |term_b| term_a == term_b))
227 }
228
229 fn merge_rows(
231 a: &HashMap<String, RdfTerm>,
232 b: &HashMap<String, RdfTerm>,
233 ) -> HashMap<String, RdfTerm> {
234 let mut merged = a.clone();
235 for (k, v) in b {
236 merged.entry(k.clone()).or_insert_with(|| v.clone());
237 }
238 merged
239 }
240}
241
242impl IntoIterator for BindingSet {
243 type Item = HashMap<String, RdfTerm>;
244 type IntoIter = std::vec::IntoIter<HashMap<String, RdfTerm>>;
245
246 fn into_iter(self) -> Self::IntoIter {
247 self.bindings.into_iter()
248 }
249}
250
251pub fn solution(pairs: &[(&str, RdfTerm)]) -> HashMap<String, RdfTerm> {
253 pairs
254 .iter()
255 .map(|(k, v)| (k.to_string(), v.clone()))
256 .collect()
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn iri(s: &str) -> RdfTerm {
266 RdfTerm::iri(s)
267 }
268
269 fn lit(s: &str) -> RdfTerm {
270 RdfTerm::string_literal(s)
271 }
272
273 fn bnode(s: &str) -> RdfTerm {
274 RdfTerm::blank(s)
275 }
276
277 fn row(pairs: &[(&str, RdfTerm)]) -> HashMap<String, RdfTerm> {
278 solution(pairs)
279 }
280
281 fn single(var: &str, term: RdfTerm) -> HashMap<String, RdfTerm> {
282 let mut m = HashMap::new();
283 m.insert(var.to_string(), term);
284 m
285 }
286
287 #[test]
290 fn test_new_is_empty() {
291 let bs = BindingSet::new();
292 assert!(bs.is_empty());
293 assert_eq!(bs.len(), 0);
294 }
295
296 #[test]
297 fn test_from_vec_empty() {
298 let bs = BindingSet::from_vec(vec![]);
299 assert!(bs.is_empty());
300 }
301
302 #[test]
303 fn test_from_vec_non_empty() {
304 let bs = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
305 assert_eq!(bs.len(), 1);
306 }
307
308 #[test]
309 fn test_add() {
310 let mut bs = BindingSet::new();
311 bs.add(single("x", iri("http://a")));
312 bs.add(single("x", iri("http://b")));
313 assert_eq!(bs.len(), 2);
314 }
315
316 #[test]
319 fn test_compatible_no_shared_vars() {
320 let a = single("x", iri("http://a"));
321 let b = single("y", iri("http://b"));
322 assert!(BindingSet::is_compatible(&a, &b));
323 }
324
325 #[test]
326 fn test_compatible_same_shared_var_same_value() {
327 let a = single("x", iri("http://a"));
328 let b = single("x", iri("http://a"));
329 assert!(BindingSet::is_compatible(&a, &b));
330 }
331
332 #[test]
333 fn test_incompatible_shared_var_different_value() {
334 let a = single("x", iri("http://a"));
335 let b = single("x", iri("http://b"));
336 assert!(!BindingSet::is_compatible(&a, &b));
337 }
338
339 #[test]
340 fn test_compatible_partial_overlap() {
341 let a = row(&[("x", iri("http://a")), ("y", iri("http://y"))]);
342 let b = row(&[("x", iri("http://a")), ("z", iri("http://z"))]);
343 assert!(BindingSet::is_compatible(&a, &b));
344 }
345
346 #[test]
347 fn test_incompatible_partial_overlap() {
348 let a = row(&[("x", iri("http://a")), ("y", iri("http://y"))]);
349 let b = row(&[("x", iri("http://DIFFERENT")), ("z", iri("http://z"))]);
350 assert!(!BindingSet::is_compatible(&a, &b));
351 }
352
353 #[test]
354 fn test_compatible_all_shared_vars_agree() {
355 let a = row(&[("x", iri("http://x")), ("y", lit("hello"))]);
356 let b = row(&[("x", iri("http://x")), ("y", lit("hello"))]);
357 assert!(BindingSet::is_compatible(&a, &b));
358 }
359
360 #[test]
361 fn test_compatible_empty_rows() {
362 let a: HashMap<String, RdfTerm> = HashMap::new();
363 let b: HashMap<String, RdfTerm> = HashMap::new();
364 assert!(BindingSet::is_compatible(&a, &b));
365 }
366
367 #[test]
370 fn test_minus_empty_self() {
371 let s = BindingSet::new();
372 let o = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
373 assert!(s.minus(&o).is_empty());
374 }
375
376 #[test]
377 fn test_minus_empty_other() {
378 let s = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
379 let o = BindingSet::new();
380 assert_eq!(s.minus(&o).len(), 1);
381 }
382
383 #[test]
384 fn test_minus_removes_compatible_row_with_shared_var() {
385 let s = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
387 let o = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
388 assert_eq!(s.minus(&o).len(), 0);
389 }
390
391 #[test]
392 fn test_minus_keeps_row_different_value_shared_var() {
393 let s = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
395 let o = BindingSet::from_vec(vec![single("x", iri("http://b"))]);
396 assert_eq!(s.minus(&o).len(), 1);
397 }
398
399 #[test]
400 fn test_minus_keeps_row_no_shared_vars() {
401 let s = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
403 let o = BindingSet::from_vec(vec![single("y", iri("http://b"))]);
404 assert_eq!(s.minus(&o).len(), 1);
405 }
406
407 #[test]
408 fn test_minus_partial_filter() {
409 let s = BindingSet::from_vec(vec![
411 single("x", iri("http://a")),
412 single("x", iri("http://b")),
413 ]);
414 let o = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
415 let result = s.minus(&o);
416 assert_eq!(result.len(), 1);
417 assert_eq!(result.bindings[0].get("x"), Some(&iri("http://b")));
418 }
419
420 #[test]
421 fn test_minus_multiple_rows_in_other() {
422 let s = BindingSet::from_vec(vec![
423 single("x", iri("http://a")),
424 single("x", iri("http://b")),
425 single("x", iri("http://c")),
426 ]);
427 let o = BindingSet::from_vec(vec![
428 single("x", iri("http://a")),
429 single("x", iri("http://c")),
430 ]);
431 let result = s.minus(&o);
432 assert_eq!(result.len(), 1);
433 assert_eq!(result.bindings[0].get("x"), Some(&iri("http://b")));
434 }
435
436 #[test]
437 fn test_minus_multi_variable_rows() {
438 let s = BindingSet::from_vec(vec![row(&[("x", iri("http://a")), ("y", lit("foo"))])]);
440 let o = BindingSet::from_vec(vec![row(&[("x", iri("http://a")), ("y", lit("bar"))])]);
441 assert_eq!(s.minus(&o).len(), 1);
442 }
443
444 #[test]
445 fn test_minus_row_with_no_vars_kept_always() {
446 let empty_row: HashMap<String, RdfTerm> = HashMap::new();
448 let s = BindingSet::from_vec(vec![empty_row]);
449 let o = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
450 assert_eq!(s.minus(&o).len(), 1);
451 }
452
453 #[test]
456 fn test_exists_filter_empty_pattern() {
457 let s = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
458 let p = BindingSet::new();
459 assert_eq!(s.exists_filter(&p).len(), 0);
461 }
462
463 #[test]
464 fn test_exists_filter_compatible_keeps_row() {
465 let s = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
466 let p = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
467 assert_eq!(s.exists_filter(&p).len(), 1);
468 }
469
470 #[test]
471 fn test_exists_filter_incompatible_removes_row() {
472 let s = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
473 let p = BindingSet::from_vec(vec![single("x", iri("http://b"))]);
474 assert_eq!(s.exists_filter(&p).len(), 0);
475 }
476
477 #[test]
478 fn test_exists_filter_no_shared_vars_compatible() {
479 let s = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
481 let p = BindingSet::from_vec(vec![single("y", iri("http://b"))]);
482 assert_eq!(s.exists_filter(&p).len(), 1);
483 }
484
485 #[test]
486 fn test_exists_filter_mixed() {
487 let s = BindingSet::from_vec(vec![
488 single("x", iri("http://a")),
489 single("x", iri("http://b")),
490 ]);
491 let p = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
492 assert_eq!(s.exists_filter(&p).len(), 1);
493 }
494
495 #[test]
498 fn test_union_empty_both() {
499 let a = BindingSet::new();
500 let b = BindingSet::new();
501 assert!(a.union(&b).is_empty());
502 }
503
504 #[test]
505 fn test_union_one_empty() {
506 let a = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
507 let b = BindingSet::new();
508 assert_eq!(a.union(&b).len(), 1);
509 }
510
511 #[test]
512 fn test_union_both_non_empty() {
513 let a = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
514 let b = BindingSet::from_vec(vec![single("y", iri("http://b"))]);
515 assert_eq!(a.union(&b).len(), 2);
516 }
517
518 #[test]
519 fn test_union_preserves_duplicates() {
520 let a = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
521 let b = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
522 assert_eq!(a.union(&b).len(), 2); }
524
525 #[test]
528 fn test_project_keeps_named_vars() {
529 let bs = BindingSet::from_vec(vec![row(&[
530 ("x", iri("http://x")),
531 ("y", iri("http://y")),
532 ("z", iri("http://z")),
533 ])]);
534 let proj = bs.project(&["x", "z"]);
535 assert_eq!(proj.len(), 1);
536 let r = &proj.bindings[0];
537 assert!(r.contains_key("x"));
538 assert!(!r.contains_key("y"));
539 assert!(r.contains_key("z"));
540 }
541
542 #[test]
543 fn test_project_no_vars() {
544 let bs = BindingSet::from_vec(vec![single("x", iri("http://x"))]);
545 let proj = bs.project(&[]);
546 assert_eq!(proj.len(), 1);
547 assert!(proj.bindings[0].is_empty());
548 }
549
550 #[test]
551 fn test_project_missing_var_omitted() {
552 let bs = BindingSet::from_vec(vec![single("x", iri("http://x"))]);
553 let proj = bs.project(&["x", "missing"]);
554 assert_eq!(proj.len(), 1);
555 assert!(proj.bindings[0].contains_key("x"));
556 assert!(!proj.bindings[0].contains_key("missing"));
557 }
558
559 #[test]
560 fn test_project_empty_set() {
561 let bs = BindingSet::new();
562 let proj = bs.project(&["x"]);
563 assert!(proj.is_empty());
564 }
565
566 #[test]
569 fn test_join_empty_left() {
570 let a = BindingSet::new();
571 let b = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
572 assert!(a.join(&b).is_empty());
573 }
574
575 #[test]
576 fn test_join_empty_right() {
577 let a = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
578 let b = BindingSet::new();
579 assert!(a.join(&b).is_empty());
580 }
581
582 #[test]
583 fn test_join_no_shared_vars_cross_product() {
584 let a = BindingSet::from_vec(vec![single("x", iri("http://1"))]);
585 let b = BindingSet::from_vec(vec![single("y", iri("http://2"))]);
586 let j = a.join(&b);
587 assert_eq!(j.len(), 1);
588 assert!(j.bindings[0].contains_key("x"));
589 assert!(j.bindings[0].contains_key("y"));
590 }
591
592 #[test]
593 fn test_join_shared_var_compatible() {
594 let a = BindingSet::from_vec(vec![row(&[("x", iri("http://a")), ("y", lit("foo"))])]);
595 let b = BindingSet::from_vec(vec![row(&[("x", iri("http://a")), ("z", lit("bar"))])]);
596 let j = a.join(&b);
597 assert_eq!(j.len(), 1);
598 assert_eq!(j.bindings[0].get("x"), Some(&iri("http://a")));
599 assert_eq!(j.bindings[0].get("y"), Some(&lit("foo")));
600 assert_eq!(j.bindings[0].get("z"), Some(&lit("bar")));
601 }
602
603 #[test]
604 fn test_join_shared_var_incompatible_excluded() {
605 let a = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
606 let b = BindingSet::from_vec(vec![single("x", iri("http://b"))]);
607 assert!(a.join(&b).is_empty());
608 }
609
610 #[test]
611 fn test_join_multiple_rows() {
612 let a = BindingSet::from_vec(vec![
613 single("x", iri("http://1")),
614 single("x", iri("http://2")),
615 ]);
616 let b = BindingSet::from_vec(vec![
617 single("x", iri("http://1")),
618 single("x", iri("http://3")),
619 ]);
620 let j = a.join(&b);
621 assert_eq!(j.len(), 1);
623 assert_eq!(j.bindings[0].get("x"), Some(&iri("http://1")));
624 }
625
626 #[test]
629 fn test_distinct_empty() {
630 let bs = BindingSet::new();
631 assert!(bs.distinct().is_empty());
632 }
633
634 #[test]
635 fn test_distinct_no_duplicates() {
636 let bs = BindingSet::from_vec(vec![
637 single("x", iri("http://a")),
638 single("x", iri("http://b")),
639 ]);
640 assert_eq!(bs.distinct().len(), 2);
641 }
642
643 #[test]
644 fn test_distinct_with_duplicates() {
645 let bs = BindingSet::from_vec(vec![
646 single("x", iri("http://a")),
647 single("x", iri("http://a")),
648 single("x", iri("http://b")),
649 ]);
650 assert_eq!(bs.distinct().len(), 2);
651 }
652
653 #[test]
654 fn test_distinct_multi_var_duplicates() {
655 let r1 = row(&[("x", iri("http://x")), ("y", lit("foo"))]);
656 let r2 = row(&[("x", iri("http://x")), ("y", lit("foo"))]);
657 let r3 = row(&[("x", iri("http://x")), ("y", lit("bar"))]);
658 let bs = BindingSet::from_vec(vec![r1, r2, r3]);
659 assert_eq!(bs.distinct().len(), 2);
660 }
661
662 #[test]
665 fn test_iter() {
666 let bs = BindingSet::from_vec(vec![
667 single("x", iri("http://1")),
668 single("x", iri("http://2")),
669 ]);
670 let collected: Vec<_> = bs.iter().collect();
671 assert_eq!(collected.len(), 2);
672 }
673
674 #[test]
677 fn test_rdf_term_iri_display() {
678 let t = iri("http://example.org/thing");
679 assert_eq!(format!("{}", t), "<http://example.org/thing>");
680 }
681
682 #[test]
683 fn test_rdf_term_literal_display() {
684 let t = lit("hello");
685 assert!(format!("{}", t).contains("hello"));
686 }
687
688 #[test]
689 fn test_rdf_term_lang_display() {
690 let t = RdfTerm::lang_literal("hello", "en");
691 assert!(format!("{}", t).contains("@en"));
692 }
693
694 #[test]
695 fn test_rdf_term_blank_display() {
696 let t = bnode("b0");
697 assert_eq!(format!("{}", t), "_:b0");
698 }
699
700 #[test]
701 fn test_rdf_term_eq() {
702 assert_eq!(iri("http://a"), iri("http://a"));
703 assert_ne!(iri("http://a"), iri("http://b"));
704 assert_ne!(iri("http://a"), lit("http://a"));
705 assert_ne!(iri("http://a"), bnode("b0"));
706 }
707
708 #[test]
711 fn test_minus_all_kept_when_no_shared_vars() {
712 let s = BindingSet::from_vec(vec![
714 single("x", iri("http://1")),
715 single("x", iri("http://2")),
716 single("x", iri("http://3")),
717 ]);
718 let o = BindingSet::from_vec(vec![single("y", iri("http://y"))]);
719 assert_eq!(s.minus(&o).len(), 3);
720 }
721
722 #[test]
723 fn test_minus_both_empty() {
724 let s = BindingSet::new();
725 let o = BindingSet::new();
726 assert!(s.minus(&o).is_empty());
727 }
728
729 #[test]
730 fn test_exists_filter_self_empty() {
731 let s = BindingSet::new();
732 let p = BindingSet::from_vec(vec![single("x", iri("http://a"))]);
733 assert!(s.exists_filter(&p).is_empty());
734 }
735
736 #[test]
737 fn test_join_all_compatible_no_shared() {
738 let a = BindingSet::from_vec(vec![
740 single("x", iri("http://1")),
741 single("x", iri("http://2")),
742 ]);
743 let b = BindingSet::from_vec(vec![
744 single("y", iri("http://a")),
745 single("y", iri("http://b")),
746 ]);
747 assert_eq!(a.join(&b).len(), 4);
748 }
749
750 #[test]
751 fn test_project_and_distinct() {
752 let bs = BindingSet::from_vec(vec![
754 row(&[("x", iri("http://x")), ("y", lit("foo")), ("z", lit("A"))]),
755 row(&[("x", iri("http://x")), ("y", lit("foo")), ("z", lit("B"))]),
756 ]);
757 let proj = bs.project(&["x", "y"]).distinct();
758 assert_eq!(proj.len(), 1);
759 }
760
761 #[test]
762 fn test_union_order_preserved() {
763 let a = BindingSet::from_vec(vec![single("x", iri("http://1"))]);
764 let b = BindingSet::from_vec(vec![single("x", iri("http://2"))]);
765 let u = a.union(&b);
766 assert_eq!(u.bindings[0].get("x"), Some(&iri("http://1")));
767 assert_eq!(u.bindings[1].get("x"), Some(&iri("http://2")));
768 }
769
770 #[test]
771 fn test_minus_literal_terms() {
772 let s = BindingSet::from_vec(vec![single("label", lit("hello"))]);
773 let o = BindingSet::from_vec(vec![single("label", lit("hello"))]);
774 assert_eq!(s.minus(&o).len(), 0);
775 }
776
777 #[test]
778 fn test_minus_blank_node_terms() {
779 let s = BindingSet::from_vec(vec![single("b", bnode("b0"))]);
780 let o = BindingSet::from_vec(vec![single("b", bnode("b0"))]);
781 assert_eq!(s.minus(&o).len(), 0);
782 }
783
784 #[test]
785 fn test_exists_filter_multiple_pattern_rows() {
786 let s = BindingSet::from_vec(vec![single("x", iri("http://c"))]);
788 let p = BindingSet::from_vec(vec![
789 single("x", iri("http://a")),
790 single("x", iri("http://b")),
791 single("x", iri("http://c")),
792 ]);
793 assert_eq!(s.exists_filter(&p).len(), 1);
794 }
795
796 #[test]
797 fn test_join_preserves_all_vars() {
798 let a = BindingSet::from_vec(vec![row(&[
799 ("subject", iri("http://s")),
800 ("predicate", iri("http://p")),
801 ])]);
802 let b = BindingSet::from_vec(vec![row(&[
803 ("predicate", iri("http://p")),
804 ("object", lit("value")),
805 ])]);
806 let j = a.join(&b);
807 assert_eq!(j.len(), 1);
808 let r = &j.bindings[0];
809 assert_eq!(r.get("subject"), Some(&iri("http://s")));
810 assert_eq!(r.get("predicate"), Some(&iri("http://p")));
811 assert_eq!(r.get("object"), Some(&lit("value")));
812 }
813}