问题
bulkWrite 使用 writeBatch 的返回数组,按位置把记录关联回输入行。如果 writeBatch 成功返回的数据违反类型、数量或顺序契约,当前实现仍可能报告成功、丢弃多余记录,或者把记录 ID 关联到错误的输入行。
复现规则
使用真实的 bulkWrite 实现进行测试。测试输入包含 1 个字段 key、2 行数据,字段值分别为 a 和 b。为便于阅读,固定输入可等价表示为以下 CSV:
批次大小为 2,因此两行会通过一次 writeBatch 调用提交。以上 CSV 仅用于直观展示输入数据,实际执行方式以后面的完整复现命令为准。
四个场景只改变 writeBatch 的成功返回值。writeBatch 均正常完成,不抛出异常,也不模拟网络错误。writeOne 只用于记录是否进入逐行降级路径。执行后检查每个输入行对应的 ok、recordId 和 writeOneCalls。
| 场景 |
writeBatch 的成功返回值 |
实际结果 |
writeOneCalls |
| 返回记录不足 |
[{ id: 'id_a' }] |
a → id_a, ok=true;b → null, ok=true |
0 |
| 返回记录过多 |
[{ id: 'id_a' }, { id: 'id_b' }, { id: 'id_extra' }] |
a → id_a;b → id_b;id_extra 不在最终结果中 |
0 |
| 返回值不是数组 |
{ records: [{ id: 'id_a' }, { id: 'id_b' }] } |
a → null, ok=true;b → null, ok=true |
0 |
| 返回顺序相反 |
[{ id: 'id_b' }, { id: 'id_a' }] |
a → id_b, ok=true;b → id_a, ok=true |
0 |
完整复现命令及实际输出
返回记录少于输入记录
pnpm exec tsx -e 'import { bulkWrite } from "./packages/core/src/utils/bulk-write.ts"; void (async () => { const rows = [{ key: "a" }, { key: "b" }]; let writeOneCalls = 0; const results = await bulkWrite(rows, { batchSize: 2, writeBatch: async () => [{ id: "id_a" }], writeOne: async (row) => { writeOneCalls++; return { id: "single_" + row.key }; }, sleep: async () => {} }); console.log(JSON.stringify({ writeOneCalls, results: results.map((r) => ({ index: r.index, input: rows[r.index].key, ok: r.ok, recordId: r.record?.id ?? null })) }, null, 2)); })();'
实际输出:
{
"writeOneCalls": 0,
"results": [
{
"index": 0,
"input": "a",
"ok": true,
"recordId": "id_a"
},
{
"index": 1,
"input": "b",
"ok": true,
"recordId": null
}
]
}
返回记录多于输入记录
pnpm exec tsx -e 'import { bulkWrite } from "./packages/core/src/utils/bulk-write.ts"; void (async () => { const rows = [{ key: "a" }, { key: "b" }]; const returned = [{ id: "id_a" }, { id: "id_b" }, { id: "id_extra" }]; let writeOneCalls = 0; const results = await bulkWrite(rows, { batchSize: 2, writeBatch: async () => returned, writeOne: async (row) => { writeOneCalls++; return { id: "single_" + row.key }; }, sleep: async () => {} }); console.log(JSON.stringify({ returnedRecordIds: returned.map((r) => r.id), writeOneCalls, results: results.map((r) => ({ index: r.index, input: rows[r.index].key, ok: r.ok, recordId: r.record?.id ?? null })) }, null, 2)); })();'
实际输出:
{
"returnedRecordIds": [
"id_a",
"id_b",
"id_extra"
],
"writeOneCalls": 0,
"results": [
{
"index": 0,
"input": "a",
"ok": true,
"recordId": "id_a"
},
{
"index": 1,
"input": "b",
"ok": true,
"recordId": "id_b"
}
]
}
返回值不是数组
pnpm exec tsx -e 'import { bulkWrite } from "./packages/core/src/utils/bulk-write.ts"; void (async () => { const rows = [{ key: "a" }, { key: "b" }]; let writeOneCalls = 0; const results = await bulkWrite(rows, { batchSize: 2, writeBatch: async () => ({ records: [{ id: "id_a" }, { id: "id_b" }] } as any), writeOne: async (row) => { writeOneCalls++; return { id: "single_" + row.key }; }, sleep: async () => {} }); console.log(JSON.stringify({ writeOneCalls, results: results.map((r) => ({ index: r.index, input: rows[r.index].key, ok: r.ok, recordId: r.record?.id ?? null })) }, null, 2)); })();'
实际输出:
{
"writeOneCalls": 0,
"results": [
{
"index": 0,
"input": "a",
"ok": true,
"recordId": null
},
{
"index": 1,
"input": "b",
"ok": true,
"recordId": null
}
]
}
返回顺序与输入顺序不同
pnpm exec tsx -e 'import { bulkWrite } from "./packages/core/src/utils/bulk-write.ts"; void (async () => { const rows = [{ key: "a" }, { key: "b" }]; let writeOneCalls = 0; const results = await bulkWrite(rows, { batchSize: 2, writeBatch: async () => [{ id: "id_b" }, { id: "id_a" }], writeOne: async (row) => { writeOneCalls++; return { id: "single_" + row.key }; }, sleep: async () => {} }); console.log(JSON.stringify({ writeOneCalls, results: results.map((r) => ({ index: r.index, input: rows[r.index].key, ok: r.ok, recordId: r.record?.id ?? null })) }, null, 2)); })();'
实际输出:
{
"writeOneCalls": 0,
"results": [
{
"index": 0,
"input": "a",
"ok": true,
"recordId": "id_b"
},
{
"index": 1,
"input": "b",
"ok": true,
"recordId": "id_a"
}
]
}
## 实际现象
- 返回记录不足时,缺少返回记录的输入行仍被标记为
ok=true。
- 返回记录过多时,多余的
id_extra 没有出现在最终结果中。
- 返回值不是数组时,两行输入都被标记为
ok=true,但 recordId 均为 null。
- 返回顺序相反时,输入行
a 被关联到 id_b,输入行 b 被关联到 id_a。
- 四个场景的
writeOneCalls 均为 0。
判定依据
因此,以上四种返回值均违反了明确的批量响应协议;当前结果仍出现 ok=true、记录缺失、结果被丢弃或记录 ID 错误关联,判定为 Bug。
影响
这可能造成 Import 错误报告成功、记录 ID 关联到错误的源数据行,或者已创建记录未被纳入 Undo 跟踪。
证据边界
以上四种情况均已通过受控的 writeBatch 返回值在真实 bulkWrite 实现上复现。该证据证明了协议违规响应的处理结果,但不能证明内置 SQL 驱动在正常运行中会自然返回这些异常或乱序响应。
问题
bulkWrite使用writeBatch的返回数组,按位置把记录关联回输入行。如果writeBatch成功返回的数据违反类型、数量或顺序契约,当前实现仍可能报告成功、丢弃多余记录,或者把记录 ID 关联到错误的输入行。复现规则
使用真实的
bulkWrite实现进行测试。测试输入包含 1 个字段key、2 行数据,字段值分别为a和b。为便于阅读,固定输入可等价表示为以下 CSV:key a b批次大小为 2,因此两行会通过一次
writeBatch调用提交。以上 CSV 仅用于直观展示输入数据,实际执行方式以后面的完整复现命令为准。四个场景只改变
writeBatch的成功返回值。writeBatch均正常完成,不抛出异常,也不模拟网络错误。writeOne只用于记录是否进入逐行降级路径。执行后检查每个输入行对应的ok、recordId和writeOneCalls。writeBatch的成功返回值writeOneCalls[{ id: 'id_a' }]a → id_a, ok=true;b → null, ok=true[{ id: 'id_a' }, { id: 'id_b' }, { id: 'id_extra' }]a → id_a;b → id_b;id_extra不在最终结果中{ records: [{ id: 'id_a' }, { id: 'id_b' }] }a → null, ok=true;b → null, ok=true[{ id: 'id_b' }, { id: 'id_a' }]a → id_b, ok=true;b → id_a, ok=true完整复现命令及实际输出
返回记录少于输入记录
实际输出:
{ "writeOneCalls": 0, "results": [ { "index": 0, "input": "a", "ok": true, "recordId": "id_a" }, { "index": 1, "input": "b", "ok": true, "recordId": null } ] }返回记录多于输入记录
实际输出:
{ "returnedRecordIds": [ "id_a", "id_b", "id_extra" ], "writeOneCalls": 0, "results": [ { "index": 0, "input": "a", "ok": true, "recordId": "id_a" }, { "index": 1, "input": "b", "ok": true, "recordId": "id_b" } ] }返回值不是数组
实际输出:
{ "writeOneCalls": 0, "results": [ { "index": 0, "input": "a", "ok": true, "recordId": null }, { "index": 1, "input": "b", "ok": true, "recordId": null } ] }返回顺序与输入顺序不同
实际输出:
{ "writeOneCalls": 0, "results": [ { "index": 0, "input": "a", "ok": true, "recordId": "id_b" }, { "index": 1, "input": "b", "ok": true, "recordId": "id_a" } ] }ok=true。id_extra没有出现在最终结果中。ok=true,但recordId均为 null。a被关联到id_b,输入行b被关联到id_a。writeOneCalls均为 0。判定依据
BulkWriteOptions.writeBatch契约 明确要求:返回值必须是记录数组,每个输入行对应一条记录,并且与输入批次保持相同顺序。bulkWrite返回结果契约 明确要求:每个输入行返回一个BulkWriteRowResult,并按照原始输入顺序建立索引。createManyData协议 同样明确要求:返回{ records: any[] },每个输入行对应一条记录,并保持相同顺序。因此,以上四种返回值均违反了明确的批量响应协议;当前结果仍出现
ok=true、记录缺失、结果被丢弃或记录 ID 错误关联,判定为 Bug。影响
这可能造成 Import 错误报告成功、记录 ID 关联到错误的源数据行,或者已创建记录未被纳入 Undo 跟踪。
证据边界
以上四种情况均已通过受控的
writeBatch返回值在真实bulkWrite实现上复现。该证据证明了协议违规响应的处理结果,但不能证明内置 SQL 驱动在正常运行中会自然返回这些异常或乱序响应。