Skip to main content

rudof_rdf/rdf_core/query/
query_solutions.rs

1use crate::rdf_core::{RDFError, Rdf, query::QuerySolution, term::Object};
2use prefixmap::{PrefixMap, PrefixMapError};
3use serde::Serialize;
4use std::fmt::Display;
5use std::io::Write;
6use tabled::{builder::Builder, settings::Style};
7
8/// Represents a collection of query solutions from a SPARQL SELECT query.
9///
10/// This type holds the complete result set from a SPARQL query execution,
11/// including all solution rows and a prefix map for qualifying IRIs in output.
12///
13/// # Type Parameters
14///
15/// * `S` - The RDF graph type implementing [`Rdf`]
16#[derive(Debug, Clone, Serialize)]
17pub struct QuerySolutions<S: Rdf> {
18    /// The collection of query solutions (result rows).
19    solutions: Vec<QuerySolution<S>>,
20    /// Prefix map for qualifying IRIs in output.
21    prefixmap: PrefixMap,
22}
23
24impl<S: Rdf> QuerySolutions<S> {
25    /// Creates a new query solutions collection with the given data.
26    ///
27    /// # Arguments
28    ///
29    /// * `solutions` - Vector of query solution rows
30    /// * `prefixmap` - Prefix map for IRI qualification in output
31    pub fn new(solutions: Vec<QuerySolution<S>>, prefixmap: PrefixMap) -> QuerySolutions<S> {
32        QuerySolutions { solutions, prefixmap }
33    }
34
35    /// Creates an empty query solutions collection.
36    ///
37    /// Returns a new instance with no solutions and an empty prefix map.
38    pub fn empty() -> QuerySolutions<S> {
39        QuerySolutions {
40            solutions: Vec::new(),
41            prefixmap: PrefixMap::new(),
42        }
43    }
44
45    /// Returns a reference to the prefix map.
46    ///
47    /// The prefix map contains namespace bindings used for qualifying IRIs
48    /// when displaying or formatting results.
49    pub fn prefixmap(&self) -> &PrefixMap {
50        &self.prefixmap
51    }
52
53    /// Extends this collection with additional solutions and prefix mappings.
54    ///
55    /// # Arguments
56    ///
57    /// * `solutions` - Additional solutions to append
58    /// * `prefixmap` - Prefix map to merge (new prefixes or updated IRIs)
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the prefix maps have conflicting definitions for
63    /// the same prefix (same prefix bound to different namespaces).
64    pub fn extend(&mut self, solutions: Vec<QuerySolution<S>>, prefixmap: PrefixMap) -> Result<(), PrefixMapError> {
65        self.solutions.extend(solutions);
66        self.prefixmap.merge(prefixmap)?;
67        Ok(())
68    }
69
70    /// Returns an iterator over the query solutions.
71    pub fn iter(&self) -> impl Iterator<Item = &QuerySolution<S>> {
72        self.solutions.iter()
73    }
74
75    /// Returns the number of solutions in this collection.
76    pub fn count(&self) -> usize {
77        self.solutions.len()
78    }
79
80    /// Writes the query solutions as a formatted ASCII table.
81    ///
82    /// Produces a human-readable table with modern rounded borders, row numbers,
83    /// and qualified IRIs using the prefix map. The table includes:
84    ///
85    /// - Row numbers in the first column (1-indexed)
86    /// - Variable names as column headers (with `?` prefix)
87    /// - Qualified IRIs using registered prefixes
88    /// - Blank nodes displayed as `_:identifier`
89    /// - Literals shown with their datatype/language tags
90    /// - RDF-star quoted triples in `<< s p o >>` syntax
91    /// - Empty cells for unbound variables
92    ///
93    /// # Arguments
94    ///
95    /// * `writer` - Output destination (stdout, file, buffer, etc.)
96    ///
97    /// Returns an error if:
98    /// - Writing to the output fails
99    /// - Term conversion fails when processing solutions
100    pub fn write_table(&self, writer: &mut dyn Write) -> Result<(), RDFError> {
101        if self.solutions.is_empty() {
102            return write!(writer, "No results").map_err(|e| RDFError::WritingTableError { error: format!("{e}") });
103        }
104
105        let first = &self.solutions[0];
106        let mut builder = Builder::default();
107
108        // Build header row with pre-allocated capacity
109        let variable_count = first.variables_iter().count();
110        let mut variables = Vec::with_capacity(variable_count + 1);
111        variables.push(String::new()); // First column = index
112        variables.extend(first.variables_iter().map(|v| v.to_string()));
113        builder.push_record(variables);
114
115        // Build data rows
116        for (idx, result) in self.solutions.iter().enumerate() {
117            let mut record = Vec::with_capacity(variable_count + 1);
118            record.push((idx + 1).to_string()); // First column = index
119
120            for (var_idx, _variable) in result.variables_iter().enumerate() {
121                let str = match result.find_solution(var_idx) {
122                    Some(term) => {
123                        let object = S::term_as_object(term)?;
124                        match object {
125                            Object::Iri(iri) => self.prefixmap.qualify(&iri),
126                            Object::BlankNode(blank_node) => format!("_:{}", blank_node),
127                            Object::Literal(literal) => literal.to_string(),
128                            Object::Triple {
129                                subject,
130                                predicate,
131                                object,
132                            } => format!(
133                                "<<{} {} {}>>",
134                                subject.show_qualified(&self.prefixmap),
135                                self.prefixmap.qualify(&predicate),
136                                object.show_qualified(&self.prefixmap)
137                            ),
138                        }
139                    },
140                    None => String::new(),
141                };
142                record.push(str);
143            }
144            builder.push_record(record);
145        }
146
147        let mut table = builder.build();
148        table.with(Style::modern_rounded());
149        writeln!(writer, "{table}").map_err(|e| RDFError::WritingTableError { error: format!("{e}") })?;
150
151        Ok(())
152    }
153}
154
155impl<S: Rdf + serde::Serialize> QuerySolutions<S> {
156    /// Serializes the query solutions as pretty-printed JSON.
157    pub fn as_json(&self) -> String {
158        serde_json::to_string_pretty(&self).unwrap_or_else(|_| "[]".to_string())
159    }
160}
161
162impl<S: Rdf> Display for QuerySolutions<S> {
163    /// Formats the query solutions as plain text, one solution per line.
164    ///
165    /// Each solution is displayed using its `show()` method, which presents
166    /// variable bindings in the format `?variable -> value`. Solutions are
167    /// separated by blank lines for readability.
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        for solution in &self.solutions {
170            writeln!(f, "{}", solution.show())?;
171        }
172        Ok(())
173    }
174}
175
176impl<S: Rdf> IntoIterator for QuerySolutions<S> {
177    /// The type of items yielded by the iterator.
178    type Item = QuerySolution<S>;
179    /// The iterator type for consuming iteration.
180    type IntoIter = std::vec::IntoIter<QuerySolution<S>>;
181
182    /// Converts the query solutions into an iterator, consuming the collection.
183    ///
184    /// This allows using `QuerySolutions` directly in for loops and other
185    /// iterator contexts. The collection is consumed, yielding owned
186    /// `QuerySolution` instances.
187    fn into_iter(self) -> Self::IntoIter {
188        self.solutions.into_iter()
189    }
190}