release: v0.4.2 — 生产就绪与崩溃自愈 + 问题清单修复 + 版本迭代
CI / test (18.x) (push) Successful in 10m7s
CI / test (20.x) (push) Successful in 10m6s
CI / test (22.x) (push) Successful in 10m2s
CI / test (24.x) (push) Successful in 10m0s

This commit is contained in:
thzxx
2026-08-09 19:15:37 +08:00
parent a1e4f5071c
commit 22b0b1fad4
34 changed files with 6626 additions and 565 deletions
+118 -6
View File
@@ -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)!;