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_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    /// `(page + 1) * pageSize < total`
34    pub fn has_more(&self, total: f64) -> bool {
35        (self.page + 1.0) * self.page_size < total
36    }
37}
38
39/// 分页参数解析(对应 JS `_resolvePage`):page/pageSize 优先,否则由 `$skip/$limit` 反推
40pub 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/// 读路径形态:决定 Host 如何执行命令序列
78#[derive(Debug, Clone, Copy, PartialEq)]
79pub enum Mode {
80    /// 纯 `$match` → 走 `find` 快路径
81    Find,
82    /// `$lookup` + 分页 → 两阶段(先取 ID 再关联)
83    TwoPhase,
84    /// 标准单阶段聚合
85    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    /// 结果回喂 core 做后处理(补默认值 / 递归裁剪 / 剥离注入依赖)所需信息
104    pub postprocess: Option<Value>,
105    /// 两阶段还原排序用的 `$sort` 阶段
106    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
128/// 用户 `$pipeline` 直通探测:GQL 根参数声明了 `$pipeline` 且对应 params 值非 nullish。
129///
130/// ⚠️ DB 独有能力(仅 MongoDB):不建议用于业务查询 —— 会带来跨方言维护的特殊化处理;
131///    仅适合数据迁移 / 功能脚本。准入:standard 档放行(Mongo 源);text2query 档禁用
132///    (判决唯一在消费点 `pipeline/build.rs::build_pipeline`)。
133pub 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
140/// 生成读路径命令序列(对应 JS `query` + `_executePipeline` 的路径选择)
141pub 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
151/// 与 [`plan_query`] 相同,但会把 owner 条件注入写回 `params`(供 queryWithCount 复用)
152pub 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
162/// queryOne:语义为「取第一条」——用户 GQL 未显式给 `$limit` 时强制下推 `$limit(1)`,
163/// 大集合不再全量取回后丢弃(对齐 MongoDB `findOne` 的 limit-1 语义)。
164///
165/// - GQL 已有 `$limit` 时不改写用户意图(取其结果首条);
166/// - `$pipeline` 全权模式不注入(用户自控的 pipeline 不做二次改写);
167/// - 注入键名固定 `__core_one_limit__`(覆盖式写入,防用户 params 键名碰撞)。
168pub 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, &params) {
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
184/// 由**已解析的 AST** 规划读路径([`plan_query_mut`] 与联邦计划共用同一套语义)
185///
186/// 含:schema 读权限校验 → owner 条件注入 → asyncFn 依赖注入 → pipeline 构建 → 形态选择。
187/// 联邦计划([`crate::federation`])会先把跨源关系从 fetch AST 中剥离后再调用
188/// [`build_plan`],因此这里把「取命令」与「取后处理」的 AST 分离开。
189pub 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    // RBAC 表级读判定(deny-wins,叠加于静态白名单之后)
202    ensure_read(registry, schema, ctx)?;
203
204    // 所有者条件注入(非 admin 用户只看自己的数据,无需 $condition 也不能越权读全表)
205    // RBAC 行条件在静态 owner 条件之上叠加(两引擎 $and)
206    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            // GQL 未显式给 $condition:静态 owner 条件与 RBAC 行条件叠加后非空即
225            // 注入合成条件为基准 $match,防越权读全表(RBAC 关闭时叠加直通 None,
226            // 行为零变化)
227            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    // R7:asyncFn 计算列的普通字段依赖(如 `name`)并入根 AST,供投影取数与
246    // process_node 裁剪保留;宿主 asyncFn 执行后由 strip_dep_injected 剥离。
247    if !has_pipeline {
248        inject.fields = collect_field_deps(&mut ast.fields, schema);
249    }
250
251    // `postprocess.ast` 取「展平后、含全部关系」的快照(build_pipeline 会原地展平 fetch ast);
252    // `$pipeline` 全权模式不做后处理,快照保持原样即可。
253    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
261/// T2 / L1 / L2 / L5 / L6:递归校验 GQL **显式请求的关系**可读性。
262///
263/// 判定口径(R0 决策 #1 / #6,与表级策略一致):关系 `read` 可读 **∧** 目标 model
264/// `can_read_schema` 可读;任一不通过 → `Err(ERR_PERMISSION)`(不再静默省略 / 降级)。
265/// 计算列 `depends` 自动引入的关系(`merge_depends_into_ast`)同样在此覆盖(L6)。
266/// `ctx = None` 时放行(fail-open,与默认姿态一致)。
267pub 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        // 未声明 model 的「关系」不是关联(与 federation `walk` 一致),按普通字段处理
281        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        // RBAC 表级读判定(关系目标模型同样叠加,deny-wins)
292        ensure_read(registry, rel_schema, ctx)?;
293        check_readable_relations(&rel_ast.relations, rel_schema, registry, ctx)?;
294    }
295    Ok(())
296}
297
298/// 由 AST 构建 [`QueryPlan`]:pipeline 用 `fetch_ast`,后处理用 `post_ast`。
299///
300/// 单库路径下两者相同;联邦路径下 `fetch_ast` 已剥离跨源关系(该源下推不到),
301/// `post_ast` 仍保留全部关系,供 [`crate::command::finalize_query`] 收尾时
302/// 递归下钻 / 权限裁剪 / 计算列(对齐联邦契约「postprocess 与单库同形状」)。
303pub 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    // T2 / L1 / L6:本单元显式请求的关系可读性(含 depends 自动引入的关系)——
315    // 关系不可读 → Err,绝不静默省略(§9.1 R6 / D2 归一)。
316    check_readable_relations(&fetch_ast.relations, schema, registry, ctx)?;
317
318    // 用户 `$pipeline` 全权模式(⚠️ DB 独有能力,准入判决见 `pipeline/build.rs`):
319    // 不做依赖注入 / 两阶段优化 / 计算列 / 默认值后处理 —— 仅按 GQL 请求字段做顶层
320    // 字段选择(R9),结果原样返回(`postprocess = None`)。
321    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    // `_score` 只由向量阶段产出(VR-03);非向量查询请求它必须显式报错,
327    // 不得返回 null 字段(D2:绝不静默返回残缺结果)。
328    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    // 根级 `$group`:命令自身已含分组投影与分组后分页(§9.3 固定序),
334    // 不能再走两阶段优化 / 追加普通 `$project`;且**分组行不是普通文档**,
335    // 不可做关系下钻 / 计算列 / 默认值后处理 → `postprocess` 置空。
336    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    // VR-03:`$vectorSearch` 必须是聚合首阶段。find 快路径只认单阶段 `$match`,
356    // 形态上已天然排除;此处加断言式护栏,防止后续改动把 `$near` 塞回 `$match`。
357    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    // ── 纯 $match 无关联 → find 快路径(`$pipeline` 全权模式不适用)──
368    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    // ── 两阶段优化($lookup + $skip/$limit,且 sort 未引用关联字段) ──
381    // §9.6 关系聚合谓词 `$lookup`(`as` = `__rp…`)属于 `$condition` 段,必须留在阶段一
382    // 过滤中,故不视作「关联下钻」的起点(否则谓词过滤被推到阶段二,分页计数失真)。
383    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    // `$pipeline` 全权模式由用户自带分页阶段,不参与本优化(否则会改写用户 pipeline)
394    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    // ── 标准单阶段聚合 ──
438    let mut final_stages = stages;
439    if let Some(p) = projection.as_ref() {
440        final_stages.push(json!({ "$project": p }));
441    }
442    // R3:阶段安全校验(拒绝 `$out`/`$merge` 写副作用 + `$where`/`$function`/`$accumulator`)。
443    // 用户 `$pipeline` 直通(DB 独有能力)与内置生成的 pipeline 走同一道闸。
444    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
455/// pipeline 的 `$sort` 是否引用关联表点号字段(如 `bidders.amount`)
456pub 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
464/// 两阶段查询后按阶段一 `_id` 顺序重排(对应 JS `_restoreSortOrder`)
465pub 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    // 阶段一 id → 位次索引(首个命中优先,保留 `position` 的重复 id 语义);
470    // O(n+m) 替代逐项线性扫描的 O(n·m)
471    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}