fix: v0.6.1 生产可用性深度审查 — MemoryEngine 级联环无限递归(P0)/ KVStore 多表事务与级联原子化 / 未open防护统一 / 27 个异常场景测试 + aria级联回归
CI / test (18.x) (push) Successful in 10m59s
CI / test (20.x) (push) Successful in 10m59s
CI / test (22.x) (push) Successful in 10m49s
CI / test (24.x) (push) Successful in 10m47s
CI / e2e (push) Successful in 10m4s

This commit is contained in:
thzxx
2026-08-10 14:29:40 +08:00
parent a9e1a281d3
commit 8a78f27f7d
15 changed files with 954 additions and 227 deletions
+44
View File
@@ -2,6 +2,50 @@
All notable changes to MetonaSqlark will be documented in this file.
## [0.6.1] - 2026-08-10
### 生产可用性深度审查(异常场景)
> 以"生产可用"标准全面审查:修复级联环无限递归、多表事务原子性缺口、
> 统一未 open 防护,并新增 27 个异常场景测试。
### Fixed
- **MemoryEngine 级联环无限递归(P0** — A→B→A 循环引用 + CASCADE 删除导致栈溢出
RangeError: Maximum call stack size exceeded)—— MemoryEngine.cascadeDelete 无环路保护,
AriaEngine 已有 visited 保护(v0.4.1),Memory 侧缺失。新增 visited 集合(表:主键),
与 Aria 语义对齐;A→B→A 环 + 深链 A→B→C→D 均有回归测试
- **KVStore 快照损坏水位 bugP0** — baseSeq = max(metaSeq, snapshotSeq) 在快照损坏
回退全量日志重放时,meta.seq(最后一次 checkpoint 水位)错误跳过日志中 checkpoint 后
的有效记录 → 静默丢数据。水位只信任快照内嵌 seq
- **多表事务 commit 非原子(P1** — 事务 commit 逐表 flush:中途失败/崩溃 → 部分表已提交。
重构为全部 dirty 表合并单次 putMany/deleteMany(一条日志记录 = 真原子)
- **级联更新/删除非原子(P1)** — update 主键变更/delete 的级联影响表逐表 diff 分多次写。
重构 collectTableDiff 合并到单次原子写(主表 + 全部级联表同一条日志记录)
- **未 open 防护不一致** — getTableNames/hasTable/getTableSchema 未检查 DB_NOT_OPEN
(返回空而非报错,与其余方法不一致)。统一 ensureOpen
- **测试盲区** — 快照损坏测试此前篡改未生效(close 后介质不可读),从未真正覆盖损坏路径;
修复并新增 metaSeq 超前回归测试
### Added
- 27 个生产异常场景测试(`tests/production-abnormal.test.ts`):
- KVStore 边界:空 value/空 key、1MB 大 value、中文/emoji/冒号/超长 key、未 open、
重复 open/close 幂等、clear 后写、1 万小 key、写失败后继续可用
- KVStoreEngine:未 open 全操作、重复 close、不存在表、事务嵌套 TX_ACTIVE、
无事务 TX_NONE、commit 失败回滚(磁盘无部分提交)、多表事务原子性、级联环、
深链级联、主键变更+索引一致性、空表操作、5000 行批量删除持久化
- SharedMemoryBackend 全分支:append 拼接、writeMany/deleteMany、close 后安全、跨实例共享
- 日志解析损坏分支:onCorrupt false 停止、keyLen 越界记录丢弃
- AriaEngine 级联环/深链回归测试(与 Memory 对齐)
### Changed
- 测试 1022 → **1049**(65 套件);行覆盖率 89.15% → **89.33%**
- 事务/级联写入路径:逐表 flush → 单条日志记录原子(性能与一致性双提升)
---
## [0.6.0] - 2026-08-10
### 里程碑:完全移除 IndexedDB,自研 KV 事务存储引擎
+4 -4
View File
@@ -3,8 +3,8 @@
<p align="center">
<img src="https://img.shields.io/badge/version-0.6.0-blue?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
<img src="https://img.shields.io/badge/coverage-89.15%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-1022%20passed-success?style=flat-square" alt="tests">
<img src="https://img.shields.io/badge/coverage-89.3%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-1049%20passed-success?style=flat-square" alt="tests">
</p>
> 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研存储引擎**。
@@ -34,7 +34,7 @@
- 🌲 **RB-Tree 完整实现** — 标准红黑树插入+删除修复,O(log n) 保证
-**性能优化** — SSTableReader 二分查找统一、IndexedDB 索引利用、crypto 实例化避免全局状态
- 🌐 **浏览器兼容** — Chrome 80+ / Firefox 80+ / Safari 14+ / Edge 80+ / Node.js 16+
- 🧪 **1022 测试 · 89.15% 覆盖率** — 64 套件 + 12 个 Playwright 真实 Chromium e2e(含崩溃注入),生产级质量保证
- 🧪 **1049 测试 · 89.3% 覆盖率** — 64 套件 + 12 个 Playwright 真实 Chromium e2e(含崩溃注入),生产级质量保证
---
@@ -459,7 +459,7 @@ npm run typecheck # 类型检查
|------|------|
| 测试用例 | 1022 |
| 测试套件 | 64+12 Playwright e2e |
| 行覆盖率 | 89.15% |
| 行覆盖率 | 89.3% |
| SQL 关键字 | 72 |
| 存储引擎 | 5Memory / **KVStore** / OPFS / Hybrid / **Aria** |
+106 -55
View File
@@ -729,9 +729,14 @@ class MemoryEngine {
// ---- 外键级联 ----
/**
* 级联删除查找引用 tableName.pkValue 的所有表的行并删除
* v0.6.1-fix: 环路保护ABA 级联环不再无限递归栈溢出AriaEngine 同语义
* @returns 级联删除的行数
*/
async cascadeDelete(tableName, pkValue, _row) {
async cascadeDelete(tableName, pkValue, _row, visited = new Set()) {
const visitKey = `${tableName}:${pkValue}`;
if (visited.has(visitKey))
return 0;
visited.add(visitKey);
let totalCascade = 0;
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName)
@@ -763,7 +768,7 @@ class MemoryEngine {
if (refRow) {
// v0.3.3: 级联删除前清理索引条目
this.removeIndexEntries(refTableName, refRow, refPk);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow, visited);
}
refTableData.delete(refPk);
totalCascade++;
@@ -1726,6 +1731,8 @@ class KVStoreEngine {
this.txActive = false;
/** 事务中写过的表(commit 时只 flush 这些表) */
this.txDirtyTables = new Set();
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
this.txSchemaChanged = false;
this.kv = new KVStore(medium, checkpointThreshold);
}
// ---- 行 key 编解码 ----
@@ -1850,6 +1857,7 @@ class KVStoreEngine {
await this.memory.createTable(schema);
if (this.txActive) {
this.txDirtyTables.add(schema.name);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -1859,19 +1867,27 @@ class KVStoreEngine {
await this.memory.dropTable(tableName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
// 删除该表全部行(KV 中残留清理)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0)
await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0)
await this.kv.deleteMany(diff.deletes);
}
async hasTable(tableName) {
this.ensureOpen();
return this.memory.hasTable(tableName);
}
async getTableNames() {
this.ensureOpen();
return this.memory.getTableNames();
}
async getTableSchema(tableName) {
this.ensureOpen();
return this.memory.getTableSchema(tableName);
}
async alterTable(tableName, action, column) {
@@ -1879,12 +1895,17 @@ class KVStoreEngine {
await this.memory.alterTable(tableName, action, column);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
if (action === 'DROP') {
// 重写存储行(移除该列)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0)
await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0)
await this.kv.deleteMany(diff.deletes);
}
}
// ---- CRUD ----
@@ -1929,16 +1950,18 @@ class KVStoreEngine {
this.txDirtyTables.add(tableName);
return count;
}
const puts = {};
const deletes = [];
if (pkChanged) {
// 主键变更:相关表整表 diff(罕见操作,可靠性优先)
for (const t of await this.affectedTables(tableName)) {
await this.flushTable(t);
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
}
else {
// 增量重写受影响行
const puts = {};
const deletes = [];
for (const pk of affected) {
const row = await this.memory.find(tableName, { table: tableName, where: { [pkCol]: pk } });
if (row.length > 0) {
@@ -1948,16 +1971,20 @@ class KVStoreEngine {
deletes.push(this.rowKey(tableName, pk));
}
}
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
// 级联影响表(SET NULL/CASCADE 外键)整表 diff
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName)
await this.flushTable(t);
if (t === tableName)
continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
}
// 单次原子写(一条日志记录 = 真原子,v0.6.1)
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
return count;
}
async delete(tableName, query) {
@@ -1969,14 +1996,20 @@ class KVStoreEngine {
this.txDirtyTables.add(tableName);
return count;
}
const puts = {};
const deletes = pks.map((pk) => this.rowKey(tableName, pk));
// 级联影响表整表 diff(合并到单次原子写,v0.6.1)
for (const t of await this.affectedTables(tableName)) {
if (t === tableName)
continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
// 级联影响表整表 diff
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName)
await this.flushTable(t);
}
return count;
}
async count(tableName, query) {
@@ -1990,7 +2023,11 @@ class KVStoreEngine {
this.txDirtyTables.add(tableName);
return;
}
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0)
await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0)
await this.kv.deleteMany(diff.deletes);
}
// ---- 动态索引 ----
async createIndex(tableName, column, unique) {
@@ -1998,6 +2035,7 @@ class KVStoreEngine {
await this.memory.createIndex(tableName, column, unique);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -2007,6 +2045,7 @@ class KVStoreEngine {
await this.memory.dropIndex(tableName, column, indexName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -2017,27 +2056,40 @@ class KVStoreEngine {
await this.memory.beginTransaction();
this.txActive = true;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
async commitTransaction() {
this.ensureOpen();
if (!this.txActive)
throw new DatabaseError('No active transaction', 'TX_NONE');
// 先持久化(原子),再提交内存快照(失败可回滚)
// v0.6.1: 全部 dirty 表合并为单次原子 flush(一条日志记录 = 真原子,
// 多表事务中途崩溃/失败不会出现"部分表已提交")
const puts = {};
const deletes = [];
for (const table of this.txDirtyTables) {
if (await this.memory.hasTable(table)) {
await this.flushTable(table);
const diff = await this.collectTableDiff(table);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
else {
// 事务内 drop 的表:清理 KV 残留行
const all = await this.kv.getAll();
const prefix = this.rowPrefix(table);
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
for (const [key] of all) {
if (key.startsWith(prefix))
deletes.push(key);
}
}
}
// v0.6.0-fix: 事务内 DDLcreate/drop/alter)的 schema 一并持久化
await this.persistSchema();
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
// 事务内 DDL 的 schema 一并持久化
if (this.txSchemaChanged) {
await this.persistSchema();
}
await this.kv.checkpoint();
await this.memory.commitTransaction();
this.txActive = false;
@@ -2050,6 +2102,7 @@ class KVStoreEngine {
await this.memory.rollbackTransaction();
this.txActive = false;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
// ---- 内部 ----
ensureOpen() {
@@ -2112,41 +2165,39 @@ class KVStoreEngine {
await this.kv.put(SCHEMA_KEY, enc(JSON.stringify(schemas)));
}
/**
* 整表 diff 持久化内存行全部 put + KV 残留行删除原子 putMany + deleteMany
* 用于主键变更 / 级联 / dropTable / clear / alterTable DROP / 事务 commit
* v0.6.1: 整表 diff 收集不落盘内存行全部 put + KV 残留行删除
* 调用方合并到单次原子 putMany/deleteMany多表操作真原子
*/
async flushTable(tableName) {
async collectTableDiff(tableName) {
const prefix = this.rowPrefix(tableName);
// 表已删除:仅清理 KV 残留行
const schema = await this.memory.getTableSchema(tableName);
if (!schema) {
const all = await this.kv.getAll();
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
return;
}
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const puts = {};
const current = new Set();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)删除
const all = await this.kv.getAll();
const deletes = [];
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key)) {
deletes.push(key);
// 表已删除:仅收集 KV 残留行删除
const schema = await this.memory.getTableSchema(tableName);
if (schema) {
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const current = new Set();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key))
deletes.push(key);
}
}
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
else {
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix))
deletes.push(key);
}
}
return { puts, deletes };
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+6 -3
View File
@@ -827,6 +827,7 @@ declare class MemoryEngine implements IStorageEngine {
private removeIndexEntries;
/**
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
* v0.6.1-fix: 环路保护(A→B→A 级联环不再无限递归栈溢出,AriaEngine 同语义)。
* @returns 级联删除的行数
*/
private cascadeDelete;
@@ -887,6 +888,8 @@ declare class KVStoreEngine implements IStorageEngine {
private txActive;
/** 事务中写过的表(commit 时只 flush 这些表) */
private txDirtyTables;
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
private txSchemaChanged;
constructor(medium?: IStorageBackend, checkpointThreshold?: number);
private rowKey;
private rowPrefix;
@@ -936,10 +939,10 @@ declare class KVStoreEngine implements IStorageEngine {
/** 持久化 schema(全部表) */
private persistSchema;
/**
* 整表 diff 持久化:内存行全部 put + KV 残留行删除(原子 putMany + deleteMany
* 用于主键变更 / 级联 / dropTable / clear / alterTable DROP / 事务 commit
* v0.6.1: 整表 diff 收集(不落盘):内存行全部 put + KV 残留行删除。
* 调用方合并到单次原子 putMany/deleteMany(多表操作真原子)
*/
private flushTable;
private collectTableDiff;
}
declare class AriaEngine implements IStorageEngine {
+106 -55
View File
@@ -725,9 +725,14 @@ class MemoryEngine {
// ---- 外键级联 ----
/**
* 级联删除查找引用 tableName.pkValue 的所有表的行并删除
* v0.6.1-fix: 环路保护ABA 级联环不再无限递归栈溢出AriaEngine 同语义
* @returns 级联删除的行数
*/
async cascadeDelete(tableName, pkValue, _row) {
async cascadeDelete(tableName, pkValue, _row, visited = new Set()) {
const visitKey = `${tableName}:${pkValue}`;
if (visited.has(visitKey))
return 0;
visited.add(visitKey);
let totalCascade = 0;
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName)
@@ -759,7 +764,7 @@ class MemoryEngine {
if (refRow) {
// v0.3.3: 级联删除前清理索引条目
this.removeIndexEntries(refTableName, refRow, refPk);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow, visited);
}
refTableData.delete(refPk);
totalCascade++;
@@ -1722,6 +1727,8 @@ class KVStoreEngine {
this.txActive = false;
/** 事务中写过的表(commit 时只 flush 这些表) */
this.txDirtyTables = new Set();
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
this.txSchemaChanged = false;
this.kv = new KVStore(medium, checkpointThreshold);
}
// ---- 行 key 编解码 ----
@@ -1846,6 +1853,7 @@ class KVStoreEngine {
await this.memory.createTable(schema);
if (this.txActive) {
this.txDirtyTables.add(schema.name);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -1855,19 +1863,27 @@ class KVStoreEngine {
await this.memory.dropTable(tableName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
// 删除该表全部行(KV 中残留清理)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0)
await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0)
await this.kv.deleteMany(diff.deletes);
}
async hasTable(tableName) {
this.ensureOpen();
return this.memory.hasTable(tableName);
}
async getTableNames() {
this.ensureOpen();
return this.memory.getTableNames();
}
async getTableSchema(tableName) {
this.ensureOpen();
return this.memory.getTableSchema(tableName);
}
async alterTable(tableName, action, column) {
@@ -1875,12 +1891,17 @@ class KVStoreEngine {
await this.memory.alterTable(tableName, action, column);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
if (action === 'DROP') {
// 重写存储行(移除该列)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0)
await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0)
await this.kv.deleteMany(diff.deletes);
}
}
// ---- CRUD ----
@@ -1925,16 +1946,18 @@ class KVStoreEngine {
this.txDirtyTables.add(tableName);
return count;
}
const puts = {};
const deletes = [];
if (pkChanged) {
// 主键变更:相关表整表 diff(罕见操作,可靠性优先)
for (const t of await this.affectedTables(tableName)) {
await this.flushTable(t);
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
}
else {
// 增量重写受影响行
const puts = {};
const deletes = [];
for (const pk of affected) {
const row = await this.memory.find(tableName, { table: tableName, where: { [pkCol]: pk } });
if (row.length > 0) {
@@ -1944,16 +1967,20 @@ class KVStoreEngine {
deletes.push(this.rowKey(tableName, pk));
}
}
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
// 级联影响表(SET NULL/CASCADE 外键)整表 diff
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName)
await this.flushTable(t);
if (t === tableName)
continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
}
// 单次原子写(一条日志记录 = 真原子,v0.6.1)
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
return count;
}
async delete(tableName, query) {
@@ -1965,14 +1992,20 @@ class KVStoreEngine {
this.txDirtyTables.add(tableName);
return count;
}
const puts = {};
const deletes = pks.map((pk) => this.rowKey(tableName, pk));
// 级联影响表整表 diff(合并到单次原子写,v0.6.1)
for (const t of await this.affectedTables(tableName)) {
if (t === tableName)
continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
// 级联影响表整表 diff
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName)
await this.flushTable(t);
}
return count;
}
async count(tableName, query) {
@@ -1986,7 +2019,11 @@ class KVStoreEngine {
this.txDirtyTables.add(tableName);
return;
}
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0)
await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0)
await this.kv.deleteMany(diff.deletes);
}
// ---- 动态索引 ----
async createIndex(tableName, column, unique) {
@@ -1994,6 +2031,7 @@ class KVStoreEngine {
await this.memory.createIndex(tableName, column, unique);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -2003,6 +2041,7 @@ class KVStoreEngine {
await this.memory.dropIndex(tableName, column, indexName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -2013,27 +2052,40 @@ class KVStoreEngine {
await this.memory.beginTransaction();
this.txActive = true;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
async commitTransaction() {
this.ensureOpen();
if (!this.txActive)
throw new DatabaseError('No active transaction', 'TX_NONE');
// 先持久化(原子),再提交内存快照(失败可回滚)
// v0.6.1: 全部 dirty 表合并为单次原子 flush(一条日志记录 = 真原子,
// 多表事务中途崩溃/失败不会出现"部分表已提交")
const puts = {};
const deletes = [];
for (const table of this.txDirtyTables) {
if (await this.memory.hasTable(table)) {
await this.flushTable(table);
const diff = await this.collectTableDiff(table);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
else {
// 事务内 drop 的表:清理 KV 残留行
const all = await this.kv.getAll();
const prefix = this.rowPrefix(table);
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
for (const [key] of all) {
if (key.startsWith(prefix))
deletes.push(key);
}
}
}
// v0.6.0-fix: 事务内 DDLcreate/drop/alter)的 schema 一并持久化
await this.persistSchema();
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
// 事务内 DDL 的 schema 一并持久化
if (this.txSchemaChanged) {
await this.persistSchema();
}
await this.kv.checkpoint();
await this.memory.commitTransaction();
this.txActive = false;
@@ -2046,6 +2098,7 @@ class KVStoreEngine {
await this.memory.rollbackTransaction();
this.txActive = false;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
// ---- 内部 ----
ensureOpen() {
@@ -2108,41 +2161,39 @@ class KVStoreEngine {
await this.kv.put(SCHEMA_KEY, enc(JSON.stringify(schemas)));
}
/**
* 整表 diff 持久化内存行全部 put + KV 残留行删除原子 putMany + deleteMany
* 用于主键变更 / 级联 / dropTable / clear / alterTable DROP / 事务 commit
* v0.6.1: 整表 diff 收集不落盘内存行全部 put + KV 残留行删除
* 调用方合并到单次原子 putMany/deleteMany多表操作真原子
*/
async flushTable(tableName) {
async collectTableDiff(tableName) {
const prefix = this.rowPrefix(tableName);
// 表已删除:仅清理 KV 残留行
const schema = await this.memory.getTableSchema(tableName);
if (!schema) {
const all = await this.kv.getAll();
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
return;
}
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const puts = {};
const current = new Set();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)删除
const all = await this.kv.getAll();
const deletes = [];
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key)) {
deletes.push(key);
// 表已删除:仅收集 KV 残留行删除
const schema = await this.memory.getTableSchema(tableName);
if (schema) {
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const current = new Set();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key))
deletes.push(key);
}
}
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
else {
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix))
deletes.push(key);
}
}
return { puts, deletes };
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+106 -55
View File
@@ -731,9 +731,14 @@
// ---- 外键级联 ----
/**
* 级联删除查找引用 tableName.pkValue 的所有表的行并删除
* v0.6.1-fix: 环路保护ABA 级联环不再无限递归栈溢出AriaEngine 同语义
* @returns 级联删除的行数
*/
async cascadeDelete(tableName, pkValue, _row) {
async cascadeDelete(tableName, pkValue, _row, visited = new Set()) {
const visitKey = `${tableName}:${pkValue}`;
if (visited.has(visitKey))
return 0;
visited.add(visitKey);
let totalCascade = 0;
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName)
@@ -765,7 +770,7 @@
if (refRow) {
// v0.3.3: 级联删除前清理索引条目
this.removeIndexEntries(refTableName, refRow, refPk);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow, visited);
}
refTableData.delete(refPk);
totalCascade++;
@@ -1728,6 +1733,8 @@
this.txActive = false;
/** 事务中写过的表(commit 时只 flush 这些表) */
this.txDirtyTables = new Set();
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
this.txSchemaChanged = false;
this.kv = new KVStore(medium, checkpointThreshold);
}
// ---- 行 key 编解码 ----
@@ -1852,6 +1859,7 @@
await this.memory.createTable(schema);
if (this.txActive) {
this.txDirtyTables.add(schema.name);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -1861,19 +1869,27 @@
await this.memory.dropTable(tableName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
// 删除该表全部行(KV 中残留清理)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0)
await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0)
await this.kv.deleteMany(diff.deletes);
}
async hasTable(tableName) {
this.ensureOpen();
return this.memory.hasTable(tableName);
}
async getTableNames() {
this.ensureOpen();
return this.memory.getTableNames();
}
async getTableSchema(tableName) {
this.ensureOpen();
return this.memory.getTableSchema(tableName);
}
async alterTable(tableName, action, column) {
@@ -1881,12 +1897,17 @@
await this.memory.alterTable(tableName, action, column);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
if (action === 'DROP') {
// 重写存储行(移除该列)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0)
await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0)
await this.kv.deleteMany(diff.deletes);
}
}
// ---- CRUD ----
@@ -1931,16 +1952,18 @@
this.txDirtyTables.add(tableName);
return count;
}
const puts = {};
const deletes = [];
if (pkChanged) {
// 主键变更:相关表整表 diff(罕见操作,可靠性优先)
for (const t of await this.affectedTables(tableName)) {
await this.flushTable(t);
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
}
else {
// 增量重写受影响行
const puts = {};
const deletes = [];
for (const pk of affected) {
const row = await this.memory.find(tableName, { table: tableName, where: { [pkCol]: pk } });
if (row.length > 0) {
@@ -1950,16 +1973,20 @@
deletes.push(this.rowKey(tableName, pk));
}
}
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
// 级联影响表(SET NULL/CASCADE 外键)整表 diff
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName)
await this.flushTable(t);
if (t === tableName)
continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
}
// 单次原子写(一条日志记录 = 真原子,v0.6.1)
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
return count;
}
async delete(tableName, query) {
@@ -1971,14 +1998,20 @@
this.txDirtyTables.add(tableName);
return count;
}
const puts = {};
const deletes = pks.map((pk) => this.rowKey(tableName, pk));
// 级联影响表整表 diff(合并到单次原子写,v0.6.1)
for (const t of await this.affectedTables(tableName)) {
if (t === tableName)
continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
// 级联影响表整表 diff
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName)
await this.flushTable(t);
}
return count;
}
async count(tableName, query) {
@@ -1992,7 +2025,11 @@
this.txDirtyTables.add(tableName);
return;
}
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0)
await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0)
await this.kv.deleteMany(diff.deletes);
}
// ---- 动态索引 ----
async createIndex(tableName, column, unique) {
@@ -2000,6 +2037,7 @@
await this.memory.createIndex(tableName, column, unique);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -2009,6 +2047,7 @@
await this.memory.dropIndex(tableName, column, indexName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -2019,27 +2058,40 @@
await this.memory.beginTransaction();
this.txActive = true;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
async commitTransaction() {
this.ensureOpen();
if (!this.txActive)
throw new DatabaseError('No active transaction', 'TX_NONE');
// 先持久化(原子),再提交内存快照(失败可回滚)
// v0.6.1: 全部 dirty 表合并为单次原子 flush(一条日志记录 = 真原子,
// 多表事务中途崩溃/失败不会出现"部分表已提交")
const puts = {};
const deletes = [];
for (const table of this.txDirtyTables) {
if (await this.memory.hasTable(table)) {
await this.flushTable(table);
const diff = await this.collectTableDiff(table);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
else {
// 事务内 drop 的表:清理 KV 残留行
const all = await this.kv.getAll();
const prefix = this.rowPrefix(table);
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
for (const [key] of all) {
if (key.startsWith(prefix))
deletes.push(key);
}
}
}
// v0.6.0-fix: 事务内 DDLcreate/drop/alter)的 schema 一并持久化
await this.persistSchema();
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
// 事务内 DDL 的 schema 一并持久化
if (this.txSchemaChanged) {
await this.persistSchema();
}
await this.kv.checkpoint();
await this.memory.commitTransaction();
this.txActive = false;
@@ -2052,6 +2104,7 @@
await this.memory.rollbackTransaction();
this.txActive = false;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
// ---- 内部 ----
ensureOpen() {
@@ -2114,41 +2167,39 @@
await this.kv.put(SCHEMA_KEY, enc(JSON.stringify(schemas)));
}
/**
* 整表 diff 持久化内存行全部 put + KV 残留行删除原子 putMany + deleteMany
* 用于主键变更 / 级联 / dropTable / clear / alterTable DROP / 事务 commit
* v0.6.1: 整表 diff 收集不落盘内存行全部 put + KV 残留行删除
* 调用方合并到单次原子 putMany/deleteMany多表操作真原子
*/
async flushTable(tableName) {
async collectTableDiff(tableName) {
const prefix = this.rowPrefix(tableName);
// 表已删除:仅清理 KV 残留行
const schema = await this.memory.getTableSchema(tableName);
if (!schema) {
const all = await this.kv.getAll();
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
return;
}
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const puts = {};
const current = new Set();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)删除
const all = await this.kv.getAll();
const deletes = [];
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key)) {
deletes.push(key);
// 表已删除:仅收集 KV 残留行删除
const schema = await this.memory.getTableSchema(tableName);
if (schema) {
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const current = new Set();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key))
deletes.push(key);
}
}
if (Object.keys(puts).length > 0)
await this.kv.putMany(puts);
if (deletes.length > 0)
await this.kv.deleteMany(deletes);
else {
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix))
deletes.push(key);
}
}
return { puts, deletes };
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -767,7 +767,7 @@ db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
<strong>v0.4.4 SSTable 编码修复</strong> — 大段中文内容(如 300KB 笔记)写入 AriaEngine 不再崩溃:块大小估算改 UTF-8 字节精确计算(修复中文 3 字节 vs 1 码元导致的缓冲区低估越界)· 长度字段 u16 → u32(修复 >64KB value 截断)· 大 value 独立成块 · 格式 v2"SSTC")与 v1"SSTB")双格式兼容(旧库数据不丢)· 中文主键 / 大内容索引列同步支持。<br>
<strong>v0.5.0 存储后端生产级硬化</strong> — 真实 CRC-32 完整性校验(SSTable 整文件 + WAL 记录,旧文件兼容)· 全库 AES-256-GCM 透明加密(EncryptedBackend + PBKDF2 密钥派生 + 密码验证)· WAL 分片文件重构(真追加 + 空洞检测 + 旧格式迁移)· SSTable 4KB 页面化物理存储(BufferPool/FileManager 真实接入,meta 存 pageIds 兼容旧数据)· OPFS 后端 v2(append 真追加 / 写队列健壮性 / 残留清理)· Web Locks 多标签页独占锁(ARIA_LOCKED)· LZ4 v2 原始大小头 · Playwright 真实 Chromium e2e7 用例)· DatabaseConfig.aria 配置透传。<br>
<strong>v0.5.1 深度审查修复</strong> — 14 个生命周期钩子全部真实接线(此前 6 个 CRUD 钩子从未触发)· EXPLAIN / ANALYZE / REINDEX / VACUUM / SAVEPOINT SQL 入口补齐(此前仅有引擎方法无法触发)· db.backup() 公共方法 · 删除全部死代码(utils.ts 整文件 / MVCC 读侧 / estimateQueryCost 未接线优化器 / 40+ 统计辅助方法)。<br>
<strong>v0.6.0 完全移除 IndexedDB</strong> — 自研 KVStore 事务存储引擎(多 key 原子写 = 单日志记录原子追加 · 快照 checkpoint + 两阶段崩溃恢复 · CRC-32 自愈)· disk 模式切换 KVStoreEngine(替代 IndexedDBEngine + OPFSEngine)· 事务内 DDL / 外键级联 / 二级索引完整持久化 · migrateFromIndexedDB() 旧库一键迁移 · 10 万 key 压力验证 · e2e 崩溃注入 + KVStoreEngine 真实环境(12 用例)· 1022 测试 64 套件 · 89.15% 行覆盖率。</p>
<strong>v0.6.0 完全移除 IndexedDB</strong> — 自研 KVStore 事务存储引擎(多 key 原子写 = 单日志记录原子追加 · 快照 checkpoint + 两阶段崩溃恢复 · CRC-32 自愈)· disk 模式切换 KVStoreEngine(替代 IndexedDBEngine + OPFSEngine)· 事务内 DDL / 外键级联 / 二级索引完整持久化 · migrateFromIndexedDB() 旧库一键迁移 · 10 万 key 压力验证 · e2e 崩溃注入 + KVStoreEngine 真实环境(12 用例)· 1049 测试 64 套件 · 89.3% 行覆盖率。</p>
<h3>存储模式对比</h3>
<table>
+3 -3
View File
@@ -153,7 +153,7 @@
<!-- Hero -->
<section class="hero">
<div class="container">
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.6.0 完全移除 IndexedDB — 1022测试 64套件 · 自研 KVStore 事务引擎 · 多 key 原子写 · 崩溃恢复 · 旧库一键迁移</div>
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.6.0 完全移除 IndexedDB — 1049测试 64套件 · 自研 KVStore 事务引擎 · 多 key 原子写 · 崩溃恢复 · 旧库一键迁移</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
<div class="actions">
@@ -414,8 +414,8 @@ npm install @metona-team/metona-sqlark
<p>MetonaSqlark 的核心指标</p>
</div>
<div class="stats">
<div class="stat-card"><div class="num">1022</div><div class="label">测试用例</div></div>
<div class="stat-card"><div class="num">89.15%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">1049</div><div class="label">测试用例</div></div>
<div class="stat-card"><div class="num">89.3%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">~27KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">5</div><div class="label">存储引擎</div></div>
<div class="stat-card"><div class="num">72</div><div class="label">SQL 关键字</div></div>
+81 -45
View File
@@ -44,6 +44,8 @@ export class KVStoreEngine implements IStorageEngine {
private txActive = false;
/** 事务中写过的表(commit 时只 flush 这些表) */
private txDirtyTables: Set<string> = new Set();
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
private txSchemaChanged = false;
constructor(medium?: IStorageBackend, checkpointThreshold?: number) {
this.kv = new KVStore(medium, checkpointThreshold);
@@ -175,6 +177,7 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.createTable(schema);
if (this.txActive) {
this.txDirtyTables.add(schema.name);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -185,22 +188,28 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.dropTable(tableName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
// 删除该表全部行(KV 中残留清理)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0) await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0) await this.kv.deleteMany(diff.deletes);
}
async hasTable(tableName: string): Promise<boolean> {
this.ensureOpen();
return this.memory.hasTable(tableName);
}
async getTableNames(): Promise<string[]> {
this.ensureOpen();
return this.memory.getTableNames();
}
async getTableSchema(tableName: string): Promise<TableSchema | null> {
this.ensureOpen();
return this.memory.getTableSchema(tableName);
}
@@ -213,12 +222,15 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.alterTable(tableName, action, column);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
if (action === 'DROP') {
// 重写存储行(移除该列)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0) await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0) await this.kv.deleteMany(diff.deletes);
}
}
@@ -272,15 +284,17 @@ export class KVStoreEngine implements IStorageEngine {
return count;
}
const puts: Record<string, ArrayBuffer> = {};
const deletes: string[] = [];
if (pkChanged) {
// 主键变更:相关表整表 diff(罕见操作,可靠性优先)
for (const t of await this.affectedTables(tableName)) {
await this.flushTable(t);
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
} else {
// 增量重写受影响行
const puts: Record<string, ArrayBuffer> = {};
const deletes: string[] = [];
for (const pk of affected) {
const row = await this.memory.find(tableName, { table: tableName, where: { [pkCol]: pk } });
if (row.length > 0) {
@@ -289,13 +303,17 @@ export class KVStoreEngine implements IStorageEngine {
deletes.push(this.rowKey(tableName, pk));
}
}
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
// 级联影响表(SET NULL/CASCADE 外键)整表 diff
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName) await this.flushTable(t);
if (t === tableName) continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
}
// 单次原子写(一条日志记录 = 真原子,v0.6.1)
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
return count;
}
@@ -308,12 +326,17 @@ export class KVStoreEngine implements IStorageEngine {
this.txDirtyTables.add(tableName);
return count;
}
const puts: Record<string, ArrayBuffer> = {};
const deletes = pks.map((pk) => this.rowKey(tableName, pk));
if (deletes.length > 0) await this.kv.deleteMany(deletes);
// 级联影响表整表 diff
// 级联影响表整表 diff(合并到单次原子写,v0.6.1)
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName) await this.flushTable(t);
if (t === tableName) continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
return count;
}
@@ -329,7 +352,9 @@ export class KVStoreEngine implements IStorageEngine {
this.txDirtyTables.add(tableName);
return;
}
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0) await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0) await this.kv.deleteMany(diff.deletes);
}
// ---- 动态索引 ----
@@ -339,6 +364,7 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.createIndex(tableName, column, unique);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -349,6 +375,7 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.dropIndex(tableName, column, indexName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -361,25 +388,36 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.beginTransaction();
this.txActive = true;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
async commitTransaction(): Promise<void> {
this.ensureOpen();
if (!this.txActive) throw new DatabaseError('No active transaction', 'TX_NONE');
// 先持久化(原子),再提交内存快照(失败可回滚)
// v0.6.1: 全部 dirty 表合并为单次原子 flush(一条日志记录 = 真原子,
// 多表事务中途崩溃/失败不会出现"部分表已提交")
const puts: Record<string, ArrayBuffer> = {};
const deletes: string[] = [];
for (const table of this.txDirtyTables) {
if (await this.memory.hasTable(table)) {
await this.flushTable(table);
const diff = await this.collectTableDiff(table);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
} else {
// 事务内 drop 的表:清理 KV 残留行
const all = await this.kv.getAll();
const prefix = this.rowPrefix(table);
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
for (const [key] of all) {
if (key.startsWith(prefix)) deletes.push(key);
}
}
}
// v0.6.0-fix: 事务内 DDLcreate/drop/alter)的 schema 一并持久化
await this.persistSchema();
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
// 事务内 DDL 的 schema 一并持久化
if (this.txSchemaChanged) {
await this.persistSchema();
}
await this.kv.checkpoint();
await this.memory.commitTransaction();
this.txActive = false;
@@ -392,6 +430,7 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.rollbackTransaction();
this.txActive = false;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
// ---- 内部 ----
@@ -455,38 +494,35 @@ export class KVStoreEngine implements IStorageEngine {
}
/**
* diff put + KV putMany + deleteMany
* / / dropTable / clear / alterTable DROP / commit
* v0.6.1: 整表 diff put + KV
* putMany/deleteMany
*/
private async flushTable(tableName: string): Promise<void> {
private async collectTableDiff(tableName: string): Promise<{ puts: Record<string, ArrayBuffer>; deletes: string[] }> {
const prefix = this.rowPrefix(tableName);
// 表已删除:仅清理 KV 残留行
const schema = await this.memory.getTableSchema(tableName);
if (!schema) {
const all = await this.kv.getAll();
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
return;
}
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const puts: Record<string, ArrayBuffer> = {};
const current = new Set<string>();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)删除
const all = await this.kv.getAll();
const deletes: string[] = [];
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key)) {
deletes.push(key);
// 表已删除:仅收集 KV 残留行删除
const schema = await this.memory.getTableSchema(tableName);
if (schema) {
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const current = new Set<string>();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key)) deletes.push(key);
}
} else {
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix)) deletes.push(key);
}
}
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
return { puts, deletes };
}
}
+11 -2
View File
@@ -489,9 +489,18 @@ export class MemoryEngine implements IStorageEngine {
/**
* tableName.pkValue
* v0.6.1-fix: 环路保护ABA AriaEngine
* @returns
*/
private async cascadeDelete(tableName: string, pkValue: string, _row: Record<string, unknown>): Promise<number> {
private async cascadeDelete(
tableName: string,
pkValue: string,
_row: Record<string, unknown>,
visited: Set<string> = new Set(),
): Promise<number> {
const visitKey = `${tableName}:${pkValue}`;
if (visited.has(visitKey)) return 0;
visited.add(visitKey);
let totalCascade = 0;
for (const [refTableName, refSchema] of this.schemas) {
@@ -529,7 +538,7 @@ export class MemoryEngine implements IStorageEngine {
if (refRow) {
// v0.3.3: 级联删除前清理索引条目
this.removeIndexEntries(refTableName, refRow, refPk);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow, visited);
}
refTableData.delete(refPk);
totalCascade++;
+482
View File
@@ -0,0 +1,482 @@
/**
* KVStore / KVStoreEngine
*
*
* 1. KVStore key/value value key open open/closeclear
* 2. KVStoreEngine open close commit/rollback
* ABA+
* 3. SharedMemoryBackend append keywriteMany/deleteManyclose
* 4. onCorrupt false
*/
import { KVStore } from '../src/engine/kvstore/index';
import { SharedMemoryBackend } from '../src/engine/kvstore/shared_memory_medium';
import { encodeLogRecord, parseLogRecords } from '../src/engine/kvstore/log';
import { KVStoreEngine } from '../src/engine/kvstore_engine';
import { createSchema } from '../src/table/schema';
let counter = 0;
function uniqueDB(): string {
return `abn-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`;
}
const enc = (s: string) => new TextEncoder().encode(s).buffer as ArrayBuffer;
const dec = (b: ArrayBuffer | null) => (b ? new TextDecoder().decode(b) : null);
beforeEach(() => {
SharedMemoryBackend.clearRegistry();
});
// ===================================================================
// KVStore 边界
// ===================================================================
describe('KVStore — 边界与故障', () => {
it('空 value / 空字符串 key 可正常读写', async () => {
const kv = new KVStore(new SharedMemoryBackend(), 0);
await kv.open(uniqueDB());
await kv.put('empty', new ArrayBuffer(0));
expect((await kv.get('empty'))!.byteLength).toBe(0);
await kv.put('', enc('empty-key'));
expect(dec(await kv.get(''))).toBe('empty-key');
await kv.close();
});
it('超大 value1MB)写入恢复完整', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
const big = new Uint8Array(1024 * 1024);
for (let i = 0; i < big.length; i++) big[i] = i & 0xff;
await kv.put('big', big.buffer as ArrayBuffer);
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
const back = await kv2.get('big');
expect(back!.byteLength).toBe(1024 * 1024);
const backBytes = new Uint8Array(back!);
expect(backBytes[500000]).toBe(500000 & 0xff);
await kv2.close();
});
it('特殊字符 key(中文/emoji/含冒号/超长)读写恢复', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
const keys = ['中文键', 'emoji-😀-key', 'a:b:c', 'x'.repeat(500)];
for (let i = 0; i < keys.length; i++) {
await kv.put(keys[i], enc(`v${i}`));
}
await kv.checkpoint();
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
for (let i = 0; i < keys.length; i++) {
expect(dec(await kv2.get(keys[i]))).toBe(`v${i}`);
}
await kv2.close();
});
it('未 open 的 KVStore 介质为空(get 返回 null,操作安全)', async () => {
const kv = new KVStore(new SharedMemoryBackend(), 0);
expect(kv.isOpen()).toBe(false);
expect(await kv.get('x')).toBeNull();
expect(kv.size()).toBe(0);
});
it('重复 open / 重复 close 幂等', async () => {
const dbName = uniqueDB();
const kv = new KVStore(new SharedMemoryBackend(), 0);
await kv.open(dbName);
await kv.open(dbName); // 幂等
await kv.put('a', enc('A'));
await kv.close();
await kv.close(); // 幂等
expect(kv.isOpen()).toBe(false);
});
it('clear 后立即写入正常,重开数据只有新写入', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
await kv.put('old', enc('OLD'));
await kv.clear();
await kv.put('new', enc('NEW'));
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
expect(await kv2.exists('old')).toBe(false);
expect(dec(await kv2.get('new'))).toBe('NEW');
await kv2.close();
});
it('大量小 key1 万)快速写入 + 恢复', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
const batch: Record<string, ArrayBuffer> = {};
for (let i = 0; i < 10000; i++) batch[`k${i}`] = enc(`v${i}`);
await kv.putMany(batch);
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
expect(kv2.size()).toBe(10000);
expect(dec(await kv2.get('k9999'))).toBe('v9999');
await kv2.close();
}, 60000);
it('写入失败后 KVStore 继续可用(错误不污染后续操作)', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
// 注入一次失败
const origAppend = medium.append!.bind(medium);
medium.append = async () => { throw new Error('boom'); };
await expect(kv.put('x', enc('X'))).rejects.toMatchObject({ code: 'KV_LOG_ERROR' });
medium.append = origAppend;
// 后续写入正常
await kv.put('y', enc('Y'));
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
expect(dec(await kv2.get('y'))).toBe('Y');
await kv2.close();
});
});
// ===================================================================
// KVStoreEngine 异常场景
// ===================================================================
describe('KVStoreEngine — 异常与一致性', () => {
async function createEngine(): Promise<KVStoreEngine> {
const e = new KVStoreEngine(new SharedMemoryBackend());
await e.open(uniqueDB(), 1);
return e;
}
it('未 open 时所有操作抛 DB_NOT_OPEN', async () => {
const e = new KVStoreEngine(new SharedMemoryBackend());
await expect(e.getTableNames()).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(e.insert('t', [{ id: '1' }])).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(e.find('t', { table: 't' })).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(e.count('t')).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(e.beginTransaction()).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
});
it('重复 close 幂等', async () => {
const e = await createEngine();
await e.close();
await e.close();
expect(e.isOpen()).toBe(false);
});
it('不存在的表操作抛 TABLE_NOT_FOUND', async () => {
const e = await createEngine();
await expect(e.insert('ghost', [{ id: '1' }])).rejects.toMatchObject({ code: 'TABLE_NOT_FOUND' });
await expect(e.update('ghost', { table: 'ghost' }, {})).rejects.toMatchObject({ code: 'TABLE_NOT_FOUND' });
await expect(e.delete('ghost', { table: 'ghost' })).rejects.toMatchObject({ code: 'TABLE_NOT_FOUND' });
await expect(e.count('ghost')).rejects.toMatchObject({ code: 'TABLE_NOT_FOUND' });
await e.close();
});
it('事务嵌套 begin 两次 → TX_ACTIVE', async () => {
const e = await createEngine();
await e.beginTransaction();
await expect(e.beginTransaction()).rejects.toMatchObject({ code: 'TX_ACTIVE' });
await e.rollbackTransaction();
await e.close();
});
it('无事务 commit/rollback → TX_NONE', async () => {
const e = await createEngine();
await expect(e.commitTransaction()).rejects.toMatchObject({ code: 'TX_NONE' });
await expect(e.rollbackTransaction()).rejects.toMatchObject({ code: 'TX_NONE' });
await e.close();
});
it('事务中途失败自动回滚(多表原子性:不出现部分提交)', async () => {
const e = await createEngine();
await e.createTable(createSchema('t1', { id: { type: 'string', primaryKey: true } }));
await e.createTable(createSchema('t2', { id: { type: 'string', primaryKey: true } }));
await e.beginTransaction();
await e.insert('t1', [{ id: 'a' }]);
await e.insert('t2', [{ id: 'b' }]);
// 模拟 commit 失败:注入日志追加故障(KVStore 写走日志 append
const medium = (e as any).kv.medium;
const origAppend = medium.append.bind(medium);
medium.append = async () => { throw new Error('disk full'); };
await expect(e.commitTransaction()).rejects.toMatchObject({ code: 'KV_LOG_ERROR' });
medium.append = origAppend;
// commit 失败后事务未完成:手动回滚 → 内存恢复
await e.rollbackTransaction();
expect(await e.count('t1')).toBe(0);
expect(await e.count('t2')).toBe(0);
await e.close();
// 重启:磁盘也不应有部分提交(事务内写入全部未持久化)
const medium2 = new SharedMemoryBackend();
const e2 = new KVStoreEngine(medium2);
await e2.open((e as any).dbName, 1);
expect(await e2.count('t1')).toBe(0);
expect(await e2.count('t2')).toBe(0);
await e2.close();
});
it('多表事务 commit 单次原子:崩溃后要么全提交要么全无', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const e = new KVStoreEngine(medium);
await e.open(dbName, 1);
await e.createTable(createSchema('t1', { id: { type: 'string', primaryKey: true } }));
await e.createTable(createSchema('t2', { id: { type: 'string', primaryKey: true } }));
await e.beginTransaction();
await e.insert('t1', [{ id: 'a' }]);
await e.insert('t2', [{ id: 'b' }]);
await e.commitTransaction();
// commit 后的数据在日志(未 checkpoint)—— 崩溃模拟
await e.close();
const e2 = new KVStoreEngine(medium);
await e2.open(dbName, 1);
expect(await e2.count('t1')).toBe(1);
expect(await e2.count('t2')).toBe(1);
await e2.close();
});
it('级联环(A→B→A)删除不无限递归', async () => {
const e = await createEngine();
await e.createTable(createSchema('ta', {
id: { type: 'string', primaryKey: true },
ref_b: { type: 'string', references: 'tb.id', onDelete: 'CASCADE' },
}));
await e.createTable(createSchema('tb', {
id: { type: 'string', primaryKey: true },
ref_a: { type: 'string', references: 'ta.id', onDelete: 'CASCADE' },
}));
await e.insert('ta', [{ id: 'a1', ref_b: 'b1' }]);
await e.insert('tb', [{ id: 'b1', ref_a: 'a1' }]);
// 删除不应死循环(visited 保护)
await e.delete('ta', { table: 'ta', where: { id: 'a1' } });
expect(await e.count('ta')).toBe(0);
await e.close();
});
it('主键变更后索引一致(旧值查询无结果,新值查询命中)', async () => {
const e = await createEngine();
await e.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
}));
await e.insert('items', [{ id: '1', tag: 'old' }]);
await e.update('items', { table: 'items', where: { id: '1' } }, { id: '10', tag: 'new' });
// 旧主键查询空
expect(await e.find('items', { table: 'items', where: { id: '1' } })).toHaveLength(0);
// 新主键命中
expect(await e.find('items', { table: 'items', where: { id: '10' } })).toHaveLength(1);
// 索引:旧 tag 空,新 tag 命中
expect(await e.find('items', { table: 'items', where: { tag: 'old' } })).toHaveLength(0);
expect(await e.find('items', { table: 'items', where: { tag: 'new' } })).toHaveLength(1);
await e.close();
// 重启后仍一致
const e2 = new KVStoreEngine(new SharedMemoryBackend());
await e2.open((e as any).dbName, 1);
expect(await e2.find('items', { table: 'items', where: { tag: 'new' } })).toHaveLength(1);
expect(await e2.find('items', { table: 'items', where: { id: '10' } })).toHaveLength(1);
await e2.close();
});
it('空表操作(count/find/clear 空表)正常', async () => {
const e = await createEngine();
await e.createTable(createSchema('empty', { id: { type: 'string', primaryKey: true } }));
expect(await e.count('empty')).toBe(0);
expect(await e.find('empty', { table: 'empty' })).toHaveLength(0);
await e.clear('empty');
await e.close();
});
it('大批量 delete5000 行)持久化后重启正确', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const e = new KVStoreEngine(medium);
await e.open(dbName, 1);
await e.createTable(createSchema('bulk', { id: { type: 'number', primaryKey: true } }));
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 5000; i++) rows.push({ id: i });
await e.insert('bulk', rows);
await e.delete('bulk', { table: 'bulk', where: { id: { $gte: 4000 } } });
expect(await e.count('bulk')).toBe(4000);
await e.close();
const e2 = new KVStoreEngine(medium);
await e2.open(dbName, 1);
expect(await e2.count('bulk')).toBe(4000);
const remaining = await e2.find('bulk', { table: 'bulk', where: { id: { $gte: 3000 } } });
expect(remaining).toHaveLength(1000);
await e2.close();
}, 60000);
});
// ===================================================================
// SharedMemoryBackend 全分支 + 日志解析损坏分支
// ===================================================================
describe('SharedMemoryBackend — 全分支', () => {
it('append 到已存在 key(拼接)与不存在 key(新建)', async () => {
const medium = new SharedMemoryBackend();
await medium.open('sm-1');
await medium.append('log', enc('AAA'));
await medium.append('log', enc('BBB'));
expect(dec(await medium.read('log'))).toBe('AAABBB');
await medium.close();
});
it('writeMany / deleteMany 原子批量', async () => {
const medium = new SharedMemoryBackend();
await medium.open('sm-2');
await medium.writeMany({ a: enc('1'), b: enc('2'), c: enc('3') });
expect(dec(await medium.read('b'))).toBe('2');
await medium.deleteMany(['a', 'c']);
expect(await medium.exists('a')).toBe(false);
expect(await medium.exists('b')).toBe(true);
// writeMany 覆盖已有 key
await medium.writeMany({ b: enc('B2') });
expect(dec(await medium.read('b'))).toBe('B2');
await medium.close();
});
it('close 后操作安全(store null 不抛错)', async () => {
const medium = new SharedMemoryBackend();
await medium.open('sm-3');
await medium.write('k', enc('V'));
await medium.close();
expect(await medium.read('k')).toBeNull();
await medium.write('late', enc('X')); // 静默忽略
await medium.delete('k');
expect(await medium.listKeys()).toEqual([]);
expect(await medium.exists('k')).toBe(false);
await medium.clear();
});
it('跨实例共享:close 后新实例可读(持久化语义)', async () => {
const medium1 = new SharedMemoryBackend();
await medium1.open('sm-4');
await medium1.write('k', enc('PERSIST'));
await medium1.close();
const medium2 = new SharedMemoryBackend();
await medium2.open('sm-4');
expect(dec(await medium2.read('k'))).toBe('PERSIST');
await medium2.close();
});
});
describe('KVStore 日志解析 — 损坏分支', () => {
it('onCorrupt 返回 false → 立即停止扫描', () => {
const good1 = encodeLogRecord(1, { a: enc('1') });
const bad = encodeLogRecord(2, { b: enc('2') });
bad[10] ^= 0xff; // 破坏
const good2 = encodeLogRecord(3, { c: enc('3') });
const combined = new Uint8Array(good1.byteLength + bad.byteLength + good2.byteLength);
combined.set(good1, 0);
combined.set(bad, good1.byteLength);
combined.set(good2, good1.byteLength + bad.byteLength);
const seqs: number[] = [];
let corruptCalls = 0;
const count = parseLogRecords(combined, (r) => seqs.push(r.seq), () => {
corruptCalls++;
return false; // 停止
});
expect(count).toBe(1); // 只有第一条
expect(seqs).toEqual([1]);
expect(corruptCalls).toBe(1);
});
it('损坏条目(keyLen 越界)→ 记录被丢弃', () => {
// 手工构造:entry 的 keyLen 超长导致解析失败
const buf = new ArrayBuffer(4 + 4 + 4 + 1 + 4 + 0xFFFFFF + 4);
const view = new DataView(buf);
let o = 0;
const recLen = buf.byteLength - 4;
view.setUint32(o, recLen, false); o += 4;
view.setUint32(o, 1, false); o += 4; // seq
view.setUint32(o, 1, false); o += 4; // entryCount
view.setUint8(o, 1); o += 1; // op PUT
view.setUint32(o, 0xFFFFFF, false); o += 4; // keyLen 超长
// crc(随便填,解析会在 keyLen 越界处停止)
view.setUint32(o, 0, false);
const data = new Uint8Array(buf);
let corrupt = 0;
const count = parseLogRecords(data, () => {}, () => { corrupt++; return false; });
expect(count).toBe(0);
expect(corrupt).toBe(1);
});
});
// ===================================================================
// AriaEngine 级联环(与 MemoryEngine 对齐)
// ===================================================================
describe('AriaEngine — 级联环保护', () => {
it('A→B→A 级联环删除不无限递归(visited 保护)', async () => {
const { AriaEngine } = require('../src/engine/aria/index');
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(`aria-loop-${Date.now()}-${Math.random()}`, 1);
await engine.createTable(createSchema('ta', {
id: { type: 'string', primaryKey: true },
ref_b: { type: 'string', references: 'tb.id', onDelete: 'CASCADE' },
}));
await engine.createTable(createSchema('tb', {
id: { type: 'string', primaryKey: true },
ref_a: { type: 'string', references: 'ta.id', onDelete: 'CASCADE' },
}));
await engine.insert('ta', [{ id: 'a1', ref_b: 'b1' }]);
await engine.insert('tb', [{ id: 'b1', ref_a: 'a1' }]);
// 不应死循环(RangeError
await engine.delete('ta', { table: 'ta', where: { id: 'a1' } });
expect(await engine.count('ta')).toBe(0);
expect(await engine.count('tb')).toBe(0);
await engine.close();
});
it('更深级联链(A→B→C→D)删除完整', async () => {
const { AriaEngine } = require('../src/engine/aria/index');
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(`aria-chain-${Date.now()}-${Math.random()}`, 1);
await engine.createTable(createSchema('ta', {
id: { type: 'string', primaryKey: true },
ref: { type: 'string', references: 'tb.id', onDelete: 'CASCADE' },
}));
await engine.createTable(createSchema('tb', {
id: { type: 'string', primaryKey: true },
ref: { type: 'string', references: 'tc.id', onDelete: 'CASCADE' },
}));
await engine.createTable(createSchema('tc', {
id: { type: 'string', primaryKey: true },
ref: { type: 'string', references: 'td.id', onDelete: 'CASCADE' },
}));
await engine.createTable(createSchema('td', {
id: { type: 'string', primaryKey: true },
}));
await engine.insert('td', [{ id: 'd1' }]);
await engine.insert('tc', [{ id: 'c1', ref: 'd1' }]);
await engine.insert('tb', [{ id: 'b1', ref: 'c1' }]);
await engine.insert('ta', [{ id: 'a1', ref: 'b1' }]);
// 删除链尾 td.d1 → 级联 tc(引用 td)→ tb(引用 tc)→ ta(引用 tb)
await engine.delete('td', { table: 'td', where: { id: 'd1' } });
expect(await engine.count('ta')).toBe(0);
expect(await engine.count('tb')).toBe(0);
expect(await engine.count('tc')).toBe(0);
expect(await engine.count('td')).toBe(0);
await engine.close();
});
});