release: v0.4.1 — Aria 级联/ALTER/clearAll + 流式查询/派生表 + 正确性加固
CI / test (20.x) (push) Successful in 10m4s
CI / test (22.x) (push) Successful in 10m8s
CI / test (24.x) (push) Successful in 9m55s
CI / test (18.x) (push) Successful in 10m9s

新增:
- 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:
thzxx
2026-08-08 13:36:34 +08:00
parent 82ad8e93b9
commit a1e4f5071c
38 changed files with 13493 additions and 8756 deletions
+85
View File
@@ -202,6 +202,91 @@ export class MetonaSqlark {
return result;
}
// ---- 流式查询(v0.4.0 ----
/**
* 流式查询:逐行回调,不一次性物化全部结果(大表友好)。
* 支持简单 SELECTWHERE/LIMIT/OFFSET/列投影);
* JOIN/GROUP BY/UNION/聚合/ORDER BY 自动回退为物化查询后逐行回调。
*
* @example
* ```ts
* let total = 0;
* await db.queryStream('SELECT * FROM logs WHERE level = \'error\'', (row) => {
* total++;
* processRow(row);
* });
* ```
*/
async queryStream<T extends Record<string, unknown> = Record<string, unknown>>(
sql: string,
onRow: (row: T) => void,
): Promise<number> {
this.ensureReady();
const stmt = parseAll(sql)[0];
if (!stmt || stmt.type !== 'SELECT') {
throw new DatabaseError('queryStream only supports SELECT statements', 'NOT_SUPPORTED');
}
const select = stmt as import('./query/ast').SelectStatement;
// 不可流式场景:JOIN / GROUP BY / HAVING / DISTINCT / 聚合 / UNION / 关联子查询 / ORDER BY
const aggregate = select.columns.some((c) => /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(c));
const streamable = !select.joins && !select.groupBy && !select.having && !select.distinct
&& !aggregate && !(select.orderBy && select.orderBy.length > 0)
&& !(select.where && select.where['$exists'] !== undefined);
if (streamable && typeof this.engine.findStream === 'function') {
// 用户回调为 async(返回 Promise)时引擎同步扫描无法 await → 回退物化
const isAsync = (onRow as { constructor?: { name?: string } }).constructor?.name === 'AsyncFunction';
if (!isAsync) {
const where = this.normalizeWhereForStream(select);
const plainCols = select.columns.filter((c) => !/\s+AS\s+\w+$/i.test(c));
return this.engine.findStream(select.from, {
table: select.from,
columns: plainCols.length > 0 && plainCols[0] !== '*' ? plainCols : ['*'],
where: where && Object.keys(where).length > 0 ? where : undefined,
limit: select.limit,
offset: select.offset,
}, onRow as (row: Record<string, unknown>) => void);
}
}
// 回退:物化后逐行回调
const result = await this.query(sql);
if (Array.isArray(result)) {
for (const row of result as T[]) {
await onRow(row);
}
return result.length;
}
return 0;
}
/** 流式查询用:剥离主表别名前缀(复用 query 路径的规范化逻辑) */
private normalizeWhereForStream(select: import('./query/ast').SelectStatement): import('./constants').WhereCondition | undefined {
const aliases = [select.alias ?? select.from].filter(Boolean);
const strip = (col: string): string => {
for (const a of aliases) {
if (col.startsWith(`${a}.`)) return col.slice(a.length + 1);
}
return col;
};
const walk = (w: import('./constants').WhereCondition): import('./constants').WhereCondition => {
const out: import('./constants').WhereCondition = {};
for (const [k, v] of Object.entries(w)) {
if (k === '$and' || k === '$or') {
out[k] = (v as import('./constants').WhereCondition[]).map(walk);
} else if (k === '$not' && typeof v === 'object' && v !== null) {
out.$not = walk(v as import('./constants').WhereCondition);
} else {
out[strip(k)] = v;
}
}
return out;
};
return walk(select.where ?? {});
}
// ---- 事务 ----
/** 执行事务 */