1use serde_json::{json, Map, Value};
4
5use crate::bson::id_key;
6use crate::computes::{collect_field_deps, merge_depends_into_ast, InjectInfo};
7use crate::permission::{can_read_schema, is_relation_readable, merge_owner_condition, Context};
8use crate::pipeline::{
9 build_pipeline, build_pipeline_projection, build_projection, flatten_object_fields, is_nullish,
10 param, parse_gql, Ast, RelAst, REL_PRED_PREFIX,
11};
12use crate::schema::{Registry, Schema};
13use crate::types::{is_truthy, validate_pipeline_stages};
14
15use super::cmd::{cmd_aggregate, cmd_find, num_value, to_number};
16use super::{ensure_context, ERR_PERMISSION, MAX_PAGE_SIZE, PHASE1_IDS};
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct Page {
20 pub page: f64,
21 pub page_size: f64,
22}
23
24impl Page {
25 pub fn to_value(&self) -> Value {
26 json!({
27 "page": num_value(self.page),
28 "pageSize": num_value(self.page_size),
29 })
30 }
31
32 pub fn has_more(&self, total: f64) -> bool {
34 (self.page + 1.0) * self.page_size < total
35 }
36}
37
38pub fn resolve_page(ast: &Ast, params: &Map<String, Value>) -> Page {
40 let (page, page_size) = if params.contains_key("page") || params.contains_key("pageSize") {
41 let page = match params.get("page").filter(|v| !v.is_null()) {
42 Some(v) => to_number(Some(v)).max(0.0).floor(),
43 None => 0.0,
44 };
45 let page_size = match params.get("pageSize").filter(|v| !v.is_null()) {
46 Some(v) => to_number(Some(v)),
47 None => 50.0,
48 };
49 (page, page_size)
50 } else {
51 let skip_val = param(params, ast.params.get("skip"));
52 let limit_val = param(params, ast.params.get("limit"));
53 let page = if !is_nullish(skip_val) && is_truthy(limit_val.unwrap_or(&Value::Null)) {
54 (to_number(skip_val) / to_number(limit_val)).floor()
55 } else {
56 0.0
57 };
58 let page_size = if is_nullish(limit_val) {
59 50.0
60 } else {
61 to_number(limit_val)
62 };
63 (page, page_size)
64 };
65
66 Page {
67 page,
68 page_size: if page_size < MAX_PAGE_SIZE {
69 page_size
70 } else {
71 MAX_PAGE_SIZE
72 },
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
78pub enum Mode {
79 Find,
81 TwoPhase,
83 Aggregate,
85}
86
87impl Mode {
88 pub fn as_str(self) -> &'static str {
89 match self {
90 Mode::Find => "find",
91 Mode::TwoPhase => "two_phase",
92 Mode::Aggregate => "aggregate",
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
98pub struct QueryPlan {
99 pub collection: String,
100 pub mode: Mode,
101 pub commands: Vec<Value>,
102 pub postprocess: Option<Value>,
104 pub sort: Option<Value>,
106}
107
108impl QueryPlan {
109 pub fn to_value(&self) -> Value {
110 json!({
111 "collection": self.collection,
112 "mode": self.mode.as_str(),
113 "commands": self.commands,
114 "postprocess": self.postprocess.clone().unwrap_or(Value::Null),
115 "sort": self.sort.clone().unwrap_or(Value::Null),
116 })
117 }
118}
119
120fn postprocess_value(ast: &Ast, inject: &InjectInfo) -> Value {
121 json!({
122 "ast": ast.to_value(),
123 "inject": if inject.is_empty() { Value::Null } else { inject.to_value() },
124 })
125}
126
127pub fn has_pipeline(ast: &Ast, params: &Map<String, Value>) -> bool {
133 ast.params
134 .get("pipeline")
135 .map(|r| !is_nullish(param(params, Some(r))))
136 .unwrap_or(false)
137}
138
139pub fn plan_query(
141 gql: &str,
142 params: &Map<String, Value>,
143 registry: &Registry,
144 ctx: Option<&Context>,
145) -> Result<QueryPlan, String> {
146 let mut params = params.clone();
147 plan_query_mut(gql, &mut params, registry, ctx)
148}
149
150pub fn plan_query_mut(
152 gql: &str,
153 params: &mut Map<String, Value>,
154 registry: &Registry,
155 ctx: Option<&Context>,
156) -> Result<QueryPlan, String> {
157 let ast = parse_gql(gql)?;
158 plan_query_ast_mut(ast, params, registry, ctx)
159}
160
161pub fn plan_query_one(
168 gql: &str,
169 params: &Map<String, Value>,
170 registry: &Registry,
171 ctx: Option<&Context>,
172) -> Result<QueryPlan, String> {
173 let mut params = params.clone();
174 let mut ast = parse_gql(gql)?;
175 if !ast.params.contains_key("limit") && !has_pipeline(&ast, ¶ms) {
176 ast.params
177 .insert("limit".to_string(), "@__core_one_limit__".to_string());
178 params.insert("__core_one_limit__".to_string(), json!(1));
179 }
180 plan_query_ast_mut(ast, &mut params, registry, ctx)
181}
182
183pub fn plan_query_ast_mut(
189 mut ast: Ast,
190 params: &mut Map<String, Value>,
191 registry: &Registry,
192 ctx: Option<&Context>,
193) -> Result<QueryPlan, String> {
194 ensure_context(registry, ctx)?;
195 let schema = registry.get(&ast.model)?;
196
197 if ctx.is_some() && !can_read_schema(schema, ctx) {
198 return Err(ERR_PERMISSION.to_string());
199 }
200
201 if ctx.is_some() {
203 if let Some(r) = ast.params.get("condition").cloned() {
204 let key = r.get(1..).unwrap_or("").to_string();
205 match merge_owner_condition(schema, ctx, params.get(&key).cloned()) {
206 Some(v) => {
207 params.insert(key, v);
208 }
209 None => {
210 params.remove(&key);
211 }
212 }
213 } else if let Some(owner) = merge_owner_condition(schema, ctx, None) {
214 ast.params
216 .insert("condition".to_string(), "@__core_owner__".to_string());
217 params.insert("__core_owner__".to_string(), owner);
218 }
219 }
220
221 let has_pipeline = has_pipeline(&ast, params);
222
223 let mut inject = if has_pipeline {
224 InjectInfo::default()
225 } else {
226 merge_depends_into_ast(&mut ast.relations, schema)?
227 };
228 if !has_pipeline {
231 inject.fields = collect_field_deps(&mut ast.fields, schema);
232 }
233
234 let mut post_ast = ast.clone();
237 if !has_pipeline {
238 flatten_object_fields(&mut post_ast, schema);
239 }
240
241 build_plan(ast, &post_ast, &inject, params, registry, ctx)
242}
243
244pub fn check_readable_relations(
251 relations: &[(String, RelAst)],
252 schema: &Schema,
253 registry: &Registry,
254 ctx: Option<&Context>,
255) -> Result<(), String> {
256 if ctx.is_none() {
257 return Ok(());
258 }
259 for (rel_name, rel_ast) in relations {
260 let Some(rel_def) = schema.relations.get(rel_name) else {
261 continue;
262 };
263 if rel_def.model.is_empty() {
265 continue;
266 }
267 if !is_relation_readable(schema, ctx, rel_name) {
268 return Err(ERR_PERMISSION.to_string());
269 }
270 let rel_schema = registry.get(&rel_def.model)?;
271 if !can_read_schema(rel_schema, ctx) {
272 return Err(ERR_PERMISSION.to_string());
273 }
274 check_readable_relations(&rel_ast.relations, rel_schema, registry, ctx)?;
275 }
276 Ok(())
277}
278
279pub fn build_plan(
285 mut fetch_ast: Ast,
286 post_ast: &Ast,
287 inject: &InjectInfo,
288 params: &Map<String, Value>,
289 registry: &Registry,
290 ctx: Option<&Context>,
291) -> Result<QueryPlan, String> {
292 ensure_context(registry, ctx)?;
293 let schema = registry.get(&fetch_ast.model)?;
294
295 check_readable_relations(&fetch_ast.relations, schema, registry, ctx)?;
298
299 let has_pipeline = has_pipeline(&fetch_ast, params);
303
304 let pipeline = build_pipeline(&mut fetch_ast, params, registry, ctx)?;
305 let stages: Vec<Value> = pipeline.as_array().cloned().unwrap_or_default();
306
307 let grouped = stages.iter().any(|s| s.get("$group").is_some());
311
312 let projection = if has_pipeline {
313 build_pipeline_projection(&fetch_ast, schema)
314 } else if grouped {
315 None
316 } else {
317 build_projection(&fetch_ast, schema, ctx)
318 };
319
320 let collection = schema.collection.clone();
321 let post = || {
322 if has_pipeline || grouped {
323 None
324 } else {
325 Some(postprocess_value(post_ast, inject))
326 }
327 };
328
329 if !has_pipeline && stages.len() == 1 {
331 if let Some(filter) = stages[0].get("$match") {
332 return Ok(QueryPlan {
333 collection: collection.clone(),
334 mode: Mode::Find,
335 commands: vec![cmd_find(schema, filter, projection.as_ref())],
336 postprocess: post(),
337 sort: None,
338 });
339 }
340 }
341
342 let first_lookup = stages.iter().position(|s| {
346 s.get("$lookup")
347 .map(|lo| {
348 !lo.get("as")
349 .and_then(|v| v.as_str())
350 .map(|a| a.starts_with(REL_PRED_PREFIX))
351 .unwrap_or(false)
352 })
353 .unwrap_or(false)
354 });
355 let has_skip_limit = !has_pipeline
357 && stages
358 .iter()
359 .any(|s| s.get("$skip").is_some() || s.get("$limit").is_some());
360 let sort_stage = stages.iter().find(|s| s.get("$sort").is_some()).cloned();
361
362 if let Some(idx) = first_lookup {
363 if !grouped && has_skip_limit && !sorts_by_relation(sort_stage.as_ref()) {
364 let mut id_pipeline: Vec<Value> = stages[..idx].to_vec();
365 for key in ["$sort", "$skip", "$limit"] {
366 if let Some(st) = stages.iter().find(|s| s.get(key).is_some()) {
367 id_pipeline.push(st.clone());
368 }
369 }
370 id_pipeline.push(json!({ "$project": { "_id": 1 } }));
371
372 let mut full: Vec<Value> = stages[idx..]
373 .iter()
374 .filter(|s| {
375 !(s.get("$sort").is_some()
376 || s.get("$skip").is_some()
377 || s.get("$limit").is_some())
378 })
379 .cloned()
380 .collect();
381 full.insert(0, json!({ "$match": { "_id": { "$in": PHASE1_IDS } } }));
382 if let Some(p) = projection.as_ref() {
383 full.push(json!({ "$project": p }));
384 }
385
386 return Ok(QueryPlan {
387 collection: collection.clone(),
388 mode: Mode::TwoPhase,
389 commands: vec![
390 cmd_aggregate(schema, &id_pipeline),
391 cmd_aggregate(schema, &full),
392 ],
393 postprocess: post(),
394 sort: sort_stage,
395 });
396 }
397 }
398
399 let mut final_stages = stages;
401 if let Some(p) = projection.as_ref() {
402 final_stages.push(json!({ "$project": p }));
403 }
404 validate_pipeline_stages(&final_stages)?;
407
408 Ok(QueryPlan {
409 collection,
410 mode: Mode::Aggregate,
411 commands: vec![cmd_aggregate(schema, &final_stages)],
412 postprocess: post(),
413 sort: None,
414 })
415}
416
417pub fn sorts_by_relation(sort_stage: Option<&Value>) -> bool {
419 sort_stage
420 .and_then(|s| s.get("$sort"))
421 .and_then(|s| s.as_object())
422 .map(|o| o.keys().any(|k| k.contains('.')))
423 .unwrap_or(false)
424}
425
426pub fn restore_sort_order(items: &mut [Value], ids: &[Value], sort_stage: Option<&Value>) {
428 if sort_stage.is_none() || ids.len() <= 1 {
429 return;
430 }
431 let mut index: std::collections::HashMap<_, usize> =
434 std::collections::HashMap::with_capacity(ids.len());
435 for (i, id) in ids.iter().enumerate() {
436 index.entry(id_key(id)).or_insert(i);
437 }
438 let keyed: Vec<(usize, Value)> = items
439 .iter()
440 .map(|it| {
441 let k = id_key(it.get("_id").unwrap_or(&Value::Null));
442 let pos = index.get(&k).copied().unwrap_or(ids.len());
443 (pos, it.clone())
444 })
445 .collect();
446 let mut sorted = keyed;
447 sorted.sort_by_key(|(pos, _)| *pos);
448 for (slot, (_, v)) in items.iter_mut().zip(sorted) {
449 *slot = v;
450 }
451}