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}