release: v0.4.2 — 生产就绪与崩溃自愈 + 问题清单修复 + 版本迭代
This commit is contained in:
+118
-6
@@ -15,6 +15,8 @@ export class MemoryEngine implements IStorageEngine {
|
||||
private schemas: Map<string, TableSchema> = new Map();
|
||||
private indexes: Map<string, Map<string, Map<unknown, Set<string>>>> = new Map();
|
||||
private opened = false;
|
||||
/** v0.4.2-fix: 库内元数据(迁移版本持久化用) */
|
||||
private metaStore: Map<string, string> = new Map();
|
||||
|
||||
// ---- 事务快照 ----
|
||||
private snapshot: {
|
||||
@@ -32,17 +34,45 @@ export class MemoryEngine implements IStorageEngine {
|
||||
this.opened = true;
|
||||
}
|
||||
async close(): Promise<void> {
|
||||
this.tables.clear(); this.schemas.clear(); this.indexes.clear(); this.opened = false;
|
||||
this.tables.clear(); this.schemas.clear(); this.indexes.clear(); this.metaStore.clear(); this.opened = false;
|
||||
}
|
||||
isOpen(): boolean { return this.opened; }
|
||||
|
||||
// ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ----
|
||||
|
||||
/** 内存引擎无需修复(无持久化损坏概念) */
|
||||
async repair(): Promise<void> { return; }
|
||||
|
||||
/** 清空全部数据与表结构 */
|
||||
async clearAll(): Promise<void> {
|
||||
const names = Array.from(this.schemas.keys());
|
||||
for (const name of names) {
|
||||
await this.dropTable(name);
|
||||
}
|
||||
this.metaStore.clear();
|
||||
}
|
||||
|
||||
async getMeta(key: string): Promise<string | null> {
|
||||
return this.metaStore.get(key) ?? null;
|
||||
}
|
||||
|
||||
async setMeta(key: string, value: string): Promise<void> {
|
||||
this.metaStore.set(key, value);
|
||||
}
|
||||
|
||||
// ---- 表管理 ----
|
||||
async createTable(schema: TableSchema): Promise<void> {
|
||||
if (this.schemas.has(schema.name)) throw new DatabaseError(`Table "${schema.name}" already exists`, 'TABLE_EXISTS');
|
||||
this.schemas.set(schema.name, schema);
|
||||
// v0.4.2-fix: 存储 schema 深拷贝 — 此前 Hybrid.reloadMemoryFromDisk 直接存入
|
||||
// disk 引擎的 schema 引用,内存/磁盘引擎共享同一对象,任一引擎 ALTER 都会污染对方
|
||||
const copy: TableSchema = { name: schema.name, columns: {} };
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
copy.columns[colName] = { ...colDef };
|
||||
}
|
||||
this.schemas.set(schema.name, copy);
|
||||
this.tables.set(schema.name, new Map());
|
||||
const tableIndexes = new Map<string, Map<unknown, Set<string>>>();
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
for (const [colName, colDef] of Object.entries(copy.columns)) {
|
||||
if (colDef.index || colDef.unique) tableIndexes.set(colName, new Map());
|
||||
}
|
||||
this.indexes.set(schema.name, tableIndexes);
|
||||
@@ -57,6 +87,35 @@ export class MemoryEngine implements IStorageEngine {
|
||||
async getTableNames(): Promise<string[]> { return Array.from(this.schemas.keys()); }
|
||||
async getTableSchema(tableName: string): Promise<TableSchema | null> { return this.schemas.get(tableName) ?? null; }
|
||||
|
||||
/**
|
||||
* v0.4.2-fix: 引擎级 ALTER TABLE — 直接修改内存 schema 引用并清理行数据。
|
||||
* (此前走 executor 通用路径,行为相同;统一到引擎层保证 Hybrid/IndexedDB 委托一致性)
|
||||
*/
|
||||
async alterTable(
|
||||
tableName: string,
|
||||
action: 'ADD' | 'DROP',
|
||||
column: import('../constants').ColumnDef & { name: string },
|
||||
): Promise<void> {
|
||||
this.ensureTable(tableName);
|
||||
const schema = this.schemas.get(tableName)!;
|
||||
if (action === 'ADD') {
|
||||
if (schema.columns[column.name]) {
|
||||
throw new DatabaseError(`Column "${column.name}" already exists in table "${tableName}"`, 'COLUMN_EXISTS');
|
||||
}
|
||||
schema.columns[column.name] = column;
|
||||
return;
|
||||
}
|
||||
if (!schema.columns[column.name]) {
|
||||
throw new DatabaseError(`Column "${column.name}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
delete schema.columns[column.name];
|
||||
// 清理已有行中该列的值(find 返回行引用,直接删除生效)
|
||||
const table = this.tables.get(tableName)!;
|
||||
for (const row of table.values()) {
|
||||
if (column.name in row) delete row[column.name];
|
||||
}
|
||||
}
|
||||
|
||||
// ---- CRUD ----
|
||||
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
|
||||
this.ensureTable(tableName);
|
||||
@@ -124,22 +183,75 @@ export class MemoryEngine implements IStorageEngine {
|
||||
this.ensureTable(tableName);
|
||||
const schema = this.schemas.get(tableName)!;
|
||||
const table = this.tables.get(tableName)!;
|
||||
const pkCol = this.getPrimaryKey(schema);
|
||||
let count = 0;
|
||||
for (const [pk, row] of table) {
|
||||
// v0.4.2-fix: 迭代期间会 delete/set 同一 Map(主键变更)→ 拷贝快照避免跳过/重复
|
||||
for (const [pk, row] of [...table]) {
|
||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||
// v0.3.3: 先移除旧值索引条目(修复 update 后唯一约束被绕过、按新值查索引丢行)
|
||||
this.removeIndexEntries(tableName, row, pk);
|
||||
const updated = { ...row, ...updates };
|
||||
this.validateRow(schema, updated);
|
||||
this.checkUniqueness(schema, updated);
|
||||
table.set(pk, updated);
|
||||
this.updateIndexes(tableName, updated, pk);
|
||||
const newPk = String(updated[pkCol]);
|
||||
// v0.4.2-fix: 主键变更 — 删除旧键 + 级联更新引用表 + 新键落表
|
||||
if (newPk !== pk) {
|
||||
await this.applyUpdateCascade(tableName, pk, newPk);
|
||||
}
|
||||
table.delete(pk);
|
||||
table.set(newPk, updated);
|
||||
this.updateIndexes(tableName, updated, newPk);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.4.2-fix: ON UPDATE 外键级联 — 被引用表主键变更时处理引用表:
|
||||
* RESTRICT 抛错 / CASCADE 更新 FK 值 / SET NULL 置空。
|
||||
* 分两阶段:先全量 RESTRICT 检查(任何修改前),再执行级联(防部分修改)。
|
||||
*/
|
||||
private async applyUpdateCascade(tableName: string, oldPk: string, newPk: string): Promise<void> {
|
||||
// 阶段 1: RESTRICT 检查(引用旧主键的行存在即拒绝)
|
||||
for (const [refTableName, refSchema] of this.schemas) {
|
||||
if (refTableName === tableName) continue;
|
||||
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
|
||||
if (!colDef.references || !colDef.onUpdate) continue;
|
||||
const [refTable] = colDef.references.split('.');
|
||||
if (refTable !== tableName) continue;
|
||||
const refTableData = this.tables.get(refTableName);
|
||||
if (!refTableData) continue;
|
||||
for (const [, refRow] of refTableData) {
|
||||
if (String(refRow[colName]) === oldPk && colDef.onUpdate === 'RESTRICT') {
|
||||
throw new DatabaseError(
|
||||
`Cannot update "${tableName}" key "${oldPk}": foreign key "${colName}" in "${refTableName}" has dependent rows`,
|
||||
'FOREIGN_KEY_VIOLATION',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 阶段 2: CASCADE / SET NULL
|
||||
for (const [refTableName, refSchema] of this.schemas) {
|
||||
if (refTableName === tableName) continue;
|
||||
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
|
||||
if (!colDef.references || !colDef.onUpdate) continue;
|
||||
const [refTable] = colDef.references.split('.');
|
||||
if (refTable !== tableName) continue;
|
||||
const refTableData = this.tables.get(refTableName);
|
||||
if (!refTableData) continue;
|
||||
if (colDef.onUpdate !== 'CASCADE' && colDef.onUpdate !== 'SET NULL') continue;
|
||||
for (const [refPk, refRow] of refTableData) {
|
||||
if (String(refRow[colName]) !== oldPk) continue;
|
||||
this.removeIndexEntries(refTableName, refRow, refPk);
|
||||
refRow[colName] = colDef.onUpdate === 'CASCADE' ? newPk : null;
|
||||
this.updateIndexes(refTableName, refRow, refPk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(tableName: string, query: QueryPlan): Promise<number> {
|
||||
this.ensureTable(tableName);
|
||||
const table = this.tables.get(tableName)!;
|
||||
|
||||
Reference in New Issue
Block a user