feat: v0.7.0 参数化查询 + 事务增量 flush + 复合主键语义硬化 + EXPLAIN 真实索引信息
- 参数化查询 db.query(sql, params):词法层 ? 绑定 + SQL 字面量安全编码 ('' 转义/注入防护);参数计数不匹配 PARAM_ERROR;对象参数显式拒绝 - KVStoreEngine 事务增量 flush:行级变更追踪,commit 仅写改动行 (1000 行表改 1 行:日志 1 条目 vs 整表 1000 条目);级联影响表漏写修复 (txFullTables 同步加入 txDirtyTables);移除每次 commit 全量 checkpoint (阈值自动 checkpoint + close 统一截断) - 复合主键显式拒绝:createSchema 校验期 SCHEMA_ERROR + ALTER ADD 主键列防护 (此前静默取第一个主键,其余标记失效) - EXPLAIN usingIndex 真实命中信息:pk / index:col / none 测试 1126 → 1147(72 套件);行覆盖率 89.8%;版本 0.7.0
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* metona-sqlark SQL Parameters — 参数化查询绑定
|
||||
* @module sql/params
|
||||
*
|
||||
* v0.7.0: `db.query(sql, params)` 位置参数(`?`)支持。
|
||||
* 绑定在词法层面完成:仅替换字符串字面量之外的 `?`,
|
||||
* 值按 SQL 字面量编码(字符串 `''` 转义、数字/布尔/JSON 直出),
|
||||
* 从根上规避 SQL 注入(不经过字符串拼接由用户自行转义)。
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user