Skip to main content

oxirs_arq/algebra/
mod.rs

1//! SPARQL Algebra Module
2//!
3//! This module provides the core algebraic representation of SPARQL queries,
4//! including basic graph patterns, joins, unions, filters, and other operations.
5
6use oxirs_core::model::{
7    BlankNode as CoreBlankNode, Literal as CoreLiteral, NamedNode, Object, Predicate, Subject,
8};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fmt;
12
13/// Variable identifier - reuse from core
14pub use oxirs_core::model::Variable;
15
16/// IRI (Internationalized Resource Identifier) - use NamedNode from core
17pub type Iri = NamedNode;
18
19/// Literal value - create a bridge type that can convert to/from core literal
20#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
21pub struct Literal {
22    pub value: String,
23    pub language: Option<String>,
24    pub datatype: Option<NamedNode>,
25}
26
27impl fmt::Display for Literal {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        write!(f, "\"{}\"", self.value)?;
30        if let Some(lang) = &self.language {
31            write!(f, "@{lang}")?;
32        } else if let Some(dt) = &self.datatype {
33            write!(f, "^^{dt}")?;
34        }
35        Ok(())
36    }
37}
38
39impl Literal {
40    /// Create a language-tagged literal
41    pub fn with_language(value: String, language: String) -> Self {
42        Self {
43            value,
44            language: Some(language),
45            datatype: None,
46        }
47    }
48}
49
50impl From<CoreLiteral> for Literal {
51    fn from(core_literal: CoreLiteral) -> Self {
52        let (value, datatype, language) = core_literal.destruct();
53        Self {
54            value,
55            language,
56            datatype,
57        }
58    }
59}
60
61impl From<Literal> for CoreLiteral {
62    fn from(literal: Literal) -> Self {
63        if let Some(lang) = literal.language {
64            CoreLiteral::new_language_tagged_literal(&literal.value, lang)
65                .unwrap_or_else(|_| CoreLiteral::new_simple_literal(literal.value))
66        } else if let Some(datatype) = literal.datatype {
67            CoreLiteral::new_typed_literal(literal.value, datatype)
68        } else {
69            CoreLiteral::new_simple_literal(literal.value)
70        }
71    }
72}
73
74/// RDF term (subject, predicate, or object) - bridge with core types
75#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
76pub enum Term {
77    Variable(Variable),
78    Iri(NamedNode),
79    Literal(Literal),
80    BlankNode(String),
81    QuotedTriple(Box<TriplePattern>),
82    PropertyPath(PropertyPath),
83}
84
85impl fmt::Display for Term {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            Term::Variable(v) => write!(f, "?{v}"),
89            Term::Iri(iri) => write!(f, "{iri}"),
90            Term::Literal(lit) => write!(f, "{lit}"),
91            Term::BlankNode(id) => write!(f, "_:{id}"),
92            Term::QuotedTriple(triple) => write!(
93                f,
94                "<<{} {} {}>>",
95                triple.subject, triple.predicate, triple.object
96            ),
97            Term::PropertyPath(path) => write!(f, "{path}"),
98        }
99    }
100}
101
102impl From<Subject> for Term {
103    fn from(subject: Subject) -> Self {
104        match subject {
105            Subject::NamedNode(n) => Term::Iri(n),
106            Subject::BlankNode(b) => Term::BlankNode(b.id().to_string()),
107            Subject::Variable(v) => Term::Variable(v),
108            Subject::QuotedTriple(quoted_triple) => {
109                // Implement proper quoted triple support for RDF-star
110                Term::QuotedTriple(Box::new(TriplePattern {
111                    subject: Term::from(quoted_triple.subject().clone()),
112                    predicate: Term::from(quoted_triple.predicate().clone()),
113                    object: Term::from(quoted_triple.object().clone()),
114                }))
115            }
116        }
117    }
118}
119
120impl From<Predicate> for Term {
121    fn from(predicate: Predicate) -> Self {
122        match predicate {
123            Predicate::NamedNode(n) => Term::Iri(n),
124            Predicate::Variable(v) => Term::Variable(v),
125        }
126    }
127}
128
129impl From<Object> for Term {
130    fn from(object: Object) -> Self {
131        match object {
132            Object::NamedNode(n) => Term::Iri(n),
133            Object::BlankNode(b) => Term::BlankNode(b.id().to_string()),
134            Object::Literal(l) => Term::Literal(l.into()),
135            Object::Variable(v) => Term::Variable(v),
136            Object::QuotedTriple(quoted_triple) => {
137                // Implement proper quoted triple support for RDF-star
138                Term::QuotedTriple(Box::new(TriplePattern {
139                    subject: Term::from(quoted_triple.subject().clone()),
140                    predicate: Term::from(quoted_triple.predicate().clone()),
141                    object: Term::from(quoted_triple.object().clone()),
142                }))
143            }
144        }
145    }
146}
147
148impl From<NamedNode> for Term {
149    fn from(node: NamedNode) -> Self {
150        Term::Iri(node)
151    }
152}
153
154impl From<CoreBlankNode> for Term {
155    fn from(node: CoreBlankNode) -> Self {
156        Term::BlankNode(node.id().to_string())
157    }
158}
159
160impl From<CoreLiteral> for Term {
161    fn from(literal: CoreLiteral) -> Self {
162        Term::Literal(literal.into())
163    }
164}
165
166impl From<Variable> for Term {
167    fn from(variable: Variable) -> Self {
168        Term::Variable(variable)
169    }
170}
171
172/// Triple pattern
173#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
174pub struct TriplePattern {
175    pub subject: Term,
176    pub predicate: Term,
177    pub object: Term,
178}
179
180/// Type alias for triple patterns used as ground triples (same structure)
181pub type Triple = TriplePattern;
182
183impl fmt::Display for TriplePattern {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        write!(f, "{} {} {}", self.subject, self.predicate, self.object)
186    }
187}
188
189/// SPARQL expression
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub enum Expression {
192    /// Variable reference
193    Variable(Variable),
194    /// Literal value
195    Literal(Literal),
196    /// IRI reference
197    Iri(Iri),
198    /// Function call
199    Function { name: String, args: Vec<Expression> },
200    /// Binary operation
201    Binary {
202        op: BinaryOperator,
203        left: Box<Expression>,
204        right: Box<Expression>,
205    },
206    /// Unary operation
207    Unary {
208        op: UnaryOperator,
209        operand: Box<Expression>,
210    },
211    /// Conditional expression (IF)
212    Conditional {
213        condition: Box<Expression>,
214        then_expr: Box<Expression>,
215        else_expr: Box<Expression>,
216    },
217    /// Bound variable check
218    Bound(Variable),
219    /// Exists clause
220    Exists(Box<Algebra>),
221    /// Not exists clause
222    NotExists(Box<Algebra>),
223}
224
225/// Binary operators
226#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
227pub enum BinaryOperator {
228    Add,
229    Subtract,
230    Multiply,
231    Divide,
232    Equal,
233    NotEqual,
234    Less,
235    LessEqual,
236    Greater,
237    GreaterEqual,
238    And,
239    Or,
240    SameTerm,
241    In,
242    NotIn,
243}
244
245impl std::fmt::Display for BinaryOperator {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        match self {
248            BinaryOperator::Add => write!(f, "+"),
249            BinaryOperator::Subtract => write!(f, "-"),
250            BinaryOperator::Multiply => write!(f, "*"),
251            BinaryOperator::Divide => write!(f, "/"),
252            BinaryOperator::Equal => write!(f, "="),
253            BinaryOperator::NotEqual => write!(f, "!="),
254            BinaryOperator::Less => write!(f, "<"),
255            BinaryOperator::LessEqual => write!(f, "<="),
256            BinaryOperator::Greater => write!(f, ">"),
257            BinaryOperator::GreaterEqual => write!(f, ">="),
258            BinaryOperator::And => write!(f, "&&"),
259            BinaryOperator::Or => write!(f, "||"),
260            BinaryOperator::SameTerm => write!(f, "sameTerm"),
261            BinaryOperator::In => write!(f, "IN"),
262            BinaryOperator::NotIn => write!(f, "NOT IN"),
263        }
264    }
265}
266
267/// Unary operators
268#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
269pub enum UnaryOperator {
270    Not,
271    Plus,
272    Minus,
273    IsIri,
274    IsBlank,
275    IsLiteral,
276    IsNumeric,
277}
278
279/// Aggregate function
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub enum Aggregate {
282    Count {
283        distinct: bool,
284        expr: Option<Expression>,
285    },
286    Sum {
287        distinct: bool,
288        expr: Expression,
289    },
290    Min {
291        distinct: bool,
292        expr: Expression,
293    },
294    Max {
295        distinct: bool,
296        expr: Expression,
297    },
298    Avg {
299        distinct: bool,
300        expr: Expression,
301    },
302    Sample {
303        distinct: bool,
304        expr: Expression,
305    },
306    GroupConcat {
307        distinct: bool,
308        expr: Expression,
309        separator: Option<String>,
310    },
311}
312
313/// Canonicalize a function name to a SPARQL set-aggregate name, or `None` if it
314/// is not an aggregate.
315///
316/// The parser encodes an unprefixed function name as `":NAME"`, so the leading
317/// colon is stripped before matching, and matching is case-insensitive. The
318/// returned value is the canonical uppercase spelling used in arity error
319/// messages (`COUNT`, `SUM`, `MIN`, `MAX`, `AVG`, `SAMPLE`, `GROUP_CONCAT`).
320///
321/// This is the single source of truth for aggregate-name recognition shared by
322/// the parse-time `HAVING` arity validator and the executor's defense-in-depth
323/// check, so both agree on exactly which function calls are aggregates.
324pub fn aggregate_function_name(name: &str) -> Option<&'static str> {
325    match name.trim_start_matches(':').to_ascii_uppercase().as_str() {
326        "COUNT" => Some("COUNT"),
327        "SUM" => Some("SUM"),
328        "MIN" => Some("MIN"),
329        "MAX" => Some("MAX"),
330        "AVG" => Some("AVG"),
331        "SAMPLE" => Some("SAMPLE"),
332        "GROUP_CONCAT" => Some("GROUP_CONCAT"),
333        _ => None,
334    }
335}
336
337/// Validate the argument count of an aggregate function call as used in a
338/// `HAVING` condition.
339///
340/// `COUNT` accepts 0 or 1 argument; every other aggregate requires exactly one.
341/// A non-aggregate function name is not this helper's concern and returns
342/// `Ok(())`. The `Err` strings are the exact texts the executor's
343/// `function_to_aggregate` (and its tests) rely on, so the parser and the
344/// executor reject the same malformed queries with identical messages.
345pub fn check_aggregate_arity(name: &str, arg_count: usize) -> Result<(), String> {
346    let Some(canonical) = aggregate_function_name(name) else {
347        return Ok(());
348    };
349    match canonical {
350        "COUNT" => {
351            if arg_count > 1 {
352                return Err("COUNT in HAVING expects at most one argument".to_string());
353            }
354        }
355        other => {
356            if arg_count != 1 {
357                return Err(format!("{other} in HAVING expects exactly one argument"));
358            }
359        }
360    }
361    Ok(())
362}
363
364/// Variable binding
365pub type Binding = HashMap<Variable, Term>;
366
367/// Solution sequence (set of bindings)
368pub type Solution = Vec<Binding>;
369
370/// Order condition
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct OrderCondition {
373    pub expr: Expression,
374    pub ascending: bool,
375}
376
377/// Group condition
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
379pub struct GroupCondition {
380    pub expr: Expression,
381    pub alias: Option<Variable>,
382}
383
384/// Property path expressions for advanced SPARQL 1.1 graph navigation
385#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
386pub enum PropertyPath {
387    /// Direct property IRI
388    Iri(Iri),
389    /// Variable property
390    Variable(Variable),
391    /// Inverse property path (^property)
392    Inverse(Box<PropertyPath>),
393    /// Sequence path (path1/path2)
394    Sequence(Box<PropertyPath>, Box<PropertyPath>),
395    /// Alternative path (path1|path2)
396    Alternative(Box<PropertyPath>, Box<PropertyPath>),
397    /// Zero or more (path*)
398    ZeroOrMore(Box<PropertyPath>),
399    /// One or more (path+)
400    OneOrMore(Box<PropertyPath>),
401    /// Zero or one (path?)
402    ZeroOrOne(Box<PropertyPath>),
403    /// Negated property set (!property)
404    NegatedPropertySet(Vec<PropertyPath>),
405}
406
407/// Property path triple pattern
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409pub struct PropertyPathPattern {
410    pub subject: Term,
411    pub path: PropertyPath,
412    pub object: Term,
413}
414
415/// SPARQL algebra expressions
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417pub enum Algebra {
418    /// Basic Graph Pattern
419    Bgp(Vec<TriplePattern>),
420
421    /// Property Path Pattern
422    PropertyPath {
423        subject: Term,
424        path: PropertyPath,
425        object: Term,
426    },
427
428    /// Join two patterns
429    Join {
430        left: Box<Algebra>,
431        right: Box<Algebra>,
432    },
433
434    /// Left join (OPTIONAL)
435    LeftJoin {
436        left: Box<Algebra>,
437        right: Box<Algebra>,
438        filter: Option<Expression>,
439    },
440
441    /// Union of patterns
442    Union {
443        left: Box<Algebra>,
444        right: Box<Algebra>,
445    },
446
447    /// Filter pattern
448    Filter {
449        pattern: Box<Algebra>,
450        condition: Expression,
451    },
452
453    /// Extend pattern (BIND)
454    Extend {
455        pattern: Box<Algebra>,
456        variable: Variable,
457        expr: Expression,
458    },
459
460    /// Minus pattern
461    Minus {
462        left: Box<Algebra>,
463        right: Box<Algebra>,
464    },
465
466    /// Service pattern (federation)
467    Service {
468        endpoint: Term,
469        pattern: Box<Algebra>,
470        silent: bool,
471    },
472
473    /// Graph pattern
474    Graph { graph: Term, pattern: Box<Algebra> },
475
476    /// Projection
477    Project {
478        pattern: Box<Algebra>,
479        variables: Vec<Variable>,
480    },
481
482    /// Distinct
483    Distinct { pattern: Box<Algebra> },
484
485    /// Reduced
486    Reduced { pattern: Box<Algebra> },
487
488    /// Slice (LIMIT/OFFSET)
489    Slice {
490        pattern: Box<Algebra>,
491        offset: Option<usize>,
492        limit: Option<usize>,
493    },
494
495    /// Order by
496    OrderBy {
497        pattern: Box<Algebra>,
498        conditions: Vec<OrderCondition>,
499    },
500
501    /// Group by
502    Group {
503        pattern: Box<Algebra>,
504        variables: Vec<GroupCondition>,
505        aggregates: Vec<(Variable, Aggregate)>,
506    },
507
508    /// Having
509    Having {
510        pattern: Box<Algebra>,
511        condition: Expression,
512    },
513
514    /// Values clause
515    Values {
516        variables: Vec<Variable>,
517        bindings: Vec<Binding>,
518    },
519
520    /// Table (empty result)
521    Table,
522
523    /// Zero matches
524    Zero,
525
526    /// Empty result set
527    Empty,
528}
529
530/// Join algorithm hints
531#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
532pub enum JoinAlgorithm {
533    #[default]
534    HashJoin,
535    SortMergeJoin,
536    NestedLoopJoin,
537    IndexNestedLoopJoin,
538    BindJoin,
539}
540
541/// Filter placement hints
542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
543pub enum FilterPlacement {
544    Early, // Push down as much as possible
545    Late,  // Keep at current level
546    #[default]
547    Optimal, // Let optimizer decide
548}
549
550/// Service capabilities
551#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
552pub struct ServiceCapabilities {
553    pub supports_projection: bool,
554    pub supports_filtering: bool,
555    pub supports_ordering: bool,
556    pub supports_aggregation: bool,
557    pub max_query_size: Option<usize>,
558}
559
560/// Projection types
561#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
562pub enum ProjectionType {
563    #[default]
564    Standard,
565    Streaming,
566    Cached,
567}
568
569/// Sort algorithms
570#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
571pub enum SortAlgorithm {
572    #[default]
573    QuickSort,
574    MergeSort,
575    HeapSort,
576    ExternalSort,
577}
578
579/// Grouping algorithms
580#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
581pub enum GroupingAlgorithm {
582    #[default]
583    HashGrouping,
584    SortGrouping,
585    StreamingGrouping,
586}
587
588/// Materialization strategies
589#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
590pub enum MaterializationStrategy {
591    InMemory,
592    Disk,
593    #[default]
594    Adaptive,
595}
596
597/// Parallelism types
598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
599pub enum ParallelismType {
600    #[default]
601    DataParallel,
602    PipelineParallel,
603    Hybrid,
604}
605
606/// Re-export IndexType from optimizer module
607pub use crate::optimizer::index_types::IndexType;
608
609/// Statistics for cost-based optimization
610#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
611pub struct Statistics {
612    /// Estimated number of triples/results
613    pub cardinality: u64,
614    /// Selectivity factor (0.0 to 1.0)
615    pub selectivity: f64,
616    /// Index availability
617    pub available_indexes: Vec<IndexType>,
618    /// Approximate cost (arbitrary units)
619    pub cost: f64,
620    /// Memory requirement estimate (bytes)
621    pub memory_estimate: u64,
622    /// IO operations estimate
623    pub io_estimate: u64,
624}
625
626/// Optimization hints for algebra nodes
627#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
628pub struct OptimizationHints {
629    /// Preferred join algorithm
630    pub join_algorithm: Option<JoinAlgorithm>,
631    /// Filter placement strategy
632    pub filter_placement: FilterPlacement,
633    /// Materialization strategy
634    pub materialization: MaterializationStrategy,
635    /// Parallelism recommendations
636    pub parallelism: Option<ParallelismType>,
637    /// Index hints
638    pub preferred_indexes: Vec<IndexType>,
639    /// Cost estimates
640    pub statistics: Option<Statistics>,
641}
642
643/// Enhanced algebra with optimization annotations
644#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
645pub struct AnnotatedAlgebra {
646    /// The core algebra expression
647    pub algebra: Algebra,
648    /// Optimization hints
649    pub hints: OptimizationHints,
650    /// Execution context
651    pub context: Option<String>,
652}
653
654impl PropertyPath {
655    /// Create a direct property path
656    pub fn iri(iri: Iri) -> Self {
657        PropertyPath::Iri(iri)
658    }
659
660    /// Create an inverse property path
661    pub fn inverse(path: PropertyPath) -> Self {
662        PropertyPath::Inverse(Box::new(path))
663    }
664
665    /// Create a sequence property path
666    pub fn sequence(left: PropertyPath, right: PropertyPath) -> Self {
667        PropertyPath::Sequence(Box::new(left), Box::new(right))
668    }
669
670    /// Create an alternative property path
671    pub fn alternative(left: PropertyPath, right: PropertyPath) -> Self {
672        PropertyPath::Alternative(Box::new(left), Box::new(right))
673    }
674
675    /// Create a zero-or-more property path
676    pub fn zero_or_more(path: PropertyPath) -> Self {
677        PropertyPath::ZeroOrMore(Box::new(path))
678    }
679
680    /// Create a one-or-more property path
681    pub fn one_or_more(path: PropertyPath) -> Self {
682        PropertyPath::OneOrMore(Box::new(path))
683    }
684
685    /// Create a zero-or-one property path
686    pub fn zero_or_one(path: PropertyPath) -> Self {
687        PropertyPath::ZeroOrOne(Box::new(path))
688    }
689
690    /// Check if path is simple (direct property)
691    pub fn is_simple(&self) -> bool {
692        matches!(self, PropertyPath::Iri(_) | PropertyPath::Variable(_))
693    }
694
695    /// Get all variables mentioned in this property path
696    pub fn variables(&self) -> Vec<Variable> {
697        let mut vars = Vec::new();
698        self.collect_variables(&mut vars);
699        vars.sort();
700        vars.dedup();
701        vars
702    }
703
704    fn collect_variables(&self, vars: &mut Vec<Variable>) {
705        match self {
706            PropertyPath::Variable(var) => vars.push(var.clone()),
707            PropertyPath::Inverse(path) => path.collect_variables(vars),
708            PropertyPath::Sequence(left, right) | PropertyPath::Alternative(left, right) => {
709                left.collect_variables(vars);
710                right.collect_variables(vars);
711            }
712            PropertyPath::ZeroOrMore(path)
713            | PropertyPath::OneOrMore(path)
714            | PropertyPath::ZeroOrOne(path) => path.collect_variables(vars),
715            PropertyPath::NegatedPropertySet(paths) => {
716                for path in paths {
717                    path.collect_variables(vars);
718                }
719            }
720            PropertyPath::Iri(_) => {}
721        }
722    }
723
724    /// Estimate complexity of property path evaluation
725    pub fn complexity(&self) -> usize {
726        match self {
727            PropertyPath::Iri(_) | PropertyPath::Variable(_) => 1,
728            PropertyPath::Inverse(path) => path.complexity() + 10,
729            PropertyPath::Sequence(left, right) => left.complexity() + right.complexity() + 20,
730            PropertyPath::Alternative(left, right) => {
731                std::cmp::max(left.complexity(), right.complexity()) + 15
732            }
733            PropertyPath::ZeroOrMore(_) | PropertyPath::OneOrMore(_) => 1000, // High complexity
734            PropertyPath::ZeroOrOne(path) => path.complexity() + 5,
735            PropertyPath::NegatedPropertySet(paths) => {
736                paths.iter().map(|p| p.complexity()).sum::<usize>() + 50
737            }
738        }
739    }
740}
741
742impl PropertyPathPattern {
743    /// Create a new property path pattern
744    pub fn new(subject: Term, path: PropertyPath, object: Term) -> Self {
745        PropertyPathPattern {
746            subject,
747            path,
748            object,
749        }
750    }
751
752    /// Get all variables mentioned in this pattern
753    pub fn variables(&self) -> Vec<Variable> {
754        let mut vars = Vec::new();
755        self.collect_variables(&mut vars);
756        vars.sort();
757        vars.dedup();
758        vars
759    }
760
761    fn collect_variables(&self, vars: &mut Vec<Variable>) {
762        self.subject.collect_variables(vars);
763        self.path.collect_variables(vars);
764        self.object.collect_variables(vars);
765    }
766}
767
768impl fmt::Display for PropertyPath {
769    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
770        match self {
771            PropertyPath::Iri(iri) => write!(f, "{iri}"),
772            PropertyPath::Variable(var) => write!(f, "?{var}"),
773            PropertyPath::Inverse(path) => write!(f, "^{path}"),
774            PropertyPath::Sequence(left, right) => write!(f, "{left}/{right}"),
775            PropertyPath::Alternative(left, right) => write!(f, "{left}|{right}"),
776            PropertyPath::ZeroOrMore(path) => write!(f, "{path}*"),
777            PropertyPath::OneOrMore(path) => write!(f, "{path} +"),
778            PropertyPath::ZeroOrOne(path) => write!(f, "{path}?"),
779            PropertyPath::NegatedPropertySet(paths) => {
780                write!(f, "!(")?;
781                for (i, path) in paths.iter().enumerate() {
782                    if i > 0 {
783                        write!(f, "|")?
784                    }
785                    write!(f, "{path}")?;
786                }
787                write!(f, ")")
788            }
789        }
790    }
791}
792
793impl fmt::Display for PropertyPathPattern {
794    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795        write!(f, "{} {} {}", self.subject, self.path, self.object)
796    }
797}
798
799impl Algebra {
800    /// Create a new BGP from triple patterns
801    pub fn bgp(patterns: Vec<TriplePattern>) -> Self {
802        Algebra::Bgp(patterns)
803    }
804
805    /// Create a property path algebra node
806    pub fn property_path(subject: Term, path: PropertyPath, object: Term) -> Self {
807        Algebra::PropertyPath {
808            subject,
809            path,
810            object,
811        }
812    }
813
814    /// Create a join of two patterns
815    pub fn join(left: Algebra, right: Algebra) -> Self {
816        Algebra::Join {
817            left: Box::new(left),
818            right: Box::new(right),
819        }
820    }
821
822    /// Create a left join (optional)
823    pub fn left_join(left: Algebra, right: Algebra, filter: Option<Expression>) -> Self {
824        Algebra::LeftJoin {
825            left: Box::new(left),
826            right: Box::new(right),
827            filter,
828        }
829    }
830
831    /// Create a union of two patterns
832    pub fn union(left: Algebra, right: Algebra) -> Self {
833        Algebra::Union {
834            left: Box::new(left),
835            right: Box::new(right),
836        }
837    }
838
839    /// Create a filter pattern
840    pub fn filter(pattern: Algebra, condition: Expression) -> Self {
841        Algebra::Filter {
842            pattern: Box::new(pattern),
843            condition,
844        }
845    }
846
847    /// Create an extend pattern (BIND)
848    pub fn extend(pattern: Algebra, variable: Variable, expr: Expression) -> Self {
849        Algebra::Extend {
850            pattern: Box::new(pattern),
851            variable,
852            expr,
853        }
854    }
855
856    /// Create a projection
857    pub fn project(pattern: Algebra, variables: Vec<Variable>) -> Self {
858        Algebra::Project {
859            pattern: Box::new(pattern),
860            variables,
861        }
862    }
863
864    /// Create a slice (LIMIT/OFFSET)
865    pub fn slice(pattern: Algebra, offset: Option<usize>, limit: Option<usize>) -> Self {
866        Algebra::Slice {
867            pattern: Box::new(pattern),
868            offset,
869            limit,
870        }
871    }
872
873    /// Get all variables mentioned in this algebra expression
874    pub fn variables(&self) -> Vec<Variable> {
875        let mut vars = Vec::new();
876        self.collect_variables(&mut vars);
877        vars.sort();
878        vars.dedup();
879        vars
880    }
881
882    fn collect_variables(&self, vars: &mut Vec<Variable>) {
883        match self {
884            Algebra::Bgp(patterns) => {
885                for pattern in patterns {
886                    pattern.collect_variables(vars);
887                }
888            }
889            Algebra::PropertyPath {
890                subject,
891                path,
892                object,
893            } => {
894                subject.collect_variables(vars);
895                path.collect_variables(vars);
896                object.collect_variables(vars);
897            }
898            Algebra::Join { left, right }
899            | Algebra::Union { left, right }
900            | Algebra::Minus { left, right } => {
901                left.collect_variables(vars);
902                right.collect_variables(vars);
903            }
904            Algebra::LeftJoin {
905                left,
906                right,
907                filter,
908            } => {
909                left.collect_variables(vars);
910                right.collect_variables(vars);
911                if let Some(filter) = filter {
912                    filter.collect_variables(vars);
913                }
914            }
915            Algebra::Filter { pattern, condition } => {
916                pattern.collect_variables(vars);
917                condition.collect_variables(vars);
918            }
919            Algebra::Extend {
920                pattern,
921                variable,
922                expr,
923            } => {
924                pattern.collect_variables(vars);
925                vars.push(variable.clone());
926                expr.collect_variables(vars);
927            }
928            Algebra::Service { pattern, .. } => {
929                pattern.collect_variables(vars);
930            }
931            Algebra::Graph { pattern, .. } => {
932                pattern.collect_variables(vars);
933            }
934            Algebra::Project { pattern, variables } => {
935                pattern.collect_variables(vars);
936                vars.extend(variables.clone());
937            }
938            Algebra::Distinct { pattern }
939            | Algebra::Reduced { pattern }
940            | Algebra::Slice { pattern, .. } => {
941                pattern.collect_variables(vars);
942            }
943            Algebra::OrderBy {
944                pattern,
945                conditions,
946            } => {
947                pattern.collect_variables(vars);
948                for condition in conditions {
949                    condition.expr.collect_variables(vars);
950                }
951            }
952            Algebra::Group {
953                pattern,
954                variables: group_vars,
955                aggregates,
956            } => {
957                pattern.collect_variables(vars);
958                for group_var in group_vars {
959                    group_var.expr.collect_variables(vars);
960                    if let Some(alias) = &group_var.alias {
961                        vars.push(alias.clone());
962                    }
963                }
964                for (var, aggregate) in aggregates {
965                    vars.push(var.clone());
966                    aggregate.collect_variables(vars);
967                }
968            }
969            Algebra::Having { pattern, condition } => {
970                pattern.collect_variables(vars);
971                condition.collect_variables(vars);
972            }
973            Algebra::Values { variables, .. } => {
974                vars.extend(variables.clone());
975            }
976            Algebra::Table | Algebra::Zero | Algebra::Empty => {}
977        }
978    }
979}
980
981impl TriplePattern {
982    /// Create a new triple pattern
983    pub fn new(subject: Term, predicate: Term, object: Term) -> Self {
984        TriplePattern {
985            subject,
986            predicate,
987            object,
988        }
989    }
990
991    fn collect_variables(&self, vars: &mut Vec<Variable>) {
992        self.subject.collect_variables(vars);
993        self.predicate.collect_variables(vars);
994        self.object.collect_variables(vars);
995    }
996
997    /// Returns all variables in this triple pattern
998    pub fn variables(&self) -> Vec<Variable> {
999        let mut vars = Vec::new();
1000        self.collect_variables(&mut vars);
1001        vars
1002    }
1003}
1004
1005impl Term {
1006    fn collect_variables(&self, vars: &mut Vec<Variable>) {
1007        if let Term::Variable(var) = self {
1008            vars.push(var.clone());
1009        }
1010    }
1011}
1012
1013impl Expression {
1014    fn collect_variables(&self, vars: &mut Vec<Variable>) {
1015        match self {
1016            Expression::Variable(var) => vars.push(var.clone()),
1017            Expression::Function { args, .. } => {
1018                for arg in args {
1019                    arg.collect_variables(vars);
1020                }
1021            }
1022            Expression::Binary { left, right, .. } => {
1023                left.collect_variables(vars);
1024                right.collect_variables(vars);
1025            }
1026            Expression::Unary { operand, .. } => {
1027                operand.collect_variables(vars);
1028            }
1029            Expression::Conditional {
1030                condition,
1031                then_expr,
1032                else_expr,
1033            } => {
1034                condition.collect_variables(vars);
1035                then_expr.collect_variables(vars);
1036                else_expr.collect_variables(vars);
1037            }
1038            Expression::Bound(var) => vars.push(var.clone()),
1039            Expression::Exists(algebra) | Expression::NotExists(algebra) => {
1040                algebra.collect_variables(vars);
1041            }
1042            Expression::Literal(_) | Expression::Iri(_) => {}
1043        }
1044    }
1045}
1046
1047impl Aggregate {
1048    fn collect_variables(&self, vars: &mut Vec<Variable>) {
1049        match self {
1050            Aggregate::Count {
1051                expr: Some(expr), ..
1052            }
1053            | Aggregate::Sum { expr, .. }
1054            | Aggregate::Min { expr, .. }
1055            | Aggregate::Max { expr, .. }
1056            | Aggregate::Avg { expr, .. }
1057            | Aggregate::Sample { expr, .. }
1058            | Aggregate::GroupConcat { expr, .. } => {
1059                expr.collect_variables(vars);
1060            }
1061            Aggregate::Count { expr: None, .. } => {}
1062        }
1063    }
1064}
1065
1066/// Convenience macros for building algebra expressions
1067#[macro_export]
1068macro_rules! triple {
1069    ($s:expr_2021, $p:expr_2021, $o:expr_2021) => {
1070        TriplePattern::new($s, $p, $o)
1071    };
1072}
1073
1074#[macro_export]
1075macro_rules! var {
1076    ($name:expr_2021) => {
1077        Term::Variable($name.to_string())
1078    };
1079}
1080
1081#[macro_export]
1082macro_rules! iri {
1083    ($iri:expr_2021) => {
1084        Term::Iri(NamedNode::new($iri).expect("macro argument should be valid IRI"))
1085    };
1086}
1087
1088#[macro_export]
1089macro_rules! literal {
1090    ($value:expr_2021) => {
1091        Term::Literal(Literal::new($value.to_string(), None, None))
1092    };
1093    ($value:expr_2021, lang: $lang:expr_2021) => {
1094        Term::Literal(Literal::new(
1095            $value.to_string(),
1096            Some($lang.to_string()),
1097            None,
1098        ))
1099    };
1100    ($value:expr_2021, datatype: $dt:expr_2021) => {
1101        Term::Literal(Literal::new(
1102            $value.to_string(),
1103            None,
1104            Some(NamedNode::new($dt).expect("macro datatype argument should be valid IRI")),
1105        ))
1106    };
1107}
1108
1109impl Default for ServiceCapabilities {
1110    fn default() -> Self {
1111        Self {
1112            supports_projection: true,
1113            supports_filtering: true,
1114            supports_ordering: false,
1115            supports_aggregation: false,
1116            max_query_size: None,
1117        }
1118    }
1119}
1120
1121impl Literal {
1122    /// Create a new literal with value only
1123    pub fn new(value: String, language: Option<String>, datatype: Option<Iri>) -> Self {
1124        Literal {
1125            value,
1126            language,
1127            datatype,
1128        }
1129    }
1130
1131    /// Create a simple string literal
1132    pub fn string(value: impl Into<String>) -> Self {
1133        Literal {
1134            value: value.into(),
1135            language: None,
1136            datatype: None,
1137        }
1138    }
1139
1140    /// Create a language-tagged literal
1141    pub fn lang_string(value: impl Into<String>, language: impl Into<String>) -> Self {
1142        Literal {
1143            value: value.into(),
1144            language: Some(language.into()),
1145            datatype: None,
1146        }
1147    }
1148
1149    /// Create a typed literal
1150    pub fn typed(value: impl Into<String>, datatype: Iri) -> Self {
1151        Literal {
1152            value: value.into(),
1153            language: None,
1154            datatype: Some(datatype),
1155        }
1156    }
1157
1158    /// Create an integer literal
1159    pub fn integer(value: i64) -> Self {
1160        Literal::typed(
1161            value.to_string(),
1162            NamedNode::new("http://www.w3.org/2001/XMLSchema#integer")
1163                .expect("XSD URI is well-formed and valid"),
1164        )
1165    }
1166
1167    /// Create a decimal literal
1168    pub fn decimal(value: f64) -> Self {
1169        Literal::typed(
1170            value.to_string(),
1171            NamedNode::new("http://www.w3.org/2001/XMLSchema#decimal")
1172                .expect("XSD URI is well-formed and valid"),
1173        )
1174    }
1175
1176    /// Create a boolean literal
1177    pub fn boolean(value: bool) -> Self {
1178        Literal::typed(
1179            value.to_string(),
1180            NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean")
1181                .expect("XSD URI is well-formed and valid"),
1182        )
1183    }
1184
1185    /// Create a date literal
1186    pub fn date(value: impl Into<String>) -> Self {
1187        Literal::typed(
1188            value.into(),
1189            NamedNode::new("http://www.w3.org/2001/XMLSchema#date")
1190                .expect("XSD URI is well-formed and valid"),
1191        )
1192    }
1193
1194    /// Create a datetime literal
1195    pub fn datetime(value: impl Into<String>) -> Self {
1196        Literal::typed(
1197            value.into(),
1198            NamedNode::new("http://www.w3.org/2001/XMLSchema#dateTime")
1199                .expect("XSD URI is well-formed and valid"),
1200        )
1201    }
1202
1203    /// Get the effective datatype (with default string type if none specified)
1204    pub fn effective_datatype(&self) -> Iri {
1205        if let Some(ref dt) = self.datatype {
1206            dt.clone()
1207        } else if self.language.is_some() {
1208            NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")
1209                .expect("RDF URI is well-formed and valid")
1210        } else {
1211            NamedNode::new("http://www.w3.org/2001/XMLSchema#string")
1212                .expect("XSD URI is well-formed and valid")
1213        }
1214    }
1215
1216    /// Check if this is a numeric literal
1217    pub fn is_numeric(&self) -> bool {
1218        if let Some(ref dt) = self.datatype {
1219            matches!(
1220                dt.as_str(),
1221                "http://www.w3.org/2001/XMLSchema#integer"
1222                    | "http://www.w3.org/2001/XMLSchema#decimal"
1223                    | "http://www.w3.org/2001/XMLSchema#float"
1224                    | "http://www.w3.org/2001/XMLSchema#double"
1225                    | "http://www.w3.org/2001/XMLSchema#long"
1226                    | "http://www.w3.org/2001/XMLSchema#int"
1227                    | "http://www.w3.org/2001/XMLSchema#short"
1228                    | "http://www.w3.org/2001/XMLSchema#byte"
1229                    | "http://www.w3.org/2001/XMLSchema#unsignedLong"
1230                    | "http://www.w3.org/2001/XMLSchema#unsignedInt"
1231                    | "http://www.w3.org/2001/XMLSchema#unsignedShort"
1232                    | "http://www.w3.org/2001/XMLSchema#unsignedByte"
1233                    | "http://www.w3.org/2001/XMLSchema#positiveInteger"
1234                    | "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"
1235                    | "http://www.w3.org/2001/XMLSchema#negativeInteger"
1236                    | "http://www.w3.org/2001/XMLSchema#nonPositiveInteger"
1237            )
1238        } else {
1239            false
1240        }
1241    }
1242
1243    /// Check if this is a string literal
1244    pub fn is_string(&self) -> bool {
1245        self.datatype.is_none() && self.language.is_none()
1246    }
1247
1248    /// Check if this is a language-tagged literal
1249    pub fn is_lang_string(&self) -> bool {
1250        self.language.is_some()
1251    }
1252
1253    /// Check if this is a boolean literal
1254    pub fn is_boolean(&self) -> bool {
1255        if let Some(ref dt) = self.datatype {
1256            dt.as_str() == "http://www.w3.org/2001/XMLSchema#boolean"
1257        } else {
1258            false
1259        }
1260    }
1261
1262    /// Check if this is a date/time literal
1263    pub fn is_datetime(&self) -> bool {
1264        if let Some(ref dt) = self.datatype {
1265            matches!(
1266                dt.as_str(),
1267                "http://www.w3.org/2001/XMLSchema#date"
1268                    | "http://www.w3.org/2001/XMLSchema#dateTime"
1269                    | "http://www.w3.org/2001/XMLSchema#time"
1270                    | "http://www.w3.org/2001/XMLSchema#gYear"
1271                    | "http://www.w3.org/2001/XMLSchema#gYearMonth"
1272                    | "http://www.w3.org/2001/XMLSchema#gMonth"
1273                    | "http://www.w3.org/2001/XMLSchema#gMonthDay"
1274                    | "http://www.w3.org/2001/XMLSchema#gDay"
1275                    | "http://www.w3.org/2001/XMLSchema#duration"
1276                    | "http://www.w3.org/2001/XMLSchema#dayTimeDuration"
1277                    | "http://www.w3.org/2001/XMLSchema#yearMonthDuration"
1278            )
1279        } else {
1280            false
1281        }
1282    }
1283}
1284
1285impl Default for Statistics {
1286    fn default() -> Self {
1287        Self {
1288            cardinality: 0,
1289            selectivity: 1.0,
1290            available_indexes: Vec::new(),
1291            cost: 0.0,
1292            memory_estimate: 0,
1293            io_estimate: 0,
1294        }
1295    }
1296}
1297
1298impl Statistics {
1299    /// Create statistics with estimated cardinality
1300    pub fn with_cardinality(cardinality: u64) -> Self {
1301        Self {
1302            cardinality,
1303            selectivity: 1.0,
1304            available_indexes: Vec::new(),
1305            cost: cardinality as f64,
1306            memory_estimate: cardinality * 64, // Rough estimate
1307            io_estimate: cardinality / 1000,   // Pages
1308        }
1309    }
1310
1311    /// Update statistics with selectivity factor
1312    pub fn with_selectivity(mut self, selectivity: f64) -> Self {
1313        self.selectivity = selectivity.clamp(0.0, 1.0);
1314        self.cardinality = (self.cardinality as f64 * self.selectivity) as u64;
1315        self.cost *= self.selectivity;
1316        self
1317    }
1318
1319    /// Add available index
1320    pub fn with_index(mut self, index: IndexType) -> Self {
1321        self.available_indexes.push(index);
1322        // Reduce cost if good indexes are available
1323        self.cost *= 0.8;
1324        self
1325    }
1326
1327    /// Combine statistics (for joins)
1328    pub fn combine(&self, other: &Statistics) -> Self {
1329        Self {
1330            cardinality: self.cardinality * other.cardinality,
1331            selectivity: self.selectivity * other.selectivity,
1332            available_indexes: self
1333                .available_indexes
1334                .iter()
1335                .chain(other.available_indexes.iter())
1336                .cloned()
1337                .collect(),
1338            cost: self.cost + other.cost,
1339            memory_estimate: self.memory_estimate + other.memory_estimate,
1340            io_estimate: self.io_estimate + other.io_estimate,
1341        }
1342    }
1343}
1344
1345impl OptimizationHints {
1346    /// Create hints for BGP patterns
1347    pub fn for_bgp(patterns: &[TriplePattern]) -> Self {
1348        let mut hints = OptimizationHints::default();
1349
1350        // Estimate based on pattern complexity
1351        let cardinality = match patterns.len() {
1352            0 => 0,
1353            1 => 1000,                         // Single pattern estimate
1354            n => 1000 / (n as u64 * n as u64), // Selectivity decreases with more patterns
1355        };
1356
1357        hints.statistics = Some(Statistics::with_cardinality(cardinality));
1358
1359        // Suggest indexes based on pattern structure
1360        for pattern in patterns {
1361            if let Term::Variable(_) = pattern.subject {
1362                hints.preferred_indexes.push(IndexType::PredicateIndex);
1363            }
1364            if let Term::Variable(_) = pattern.predicate {
1365                hints.preferred_indexes.push(IndexType::SubjectIndex);
1366            }
1367            if let Term::Variable(_) = pattern.object {
1368                hints
1369                    .preferred_indexes
1370                    .push(IndexType::SubjectPredicateIndex);
1371            }
1372        }
1373
1374        hints
1375    }
1376
1377    /// Create hints for join operations
1378    pub fn for_join(left_hints: &OptimizationHints, right_hints: &OptimizationHints) -> Self {
1379        let mut hints = OptimizationHints::default();
1380
1381        // Combine statistics
1382        if let (Some(left_stats), Some(right_stats)) =
1383            (&left_hints.statistics, &right_hints.statistics)
1384        {
1385            hints.statistics = Some(left_stats.combine(right_stats));
1386
1387            // Choose join algorithm based on cardinalities
1388            hints.join_algorithm = Some(match (left_stats.cardinality, right_stats.cardinality) {
1389                (l, r) if l < 1000 && r < 1000 => JoinAlgorithm::NestedLoopJoin,
1390                (l, r) if l > 100000 || r > 100000 => JoinAlgorithm::SortMergeJoin,
1391                _ => JoinAlgorithm::HashJoin,
1392            });
1393        }
1394
1395        // Inherit index preferences
1396        hints.preferred_indexes = left_hints
1397            .preferred_indexes
1398            .iter()
1399            .chain(right_hints.preferred_indexes.iter())
1400            .cloned()
1401            .collect();
1402
1403        hints
1404    }
1405
1406    /// Create hints for filter operations
1407    pub fn for_filter(pattern_hints: &OptimizationHints, condition: &Expression) -> Self {
1408        let mut hints = pattern_hints.clone();
1409
1410        // Apply filter selectivity
1411        if let Some(ref mut stats) = hints.statistics {
1412            let filter_selectivity = estimate_filter_selectivity(condition);
1413            *stats = stats.clone().with_selectivity(filter_selectivity);
1414        }
1415
1416        // Suggest early filter placement for selective filters
1417        hints.filter_placement = if estimate_filter_selectivity(condition) < 0.1 {
1418            FilterPlacement::Early
1419        } else {
1420            FilterPlacement::Optimal
1421        };
1422
1423        hints
1424    }
1425}
1426
1427impl AnnotatedAlgebra {
1428    /// Create annotated algebra with default hints
1429    pub fn new(algebra: Algebra) -> Self {
1430        let hints = match &algebra {
1431            Algebra::Bgp(patterns) => OptimizationHints::for_bgp(patterns),
1432            Algebra::Join { left: _, right: _ } => {
1433                // For now, use default hints - in practice, we'd analyze the children
1434                OptimizationHints::default()
1435            }
1436            Algebra::Filter { .. } => OptimizationHints::default(),
1437            _ => OptimizationHints::default(),
1438        };
1439
1440        Self {
1441            algebra,
1442            hints,
1443            context: None,
1444        }
1445    }
1446
1447    /// Create annotated algebra with custom hints
1448    pub fn with_hints(algebra: Algebra, hints: OptimizationHints) -> Self {
1449        Self {
1450            algebra,
1451            hints,
1452            context: None,
1453        }
1454    }
1455
1456    /// Add execution context
1457    pub fn with_context(mut self, context: String) -> Self {
1458        self.context = Some(context);
1459        self
1460    }
1461
1462    /// Get estimated cost
1463    pub fn estimated_cost(&self) -> f64 {
1464        self.hints
1465            .statistics
1466            .as_ref()
1467            .map(|s| s.cost)
1468            .unwrap_or(0.0)
1469    }
1470
1471    /// Get estimated cardinality
1472    pub fn estimated_cardinality(&self) -> u64 {
1473        self.hints
1474            .statistics
1475            .as_ref()
1476            .map(|s| s.cardinality)
1477            .unwrap_or(0)
1478    }
1479}
1480
1481/// Estimate selectivity of a filter condition (rough heuristic)
1482fn estimate_filter_selectivity(condition: &Expression) -> f64 {
1483    match condition {
1484        Expression::Binary { op, .. } => match op {
1485            BinaryOperator::Equal => 0.01,    // Very selective
1486            BinaryOperator::NotEqual => 0.99, // Not selective
1487            BinaryOperator::Less
1488            | BinaryOperator::LessEqual
1489            | BinaryOperator::Greater
1490            | BinaryOperator::GreaterEqual => 0.33, // Range
1491            BinaryOperator::And => 0.25,      // Compound - more selective
1492            BinaryOperator::Or => 0.75,       // Compound - less selective
1493            _ => 0.5,                         // Default
1494        },
1495        Expression::Function { name, .. } => match name.as_str() {
1496            "regex" | "contains" => 0.2,              // Text search
1497            "bound" => 0.8,                           // Usually true
1498            "isIRI" | "isLiteral" | "isBlank" => 0.3, // Type checks
1499            _ => 0.5,                                 // Default
1500        },
1501        Expression::Unary {
1502            op: UnaryOperator::Not,
1503            ..
1504        } => 0.5, // Invert selectivity (simplified)
1505        Expression::Unary { .. } => 0.5,
1506        _ => 0.5, // Default selectivity
1507    }
1508}
1509
1510/// Evaluation context for query execution
1511#[derive(Debug, Clone, Default)]
1512pub struct EvaluationContext {
1513    /// Variable bindings
1514    pub bindings: HashMap<Variable, Term>,
1515    /// Dataset being queried
1516    pub dataset: Option<String>,
1517    /// Query execution options
1518    pub options: HashMap<String, String>,
1519}
1520
1521#[cfg(test)]
1522mod algebra_tests;