release: v0.4.1 — Aria 级联/ALTER/clearAll + 流式查询/派生表 + 正确性加固
新增: - AriaEngine 外键级联(CASCADE/SET NULL/RESTRICT)+ clearAll() 重置 API - 引擎级 alterTable:Aria DROP COLUMN 重写存储行 + schema 持久化 - 流式查询 queryStream / findStream(LSM 惰性扫描不物化) - FROM 派生表 / 多列 ON 哈希连接 / COUNT(DISTINCT) / NULLS FIRST/LAST - 普通列别名 + ORDER BY 别名 + 无表查询 + 字符串常量列 - 演示页引擎切换器(Memory/Aria)+ 预设自动重置 修复: - Aria WAL DROP_TABLE 崩溃恢复(删表复活)+ 恢复后 WAL 截断 - Memory update/delete 索引维护(unique 约束绕过) - 关联 EXISTS 绑定失效 / HAVING 标量子查询 / INSERT SELECT 位置错位 - 裸布尔列条件(WHERE done / CASE WHEN done) - Aria $in 重复行 / JOIN 主表 WHERE 下推 / DROP INDEX 报错 - ORDER BY/GROUP BY/SELECT 表前缀列 + SQL '' 标准转义 质量:894 测试 · 47 套件 · 81.5% 覆盖率
This commit is contained in:
+74
-9
@@ -97,6 +97,29 @@ export class MemoryEngine implements IStorageEngine {
|
||||
return results;
|
||||
}
|
||||
|
||||
/** v0.4.0: 流式查询 — 逐行回调(单次迭代,不物化结果数组) */
|
||||
async findStream(tableName: string, query: QueryPlan, onRow: (row: Record<string, unknown>) => void): Promise<number> {
|
||||
this.ensureTable(tableName);
|
||||
const table = this.tables.get(tableName)!;
|
||||
const hasWhere = !!(query.where && Object.keys(query.where).length > 0);
|
||||
const limit = query.limit ?? Infinity;
|
||||
const offset = query.offset ?? 0;
|
||||
const project = query.columns && query.columns.length > 0 && query.columns[0] !== '*'
|
||||
? (row: Record<string, unknown>) => projectColumns(row, query.columns!)
|
||||
: null;
|
||||
|
||||
let count = 0;
|
||||
let skipped = 0;
|
||||
for (const row of table.values()) {
|
||||
if (hasWhere && !matchWhere(row, query.where!)) continue;
|
||||
if (skipped < offset) { skipped++; continue; }
|
||||
onRow(project ? project(row) : row);
|
||||
count++;
|
||||
if (count >= limit) break;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
|
||||
this.ensureTable(tableName);
|
||||
const schema = this.schemas.get(tableName)!;
|
||||
@@ -104,9 +127,13 @@ export class MemoryEngine implements IStorageEngine {
|
||||
let count = 0;
|
||||
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);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -119,6 +146,8 @@ export class MemoryEngine implements IStorageEngine {
|
||||
const toDelete: string[] = [];
|
||||
for (const [pk, row] of table) {
|
||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||
// v0.3.3: 删除行前清理其索引条目(修复删除后索引残留)
|
||||
this.removeIndexEntries(tableName, row, pk);
|
||||
toDelete.push(pk);
|
||||
}
|
||||
}
|
||||
@@ -177,6 +206,10 @@ export class MemoryEngine implements IStorageEngine {
|
||||
const schema = this.schemas.get(tableName)!;
|
||||
const colDef = schema.columns[column];
|
||||
if (!colDef) throw new DatabaseError(`Column "${column}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
// v0.4.1: DROP 不存在的索引应报错(此前静默成功)
|
||||
if (!colDef.index && !colDef.unique) {
|
||||
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
|
||||
}
|
||||
colDef.index = false;
|
||||
colDef.unique = false;
|
||||
const tableIndexes = this.indexes.get(tableName);
|
||||
@@ -288,17 +321,24 @@ export class MemoryEngine implements IStorageEngine {
|
||||
const tableIndexes = this.indexes.get(tableName);
|
||||
if (!tableIndexes || !query.where) return Array.from(table.values());
|
||||
for (const [col, condition] of Object.entries(query.where)) {
|
||||
// v0.4.1: 支持 { $eq: value } 形式(SQL 解析器生成的等值条件)走索引
|
||||
let targetValue: unknown;
|
||||
if (typeof condition !== 'object' || condition === null) {
|
||||
const colIndex = tableIndexes.get(col);
|
||||
if (colIndex) {
|
||||
const pks = colIndex.get(condition);
|
||||
if (pks) {
|
||||
const result: Record<string, unknown>[] = [];
|
||||
for (const pk of pks) { const r = table.get(pk); if (r) result.push(r); }
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
targetValue = condition;
|
||||
} else if ('$eq' in (condition as Record<string, unknown>) && Object.keys(condition as Record<string, unknown>).length === 1) {
|
||||
targetValue = (condition as Record<string, unknown>).$eq;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
const colIndex = tableIndexes.get(col);
|
||||
if (colIndex) {
|
||||
const pks = colIndex.get(targetValue);
|
||||
if (pks) {
|
||||
const result: Record<string, unknown>[] = [];
|
||||
for (const pk of pks) { const r = table.get(pk); if (r) result.push(r); }
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return Array.from(table.values());
|
||||
@@ -317,6 +357,22 @@ export class MemoryEngine implements IStorageEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/** v0.3.3: 从所有索引中移除一行的条目(update/delete 前调用,修复索引过期/残留) */
|
||||
private removeIndexEntries(tableName: string, row: Record<string, unknown>, pk: string): void {
|
||||
const tableIndexes = this.indexes.get(tableName);
|
||||
if (!tableIndexes) return;
|
||||
for (const [colName, colIndex] of tableIndexes) {
|
||||
const value = row[colName];
|
||||
if (value !== undefined && value !== null) {
|
||||
const pks = colIndex.get(value);
|
||||
if (pks) {
|
||||
pks.delete(pk);
|
||||
if (pks.size === 0) colIndex.delete(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 外键级联 ----
|
||||
|
||||
/**
|
||||
@@ -359,6 +415,8 @@ export class MemoryEngine implements IStorageEngine {
|
||||
for (const refPk of toDelete) {
|
||||
const refRow = refTableData.get(refPk);
|
||||
if (refRow) {
|
||||
// v0.3.3: 级联删除前清理索引条目
|
||||
this.removeIndexEntries(refTableName, refRow, refPk);
|
||||
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
|
||||
}
|
||||
refTableData.delete(refPk);
|
||||
@@ -368,6 +426,13 @@ export class MemoryEngine implements IStorageEngine {
|
||||
for (const refPk of toDelete) {
|
||||
const refRow = refTableData.get(refPk);
|
||||
if (refRow) {
|
||||
// v0.3.3: 外键列置空后同步更新索引
|
||||
if (refRow[colName] !== undefined && refRow[colName] !== null) {
|
||||
const pks = this.indexes.get(refTableName)?.get(colName);
|
||||
if (pks) {
|
||||
pks.get(refRow[colName])?.delete(refPk);
|
||||
}
|
||||
}
|
||||
refRow[colName] = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user