Skip to main content

tensorlogic_oxirs_bridge/
sparql_builder.rs

1//! Programmatic SPARQL query builder — a fluent builder API that constructs
2//! SPARQL query strings without requiring manual string formatting.
3//!
4//! # Example
5//!
6//! ```rust
7//! use tensorlogic_oxirs_bridge::sparql_builder::{
8//!     SelectQuery, WhereClause, SparqlTerm, SparqlFilter,
9//! };
10//!
11//! let query = SelectQuery::new()
12//!     .prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
13//!     .prefix("ex", "http://example.org/")
14//!     .select("x")
15//!     .select("name")
16//!     .where_clause(
17//!         WhereClause::new()
18//!             .triple(
19//!                 SparqlTerm::var("x"),
20//!                 SparqlTerm::prefixed("rdf", "type"),
21//!                 SparqlTerm::prefixed("ex", "Person"),
22//!             )
23//!             .filter(SparqlFilter::gt("age", 18.0)),
24//!     )
25//!     .limit(100)
26//!     .build()
27//!     .expect("valid query");
28//!
29//! assert!(query.contains("SELECT"));
30//! ```
31
32use std::fmt;
33
34// ---------------------------------------------------------------------------
35// Error type
36// ---------------------------------------------------------------------------
37
38/// Errors that can be produced by the SPARQL query builder.
39#[derive(Debug, Clone)]
40pub enum SparqlBuilderError {
41    /// The WHERE clause contained no patterns (SELECT query requires at least one).
42    EmptyWhereClause,
43    /// A variable name was syntactically invalid.
44    InvalidVariableName(String),
45    /// Mutually exclusive modifiers were both set.
46    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
71// ---------------------------------------------------------------------------
72// validate_variable_name
73// ---------------------------------------------------------------------------
74
75/// Validate a SPARQL variable name.
76///
77/// A valid name must start with a letter (ASCII or Unicode) or `_`, followed
78/// by letters, digits, `_`, `-`, or `.`.
79pub 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// ---------------------------------------------------------------------------
89// SparqlLiteral
90// ---------------------------------------------------------------------------
91
92/// An RDF literal with an optional datatype or language tag.
93#[derive(Debug, Clone, PartialEq)]
94pub struct SparqlLiteral {
95    /// The lexical value of the literal.
96    pub value: String,
97    /// Optional datatype IRI (e.g. `"xsd:integer"`).
98    pub datatype: Option<String>,
99    /// Optional BCP-47 language tag (e.g. `"en"`).
100    pub lang_tag: Option<String>,
101}
102
103// ---------------------------------------------------------------------------
104// SparqlTerm
105// ---------------------------------------------------------------------------
106
107/// A SPARQL term: IRI, prefixed name, literal, blank node, or variable.
108#[derive(Debug, Clone, PartialEq)]
109pub enum SparqlTerm {
110    /// A full IRI, e.g. `<http://example.org/foo>`.
111    Iri(String),
112    /// A prefixed name, e.g. `rdf:type`.
113    PrefixedName(String, String),
114    /// An RDF literal.
115    Literal(SparqlLiteral),
116    /// A blank node, e.g. `_:b0`.
117    BlankNode(String),
118    /// A SPARQL variable, e.g. `?x`.
119    Variable(String),
120}
121
122impl SparqlTerm {
123    /// Create an IRI term.  If the string is not already wrapped in `<…>`,
124    /// the angle brackets are added automatically when rendered.
125    pub fn iri(s: impl Into<String>) -> Self {
126        SparqlTerm::Iri(s.into())
127    }
128
129    /// Create a variable term.
130    pub fn var(name: impl Into<String>) -> Self {
131        SparqlTerm::Variable(name.into())
132    }
133
134    /// Create a plain string literal.
135    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    /// Create a typed literal, e.g. `"42"^^xsd:integer`.
144    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    /// Create a language-tagged literal, e.g. `"hello"@en`.
153    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    /// Create a blank-node term.
162    pub fn blank(id: impl Into<String>) -> Self {
163        SparqlTerm::BlankNode(id.into())
164    }
165
166    /// Create a prefixed-name term.
167    pub fn prefixed(prefix: impl Into<String>, local: impl Into<String>) -> Self {
168        SparqlTerm::PrefixedName(prefix.into(), local.into())
169    }
170
171    /// Render this term as a SPARQL string fragment.
172    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
189/// Render an [`SparqlLiteral`] to its SPARQL string representation.
190fn render_literal(lit: &SparqlLiteral) -> String {
191    // Escape double-quotes inside the value.
192    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// ---------------------------------------------------------------------------
204// TriplePattern (builder variant)
205// ---------------------------------------------------------------------------
206
207/// A SPARQL triple pattern `(subject predicate object)`.
208#[derive(Debug, Clone)]
209pub struct BuilderTriplePattern {
210    /// The subject term.
211    pub subject: SparqlTerm,
212    /// The predicate term.
213    pub predicate: SparqlTerm,
214    /// The object term.
215    pub object: SparqlTerm,
216}
217
218impl BuilderTriplePattern {
219    /// Create a new triple pattern.
220    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    /// Render this triple pattern as a SPARQL line, e.g. `?s rdf:type ?o .`
229    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// ---------------------------------------------------------------------------
240// SparqlFilter
241// ---------------------------------------------------------------------------
242
243/// A SPARQL FILTER expression stored as a raw expression string.
244#[derive(Debug, Clone)]
245pub struct SparqlFilter {
246    /// The expression string, e.g. `?age > 18`.
247    pub expression: String,
248}
249
250impl SparqlFilter {
251    /// Create a filter from a raw expression string.
252    pub fn new(expr: impl Into<String>) -> Self {
253        SparqlFilter {
254            expression: expr.into(),
255        }
256    }
257
258    /// `?var > value`
259    pub fn gt(var: &str, value: f64) -> Self {
260        SparqlFilter::new(format!("?{var} > {value}"))
261    }
262
263    /// `?var < value`
264    pub fn lt(var: &str, value: f64) -> Self {
265        SparqlFilter::new(format!("?{var} < {value}"))
266    }
267
268    /// `?var = other`
269    pub fn eq(var: &str, other: &str) -> Self {
270        SparqlFilter::new(format!("?{var} = {other}"))
271    }
272
273    /// `langMatches(lang(?var), "lang")`
274    pub fn lang_matches(var: &str, lang: &str) -> Self {
275        SparqlFilter::new(format!("langMatches(lang(?{var}), \"{lang}\")"))
276    }
277
278    /// `regex(?var, "pattern")`
279    pub fn regex(var: &str, pattern: &str) -> Self {
280        SparqlFilter::new(format!("regex(?{var}, \"{pattern}\")"))
281    }
282
283    /// Logical AND of two filters.
284    pub fn and(left: SparqlFilter, right: SparqlFilter) -> Self {
285        SparqlFilter::new(format!("({}) && ({})", left.expression, right.expression))
286    }
287
288    /// Logical OR of two filters.
289    pub fn or(left: SparqlFilter, right: SparqlFilter) -> Self {
290        SparqlFilter::new(format!("({}) || ({})", left.expression, right.expression))
291    }
292
293    /// Logical NOT of a filter.
294    pub fn negate(inner: SparqlFilter) -> Self {
295        SparqlFilter::new(format!("!({})", inner.expression))
296    }
297
298    /// Render as a `FILTER (...)` clause line.
299    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// ---------------------------------------------------------------------------
313// OrderDirection
314// ---------------------------------------------------------------------------
315
316/// ORDER BY direction for a single variable.
317#[derive(Debug, Clone)]
318pub enum OrderDirection {
319    /// `ASC(?var)`
320    Asc(String),
321    /// `DESC(?var)`
322    Desc(String),
323}
324
325impl OrderDirection {
326    /// Render as an ORDER BY term, e.g. `ASC(?x)`.
327    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// ---------------------------------------------------------------------------
336// WhereClause
337// ---------------------------------------------------------------------------
338
339/// A single item inside a WHERE clause.
340#[derive(Debug, Clone)]
341pub enum WhereClauseItem {
342    /// A triple pattern.
343    Triple(BuilderTriplePattern),
344    /// A FILTER clause.
345    Filter(SparqlFilter),
346    /// An OPTIONAL block.
347    Optional(WhereClause),
348    /// A UNION of two sub-patterns.
349    Union(WhereClause, WhereClause),
350    /// A BIND assignment: `BIND (<expr> AS ?<var>)`.
351    Bind(String, String),
352    /// A VALUES inline data block: `VALUES ?var { val1 val2 … }`.
353    Values(String, Vec<SparqlTerm>),
354    /// A multi-variable VALUES inline table: `VALUES (?v1 ?v2 …) { (t1 t2) … }`.
355    ValuesMulti(Vec<String>, Vec<Vec<SparqlTerm>>),
356}
357
358/// A SPARQL WHERE clause composed of triple patterns, filters, optional
359/// blocks, unions, binds, and inline VALUES blocks.
360#[derive(Debug, Clone, Default)]
361pub struct WhereClause {
362    patterns: Vec<WhereClauseItem>,
363}
364
365impl WhereClause {
366    /// Create an empty WHERE clause.
367    pub fn new() -> Self {
368        WhereClause::default()
369    }
370
371    /// Append a triple pattern.
372    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    /// Append a FILTER.
379    pub fn filter(mut self, f: SparqlFilter) -> Self {
380        self.patterns.push(WhereClauseItem::Filter(f));
381        self
382    }
383
384    /// Append an OPTIONAL block.
385    pub fn optional(mut self, inner: WhereClause) -> Self {
386        self.patterns.push(WhereClauseItem::Optional(inner));
387        self
388    }
389
390    /// Append a UNION of two sub-clauses.
391    pub fn union(mut self, left: WhereClause, right: WhereClause) -> Self {
392        self.patterns.push(WhereClauseItem::Union(left, right));
393        self
394    }
395
396    /// Append a BIND assignment.
397    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    /// Append an inline VALUES block.
404    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    /// Append a multi-variable inline VALUES table.
411    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    /// Returns `true` when the clause has no items.
422    pub fn is_empty(&self) -> bool {
423        self.patterns.is_empty()
424    }
425
426    /// Number of top-level items in this clause.
427    pub fn pattern_count(&self) -> usize {
428        self.patterns.len()
429    }
430
431    /// Render the full WHERE block with `{` / `}` and 2-space indentation.
432    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    /// Render only the inner content (without the outer `WHERE { … }` wrapper),
442    /// used for nested clauses like OPTIONAL and UNION.
443    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
454/// Push `depth * 2` spaces of indentation into `buf`.
455fn push_indent(buf: &mut String, depth: usize) {
456    for _ in 0..depth * 2 {
457        buf.push(' ');
458    }
459}
460
461/// Render a single [`WhereClauseItem`] into `buf` at the given indentation
462/// depth.
463fn 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// ---------------------------------------------------------------------------
517// SelectQuery
518// ---------------------------------------------------------------------------
519
520/// A fluent builder for SPARQL SELECT queries.
521#[derive(Debug, Clone)]
522pub struct SelectQuery {
523    /// PREFIX declarations: `(prefix, iri)`.
524    pub prefixes: Vec<(String, String)>,
525    /// Variables to project.  Empty means `SELECT *`.
526    pub projection: Vec<String>,
527    /// Whether to apply `DISTINCT`.
528    pub distinct: bool,
529    /// Whether to apply `REDUCED`.
530    pub reduced: bool,
531    /// The WHERE clause.
532    pub where_clause: WhereClause,
533    /// ORDER BY terms.
534    pub order_by: Vec<OrderDirection>,
535    /// GROUP BY variables.
536    pub group_by: Vec<String>,
537    /// HAVING filter (only meaningful when GROUP BY is set).
538    pub having: Option<SparqlFilter>,
539    /// LIMIT value.
540    pub limit: Option<usize>,
541    /// OFFSET value.
542    pub offset: Option<usize>,
543}
544
545impl SelectQuery {
546    /// Create a new, empty SELECT query builder.
547    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    /// Add a PREFIX declaration.
563    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    /// Add a variable to the SELECT projection.
569    pub fn select(mut self, var: impl Into<String>) -> Self {
570        self.projection.push(var.into());
571        self
572    }
573
574    /// Set projection to `SELECT *`.
575    pub fn select_all(mut self) -> Self {
576        self.projection.clear();
577        self
578    }
579
580    /// Add `DISTINCT` to the query.
581    pub fn distinct(mut self) -> Self {
582        self.distinct = true;
583        self
584    }
585
586    /// Add `REDUCED` to the query.
587    pub fn reduced(mut self) -> Self {
588        self.reduced = true;
589        self
590    }
591
592    /// Set the WHERE clause.
593    pub fn where_clause(mut self, clause: WhereClause) -> Self {
594        self.where_clause = clause;
595        self
596    }
597
598    /// Add an `ORDER BY ASC(?var)` term.
599    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    /// Add an `ORDER BY DESC(?var)` term.
605    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    /// Add a GROUP BY variable.
611    pub fn group_by(mut self, var: impl Into<String>) -> Self {
612        self.group_by.push(var.into());
613        self
614    }
615
616    /// Set the HAVING filter.
617    pub fn having(mut self, filter: SparqlFilter) -> Self {
618        self.having = Some(filter);
619        self
620    }
621
622    /// Set the LIMIT.
623    pub fn limit(mut self, n: usize) -> Self {
624        self.limit = Some(n);
625        self
626    }
627
628    /// Set the OFFSET.
629    pub fn offset(mut self, n: usize) -> Self {
630        self.offset = Some(n);
631        self
632    }
633
634    /// Build the SPARQL query string, performing validation first.
635    pub fn build(&self) -> Result<String, SparqlBuilderError> {
636        // Validate: at least one pattern in WHERE
637        if self.where_clause.is_empty() {
638            return Err(SparqlBuilderError::EmptyWhereClause);
639        }
640
641        // Validate: DISTINCT and REDUCED are mutually exclusive
642        if self.distinct && self.reduced {
643            return Err(SparqlBuilderError::ConflictingModifiers(
644                "DISTINCT and REDUCED cannot both be set".to_string(),
645            ));
646        }
647
648        // Validate variable names in projection
649        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    /// Build the SPARQL query string without validation.
659    pub fn build_unchecked(&self) -> String {
660        let mut out = String::new();
661
662        // PREFIX declarations
663        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        // SELECT [DISTINCT|REDUCED] [vars | *]
677        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        // WHERE { … }
694        out.push_str(&self.where_clause.to_sparql());
695        out.push('\n');
696
697        // GROUP BY
698        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        // HAVING
704        if let Some(having) = &self.having {
705            out.push_str(&format!("HAVING ({})\n", having.expression));
706        }
707
708        // ORDER BY
709        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        // LIMIT
715        if let Some(limit) = self.limit {
716            out.push_str(&format!("LIMIT {limit}\n"));
717        }
718
719        // OFFSET
720        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// ---------------------------------------------------------------------------
735// AskQuery
736// ---------------------------------------------------------------------------
737
738/// A fluent builder for SPARQL ASK queries.
739#[derive(Debug, Clone)]
740pub struct AskQuery {
741    /// PREFIX declarations.
742    pub prefixes: Vec<(String, String)>,
743    /// The WHERE clause.
744    pub where_clause: WhereClause,
745}
746
747impl AskQuery {
748    /// Create a new, empty ASK query builder.
749    pub fn new() -> Self {
750        AskQuery {
751            prefixes: Vec::new(),
752            where_clause: WhereClause::new(),
753        }
754    }
755
756    /// Add a PREFIX declaration.
757    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    /// Set the WHERE clause.
763    pub fn where_clause(mut self, clause: WhereClause) -> Self {
764        self.where_clause = clause;
765        self
766    }
767
768    /// Build the SPARQL ASK query string.
769    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// ---------------------------------------------------------------------------
799// Tests
800// ---------------------------------------------------------------------------
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805
806    // --- SparqlTerm rendering -----------------------------------------------
807
808    #[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        // Bare IRI gets wrapped in angle brackets.
816        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        // Already-wrapped IRI is used as-is.
825        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    // --- TriplePattern ------------------------------------------------------
863
864    #[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    // --- SparqlFilter -------------------------------------------------------
875
876    #[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    // --- WhereClause --------------------------------------------------------
925
926    #[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    // --- SelectQuery --------------------------------------------------------
1004
1005    #[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    // --- AskQuery -----------------------------------------------------------
1138
1139    #[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    // --- validate_variable_name ---------------------------------------------
1166
1167    #[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    // --- OrderDirection -----------------------------------------------------
1183
1184    #[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    // --- SparqlBuilderError Display -----------------------------------------
1198
1199    #[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}