Skip to main content

rust_store_core/command/
query.rs

1//! 读路径计划:分页解析、两阶段优化、排序还原
2
3use 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_projection, flatten_object_fields, is_nullish, param, parse_gql, Ast,
10    RelAst, REL_PRED_PREFIX,
11};
12use crate::schema::{Registry, Schema};
13use crate::types::is_truthy;
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    /// `(page + 1) * pageSize < total`
33    pub fn has_more(&self, total: f64) -> bool {
34        (self.page + 1.0) * self.page_size < total
35    }
36}
37
38/// 分页参数解析(对应 JS `_resolvePage`):page/pageSize 优先,否则由 `$skip/$limit` 反推
39pub 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/// 读路径形态:决定 Host 如何执行命令序列
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub enum Mode {
79    /// 纯 `$match` → 走 `find` 快路径
80    Find,
81    /// `$lookup` + 分页 → 两阶段(先取 ID 再关联)
82    TwoPhase,
83    /// 标准单阶段聚合
84    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    /// 结果回喂 core 做后处理(补默认值 / 递归裁剪 / 剥离注入依赖)所需信息
103    pub postprocess: Option<Value>,
104    /// 两阶段还原排序用的 `$sort` 阶段
105    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
127/// 生成读路径命令序列(对应 JS `query` + `_executePipeline` 的路径选择)
128pub fn plan_query(
129    gql: &str,
130    params: &Map<String, Value>,
131    registry: &Registry,
132    ctx: Option<&Context>,
133) -> Result<QueryPlan, String> {
134    let mut params = params.clone();
135    plan_query_mut(gql, &mut params, registry, ctx)
136}
137
138/// 与 [`plan_query`] 相同,但会把 owner 条件注入写回 `params`(供 queryWithCount 复用)
139pub fn plan_query_mut(
140    gql: &str,
141    params: &mut Map<String, Value>,
142    registry: &Registry,
143    ctx: Option<&Context>,
144) -> Result<QueryPlan, String> {
145    let ast = parse_gql(gql)?;
146    plan_query_ast_mut(ast, params, registry, ctx)
147}
148
149/// queryOne:语义为「取第一条」——用户 GQL 未显式给 `$limit` 时强制下推 `$limit(1)`,
150/// 大集合不再全量取回后丢弃(对齐 MongoDB `findOne` 的 limit-1 语义)。
151///
152/// - GQL 已有 `$limit` 时不改写用户意图(取其结果首条);
153/// - 注入键名固定 `__core_one_limit__`(覆盖式写入,防用户 params 键名碰撞)。
154pub fn plan_query_one(
155    gql: &str,
156    params: &Map<String, Value>,
157    registry: &Registry,
158    ctx: Option<&Context>,
159) -> Result<QueryPlan, String> {
160    let mut params = params.clone();
161    let mut ast = parse_gql(gql)?;
162    if !ast.params.contains_key("limit") {
163        ast.params
164            .insert("limit".to_string(), "@__core_one_limit__".to_string());
165        params.insert("__core_one_limit__".to_string(), json!(1));
166    }
167    plan_query_ast_mut(ast, &mut params, registry, ctx)
168}
169
170/// 由**已解析的 AST** 规划读路径([`plan_query_mut`] 与联邦计划共用同一套语义)
171///
172/// 含:schema 读权限校验 → owner 条件注入 → asyncFn 依赖注入 → pipeline 构建 → 形态选择。
173/// 联邦计划([`crate::federation`])会先把跨源关系从 fetch AST 中剥离后再调用
174/// [`build_plan`],因此这里把「取命令」与「取后处理」的 AST 分离开。
175pub fn plan_query_ast_mut(
176    mut ast: Ast,
177    params: &mut Map<String, Value>,
178    registry: &Registry,
179    ctx: Option<&Context>,
180) -> Result<QueryPlan, String> {
181    ensure_context(registry, ctx)?;
182    let schema = registry.get(&ast.model)?;
183
184    if ctx.is_some() && !can_read_schema(schema, ctx) {
185        return Err(ERR_PERMISSION.to_string());
186    }
187
188    // 所有者条件注入(非 admin 用户只看自己的数据,无需 $condition 也不能越权读全表)
189    if ctx.is_some() {
190        if let Some(r) = ast.params.get("condition").cloned() {
191            let key = r.get(1..).unwrap_or("").to_string();
192            match merge_owner_condition(schema, ctx, params.get(&key).cloned()) {
193                Some(v) => {
194                    params.insert(key, v);
195                }
196                None => {
197                    params.remove(&key);
198                }
199            }
200        } else if let Some(owner) = merge_owner_condition(schema, ctx, None) {
201            // GQL 未显式给 $condition:注入合成 owner 条件为基准 $match,防越权读全表
202            ast.params
203                .insert("condition".to_string(), "@__core_owner__".to_string());
204            params.insert("__core_owner__".to_string(), owner);
205        }
206    }
207
208    let mut inject = merge_depends_into_ast(&mut ast.relations, schema)?;
209    // R7:asyncFn 计算列的普通字段依赖(如 `name`)并入根 AST,供投影取数与
210    // process_node 裁剪保留;宿主 asyncFn 执行后由 strip_dep_injected 剥离。
211    inject.fields = collect_field_deps(&mut ast.fields, schema);
212
213    // `postprocess.ast` 取「展平后、含全部关系」的快照(build_pipeline 会原地展平 fetch ast)
214    let mut post_ast = ast.clone();
215    flatten_object_fields(&mut post_ast, schema);
216
217    build_plan(ast, &post_ast, &inject, params, registry, ctx)
218}
219
220/// T2 / L1 / L2 / L5 / L6:递归校验 GQL **显式请求的关系**可读性。
221///
222/// 判定口径(R0 决策 #1 / #6,与表级策略一致):关系 `read` 可读 **∧** 目标 model
223/// `can_read_schema` 可读;任一不通过 → `Err(ERR_PERMISSION)`(不再静默省略 / 降级)。
224/// 计算列 `depends` 自动引入的关系(`merge_depends_into_ast`)同样在此覆盖(L6)。
225/// `ctx = None` 时放行(fail-open,与默认姿态一致)。
226pub fn check_readable_relations(
227    relations: &[(String, RelAst)],
228    schema: &Schema,
229    registry: &Registry,
230    ctx: Option<&Context>,
231) -> Result<(), String> {
232    if ctx.is_none() {
233        return Ok(());
234    }
235    for (rel_name, rel_ast) in relations {
236        let Some(rel_def) = schema.relations.get(rel_name) else {
237            continue;
238        };
239        // 未声明 model 的「关系」不是关联(与 federation `walk` 一致),按普通字段处理
240        if rel_def.model.is_empty() {
241            continue;
242        }
243        if !is_relation_readable(schema, ctx, rel_name) {
244            return Err(ERR_PERMISSION.to_string());
245        }
246        let rel_schema = registry.get(&rel_def.model)?;
247        if !can_read_schema(rel_schema, ctx) {
248            return Err(ERR_PERMISSION.to_string());
249        }
250        check_readable_relations(&rel_ast.relations, rel_schema, registry, ctx)?;
251    }
252    Ok(())
253}
254
255/// 由 AST 构建 [`QueryPlan`]:pipeline 用 `fetch_ast`,后处理用 `post_ast`。
256///
257/// 单库路径下两者相同;联邦路径下 `fetch_ast` 已剥离跨源关系(该源下推不到),
258/// `post_ast` 仍保留全部关系,供 [`crate::command::finalize_query`] 收尾时
259/// 递归下钻 / 权限裁剪 / 计算列(对齐联邦契约「postprocess 与单库同形状」)。
260pub fn build_plan(
261    mut fetch_ast: Ast,
262    post_ast: &Ast,
263    inject: &InjectInfo,
264    params: &Map<String, Value>,
265    registry: &Registry,
266    ctx: Option<&Context>,
267) -> Result<QueryPlan, String> {
268    ensure_context(registry, ctx)?;
269    let schema = registry.get(&fetch_ast.model)?;
270
271    // T2 / L1 / L6:本单元显式请求的关系可读性(含 depends 自动引入的关系)——
272    // 关系不可读 → Err,绝不静默省略(§9.1 R6 / D2 归一)。
273    check_readable_relations(&fetch_ast.relations, schema, registry, ctx)?;
274
275    let pipeline = build_pipeline(&mut fetch_ast, params, registry, ctx)?;
276    let stages: Vec<Value> = pipeline.as_array().cloned().unwrap_or_default();
277
278    // 根级 `$group`:命令自身已含分组投影与分组后分页(§9.3 固定序),
279    // 不能再走两阶段优化 / 追加普通 `$project`;且**分组行不是普通文档**,
280    // 不可做关系下钻 / 计算列 / 默认值后处理 → `postprocess` 置空。
281    let grouped = stages.iter().any(|s| s.get("$group").is_some());
282
283    let projection = if grouped {
284        None
285    } else {
286        build_projection(&fetch_ast, schema, ctx)
287    };
288
289    let collection = schema.collection.clone();
290    let post = || {
291        if grouped {
292            None
293        } else {
294            Some(postprocess_value(post_ast, inject))
295        }
296    };
297
298    // ── 纯 $match 无关联 → find 快路径 ──
299    if stages.len() == 1 {
300        if let Some(filter) = stages[0].get("$match") {
301            return Ok(QueryPlan {
302                collection: collection.clone(),
303                mode: Mode::Find,
304                commands: vec![cmd_find(schema, filter, projection.as_ref())],
305                postprocess: post(),
306                sort: None,
307            });
308        }
309    }
310
311    // ── 两阶段优化($lookup + $skip/$limit,且 sort 未引用关联字段) ──
312    // §9.6 关系聚合谓词 `$lookup`(`as` = `__rp…`)属于 `$condition` 段,必须留在阶段一
313    // 过滤中,故不视作「关联下钻」的起点(否则谓词过滤被推到阶段二,分页计数失真)。
314    let first_lookup = stages.iter().position(|s| {
315        s.get("$lookup")
316            .map(|lo| {
317                !lo.get("as")
318                    .and_then(|v| v.as_str())
319                    .map(|a| a.starts_with(REL_PRED_PREFIX))
320                    .unwrap_or(false)
321            })
322            .unwrap_or(false)
323    });
324    let has_skip_limit = stages
325        .iter()
326        .any(|s| s.get("$skip").is_some() || s.get("$limit").is_some());
327    let sort_stage = stages.iter().find(|s| s.get("$sort").is_some()).cloned();
328
329    if let Some(idx) = first_lookup {
330        if !grouped && has_skip_limit && !sorts_by_relation(sort_stage.as_ref()) {
331            let mut id_pipeline: Vec<Value> = stages[..idx].to_vec();
332            for key in ["$sort", "$skip", "$limit"] {
333                if let Some(st) = stages.iter().find(|s| s.get(key).is_some()) {
334                    id_pipeline.push(st.clone());
335                }
336            }
337            id_pipeline.push(json!({ "$project": { "_id": 1 } }));
338
339            let mut full: Vec<Value> = stages[idx..]
340                .iter()
341                .filter(|s| {
342                    !(s.get("$sort").is_some()
343                        || s.get("$skip").is_some()
344                        || s.get("$limit").is_some())
345                })
346                .cloned()
347                .collect();
348            full.insert(0, json!({ "$match": { "_id": { "$in": PHASE1_IDS } } }));
349            if let Some(p) = projection.as_ref() {
350                full.push(json!({ "$project": p }));
351            }
352
353            return Ok(QueryPlan {
354                collection: collection.clone(),
355                mode: Mode::TwoPhase,
356                commands: vec![
357                    cmd_aggregate(schema, &id_pipeline),
358                    cmd_aggregate(schema, &full),
359                ],
360                postprocess: post(),
361                sort: sort_stage,
362            });
363        }
364    }
365
366    // ── 标准单阶段聚合 ──
367    let mut final_stages = stages;
368    if let Some(p) = projection.as_ref() {
369        final_stages.push(json!({ "$project": p }));
370    }
371
372    Ok(QueryPlan {
373        collection,
374        mode: Mode::Aggregate,
375        commands: vec![cmd_aggregate(schema, &final_stages)],
376        postprocess: post(),
377        sort: None,
378    })
379}
380
381/// pipeline 的 `$sort` 是否引用关联表点号字段(如 `bidders.amount`)
382pub fn sorts_by_relation(sort_stage: Option<&Value>) -> bool {
383    sort_stage
384        .and_then(|s| s.get("$sort"))
385        .and_then(|s| s.as_object())
386        .map(|o| o.keys().any(|k| k.contains('.')))
387        .unwrap_or(false)
388}
389
390/// 两阶段查询后按阶段一 `_id` 顺序重排(对应 JS `_restoreSortOrder`)
391pub fn restore_sort_order(items: &mut [Value], ids: &[Value], sort_stage: Option<&Value>) {
392    if sort_stage.is_none() || ids.len() <= 1 {
393        return;
394    }
395    // 阶段一 id → 位次索引(首个命中优先,保留 `position` 的重复 id 语义);
396    // O(n+m) 替代逐项线性扫描的 O(n·m)
397    let mut index: std::collections::HashMap<_, usize> =
398        std::collections::HashMap::with_capacity(ids.len());
399    for (i, id) in ids.iter().enumerate() {
400        index.entry(id_key(id)).or_insert(i);
401    }
402    let keyed: Vec<(usize, Value)> = items
403        .iter()
404        .map(|it| {
405            let k = id_key(it.get("_id").unwrap_or(&Value::Null));
406            let pos = index.get(&k).copied().unwrap_or(ids.len());
407            (pos, it.clone())
408        })
409        .collect();
410    let mut sorted = keyed;
411    sorted.sort_by_key(|(pos, _)| *pos);
412    for (slot, (_, v)) in items.iter_mut().zip(sorted) {
413        *slot = v;
414    }
415}