Repository navigation
perf/reliability: seed + data-import bypass the engine's bulk-insert path → N round-trips, N summary recomputes, no transient retry #2678
Description
Summary
The two bulk-write paths — the seed loader (defineDataset replay) and the user-facing data import (CSV / bulk records) — write records one at a time through the single-record engine path, even though ObjectQL's engine already has an efficient batched branch (driver.bulkCreate + parent-deduplicated summary recompute). Over a remote DB (turso) this makes both:
- slow — N round-trips + N summary recomputes instead of ~N/batch, and
- fragile — each round-trip can hit a transient network error that is caught → the row is dropped → no retry.
Surfaced while debugging the 2026-07-06 HotCRM first-boot incident: a HotCRM seed's crm_contact inserts intermittently dropped rows on transient turso errors → the seed reported errors → the runtime's seed-once stamp never landed → per-env kernel churn (re-seed every boot). Non-deterministic (network-sensitive): the same seed inserted 5/5 contacts on one env and 1/5 on another.
Evidence
Seed loader — per record
packages/metadata-protocol/src/seed-loader.ts — loadDataset() loops per record calling this.engine.insert(objectName, record, opts) with a single record. No array / bulk form is ever constructed.
Data import — per record
Entry points (packages/rest/src/rest-server.ts):
POST /data/:object/import(sync, ≤5k rows) — ~L3524POST /data/:object/import/jobs(async, 5k–50k, persistssys_import_job) — ~L3598POST /data/:object/createMany(optional bulk endpoint, may be unwired) — ~L5749
Both sync + async delegate to packages/rest/src/import-runner.ts runImport(), which loops per row calling p.createData({...}) / p.updateData({...}) with a single record (~L201–274). It does not construct arrays or use the engine's bulk branch.
The engine ALREADY has the efficient bulk primitive
packages/objectql/src/engine.ts:
insert(object, data: any | any[])— theArray.isArray(hookContext.input.data)branch (~L2158) routes todriver.bulkCreate(...)— one round-trip per batch.recomputeSummaries()(~L1746–1782) deduplicates parent ids across the whole array and recomputes once per unique parent (1000 children of the same parent = 1 rollup recompute, not 1000). Called once perinsert()call (~L2206).
Because neither seed nor import passes arrays, both get N single-record insert() calls → N round-trips + N recomputeSummaries() invocations.
No transient-error retry (silent row loss)
import-runner.ts~L260:catch → results.push({ ok:false, action:'failed', ... })→ continue. No retry.seed-loader.ts~L413:catch → errors.push(...)→ continue. No retry.
A transient driver/network blip (fetch failed / timeout / connection reset) on one row silently drops that row. Import returns a per-row report, so it's visible, but there's no automatic recovery.
Impact
Import 1000 rows into an object with a rollup summary, over a remote turso DB (~100ms/round-trip):
- 1000 inserts + up to 1000 summary recomputes = 2000+ round-trips ≈ 3+ minutes, plus a handful of transient failures that drop rows.
- With K rollup parents: 1000 inserts + K×1000 aggregate queries.
The seed variant additionally triggered kernel churn in the HotCRM incident (dropped seed rows → errs>0 → the cloud seed-once stamp never lands → re-seed every cold boot).
Proposed fix
A shared batched-write helper used by BOTH seed-loader and import-runner:
- Batch rows (e.g. 100–500/batch) and call the engine's array/bulk
insert→driver.bulkCreate(one round-trip per batch). The engine already dedups summary recomputes for arrays. - Retry TRANSIENT driver errors (network / timeout / connection reset) per batch with backoff. Do not retry logical errors (validation / constraint).
- On a batch that fails on a logical error, degrade to per-row within that batch so one bad row doesn't drop the whole batch (bad row reported, others succeed).
- (Optional) collapse summary recompute to once per parent at the end of the whole load.
Related runtime-side follow-up (cloud, tracked separately): the seed-once stamp guard is errs===0 — a single transient error blocks the stamp forever → churn; relax to stamp-on-progress + surface residual errors via reportObserved.
Acceptance criteria
- Seed + import route through the shared bulk helper; a 1000-row load over a
bulkCreate-capable driver issues ~ceil(N/batch) writes, not N. - Summary-recompute count is bounded by unique parents, not row count.
- A transient driver error is retried and does not drop the row; a logical error is reported per-row without failing the batch.
- Tests: bulk-path write-count assertion, transient-retry, logical-error per-row degradation, summary-dedup.
Refs
- HotCRM new-workspace "打开环境一直失败" incident — cloud#752 / Update Spec priority Roadmap (2026.02) #754 / feed api #756, framework#2661.
Activity
fix(driver-sql): bulkCreate must assign a client-side id like create(…
perf(seed,import): route bulk writes through the engine's batch inser…
baozhoutao commentedon Jul 17, 2026
#2678 / PR #2680 批量写入路径 — 补充测试报告(2026-07-17,main @ 021ba4c)
021ba4caf(2026-07-17,worktree 与 origin/main 完全一致)git status全程 clean);③ 关键结论均以引擎/加载器真实源码路径执行验证,非 mock 行为推断一、结论摘要
问题一(批量写入与汇总去重):通过。 批次切分、批次封顶、汇总按唯一父去重、混父批次正确性、降级收敛全部符合设计。
问题二(暂时性错误重试):部分通过。 批量主路径、import update 路径、seed upsert 更新路径的重试均有效;但 4 个已知未修缺陷(#2803/#2804/#2805/#2806)在当前 HEAD 全部复现,另新发现 8 个问题(6 个后端 + 2 个前端 UI),合计 12 项缺陷(见第四、七节)。
真机 UI 实测(第七节): 全新 SQLite 冷启动的 showcase 上,种子加载、导入向导、坏行隔离、服务端 dry-run、汇总重算均通过并有截图证据;#2806 在最新 framework + 最新 objectui 上 UI 级复现;另实测发现两个 objectui 缺陷——必填未映射时向导禁用无提示(缺陷 11)、旧版 console 静默降级路径把原始自然键写进 FK 且报成功(缺陷 12,数据损坏级)。
hook / 流程自动化 / 公式 / 累积汇总与批量路径的交互: 逐行 hook 语境(#2922)、skipTriggers/skipAutomations 抑制、流程逐行触发均正确;但 beforeInsert 类 hook 在批降级时双跑(缺陷 7),汇总重算失败被静默吞掉(缺陷 10)。公式为读时虚拟计算,与写入路径无交互,无需测试。
二、正式测试套件结果(全绿)
三、场景矩阵与结果
A. bulkWrite 核心助手(packages/core)
B. import-runner(packages/rest)
C. seed-loader(packages/metadata-protocol)
D. 引擎 array-insert 分支(packages/objectql)
ctx.input.data派生字段落库;每行恰 1 次T. 流程自动化 × 批量路径
{skipTriggers:true}逐行传至ctx.session,record-change 触发器全抑制{skipAutomations:true}:metadata 绑定 hook(flow)被引擎跳过,代码注册 hook(审计/安全)照常S. 累积汇总 × 批量路径
公式字段
读时虚拟计算(find 投影时求值),插入路径不参与 → 与批量写入无交互,无测试面。
四、缺陷清单(按严重度排序)
column network_id…、…row 503、CHECK constraint failed: network_zone等逻辑错误被误判 transient。后果仅多重试两轮,不丢数据undefined返回(engine.ts L2330),afterInsert 拿 undefined 不报错;下游 seed externalId 映射静默缺失、import 行报 created 无 id。#2803 在引擎层的镜像共性根因归纳:① "吞错报成功"模式(缺陷 1、10,及 9 的静默补位)——失败只 warn 不上报;② 重试覆盖不全(缺陷 3、4,及 1、10 的无重试)——withTransientRetry 只包了部分写点;③ 无幂等/契约防御(缺陷 5、8、9)——重试语义是 at-least-once 但无去重手段,批量响应无校验。
五、修复建议(按投入产出排序)
withTransientRetry;最终失败必须计入 errors/result(至少给调用方信号)。withTransientRetry,与既有 update 路径对齐。六、对 QA 测试资产的建议
现有 QA 对象(qa_seed_item / qa_import_item / qa_summary_*)全为裸字段,测不出缺陷 7/8/10 类问题。建议补充:
七、真机 UI 实测(第四轮,2026-07-17)
环境:showcase 示例应用,
objectstack dev --ui --seed-admin -p 3777,全新 SQLite(/tmp一次性库),首次冷启动;console 分别用 6/29 vendored build(:3777)与 objectui 当前 main(b4ef588,HMR :5181)双版本对照。全程浏览器以 Dev Admin 真实操作,截图取证(仅留本地/对话,未进仓库)。通过项(带 UI 证据)
not-a-number)→ 预览红框高亮可改;导入结果「已导入 3 条 / 已跳过 1 条」+ 行级错误 + toast;DB 精确 14→17,坏行未入库/import导入 2 条发票行(共 200)→ INV-1001 total 1619.96→1819.96,详情页「合计」可见真机确认与新发现的缺陷(续编号)
no showcase_category matches "QA-2806B-New-Parent";DB 确认引用已存在记录的控制行正确解析、同文件引用行被丢。与组件级探针结果一字不差。缓解:「校验数据」dry-run 能提前预警(但报错原因对用户不可理喻——父记录明明就在同一文件里)。中(objectui,可用性):字段映射步骤,必填字段未在 CSV 中出现时「下一步」禁用但无任何提示(ImportWizard.tsx:2282 的
missingRequired只用于禁用,从不渲染)。真实操作中我因缺status列卡住数分钟,无从得知缺哪个字段。当前 objectui main 源码属实。高(objectui,数据损坏):6/29 版 console 的导入向导(无
/import能力时的静默逐行 create 降级路径)不做引用解析、不做类型强转:3 行分类 CSV 全部报「已导入 3 条」,但 FK 列里写入的是原始自然键字符串(parent='QA-2806-New-Parent'),空值写成'',连引用已存在记录的行都没解析——网格「上级」列显示"—",损坏无声无息。当前 objectui main 仍保留同一「older adapter → silently fall back」分支(ImportWizard.tsx ~L1942),任何/import不可达的环境(旧服务端/适配器不匹配)都会静默重现。建议:fallback 至少显式告警降级,且 lookup 列在 fallback 下应拒绝导入而非原样写入。真机环境坑(记录备查)
pnpm install+ build@objectstack/connector-*(嵌套目录,--filter './packages/*'覆盖不到)。../../objectui),需临时符号链接(测试后已删)。objectstack dev静默退避 +1 端口(3777→3778),易造成"旧实例假象",起服务后必须核对日志端口。八、覆盖缺口声明
九、过程记录
git statusclean。baozhoutao commentedon Jul 17, 2026
缺陷立项汇总(上述报告中的问题,均为独立新 issue)
framework
objectui
并行修复提示:#3148 与 #3150 都改
import-runner.ts,#3147/#3151/#3152 都涉及engine.ts——建议分波执行避免同文件冲突(第一波 #3147/#3148/#3149 + objectui 两单,第二波 #3150/#3151/#3152/#2805)。