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