/** * metona-sqlark SQL Parameters — 参数化查询绑定 * @module sql/params * * v0.7.0: `db.query(sql, params)` 位置参数(`?`)支持。 * 绑定在词法层面完成:仅替换字符串字面量之外的 `?`, * 值按 SQL 字面量编码(字符串 `''` 转义、数字/布尔/JSON 直出), * 从根上规避 SQL 注入(不经过字符串拼接由用户自行转义)。 * * v0.7.2: 词法扫描感知注释 —— 行注释(`--`)与块注释(slash-star 包裹)中的 `?` * 与引号不再参与占位符识别与字符串状态机(此前注释中的 `?` 计入占位符导致 * PARAM_ERROR 错位、注释中的单引号触发 "Unterminated string literal")。 */ import { DatabaseError } from '../constants'; /** 将单个参数值编码为 SQL 字面量 */ function encodeParam(value: unknown): string { if (value === null || value === undefined) return 'NULL'; if (typeof value === 'number') { if (Number.isFinite(value)) return String(value); return 'NULL'; // NaN/Infinity 无 SQL 字面量 → NULL } if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE'; if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`; // 对象/数组无 SQL 字面量(SQL 方言不支持 json 字面量),显式拒绝而非静默错配 throw new DatabaseError( 'Object/array query parameters are not supported by SQL binding (pass JSON strings explicitly)', 'PARAM_ERROR', ); } /** * 将 SQL 中的位置参数 `?`(字符串字面量与注释之外)替换为编码后的字面量。 * @param sql 含 `?` 占位符的 SQL * @param params 位置参数数组 * @throws PARAM_ERROR 参数数量不匹配 */ export function bindParameters(sql: string, params?: unknown[]): string { // undefined = 不启用绑定;[] + 含 ? 的 SQL 由循环内报 PARAM_ERROR if (!params) return sql; let out = ''; let i = 0; let pIdx = 0; let quote: string | null = null; while (i < sql.length) { const ch = sql[i]; if (quote !== null) { out += ch; if (ch === quote) { // SQL 标准 '' 转义:两个连续引号 = 一个引号(原样保留) if (sql[i + 1] === quote) { out += sql[i + 1]; i += 2; continue; } quote = null; } i++; continue; } if (ch === "'" || ch === '"') { quote = ch; out += ch; i++; continue; } // v0.7.2: 行注释 `-- ...`(含其中的 ? 与引号)原样保留、不参与绑定 if (ch === '-' && sql[i + 1] === '-') { while (i < sql.length && sql[i] !== '\n' && sql[i] !== '\r') { out += sql[i]; i++; } continue; } // v0.7.2: 块注释(slash-star 包裹)同样跳过 if (ch === '/' && sql[i + 1] === '*') { out += sql[i] + sql[i + 1]; i += 2; while (i < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) { out += sql[i]; i++; } if (i < sql.length) { out += sql[i] + sql[i + 1]; i += 2; } continue; } if (ch === '?') { if (pIdx >= params.length) { throw new DatabaseError( `Too few query parameters: placeholder #${pIdx + 1} has no value (got ${params.length} total)`, 'PARAM_ERROR', ); } out += encodeParam(params[pIdx]); pIdx++; i++; continue; } out += ch; i++; } if (quote !== null) { throw new DatabaseError('Unterminated string literal in SQL', 'PARSE_ERROR'); } if (pIdx < params.length) { throw new DatabaseError( `Too many query parameters: ${params.length} provided but only ${pIdx} placeholders`, 'PARAM_ERROR', ); } return out; }