Skip to main content

meta_language/
query_plan.rs

1//! Language-neutral executable query-plan concepts shared by query frontends.
2
3use std::collections::BTreeMap;
4
5use serde_json::{json, Map, Number, Value};
6
7use crate::link_network::{Link, LinkId, LinkMetadata, LinkNetwork, LinkType};
8use crate::source::SourceSpan;
9
10/// Stable schema version emitted by [`QueryPlan::canonical_json`].
11pub const QUERY_PLAN_VERSION: u8 = 1;
12
13/// Whether a lowered plan has been authorized for execution.
14///
15/// Parsing and semantic lowering never grant permission to execute a query.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum QueryAuthorization {
18    /// A consuming engine must still apply its authorization policy.
19    Required,
20}
21
22/// Canonical executable operation.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum QueryOperation {
25    /// Read matching resources.
26    Select,
27    /// Create a resource.
28    Insert,
29    /// Modify matching resources.
30    Update,
31    /// Remove matching resources.
32    Delete,
33}
34
35impl QueryOperation {
36    /// Canonical lower-case label.
37    #[must_use]
38    pub const fn as_str(self) -> &'static str {
39        match self {
40            Self::Select => "select",
41            Self::Insert => "insert",
42            Self::Update => "update",
43            Self::Delete => "delete",
44        }
45    }
46
47    /// Parses a canonical operation label.
48    #[must_use]
49    pub fn parse(value: &str) -> Option<Self> {
50        match value {
51            "select" => Some(Self::Select),
52            "insert" => Some(Self::Insert),
53            "update" => Some(Self::Update),
54            "delete" => Some(Self::Delete),
55            _ => None,
56        }
57    }
58}
59
60/// Canonical comparison operator.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum QueryComparisonOperator {
63    /// Equal.
64    Equal,
65    /// Not equal.
66    NotEqual,
67    /// Less than.
68    LessThan,
69    /// Less than or equal.
70    LessThanOrEqual,
71    /// Greater than.
72    GreaterThan,
73    /// Greater than or equal.
74    GreaterThanOrEqual,
75    /// Membership in a list.
76    In,
77    /// Non-membership in a list.
78    NotIn,
79    /// String/pattern match.
80    Like,
81    /// Null predicate; the boolean value selects `IS NULL` or `IS NOT NULL`.
82    IsNull,
83}
84
85impl QueryComparisonOperator {
86    /// Canonical label.
87    #[must_use]
88    pub const fn as_str(self) -> &'static str {
89        match self {
90            Self::Equal => "eq",
91            Self::NotEqual => "neq",
92            Self::LessThan => "lt",
93            Self::LessThanOrEqual => "lte",
94            Self::GreaterThan => "gt",
95            Self::GreaterThanOrEqual => "gte",
96            Self::In => "in",
97            Self::NotIn => "not-in",
98            Self::Like => "like",
99            Self::IsNull => "is-null",
100        }
101    }
102
103    /// Maps the adapter's standard GraphQL filter key to a canonical operator.
104    #[must_use]
105    pub fn from_graphql_key(value: &str) -> Option<Self> {
106        match value {
107            "eq" => Some(Self::Equal),
108            "neq" | "ne" => Some(Self::NotEqual),
109            "lt" => Some(Self::LessThan),
110            "lte" => Some(Self::LessThanOrEqual),
111            "gt" => Some(Self::GreaterThan),
112            "gte" => Some(Self::GreaterThanOrEqual),
113            "in" => Some(Self::In),
114            "notIn" => Some(Self::NotIn),
115            "like" => Some(Self::Like),
116            "isNull" => Some(Self::IsNull),
117            _ => None,
118        }
119    }
120}
121
122/// Scalar and composite literal used by filters and mutations.
123#[derive(Clone, Debug, PartialEq)]
124pub enum QueryValue {
125    /// GraphQL/JSON null.
126    Null,
127    /// Boolean literal.
128    Boolean(bool),
129    /// Integer literal.
130    Integer(i64),
131    /// Finite floating-point literal.
132    Float(f64),
133    /// String or enum literal. Canonical plans intentionally erase that
134    /// source-syntax distinction.
135    String(String),
136    /// Ordered list literal.
137    List(Vec<Self>),
138    /// Deterministically ordered object literal.
139    Object(BTreeMap<String, Self>),
140}
141
142impl QueryValue {
143    /// Converts to the JSON value used in canonical plans.
144    #[must_use]
145    pub fn to_json(&self) -> Value {
146        match self {
147            Self::Null => Value::Null,
148            Self::Boolean(value) => Value::Bool(*value),
149            Self::Integer(value) => Value::Number(Number::from(*value)),
150            Self::Float(value) => Value::Number(
151                Number::from_f64(*value).expect("query plan floating-point values must be finite"),
152            ),
153            Self::String(value) => Value::String(value.clone()),
154            Self::List(values) => Value::Array(values.iter().map(Self::to_json).collect()),
155            Self::Object(values) => Value::Object(
156                values
157                    .iter()
158                    .map(|(key, value)| (key.clone(), value.to_json()))
159                    .collect(),
160            ),
161        }
162    }
163}
164
165/// Canonical boolean filter tree.
166#[derive(Clone, Debug, PartialEq)]
167pub enum QueryFilter {
168    /// Field comparison.
169    Compare {
170        /// Canonical domain field.
171        field: String,
172        /// Comparison operator.
173        operator: QueryComparisonOperator,
174        /// Right-hand literal.
175        value: QueryValue,
176    },
177    /// All children must match.
178    And(Vec<Self>),
179    /// At least one child must match.
180    Or(Vec<Self>),
181    /// Child must not match.
182    Not(Box<Self>),
183}
184
185impl QueryFilter {
186    fn to_json(&self) -> Value {
187        match self {
188            Self::Compare {
189                field,
190                operator,
191                value,
192            } => json!({
193                "compare": {
194                    "field": field,
195                    "operator": operator.as_str(),
196                    "value": value.to_json(),
197                }
198            }),
199            Self::And(children) => {
200                json!({"and": children.iter().map(Self::to_json).collect::<Vec<_>>()})
201            }
202            Self::Or(children) => {
203                json!({"or": children.iter().map(Self::to_json).collect::<Vec<_>>()})
204            }
205            Self::Not(child) => json!({"not": child.to_json()}),
206        }
207    }
208}
209
210/// Sort direction.
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub enum QuerySortDirection {
213    /// Ascending.
214    Ascending,
215    /// Descending.
216    Descending,
217}
218
219impl QuerySortDirection {
220    /// Canonical label.
221    #[must_use]
222    pub const fn as_str(self) -> &'static str {
223        match self {
224            Self::Ascending => "asc",
225            Self::Descending => "desc",
226        }
227    }
228}
229
230/// Canonical ordering entry.
231#[derive(Clone, Debug, PartialEq, Eq)]
232pub struct QueryOrder {
233    pub(crate) field: String,
234    pub(crate) direction: QuerySortDirection,
235}
236
237impl QueryOrder {
238    /// Creates an ordering entry for a canonical field.
239    #[must_use]
240    pub fn new(field: impl Into<String>, direction: QuerySortDirection) -> Self {
241        Self {
242            field: field.into(),
243            direction,
244        }
245    }
246
247    /// Canonical field.
248    #[must_use]
249    pub fn field(&self) -> &str {
250        &self.field
251    }
252
253    /// Sort direction.
254    #[must_use]
255    pub const fn direction(&self) -> QuerySortDirection {
256        self.direction
257    }
258}
259
260/// Supported common aggregate.
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub enum QueryAggregateFunction {
263    /// Row/value count.
264    Count,
265    /// Sum.
266    Sum,
267    /// Arithmetic mean.
268    Average,
269    /// Minimum.
270    Minimum,
271    /// Maximum.
272    Maximum,
273    /// Population variance.
274    PopulationVariance,
275    /// Population standard deviation.
276    PopulationStandardDeviation,
277}
278
279impl QueryAggregateFunction {
280    /// Canonical label.
281    #[must_use]
282    pub const fn as_str(self) -> &'static str {
283        match self {
284            Self::Count => "count",
285            Self::Sum => "sum",
286            Self::Average => "avg",
287            Self::Minimum => "min",
288            Self::Maximum => "max",
289            Self::PopulationVariance => "variance-population",
290            Self::PopulationStandardDeviation => "stddev-population",
291        }
292    }
293
294    /// Parses a canonical aggregate label.
295    #[must_use]
296    pub fn parse(value: &str) -> Option<Self> {
297        match value {
298            "count" => Some(Self::Count),
299            "sum" => Some(Self::Sum),
300            "avg" => Some(Self::Average),
301            "min" => Some(Self::Minimum),
302            "max" => Some(Self::Maximum),
303            "variance-population" => Some(Self::PopulationVariance),
304            "stddev-population" => Some(Self::PopulationStandardDeviation),
305            _ => None,
306        }
307    }
308}
309
310/// Aggregate projection.
311#[derive(Clone, Debug, PartialEq, Eq)]
312pub struct QueryAggregate {
313    pub(crate) function: QueryAggregateFunction,
314    pub(crate) field: Option<String>,
315    pub(crate) alias: Option<String>,
316}
317
318impl QueryAggregate {
319    /// Creates an aggregate projection.
320    #[must_use]
321    pub const fn new(
322        function: QueryAggregateFunction,
323        field: Option<String>,
324        alias: Option<String>,
325    ) -> Self {
326        Self {
327            function,
328            field,
329            alias,
330        }
331    }
332
333    /// Aggregate function.
334    #[must_use]
335    pub const fn function(&self) -> QueryAggregateFunction {
336        self.function
337    }
338
339    /// Canonical input field, absent for `COUNT(*)`-style operations.
340    #[must_use]
341    pub fn field(&self) -> Option<&str> {
342        self.field.as_deref()
343    }
344
345    /// Result alias, when supplied by the frontend.
346    #[must_use]
347    pub fn alias(&self) -> Option<&str> {
348        self.alias.as_deref()
349    }
350}
351
352/// Source evidence attached to a canonical plan element.
353#[derive(Clone, Debug, PartialEq, Eq)]
354pub struct QuerySourceEvidence {
355    role: String,
356    span: SourceSpan,
357}
358
359impl QuerySourceEvidence {
360    /// Creates evidence connecting a semantic plan role to an exact source range.
361    #[must_use]
362    pub fn new(role: impl Into<String>, span: SourceSpan) -> Self {
363        Self {
364            role: role.into(),
365            span,
366        }
367    }
368
369    /// Semantic role evidenced by the source range.
370    #[must_use]
371    pub fn role(&self) -> &str {
372        &self.role
373    }
374
375    /// Exact source range.
376    #[must_use]
377    pub const fn span(&self) -> SourceSpan {
378        self.span
379    }
380}
381
382/// Language-neutral executable query plan.
383#[derive(Clone, Debug, PartialEq)]
384pub struct QueryPlan {
385    pub(crate) operation: QueryOperation,
386    pub(crate) resource: String,
387    pub(crate) projection: Vec<String>,
388    pub(crate) filter: Option<QueryFilter>,
389    pub(crate) order: Vec<QueryOrder>,
390    pub(crate) limit: Option<u64>,
391    pub(crate) offset: Option<u64>,
392    pub(crate) group_by: Vec<String>,
393    pub(crate) aggregates: Vec<QueryAggregate>,
394    pub(crate) mutation: BTreeMap<String, QueryValue>,
395    pub(crate) source_evidence: Vec<QuerySourceEvidence>,
396}
397
398impl QueryPlan {
399    /// Starts a plan for `operation` over the canonical `resource` name.
400    #[must_use]
401    pub fn new(operation: QueryOperation, resource: impl Into<String>) -> Self {
402        Self {
403            operation,
404            resource: resource.into(),
405            projection: Vec::new(),
406            filter: None,
407            order: Vec::new(),
408            limit: None,
409            offset: None,
410            group_by: Vec::new(),
411            aggregates: Vec::new(),
412            mutation: BTreeMap::new(),
413            source_evidence: Vec::new(),
414        }
415    }
416
417    /// Appends a canonical field to the response projection.
418    pub fn add_projection(&mut self, field: impl Into<String>) {
419        self.projection.push(field.into());
420    }
421
422    /// Replaces the boolean filter.
423    pub fn set_filter(&mut self, filter: QueryFilter) {
424        self.filter = Some(filter);
425    }
426
427    /// Appends an ordering entry.
428    pub fn add_order(&mut self, order: QueryOrder) {
429        self.order.push(order);
430    }
431
432    /// Sets result pagination.
433    pub const fn set_pagination(&mut self, limit: Option<u64>, offset: Option<u64>) {
434        self.limit = limit;
435        self.offset = offset;
436    }
437
438    /// Appends a canonical grouping field.
439    pub fn add_group_by(&mut self, field: impl Into<String>) {
440        self.group_by.push(field.into());
441    }
442
443    /// Appends an aggregate projection.
444    pub fn add_aggregate(&mut self, aggregate: QueryAggregate) {
445        self.aggregates.push(aggregate);
446    }
447
448    /// Sets a canonical mutation assignment.
449    pub fn set_mutation_value(&mut self, field: impl Into<String>, value: QueryValue) {
450        self.mutation.insert(field.into(), value);
451    }
452
453    /// Attaches source evidence to the plan.
454    pub fn add_source_evidence(&mut self, evidence: QuerySourceEvidence) {
455        self.source_evidence.push(evidence);
456    }
457
458    /// Operation.
459    #[must_use]
460    pub const fn operation(&self) -> QueryOperation {
461        self.operation
462    }
463
464    /// Canonical resource name.
465    #[must_use]
466    pub fn resource(&self) -> &str {
467        &self.resource
468    }
469
470    /// Canonical projected fields in response order.
471    #[must_use]
472    pub fn projection(&self) -> &[String] {
473        &self.projection
474    }
475
476    /// Boolean filter.
477    #[must_use]
478    pub const fn filter(&self) -> Option<&QueryFilter> {
479        self.filter.as_ref()
480    }
481
482    /// Ordering entries.
483    #[must_use]
484    pub fn order(&self) -> &[QueryOrder] {
485        &self.order
486    }
487
488    /// Maximum result count.
489    #[must_use]
490    pub const fn limit(&self) -> Option<u64> {
491        self.limit
492    }
493
494    /// Result offset.
495    #[must_use]
496    pub const fn offset(&self) -> Option<u64> {
497        self.offset
498    }
499
500    /// Canonical grouping fields.
501    #[must_use]
502    pub fn group_by(&self) -> &[String] {
503        &self.group_by
504    }
505
506    /// Aggregate projections.
507    #[must_use]
508    pub fn aggregates(&self) -> &[QueryAggregate] {
509        &self.aggregates
510    }
511
512    /// Canonical mutation assignments.
513    #[must_use]
514    pub const fn mutation(&self) -> &BTreeMap<String, QueryValue> {
515        &self.mutation
516    }
517
518    /// Source ranges retained by the frontend adapter.
519    #[must_use]
520    pub fn source_evidence(&self) -> &[QuerySourceEvidence] {
521        &self.source_evidence
522    }
523
524    /// Lowering validates meaning but never authorizes execution.
525    #[must_use]
526    pub const fn authorization(&self) -> QueryAuthorization {
527        QueryAuthorization::Required
528    }
529
530    /// Returns the provenance-free canonical JSON value used to compare plans
531    /// produced by different query languages.
532    #[must_use]
533    pub fn canonical_value(&self) -> Value {
534        let order = self
535            .order
536            .iter()
537            .map(|entry| {
538                json!({
539                    "field": entry.field,
540                    "direction": entry.direction.as_str(),
541                })
542            })
543            .collect::<Vec<_>>();
544        let aggregates = self
545            .aggregates
546            .iter()
547            .map(|aggregate| {
548                json!({
549                    "function": aggregate.function.as_str(),
550                    "field": aggregate.field,
551                    "alias": aggregate.alias,
552                })
553            })
554            .collect::<Vec<_>>();
555        let mutation = self
556            .mutation
557            .iter()
558            .map(|(field, value)| (field.clone(), value.to_json()))
559            .collect::<Map<_, _>>();
560
561        json!({
562            "version": QUERY_PLAN_VERSION,
563            "operation": self.operation.as_str(),
564            "resource": self.resource,
565            "projection": self.projection,
566            "filter": self.filter.as_ref().map(QueryFilter::to_json),
567            "order": order,
568            "limit": self.limit,
569            "offset": self.offset,
570            "groupBy": self.group_by,
571            "aggregates": aggregates,
572            "mutation": mutation,
573        })
574    }
575
576    /// Deterministic provenance-free serialization for cross-frontend
577    /// conformance checks.
578    #[must_use]
579    pub fn canonical_json(&self) -> String {
580        serde_json::to_string(&self.canonical_value()).expect("query plan values are serializable")
581    }
582}
583
584/// A canonical plan together with its provenance-connected links network.
585#[derive(Clone, Debug, PartialEq)]
586pub struct LoweredQueryPlan {
587    plan: QueryPlan,
588    network: LinkNetwork,
589    root_link: LinkId,
590}
591
592impl LoweredQueryPlan {
593    pub(crate) const fn new(plan: QueryPlan, network: LinkNetwork, root_link: LinkId) -> Self {
594        Self {
595            plan,
596            network,
597            root_link,
598        }
599    }
600
601    /// Canonical executable plan.
602    #[must_use]
603    pub const fn plan(&self) -> &QueryPlan {
604        &self.plan
605    }
606
607    /// Original source CST plus attached semantic plan links.
608    #[must_use]
609    pub const fn network(&self) -> &LinkNetwork {
610        &self.network
611    }
612
613    /// Root semantic link for the canonical plan.
614    #[must_use]
615    pub const fn root_link(&self) -> LinkId {
616        self.root_link
617    }
618
619    /// Splits the result into its public components.
620    #[must_use]
621    pub fn into_parts(self) -> (QueryPlan, LinkNetwork, LinkId) {
622        (self.plan, self.network, self.root_link)
623    }
624}
625
626pub(crate) fn attach_plan_links(
627    network: &mut LinkNetwork,
628    plan: &QueryPlan,
629    language: &str,
630) -> LinkId {
631    let cst_links = plan
632        .source_evidence
633        .iter()
634        .map(|evidence| closest_cst(network, evidence.span()))
635        .collect::<Vec<_>>();
636    let plan_concept = network.insert_point("executable-query-plan");
637    let mut references = vec![plan_concept];
638    for (evidence, cst) in plan.source_evidence.iter().zip(cst_links) {
639        let concept = network.insert_point(evidence.role());
640        let mut child_references = vec![concept];
641        if let Some(cst) = cst {
642            child_references.push(cst);
643        }
644        let child = network.insert_dynamic_link(
645            &child_references,
646            LinkMetadata::new()
647                .with_link_type(LinkType::Semantic)
648                .with_named(true)
649                .with_term(evidence.role())
650                .with_language(language)
651                .with_span(evidence.span()),
652        );
653        references.push(child);
654    }
655    if let Some(cst) = plan
656        .source_evidence
657        .first()
658        .and_then(|evidence| closest_cst(network, evidence.span()))
659    {
660        references.push(cst);
661    }
662    let root_span = plan
663        .source_evidence
664        .first()
665        .map(QuerySourceEvidence::span)
666        .expect("lowered plans always retain root evidence");
667    network.insert_dynamic_link(
668        &references,
669        LinkMetadata::new()
670            .with_link_type(LinkType::Semantic)
671            .with_named(true)
672            .with_term("executable-query-plan")
673            .with_language(language)
674            .with_span(root_span),
675    )
676}
677
678fn closest_cst(network: &LinkNetwork, span: SourceSpan) -> Option<LinkId> {
679    let target = span.byte_range();
680    network
681        .links()
682        .filter(|link| link.metadata().link_type() == Some(LinkType::Syntax))
683        .filter_map(|link| Some((link, link.metadata().span()?.byte_range())))
684        .filter(|(_, candidate)| {
685            candidate.start() <= target.start() && candidate.end() >= target.end()
686        })
687        .min_by_key(|(_, candidate)| candidate.end() - candidate.start())
688        .map(|(link, _)| Link::id(link))
689}