Files
MetonaSqlark/src/sql/params.ts
T
thzxx 05e6823bf1
CI / test (22.x) (push) Successful in 24m26s
CI / e2e (push) Successful in 10m0s
CI / test (18.x) (push) Successful in 27m14s
CI / test (20.x) (push) Failing after 1h19m9s
CI / test (24.x) (push) Successful in 37m49s
fix: v0.7.2 语句级原子性 + 事务 DDL 拒绝 + 约束/绑定硬化 — 6 项修复 + 43 回归 + CI 重型套件串行
- 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% 保持
2026-08-13 15:31:16 +08:00

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