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)) })