feat: v0.1.13 — 事务回滚 + 子查询 + 外键级联 + 连接池
This commit is contained in:
+112
-34
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
||||
* @module engine/indexeddb
|
||||
*
|
||||
* v0.1.13: 支持事务 — beginTransaction 延迟 IDB 写入,commit 批量刷盘,rollback 恢复快照。
|
||||
*/
|
||||
|
||||
import type { IStorageEngine } from './interface';
|
||||
@@ -17,6 +19,9 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
private version = 1;
|
||||
private memoryCache: MemoryEngine = new MemoryEngine();
|
||||
|
||||
// ---- 事务状态 ----
|
||||
private txActive = false;
|
||||
|
||||
async open(dbName: string, version: number): Promise<void> {
|
||||
this.dbName = dbName; this.version = version;
|
||||
await this.memoryCache.open(dbName, version);
|
||||
@@ -38,6 +43,88 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
// ---- 表管理 ----
|
||||
async createTable(schema: TableSchema): Promise<void> {
|
||||
await this.memoryCache.createTable(schema);
|
||||
if (this.txActive) return; // 事务中延迟 IDB 操作
|
||||
await this.idbCreateTable(schema);
|
||||
}
|
||||
|
||||
async dropTable(tableName: string): Promise<void> {
|
||||
await this.memoryCache.dropTable(tableName);
|
||||
if (this.txActive) return;
|
||||
await this.idbDropTable(tableName);
|
||||
}
|
||||
|
||||
async hasTable(tableName: string): Promise<boolean> { return this.ensureDB().objectStoreNames.contains(tableName); }
|
||||
async getTableNames(): Promise<string[]> { return Array.from(this.ensureDB().objectStoreNames); }
|
||||
async getTableSchema(tableName: string): Promise<TableSchema | null> { return this.memoryCache.getTableSchema(tableName); }
|
||||
|
||||
// ---- CRUD ----
|
||||
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
|
||||
const pks = await this.memoryCache.insert(tableName, rows);
|
||||
if (this.txActive) return pks; // 事务中延迟写入
|
||||
await this.idbInsert(tableName, rows);
|
||||
return pks;
|
||||
}
|
||||
|
||||
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
|
||||
// 事务中从内存缓存读取(保证读到未提交的变更),否则走 IDB
|
||||
if (this.txActive) return this.memoryCache.find(tableName, query);
|
||||
return this.idbFind(tableName, query);
|
||||
}
|
||||
|
||||
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
|
||||
const count = await this.memoryCache.update(tableName, query, updates);
|
||||
if (this.txActive) return count;
|
||||
await this.idbUpdate(tableName, query, updates);
|
||||
return count;
|
||||
}
|
||||
|
||||
async delete(tableName: string, query: QueryPlan): Promise<number> {
|
||||
const count = await this.memoryCache.delete(tableName, query);
|
||||
if (this.txActive) return count;
|
||||
await this.idbDelete(tableName, query);
|
||||
return count;
|
||||
}
|
||||
|
||||
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
||||
if (this.txActive) return this.memoryCache.count(tableName, query);
|
||||
const results = await this.idbFind(tableName, { table: tableName, where: query?.where ?? {} });
|
||||
return results.length;
|
||||
}
|
||||
|
||||
async clear(tableName: string): Promise<void> {
|
||||
await this.memoryCache.clear(tableName);
|
||||
if (this.txActive) return;
|
||||
await this.idbClear(tableName);
|
||||
}
|
||||
|
||||
// ---- 事务 ----
|
||||
|
||||
async beginTransaction(): Promise<void> {
|
||||
if (this.txActive) throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||
this.txActive = true;
|
||||
// 保存内存快照到 MemoryEngine 内部的 beginTransaction
|
||||
await this.memoryCache.beginTransaction();
|
||||
}
|
||||
|
||||
async commitTransaction(): Promise<void> {
|
||||
if (!this.txActive) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||
// 确认内存层的变更
|
||||
await this.memoryCache.commitTransaction();
|
||||
// 批量将内存数据刷到 IndexedDB
|
||||
await this.flushToIDB();
|
||||
this.txActive = false;
|
||||
}
|
||||
|
||||
async rollbackTransaction(): Promise<void> {
|
||||
if (!this.txActive) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||
// 恢复内存层到快照状态
|
||||
await this.memoryCache.rollbackTransaction();
|
||||
this.txActive = false;
|
||||
}
|
||||
|
||||
// ---- IDB 原生操作 ----
|
||||
|
||||
private async idbCreateTable(schema: TableSchema): Promise<void> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const newVersion = db.version + 1; db.close();
|
||||
@@ -57,8 +144,7 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
});
|
||||
}
|
||||
|
||||
async dropTable(tableName: string): Promise<void> {
|
||||
await this.memoryCache.dropTable(tableName);
|
||||
private async idbDropTable(tableName: string): Promise<void> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const newVersion = db.version + 1; db.close();
|
||||
@@ -72,29 +158,18 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
});
|
||||
}
|
||||
|
||||
async hasTable(tableName: string): Promise<boolean> { return this.ensureDB().objectStoreNames.contains(tableName); }
|
||||
async getTableNames(): Promise<string[]> { return Array.from(this.ensureDB().objectStoreNames); }
|
||||
async getTableSchema(tableName: string): Promise<TableSchema | null> { return this.memoryCache.getTableSchema(tableName); }
|
||||
|
||||
// ---- CRUD ----
|
||||
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
|
||||
await this.memoryCache.insert(tableName, rows);
|
||||
private async idbInsert(tableName: string, rows: Record<string, unknown>[]): Promise<void> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readwrite');
|
||||
const store = tx.objectStore(tableName);
|
||||
const pks: string[] = [];
|
||||
for (const row of rows) {
|
||||
const req = store.add(row);
|
||||
req.onsuccess = () => pks.push(String(req.result));
|
||||
req.onerror = () => {}; // 内存层已校验,忽略 IDB 重复键
|
||||
}
|
||||
tx.oncomplete = () => resolve(pks);
|
||||
for (const row of rows) store.add(row);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||
});
|
||||
}
|
||||
|
||||
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
|
||||
private async idbFind(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readonly');
|
||||
@@ -119,28 +194,25 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
});
|
||||
}
|
||||
|
||||
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
|
||||
await this.memoryCache.update(tableName, query, updates);
|
||||
private async idbUpdate(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<void> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readwrite');
|
||||
const store = tx.objectStore(tableName);
|
||||
const getAllReq = store.getAll();
|
||||
let count = 0;
|
||||
getAllReq.onsuccess = () => {
|
||||
for (const row of getAllReq.result ?? []) {
|
||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||
Object.assign(row, updates); store.put(row); count++;
|
||||
Object.assign(row, updates); store.put(row);
|
||||
}
|
||||
}
|
||||
};
|
||||
tx.oncomplete = () => resolve(count);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||
});
|
||||
}
|
||||
|
||||
async delete(tableName: string, query: QueryPlan): Promise<number> {
|
||||
await this.memoryCache.delete(tableName, query);
|
||||
private async idbDelete(tableName: string, query: QueryPlan): Promise<void> {
|
||||
const db = this.ensureDB();
|
||||
const schema = await this.memoryCache.getTableSchema(tableName);
|
||||
if (!schema) throw new DatabaseError(`Table "${tableName}" not found`, 'TABLE_NOT_FOUND');
|
||||
@@ -149,26 +221,19 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
const tx = db.transaction(tableName, 'readwrite');
|
||||
const store = tx.objectStore(tableName);
|
||||
const getAllReq = store.getAll();
|
||||
let count = 0;
|
||||
getAllReq.onsuccess = () => {
|
||||
for (const row of getAllReq.result ?? []) {
|
||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||
store.delete(row[pkColumn] as IDBValidKey); count++;
|
||||
store.delete(row[pkColumn] as IDBValidKey);
|
||||
}
|
||||
}
|
||||
};
|
||||
tx.oncomplete = () => resolve(count);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||
});
|
||||
}
|
||||
|
||||
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
||||
const results = await this.find(tableName, { table: tableName, where: query?.where ?? {} });
|
||||
return results.length;
|
||||
}
|
||||
|
||||
async clear(tableName: string): Promise<void> {
|
||||
await this.memoryCache.clear(tableName);
|
||||
private async idbClear(tableName: string): Promise<void> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readwrite');
|
||||
@@ -178,6 +243,19 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
});
|
||||
}
|
||||
|
||||
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
|
||||
private async flushToIDB(): Promise<void> {
|
||||
const tableNames = await this.memoryCache.getTableNames();
|
||||
for (const tableName of tableNames) {
|
||||
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||
// Clear + bulk insert for simplicity
|
||||
await this.idbClear(tableName);
|
||||
if (rows.length > 0) {
|
||||
await this.idbInsert(tableName, rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ensureDB(): IDBDatabase {
|
||||
if (!this.db) throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
||||
return this.db;
|
||||
|
||||
@@ -54,4 +54,15 @@ export interface IStorageEngine {
|
||||
|
||||
/** 清空表数据(保留结构) */
|
||||
clear(tableName: string): Promise<void>;
|
||||
|
||||
// ---- 事务 ----
|
||||
|
||||
/** 开始事务 */
|
||||
beginTransaction(): Promise<void>;
|
||||
|
||||
/** 提交事务 */
|
||||
commitTransaction(): Promise<void>;
|
||||
|
||||
/** 回滚事务 */
|
||||
rollbackTransaction(): Promise<void>;
|
||||
}
|
||||
|
||||
+118
-1
@@ -16,6 +16,13 @@ export class MemoryEngine implements IStorageEngine {
|
||||
private indexes: Map<string, Map<string, Map<unknown, Set<string>>>> = new Map();
|
||||
private opened = false;
|
||||
|
||||
// ---- 事务快照 ----
|
||||
private snapshot: {
|
||||
tables: Map<string, Map<string, Record<string, unknown>>>;
|
||||
schemas: Map<string, TableSchema>;
|
||||
indexes: Map<string, Map<string, Map<unknown, Set<string>>>>;
|
||||
} | null = null;
|
||||
|
||||
// ---- 生命周期 ----
|
||||
async open(_dbName: string, _version: number): Promise<void> { this.opened = true; }
|
||||
async close(): Promise<void> {
|
||||
@@ -109,8 +116,14 @@ export class MemoryEngine implements IStorageEngine {
|
||||
toDelete.push(pk);
|
||||
}
|
||||
}
|
||||
// 级联删除:检查引用此表的其他表
|
||||
let cascadeCount = 0;
|
||||
for (const pk of toDelete) {
|
||||
const row = table.get(pk);
|
||||
if (row) cascadeCount += await this.cascadeDelete(tableName, pk, row);
|
||||
}
|
||||
for (const pk of toDelete) table.delete(pk);
|
||||
return toDelete.length;
|
||||
return toDelete.length + cascadeCount;
|
||||
}
|
||||
|
||||
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
||||
@@ -129,6 +142,56 @@ export class MemoryEngine implements IStorageEngine {
|
||||
if (tableIndexes) for (const colIndex of tableIndexes.values()) colIndex.clear();
|
||||
}
|
||||
|
||||
// ---- 事务 ----
|
||||
|
||||
async beginTransaction(): Promise<void> {
|
||||
if (this.snapshot) throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||
this.snapshot = {
|
||||
tables: this.deepCloneMapMap(this.tables),
|
||||
schemas: new Map(this.schemas),
|
||||
indexes: this.deepCloneIndexes(this.indexes),
|
||||
};
|
||||
}
|
||||
|
||||
async commitTransaction(): Promise<void> {
|
||||
if (!this.snapshot) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||
this.snapshot = null;
|
||||
}
|
||||
|
||||
async rollbackTransaction(): Promise<void> {
|
||||
if (!this.snapshot) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||
this.tables = this.snapshot.tables;
|
||||
this.schemas = this.snapshot.schemas;
|
||||
this.indexes = this.snapshot.indexes;
|
||||
this.snapshot = null;
|
||||
}
|
||||
|
||||
// ---- 事务快照辅助 ----
|
||||
|
||||
private deepCloneMapMap(source: Map<string, Map<string, Record<string, unknown>>>): Map<string, Map<string, Record<string, unknown>>> {
|
||||
const clone = new Map<string, Map<string, Record<string, unknown>>>();
|
||||
for (const [k, v] of source) {
|
||||
const innerClone = new Map<string, Record<string, unknown>>();
|
||||
for (const [ik, iv] of v) innerClone.set(ik, { ...iv });
|
||||
clone.set(k, innerClone);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
private deepCloneIndexes(source: Map<string, Map<string, Map<unknown, Set<string>>>>): Map<string, Map<string, Map<unknown, Set<string>>>> {
|
||||
const clone = new Map<string, Map<string, Map<unknown, Set<string>>>>();
|
||||
for (const [tableName, tableIndexes] of source) {
|
||||
const tableClone = new Map<string, Map<unknown, Set<string>>>();
|
||||
for (const [col, colIndex] of tableIndexes) {
|
||||
const colClone = new Map<unknown, Set<string>>();
|
||||
for (const [val, pkSet] of colIndex) colClone.set(val, new Set(pkSet));
|
||||
tableClone.set(col, colClone);
|
||||
}
|
||||
clone.set(tableName, tableClone);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
// ---- 内部辅助 ----
|
||||
private ensureTable(tableName: string): void {
|
||||
if (!this.tables.has(tableName)) throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
|
||||
@@ -212,4 +275,58 @@ export class MemoryEngine implements IStorageEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 外键级联 ----
|
||||
|
||||
/**
|
||||
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
|
||||
* @returns 级联删除的行数
|
||||
*/
|
||||
private async cascadeDelete(tableName: string, pkValue: string, _row: Record<string, unknown>): Promise<number> {
|
||||
let totalCascade = 0;
|
||||
|
||||
for (const [refTableName, refSchema] of this.schemas) {
|
||||
if (refTableName === tableName) continue;
|
||||
|
||||
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
|
||||
if (!colDef.references || !colDef.onDelete || colDef.onDelete === 'RESTRICT') continue;
|
||||
|
||||
const [refTable, refCol] = colDef.references.split('.');
|
||||
if (refTable !== tableName) continue;
|
||||
|
||||
const refPkCol = refCol;
|
||||
const refTableData = this.tables.get(refTableName);
|
||||
if (!refTableData) continue;
|
||||
|
||||
// 查找所有引用此主键的行
|
||||
const toDelete: string[] = [];
|
||||
for (const [refPk, refRow] of refTableData) {
|
||||
if (String(refRow[colName]) === pkValue) {
|
||||
toDelete.push(refPk);
|
||||
}
|
||||
}
|
||||
|
||||
if (colDef.onDelete === 'CASCADE') {
|
||||
// 递归级联
|
||||
for (const refPk of toDelete) {
|
||||
const refRow = refTableData.get(refPk);
|
||||
if (refRow) {
|
||||
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
|
||||
}
|
||||
refTableData.delete(refPk);
|
||||
totalCascade++;
|
||||
}
|
||||
} else if (colDef.onDelete === 'SET NULL') {
|
||||
for (const refPk of toDelete) {
|
||||
const refRow = refTableData.get(refPk);
|
||||
if (refRow) {
|
||||
refRow[colName] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalCascade;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,26 @@ export class OPFSEngine implements IStorageEngine {
|
||||
await this.writeTableData(tableName, []);
|
||||
}
|
||||
|
||||
// ---- 事务 ----
|
||||
|
||||
async beginTransaction(): Promise<void> {
|
||||
await this.memoryCache.beginTransaction();
|
||||
}
|
||||
|
||||
async commitTransaction(): Promise<void> {
|
||||
await this.memoryCache.commitTransaction();
|
||||
// 将内存数据刷到 OPFS
|
||||
const tableNames = await this.memoryCache.getTableNames();
|
||||
for (const tableName of tableNames) {
|
||||
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||
await this.writeTableData(tableName, rows);
|
||||
}
|
||||
}
|
||||
|
||||
async rollbackTransaction(): Promise<void> {
|
||||
await this.memoryCache.rollbackTransaction();
|
||||
}
|
||||
|
||||
// ---- 内部辅助 ----
|
||||
|
||||
private ensureDir(): FileSystemDirectoryHandle {
|
||||
|
||||
Reference in New Issue
Block a user