Skip to main content

qualia_core_db/sparql_library/
sparql_planner.rs

1//! SPARQL Logical Query Planner
2//!
3//! Transforms parsed AST into an execution plan using zero-allocation patterns.
4
5use crate::sparql_ast::*;
6
7/// Physical operator types for the execution plan
8#[repr(C)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum PhysicalOperatorType {
11    /// Scan all quins matching a subject
12    SubjectScan { subject: u64 },
13    /// Scan all quins matching a predicate
14    PredicateScan { predicate: u64 },
15    /// Scan all quins matching an object
16    ObjectScan { object: u64 },
17    /// Triple pattern scan with all three components
18    TripleScan {
19        subject: u64,
20        predicate: u64,
21        object: u64,
22    },
23    /// Hash join between two operators
24    HashJoin {
25        left: OperatorId,
26        right: OperatorId,
27        join_var: VariableId,
28    },
29    /// Nested loop join for small datasets
30    NestedLoopJoin {
31        left: OperatorId,
32        right: OperatorId,
33        join_var: VariableId,
34    },
35    /// Filter operator
36    Filter {
37        input: OperatorId,
38        expression: ExpressionId,
39    },
40    /// Bind operator — extends each input row with `var` = eval(`expression`)
41    /// (SPARQL 1.1 Extend). Never drops rows; an expression error leaves the
42    /// variable unbound.
43    Bind {
44        input: OperatorId,
45        var: VariableId,
46        expression: ExpressionId,
47    },
48    /// Projection operator
49    Project {
50        input: OperatorId,
51        vars: [VariableId; MAX_VARIABLES],
52        var_count: u8,
53    },
54    /// Limit operator
55    Limit {
56        input: OperatorId,
57        limit: u64,
58        offset: u64,
59    },
60    /// Sort operator
61    Sort {
62        input: OperatorId,
63        order_by: [ExpressionId; MAX_ORDER_CONDITIONS],
64        order_count: u8,
65        ascending: [bool; MAX_ORDER_CONDITIONS],
66    },
67    /// Union operator
68    Union { left: OperatorId, right: OperatorId },
69    /// Optional operator (SPARQL 1.1 left-join)
70    Optional { left: OperatorId, right: OperatorId },
71    /// Anti-join operator (SPARQL 1.1 MINUS): keep a left solution unless a
72    /// right solution is compatible with it AND shares a bound variable.
73    AntiJoin { left: OperatorId, right: OperatorId },
74    /// Distinct operator
75    Distinct { input: OperatorId },
76    /// Sub-SELECT: evaluate stored subquery `query_id` independently and join
77    /// its projected solutions with the enclosing bindings.
78    SubSelect { query_id: u16 },
79    /// GroupBy operator with Aggregates
80    GroupBy {
81        input: OperatorId,
82        group_vars: [VariableId; MAX_VARIABLES],
83        group_var_count: u8,
84        aggregates: [AggregateSpec; 16],
85        aggregate_count: u8,
86    },
87    /// Having operator
88    Having {
89        input: OperatorId,
90        expression: ExpressionId,
91    },
92    /// Property path operator
93    PropertyPath {
94        subject: u64,
95        path_id: PathId,
96        object: u64,
97    },
98    /// Graph operator
99    Graph {
100        graph_var_or_id: u64,
101        inner: OperatorId,
102    },
103    /// Service operator (Federated Query with DID)
104    Service {
105        endpoint_did_id: u64,
106        inner_pattern: OperatorId,
107    },
108    /// AS OF / AT TIME temporal snapshot operator (Phase 4).
109    AsOf {
110        input: OperatorId,
111        timestamp_ms: u64,
112        mode: TemporalMode,
113    },
114    /// RDF-Star annotation triple scan.
115    StarTripleScan {
116        inner_subject: u64,
117        inner_predicate: u64,
118        inner_object: u64,
119        outer_predicate: u64,
120        outer_object: u64,
121    },
122}
123
124pub type OperatorId = u16;
125
126/// Aggregate specification
127#[repr(C)]
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct AggregateSpec {
130    pub func: u8, // 0=COUNT, 1=SUM, 2=AVG, 3=MIN, 4=MAX
131    pub input_var: VariableId,
132    pub output_var: VariableId,
133}
134
135/// Execution plan operator
136#[repr(C)]
137#[derive(Debug, Clone, Copy)]
138pub struct PlanOperator {
139    pub operator_type: PhysicalOperatorType,
140    pub estimated_cardinality: u64,
141}
142
143/// Execution plan
144#[repr(C)]
145pub struct ExecutionPlan {
146    pub operators: [PlanOperator; 64], // Max 64 operators in a plan
147    pub operator_count: u8,
148    pub root_operator: OperatorId,
149}
150
151impl ExecutionPlan {
152    pub fn new() -> Self {
153        Self {
154            operators: [PlanOperator {
155                operator_type: PhysicalOperatorType::SubjectScan { subject: 0 },
156                estimated_cardinality: 0,
157            }; 64],
158            operator_count: 0,
159            root_operator: 0,
160        }
161    }
162
163    pub fn add_operator(
164        &mut self,
165        op: PhysicalOperatorType,
166        cardinality: u64,
167    ) -> Result<OperatorId, String> {
168        if self.operator_count >= 64 {
169            return Err("Operator overflow".to_string());
170        }
171        let id = self.operator_count as OperatorId;
172        self.operators[self.operator_count as usize] = PlanOperator {
173            operator_type: op,
174            estimated_cardinality: cardinality,
175        };
176        self.operator_count += 1;
177        Ok(id)
178    }
179}
180
181impl Default for ExecutionPlan {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187/// Query planner
188pub struct QueryPlanner;
189
190impl QueryPlanner {
191    /// Plan a SPARQL query into an execution plan
192    pub fn plan(query: &SparqlQuery, ctx: &SparqlQueryContext) -> Result<ExecutionPlan, String> {
193        match query {
194            SparqlQuery::Select(select) => Self::plan_select(select, ctx),
195            SparqlQuery::Ask(ask) => Self::plan_ask(ask, ctx),
196            SparqlQuery::Construct(construct) => Self::plan_construct(construct, ctx),
197            SparqlQuery::Describe(describe) => Self::plan_describe(describe, ctx),
198        }
199    }
200
201    fn plan_select(
202        select: &SelectQuery,
203        ctx: &SparqlQueryContext,
204    ) -> Result<ExecutionPlan, String> {
205        let mut plan = ExecutionPlan::new();
206
207        // Plan the WHERE clause
208        let root_op = Self::plan_pattern(select.root_pattern, ctx, &mut plan)?;
209
210        // Apply GroupBy/Aggregates
211        let group_op = if select.group_by_count > 0 || select.aggregate_count > 0 {
212            let mut group_vars = [0u8; MAX_VARIABLES];
213            if select.group_by_count > 0 {
214                group_vars[..select.group_by_count as usize]
215                    .copy_from_slice(&select.group_by[..select.group_by_count as usize]);
216            }
217            let mut aggregates = [crate::sparql_planner::AggregateSpec {
218                func: 0,
219                input_var: 0,
220                output_var: 0,
221            }; 16];
222            if select.aggregate_count > 0 {
223                aggregates[..select.aggregate_count as usize]
224                    .copy_from_slice(&select.aggregates[..select.aggregate_count as usize]);
225            }
226            plan.add_operator(
227                PhysicalOperatorType::GroupBy {
228                    input: root_op,
229                    group_vars,
230                    group_var_count: select.group_by_count,
231                    aggregates,
232                    aggregate_count: select.aggregate_count,
233                },
234                0,
235            )?
236        } else {
237            root_op
238        };
239
240        // Apply projection
241        let project_op = if select.var_count > 0 {
242            let mut vars = [0u8; MAX_VARIABLES];
243            vars[..select.var_count as usize]
244                .copy_from_slice(&select.variables[..select.var_count as usize]);
245            plan.add_operator(
246                PhysicalOperatorType::Project {
247                    input: group_op,
248                    vars,
249                    var_count: select.var_count,
250                },
251                0, // Cardinality unknown
252            )?
253        } else {
254            root_op
255        };
256
257        // Apply DISTINCT / REDUCED — dedup the projected solutions. (Previously
258        // the `distinct` flag was parsed but never planned, so `SELECT DISTINCT`
259        // silently returned duplicates.) Eliminating all duplicates is also a
260        // conformant realisation of REDUCED.
261        let distinct_op = if select.distinct || select.reduced {
262            plan.add_operator(PhysicalOperatorType::Distinct { input: project_op }, 0)?
263        } else {
264            project_op
265        };
266
267        // Apply sorting
268        let sort_op = if select.order_by_count > 0 {
269            let mut order_by = [0u16; MAX_ORDER_CONDITIONS];
270            let mut ascending = [true; MAX_ORDER_CONDITIONS];
271            for i in 0..select.order_by_count as usize {
272                order_by[i] = select.order_by[i].expr;
273                ascending[i] = select.order_by[i].ascending;
274            }
275            plan.add_operator(
276                PhysicalOperatorType::Sort {
277                    input: distinct_op,
278                    order_by,
279                    order_count: select.order_by_count,
280                    ascending,
281                },
282                0,
283            )?
284        } else {
285            distinct_op
286        };
287
288        // Apply limit/offset
289        let final_op = if select.limit.is_some() || select.offset > 0 {
290            plan.add_operator(
291                PhysicalOperatorType::Limit {
292                    input: sort_op,
293                    limit: select.limit.unwrap_or(u64::MAX),
294                    offset: select.offset,
295                },
296                0,
297            )?
298        } else {
299            sort_op
300        };
301
302        plan.root_operator = final_op;
303        Ok(plan)
304    }
305
306    fn plan_ask(ask: &AskQuery, ctx: &SparqlQueryContext) -> Result<ExecutionPlan, String> {
307        let mut plan = ExecutionPlan::new();
308        let root_op = Self::plan_pattern(ask.root_pattern, ctx, &mut plan)?;
309        plan.root_operator = root_op;
310        Ok(plan)
311    }
312
313    fn plan_construct(
314        construct: &ConstructQuery,
315        ctx: &SparqlQueryContext,
316    ) -> Result<ExecutionPlan, String> {
317        let mut plan = ExecutionPlan::new();
318        let root_op = Self::plan_pattern(construct.root_pattern, ctx, &mut plan)?;
319        plan.root_operator = root_op;
320        Ok(plan)
321    }
322
323    fn plan_describe(
324        describe: &DescribeQuery,
325        ctx: &SparqlQueryContext,
326    ) -> Result<ExecutionPlan, String> {
327        let mut plan = ExecutionPlan::new();
328        if let Some(pattern_id) = describe.root_pattern {
329            let root_op = Self::plan_pattern(pattern_id, ctx, &mut plan)?;
330            plan.root_operator = root_op;
331        }
332        Ok(plan)
333    }
334
335    pub(crate) fn plan_pattern(
336        pattern_id: PatternId,
337        ctx: &SparqlQueryContext,
338        plan: &mut ExecutionPlan,
339    ) -> Result<OperatorId, String> {
340        let pattern = ctx
341            .patterns
342            .get(pattern_id as usize)
343            .ok_or("Pattern ID out of bounds")?;
344
345        match pattern {
346            Pattern::Triple {
347                subject,
348                predicate,
349                object,
350            } => plan.add_operator(
351                PhysicalOperatorType::TripleScan {
352                    subject: *subject,
353                    predicate: *predicate,
354                    object: *object,
355                },
356                0,
357            ),
358            Pattern::StarTriple {
359                inner_subject,
360                inner_predicate,
361                inner_object,
362                outer_predicate,
363                outer_object,
364            } => plan.add_operator(
365                PhysicalOperatorType::StarTripleScan {
366                    inner_subject: *inner_subject,
367                    inner_predicate: *inner_predicate,
368                    inner_object: *inner_object,
369                    outer_predicate: *outer_predicate,
370                    outer_object: *outer_object,
371                },
372                0,
373            ),
374            Pattern::Optional { inner } => {
375                let inner_op = Self::plan_pattern(*inner, ctx, plan)?;
376                // For now, just return the inner operator (simplified)
377                Ok(inner_op)
378            }
379            Pattern::Union { left, right } => {
380                let left_op = Self::plan_pattern(*left, ctx, plan)?;
381                let right_op = Self::plan_pattern(*right, ctx, plan)?;
382                plan.add_operator(
383                    PhysicalOperatorType::Union {
384                        left: left_op,
385                        right: right_op,
386                    },
387                    0,
388                )
389            }
390
391            Pattern::Filter {
392                pattern: inner_pattern,
393                expression,
394            } => {
395                let inner_op = Self::plan_pattern(*inner_pattern, ctx, plan)?;
396                plan.add_operator(
397                    PhysicalOperatorType::Filter {
398                        input: inner_op,
399                        expression: *expression,
400                    },
401                    0,
402                )
403            }
404            Pattern::Bind {
405                pattern: inner_pattern,
406                var,
407                expression,
408            } => {
409                let inner_op = Self::plan_pattern(*inner_pattern, ctx, plan)?;
410                plan.add_operator(
411                    PhysicalOperatorType::Bind {
412                        input: inner_op,
413                        var: *var,
414                        expression: *expression,
415                    },
416                    0,
417                )
418            }
419            Pattern::Minus { inner } => Self::plan_pattern(*inner, ctx, plan),
420            Pattern::Group { start_idx, len } => {
421                // Plan the group's children left-to-right. Plain children join
422                // (natural join — the nested-loop operator checks compatibility
423                // across all bound slots). OPTIONAL children become a real
424                // left-join and MINUS children a real anti-join against the
425                // accumulated result, rather than being folded into a plain join
426                // (which would make OPTIONAL required and MINUS behave like an
427                // intersection — both wrong).
428                let mut current_op: Option<OperatorId> = None;
429                for i in *start_idx..(*start_idx + *len) {
430                    let child = ctx
431                        .patterns
432                        .get(i as usize)
433                        .ok_or("Pattern ID out of bounds")?;
434                    match child {
435                        Pattern::Optional { inner } => {
436                            let right = Self::plan_pattern(*inner, ctx, plan)?;
437                            current_op = Some(match current_op {
438                                Some(left) => plan.add_operator(
439                                    PhysicalOperatorType::Optional { left, right },
440                                    0,
441                                )?,
442                                // A leading OPTIONAL has no required left side —
443                                // its solutions stand alone.
444                                None => right,
445                            });
446                        }
447                        Pattern::Minus { inner } => {
448                            let right = Self::plan_pattern(*inner, ctx, plan)?;
449                            current_op = Some(match current_op {
450                                Some(left) => plan.add_operator(
451                                    PhysicalOperatorType::AntiJoin { left, right },
452                                    0,
453                                )?,
454                                // MINUS with nothing to subtract from is
455                                // degenerate (malformed SPARQL); nothing is
456                                // removed, so fall back to the inner solutions.
457                                None => right,
458                            });
459                        }
460                        _ => {
461                            let pattern_op = Self::plan_pattern(i, ctx, plan)?;
462                            current_op = Some(match current_op {
463                                Some(curr) => plan.add_operator(
464                                    PhysicalOperatorType::NestedLoopJoin {
465                                        left: curr,
466                                        right: pattern_op,
467                                        join_var: 0,
468                                    },
469                                    0,
470                                )?,
471                                None => pattern_op,
472                            });
473                        }
474                    }
475                }
476                current_op.ok_or("Empty group pattern".to_string())
477            }
478            Pattern::PropertyPath {
479                subject,
480                path,
481                object,
482            } => plan.add_operator(
483                PhysicalOperatorType::PropertyPath {
484                    subject: *subject,
485                    path_id: *path,
486                    object: *object,
487                },
488                0,
489            ),
490            Pattern::SubSelect { query_id } => plan.add_operator(
491                PhysicalOperatorType::SubSelect {
492                    query_id: *query_id,
493                },
494                0,
495            ),
496            Pattern::Graph {
497                graph_var_or_id,
498                inner,
499            } => {
500                let inner_op = Self::plan_pattern(*inner, ctx, plan)?;
501                plan.add_operator(
502                    PhysicalOperatorType::Graph {
503                        graph_var_or_id: *graph_var_or_id,
504                        inner: inner_op,
505                    },
506                    0,
507                )
508            }
509            Pattern::Service {
510                endpoint_did_id,
511                inner_pattern,
512            } => {
513                let inner_op = Self::plan_pattern(*inner_pattern, ctx, plan)?;
514                plan.add_operator(
515                    PhysicalOperatorType::Service {
516                        endpoint_did_id: *endpoint_did_id,
517                        inner_pattern: inner_op,
518                    },
519                    0,
520                )
521            }
522            Pattern::AsOf {
523                inner,
524                timestamp_ms,
525                mode,
526            } => {
527                let inner_op = Self::plan_pattern(*inner, ctx, plan)?;
528                plan.add_operator(
529                    PhysicalOperatorType::AsOf {
530                        input: inner_op,
531                        timestamp_ms: *timestamp_ms,
532                        mode: *mode,
533                    },
534                    0,
535                )
536            }
537        }
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn test_execution_plan_creation() {
547        let plan = ExecutionPlan::new();
548        assert_eq!(plan.operator_count, 0);
549    }
550
551    #[test]
552    fn test_add_operator() {
553        let mut plan = ExecutionPlan::new();
554        let op = PhysicalOperatorType::SubjectScan { subject: 42 };
555        let id = plan.add_operator(op, 0).unwrap();
556        assert_eq!(id, 0);
557        assert_eq!(plan.operator_count, 1);
558    }
559
560    #[test]
561    fn test_plan_triple_pattern() {
562        let mut ctx = SparqlQueryContext::new();
563        let pattern = Pattern::Triple {
564            subject: 1,
565            predicate: 2,
566            object: 3,
567        };
568        let pattern_id = ctx.alloc_pattern(pattern).unwrap();
569
570        let mut plan = ExecutionPlan::new();
571        let op_id = QueryPlanner::plan_pattern(pattern_id, &ctx, &mut plan).unwrap();
572        assert_eq!(op_id, 0);
573        assert_eq!(plan.operator_count, 1);
574    }
575}