use std::collections::BTreeMap;
use serde_json::{json, Map, Number, Value};
use crate::link_network::{Link, LinkId, LinkMetadata, LinkNetwork, LinkType};
use crate::source::SourceSpan;
pub const QUERY_PLAN_VERSION: u8 = 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QueryAuthorization {
Required,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QueryOperation {
Select,
Insert,
Update,
Delete,
}
impl QueryOperation {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Select => "select",
Self::Insert => "insert",
Self::Update => "update",
Self::Delete => "delete",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value {
"select" => Some(Self::Select),
"insert" => Some(Self::Insert),
"update" => Some(Self::Update),
"delete" => Some(Self::Delete),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QueryComparisonOperator {
Equal,
NotEqual,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
In,
NotIn,
Like,
IsNull,
}
impl QueryComparisonOperator {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Equal => "eq",
Self::NotEqual => "neq",
Self::LessThan => "lt",
Self::LessThanOrEqual => "lte",
Self::GreaterThan => "gt",
Self::GreaterThanOrEqual => "gte",
Self::In => "in",
Self::NotIn => "not-in",
Self::Like => "like",
Self::IsNull => "is-null",
}
}
#[must_use]
pub fn from_graphql_key(value: &str) -> Option<Self> {
match value {
"eq" => Some(Self::Equal),
"neq" | "ne" => Some(Self::NotEqual),
"lt" => Some(Self::LessThan),
"lte" => Some(Self::LessThanOrEqual),
"gt" => Some(Self::GreaterThan),
"gte" => Some(Self::GreaterThanOrEqual),
"in" => Some(Self::In),
"notIn" => Some(Self::NotIn),
"like" => Some(Self::Like),
"isNull" => Some(Self::IsNull),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum QueryValue {
Null,
Boolean(bool),
Integer(i64),
Float(f64),
String(String),
List(Vec<Self>),
Object(BTreeMap<String, Self>),
}
impl QueryValue {
#[must_use]
pub fn to_json(&self) -> Value {
match self {
Self::Null => Value::Null,
Self::Boolean(value) => Value::Bool(*value),
Self::Integer(value) => Value::Number(Number::from(*value)),
Self::Float(value) => Value::Number(
Number::from_f64(*value).expect("query plan floating-point values must be finite"),
),
Self::String(value) => Value::String(value.clone()),
Self::List(values) => Value::Array(values.iter().map(Self::to_json).collect()),
Self::Object(values) => Value::Object(
values
.iter()
.map(|(key, value)| (key.clone(), value.to_json()))
.collect(),
),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum QueryFilter {
Compare {
field: String,
operator: QueryComparisonOperator,
value: QueryValue,
},
And(Vec<Self>),
Or(Vec<Self>),
Not(Box<Self>),
}
impl QueryFilter {
fn to_json(&self) -> Value {
match self {
Self::Compare {
field,
operator,
value,
} => json!({
"compare": {
"field": field,
"operator": operator.as_str(),
"value": value.to_json(),
}
}),
Self::And(children) => {
json!({"and": children.iter().map(Self::to_json).collect::<Vec<_>>()})
}
Self::Or(children) => {
json!({"or": children.iter().map(Self::to_json).collect::<Vec<_>>()})
}
Self::Not(child) => json!({"not": child.to_json()}),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QuerySortDirection {
Ascending,
Descending,
}
impl QuerySortDirection {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ascending => "asc",
Self::Descending => "desc",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QueryOrder {
pub(crate) field: String,
pub(crate) direction: QuerySortDirection,
}
impl QueryOrder {
#[must_use]
pub fn new(field: impl Into<String>, direction: QuerySortDirection) -> Self {
Self {
field: field.into(),
direction,
}
}
#[must_use]
pub fn field(&self) -> &str {
&self.field
}
#[must_use]
pub const fn direction(&self) -> QuerySortDirection {
self.direction
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QueryAggregateFunction {
Count,
Sum,
Average,
Minimum,
Maximum,
PopulationVariance,
PopulationStandardDeviation,
}
impl QueryAggregateFunction {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Count => "count",
Self::Sum => "sum",
Self::Average => "avg",
Self::Minimum => "min",
Self::Maximum => "max",
Self::PopulationVariance => "variance-population",
Self::PopulationStandardDeviation => "stddev-population",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value {
"count" => Some(Self::Count),
"sum" => Some(Self::Sum),
"avg" => Some(Self::Average),
"min" => Some(Self::Minimum),
"max" => Some(Self::Maximum),
"variance-population" => Some(Self::PopulationVariance),
"stddev-population" => Some(Self::PopulationStandardDeviation),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QueryAggregate {
pub(crate) function: QueryAggregateFunction,
pub(crate) field: Option<String>,
pub(crate) alias: Option<String>,
}
impl QueryAggregate {
#[must_use]
pub const fn new(
function: QueryAggregateFunction,
field: Option<String>,
alias: Option<String>,
) -> Self {
Self {
function,
field,
alias,
}
}
#[must_use]
pub const fn function(&self) -> QueryAggregateFunction {
self.function
}
#[must_use]
pub fn field(&self) -> Option<&str> {
self.field.as_deref()
}
#[must_use]
pub fn alias(&self) -> Option<&str> {
self.alias.as_deref()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QuerySourceEvidence {
role: String,
span: SourceSpan,
}
impl QuerySourceEvidence {
#[must_use]
pub fn new(role: impl Into<String>, span: SourceSpan) -> Self {
Self {
role: role.into(),
span,
}
}
#[must_use]
pub fn role(&self) -> &str {
&self.role
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.span
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct QueryPlan {
pub(crate) operation: QueryOperation,
pub(crate) resource: String,
pub(crate) projection: Vec<String>,
pub(crate) filter: Option<QueryFilter>,
pub(crate) order: Vec<QueryOrder>,
pub(crate) limit: Option<u64>,
pub(crate) offset: Option<u64>,
pub(crate) group_by: Vec<String>,
pub(crate) aggregates: Vec<QueryAggregate>,
pub(crate) mutation: BTreeMap<String, QueryValue>,
pub(crate) source_evidence: Vec<QuerySourceEvidence>,
}
impl QueryPlan {
#[must_use]
pub fn new(operation: QueryOperation, resource: impl Into<String>) -> Self {
Self {
operation,
resource: resource.into(),
projection: Vec::new(),
filter: None,
order: Vec::new(),
limit: None,
offset: None,
group_by: Vec::new(),
aggregates: Vec::new(),
mutation: BTreeMap::new(),
source_evidence: Vec::new(),
}
}
pub fn add_projection(&mut self, field: impl Into<String>) {
self.projection.push(field.into());
}
pub fn set_filter(&mut self, filter: QueryFilter) {
self.filter = Some(filter);
}
pub fn add_order(&mut self, order: QueryOrder) {
self.order.push(order);
}
pub const fn set_pagination(&mut self, limit: Option<u64>, offset: Option<u64>) {
self.limit = limit;
self.offset = offset;
}
pub fn add_group_by(&mut self, field: impl Into<String>) {
self.group_by.push(field.into());
}
pub fn add_aggregate(&mut self, aggregate: QueryAggregate) {
self.aggregates.push(aggregate);
}
pub fn set_mutation_value(&mut self, field: impl Into<String>, value: QueryValue) {
self.mutation.insert(field.into(), value);
}
pub fn add_source_evidence(&mut self, evidence: QuerySourceEvidence) {
self.source_evidence.push(evidence);
}
#[must_use]
pub const fn operation(&self) -> QueryOperation {
self.operation
}
#[must_use]
pub fn resource(&self) -> &str {
&self.resource
}
#[must_use]
pub fn projection(&self) -> &[String] {
&self.projection
}
#[must_use]
pub const fn filter(&self) -> Option<&QueryFilter> {
self.filter.as_ref()
}
#[must_use]
pub fn order(&self) -> &[QueryOrder] {
&self.order
}
#[must_use]
pub const fn limit(&self) -> Option<u64> {
self.limit
}
#[must_use]
pub const fn offset(&self) -> Option<u64> {
self.offset
}
#[must_use]
pub fn group_by(&self) -> &[String] {
&self.group_by
}
#[must_use]
pub fn aggregates(&self) -> &[QueryAggregate] {
&self.aggregates
}
#[must_use]
pub const fn mutation(&self) -> &BTreeMap<String, QueryValue> {
&self.mutation
}
#[must_use]
pub fn source_evidence(&self) -> &[QuerySourceEvidence] {
&self.source_evidence
}
#[must_use]
pub const fn authorization(&self) -> QueryAuthorization {
QueryAuthorization::Required
}
#[must_use]
pub fn canonical_value(&self) -> Value {
let order = self
.order
.iter()
.map(|entry| {
json!({
"field": entry.field,
"direction": entry.direction.as_str(),
})
})
.collect::<Vec<_>>();
let aggregates = self
.aggregates
.iter()
.map(|aggregate| {
json!({
"function": aggregate.function.as_str(),
"field": aggregate.field,
"alias": aggregate.alias,
})
})
.collect::<Vec<_>>();
let mutation = self
.mutation
.iter()
.map(|(field, value)| (field.clone(), value.to_json()))
.collect::<Map<_, _>>();
json!({
"version": QUERY_PLAN_VERSION,
"operation": self.operation.as_str(),
"resource": self.resource,
"projection": self.projection,
"filter": self.filter.as_ref().map(QueryFilter::to_json),
"order": order,
"limit": self.limit,
"offset": self.offset,
"groupBy": self.group_by,
"aggregates": aggregates,
"mutation": mutation,
})
}
#[must_use]
pub fn canonical_json(&self) -> String {
serde_json::to_string(&self.canonical_value()).expect("query plan values are serializable")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LoweredQueryPlan {
plan: QueryPlan,
network: LinkNetwork,
root_link: LinkId,
}
impl LoweredQueryPlan {
pub(crate) const fn new(plan: QueryPlan, network: LinkNetwork, root_link: LinkId) -> Self {
Self {
plan,
network,
root_link,
}
}
#[must_use]
pub const fn plan(&self) -> &QueryPlan {
&self.plan
}
#[must_use]
pub const fn network(&self) -> &LinkNetwork {
&self.network
}
#[must_use]
pub const fn root_link(&self) -> LinkId {
self.root_link
}
#[must_use]
pub fn into_parts(self) -> (QueryPlan, LinkNetwork, LinkId) {
(self.plan, self.network, self.root_link)
}
}
pub(crate) fn attach_plan_links(
network: &mut LinkNetwork,
plan: &QueryPlan,
language: &str,
) -> LinkId {
let cst_links = plan
.source_evidence
.iter()
.map(|evidence| closest_cst(network, evidence.span()))
.collect::<Vec<_>>();
let plan_concept = network.insert_point("executable-query-plan");
let mut references = vec![plan_concept];
for (evidence, cst) in plan.source_evidence.iter().zip(cst_links) {
let concept = network.insert_point(evidence.role());
let mut child_references = vec![concept];
if let Some(cst) = cst {
child_references.push(cst);
}
let child = network.insert_dynamic_link(
&child_references,
LinkMetadata::new()
.with_link_type(LinkType::Semantic)
.with_named(true)
.with_term(evidence.role())
.with_language(language)
.with_span(evidence.span()),
);
references.push(child);
}
if let Some(cst) = plan
.source_evidence
.first()
.and_then(|evidence| closest_cst(network, evidence.span()))
{
references.push(cst);
}
let root_span = plan
.source_evidence
.first()
.map(QuerySourceEvidence::span)
.expect("lowered plans always retain root evidence");
network.insert_dynamic_link(
&references,
LinkMetadata::new()
.with_link_type(LinkType::Semantic)
.with_named(true)
.with_term("executable-query-plan")
.with_language(language)
.with_span(root_span),
)
}
fn closest_cst(network: &LinkNetwork, span: SourceSpan) -> Option<LinkId> {
let target = span.byte_range();
network
.links()
.filter(|link| link.metadata().link_type() == Some(LinkType::Syntax))
.filter_map(|link| Some((link, link.metadata().span()?.byte_range())))
.filter(|(_, candidate)| {
candidate.start() <= target.start() && candidate.end() >= target.end()
})
.min_by_key(|(_, candidate)| candidate.end() - candidate.start())
.map(|(link, _)| Link::id(link))
}