feat: v0.7.0 参数化查询 + 事务增量 flush + 复合主键语义硬化 + EXPLAIN 真实索引信息
CI / test (22.x) (push) Successful in 17m29s
CI / test (24.x) (push) Failing after 17m32s
CI / e2e (push) Successful in 9m53s
CI / test (18.x) (push) Successful in 19m17s
CI / test (20.x) (push) Successful in 18m15s

- 参数化查询 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:
thzxx
2026-08-13 10:57:26 +08:00
parent f97c5a6001
commit 57415975ea
20 changed files with 1542 additions and 69 deletions
+96
View File
@@ -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;
}