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:
+57
-4
@@ -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/DELETE)WHERE 的子查询解析。
|
||||
* 非关联子查询($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,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)) {
|
||||
// 顶层 $caseResult(v0.3.2):由 Executor 对 CASE WHEN 表达式逐行求值后产生
|
||||
if (field === '$caseResult') {
|
||||
if (condition !== true) return false;
|
||||
|
||||
Reference in New Issue
Block a user