use serde_json::{json, Map, Value};
use crate::bson::id_key;
use crate::computes::{collect_field_deps, merge_depends_into_ast, InjectInfo};
use crate::permission::{can_read_schema, is_relation_readable, merge_owner_condition, Context};
use crate::pipeline::{
build_pipeline, build_pipeline_projection, build_projection, flatten_object_fields, is_nullish,
param, parse_gql, Ast, RelAst, REL_PRED_PREFIX,
};
use crate::schema::{Registry, Schema};
use crate::types::{is_truthy, validate_pipeline_stages};
use super::cmd::{cmd_aggregate, cmd_find, num_value, to_number};
use super::{ensure_context, ERR_PERMISSION, MAX_PAGE_SIZE, PHASE1_IDS};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Page {
pub page: f64,
pub page_size: f64,
}
impl Page {
pub fn to_value(&self) -> Value {
json!({
"page": num_value(self.page),
"pageSize": num_value(self.page_size),
})
}
pub fn has_more(&self, total: f64) -> bool {
(self.page + 1.0) * self.page_size < total
}
}
pub fn resolve_page(ast: &Ast, params: &Map<String, Value>) -> Page {
let (page, page_size) = if params.contains_key("page") || params.contains_key("pageSize") {
let page = match params.get("page").filter(|v| !v.is_null()) {
Some(v) => to_number(Some(v)).max(0.0).floor(),
None => 0.0,
};
let page_size = match params.get("pageSize").filter(|v| !v.is_null()) {
Some(v) => to_number(Some(v)),
None => 50.0,
};
(page, page_size)
} else {
let skip_val = param(params, ast.params.get("skip"));
let limit_val = param(params, ast.params.get("limit"));
let page = if !is_nullish(skip_val) && is_truthy(limit_val.unwrap_or(&Value::Null)) {
(to_number(skip_val) / to_number(limit_val)).floor()
} else {
0.0
};
let page_size = if is_nullish(limit_val) {
50.0
} else {
to_number(limit_val)
};
(page, page_size)
};
Page {
page,
page_size: if page_size < MAX_PAGE_SIZE {
page_size
} else {
MAX_PAGE_SIZE
},
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Mode {
Find,
TwoPhase,
Aggregate,
}
impl Mode {
pub fn as_str(self) -> &'static str {
match self {
Mode::Find => "find",
Mode::TwoPhase => "two_phase",
Mode::Aggregate => "aggregate",
}
}
}
#[derive(Debug, Clone)]
pub struct QueryPlan {
pub collection: String,
pub mode: Mode,
pub commands: Vec<Value>,
pub postprocess: Option<Value>,
pub sort: Option<Value>,
}
impl QueryPlan {
pub fn to_value(&self) -> Value {
json!({
"collection": self.collection,
"mode": self.mode.as_str(),
"commands": self.commands,
"postprocess": self.postprocess.clone().unwrap_or(Value::Null),
"sort": self.sort.clone().unwrap_or(Value::Null),
})
}
}
fn postprocess_value(ast: &Ast, inject: &InjectInfo) -> Value {
json!({
"ast": ast.to_value(),
"inject": if inject.is_empty() { Value::Null } else { inject.to_value() },
})
}
pub fn has_pipeline(ast: &Ast, params: &Map<String, Value>) -> bool {
ast.params
.get("pipeline")
.map(|r| !is_nullish(param(params, Some(r))))
.unwrap_or(false)
}
pub fn plan_query(
gql: &str,
params: &Map<String, Value>,
registry: &Registry,
ctx: Option<&Context>,
) -> Result<QueryPlan, String> {
let mut params = params.clone();
plan_query_mut(gql, &mut params, registry, ctx)
}
pub fn plan_query_mut(
gql: &str,
params: &mut Map<String, Value>,
registry: &Registry,
ctx: Option<&Context>,
) -> Result<QueryPlan, String> {
let ast = parse_gql(gql)?;
plan_query_ast_mut(ast, params, registry, ctx)
}
pub fn plan_query_one(
gql: &str,
params: &Map<String, Value>,
registry: &Registry,
ctx: Option<&Context>,
) -> Result<QueryPlan, String> {
let mut params = params.clone();
let mut ast = parse_gql(gql)?;
if !ast.params.contains_key("limit") && !has_pipeline(&ast, ¶ms) {
ast.params
.insert("limit".to_string(), "@__core_one_limit__".to_string());
params.insert("__core_one_limit__".to_string(), json!(1));
}
plan_query_ast_mut(ast, &mut params, registry, ctx)
}
pub fn plan_query_ast_mut(
mut ast: Ast,
params: &mut Map<String, Value>,
registry: &Registry,
ctx: Option<&Context>,
) -> Result<QueryPlan, String> {
ensure_context(registry, ctx)?;
let schema = registry.get(&ast.model)?;
if ctx.is_some() && !can_read_schema(schema, ctx) {
return Err(ERR_PERMISSION.to_string());
}
if ctx.is_some() {
if let Some(r) = ast.params.get("condition").cloned() {
let key = r.get(1..).unwrap_or("").to_string();
match merge_owner_condition(schema, ctx, params.get(&key).cloned()) {
Some(v) => {
params.insert(key, v);
}
None => {
params.remove(&key);
}
}
} else if let Some(owner) = merge_owner_condition(schema, ctx, None) {
ast.params
.insert("condition".to_string(), "@__core_owner__".to_string());
params.insert("__core_owner__".to_string(), owner);
}
}
let has_pipeline = has_pipeline(&ast, params);
let mut inject = if has_pipeline {
InjectInfo::default()
} else {
merge_depends_into_ast(&mut ast.relations, schema)?
};
if !has_pipeline {
inject.fields = collect_field_deps(&mut ast.fields, schema);
}
let mut post_ast = ast.clone();
if !has_pipeline {
flatten_object_fields(&mut post_ast, schema);
}
build_plan(ast, &post_ast, &inject, params, registry, ctx)
}
pub fn check_readable_relations(
relations: &[(String, RelAst)],
schema: &Schema,
registry: &Registry,
ctx: Option<&Context>,
) -> Result<(), String> {
if ctx.is_none() {
return Ok(());
}
for (rel_name, rel_ast) in relations {
let Some(rel_def) = schema.relations.get(rel_name) else {
continue;
};
if rel_def.model.is_empty() {
continue;
}
if !is_relation_readable(schema, ctx, rel_name) {
return Err(ERR_PERMISSION.to_string());
}
let rel_schema = registry.get(&rel_def.model)?;
if !can_read_schema(rel_schema, ctx) {
return Err(ERR_PERMISSION.to_string());
}
check_readable_relations(&rel_ast.relations, rel_schema, registry, ctx)?;
}
Ok(())
}
pub fn build_plan(
mut fetch_ast: Ast,
post_ast: &Ast,
inject: &InjectInfo,
params: &Map<String, Value>,
registry: &Registry,
ctx: Option<&Context>,
) -> Result<QueryPlan, String> {
ensure_context(registry, ctx)?;
let schema = registry.get(&fetch_ast.model)?;
check_readable_relations(&fetch_ast.relations, schema, registry, ctx)?;
let has_pipeline = has_pipeline(&fetch_ast, params);
let pipeline = build_pipeline(&mut fetch_ast, params, registry, ctx)?;
let stages: Vec<Value> = pipeline.as_array().cloned().unwrap_or_default();
let grouped = stages.iter().any(|s| s.get("$group").is_some());
let projection = if has_pipeline {
build_pipeline_projection(&fetch_ast, schema)
} else if grouped {
None
} else {
build_projection(&fetch_ast, schema, ctx)
};
let collection = schema.collection.clone();
let post = || {
if has_pipeline || grouped {
None
} else {
Some(postprocess_value(post_ast, inject))
}
};
if !has_pipeline && stages.len() == 1 {
if let Some(filter) = stages[0].get("$match") {
return Ok(QueryPlan {
collection: collection.clone(),
mode: Mode::Find,
commands: vec![cmd_find(schema, filter, projection.as_ref())],
postprocess: post(),
sort: None,
});
}
}
let first_lookup = stages.iter().position(|s| {
s.get("$lookup")
.map(|lo| {
!lo.get("as")
.and_then(|v| v.as_str())
.map(|a| a.starts_with(REL_PRED_PREFIX))
.unwrap_or(false)
})
.unwrap_or(false)
});
let has_skip_limit = !has_pipeline
&& stages
.iter()
.any(|s| s.get("$skip").is_some() || s.get("$limit").is_some());
let sort_stage = stages.iter().find(|s| s.get("$sort").is_some()).cloned();
if let Some(idx) = first_lookup {
if !grouped && has_skip_limit && !sorts_by_relation(sort_stage.as_ref()) {
let mut id_pipeline: Vec<Value> = stages[..idx].to_vec();
for key in ["$sort", "$skip", "$limit"] {
if let Some(st) = stages.iter().find(|s| s.get(key).is_some()) {
id_pipeline.push(st.clone());
}
}
id_pipeline.push(json!({ "$project": { "_id": 1 } }));
let mut full: Vec<Value> = stages[idx..]
.iter()
.filter(|s| {
!(s.get("$sort").is_some()
|| s.get("$skip").is_some()
|| s.get("$limit").is_some())
})
.cloned()
.collect();
full.insert(0, json!({ "$match": { "_id": { "$in": PHASE1_IDS } } }));
if let Some(p) = projection.as_ref() {
full.push(json!({ "$project": p }));
}
return Ok(QueryPlan {
collection: collection.clone(),
mode: Mode::TwoPhase,
commands: vec![
cmd_aggregate(schema, &id_pipeline),
cmd_aggregate(schema, &full),
],
postprocess: post(),
sort: sort_stage,
});
}
}
let mut final_stages = stages;
if let Some(p) = projection.as_ref() {
final_stages.push(json!({ "$project": p }));
}
validate_pipeline_stages(&final_stages)?;
Ok(QueryPlan {
collection,
mode: Mode::Aggregate,
commands: vec![cmd_aggregate(schema, &final_stages)],
postprocess: post(),
sort: None,
})
}
pub fn sorts_by_relation(sort_stage: Option<&Value>) -> bool {
sort_stage
.and_then(|s| s.get("$sort"))
.and_then(|s| s.as_object())
.map(|o| o.keys().any(|k| k.contains('.')))
.unwrap_or(false)
}
pub fn restore_sort_order(items: &mut [Value], ids: &[Value], sort_stage: Option<&Value>) {
if sort_stage.is_none() || ids.len() <= 1 {
return;
}
let mut index: std::collections::HashMap<_, usize> =
std::collections::HashMap::with_capacity(ids.len());
for (i, id) in ids.iter().enumerate() {
index.entry(id_key(id)).or_insert(i);
}
let keyed: Vec<(usize, Value)> = items
.iter()
.map(|it| {
let k = id_key(it.get("_id").unwrap_or(&Value::Null));
let pos = index.get(&k).copied().unwrap_or(ids.len());
(pos, it.clone())
})
.collect();
let mut sorted = keyed;
sorted.sort_by_key(|(pos, _)| *pos);
for (slot, (_, v)) in items.iter_mut().zip(sorted) {
*slot = v;
}
}