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