Files
order_site/apps/backend/src/db/sql-arity.test.ts
T
yml2213 30ae35d3a5 fix(consignment): 撤销寄售履约回库分支必 500 —— SQL 参数比占位符多一个
线上客服 kf_13 撤销 WO26092609475746 时返回 500 internal_error,
真实库报:bind message supplies 5 parameters, but prepared statement "" requires 4。

根因:解绑现货的两条分支共用一个 5 元参数数组,但回库/取消分支的 SQL 只用到
$1..$4(第 5 个「备注前缀」只有作废分支才用)。于是运营只要选
「退单回库并恢复工单为未指派」或「退单回库并取消工单」就必定 500,
而假 client 的单测与类型检查都看不到这类错。

修复:
- 解绑 SQL 与参数抽到 consignment-revoke-inventory-sql.ts 一起构造,
  回库/取消分支只传 4 个参数,作废分支才多带备注前缀($5);
- 单测锁定「占位符个数 == 参数个数」,覆盖 reopen / archive_inventory /
  已确认加好友自动作废三条分支;
- db 层加参数个数自检(db/sql-arity.ts + db/client.ts):参数比占位符多时
  提前抛出带 SQL 片段的错误,不再让 pg 的 bind 报错裸奔,
  并跳过字符串/注释/dollar-quote 里的 $n,避免误杀。

验证:本地影子库端到端跑通三条分支(工单 100140 + 现货 INV26091917439822),
并复现修复前的 bind 报错;全量单测 588 通过,typecheck/lint/build 均通过。
2026-10-03 16:03:14 +08:00

55 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import test from 'node:test'
import { assertSqlParamArity, maxSqlPlaceholderIndex } from './sql-arity.js'
test('统计最大占位符序号', () => {
assert.equal(maxSqlPlaceholderIndex('SELECT 1'), 0)
assert.equal(maxSqlPlaceholderIndex('SELECT * FROM t WHERE id = $1'), 1)
assert.equal(maxSqlPlaceholderIndex('INSERT INTO t (a, b, c) VALUES ($1, $2, $3)'), 3)
// 只用到 $1/$4 时,PostgreSQL 要求 4 个参数($2/$3 空着)
assert.equal(maxSqlPlaceholderIndex('UPDATE t SET a = $1 WHERE b = $4'), 4)
})
test('字符串字面量、注释、dollar-quote 里的 $n 不算占位符', () => {
assert.equal(maxSqlPlaceholderIndex("SELECT note FROM t WHERE note = '$1'"), 0)
assert.equal(maxSqlPlaceholderIndex("SELECT note FROM t WHERE note = 'it''s $9'"), 0)
assert.equal(maxSqlPlaceholderIndex("SELECT e FROM t WHERE e = E'\\$7'"), 0)
assert.equal(maxSqlPlaceholderIndex('SELECT 1 -- $8\nWHERE id = $1'), 1)
assert.equal(maxSqlPlaceholderIndex('SELECT 1 /* $9 */ WHERE id = $2'), 2)
assert.equal(maxSqlPlaceholderIndex('SELECT "col$5" FROM t WHERE id = $1'), 1)
assert.equal(maxSqlPlaceholderIndex('SELECT $$ $6 $$ FROM t WHERE id = $1'), 1)
assert.equal(maxSqlPlaceholderIndex('SELECT $tag$ $7 $tag$ FROM t WHERE id = $1'), 1)
})
test('参数比占位符多时必须提前报错(撤销回库分支的线上 500)', () => {
const reopenSql = `
UPDATE consignment_inventories
SET remark = remark || ' [撤销回库: ' || $1 || ']', updated_at = $2
WHERE id = $3 AND status = 'consumed' AND bound_work_order_id = $4
`
assert.throws(
() => assertSqlParamArity(reopenSql, ['原因', '时间', 23, 100140, ' [交付异常作废: ']),
/提供了 5 个参数,语句最多只用到 \$4/,
)
// 对齐参数后放行;作废分支带 $5 时 5 个参数同样放行
assert.doesNotThrow(() => assertSqlParamArity(reopenSql, ['原因', '时间', 23, 100140]))
assert.doesNotThrow(() =>
assertSqlParamArity('UPDATE t SET remark = $1 || $5, updated_at = $2 WHERE id = $3', [
'r',
'n',
1,
2,
'p',
]),
)
})
test('占位符比参数多时不在这一层拦(交给 PostgreSQL 报 no parameter)', () => {
// SQL 文本里出现字面量 $9 时容易误判,宁可漏报也不错杀正常查询
assert.doesNotThrow(() => assertSqlParamArity('SELECT * FROM t WHERE id = $1 AND x = $9', [1]))
assert.doesNotThrow(() => assertSqlParamArity('SELECT 1', []))
assert.doesNotThrow(() => assertSqlParamArity('BEGIN', undefined))
})