Skip to main content

rust_store_core/command/
mod.rs

1//! Command 序列契约(方案 A 的「core 产出指令、Host 执行」边界)
2//!
3//! core 不持有 MongoDB 驱动。读/写路径被拆成两步:
4//!
5//! ```text
6//!   Host: plan = core.plan_query(gql, params, ctx)      // 纯逻辑,产出命令
7//!   Host: for cmd in plan.commands { driver.execute(cmd) }   // 唯一 IO 边界
8//!   Host: items = core.finalize(plan, items)            // 回喂 core 做后处理
9//! ```
10//!
11//! 命令 JSON 形状(语言无关,Node/Python 两侧共用):
12//!
13//! `source` / `namespace` / `collection` 为**定位三元组**,由生成该命令的 schema 一次填入
14//! (`source` 缺省 `"default"`,`namespace` 缺省 `null` = 连接自身默认库/schema)。
15//! Host 依据 `cmd.source`(而非 collection 反查)选择连接;`namespace` 的用法见
16//! `multi-datasource-routing-plan.md`(Mongo client 型 source / SQL qualified 表名)。
17//!
18//! ```json
19//! { "kind": "find", "source": "default", "namespace": null, "collection": "u", "filter": {...}, "projection": {...}|null }
20//! { "kind": "aggregate", "source": "pg1", "namespace": "app", "collection": "u", "pipeline": [ ... ] }
21//! { "kind": "countDocuments", "source": "default", "namespace": null, "collection": "u", "filter": {...} }
22//! { "kind": "findOne", "source": "default", "namespace": null, "collection": "u", "filter": {...}, "projection": {...}|null }
23//! { "kind": "insertOne", "source": "default", "namespace": null, "collection": "u", "doc": {...} }
24//! { "kind": "insertMany", "source": "default", "namespace": null, "collection": "u", "docs": [ ... ] }
25//! { "kind": "findOneAndUpdate", "source": "default", "namespace": null, "collection": "u", "filter": {...}, "update": {...}, "options": {...} }
26//! { "kind": "updateMany", "source": "default", "namespace": null, "collection": "u", "filter": {...}, "update": {...} }
27//! { "kind": "deleteMany", "source": "default", "namespace": null, "collection": "u", "filter": {...} }
28//! ```
29//!
30//! 两阶段查询(`$lookup` + `$skip/$limit`)的命令序列里,第二条 aggregate 的
31//! `$match._id.$in` 为占位符 [`PHASE1_IDS`],Host 需用第一条命令返回的
32//! `_id` 数组(**保持顺序**)替换后再执行;执行完调用 [`restore_sort_order`] 还原排序。
33//!
34//! 写路径占位符:mutation 步骤间的父子依赖用 `{{step.<N>._id}}`
35//! ([`step_id_placeholder`])表达——第 N 步执行结果文档的 `_id`,Host 在该步
36//! 执行完成后回填到后续命令。需新生成的 `_id` 不用占位符:由 Host 供给
37//! `new_ids` / `new_id` 参数,core 按序消费(core 无随机源)。
38//!
39//! 子模块划分:
40//!   - [`cmd`]:命令构造 + 数值工具
41//!   - [`query`]:读路径计划(分页 / 两阶段 / 排序还原)
42//!   - [`count`]:列表 + total 计划
43//!   - [`write`]:插入与简单查询计划 + 写权限探针
44//!   - [`mutate`]:update / updateMany / remove / upsert / insertMany 计划
45//!   - [`mutation`]:mutation 递归规划(父子文档步骤序列)
46//!   - [`finalize`]:结果回喂(后处理 / asyncFn 桥 / 剥离注入)
47
48mod cmd;
49mod count;
50mod finalize;
51mod mutate;
52mod mutation;
53mod query;
54mod write;
55
56use crate::permission::Context;
57use crate::schema::{Profile, Registry};
58
59pub use cmd::{
60    apply_route_override, cmd_aggregate, cmd_count_documents, cmd_delete_many, cmd_find,
61    cmd_find_one, cmd_find_one_and_update, cmd_insert_many, cmd_insert_one, cmd_update_many,
62};
63pub use count::{plan_query_with_count, CountQueryPlan};
64pub use finalize::{finalize_query, prepare_query, strip_query};
65pub use mutate::{
66    plan_archive_docs, plan_insert_many, plan_remove, plan_update, plan_update_many, plan_upsert,
67};
68pub use mutation::plan_mutation;
69pub use query::{
70    build_plan, check_readable_relations, plan_query, plan_query_ast_mut, plan_query_mut,
71    plan_query_one, resolve_page, restore_sort_order, sorts_by_relation, Mode, Page, QueryPlan,
72};
73pub use write::{check_write_perm, plan_count, plan_exists, plan_insert, Probe};
74
75/// 权限拒绝哨兵:Host 需映射为各自的 PermissionError。
76///
77/// 所有权限类错误统一携带 `ERR_PERMISSION:` 稳定前缀 —— Host 按**前缀**识别
78/// 权限错误(构造后剥离前缀),不再对具体中文文案做脆弱匹配(core 文案可自由调整)。
79pub const ERR_PERM_PREFIX: &str = "ERR_PERMISSION:";
80pub const ERR_PERMISSION: &str = "ERR_PERMISSION:无访问权限";
81pub const ERR_NO_WRITE: &str = "ERR_PERMISSION:无写入权限";
82pub const ERR_NO_DELETE: &str = "ERR_PERMISSION:无删除权限";
83pub const ERR_NO_BATCH_WRITE: &str = "ERR_PERMISSION:无批量写入权限";
84
85/// `require_context` 开启时 ctx 缺失的拒绝哨兵(fail-secure;Host 按前缀识别,
86/// 一般映射为 500 配置/契约错误而非 403 —— 这是调用方合约违反,不是用户无权限)。
87pub const ERR_NO_CONTEXT: &str =
88    "ERR_NO_CONTEXT:require_context 已开启,调用必须携带用户上下文(内部调用请传 {\"internal\": true})";
89
90/// fail-secure 门禁:`require_context` 开启时拒绝 `ctx: None`(默认关闭时零开销放行)。
91///
92/// 挂在所有含 ctx 的 plan 公开入口顶部(读/写/联邦),保证「绑定层无论怎么调,
93/// 只要开关开着,缺上下文一律显式报错」——而非静默按系统调用放行。
94pub fn ensure_context(registry: &Registry, ctx: Option<&Context>) -> Result<(), String> {
95    if registry.require_context() && ctx.is_none() {
96        return Err(ERR_NO_CONTEXT.to_string());
97    }
98    // 档位叠加(正交但叠加):text2query 档等效强制携带用户上下文(见 [`ensure_profile_ctx`])。
99    // 挂在同一处 → 所有已挂 `ensure_context` 的入口(读/写/联邦)自动获得档位强制,零遗漏。
100    ensure_profile_ctx(registry, ctx)
101}
102
103/// 档位拒绝哨兵:text2query 档命中硬限制/收缩项时的稳定前缀。
104///
105/// Host 按**前缀**映射为各自的 ProfileViolation(构造后剥离前缀),
106/// 不对中文文案做脆弱匹配(core 文案可自由调整)——与 `ERR_PERM_PREFIX` 同构。
107pub const ERR_TEXT2QUERY: &str = "ERR_TEXT2QUERY:";
108
109/// text2query 档硬限制(单点定义,Host 可读;取值**严于** standard)
110///
111/// - 单次取数行数:严于 [`MAX_PAGE_SIZE`](5000)—— AI 问数交互式结果规模;
112/// - 关系嵌套深度:严于 `pipeline::MAX_DEPTH`(10)—— AI 生成的关系嵌套实用上限;
113/// - 联邦单源行数:严于 `federation::MAX_FEDERATION_ROWS`(100000)。
114pub const T2Q_MAX_ROWS: f64 = 1000.0;
115pub const T2Q_MAX_DEPTH: usize = 3;
116pub const T2Q_MAX_FEDERATION_ROWS: usize = 10_000;
117
118/// text2query 档强制上下文(叠加于 [`ensure_context`] 之上)。
119///
120/// `require_context` 与档位**正交但叠加**:text2query 档等效强制开启,
121/// 退出档位后恢复用户原设置(见 Host 的 `text2query()` 上下文管理器)。
122pub fn ensure_profile_ctx(registry: &Registry, ctx: Option<&Context>) -> Result<(), String> {
123    if registry.profile() == Profile::Text2Query && ctx.is_none() {
124        return Err(format!("{ERR_TEXT2QUERY}text2query 档必须携带用户上下文"));
125    }
126    Ok(())
127}
128
129/// text2query 档禁用某项能力(standard 档放行)。
130///
131/// 用于「DB 独有能力 / 直通 / 受限参数」类收缩项;standard 档不做任何拦截。
132pub fn forbid_t2q(registry: &Registry, feature: &str) -> Result<(), String> {
133    if registry.profile() == Profile::Text2Query {
134        return Err(format!(
135            "{ERR_TEXT2QUERY}text2query 档禁用 [{feature}](功能收缩)"
136        ));
137    }
138    Ok(())
139}
140
141/// text2query 档禁止携带 `route_override`(受信来源门禁)。
142///
143/// `route_override`(`{source, namespace}`)是多租户路由覆盖的**受信服务端参数**,
144/// 禁止透传用户输入 / AI 生成(否则可被用于跨源路由,CWE-639)。standard 档维持
145/// 受信可用;text2query 档只要携带即拒。判决逻辑单点在此,绑定层(core-py/core-node)
146/// 在 `with_route_override` 唯一注入点调用,覆盖读 / 写全部 plan 入口。
147pub fn ensure_route_override_allowed(registry: &Registry, present: bool) -> Result<(), String> {
148    if present && registry.profile() == Profile::Text2Query {
149        return Err(format!(
150            "{ERR_TEXT2QUERY}text2query 档禁用 route_override(受信参数,禁 AI 侧指定)"
151        ));
152    }
153    Ok(())
154}
155
156/// 两阶段查询中由 Host 替换的阶段一 `_id` 顺序数组
157pub const PHASE1_IDS: &str = "{{phase1.ids}}";
158
159/// mutation 第 `idx` 步执行结果文档 `_id` 的占位符
160pub fn step_id_placeholder(idx: usize) -> String {
161    format!("{{{{step.{}._id}}}}", idx)
162}
163
164/// 读取上限(pageSize 封顶,防拖库)
165pub const MAX_PAGE_SIZE: f64 = 5000.0;