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 均通过。
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
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))
|
||||
})
|
||||
Reference in New Issue
Block a user