Skip to content

feat(mongodb): 内嵌 QuickJS 运行时执行 MongoDB shell JavaScript 脚本 - #12661

Open
q396921921 wants to merge 5 commits into
t8y2:mainfrom
q396921921:feature/mongo-js-script
Open

q396921921 wants to merge 5 commits into
t8y2:mainfrom
q396921921:feature/mongo-js-script

Conversation

@q396921921

Copy link
Copy Markdown
Contributor

变更说明 / Change Description

MongoDB 标签页新增「作为 MongoDB 脚本执行」工具栏按钮,用内嵌的 QuickJS 运行时执行多语句 shell JavaScript(IIFE、var/let、循环、forEach、toArray、print 等),对应 #3050 里"mongo 下执行 js 脚本报 Use MongoDB-specific commands"的诉求。

与 #6158 的关系:#6158 用同样的 QuickJS 方案做同一功能,维护者三轮 review 提出了明确的安全边界要求,但该 PR 自 8 月 24 日起没有再推进。我们日常确实需要这个能力,所以在它的基础上重新实现并逐条落实了那三轮 review 的全部意见,另外修掉了自查中发现的一批数据正确性问题。如果 #6158 的作者愿意继续,我们也乐意配合合并到同一个 PR。

对 #6158 三轮 review 意见的处理

维护者意见 本 PR 的处理 验证
不能在普通解析失败时自动进入 QuickJS,需要显式执行入口 只有工具栏「MongoDB 脚本」按钮会进入运行时;普通 ▶ 运行遇到解析失败仍然只报错,附一句"可改用脚本执行"的提示。mongoExecuteScript 全仓库只有这一个调用点 前端回归:拼错命令、不支持的游标方法、语法错误、合法脚本用普通运行,均不调用脚本接口、不发数据库请求
生产库保护必须在后端按每次 host operation 的实际库和可变性判断,不能信任前端布尔值 每个 db.* 操作按它实际写入的库判断(含 getSiblingDB、$out/$merge);连接配置查不到时一律按生产库处理。前端只传"用户已确认生产写入",客户端传来的 productionDatabase/allowWrites 一律忽略 真机:从非生产连接用 getSiblingDB("prod") 写被标记为生产的库会被拦截,库内无写入;确认后才写入;请求里伪造旧字段同样拦截
取消/超时要区分 confirmed completed 和 unknown outcome,不能让用户误以为回滚 错误里附"已确认完成 N 个操作(写 M 个)",被打断的写标为"结果未知"并注明写入不会回滚;取消后不再发任何新请求。服务端明确拒绝的写(如 E11000)记为失败而不是未知;脚本 catch 住的写失败也在成功结果里汇总 真机:取消、超时各测,库内实际条数与报告一致(见截图 05);重复键用例、catch 用例
maxRows 要在服务端 clamp 到硬上限 服务端 clamp 到 [1, 10000](默认 1000),发请求前截断;__dbx_exec 直接传大 limit 也绕不过 单测 + 真机
源码进入 QuickJS 之前要有字节上限 256 KiB,在 Tauri 命令 / Web 路由共用的入口里、拿并发名额之前就拒绝 单测边界用例
应用级共享并发准入,验证排队/取消/超时/释放 全局 Semaphore(4);排队可取消、受 deadline 约束;名额在 QuickJS Runtime 析构之后才释放 单测覆盖排队、排队中取消、排队超时、运行中取消后释放

自查补充修复

  • BSON 类型往返:host 回包改为 canonical Extended JSON 文档数组(不再走表格化),脚本里读出再写回的文档保持 ObjectId / Int32 / Int64 / Decimal128 / Binary / Date 等类型,异构文档不会被补上 null 字段。
  • 游标不再静默截断:读取超过行数上限直接报错(提示加 limit() 或分批),cursor.count() 改发 countDocuments;只有脚本最终返回值(仅用于展示)允许截断并标记 truncated。
  • 写操作返回值:updateOne/updateMany 返回服务端真实的 matched/modified/upserted 计数;insertOne/insertMany 在客户端生成 _id 并返回 insertedId(s)。
  • 资源上限:单次 host 回包 16 MiB、最终结果 16 MiB、print 输出 10000 行 / 4 MB(按 UTF-8 字节计)。
  • 超时:使用连接的查询超时;连接未设置时服务端封顶 10 分钟。
  • 错误信息:过滤 prelude 内部栈帧,用户脚本用独立文件名执行;进度文案已做 13 种语言。
  • 构建 / CI:rquickjs 只在 rquickjs-sys 自带 bindings 的 target 上作为依赖;后端新增能力位(Tauri mongo_script_available、Web GET /api/mongo/script-available),运行时未编入时前端隐藏按钮;mongo-script 加入 ci-rust.mjs 的 CI 特性集,让运行时的单测和 clippy 真正在 CI 上跑。
  • 解析器:安全校验把 $numberInt/$numberDouble/$numberDecimal/$date{$numberLong}/$binary/$timestamp/$uuid 这类 Extended JSON 字面量算作有效过滤值(脚本发出的是 canonical EJSON)。这同样作用于普通命令,属于修正误拦截。

已知限制(设计取舍,特此说明)

  1. 值为整数的 Double(如 2.0)写回后变成 Int32,JS 区分不了 2 和 2.0,mongosh 行为相同。
  2. 内容形如 ISODate("…") 的普通字符串在 driver 写入路径会被转成日期,普通命令也是如此,本 PR 未改。
  3. aggregate / distinct 返回 relaxed EJSON,大于 2^53 的整数在 JS 里会丢精度。
  4. 生产库写入确认是对整段脚本一次性放行。
  5. 脚本里 runCommand、drop、无过滤条件的删除/更新、$out/$merge 一律拦截,需作为单独命令执行;db.getCollectionNames()、db.adminCommand() 暂不支持,会给出明确的"not supported in DBX scripts"错误。
  6. Windows arm64 与 win7 target 不包含该运行时(rquickjs-sys 没有这两个 target 的预生成 bindings),这两个平台的桌面版会隐藏脚本按钮。本机无法交叉编译,这两个 target 的构建请以 CI 结果为准。

包体积:macOS arm64 release 版 dbx-web 开启 mongo-script 后增加约 638 KiB(+1.17%)。

变更类型 / Change Type

  • 新功能 / New feature
  • CI / 构建 / CI or build

涉及前端 / Frontend Changes

  • 本 PR 涉及前端改动,已附截图/录屏(见下方) / This PR includes frontend changes and screenshots or a recording are attached below.

真机截图(web 模式,MongoDB 8.2.3)见下方评论。

验证 / Validation

  • make check 通过 / make check passes(connection-types / format / lint / typecheck / vitest 全量)

  • make cargo-check-fast 通过 / make cargo-check-fast passes

  • 相关测试通过 / Relevant tests pass

  • CI 同款 clippy(ci-rust.mjs clippy workspace full,-D warnings):开启和关闭 mongo-script 两种特性组合均无警告

  • Rust 单测:dbx-core mongo_script 38/38;dbx-driver-mongodb mongo_shell / mongo_driver 全部通过

  • 真机集成测试 crates/dbx-core/tests/live_mongodb_script.rs(MongoDB 8.2.3):12/12,覆盖类型往返、异构文档、行数上限、写返回值、重复键分类、catch 后统计、生产库 getSiblingDB 拦截、取消/超时进度报告

  • 手工验证(web 模式):按钮与 tooltip、IIFE 脚本结果、普通运行只提示、超上限报错、取消时的进度报告、生产库确认框、写入统计消息

关联 Issue / Related Issue

Fixes #3050
Related #6158

…ntime

Add an explicit "Run as MongoDB script" toolbar action for MongoDB tabs
that executes multi-statement shell JavaScript (IIFE, var/let, loops,
forEach, toArray, print) in a sandboxed QuickJS runtime behind the new
`mongo-script` feature. The regular Run button never enters the runtime:
parser failures stay failures and only hint at the script action.

Safety boundaries (per the review on t8y2#6158):
- Every host operation is checked on the backend against the database it
  actually targets (getSiblingDB, $out/$merge included); unknown
  connections are treated as production. The client only reports that
  the user confirmed production writes; client-sent policy flags are
  ignored.
- Cancellation and timeouts report confirmed completed operations versus
  writes with an unknown outcome, and state that writes are not rolled
  back. Server-rejected writes (e.g. E11000) are reported as failed, not
  unknown. Caught write failures are still summarized on success.
- Server-side limits: maxRows clamped to [1, 10000], 256 KiB source
  limit before parsing, 16 MiB per host reply and per result, bounded
  print output, 10 minute ceiling on the connection query timeout.
- A global permit (4 concurrent runtimes) with cancellable, deadline
  aware queueing; permits are released only after the runtime is dropped.

Data fidelity: host replies are canonical Extended JSON document arrays
so documents read in a script and written back keep their BSON types and
heterogeneous documents gain no null fields. Cursors over the row limit
fail loudly instead of truncating, cursor.count() issues countDocuments,
insert generates client-side _id values, and update results carry the
server's matched/modified/upserted counts.

Build and CI: rquickjs is only a dependency on targets where
rquickjs-sys ships bindings, the backend exposes a capability flag
(Tauri `mongo_script_available`, Web `GET /api/mongo/script-available`)
so the button is hidden when the runtime is not compiled in, and
`mongo-script` is added to the CI feature set so the runtime tests and
clippy run on CI. Typed Extended JSON literals ($numberInt, $numberDouble,
$numberDecimal, $date{$numberLong}, $binary, $timestamp, $uuid) now count
as bounded filter values in the shell safety check.

Fixes t8y2#3050
@q396921921

Copy link
Copy Markdown
Contributor Author

真机截图(web 模式,MongoDB 8.2.3,中文界面):

1. 工具栏新增「MongoDB 脚本」按钮,tooltip 注明最长运行时间与"写入不回滚"
button-tooltip

2. 普通 ▶ 运行遇到脚本只给提示,不进入运行时
plain-run-hint

3. 用脚本按钮执行 IIFE(插入、forEach 更新、查询),结果格式与普通 find 一致
iife-result

4. forEach 一个 10,500 条的集合,超过 10,000 行上限时直接报错而不是静默截断
row-limit-error

5. 长循环写入时取消:显示"已确认 237 个写入、第 238 个结果未知、写入不回滚",库里实际 238 条
cancel-progress

6. 目标库被标记为生产库时弹出确认框,内容为整段脚本
production-confirm

7. 成功结果附带写入统计消息
write-summary

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] mongo 3.4 下执行js脚本报错 Use MongoDB-specific commands

1 participant