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::rbac::{ensure_read, merge_row_condition};
13use crate::schema::{Registry, Schema};
14use crate::types::{is_truthy, validate_pipeline_stages};
15
16use super::cmd::{cmd_aggregate, cmd_find, num_value, to_number};
17use super::{ensure_context, ERR_PERMISSION, MAX_PAGE_SIZE, PHASE1_IDS};
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct Page {
21 pub page: f64,
22 pub page_size: f64,
23}
24
25impl Page {
26 pub fn to_value(&self) -> Value {
27 json!({
28 "page": num_value(self.page),
29 "pageSize": num_value(self.page_size),
30 })
31 }
32
33 pub fn has_more(&self, total: f64) -> bool {
35 (self.page + 1.0) * self.page_size < total
36 }
37}
38
39pub fn resolve_page(ast: &Ast, params: &Map<String, Value>) -> Page {
41 let (page, page_size) = if params.contains_key("page") || params.contains_key("pageSize") {
42 let page = match params.get("page").filter(|v| !v.is_null()) {
43 Some(v) => to_number(Some(v)).max(0.0).floor(),
44 None => 0.0,
45 };
46 let page_size = match params.get("pageSize").filter(|v| !v.is_null()) {
47 Some(v) => to_number(Some(v)),
48 None => 50.0,
49 };
50 (page, page_size)
51 } else {
52 let skip_val = param(params, ast.params.get("skip"));
53 let limit_val = param(params, ast.params.get("limit"));
54 let page = if !is_nullish(skip_val) && is_truthy(limit_val.unwrap_or(&Value::Null)) {
55 (to_number(skip_val) / to_number(limit_val)).floor()
56 } else {
57 0.0
58 };
59 let page_size = if is_nullish(limit_val) {
60 50.0
61 } else {
62 to_number(limit_val)
63 };
64 (page, page_size)
65 };
66
67 Page {
68 page,
69 page_size: if page_size < MAX_PAGE_SIZE {
70 page_size
71 } else {
72 MAX_PAGE_SIZE
73 },
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
79pub enum Mode {
80 Find,
82 TwoPhase,
84 Aggregate,
86}
87
88impl Mode {
89 pub fn as_str(self) -> &'static str {
90 match self {
91 Mode::Find => "find",
92 Mode::TwoPhase => "two_phase",
93 Mode::Aggregate => "aggregate",
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
99pub struct QueryPlan {
100 pub collection: String,
101 pub mode: Mode,
102 pub commands: Vec<Value>,
103 pub postprocess: Option<Value>,
105 pub sort: Option<Value>,
107}
108
109impl QueryPlan {
110 pub fn to_value(&self) -> Value {
111 json!({
112 "collection": self.collection,
113 "mode": self.mode.as_str(),
114 "commands": self.commands,
115 "postprocess": self.postprocess.clone().unwrap_or(Value::Null),
116 "sort": self.sort.clone().unwrap_or(Value::Null),
117 })
118 }
119}
120
121fn postprocess_value(ast: &Ast, inject: &InjectInfo) -> Value {
122 json!({
123 "ast": ast.to_value(),
124 "inject": if inject.is_empty() { Value::Null } else { inject.to_value() },
125 })
126}
127
128pub fn has_pipeline(ast: &Ast, params: &Map<String, Value>) -> bool {
134 ast.params
135 .get("pipeline")
136 .map(|r| !is_nullish(param(params, Some(r))))
137 .unwrap_or(false)
138}
139
140pub fn plan_query(
142 gql: &str,
143 params: &Map<String, Value>,
144 registry: &Registry,
145 ctx: Option<&Context>,
146) -> Result<QueryPlan, String> {
147 let mut params = params.clone();
148 plan_query_mut(gql, &mut params, registry, ctx)
149}
150
151pub fn plan_query_mut(
153 gql: &str,
154 params: &mut Map<String, Value>,
155 registry: &Registry,
156 ctx: Option<&Context>,
157) -> Result<QueryPlan, String> {
158 let ast = parse_gql(gql)?;
159 plan_query_ast_mut(ast, params, registry, ctx)
160}
161
162pub fn plan_query_one(
169 gql: &str,
170 params: &Map<String, Value>,
171 registry: &Registry,
172 ctx: Option<&Context>,
173) -> Result<QueryPlan, String> {
174 let mut params = params.clone();
175 let mut ast = parse_gql(gql)?;
176 if !ast.params.contains_key("limit") && !has_pipeline(&ast, ¶ms) {
177 ast.params
178 .insert("limit".to_string(), "@__core_one_limit__".to_string());
179 params.insert("__core_one_limit__".to_string(), json!(1));
180 }
181 plan_query_ast_mut(ast, &mut params, registry, ctx)
182}
183
184pub fn plan_query_ast_mut(
190 mut ast: Ast,
191 params: &mut Map<String, Value>,
192 registry: &Registry,
193 ctx: Option<&Context>,
194) -> Result<QueryPlan, String> {
195 ensure_context(registry, ctx)?;
196 let schema = registry.get(&ast.model)?;
197
198 if ctx.is_some() && !can_read_schema(registry.role_rules(), schema, ctx) {
199 return Err(ERR_PERMISSION.to_string());
200 }
201 ensure_read(registry, schema, ctx)?;
203
204 if ctx.is_some() {
207 if let Some(r) = ast.params.get("condition").cloned() {
208 let key = r.get(1..).unwrap_or("").to_string();
209 let owner_merged = merge_owner_condition(
210 registry.role_rules(),
211 schema,
212 ctx,
213 params.get(&key).cloned(),
214 );
215 match merge_row_condition(registry, schema, ctx, "read", owner_merged) {
216 Some(v) => {
217 params.insert(key, v);
218 }
219 None => {
220 params.remove(&key);
221 }
222 }
223 } else {
224 let static_owner = merge_owner_condition(registry.role_rules(), schema, ctx, None);
228 if let Some(owner) = merge_row_condition(registry, schema, ctx, "read", static_owner)
229 .filter(|v| v.as_object().map(|o| !o.is_empty()).unwrap_or(false))
230 {
231 ast.params
232 .insert("condition".to_string(), "@__core_owner__".to_string());
233 params.insert("__core_owner__".to_string(), owner);
234 }
235 }
236 }
237
238 let has_pipeline = has_pipeline(&ast, params);
239
240 let mut inject = if has_pipeline {
241 InjectInfo::default()
242 } else {
243 merge_depends_into_ast(&mut ast.relations, schema)?
244 };
245 if !has_pipeline {
248 inject.fields = collect_field_deps(&mut ast.fields, schema);
249 }
250
251 let mut post_ast = ast.clone();
254 if !has_pipeline {
255 flatten_object_fields(&mut post_ast, schema);
256 }
257
258 build_plan(ast, &post_ast, &inject, params, registry, ctx)
259}
260
261pub fn check_readable_relations(
268 relations: &[(String, RelAst)],
269 schema: &Schema,
270 registry: &Registry,
271 ctx: Option<&Context>,
272) -> Result<(), String> {
273 if ctx.is_none() {
274 return Ok(());
275 }
276 for (rel_name, rel_ast) in relations {
277 let Some(rel_def) = schema.relations.get(rel_name) else {
278 continue;
279 };
280 if rel_def.model.is_empty() {
282 continue;
283 }
284 if !is_relation_readable(registry.role_rules(), schema, ctx, rel_name) {
285 return Err(ERR_PERMISSION.to_string());
286 }
287 let rel_schema = registry.get(&rel_def.model)?;
288 if !can_read_schema(registry.role_rules(), rel_schema, ctx) {
289 return Err(ERR_PERMISSION.to_string());
290 }
291 ensure_read(registry, rel_schema, ctx)?;
293 check_readable_relations(&rel_ast.relations, rel_schema, registry, ctx)?;
294 }
295 Ok(())
296}
297
298pub fn build_plan(
304 mut fetch_ast: Ast,
305 post_ast: &Ast,
306 inject: &InjectInfo,
307 params: &Map<String, Value>,
308 registry: &Registry,
309 ctx: Option<&Context>,
310) -> Result<QueryPlan, String> {
311 ensure_context(registry, ctx)?;
312 let schema = registry.get(&fetch_ast.model)?;
313
314 check_readable_relations(&fetch_ast.relations, schema, registry, ctx)?;
317
318 let has_pipeline = has_pipeline(&fetch_ast, params);
322
323 let pipeline = build_pipeline(&mut fetch_ast, params, registry, ctx)?;
324 let stages: Vec<Value> = pipeline.as_array().cloned().unwrap_or_default();
325
326 let has_vector = stages.iter().any(|s| s.get("$vectorSearch").is_some());
329 if fetch_ast.fields.iter().any(|f| f == "_score") && !has_vector {
330 return Err("字段 _score 仅在向量检索($near)查询中可用".to_string());
331 }
332
333 let grouped = stages.iter().any(|s| s.get("$group").is_some());
337
338 let projection = if has_pipeline {
339 build_pipeline_projection(&fetch_ast, schema)
340 } else if grouped {
341 None
342 } else {
343 build_projection(&fetch_ast, schema, ctx, registry)
344 };
345
346 let collection = schema.collection.clone();
347 let post = || {
348 if has_pipeline || grouped {
349 None
350 } else {
351 Some(postprocess_value(post_ast, inject))
352 }
353 };
354
355 let vector_first = stages
358 .first()
359 .map(|s| s.get("$vectorSearch").is_some())
360 .unwrap_or(false);
361 if vector_first && stages.len() == 1 {
362 return Err(
363 "向量检索计划缺少后置阶段:$vectorSearch 必须与 $addFields._score 成对出现".into(),
364 );
365 }
366
367 if !has_pipeline && stages.len() == 1 {
369 if let Some(filter) = stages[0].get("$match") {
370 return Ok(QueryPlan {
371 collection: collection.clone(),
372 mode: Mode::Find,
373 commands: vec![cmd_find(schema, filter, projection.as_ref())],
374 postprocess: post(),
375 sort: None,
376 });
377 }
378 }
379
380 let first_lookup = stages.iter().position(|s| {
384 s.get("$lookup")
385 .map(|lo| {
386 !lo.get("as")
387 .and_then(|v| v.as_str())
388 .map(|a| a.starts_with(REL_PRED_PREFIX))
389 .unwrap_or(false)
390 })
391 .unwrap_or(false)
392 });
393 let has_skip_limit = !has_pipeline
395 && stages
396 .iter()
397 .any(|s| s.get("$skip").is_some() || s.get("$limit").is_some());
398 let sort_stage = stages.iter().find(|s| s.get("$sort").is_some()).cloned();
399
400 if let Some(idx) = first_lookup {
401 if !grouped && has_skip_limit && !sorts_by_relation(sort_stage.as_ref()) {
402 let mut id_pipeline: Vec<Value> = stages[..idx].to_vec();
403 for key in ["$sort", "$skip", "$limit"] {
404 if let Some(st) = stages.iter().find(|s| s.get(key).is_some()) {
405 id_pipeline.push(st.clone());
406 }
407 }
408 id_pipeline.push(json!({ "$project": { "_id": 1 } }));
409
410 let mut full: Vec<Value> = stages[idx..]
411 .iter()
412 .filter(|s| {
413 !(s.get("$sort").is_some()
414 || s.get("$skip").is_some()
415 || s.get("$limit").is_some())
416 })
417 .cloned()
418 .collect();
419 full.insert(0, json!({ "$match": { "_id": { "$in": PHASE1_IDS } } }));
420 if let Some(p) = projection.as_ref() {
421 full.push(json!({ "$project": p }));
422 }
423
424 return Ok(QueryPlan {
425 collection: collection.clone(),
426 mode: Mode::TwoPhase,
427 commands: vec![
428 cmd_aggregate(schema, &id_pipeline),
429 cmd_aggregate(schema, &full),
430 ],
431 postprocess: post(),
432 sort: sort_stage,
433 });
434 }
435 }
436
437 let mut final_stages = stages;
439 if let Some(p) = projection.as_ref() {
440 final_stages.push(json!({ "$project": p }));
441 }
442 validate_pipeline_stages(&final_stages)?;
445
446 Ok(QueryPlan {
447 collection,
448 mode: Mode::Aggregate,
449 commands: vec![cmd_aggregate(schema, &final_stages)],
450 postprocess: post(),
451 sort: None,
452 })
453}
454
455pub fn sorts_by_relation(sort_stage: Option<&Value>) -> bool {
457 sort_stage
458 .and_then(|s| s.get("$sort"))
459 .and_then(|s| s.as_object())
460 .map(|o| o.keys().any(|k| k.contains('.')))
461 .unwrap_or(false)
462}
463
464pub fn restore_sort_order(items: &mut [Value], ids: &[Value], sort_stage: Option<&Value>) {
466 if sort_stage.is_none() || ids.len() <= 1 {
467 return;
468 }
469 let mut index: std::collections::HashMap<_, usize> =
472 std::collections::HashMap::with_capacity(ids.len());
473 for (i, id) in ids.iter().enumerate() {
474 index.entry(id_key(id)).or_insert(i);
475 }
476 let keyed: Vec<(usize, Value)> = items
477 .iter()
478 .map(|it| {
479 let k = id_key(it.get("_id").unwrap_or(&Value::Null));
480 let pos = index.get(&k).copied().unwrap_or(ids.len());
481 (pos, it.clone())
482 })
483 .collect();
484 let mut sorted = keyed;
485 sorted.sort_by_key(|(pos, _)| *pos);
486 for (slot, (_, v)) in items.iter_mut().zip(sorted) {
487 *slot = v;
488 }
489}