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
+118 -7
View File
@@ -46,6 +46,15 @@ export class KVStoreEngine implements IStorageEngine {
private txDirtyTables: Set<string> = new Set();
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
private txSchemaChanged = false;
/**
* v0.7.0: 事务行级变更记录(table → pk → put/delete)。
* commit 时按行增量 flush(此前整表 diff:大表事务改 1 行也重写全表)。
*/
private txChanges: Map<string, Map<string, 'put' | 'delete'>> = new Map();
/** v0.7.0: 无法行级追踪的表(主键变更/级联影响表)→ commit 时整表 diff */
private txFullTables: Set<string> = new Set();
/** v0.7.0: 事务内 clear 的表 → commit 时清空 KV 行 */
private txClearedTables: Set<string> = new Set();
constructor(medium?: IStorageBackend, checkpointThreshold?: number) {
this.kv = new KVStore(medium, checkpointThreshold);
@@ -119,6 +128,9 @@ export class KVStoreEngine implements IStorageEngine {
if (this.txActive) {
try { await this.rollbackTransaction(); } catch { /* ignore */ }
}
// v0.7.0: 关闭前 checkpoint(截断日志,重开更快)。
// 失败不阻塞关闭(日志已持久,重开可全量重放)。
try { await this.kv.checkpoint(); } catch { /* 数据在日志中,重开放心重放 */ }
await this.kv.close();
await this.memory.close();
this.opened = false;
@@ -239,6 +251,19 @@ export class KVStoreEngine implements IStorageEngine {
const pks = await this.memory.insert(tableName, rows);
if (this.txActive) {
this.txDirtyTables.add(tableName);
// v0.7.0: 事务内 clear 后又写入 → 清空语义被覆盖,整表 diff 兜底
if (this.txClearedTables.has(tableName)) {
this.txClearedTables.delete(tableName);
this.txFullTables.add(tableName);
return pks;
}
// v0.7.0: 行级变更记录(增量 flush)
let changes = this.txChanges.get(tableName);
if (!changes) {
changes = new Map();
this.txChanges.set(tableName, changes);
}
for (const pk of pks) changes.set(pk, 'put');
return pks;
}
// 增量持久化(原子 putMany
@@ -279,6 +304,28 @@ export class KVStoreEngine implements IStorageEngine {
const count = await this.memory.update(tableName, query, updates);
if (this.txActive) {
this.txDirtyTables.add(tableName);
// v0.7.0: 行级变更记录 —— 主键变更无法行级追踪(旧键删除+新键落盘+级联),
// 相关表整表 diff 兜底;普通更新记录受影响行
if (pkChanged) {
for (const t of await this.affectedTables(tableName)) {
this.txFullTables.add(t);
this.txDirtyTables.add(t);
}
} else {
let changes = this.txChanges.get(tableName);
if (!changes) {
changes = new Map();
this.txChanges.set(tableName, changes);
}
for (const pk of affected) changes.set(pk, 'put');
// 级联影响表(理论上非主键更新不级联,防御性兜底)
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName) {
this.txFullTables.add(t);
this.txDirtyTables.add(t);
}
}
}
return count;
}
@@ -328,6 +375,19 @@ export class KVStoreEngine implements IStorageEngine {
const count = await this.memory.delete(tableName, query);
if (this.txActive) {
this.txDirtyTables.add(tableName);
// v0.7.0: 行级变更记录 —— 删除行记录 delete;级联影响表整表 diff 兜底
let changes = this.txChanges.get(tableName);
if (!changes) {
changes = new Map();
this.txChanges.set(tableName, changes);
}
for (const pk of pks) changes.set(pk, 'delete');
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName) {
this.txFullTables.add(t);
this.txDirtyTables.add(t);
}
}
return count;
}
const puts: Record<string, ArrayBuffer> = {};
@@ -353,6 +413,9 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.clear(tableName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
// v0.7.0: 事务内清空 → commit 时删除全部 KV 行(比整表 diff 更高效)
this.txClearedTables.add(tableName);
this.txChanges.delete(tableName);
return;
}
const diff = await this.collectTableDiff(tableName);
@@ -391,27 +454,67 @@ export class KVStoreEngine implements IStorageEngine {
this.txActive = true;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
this.txChanges = new Map();
this.txFullTables = new Set();
this.txClearedTables = new Set();
}
async commitTransaction(): Promise<void> {
this.ensureOpen();
if (!this.txActive) throw new DatabaseError('No active transaction', 'TX_NONE');
// v0.6.1: 全部 dirty 表合并为单次原子 flush(一条日志记录 = 真原子,
// 多表事务中途崩溃/失败不会出现"部分表已提交")
// 多表事务中途崩溃/失败不会出现"部分表已提交")
// v0.7.0: 行级增量 flush —— 普通 insert/update/delete 仅写事务内改动的行
// (此前 collectTableDiff 整表重写:大表事务改 1 行也 O(表大小));
// 主键变更/级联影响表整表 diff 兜底;clear/drop 表只删 KV 行。
const puts: Record<string, ArrayBuffer> = {};
const deletes: string[] = [];
for (const table of this.txDirtyTables) {
if (await this.memory.hasTable(table)) {
const diff = await this.collectTableDiff(table);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
} else {
if (!(await this.memory.hasTable(table))) {
// 事务内 drop 的表:清理 KV 残留行
const all = await this.kv.getAll();
const prefix = this.rowPrefix(table);
for (const [key] of all) {
if (key.startsWith(prefix)) deletes.push(key);
}
continue;
}
if (this.txClearedTables.has(table)) {
// 事务内 clear 的表:删除全部 KV 行
const all = await this.kv.getAll();
const prefix = this.rowPrefix(table);
for (const [key] of all) {
if (key.startsWith(prefix)) deletes.push(key);
}
continue;
}
if (this.txFullTables.has(table)) {
const diff = await this.collectTableDiff(table);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
continue;
}
const changes = this.txChanges.get(table);
if (changes && changes.size > 0) {
// v0.7.0: 增量 flush —— 单次全表扫描 + 变更集合过滤
const schema = await this.memory.getTableSchema(table);
if (!schema) continue;
const pkCol = this.getPK(schema);
const pending = new Map(changes);
const rows = await this.memory.find(table, { table: table });
for (const row of rows) {
const pkStr = String(row[pkCol]);
const kind = pending.get(pkStr);
if (kind !== undefined) {
if (kind === 'put') puts[this.rowKey(table, pkStr)] = enc(JSON.stringify(row));
else deletes.push(this.rowKey(table, pkStr));
pending.delete(pkStr);
}
}
// 内存中已不存在的行(后续操作删除)→ KV 行删除
for (const [pk, kind] of pending) {
if (kind === 'delete') deletes.push(this.rowKey(table, pk));
}
}
}
await this.kv.writeBatch(puts, deletes);
@@ -419,10 +522,15 @@ export class KVStoreEngine implements IStorageEngine {
if (this.txSchemaChanged) {
await this.persistSchema();
}
await this.kv.checkpoint();
// v0.7.0-perf: 移除每次 commit 的强制全量 checkpoint —— KVStore 按日志阈值
// 自动 checkpoint(日志重放保证崩溃恢复正确),大库高频事务不再 O(库大小)。
// close() 时统一 checkpoint(截断日志,重开更快)。
await this.memory.commitTransaction();
this.txActive = false;
this.txDirtyTables = new Set();
this.txChanges = new Map();
this.txFullTables = new Set();
this.txClearedTables = new Set();
}
async rollbackTransaction(): Promise<void> {
@@ -432,6 +540,9 @@ export class KVStoreEngine implements IStorageEngine {
this.txActive = false;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
this.txChanges = new Map();
this.txFullTables = new Set();
this.txClearedTables = new Set();
}
// ---- 内部 ----