Skip to main content

qualia_core_db/sparql_library/
sparql_ast.rs

1//! SPARQL AST - Index-Based Zero-Allocation Query Representation
2//!
3//! Uses u16 indices into flat arrays to avoid Box allocation and recursive type errors.
4//! Fully compliant with AGENTS.md zero-allocation constraints.
5
6pub type PatternId = u16;
7pub type ExpressionId = u16;
8pub type VariableId = u8;
9
10/// Maximum number of patterns in a query context
11pub const MAX_PATTERNS: usize = 128;
12
13/// Maximum number of expressions in a query context
14pub const MAX_EXPRESSIONS: usize = 128;
15
16/// Maximum number of variables per query
17pub const MAX_VARIABLES: usize = 16;
18
19/// Maximum number of bindings per row
20pub const MAX_BINDINGS: usize = 16;
21
22/// Maximum number of order conditions
23pub const MAX_ORDER_CONDITIONS: usize = 16;
24
25/// SPARQL query forms
26#[repr(C)]
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum SparqlQuery {
29    Select(SelectQuery),
30    Ask(AskQuery),
31    Construct(ConstructQuery),
32    Describe(DescribeQuery),
33}
34
35/// SELECT query
36#[repr(C)]
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct SelectQuery {
39    pub distinct: bool,
40    pub reduced: bool,
41    pub variables: [VariableId; MAX_VARIABLES],
42    pub var_count: u8,
43    pub root_pattern: PatternId,
44    pub group_by: [VariableId; MAX_VARIABLES],
45    pub group_by_count: u8,
46    pub aggregates: [crate::sparql_planner::AggregateSpec; 16],
47    pub aggregate_count: u8,
48    pub having: Option<ExpressionId>,
49    pub order_by: [OrderCondition; MAX_ORDER_CONDITIONS],
50    pub order_by_count: u8,
51    pub limit: Option<u64>,
52    pub offset: u64,
53}
54
55impl Default for SelectQuery {
56    fn default() -> Self {
57        Self {
58            distinct: false,
59            reduced: false,
60            variables: [0; MAX_VARIABLES],
61            var_count: 0,
62            root_pattern: 0,
63            group_by: [0; MAX_VARIABLES],
64            group_by_count: 0,
65            aggregates: [crate::sparql_planner::AggregateSpec {
66                func: 0,
67                input_var: 0,
68                output_var: 0,
69            }; 16],
70            aggregate_count: 0,
71            having: None,
72            order_by: [OrderCondition::default(); MAX_ORDER_CONDITIONS],
73            order_by_count: 0,
74            limit: None,
75            offset: 0,
76        }
77    }
78}
79
80/// ASK query
81#[repr(C)]
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct AskQuery {
84    pub root_pattern: PatternId,
85}
86
87/// CONSTRUCT query
88#[repr(C)]
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct ConstructQuery {
91    pub template_pattern: PatternId,
92    pub root_pattern: PatternId,
93    pub group_by: [VariableId; MAX_VARIABLES],
94    pub group_by_count: u8,
95    pub having: Option<ExpressionId>,
96    pub order_by: [OrderCondition; MAX_ORDER_CONDITIONS],
97    pub order_by_count: u8,
98    pub limit: Option<u64>,
99    pub offset: u64,
100}
101
102/// DESCRIBE query
103#[repr(C)]
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct DescribeQuery {
106    pub vars_or_ids: [u64; MAX_VARIABLES],
107    pub var_count: u8,
108    pub root_pattern: Option<PatternId>,
109}
110
111/// Temporal snapshot mode for `AS OF` / `AT TIME` queries.
112///
113/// Used in `Pattern::AsOf` and `PhysicalOperatorType::AsOf` (Phase 4).
114#[repr(u8)]
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum TemporalMode {
117    /// Assertion-time snapshot: include quins with `prov:generatedAtTime ≤ timestamp_ms`.
118    AsOf = 0,
119    /// Valid-time point: include quins where `startedAtTime ≤ t ≤ endedAtTime`.
120    AtTime = 1,
121}
122
123/// Graph pattern - now uses PatternId indices instead of Box
124#[repr(C)]
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum Pattern {
127    /// Basic triple pattern
128    Triple {
129        subject: u64,
130        predicate: u64,
131        object: u64,
132    },
133    /// OPTIONAL pattern - references inner pattern by ID
134    Optional { inner: PatternId },
135    /// UNION pattern - references left and right by IDs
136    Union { left: PatternId, right: PatternId },
137    /// GRAPH pattern - references graph var/IRI and inner pattern
138    Graph {
139        graph_var_or_id: u64,
140        inner: PatternId,
141    },
142    /// FILTER pattern - references pattern to filter and expression
143    Filter {
144        pattern: PatternId,
145        expression: ExpressionId,
146    },
147    /// BIND(expr AS ?var) — extends each solution of `pattern` with `var`
148    /// bound to the value of `expression` (SPARQL 1.1 Extend). If the
149    /// expression errors, `var` is left unbound and the row is kept.
150    Bind {
151        pattern: PatternId,
152        var: VariableId,
153        expression: ExpressionId,
154    },
155    /// MINUS pattern
156    Minus { inner: PatternId },
157    /// Group graph pattern - references range in child array
158    Group { start_idx: u16, len: u16 },
159    /// Property path pattern (SPARQL 1.1)
160    PropertyPath {
161        subject: u64,
162        path: PathId,
163        object: u64,
164    },
165    /// SERVICE pattern (Federated Query with DID)
166    Service {
167        endpoint_did_id: u64, // DID with 0x8 prefix for identity recognition
168        inner_pattern: PatternId,
169    },
170    /// AS OF / AT TIME temporal snapshot (Phase 4, §5).
171    ///
172    /// Wraps `inner` and filters its results to a historical snapshot.
173    /// The executor checks T_CONTEXT PROV-O quins for each bound subject.
174    AsOf {
175        inner: PatternId,
176        timestamp_ms: u64,
177        mode: TemporalMode,
178    },
179    /// RDF-Star quoted triple `<< s p o >> outerP outerO`.
180    StarTriple {
181        inner_subject: u64,
182        inner_predicate: u64,
183        inner_object: u64,
184        outer_predicate: u64,
185        outer_object: u64,
186    },
187    /// Sub-`SELECT` `{ SELECT … WHERE { … } }` — an independently-evaluated
188    /// nested query (index into `SparqlQueryContext::subqueries`) whose projected
189    /// solutions join with the enclosing group. Only the sub-select's SELECT
190    /// variables are visible outside it.
191    SubSelect { query_id: u16 },
192}
193
194/// Property path (SPARQL 1.1)
195#[repr(C)]
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum Path {
198    /// Simple predicate
199    Predicate(u64),
200    /// Inverse predicate (^pred)
201    Inverse(PathId),
202    /// Sequence (pred1/pred2)
203    Sequence { left: PathId, right: PathId },
204    /// Alternation (pred1|pred2)
205    Alternative { left: PathId, right: PathId },
206    /// Zero or more (pred*)
207    ZeroOrMore(PathId),
208    /// One or more (pred+)
209    OneOrMore(PathId),
210    /// Zero or one (pred?)
211    ZeroOrOne(PathId),
212}
213
214pub type PathId = u16;
215
216/// Expression - uses ExpressionId for nested expressions
217#[repr(C)]
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum Expression {
220    /// Variable reference
221    Variable(VariableId),
222    /// Literal value (index into literal table)
223    Literal(u64),
224    /// IRI reference
225    Iri(u64),
226    /// Unary operation
227    UnaryOp { op: UnaryOp, expr: ExpressionId },
228    /// Binary operation
229    BinaryOp {
230        op: BinaryOp,
231        left: ExpressionId,
232        right: ExpressionId,
233    },
234    /// Function call
235    Function {
236        func: Function,
237        args_start: u16,
238        args_len: u16,
239    },
240    /// Subquery
241    Subquery {
242        query_id: u16, // Index into query array
243    },
244    /// Embedded triple (RDF-Star)
245    EmbeddedTriple {
246        subject: u64,
247        predicate: u64,
248        object: u64,
249    },
250    /// `EXISTS { … }` / `NOT EXISTS { … }` — true iff the inner group graph
251    /// pattern has ≥1 solution when the current row's bindings are substituted
252    /// in. `negated` flips the result (NOT EXISTS). Evaluated by the executor
253    /// (it needs graph access), not the pure value evaluator; it is only valid
254    /// in a FILTER/HAVING boolean position.
255    Exists { pattern: PatternId, negated: bool },
256}
257
258/// Unary operators
259#[repr(C)]
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum UnaryOp {
262    Not,
263    Plus,
264    Minus,
265}
266
267/// Binary operators
268#[repr(C)]
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub enum BinaryOp {
271    Or,
272    And,
273    Equal,
274    NotEqual,
275    LessThan,
276    LessThanOrEqual,
277    GreaterThan,
278    GreaterThanOrEqual,
279    Add,
280    Subtract,
281    Multiply,
282    Divide,
283}
284
285/// Built-in SPARQL functions
286#[repr(C)]
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum Function {
289    Str,
290    Lang,
291    LangMatches,
292    Datatype,
293    Bound,
294    Iri,
295    Uri,
296    Bnode,
297    Rand,
298    Abs,
299    Ceil,
300    Floor,
301    Round,
302    Concat,
303    Substring,
304    Strlen,
305    Ucase,
306    Lcase,
307    EncodeForUri,
308    Contains,
309    VarStarts,
310    VarEnds,
311    StrBefore,
312    StrAfter,
313    Year,
314    Month,
315    Day,
316    Hours,
317    Minutes,
318    Seconds,
319    Timezone,
320    Tz,
321    Now,
322    Uuid,
323    StrUuid,
324    Coalesce,
325    If,
326    StrLang,
327    StrDt,
328    SameTerm,
329    IsIri,
330    IsUri,
331    IsBlank,
332    IsLiteral,
333    IsNumeric,
334    Regex,
335    // SPARQL-Star functions
336    TripleSubject,
337    TriplePredicate,
338    TripleObject,
339    Triple,
340    Custom(u64), // Index into custom function table
341}
342
343/// ORDER BY condition
344#[repr(C)]
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
346pub struct OrderCondition {
347    pub ascending: bool,
348    pub expr: ExpressionId,
349}
350
351/// Flat query context arena - pre-allocated, no heap allocation
352#[repr(C)]
353pub struct SparqlQueryContext {
354    /// Flat array of all patterns in the query
355    pub patterns: [Pattern; MAX_PATTERNS],
356    /// Number of patterns currently allocated
357    pub pattern_count: usize,
358    /// Flat array of all expressions in the query
359    pub expressions: [Expression; MAX_EXPRESSIONS],
360    /// Number of expressions currently allocated
361    pub expression_count: usize,
362    /// Flat array of all property paths in the query
363    pub paths: [Path; MAX_PATTERNS],
364    /// Number of paths currently allocated
365    pub path_count: usize,
366    /// Flat array of subqueries (for nested queries)
367    pub subqueries: [SparqlQuery; 16],
368    /// Number of subqueries currently allocated
369    pub subquery_count: usize,
370    /// Variable name to ID mapping (simplified - stores as hash for now)
371    pub variable_hashes: [u64; MAX_VARIABLES],
372    /// Number of variables
373    pub variable_count: usize,
374    /// Argument storage for function calls (flat array)
375    pub function_args: [ExpressionId; 64],
376    /// Number of function args
377    pub function_arg_count: usize,
378}
379
380/// Query-scoped table of literal text (`hash -> string`) built by the parser for
381/// string/geometry constants. It lives **outside** the zero-heap
382/// `SparqlQueryContext` — it is a cold parse/eval-time structure — so functions
383/// that need literal text (`geof:*`, `STR`, `REGEX`, …) can recover it without
384/// putting `String`/`Vec` on the zero-heap execution hot path (CLAUDE.md §6).
385/// Canonical hash for a literal term, distinguishing a plain literal from a
386/// language-tagged or datatype-tagged one (`"x"`, `"x"@en`, and `"x"^^:t` are three
387/// distinct RDF terms). The parser and the [`StringSink`] both use this so a produced
388/// `STRLANG`/`STRDT` term round-trips its tag.
389pub fn literal_term_hash(text: &str, lang: Option<&str>, datatype: Option<&str>) -> u64 {
390    match (lang, datatype) {
391        (None, None) => crate::lexicon::generate_60bit_token(text.as_bytes()),
392        (Some(l), _) => crate::lexicon::generate_60bit_token(format!("{text}@{l}").as_bytes()),
393        (None, Some(d)) => crate::lexicon::generate_60bit_token(format!("{text}^^{d}").as_bytes()),
394    }
395}
396
397/// Query-scoped table of literal text + optional language tag / datatype IRI,
398/// `hash -> (text, lang?, datatype?)`, built by the parser for string/geometry/typed
399/// constants. Lives **outside** the zero-heap `SparqlQueryContext` — a cold
400/// parse/eval-time structure — so builtins (`STR`/`LANG`/`DATATYPE`/`geof:*`/…) recover
401/// what they need without heap on the §6 hot path.
402#[derive(Debug, Default, Clone)]
403pub struct LiteralTable {
404    entries: Vec<(u64, String, Option<String>, Option<String>)>,
405}
406
407impl LiteralTable {
408    pub fn new() -> Self {
409        Self::default()
410    }
411    /// Record the text for a plain literal hash (idempotent, no lang/datatype).
412    pub fn intern(&mut self, hash: u64, text: &str) {
413        self.intern_tagged(hash, text, None, None);
414    }
415    /// Record a literal with an optional language tag and/or datatype IRI.
416    pub fn intern_tagged(
417        &mut self,
418        hash: u64,
419        text: &str,
420        lang: Option<&str>,
421        datatype: Option<&str>,
422    ) {
423        if self.entries.iter().any(|(h, ..)| *h == hash) {
424            return;
425        }
426        self.entries.push((
427            hash,
428            text.to_string(),
429            lang.map(str::to_string),
430            datatype.map(str::to_string),
431        ));
432    }
433    pub fn resolve(&self, hash: u64) -> Option<&str> {
434        self.entries
435            .iter()
436            .find(|(h, ..)| *h == hash)
437            .map(|(_, s, ..)| s.as_str())
438    }
439    /// The language tag of an interned literal, if it has one.
440    pub fn lang(&self, hash: u64) -> Option<&str> {
441        self.entries
442            .iter()
443            .find(|(h, ..)| *h == hash)
444            .and_then(|(_, _, l, _)| l.as_deref())
445    }
446    /// The datatype IRI of an interned literal, if it was tagged with one.
447    pub fn datatype(&self, hash: u64) -> Option<&str> {
448        self.entries
449            .iter()
450            .find(|(h, ..)| *h == hash)
451            .and_then(|(_, _, _, d)| d.as_deref())
452    }
453    /// Whether this hash names a known (interned) literal.
454    pub fn contains(&self, hash: u64) -> bool {
455        self.entries.iter().any(|(h, ..)| *h == hash)
456    }
457    pub fn len(&self) -> usize {
458        self.entries.len()
459    }
460    pub fn is_empty(&self) -> bool {
461        self.entries.is_empty()
462    }
463}
464
465/// Query-scoped sink for strings **produced** by expression evaluation — the result
466/// of `CONCAT`/`SUBSTR`/`UCASE`/…, an `xsd:dateTime` lexical from `NOW`, a `UUID`
467/// string, or a value-producing `BIND`. `EvalResult` only carries a `u64` hash, so a
468/// produced string must be interned somewhere its hash can be resolved again; this is
469/// that table.
470///
471/// It uses interior mutability (`RefCell`) so it can be carried by the `Copy`
472/// [`TextResolver`] and written during evaluation **without** threading `&mut` through
473/// the recursive evaluator. This is the *cold* expression-eval tier (which already
474/// allocates `String`s for text builtins), not the §6 zero-heap hot path, so a small
475/// interning table here does not violate the hot-path invariant.
476#[derive(Debug, Default)]
477pub struct StringSink {
478    produced: std::cell::RefCell<Vec<(u64, String, Option<String>, Option<String>)>>,
479}
480
481impl StringSink {
482    pub fn new() -> Self {
483        Self::default()
484    }
485    /// Intern a produced plain string, returning its stable content-derived token.
486    /// Deterministic (same text → same token), so repeated/​reordered evaluation of a
487    /// pure builtin is referentially transparent (plan §4.4, QISP-R06).
488    pub fn intern(&self, text: &str) -> u64 {
489        self.intern_tagged(text, None, None)
490    }
491    /// Intern a produced string carrying an optional language tag / datatype IRI
492    /// (`STRLANG`/`STRDT`). The token distinguishes `"x"`, `"x"@en`, `"x"^^:t` via
493    /// [`literal_term_hash`], so `LANG`/`DATATYPE` can read the tag back.
494    pub fn intern_tagged(&self, text: &str, lang: Option<&str>, datatype: Option<&str>) -> u64 {
495        let hash = literal_term_hash(text, lang, datatype);
496        let mut v = self.produced.borrow_mut();
497        if !v.iter().any(|(h, ..)| *h == hash) {
498            v.push((
499                hash,
500                text.to_string(),
501                lang.map(str::to_string),
502                datatype.map(str::to_string),
503            ));
504        }
505        hash
506    }
507    /// Resolve a previously-interned produced string.
508    pub fn resolve(&self, hash: u64) -> Option<String> {
509        self.produced
510            .borrow()
511            .iter()
512            .find(|(h, ..)| *h == hash)
513            .map(|(_, s, ..)| s.clone())
514    }
515    /// The language tag of a produced string term, if any.
516    pub fn lang(&self, hash: u64) -> Option<String> {
517        self.produced
518            .borrow()
519            .iter()
520            .find(|(h, ..)| *h == hash)
521            .and_then(|(_, _, l, _)| l.clone())
522    }
523    /// The datatype IRI of a produced string term, if any.
524    pub fn datatype(&self, hash: u64) -> Option<String> {
525        self.produced
526            .borrow()
527            .iter()
528            .find(|(h, ..)| *h == hash)
529            .and_then(|(_, _, _, d)| d.clone())
530    }
531}
532
533/// Borrowed text resolver threaded into expression evaluation. Resolves a term
534/// hash to its literal text via, in order: the query-scoped `LiteralTable`
535/// (query constants), a [`StringSink`] of values produced during this query, an
536/// optional ingested-data lexicon closure (e.g. wrapping a `Q42LexMmap::lookup_hash`),
537/// and finally the global demo lexicon. Also carries the query-stable `now_ms` clock
538/// and `seed` used by the temporal / `UUID` / `RAND` builtins so their results are
539/// referentially transparent within one query snapshot (plan §4.4). All borrowed — no
540/// per-query heap in the evaluator, so the §6 hot-path invariant holds.
541#[derive(Clone, Copy)]
542pub struct TextResolver<'a> {
543    pub literals: &'a LiteralTable,
544    pub lexicon: Option<&'a dyn Fn(u64) -> Option<String>>,
545    /// Interner for strings produced by evaluation (`CONCAT`, `NOW`, `UUID`, …).
546    pub sink: Option<&'a StringSink>,
547    /// Query-stable wall clock, ms since the Unix epoch. `0` = unset (temporal
548    /// builtins then fail closed rather than fabricate a non-deterministic time).
549    pub now_ms: u64,
550    /// Query-stable RNG seed for `UUID`/`STRUUID`. `0` = unset.
551    pub seed: u64,
552}
553
554impl<'a> TextResolver<'a> {
555    pub fn new(literals: &'a LiteralTable) -> Self {
556        Self {
557            literals,
558            lexicon: None,
559            sink: None,
560            now_ms: 0,
561            seed: 0,
562        }
563    }
564    pub fn with_lexicon(
565        literals: &'a LiteralTable,
566        lexicon: &'a dyn Fn(u64) -> Option<String>,
567    ) -> Self {
568        Self {
569            literals,
570            lexicon: Some(lexicon),
571            sink: None,
572            now_ms: 0,
573            seed: 0,
574        }
575    }
576    /// Attach a [`StringSink`] so value-producing builtins can intern their results.
577    pub fn with_sink(mut self, sink: &'a StringSink) -> Self {
578        self.sink = Some(sink);
579        self
580    }
581    /// Set the query-stable clock (ms since epoch) and RNG seed used by `NOW`/date
582    /// and `UUID`/`STRUUID`. Both must be query-stable for referential transparency.
583    pub fn with_env(mut self, now_ms: u64, seed: u64) -> Self {
584        self.now_ms = now_ms;
585        self.seed = seed;
586        self
587    }
588    /// Resolve a term hash to its literal text, if known.
589    pub fn resolve_text(&self, hash: u64) -> Option<String> {
590        if let Some(s) = self.literals.resolve(hash) {
591            return Some(s.to_string());
592        }
593        if let Some(sink) = self.sink {
594            if let Some(s) = sink.resolve(hash) {
595                return Some(s);
596            }
597        }
598        if let Some(f) = self.lexicon {
599            if let Some(s) = f(hash) {
600                return Some(s);
601            }
602        }
603        crate::resolver::resolve_hash(hash).and_then(|b| String::from_utf8(b.to_vec()).ok())
604    }
605
606    /// The language tag of a literal term (`LANG`). A plain, non-tagged literal has the
607    /// empty tag `""` (correct SPARQL default); `None` only for a term that is not a
608    /// known literal at all.
609    pub fn lang_of(&self, hash: u64) -> Option<String> {
610        if let Some(l) = self.literals.lang(hash) {
611            return Some(l.to_string());
612        }
613        if let Some(sink) = self.sink {
614            if let Some(l) = sink.lang(hash) {
615                return Some(l);
616            }
617        }
618        // A known/resolvable literal with no lang tag → "".
619        if self.literals.contains(hash)
620            || self
621                .sink
622                .map(|s| s.resolve(hash).is_some())
623                .unwrap_or(false)
624            || crate::resolver::classify_inline_literal(hash).is_some()
625        {
626            return Some(String::new());
627        }
628        None
629    }
630
631    /// The datatype IRI of a literal term (`DATATYPE`): an explicit tag, else
632    /// `rdf:langString` for a lang-tagged literal, else the inline-encoded XSD type
633    /// (integer/decimal/double/boolean), else `xsd:string` for a plain known literal.
634    /// `None` for a term that is not a known/typed literal.
635    pub fn datatype_of(&self, hash: u64) -> Option<String> {
636        const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
637        const RDF_LANGSTRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
638        if let Some(dt) = self.literals.datatype(hash) {
639            return Some(dt.to_string());
640        }
641        if let Some(sink) = self.sink {
642            if let Some(dt) = sink.datatype(hash) {
643                return Some(dt);
644            }
645            if sink.lang(hash).is_some() {
646                return Some(RDF_LANGSTRING.to_string());
647            }
648        }
649        if self.literals.lang(hash).is_some() {
650            return Some(RDF_LANGSTRING.to_string());
651        }
652        if let Some(lit) = crate::resolver::classify_inline_literal(hash) {
653            return Some(lit.datatype_iri().to_string());
654        }
655        if self.literals.contains(hash)
656            || self
657                .sink
658                .map(|s| s.resolve(hash).is_some())
659                .unwrap_or(false)
660        {
661            return Some(XSD_STRING.to_string());
662        }
663        None
664    }
665}
666
667impl SparqlQueryContext {
668    pub fn new() -> Self {
669        Self {
670            patterns: [Pattern::Triple {
671                subject: 0,
672                predicate: 0,
673                object: 0,
674            }; MAX_PATTERNS],
675            pattern_count: 0,
676            expressions: [Expression::Variable(0); MAX_EXPRESSIONS],
677            expression_count: 0,
678            paths: [Path::Predicate(0); MAX_PATTERNS],
679            path_count: 0,
680            subqueries: [SparqlQuery::Select(SelectQuery {
681                distinct: false,
682                reduced: false,
683                variables: [0; MAX_VARIABLES],
684                var_count: 0,
685                root_pattern: 0,
686                group_by: [0; MAX_VARIABLES],
687                group_by_count: 0,
688                aggregates: [crate::sparql_planner::AggregateSpec {
689                    func: 0,
690                    input_var: 0,
691                    output_var: 0,
692                }; 16],
693                aggregate_count: 0,
694                having: None,
695                order_by: [OrderCondition {
696                    expr: 0,
697                    ascending: true,
698                }; MAX_ORDER_CONDITIONS],
699                order_by_count: 0,
700                limit: None,
701                offset: 0,
702            }); 16],
703            subquery_count: 0,
704            variable_hashes: [0; MAX_VARIABLES],
705            variable_count: 0,
706            function_args: [0; 64],
707            function_arg_count: 0,
708        }
709    }
710
711    /// Allocate a new pattern, returns its ID
712    pub fn alloc_pattern(&mut self, pattern: Pattern) -> Result<PatternId, String> {
713        if self.pattern_count >= MAX_PATTERNS {
714            return Err("Pattern overflow".to_string());
715        }
716        let id = self.pattern_count as PatternId;
717        self.patterns[self.pattern_count] = pattern;
718        self.pattern_count += 1;
719        Ok(id)
720    }
721
722    /// Allocate a new expression, returns its ID
723    pub fn alloc_expression(&mut self, expr: Expression) -> Result<ExpressionId, String> {
724        if self.expression_count >= MAX_EXPRESSIONS {
725            return Err("Expression overflow".to_string());
726        }
727        let id = self.expression_count as ExpressionId;
728        self.expressions[self.expression_count] = expr;
729        self.expression_count += 1;
730        Ok(id)
731    }
732
733    /// Allocate a new property path, returns its ID
734    pub fn alloc_path(&mut self, path: Path) -> Result<PathId, String> {
735        if self.path_count >= MAX_PATTERNS {
736            return Err("Path overflow".to_string());
737        }
738        let id = self.path_count as PathId;
739        self.paths[self.path_count] = path;
740        self.path_count += 1;
741        Ok(id)
742    }
743
744    /// Allocate a new subquery, returns its ID
745    pub fn alloc_subquery(&mut self, query: SparqlQuery) -> Result<u16, String> {
746        if self.subquery_count >= 16 {
747            return Err("Subquery overflow".to_string());
748        }
749        let id = self.subquery_count as u16;
750        self.subqueries[self.subquery_count] = query;
751        self.subquery_count += 1;
752        Ok(id)
753    }
754
755    /// Register a variable name, returns its ID
756    pub fn register_variable(&mut self, name: &str) -> Result<VariableId, String> {
757        if self.variable_count >= MAX_VARIABLES {
758            return Err("Variable overflow".to_string());
759        }
760        let hash = crate::lexicon::generate_60bit_token(name.as_bytes());
761        // Check if variable already exists
762        for (i, var_hash) in self.variable_hashes.iter().enumerate() {
763            if *var_hash == hash {
764                return Ok(i as VariableId);
765            }
766        }
767        let id = self.variable_count as VariableId;
768        self.variable_hashes[self.variable_count] = hash;
769        self.variable_count += 1;
770        Ok(id)
771    }
772
773    /// Reset the context for reuse (clears all allocations)
774    pub fn reset(&mut self) {
775        self.pattern_count = 0;
776        self.expression_count = 0;
777        self.variable_count = 0;
778        self.function_arg_count = 0;
779        self.variable_hashes = [0; MAX_VARIABLES];
780    }
781}
782
783impl Default for SparqlQueryContext {
784    fn default() -> Self {
785        Self::new()
786    }
787}
788
789/// Binding row - stack-allocated row for variable bindings
790#[repr(C)]
791#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
792pub struct BindingRow {
793    /// Slot array - None means unbound
794    pub slots: [Option<u64>; MAX_BINDINGS],
795}
796
797impl BindingRow {
798    pub fn new() -> Self {
799        Self {
800            slots: [None; MAX_BINDINGS],
801        }
802    }
803
804    pub fn set(&mut self, var_id: VariableId, value: u64) {
805        if (var_id as usize) < MAX_BINDINGS {
806            self.slots[var_id as usize] = Some(value);
807        }
808    }
809
810    pub fn get(&self, var_id: VariableId) -> Option<u64> {
811        if (var_id as usize) < MAX_BINDINGS {
812            self.slots[var_id as usize]
813        } else {
814            None
815        }
816    }
817
818    pub fn clear(&mut self) {
819        self.slots = [None; MAX_BINDINGS];
820    }
821}
822
823impl Default for BindingRow {
824    fn default() -> Self {
825        Self::new()
826    }
827}
828
829/// Physical operator trait for query execution
830pub trait PhysicalOperator {
831    /// Advance to next result, returns true if more results available
832    fn next(&mut self, ctx: &SparqlQueryContext, row: &mut BindingRow) -> bool;
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838
839    #[test]
840    fn test_query_context_allocation() {
841        let mut ctx = SparqlQueryContext::new();
842
843        let pattern = Pattern::Triple {
844            subject: 1,
845            predicate: 2,
846            object: 3,
847        };
848
849        let id = ctx.alloc_pattern(pattern).unwrap();
850        assert_eq!(id, 0);
851        assert_eq!(ctx.pattern_count, 1);
852    }
853
854    #[test]
855    fn test_variable_registration() {
856        let mut ctx = SparqlQueryContext::new();
857
858        let id1 = ctx.register_variable("?x").unwrap();
859        let id2 = ctx.register_variable("?y").unwrap();
860
861        assert_eq!(id1, 0);
862        assert_eq!(id2, 1);
863        assert_eq!(ctx.variable_count, 2);
864    }
865
866    #[test]
867    fn test_variable_duplicate() {
868        let mut ctx = SparqlQueryContext::new();
869
870        let id1 = ctx.register_variable("?x").unwrap();
871        let id2 = ctx.register_variable("?x").unwrap();
872
873        assert_eq!(id1, id2);
874        assert_eq!(ctx.variable_count, 1);
875    }
876
877    #[test]
878    fn test_binding_row() {
879        let mut row = BindingRow::new();
880
881        row.set(0, 42);
882        assert_eq!(row.get(0), Some(42));
883        assert_eq!(row.get(1), None);
884    }
885
886    #[test]
887    fn test_optional_pattern_index() {
888        let mut ctx = SparqlQueryContext::new();
889
890        let inner = Pattern::Triple {
891            subject: 1,
892            predicate: 2,
893            object: 3,
894        };
895        let inner_id = ctx.alloc_pattern(inner).unwrap();
896
897        let optional = Pattern::Optional { inner: inner_id };
898        let optional_id = ctx.alloc_pattern(optional).unwrap();
899
900        assert_eq!(ctx.pattern_count, 2);
901        if let Pattern::Optional { inner } = ctx.patterns[optional_id as usize] {
902            assert_eq!(inner, inner_id);
903        } else {
904            panic!("Expected Optional pattern");
905        }
906    }
907
908    #[test]
909    fn test_union_pattern_index() {
910        let mut ctx = SparqlQueryContext::new();
911
912        let left = Pattern::Triple {
913            subject: 1,
914            predicate: 2,
915            object: 3,
916        };
917        let right = Pattern::Triple {
918            subject: 4,
919            predicate: 5,
920            object: 6,
921        };
922
923        let left_id = ctx.alloc_pattern(left).unwrap();
924        let right_id = ctx.alloc_pattern(right).unwrap();
925
926        let union = Pattern::Union {
927            left: left_id,
928            right: right_id,
929        };
930        let union_id = ctx.alloc_pattern(union).unwrap();
931
932        assert_eq!(ctx.pattern_count, 3);
933        if let Pattern::Union { left, right } = ctx.patterns[union_id as usize] {
934            assert_eq!(left, left_id);
935            assert_eq!(right, right_id);
936        } else {
937            panic!("Expected Union pattern");
938        }
939    }
940}