fix: v0.7.4 写语句子查询 / 约束硬化 / 真惰性流式 — UPDATE-DELETE WHERE 子查询静默 0 行(四引擎,关联引用显式 NOT_SUPPORTED + EXPLAIN 同步)/ 主键 NULL-undefined 强制拒绝 / DROP INDEX 保留建表 UNIQUE(仅索引来源可解除)/ GROUP BY-DISTINCT-UNION 键类型安全编码 / UPDATE 未知列报错 / queryStream 多语句拒绝 / KVStore 后台错误跨 reopen 清理 + Hybrid begin 补偿 / RB-Tree 删除双黑修复 + LSM 死代码清理 / findStream 迭代器化真惰性(limit 早停 O(1) 内存)/ REINDEX 单次扫描 + 48 回归

This commit is contained in:
thzxx
2026-08-15 15:08:33 +08:00
parent ccfc39656b
commit d4a3f4acd2
17 changed files with 943 additions and 72 deletions
+57 -4
View File
@@ -18,6 +18,26 @@ import { matchWhere, applyOrderBy, projectColumns } from './where-matcher';
import { parseWhereCondition } from '../sql/parser';
import type { WhereCondition } from '../constants';
// ---------------------------------------------------------------------------
// 分组 / 去重键编码(v0.7.4)
// ---------------------------------------------------------------------------
/**
* v0.7.4: 分组/去重键的类型安全编码 —— 此前 `String(v ?? 'null')` 使
* null 与字符串 'null' 合并为一组(GROUP BY 静默少组),`String(v ?? '\0')`
* 使 null/undefined/'\0' 在 DISTINCT/UNION 中互相吞并。类型前缀编码后
* 各类型独立,仅同类型同值合并(与 where-matcher 的 === 语义一致)。
*/
function encodeGroupKey(v: unknown): string {
if (v === null) return 'n';
if (v === undefined) return 'u';
if (typeof v === 'string') return `s${v}`;
if (typeof v === 'number') return `d${v}`;
if (typeof v === 'boolean') return `b${v}`;
if (typeof v === 'object') return `o${JSON.stringify(v)}`;
return `x${String(v)}`;
}
// ---------------------------------------------------------------------------
// CASE WHEN 表达式(v0.3.1
// ---------------------------------------------------------------------------
@@ -147,7 +167,7 @@ export class QueryExecutor {
const seen = new Set<string>();
const result: Record<string, unknown>[] = [];
for (const row of normalized) {
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
if (!seen.has(key)) {
seen.add(key);
result.push(row);
@@ -155,7 +175,7 @@ export class QueryExecutor {
}
for (const row of rightRows) {
const projected = this.projectUnionRow(row, leftCols);
const key = Object.values(projected).map((v) => String(v ?? '\0')).join('\x1f');
const key = Object.values(projected).map(encodeGroupKey).join('\x1f');
if (!seen.has(key)) {
seen.add(key);
result.push(projected);
@@ -195,7 +215,10 @@ export class QueryExecutor {
rows = Array.isArray(result) ? result.length : 0;
} else if (stmt.query.type === 'UPDATE' || stmt.query.type === 'DELETE') {
try {
// v0.7.4: 子查询解析后估算 —— 此前 $subquery 未解析使 count 恒 0
await this.resolveWriteWhere(stmt.query);
const plan = compileStatement(stmt.query);
plan.where = stmt.query.where;
rows = await this.engine.count(plan.table, plan);
} catch { rows = 0; }
}
@@ -593,7 +616,9 @@ export class QueryExecutor {
private executeGroupBy(rows: Record<string, unknown>[], stmt: SelectStatement): Record<string, unknown>[] {
const groups = new Map<string, Record<string, unknown>[]>();
for (const row of rows) {
const key = stmt.groupBy!.map((col) => String(row[col] ?? 'null')).join('|');
// v0.7.4: 类型安全键编码 —— 此前 String(row[col] ?? 'null') 使
// null 与字符串 'null' 合并为一组(GROUP BY 静默少组)
const key = stmt.groupBy!.map((col) => encodeGroupKey(row[col])).join('\x1f');
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(row);
}
@@ -660,7 +685,8 @@ export class QueryExecutor {
private executeDistinct(rows: Record<string, unknown>[]): Record<string, unknown>[] {
const seen = new Set<string>();
return rows.filter((row) => {
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
// v0.7.4: 类型安全键编码(null 与 'null' 字符串、'\0' 分离)
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
if (seen.has(key)) return false;
seen.add(key);
return true;
@@ -714,15 +740,42 @@ export class QueryExecutor {
}
private async executeUpdate(stmt: UpdateStatement): Promise<number> {
// v0.7.4: 先解析 WHERE 子查询 —— 此前直接 compileStatement 调引擎:
// 引擎层 matchWhere 的 $in/$nin 遇未解析的 $subquery 对象恒 false →
// 所有行不匹配,UPDATE 静默影响 0 行(与 queryStream v0.7.3 修复同类)。
await this.resolveWriteWhere(stmt);
const plan = compileStatement(stmt);
plan.where = stmt.where;
return this.engine.update(plan.table, plan, stmt.sets);
}
private async executeDelete(stmt: DeleteStatement): Promise<number> {
// v0.7.4: 同 executeUpdate —— DELETE 子查询 WHERE 此前静默删除 0 行
await this.resolveWriteWhere(stmt);
const plan = compileStatement(stmt);
plan.where = stmt.where;
return this.engine.delete(plan.table, plan);
}
/**
* v0.7.4: 写语句(UPDATE/DELETEWHERE 的子查询解析。
* 非关联子查询($subquery)解析为具体值列表/标量;
* 关联引用($col / 关联 EXISTS)在写语句中无法逐行绑定外层上下文
* (引擎层 matchWhere 无 $col 绑定选项)→ 显式 NOT_SUPPORTED 而非静默 0 行。
*/
private async resolveWriteWhere(stmt: UpdateStatement | DeleteStatement): Promise<WhereCondition> {
const where = stmt.where;
if (!where || Object.keys(where).length === 0) return where ?? {};
if (this.hasCorrelatedRefs(where)) {
throw new DatabaseError(
'Correlated subqueries and column references are not supported in UPDATE/DELETE WHERE clauses',
'NOT_SUPPORTED',
);
}
stmt.where = await this.resolveSubqueries(where);
return stmt.where;
}
private async executeCreateTable(stmt: CreateTableStatement): Promise<void> {
// IF NOT EXISTS: 表已存在时静默返回
if (stmt.ifNotExists) {
+31 -2
View File
@@ -31,6 +31,36 @@ function compileLikeRegex(pattern: string): RegExp {
// WHERE 匹配(顶层入口)
// ---------------------------------------------------------------------------
/**
* v0.7.4: 检测 WHERE 中未解析的子查询/列引用标记($subquery / $col / $exists)。
* QueryBuilder 等直通引擎的写路径(update/delete)不经 Executor 解析子查询,
* 引擎层 matchWhere 对未解析标记恒 false → 静默影响 0 行。
* 写路径预检阶段显式拒绝;SELECT 路径由 Executor 解析(不调用本函数)。
*/
export function containsUnresolvedSubqueries(where: WhereCondition | undefined): boolean {
if (!where) return false;
for (const [k, v] of Object.entries(where)) {
if (k === '$and' || k === '$or') {
if ((v as WhereCondition[]).some((sub) => containsUnresolvedSubqueries(sub))) return true;
continue;
}
if (k === '$not') {
if (containsUnresolvedSubqueries(v as WhereCondition)) return true;
continue;
}
if (k === '$exists') return true;
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
for (const [, operand] of Object.entries(v as Record<string, unknown>)) {
if (typeof operand === 'object' && operand !== null) {
const ops = operand as Record<string, unknown>;
if ('$subquery' in ops || '$col' in ops) return true;
}
}
}
}
return false;
}
/**
* 匹配完整 WHERE 条件
* @param row 当前数据行
@@ -41,8 +71,7 @@ export function matchWhere(
row: Record<string, unknown>,
where: WhereCondition,
options: { $col?: boolean } = {},
): boolean {
for (const [field, condition] of Object.entries(where)) {
): boolean { for (const [field, condition] of Object.entries(where)) {
// 顶层 $caseResultv0.3.2):由 Executor 对 CASE WHEN 表达式逐行求值后产生
if (field === '$caseResult') {
if (condition !== true) return false;