Skip to main content

oxirs_arq/executor/
dataset.rs

1//! Dataset Implementation
2//!
3//! This module provides dataset abstractions and implementations for query execution.
4
5use crate::algebra::{PropertyPath, Term as AlgebraTerm, TriplePattern};
6use crate::path::{PathDataset, PropertyPath as PathPropertyPath};
7use anyhow::{anyhow, Result};
8use oxirs_core::model::{GraphName, NamedNode};
9use oxirs_core::RdfTerm;
10use std::collections::HashSet;
11
12/// Selects which graph(s) a triple-pattern lookup targets within a
13/// [`Dataset`].
14///
15/// This mirrors the SPARQL RDF-dataset distinction between the single active
16/// *default graph* and an individually addressed *named graph*. It deliberately
17/// does not offer an "all graphs / union" mode, because unioning every graph
18/// for a plain pattern is exactly the semantic bug this abstraction exists to
19/// prevent (see [`Dataset::find_triples`]).
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum GraphSelector {
22    /// The dataset's active default graph.
23    DefaultGraph,
24    /// A single named graph addressed by its IRI (e.g. `GRAPH <iri> { ... }`).
25    Named(NamedNode),
26}
27
28/// Dataset trait for data access during query execution
29pub trait Dataset: Send + Sync {
30    /// Find all triples matching the given pattern in the dataset's **active
31    /// default graph**.
32    ///
33    /// Per SPARQL RDF-dataset semantics a plain (non-`GRAPH`) basic graph
34    /// pattern reads only the default graph, not the union of every named
35    /// graph. Implementors backed by a quad store MUST scope this to the
36    /// default graph rather than passing an unbound graph filter.
37    fn find_triples(
38        &self,
39        pattern: &TriplePattern,
40    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>>;
41
42    /// Check if a triple exists in the dataset
43    fn contains_triple(
44        &self,
45        subject: &AlgebraTerm,
46        predicate: &AlgebraTerm,
47        object: &AlgebraTerm,
48    ) -> Result<bool>;
49
50    /// Get all subjects in the dataset
51    fn subjects(&self) -> Result<Vec<AlgebraTerm>>;
52
53    /// Get all predicates in the dataset
54    fn predicates(&self) -> Result<Vec<AlgebraTerm>>;
55
56    /// Get all objects in the dataset
57    fn objects(&self) -> Result<Vec<AlgebraTerm>>;
58
59    /// Whether this dataset can honor graph-scoped access
60    /// ([`Dataset::find_triples_in`]) and named-graph enumeration
61    /// ([`Dataset::named_graphs`]).
62    ///
63    /// Defaults to `false`; graph-capable implementors override it to `true`.
64    fn has_graph_support(&self) -> bool {
65        false
66    }
67
68    /// Find all triples matching `pattern` within the graph selected by
69    /// `selector`.
70    ///
71    /// The default implementation FAILS LOUD: a dataset that cannot scope by
72    /// graph must never silently fall back to unioning every graph, since that
73    /// corrupts `GRAPH` / `FROM` semantics. Graph-capable implementors override
74    /// this (and set [`Dataset::has_graph_support`] to `true`).
75    fn find_triples_in(
76        &self,
77        selector: &GraphSelector,
78        pattern: &TriplePattern,
79    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
80        let _ = (selector, pattern);
81        Err(anyhow!(
82            "graph-scoped access not supported by this dataset: it does not \
83             implement Dataset::find_triples_in (has_graph_support() is false)"
84        ))
85    }
86
87    /// Enumerate the named graphs visible in this dataset, as `Term::Iri`
88    /// values, for `GRAPH ?g` evaluation.
89    ///
90    /// The default implementation FAILS LOUD for the same reason as
91    /// [`Dataset::find_triples_in`].
92    fn named_graphs(&self) -> Result<Vec<AlgebraTerm>> {
93        Err(anyhow!(
94            "named-graph enumeration not supported by this dataset: it does \
95             not implement Dataset::named_graphs (has_graph_support() is false)"
96        ))
97    }
98}
99
100/// In-memory dataset implementation for testing.
101///
102/// Each stored triple carries an optional graph label: `None` places it in the
103/// default graph, `Some(iri)` in the named graph `iri`. This lets the in-memory
104/// dataset exercise `GRAPH` / `FROM` scoping without a backing quad store.
105#[derive(Debug, Clone, Default)]
106pub struct InMemoryDataset {
107    quads: Vec<(Option<NamedNode>, AlgebraTerm, AlgebraTerm, AlgebraTerm)>,
108}
109
110impl InMemoryDataset {
111    pub fn new() -> Self {
112        Self { quads: Vec::new() }
113    }
114
115    /// Add a triple to the **default graph**.
116    pub fn add_triple(
117        &mut self,
118        subject: AlgebraTerm,
119        predicate: AlgebraTerm,
120        object: AlgebraTerm,
121    ) {
122        self.quads.push((None, subject, predicate, object));
123    }
124
125    /// Add a triple to the named graph `graph`.
126    pub fn add_triple_in_graph(
127        &mut self,
128        graph: NamedNode,
129        subject: AlgebraTerm,
130        predicate: AlgebraTerm,
131        object: AlgebraTerm,
132    ) {
133        self.quads.push((Some(graph), subject, predicate, object));
134    }
135
136    /// Build a default-graph-only dataset from a list of triples.
137    pub fn from_triples(triples: Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>) -> Self {
138        Self {
139            quads: triples
140                .into_iter()
141                .map(|(s, p, o)| (None, s, p, o))
142                .collect(),
143        }
144    }
145
146    /// Filter stored quads by an explicit graph target and the pattern.
147    fn scan(
148        &self,
149        want_graph: Option<&NamedNode>,
150        pattern: &TriplePattern,
151    ) -> Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)> {
152        self.quads
153            .iter()
154            .filter(|(g, s, p, o)| {
155                let graph_ok = match (want_graph, g) {
156                    (None, None) => true,
157                    (Some(want), Some(have)) => want == have,
158                    _ => false,
159                };
160                graph_ok
161                    && matches_term(&pattern.subject, s)
162                    && matches_term(&pattern.predicate, p)
163                    && matches_term(&pattern.object, o)
164            })
165            .map(|(_, s, p, o)| (s.clone(), p.clone(), o.clone()))
166            .collect()
167    }
168}
169
170impl Dataset for InMemoryDataset {
171    fn find_triples(
172        &self,
173        pattern: &TriplePattern,
174    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
175        // Plain BGP reads the default graph only.
176        Ok(self.scan(None, pattern))
177    }
178
179    fn find_triples_in(
180        &self,
181        selector: &GraphSelector,
182        pattern: &TriplePattern,
183    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
184        let want = match selector {
185            GraphSelector::DefaultGraph => None,
186            GraphSelector::Named(iri) => Some(iri),
187        };
188        Ok(self.scan(want, pattern))
189    }
190
191    fn has_graph_support(&self) -> bool {
192        true
193    }
194
195    fn named_graphs(&self) -> Result<Vec<AlgebraTerm>> {
196        let mut seen: HashSet<NamedNode> = HashSet::new();
197        let mut names: Vec<AlgebraTerm> = Vec::new();
198        for (g, _, _, _) in &self.quads {
199            if let Some(iri) = g {
200                if seen.insert(iri.clone()) {
201                    names.push(AlgebraTerm::Iri(iri.clone()));
202                }
203            }
204        }
205        Ok(names)
206    }
207
208    fn contains_triple(
209        &self,
210        subject: &AlgebraTerm,
211        predicate: &AlgebraTerm,
212        object: &AlgebraTerm,
213    ) -> Result<bool> {
214        // Existence check against the default graph, consistent with
215        // `find_triples`.
216        Ok(self
217            .quads
218            .iter()
219            .any(|(g, s, p, o)| g.is_none() && s == subject && p == predicate && o == object))
220    }
221
222    fn subjects(&self) -> Result<Vec<AlgebraTerm>> {
223        let subjects: HashSet<_> = self.quads.iter().map(|(_, s, _, _)| s.clone()).collect();
224        Ok(subjects.into_iter().collect())
225    }
226
227    fn predicates(&self) -> Result<Vec<AlgebraTerm>> {
228        let predicates: HashSet<_> = self.quads.iter().map(|(_, _, p, _)| p.clone()).collect();
229        Ok(predicates.into_iter().collect())
230    }
231
232    fn objects(&self) -> Result<Vec<AlgebraTerm>> {
233        let objects: HashSet<_> = self.quads.iter().map(|(_, _, _, o)| o.clone()).collect();
234        Ok(objects.into_iter().collect())
235    }
236}
237
238fn matches_term(pattern: &AlgebraTerm, term: &AlgebraTerm) -> bool {
239    match pattern {
240        AlgebraTerm::Variable(_) => true, // Variables match any term
241        _ => pattern == term,
242    }
243}
244
245/// Adapter to make Dataset implement PathDataset
246pub struct DatasetPathAdapter<'a> {
247    dataset: &'a dyn Dataset,
248}
249
250impl<'a> DatasetPathAdapter<'a> {
251    pub fn new(dataset: &'a dyn Dataset) -> Self {
252        Self { dataset }
253    }
254}
255
256impl<'a> PathDataset for DatasetPathAdapter<'a> {
257    fn find_outgoing(
258        &self,
259        subject: &AlgebraTerm,
260        predicate: &AlgebraTerm,
261    ) -> Result<Vec<AlgebraTerm>> {
262        let pattern = TriplePattern::new(
263            subject.clone(),
264            predicate.clone(),
265            AlgebraTerm::Variable(crate::algebra::Variable::new("?o")?),
266        );
267        let triples = self.dataset.find_triples(&pattern)?;
268        Ok(triples.into_iter().map(|(_, _, o)| o).collect())
269    }
270
271    fn find_incoming(
272        &self,
273        predicate: &AlgebraTerm,
274        object: &AlgebraTerm,
275    ) -> Result<Vec<AlgebraTerm>> {
276        let pattern = TriplePattern::new(
277            AlgebraTerm::Variable(crate::algebra::Variable::new("?s")?),
278            predicate.clone(),
279            object.clone(),
280        );
281        let triples = self.dataset.find_triples(&pattern)?;
282        Ok(triples.into_iter().map(|(s, _, _)| s).collect())
283    }
284
285    fn find_predicates(
286        &self,
287        subject: &AlgebraTerm,
288        object: &AlgebraTerm,
289    ) -> Result<Vec<AlgebraTerm>> {
290        let pattern = TriplePattern::new(
291            subject.clone(),
292            AlgebraTerm::Variable(crate::algebra::Variable::new("?p")?),
293            object.clone(),
294        );
295        let triples = self.dataset.find_triples(&pattern)?;
296        Ok(triples.into_iter().map(|(_, p, _)| p).collect())
297    }
298
299    fn get_predicates(&self) -> Result<Vec<AlgebraTerm>> {
300        self.dataset.predicates()
301    }
302
303    fn contains_triple(
304        &self,
305        subject: &AlgebraTerm,
306        predicate: &AlgebraTerm,
307        object: &AlgebraTerm,
308    ) -> Result<bool> {
309        self.dataset.contains_triple(subject, predicate, object)
310    }
311}
312
313/// Convert algebra PropertyPath to path module PropertyPath
314pub fn convert_property_path(path: &PropertyPath) -> Result<PathPropertyPath> {
315    match path {
316        PropertyPath::Iri(iri) => Ok(PathPropertyPath::Direct(AlgebraTerm::Iri(iri.clone()))),
317        PropertyPath::Variable(var) => {
318            Ok(PathPropertyPath::Direct(AlgebraTerm::Variable(var.clone())))
319        }
320        PropertyPath::Inverse(inner) => {
321            let inner_path = convert_property_path(inner)?;
322            Ok(PathPropertyPath::Inverse(Box::new(inner_path)))
323        }
324        PropertyPath::Sequence(left, right) => {
325            let left_path = convert_property_path(left)?;
326            let right_path = convert_property_path(right)?;
327            Ok(PathPropertyPath::Sequence(
328                Box::new(left_path),
329                Box::new(right_path),
330            ))
331        }
332        PropertyPath::Alternative(left, right) => {
333            let left_path = convert_property_path(left)?;
334            let right_path = convert_property_path(right)?;
335            Ok(PathPropertyPath::Alternative(
336                Box::new(left_path),
337                Box::new(right_path),
338            ))
339        }
340        PropertyPath::ZeroOrMore(inner) => {
341            let inner_path = convert_property_path(inner)?;
342            Ok(PathPropertyPath::ZeroOrMore(Box::new(inner_path)))
343        }
344        PropertyPath::OneOrMore(inner) => {
345            let inner_path = convert_property_path(inner)?;
346            Ok(PathPropertyPath::OneOrMore(Box::new(inner_path)))
347        }
348        PropertyPath::ZeroOrOne(inner) => {
349            let inner_path = convert_property_path(inner)?;
350            Ok(PathPropertyPath::ZeroOrOne(Box::new(inner_path)))
351        }
352        PropertyPath::NegatedPropertySet(paths) => {
353            let mut terms = Vec::new();
354            for p in paths {
355                match p {
356                    PropertyPath::Iri(iri) => terms.push(AlgebraTerm::Iri(iri.clone())),
357                    PropertyPath::Variable(var) => terms.push(AlgebraTerm::Variable(var.clone())),
358                    _ => {
359                        return Err(anyhow!(
360                            "Negated property set can only contain IRIs or variables"
361                        ))
362                    }
363                }
364            }
365            Ok(PathPropertyPath::NegatedPropertySet(terms))
366        }
367    }
368}
369
370/// Adapter to make ConcreteStore implement Dataset trait
371/// This is primarily for benchmarking and testing purposes
372pub struct ConcreteStoreDataset {
373    store: std::sync::Arc<oxirs_core::rdf_store::ConcreteStore>,
374}
375
376impl ConcreteStoreDataset {
377    pub fn new(store: oxirs_core::rdf_store::ConcreteStore) -> Self {
378        Self {
379            store: std::sync::Arc::new(store),
380        }
381    }
382
383    pub fn from_arc(store: std::sync::Arc<oxirs_core::rdf_store::ConcreteStore>) -> Self {
384        Self { store }
385    }
386}
387
388impl Clone for ConcreteStoreDataset {
389    fn clone(&self) -> Self {
390        Self {
391            store: std::sync::Arc::clone(&self.store),
392        }
393    }
394}
395
396impl Dataset for ConcreteStoreDataset {
397    fn find_triples(
398        &self,
399        pattern: &TriplePattern,
400    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
401        // Plain BGP reads the default graph only (SPARQL dataset semantics).
402        self.find_triples_in(&GraphSelector::DefaultGraph, pattern)
403    }
404
405    fn find_triples_in(
406        &self,
407        selector: &GraphSelector,
408        pattern: &TriplePattern,
409    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
410        use oxirs_core::rdf_store::Store;
411
412        let Some((subject, predicate, object)) = pattern_to_query_terms(pattern)? else {
413            // The pattern references a term that cannot occupy its position, so
414            // it matches nothing (not an error).
415            return Ok(Vec::new());
416        };
417        let graph_name = graph_selector_to_name(selector);
418        let quads = self.store.find_quads(
419            subject.as_ref(),
420            predicate.as_ref(),
421            object.as_ref(),
422            Some(&graph_name),
423        )?;
424        Ok(quads_to_algebra_triples(quads))
425    }
426
427    fn has_graph_support(&self) -> bool {
428        true
429    }
430
431    fn named_graphs(&self) -> Result<Vec<AlgebraTerm>> {
432        collect_named_graphs(self.store.as_ref())
433    }
434
435    fn contains_triple(
436        &self,
437        subject: &AlgebraTerm,
438        predicate: &AlgebraTerm,
439        object: &AlgebraTerm,
440    ) -> Result<bool> {
441        let pattern = TriplePattern::new(subject.clone(), predicate.clone(), object.clone());
442        let triples = self.find_triples(&pattern)?;
443        Ok(!triples.is_empty())
444    }
445
446    fn subjects(&self) -> Result<Vec<AlgebraTerm>> {
447        use oxirs_core::rdf_store::Store;
448        let quads = self.store.find_quads(None, None, None, None)?;
449        let subjects: HashSet<_> = quads
450            .into_iter()
451            .filter_map(|quad| match quad.subject() {
452                oxirs_core::model::Subject::NamedNode(n) => Some(AlgebraTerm::Iri(
453                    oxirs_core::model::NamedNode::new(n.as_str()).ok()?,
454                )),
455                oxirs_core::model::Subject::BlankNode(b) => {
456                    Some(AlgebraTerm::BlankNode(b.as_str().to_string()))
457                }
458                oxirs_core::model::Subject::Variable(v) => Some(AlgebraTerm::Variable(v.clone())),
459                oxirs_core::model::Subject::QuotedTriple(_) => None,
460            })
461            .collect();
462        Ok(subjects.into_iter().collect())
463    }
464
465    fn predicates(&self) -> Result<Vec<AlgebraTerm>> {
466        use oxirs_core::rdf_store::Store;
467        let quads = self.store.find_quads(None, None, None, None)?;
468        let predicates: HashSet<_> = quads
469            .into_iter()
470            .filter_map(|quad| {
471                Some(AlgebraTerm::Iri(
472                    oxirs_core::model::NamedNode::new(quad.predicate().as_str()).ok()?,
473                ))
474            })
475            .collect();
476        Ok(predicates.into_iter().collect())
477    }
478
479    fn objects(&self) -> Result<Vec<AlgebraTerm>> {
480        use oxirs_core::rdf_store::Store;
481        let quads = self.store.find_quads(None, None, None, None)?;
482        let objects: HashSet<_> = quads
483            .into_iter()
484            .filter_map(|quad| match quad.object() {
485                oxirs_core::model::Object::NamedNode(n) => Some(AlgebraTerm::Iri(
486                    oxirs_core::model::NamedNode::new(n.as_str()).ok()?,
487                )),
488                oxirs_core::model::Object::Literal(l) => {
489                    Some(AlgebraTerm::Literal(core_literal_to_algebra(l)))
490                }
491                oxirs_core::model::Object::BlankNode(b) => {
492                    Some(AlgebraTerm::BlankNode(b.as_str().to_string()))
493                }
494                oxirs_core::model::Object::Variable(v) => Some(AlgebraTerm::Variable(v.clone())),
495                oxirs_core::model::Object::QuotedTriple(_) => None,
496            })
497            .collect();
498        Ok(objects.into_iter().collect())
499    }
500}
501
502/// Well-known datatype IRIs whose presence is implicit for plain / language
503/// literals and therefore represented as `datatype: None` in the algebra model.
504const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
505const RDF_LANG_STRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
506
507/// Build a store `Literal` from an algebra `Literal`, preserving the datatype
508/// and language tag. Mirrors `update::UpdateExecutor::term_to_object` so that
509/// typed/lang-tagged literal patterns match real data in the store instead of
510/// being silently coerced to plain `xsd:string`.
511pub(crate) fn algebra_literal_to_core(lit: &crate::algebra::Literal) -> oxirs_core::model::Literal {
512    if let Some(lang) = &lit.language {
513        oxirs_core::model::Literal::new_language_tagged_literal(&lit.value, lang)
514            .unwrap_or_else(|_| oxirs_core::model::Literal::new(&lit.value))
515    } else if let Some(dt) = &lit.datatype {
516        oxirs_core::model::Literal::new_typed(&lit.value, dt.clone())
517    } else {
518        oxirs_core::model::Literal::new(&lit.value)
519    }
520}
521
522/// Convert a store `Literal` into an algebra `Literal`, preserving the datatype
523/// and language tag (dropping only the implicit `xsd:string` / `rdf:langString`
524/// datatypes, which the algebra model expresses via `datatype: None`).
525pub(crate) fn core_literal_to_algebra(l: &oxirs_core::model::Literal) -> crate::algebra::Literal {
526    if let Some(lang) = l.language() {
527        crate::algebra::Literal {
528            value: l.value().to_string(),
529            language: Some(lang.to_string()),
530            datatype: None,
531        }
532    } else {
533        let dt = l.datatype().into_owned();
534        let datatype = if dt.as_str() == XSD_STRING || dt.as_str() == RDF_LANG_STRING {
535            None
536        } else {
537            Some(dt)
538        };
539        crate::algebra::Literal {
540            value: l.value().to_string(),
541            language: None,
542            datatype,
543        }
544    }
545}
546
547/// Convert a triple pattern into optional (subject, predicate, object) store
548/// query terms. `None` in any position means an unbound wildcard.
549/// Convert a triple pattern's terms into a store `find_quads` filter.
550///
551/// Returns `Ok(None)` when a concrete term cannot occupy its position — a
552/// literal (or property-path / quoted-triple) subject, or a literal / blank
553/// predicate — because such a pattern matches nothing in a well-formed RDF
554/// graph. Callers must treat `None` as an empty result rather than an error:
555/// the property-path engine, when both endpoints are variables, probes every
556/// term (including literals) as a candidate start node, and those probes must
557/// yield no rows rather than failing the whole query.
558#[allow(clippy::type_complexity)]
559pub(crate) fn pattern_to_query_terms(
560    pattern: &TriplePattern,
561) -> Result<
562    Option<(
563        Option<oxirs_core::model::Subject>,
564        Option<oxirs_core::model::Predicate>,
565        Option<oxirs_core::model::Object>,
566    )>,
567> {
568    let subject = match &pattern.subject {
569        AlgebraTerm::Iri(iri) => Some(oxirs_core::model::Subject::NamedNode(iri.clone())),
570        AlgebraTerm::Variable(_) => None,
571        AlgebraTerm::BlankNode(id) => Some(oxirs_core::model::Subject::BlankNode(
572            oxirs_core::model::BlankNode::new(id)
573                .map_err(|e| anyhow!("Invalid blank node: {}", e))?,
574        )),
575        // A literal / property-path / quoted-triple can never be a subject.
576        _ => return Ok(None),
577    };
578
579    let predicate = match &pattern.predicate {
580        AlgebraTerm::Iri(iri) => Some(oxirs_core::model::Predicate::NamedNode(iri.clone())),
581        AlgebraTerm::Variable(_) => None,
582        AlgebraTerm::PropertyPath(path) => match path {
583            PropertyPath::Iri(iri) => Some(oxirs_core::model::Predicate::NamedNode(iri.clone())),
584            PropertyPath::Variable(_) => None,
585            // A complex path must be evaluated by the path engine, not matched
586            // as a single triple; reaching here is an engine routing fault.
587            _ => {
588                return Err(anyhow!(
589                    "Complex property paths not yet supported in find_triples"
590                ))
591            }
592        },
593        // A literal / blank node can never be a predicate.
594        _ => return Ok(None),
595    };
596
597    let object = match &pattern.object {
598        AlgebraTerm::Iri(iri) => Some(oxirs_core::model::Object::NamedNode(iri.clone())),
599        AlgebraTerm::Literal(lit) => Some(oxirs_core::model::Object::Literal(
600            algebra_literal_to_core(lit),
601        )),
602        AlgebraTerm::BlankNode(id) => Some(oxirs_core::model::Object::BlankNode(
603            oxirs_core::model::BlankNode::new(id)
604                .map_err(|e| anyhow!("Invalid blank node: {}", e))?,
605        )),
606        AlgebraTerm::Variable(_) => None,
607        // A property-path / quoted-triple can never be an object here.
608        _ => return Ok(None),
609    };
610
611    Ok(Some((subject, predicate, object)))
612}
613
614/// Convert store quads into algebra triples, preserving literal datatype and
615/// language. RDF-star quoted triples are skipped.
616pub(crate) fn quads_to_algebra_triples(
617    quads: Vec<oxirs_core::model::Quad>,
618) -> Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)> {
619    quads
620        .into_iter()
621        .filter_map(|quad| {
622            let s = match quad.subject() {
623                oxirs_core::model::Subject::NamedNode(n) => {
624                    AlgebraTerm::Iri(oxirs_core::model::NamedNode::new(n.as_str()).ok()?)
625                }
626                oxirs_core::model::Subject::BlankNode(b) => {
627                    AlgebraTerm::BlankNode(b.as_str().to_string())
628                }
629                oxirs_core::model::Subject::Variable(v) => AlgebraTerm::Variable(v.clone()),
630                oxirs_core::model::Subject::QuotedTriple(_) => return None,
631            };
632
633            let p = AlgebraTerm::Iri(
634                oxirs_core::model::NamedNode::new(quad.predicate().as_str()).ok()?,
635            );
636
637            let o = match quad.object() {
638                oxirs_core::model::Object::NamedNode(n) => {
639                    AlgebraTerm::Iri(oxirs_core::model::NamedNode::new(n.as_str()).ok()?)
640                }
641                oxirs_core::model::Object::Literal(l) => {
642                    AlgebraTerm::Literal(core_literal_to_algebra(l))
643                }
644                oxirs_core::model::Object::BlankNode(b) => {
645                    AlgebraTerm::BlankNode(b.as_str().to_string())
646                }
647                oxirs_core::model::Object::Variable(v) => AlgebraTerm::Variable(v.clone()),
648                oxirs_core::model::Object::QuotedTriple(_) => return None,
649            };
650
651            Some((s, p, o))
652        })
653        .collect()
654}
655
656/// Map a [`GraphSelector`] to the concrete store [`GraphName`] it addresses.
657pub(crate) fn graph_selector_to_name(selector: &GraphSelector) -> GraphName {
658    match selector {
659        GraphSelector::DefaultGraph => GraphName::DefaultGraph,
660        GraphSelector::Named(iri) => GraphName::NamedNode(iri.clone()),
661    }
662}
663
664/// Enumerate the distinct named graphs of a store as algebra `Term::Iri`
665/// values.
666///
667/// Delegates to the store's [`Store::named_graphs`](oxirs_core::rdf_store::Store::named_graphs)
668/// trait method, which both real oxirs-core stores (`RdfStore` and
669/// `ConcreteStore`) override to read the interned graph-name index in
670/// O(graphs). This replaces the earlier streaming `for_each_quad` scan
671/// (O(quads)) that only existed because the trait method used to return an
672/// empty vector; now that the concrete stores populate it for real, the trait
673/// is the authoritative source.
674pub(crate) fn collect_named_graphs(
675    store: &dyn oxirs_core::rdf_store::Store,
676) -> Result<Vec<AlgebraTerm>> {
677    Ok(store
678        .named_graphs()?
679        .into_iter()
680        .map(AlgebraTerm::Iri)
681        .collect())
682}
683
684/// Adapter exposing any `&dyn oxirs_core::Store` as a query [`Dataset`].
685///
686/// This is the bridge used by `UPDATE ... WHERE` evaluation so that
687/// DELETE/INSERT WHERE and DELETE-INSERT operate on the real triple store
688/// (via `Store::find_quads`) rather than a disconnected in-memory executor.
689pub struct StoreRefDataset<'s> {
690    store: &'s dyn oxirs_core::rdf_store::Store,
691}
692
693impl<'s> StoreRefDataset<'s> {
694    /// Wrap a borrowed store as a dataset.
695    pub fn new(store: &'s dyn oxirs_core::rdf_store::Store) -> Self {
696        Self { store }
697    }
698}
699
700impl<'s> Dataset for StoreRefDataset<'s> {
701    fn find_triples(
702        &self,
703        pattern: &TriplePattern,
704    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
705        // Plain BGP reads the dataset's default graph only. Previously this
706        // passed `None` (unbound graph), which UNIONED every named graph and
707        // violated SPARQL default-graph semantics.
708        self.find_triples_in(&GraphSelector::DefaultGraph, pattern)
709    }
710
711    fn find_triples_in(
712        &self,
713        selector: &GraphSelector,
714        pattern: &TriplePattern,
715    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
716        let Some((subject, predicate, object)) = pattern_to_query_terms(pattern)? else {
717            return Ok(Vec::new());
718        };
719        let graph_name = graph_selector_to_name(selector);
720        let quads = self.store.find_quads(
721            subject.as_ref(),
722            predicate.as_ref(),
723            object.as_ref(),
724            Some(&graph_name),
725        )?;
726        Ok(quads_to_algebra_triples(quads))
727    }
728
729    fn has_graph_support(&self) -> bool {
730        true
731    }
732
733    fn named_graphs(&self) -> Result<Vec<AlgebraTerm>> {
734        collect_named_graphs(self.store)
735    }
736
737    fn contains_triple(
738        &self,
739        subject: &AlgebraTerm,
740        predicate: &AlgebraTerm,
741        object: &AlgebraTerm,
742    ) -> Result<bool> {
743        let pattern = TriplePattern::new(subject.clone(), predicate.clone(), object.clone());
744        Ok(!self.find_triples(&pattern)?.is_empty())
745    }
746
747    fn subjects(&self) -> Result<Vec<AlgebraTerm>> {
748        let quads = self.store.find_quads(None, None, None, None)?;
749        let subjects: HashSet<_> = quads
750            .into_iter()
751            .filter_map(|quad| match quad.subject() {
752                oxirs_core::model::Subject::NamedNode(n) => Some(AlgebraTerm::Iri(
753                    oxirs_core::model::NamedNode::new(n.as_str()).ok()?,
754                )),
755                oxirs_core::model::Subject::BlankNode(b) => {
756                    Some(AlgebraTerm::BlankNode(b.as_str().to_string()))
757                }
758                oxirs_core::model::Subject::Variable(v) => Some(AlgebraTerm::Variable(v.clone())),
759                oxirs_core::model::Subject::QuotedTriple(_) => None,
760            })
761            .collect();
762        Ok(subjects.into_iter().collect())
763    }
764
765    fn predicates(&self) -> Result<Vec<AlgebraTerm>> {
766        let quads = self.store.find_quads(None, None, None, None)?;
767        let predicates: HashSet<_> = quads
768            .into_iter()
769            .filter_map(|quad| {
770                Some(AlgebraTerm::Iri(
771                    oxirs_core::model::NamedNode::new(quad.predicate().as_str()).ok()?,
772                ))
773            })
774            .collect();
775        Ok(predicates.into_iter().collect())
776    }
777
778    fn objects(&self) -> Result<Vec<AlgebraTerm>> {
779        let quads = self.store.find_quads(None, None, None, None)?;
780        let objects: HashSet<_> = quads
781            .into_iter()
782            .filter_map(|quad| match quad.object() {
783                oxirs_core::model::Object::NamedNode(n) => Some(AlgebraTerm::Iri(
784                    oxirs_core::model::NamedNode::new(n.as_str()).ok()?,
785                )),
786                oxirs_core::model::Object::Literal(l) => {
787                    Some(AlgebraTerm::Literal(core_literal_to_algebra(l)))
788                }
789                oxirs_core::model::Object::BlankNode(b) => {
790                    Some(AlgebraTerm::BlankNode(b.as_str().to_string()))
791                }
792                oxirs_core::model::Object::Variable(v) => Some(AlgebraTerm::Variable(v.clone())),
793                oxirs_core::model::Object::QuotedTriple(_) => None,
794            })
795            .collect();
796        Ok(objects.into_iter().collect())
797    }
798}
799
800/// Build a wildcard triple pattern (`?s ?p ?o`) for whole-graph scans.
801fn wildcard_pattern() -> TriplePattern {
802    TriplePattern::new(
803        AlgebraTerm::Variable(crate::algebra::Variable::new_unchecked("s")),
804        AlgebraTerm::Variable(crate::algebra::Variable::new_unchecked("p")),
805        AlgebraTerm::Variable(crate::algebra::Variable::new_unchecked("o")),
806    )
807}
808
809/// A [`Dataset`] view that pins every plain lookup to a single graph.
810///
811/// Wrapping a base dataset in a `GraphScopedDataset` makes the *unscoped*
812/// [`Dataset::find_triples`] entry point — the funnel that BGP evaluation,
813/// property paths and `EXISTS` all pass through — read only the selected graph.
814/// The inner pattern-evaluation code therefore needs no changes to honor
815/// `GRAPH` scoping: it keeps calling `find_triples`, and the view redirects it.
816///
817/// A nested `GRAPH` inside a scoped view overrides the outer scope, exactly as
818/// SPARQL requires: [`Dataset::find_triples_in`] forwards the inner selector to
819/// the base rather than intersecting with the outer one.
820pub struct GraphScopedDataset<'a> {
821    base: &'a dyn Dataset,
822    selector: GraphSelector,
823}
824
825impl<'a> GraphScopedDataset<'a> {
826    /// Restrict all plain lookups on `base` to the graph named by `selector`.
827    pub fn new(base: &'a dyn Dataset, selector: GraphSelector) -> Self {
828        Self { base, selector }
829    }
830}
831
832impl<'a> Dataset for GraphScopedDataset<'a> {
833    fn find_triples(
834        &self,
835        pattern: &TriplePattern,
836    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
837        self.base.find_triples_in(&self.selector, pattern)
838    }
839
840    fn find_triples_in(
841        &self,
842        selector: &GraphSelector,
843        pattern: &TriplePattern,
844    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
845        // Inner GRAPH wins over the outer scope.
846        self.base.find_triples_in(selector, pattern)
847    }
848
849    fn has_graph_support(&self) -> bool {
850        self.base.has_graph_support()
851    }
852
853    fn named_graphs(&self) -> Result<Vec<AlgebraTerm>> {
854        self.base.named_graphs()
855    }
856
857    fn contains_triple(
858        &self,
859        subject: &AlgebraTerm,
860        predicate: &AlgebraTerm,
861        object: &AlgebraTerm,
862    ) -> Result<bool> {
863        let pattern = TriplePattern::new(subject.clone(), predicate.clone(), object.clone());
864        Ok(!self.find_triples(&pattern)?.is_empty())
865    }
866
867    fn subjects(&self) -> Result<Vec<AlgebraTerm>> {
868        let triples = self.find_triples(&wildcard_pattern())?;
869        let set: HashSet<_> = triples.into_iter().map(|(s, _, _)| s).collect();
870        Ok(set.into_iter().collect())
871    }
872
873    fn predicates(&self) -> Result<Vec<AlgebraTerm>> {
874        let triples = self.find_triples(&wildcard_pattern())?;
875        let set: HashSet<_> = triples.into_iter().map(|(_, p, _)| p).collect();
876        Ok(set.into_iter().collect())
877    }
878
879    fn objects(&self) -> Result<Vec<AlgebraTerm>> {
880        let triples = self.find_triples(&wildcard_pattern())?;
881        let set: HashSet<_> = triples.into_iter().map(|(_, _, o)| o).collect();
882        Ok(set.into_iter().collect())
883    }
884}
885
886/// A [`Dataset`] view implementing SPARQL `FROM` / `FROM NAMED` dataset
887/// construction over a base dataset.
888///
889/// * The active **default graph** is the union of the `FROM` graphs
890///   (`default_graphs`); with no `FROM` graphs it falls back to the base
891///   dataset's own default graph.
892/// * The visible **named graphs** (for `GRAPH` and `GRAPH ?g` enumeration) are
893///   exactly the `FROM NAMED` graphs (`named_graphs`); a `FROM`-only clause
894///   therefore exposes no named graphs.
895/// * An entirely empty clause is a transparent passthrough of the base's
896///   semantics.
897///
898/// Construct with [`with_dataset_clause`] from a parsed
899/// [`crate::query::DatasetClause`], or with [`DatasetView::new`] from explicit
900/// graph lists.
901pub struct DatasetView<'a> {
902    base: &'a dyn Dataset,
903    default_graphs: Vec<NamedNode>,
904    named_graphs: Vec<NamedNode>,
905}
906
907impl<'a> DatasetView<'a> {
908    /// Build a view from explicit `FROM` / `FROM NAMED` graph lists.
909    pub fn new(
910        base: &'a dyn Dataset,
911        default_graphs: Vec<NamedNode>,
912        named_graphs: Vec<NamedNode>,
913    ) -> Self {
914        Self {
915            base,
916            default_graphs,
917            named_graphs,
918        }
919    }
920
921    /// Whether the clause is empty (no `FROM`, no `FROM NAMED`); such a view is
922    /// a transparent passthrough of the base dataset.
923    fn is_passthrough(&self) -> bool {
924        self.default_graphs.is_empty() && self.named_graphs.is_empty()
925    }
926
927    /// Triples visible in the view's active default graph: the union of the
928    /// `FROM` graphs, or the base default graph when there are none.
929    fn default_graph_triples(
930        &self,
931        pattern: &TriplePattern,
932    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
933        if self.default_graphs.is_empty() {
934            return self.base.find_triples(pattern);
935        }
936        let mut seen: HashSet<(AlgebraTerm, AlgebraTerm, AlgebraTerm)> = HashSet::new();
937        let mut out: Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)> = Vec::new();
938        for graph in &self.default_graphs {
939            let selector = GraphSelector::Named(graph.clone());
940            for triple in self.base.find_triples_in(&selector, pattern)? {
941                if seen.insert(triple.clone()) {
942                    out.push(triple);
943                }
944            }
945        }
946        Ok(out)
947    }
948}
949
950/// Build a [`DatasetView`] over `base` from a parsed `FROM` / `FROM NAMED`
951/// clause.
952///
953/// The fuseki wave calls this with `Query.dataset` to materialize the RDF
954/// dataset a query runs against. An empty clause yields a passthrough view.
955pub fn with_dataset_clause<'a>(
956    base: &'a dyn Dataset,
957    clause: &crate::query::DatasetClause,
958) -> DatasetView<'a> {
959    DatasetView::new(
960        base,
961        clause.default_graphs.clone(),
962        clause.named_graphs.clone(),
963    )
964}
965
966impl<'a> Dataset for DatasetView<'a> {
967    fn find_triples(
968        &self,
969        pattern: &TriplePattern,
970    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
971        if self.is_passthrough() {
972            return self.base.find_triples(pattern);
973        }
974        self.default_graph_triples(pattern)
975    }
976
977    fn find_triples_in(
978        &self,
979        selector: &GraphSelector,
980        pattern: &TriplePattern,
981    ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
982        if self.is_passthrough() {
983            return self.base.find_triples_in(selector, pattern);
984        }
985        match selector {
986            GraphSelector::DefaultGraph => self.default_graph_triples(pattern),
987            GraphSelector::Named(graph) => {
988                // Only graphs named in FROM NAMED are visible.
989                if self.named_graphs.iter().any(|n| n == graph) {
990                    self.base.find_triples_in(selector, pattern)
991                } else {
992                    Ok(Vec::new())
993                }
994            }
995        }
996    }
997
998    fn has_graph_support(&self) -> bool {
999        self.base.has_graph_support()
1000    }
1001
1002    fn named_graphs(&self) -> Result<Vec<AlgebraTerm>> {
1003        if self.is_passthrough() {
1004            return self.base.named_graphs();
1005        }
1006        Ok(self
1007            .named_graphs
1008            .iter()
1009            .cloned()
1010            .map(AlgebraTerm::Iri)
1011            .collect())
1012    }
1013
1014    fn contains_triple(
1015        &self,
1016        subject: &AlgebraTerm,
1017        predicate: &AlgebraTerm,
1018        object: &AlgebraTerm,
1019    ) -> Result<bool> {
1020        let pattern = TriplePattern::new(subject.clone(), predicate.clone(), object.clone());
1021        Ok(!self.find_triples(&pattern)?.is_empty())
1022    }
1023
1024    fn subjects(&self) -> Result<Vec<AlgebraTerm>> {
1025        if self.is_passthrough() {
1026            return self.base.subjects();
1027        }
1028        let triples = self.default_graph_triples(&wildcard_pattern())?;
1029        let set: HashSet<_> = triples.into_iter().map(|(s, _, _)| s).collect();
1030        Ok(set.into_iter().collect())
1031    }
1032
1033    fn predicates(&self) -> Result<Vec<AlgebraTerm>> {
1034        if self.is_passthrough() {
1035            return self.base.predicates();
1036        }
1037        let triples = self.default_graph_triples(&wildcard_pattern())?;
1038        let set: HashSet<_> = triples.into_iter().map(|(_, p, _)| p).collect();
1039        Ok(set.into_iter().collect())
1040    }
1041
1042    fn objects(&self) -> Result<Vec<AlgebraTerm>> {
1043        if self.is_passthrough() {
1044            return self.base.objects();
1045        }
1046        let triples = self.default_graph_triples(&wildcard_pattern())?;
1047        let set: HashSet<_> = triples.into_iter().map(|(_, _, o)| o).collect();
1048        Ok(set.into_iter().collect())
1049    }
1050}
1051
1052#[cfg(test)]
1053mod store_ref_dataset_tests {
1054    use super::*;
1055    use crate::algebra::{Literal, Variable};
1056    use oxirs_core::model::{GraphName, Literal as CoreLiteral, NamedNode, Quad};
1057    use oxirs_core::rdf_store::{ConcreteStore, Store};
1058
1059    #[test]
1060    fn store_ref_dataset_matches_typed_literal() {
1061        let store = ConcreteStore::new().expect("store");
1062        let s = NamedNode::new_unchecked("http://ex/s");
1063        let age = NamedNode::new_unchecked("http://ex/age");
1064        let xsd_int = NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#integer");
1065
1066        // One typed and one plain literal with the same lexical form.
1067        store
1068            .insert(&Quad::new(
1069                s.clone(),
1070                age.clone(),
1071                CoreLiteral::new_typed("25", xsd_int.clone()),
1072                GraphName::DefaultGraph,
1073            ))
1074            .expect("insert typed");
1075        store
1076            .insert(&Quad::new(
1077                s.clone(),
1078                age.clone(),
1079                CoreLiteral::new("25"),
1080                GraphName::DefaultGraph,
1081            ))
1082            .expect("insert plain");
1083
1084        let ds = StoreRefDataset::new(&store);
1085        let pattern = TriplePattern {
1086            subject: AlgebraTerm::Variable(Variable::new_unchecked("s")),
1087            predicate: AlgebraTerm::Iri(age.clone()),
1088            object: AlgebraTerm::Literal(Literal {
1089                value: "25".to_string(),
1090                language: None,
1091                datatype: Some(xsd_int.clone()),
1092            }),
1093        };
1094        let results = ds.find_triples(&pattern).expect("find");
1095        assert_eq!(
1096            results.len(),
1097            1,
1098            "typed-literal pattern must match exactly the typed triple"
1099        );
1100        // The returned object literal must preserve the integer datatype (the
1101        // pre-fix code coerced everything to plain xsd:string and dropped it).
1102        match &results[0].2 {
1103            AlgebraTerm::Literal(lit) => {
1104                assert_eq!(lit.value, "25");
1105                assert_eq!(
1106                    lit.datatype.as_ref().map(|d| d.as_str()),
1107                    Some("http://www.w3.org/2001/XMLSchema#integer")
1108                );
1109            }
1110            other => panic!("expected literal, got {other:?}"),
1111        }
1112    }
1113
1114    #[test]
1115    fn store_ref_dataset_preserves_language_tag() {
1116        let store = ConcreteStore::new().expect("store");
1117        let s = NamedNode::new_unchecked("http://ex/s");
1118        let label = NamedNode::new_unchecked("http://ex/label");
1119        store
1120            .insert(&Quad::new(
1121                s.clone(),
1122                label.clone(),
1123                CoreLiteral::new_language_tagged_literal("hi", "en").expect("lang literal"),
1124                GraphName::DefaultGraph,
1125            ))
1126            .expect("insert");
1127
1128        let ds = StoreRefDataset::new(&store);
1129        let pattern = TriplePattern {
1130            subject: AlgebraTerm::Variable(Variable::new_unchecked("s")),
1131            predicate: AlgebraTerm::Iri(label.clone()),
1132            object: AlgebraTerm::Literal(Literal {
1133                value: "hi".to_string(),
1134                language: Some("en".to_string()),
1135                datatype: None,
1136            }),
1137        };
1138        let results = ds.find_triples(&pattern).expect("find");
1139        assert_eq!(results.len(), 1, "language-tagged pattern must match");
1140        match &results[0].2 {
1141            AlgebraTerm::Literal(lit) => {
1142                assert_eq!(lit.language.as_deref(), Some("en"));
1143            }
1144            other => panic!("expected literal, got {other:?}"),
1145        }
1146    }
1147}
1148
1149#[cfg(test)]
1150mod graph_scoping_tests {
1151    use super::*;
1152    use crate::algebra::Variable;
1153    use oxirs_core::model::{GraphName, NamedNode, Quad};
1154    use oxirs_core::rdf_store::{ConcreteStore, Store};
1155
1156    fn nn(s: &str) -> NamedNode {
1157        NamedNode::new_unchecked(s)
1158    }
1159
1160    fn wildcard() -> TriplePattern {
1161        TriplePattern {
1162            subject: AlgebraTerm::Variable(Variable::new_unchecked("s")),
1163            predicate: AlgebraTerm::Variable(Variable::new_unchecked("p")),
1164            object: AlgebraTerm::Variable(Variable::new_unchecked("o")),
1165        }
1166    }
1167
1168    /// Store with one default-graph triple and two triples spread over two
1169    /// named graphs `<g1>` (two triples) and `<g2>` (one triple).
1170    fn seeded_store() -> ConcreteStore {
1171        let store = ConcreteStore::new().expect("store");
1172        let p = nn("http://ex/p");
1173        store
1174            .insert(&Quad::new(
1175                nn("http://ex/sd"),
1176                p.clone(),
1177                nn("http://ex/od"),
1178                GraphName::DefaultGraph,
1179            ))
1180            .expect("insert default");
1181        store
1182            .insert(&Quad::new(
1183                nn("http://ex/s1"),
1184                p.clone(),
1185                nn("http://ex/o1"),
1186                GraphName::NamedNode(nn("http://ex/g1")),
1187            ))
1188            .expect("insert g1 a");
1189        store
1190            .insert(&Quad::new(
1191                nn("http://ex/s2"),
1192                p.clone(),
1193                nn("http://ex/o2"),
1194                GraphName::NamedNode(nn("http://ex/g1")),
1195            ))
1196            .expect("insert g1 b");
1197        store
1198            .insert(&Quad::new(
1199                nn("http://ex/s3"),
1200                p,
1201                nn("http://ex/o3"),
1202                GraphName::NamedNode(nn("http://ex/g2")),
1203            ))
1204            .expect("insert g2");
1205        store
1206    }
1207
1208    #[test]
1209    fn store_ref_find_triples_reads_default_graph_only() {
1210        let store = seeded_store();
1211        let ds = StoreRefDataset::new(&store);
1212        // Regression for the union bug: the plain lookup must see ONLY the one
1213        // default-graph triple, not all four.
1214        let all = ds.find_triples(&wildcard()).expect("find default");
1215        assert_eq!(
1216            all.len(),
1217            1,
1218            "plain find_triples must be default-graph-only"
1219        );
1220        assert_eq!(all[0].0, AlgebraTerm::Iri(nn("http://ex/sd")));
1221    }
1222
1223    #[test]
1224    fn store_ref_find_triples_in_named_graph() {
1225        let store = seeded_store();
1226        let ds = StoreRefDataset::new(&store);
1227        let g1 = ds
1228            .find_triples_in(&GraphSelector::Named(nn("http://ex/g1")), &wildcard())
1229            .expect("find g1");
1230        assert_eq!(g1.len(), 2, "named graph g1 has two triples");
1231        let g2 = ds
1232            .find_triples_in(&GraphSelector::Named(nn("http://ex/g2")), &wildcard())
1233            .expect("find g2");
1234        assert_eq!(g2.len(), 1, "named graph g2 has one triple");
1235        let dg = ds
1236            .find_triples_in(&GraphSelector::DefaultGraph, &wildcard())
1237            .expect("find default");
1238        assert_eq!(dg.len(), 1, "default graph has one triple");
1239    }
1240
1241    #[test]
1242    fn store_ref_named_graphs_enumerates() {
1243        let store = seeded_store();
1244        let ds = StoreRefDataset::new(&store);
1245        assert!(ds.has_graph_support());
1246        let mut graphs = ds.named_graphs().expect("named graphs");
1247        graphs.sort_by_key(|t| format!("{t:?}"));
1248        assert_eq!(graphs.len(), 2, "two distinct named graphs");
1249        assert!(graphs.contains(&AlgebraTerm::Iri(nn("http://ex/g1"))));
1250        assert!(graphs.contains(&AlgebraTerm::Iri(nn("http://ex/g2"))));
1251    }
1252
1253    #[test]
1254    fn default_impl_find_triples_in_fails_loud() {
1255        // A dataset that does not override the graph methods must FAIL LOUD
1256        // rather than silently union all graphs.
1257        struct NoGraphDataset;
1258        impl Dataset for NoGraphDataset {
1259            fn find_triples(
1260                &self,
1261                _pattern: &TriplePattern,
1262            ) -> Result<Vec<(AlgebraTerm, AlgebraTerm, AlgebraTerm)>> {
1263                Ok(Vec::new())
1264            }
1265            fn contains_triple(
1266                &self,
1267                _s: &AlgebraTerm,
1268                _p: &AlgebraTerm,
1269                _o: &AlgebraTerm,
1270            ) -> Result<bool> {
1271                Ok(false)
1272            }
1273            fn subjects(&self) -> Result<Vec<AlgebraTerm>> {
1274                Ok(Vec::new())
1275            }
1276            fn predicates(&self) -> Result<Vec<AlgebraTerm>> {
1277                Ok(Vec::new())
1278            }
1279            fn objects(&self) -> Result<Vec<AlgebraTerm>> {
1280                Ok(Vec::new())
1281            }
1282        }
1283        let ds = NoGraphDataset;
1284        assert!(!ds.has_graph_support());
1285        assert!(ds
1286            .find_triples_in(&GraphSelector::Named(nn("http://ex/g")), &wildcard())
1287            .is_err());
1288        assert!(ds.named_graphs().is_err());
1289    }
1290
1291    #[test]
1292    fn dataset_view_from_single_graph_default() {
1293        let store = seeded_store();
1294        let base = StoreRefDataset::new(&store);
1295        // FROM <g1>: default graph becomes g1's two triples.
1296        let view = DatasetView::new(&base, vec![nn("http://ex/g1")], vec![]);
1297        let default = view.find_triples(&wildcard()).expect("view default");
1298        assert_eq!(default.len(), 2, "FROM <g1> default graph = g1's triples");
1299        // FROM-only clause exposes no named graphs.
1300        assert!(view.named_graphs().expect("named").is_empty());
1301    }
1302
1303    #[test]
1304    fn dataset_view_from_multiple_graphs_union() {
1305        let store = seeded_store();
1306        let base = StoreRefDataset::new(&store);
1307        // FROM <g1> FROM <g2>: default graph = union (2 + 1 = 3 triples).
1308        let view = DatasetView::new(&base, vec![nn("http://ex/g1"), nn("http://ex/g2")], vec![]);
1309        let default = view.find_triples(&wildcard()).expect("view default");
1310        assert_eq!(default.len(), 3, "FROM union of g1+g2 = three triples");
1311    }
1312
1313    #[test]
1314    fn dataset_view_from_named_restricts_visibility() {
1315        let store = seeded_store();
1316        let base = StoreRefDataset::new(&store);
1317        // FROM NAMED <g1> only: g1 visible, g2 not.
1318        let view = DatasetView::new(&base, vec![], vec![nn("http://ex/g1")]);
1319        let named = view.named_graphs().expect("named graphs");
1320        assert_eq!(named, vec![AlgebraTerm::Iri(nn("http://ex/g1"))]);
1321        // g1 is visible.
1322        let g1 = view
1323            .find_triples_in(&GraphSelector::Named(nn("http://ex/g1")), &wildcard())
1324            .expect("find g1");
1325        assert_eq!(g1.len(), 2);
1326        // g2 is NOT in FROM NAMED -> invisible -> empty.
1327        let g2 = view
1328            .find_triples_in(&GraphSelector::Named(nn("http://ex/g2")), &wildcard())
1329            .expect("find g2");
1330        assert!(g2.is_empty(), "graph outside FROM NAMED must be invisible");
1331        // No FROM -> default graph falls back to the store default (one triple).
1332        let default = view.find_triples(&wildcard()).expect("view default");
1333        assert_eq!(default.len(), 1, "no FROM => store default graph");
1334    }
1335
1336    #[test]
1337    fn dataset_view_empty_clause_is_passthrough() {
1338        let store = seeded_store();
1339        let base = StoreRefDataset::new(&store);
1340        let view = DatasetView::new(&base, vec![], vec![]);
1341        // Passthrough: identical to the base dataset's behavior.
1342        let default = view.find_triples(&wildcard()).expect("view default");
1343        assert_eq!(
1344            default.len(),
1345            1,
1346            "empty clause preserves default-graph read"
1347        );
1348        let mut graphs = view.named_graphs().expect("named");
1349        graphs.sort_by_key(|t| format!("{t:?}"));
1350        assert_eq!(graphs.len(), 2, "empty clause preserves base named graphs");
1351    }
1352
1353    #[test]
1354    fn graph_scoped_dataset_routes_plain_lookup() {
1355        let store = seeded_store();
1356        let base = StoreRefDataset::new(&store);
1357        // Wrapping in a scope for g1 makes the *plain* find_triples read g1.
1358        let scoped = GraphScopedDataset::new(&base, GraphSelector::Named(nn("http://ex/g1")));
1359        let triples = scoped.find_triples(&wildcard()).expect("scoped find");
1360        assert_eq!(triples.len(), 2, "scoped plain lookup reads g1 only");
1361    }
1362}