pub mod backend;
pub mod bulk;
pub mod error;
pub mod ir;
pub mod lower;
pub mod schema;
pub mod spec;
pub mod vocab;
pub mod write;
pub use backend::SqlDialect;
pub use error::QueryError;
pub use lower::Params;
pub use schema::EntityRegistry;
use crate::config::QueryConfig;
use ir::Cond;
use sea_query::SelectStatement;
use serde_json::Value as Json;
#[derive(Debug, Clone)]
pub struct SqlPlan {
pub main: SelectStatement,
pub includes: Vec<IncludePlan>,
pub strip: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct IncludePlan {
pub field: String,
pub target_table: String,
pub local: String,
pub foreign: String,
pub fields: Vec<String>,
pub sort: Vec<spec::SortKey>,
pub limit: u64,
}
impl IncludePlan {
pub fn projection(&self) -> Vec<String> {
if self.fields.is_empty() {
return Vec::new();
}
let mut cols = self.fields.clone();
for extra in std::iter::once(&self.foreign).chain(self.sort.iter().map(|k| &k.field)) {
if !cols.contains(extra) {
cols.push(extra.clone());
}
}
cols
}
pub fn strip(&self) -> Vec<String> {
self.projection()
.into_iter()
.filter(|c| !self.fields.contains(c))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GroupKey {
Bool(bool),
Int(i64),
Float(u64),
Str(String),
}
impl GroupKey {
pub fn from_json(v: &Json) -> Option<Self> {
match v {
Json::Bool(b) => Some(GroupKey::Bool(*b)),
Json::Number(n) => match n.as_i64() {
Some(i) => Some(GroupKey::Int(i)),
None => n.as_f64().map(Self::from_f64),
},
Json::String(s) => Some(match s.parse::<i64>() {
Ok(i) if i.to_string() == *s => GroupKey::Int(i),
_ => GroupKey::Str(s.clone()),
}),
_ => None,
}
}
fn from_f64(f: f64) -> Self {
if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
GroupKey::Int(f as i64)
} else {
GroupKey::Float(f.to_bits())
}
}
}
fn prepare(
query: &Json,
params: &Params,
reg: &EntityRegistry,
) -> Result<(spec::QuerySpec, Cond, String), QueryError> {
let spec = spec::parse(query)?;
let table = reg.physical_table(&spec.source)?;
let cond = match &spec.filter {
Some(f) => lower::lower_with(f, params, reg, &spec.source)?,
None => Cond::True,
};
let spec = spec.resolve_names(reg)?;
Ok((spec, cond, table))
}
pub fn plan_sql(
query: &Json,
params: &Params,
reg: &EntityRegistry,
dialect: SqlDialect,
limits: &QueryConfig,
) -> Result<SqlPlan, QueryError> {
let (mut spec, cond, root_table) = prepare(query, params, reg)?;
let include_specs = std::mem::take(&mut spec.include);
let mut includes = Vec::new();
let mut strip: Vec<String> = Vec::new();
for inc in &include_specs {
let (rel, _target) = reg.resolve_relation(&spec.source, &inc.relation, "include")?;
if rel.through.is_some() {
return Err(QueryError::FeatureUnsupportedByTarget {
feature: format!("many-to-many include '{}'", inc.relation),
target: "sql".to_string(),
});
}
if inc.sort.is_empty() {
return Err(QueryError::InvalidEnvelope(format!(
"include.{} requires a 'sort' — the per-parent page needs a \
deterministic order key (e.g. \"sort\": [{{\"id\": \"asc\"}}])",
inc.relation
)));
}
if !spec.fields.is_empty() && !spec.fields.iter().any(|f| f == &rel.local) {
spec.fields.push(rel.local.clone());
if !strip.contains(&rel.local) {
strip.push(rel.local.clone());
}
}
let limit = backend::resolve_limit(inc.limit, limits)?;
includes.push(IncludePlan {
field: inc.relation.clone(),
target_table: rel.target_table,
local: rel.local,
foreign: rel.foreign,
fields: inc.fields.clone(),
sort: inc.sort.clone(),
limit,
});
}
let main = backend::sql::render(&spec, &cond, &root_table, dialect, limits)?;
Ok(SqlPlan {
main,
includes,
strip,
})
}
pub fn translate_mongo(
query: &Json,
params: &Params,
reg: &EntityRegistry,
limits: &QueryConfig,
) -> Result<backend::mongo::MongoQuery, QueryError> {
let (spec, cond, collection) = prepare(query, params, reg)?;
backend::mongo::render(&spec, &cond, &collection, limits)
}
pub fn translate_es(
query: &Json,
params: &Params,
reg: &EntityRegistry,
limits: &QueryConfig,
) -> Result<backend::es::EsQuery, QueryError> {
let (spec, cond, index) = prepare(query, params, reg)?;
backend::es::render(&spec, &cond, &index, limits)
}
#[cfg(test)]
mod group_key_tests {
use super::GroupKey;
use serde_json::json;
#[test]
fn integral_keys_group_regardless_of_how_the_driver_rendered_them() {
let expected = Some(GroupKey::Int(7));
assert_eq!(GroupKey::from_json(&json!(7)), expected);
assert_eq!(GroupKey::from_json(&json!(7.0)), expected);
assert_eq!(GroupKey::from_json(&json!("7")), expected);
assert_eq!(GroupKey::from_json(&json!(-7)), Some(GroupKey::Int(-7)));
assert_eq!(GroupKey::from_json(&json!("-7")), Some(GroupKey::Int(-7)));
}
#[test]
fn text_that_merely_parses_as_a_number_keeps_its_own_identity() {
assert_eq!(
GroupKey::from_json(&json!("007")),
Some(GroupKey::Str("007".into()))
);
assert_eq!(
GroupKey::from_json(&json!(" 7")),
Some(GroupKey::Str(" 7".into()))
);
assert_eq!(
GroupKey::from_json(&json!("+7")),
Some(GroupKey::Str("+7".into()))
);
assert_ne!(
GroupKey::from_json(&json!("u1")),
GroupKey::from_json(&json!("u2"))
);
}
#[test]
fn non_joinable_values_have_no_key() {
for v in [json!(null), json!([1]), json!({ "a": 1 })] {
assert_eq!(GroupKey::from_json(&v), None, "{v}");
}
}
#[test]
fn booleans_and_fractional_numbers_keep_their_own_variants() {
assert_eq!(
GroupKey::from_json(&json!(true)),
Some(GroupKey::Bool(true))
);
assert_ne!(
GroupKey::from_json(&json!(1.5)),
GroupKey::from_json(&json!(1))
);
assert_eq!(
GroupKey::from_json(&json!(1.5)),
GroupKey::from_json(&json!(1.5))
);
}
}