use std::marker::PhantomData;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueryDecl {
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub doc: String,
pub bindings: Vec<BindingDecl>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filter: Option<BoolExpr>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub group_by: Vec<ScalarExpr>,
pub columns: Vec<OutputColumn>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub order_by: Vec<QueryOrder>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BindingDecl {
pub id: u32,
pub kind: String,
pub source: BindingSource,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BindingSource {
Root,
Follow {
from: u32,
field: String,
direction: EdgeDirection,
#[serde(default = "yes")]
required: bool,
},
}
fn yes() -> bool {
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EdgeDirection {
Out,
In,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FieldRef {
pub binding: u32,
pub field: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScalarExpr {
Field(FieldRef),
Literal(TypedLiteral),
Aggregate {
op: AggregateOp,
#[serde(default, skip_serializing_if = "Option::is_none")]
expr: Option<Box<ScalarExpr>>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TypedLiteral {
String(String),
Int(i64),
Decimal(String),
Date(NaiveDate),
Bool(bool),
Enum { type_name: String, variant: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AggregateOp {
Count,
CountDistinct,
Sum,
Min,
Max,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BoolExpr {
And(Vec<BoolExpr>),
Or(Vec<BoolExpr>),
Not(Box<BoolExpr>),
Compare {
left: ScalarExpr,
op: CompareOp,
right: ScalarExpr,
},
}
impl BoolExpr {
pub fn and(self, other: BoolExpr) -> BoolExpr {
match self {
BoolExpr::And(mut xs) => {
xs.push(other);
BoolExpr::And(xs)
}
one => BoolExpr::And(vec![one, other]),
}
}
pub fn or(self, other: BoolExpr) -> BoolExpr {
match self {
BoolExpr::Or(mut xs) => {
xs.push(other);
BoolExpr::Or(xs)
}
one => BoolExpr::Or(vec![one, other]),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompareOp {
Eq,
Ne,
Gt,
Gte,
Lt,
Lte,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutputColumn {
pub name: String,
pub expr: ScalarExpr,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<ColumnFormat>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ColumnFormat {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub thousands: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub decimals: Option<u8>,
}
impl ColumnFormat {
pub fn money(prefix: impl Into<String>) -> ColumnFormat {
ColumnFormat {
prefix: Some(prefix.into()),
suffix: None,
thousands: true,
decimals: Some(2),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueryOrder {
pub expr: ScalarExpr,
pub direction: SortDirection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SortDirection {
Asc,
Desc,
}
pub trait QueryModel: Sized {
type Fields;
const KIND: &'static str;
fn fields(binding: u32) -> Self::Fields;
}
#[derive(Debug)]
pub struct FieldExpr<T> {
field: FieldRef,
marker: PhantomData<fn() -> T>,
}
impl<T> Clone for FieldExpr<T> {
fn clone(&self) -> Self {
Self {
field: self.field.clone(),
marker: PhantomData,
}
}
}
impl<T> FieldExpr<T> {
pub fn new(binding: u32, field: impl Into<String>) -> Self {
Self {
field: FieldRef {
binding,
field: field.into(),
},
marker: PhantomData,
}
}
pub fn expr(&self) -> ScalarExpr {
ScalarExpr::Field(self.field.clone())
}
fn compare(&self, op: CompareOp, value: T) -> BoolExpr
where
T: QueryLiteral,
{
BoolExpr::Compare {
left: self.expr(),
op,
right: ScalarExpr::Literal(value.into_query_literal()),
}
}
pub fn eq(&self, value: T) -> BoolExpr
where
T: QueryLiteral,
{
self.compare(CompareOp::Eq, value)
}
pub fn ne(&self, value: T) -> BoolExpr
where
T: QueryLiteral,
{
self.compare(CompareOp::Ne, value)
}
pub fn gt(&self, value: T) -> BoolExpr
where
T: QueryLiteral,
{
self.compare(CompareOp::Gt, value)
}
pub fn gte(&self, value: T) -> BoolExpr
where
T: QueryLiteral,
{
self.compare(CompareOp::Gte, value)
}
pub fn lt(&self, value: T) -> BoolExpr
where
T: QueryLiteral,
{
self.compare(CompareOp::Lt, value)
}
pub fn lte(&self, value: T) -> BoolExpr
where
T: QueryLiteral,
{
self.compare(CompareOp::Lte, value)
}
pub fn binding(&self) -> u32 {
self.field.binding
}
}
pub trait QueryLiteral {
fn into_query_literal(self) -> TypedLiteral;
}
impl QueryLiteral for String {
fn into_query_literal(self) -> TypedLiteral {
TypedLiteral::String(self)
}
}
impl QueryLiteral for &str {
fn into_query_literal(self) -> TypedLiteral {
TypedLiteral::String(self.to_string())
}
}
impl QueryLiteral for i64 {
fn into_query_literal(self) -> TypedLiteral {
TypedLiteral::Int(self)
}
}
impl QueryLiteral for bool {
fn into_query_literal(self) -> TypedLiteral {
TypedLiteral::Bool(self)
}
}
impl QueryLiteral for NaiveDate {
fn into_query_literal(self) -> TypedLiteral {
TypedLiteral::Date(self)
}
}
pub struct QueryBuilder {
query: QueryDecl,
next_binding: u32,
}
impl QueryBuilder {
pub fn new(name: impl Into<String>, doc: impl Into<String>) -> Self {
Self {
query: QueryDecl {
name: name.into(),
doc: doc.into(),
bindings: vec![],
filter: None,
group_by: vec![],
columns: vec![],
order_by: vec![],
limit: None,
},
next_binding: 0,
}
}
pub fn from<M: QueryModel>(&mut self) -> M::Fields {
let id = self.next_binding;
self.next_binding += 1;
self.query.bindings.push(BindingDecl {
id,
kind: M::KIND.into(),
source: BindingSource::Root,
});
M::fields(id)
}
pub fn filter(&mut self, expr: BoolExpr) {
self.query.filter = Some(expr);
}
pub fn group_by<T>(&mut self, field: &FieldExpr<T>) {
self.query.group_by.push(field.expr());
}
pub fn field<T>(&mut self, name: impl Into<String>, field: &FieldExpr<T>) -> ColumnHandle<'_> {
self.push_column(name, field.expr())
}
fn push_column(&mut self, name: impl Into<String>, expr: ScalarExpr) -> ColumnHandle<'_> {
self.query.columns.push(OutputColumn {
name: name.into(),
expr,
format: None,
});
ColumnHandle {
column: self.query.columns.last_mut().expect("just pushed"),
}
}
pub fn count(&mut self, name: impl Into<String>) -> ColumnHandle<'_> {
self.push_column(
name,
ScalarExpr::Aggregate {
op: AggregateOp::Count,
expr: None,
},
)
}
pub fn count_of<T>(
&mut self,
name: impl Into<String>,
field: &FieldExpr<T>,
) -> ColumnHandle<'_> {
self.aggregate(name, AggregateOp::Count, field)
}
pub fn count_distinct<T>(
&mut self,
name: impl Into<String>,
field: &FieldExpr<T>,
) -> ColumnHandle<'_> {
self.aggregate(name, AggregateOp::CountDistinct, field)
}
pub fn sum<T>(&mut self, name: impl Into<String>, field: &FieldExpr<T>) -> ColumnHandle<'_> {
self.aggregate(name, AggregateOp::Sum, field)
}
pub fn min<T>(&mut self, name: impl Into<String>, field: &FieldExpr<T>) -> ColumnHandle<'_> {
self.aggregate(name, AggregateOp::Min, field)
}
pub fn max<T>(&mut self, name: impl Into<String>, field: &FieldExpr<T>) -> ColumnHandle<'_> {
self.aggregate(name, AggregateOp::Max, field)
}
fn aggregate<T>(
&mut self,
name: impl Into<String>,
op: AggregateOp,
field: &FieldExpr<T>,
) -> ColumnHandle<'_> {
self.push_column(
name,
ScalarExpr::Aggregate {
op,
expr: Some(Box::new(field.expr())),
},
)
}
pub fn follow_in<M: QueryModel>(
&mut self,
anchor: u32,
field: impl Into<String>,
required: bool,
) -> M::Fields {
self.follow::<M>(anchor, field, EdgeDirection::In, required)
}
pub fn follow_out<M: QueryModel>(
&mut self,
anchor: u32,
field: impl Into<String>,
required: bool,
) -> M::Fields {
self.follow::<M>(anchor, field, EdgeDirection::Out, required)
}
fn follow<M: QueryModel>(
&mut self,
anchor: u32,
field: impl Into<String>,
direction: EdgeDirection,
required: bool,
) -> M::Fields {
let id = self.next_binding;
self.next_binding += 1;
self.query.bindings.push(BindingDecl {
id,
kind: M::KIND.into(),
source: BindingSource::Follow {
from: anchor,
field: field.into(),
direction,
required,
},
});
M::fields(id)
}
pub fn order_by<T>(&mut self, field: &FieldExpr<T>, direction: SortDirection) {
self.query.order_by.push(QueryOrder {
expr: field.expr(),
direction,
});
}
pub fn order_by_column(&mut self, name: &str, direction: SortDirection) {
if let Some(col) = self.query.columns.iter().find(|c| c.name == name) {
self.query.order_by.push(QueryOrder {
expr: col.expr.clone(),
direction,
});
}
}
pub fn limit(&mut self, limit: usize) {
self.query.limit = Some(limit);
}
pub fn finish(self) -> QueryDecl {
self.query
}
}
pub struct ColumnHandle<'a> {
column: &'a mut OutputColumn,
}
impl ColumnHandle<'_> {
pub fn format(self, format: ColumnFormat) -> Self {
self.column.format = Some(format);
self
}
pub fn money(self, prefix: impl Into<String>) -> Self {
self.format(ColumnFormat::money(prefix))
}
}
pub fn query(
name: impl Into<String>,
doc: impl Into<String>,
build: impl FnOnce(&mut QueryBuilder),
) -> QueryDecl {
let mut q = QueryBuilder::new(name, doc);
build(&mut q);
q.finish()
}