- UPDATE 语句级部分提交(P1,四引擎):两阶段全量预检后执行,批内唯一互查, 任何一行失败整句不执行(aria 场景 WAL 与内存不再错位) - 事务内 ALTER/CREATE INDEX/DROP INDEX 残留(P1):Memory/KVStore 显式拒绝 (对齐 Aria),createTable/dropTable 保持可回滚 - SET NULL 级联绕过 required 约束(P1):预检阶段整体拒绝 FOREIGN_KEY_VIOLATION - bindParameters 注释误判(P2):行注释/块注释中的 ? 与引号不再参与绑定 - 未闭合字符串静默接受 → lexer 抛 PARSE_ERROR;未知 where 操作符抛 QUERY_ERROR - UPDATE undefined 覆盖列值 → 语义化为不更新(null 仍置空) - Hybrid 写穿透非原子(P1):磁盘失败自动重载内存对齐磁盘再抛原错误 - CI:Run tests 拆常规并行 + 重型串行(runInBand),重型测试超时余量提升, 性能护栏 kv 120→240s / opfs 150→300s(仍拦截悬崖回归) - 测试 1155 → 1198(74 套件),覆盖率 89.82% 保持
125 lines
3.7 KiB
TypeScript
125 lines
3.7 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|