fix: v0.7.3 数据正确性与边界窗口收尾 — INSERT 语句级原子(三引擎+Aria PK 批内重复)/ 索引列 IS NULL 恒空 / delete RESTRICT 破坏索引 / queryStream 子查询静默空结果 / ALTER DROP 索引残留 / UNIQUE INDEX 存量校验 / SELECT * 别名投影 / WAL BEGIN/ROLLBACK 事务边界 / aria $in 与级联重复扫描性能 / $and 等值下推 / ANALYZE 索引统计 / React-Vue hooks 生命周期 / 迁移主键兜底 + 58 回归
CI / test (18.x) (push) Successful in 17m43s
CI / test (22.x) (push) Successful in 13m45s
CI / test (20.x) (push) Successful in 15m33s
CI / test (24.x) (push) Successful in 24m46s
CI / e2e (push) Successful in 52s

This commit is contained in:
thzxx
2026-08-14 22:53:46 +08:00
parent f8f8d1b2ff
commit 50468b9b0e
29 changed files with 2374 additions and 650 deletions
+40 -12
View File
@@ -209,18 +209,31 @@ export class QueryExecutor {
// v0.7.0: 真实索引命中信息(此前 usingIndex 恒为 'auto' 占位)。
// 引擎无关启发式:WHERE 中存在主键/索引/唯一列条件 → 对应引擎索引路径。
// v0.7.3: 递归识别 $and 嵌套等值条件(与 Memory/Aria 的 $and 下推行为对齐;
// $or/$not 不下推,保持 none)。
let usingIndex: string = plan?.table ? 'none' : 'none';
if (plan && plan.table && plan.where && Object.keys(plan.where).length > 0) {
try {
const schema = await this.engine.getTableSchema(plan.table);
if (schema) {
for (const col of Object.keys(plan.where)) {
if (col.startsWith('$')) continue;
const colDef = schema.columns[col];
if (!colDef) continue;
if (colDef.primaryKey) { usingIndex = 'pk'; break; }
if (colDef.index || colDef.unique) { usingIndex = `index:${col}`; break; }
}
const findIndex = (w: import('../constants').WhereCondition): string | null => {
for (const [k, v] of Object.entries(w)) {
if (k === '$and') {
for (const sub of (v as import('../constants').WhereCondition[])) {
const hit = findIndex(sub);
if (hit) return hit;
}
continue;
}
if (k === '$or' || k === '$not') continue;
const colDef = schema.columns[k];
if (!colDef) continue;
if (colDef.primaryKey) return 'pk';
if (colDef.index || colDef.unique) return `index:${k}`;
}
return null;
};
usingIndex = findIndex(plan.where) ?? 'none';
}
} catch { /* schema 读取失败保持 none */ }
}
@@ -341,7 +354,11 @@ export class QueryExecutor {
rows = rows.filter((row) => matchWhere(row, stmt.having!));
}
if (stmt.orderBy && stmt.orderBy.length > 0) rows = applyOrderBy(rows, stmt.orderBy);
if (!hasGroupBy && !hasAggregate && stmt.columns.length > 0 && stmt.columns[0] !== '*') {
// v0.7.3: `SELECT *, col AS alias` —— 此前 columns[0]==='*' 直接不投影,
// 别名列/常量列丢失。仅当 '*' 是唯一列时跳过投影(projectRow 对裸 '*'
// 合并原行全部列,其余表达式覆盖/追加)
if (!hasGroupBy && !hasAggregate && stmt.columns.length > 0
&& !(stmt.columns.length === 1 && stmt.columns[0] === '*')) {
rows = rows.map((row) => this.projectRow(row, stmt.columns));
}
// v0.3.3: ORDER BY 别名 → 投影后才存在,需在投影后重新排序
@@ -942,8 +959,13 @@ export class QueryExecutor {
const aliasCols: { alias: string; source: string }[] = [];
const caseCols: { alias: string; expr: CaseExpression }[] = [];
const constCols: { key: string; value: unknown }[] = [];
// v0.7.3: 裸 '*' 与列表达式混合(SELECT *, name AS nick)→ 原行全部列为基
let hasStar = false;
for (const col of columns) {
if (col === '*') continue;
if (col === '*') {
hasStar = true;
continue;
}
const expr = parseCaseExpression(col);
if (expr) {
caseCols.push({ alias: expr.alias ?? col, expr });
@@ -957,19 +979,25 @@ export class QueryExecutor {
// v0.4.0: 字符串常量列 SELECT 'lit' → 常量输出
const lit = col.match(/^'(.*)'$/s);
if (lit) {
const value = lit[1].replace(/\\'/g, "'");
// v0.7.3: SQL 标准 '' 转义还原(readString 已把 '' 合并为单个 '
// 打包回列的文本中相邻两个 ' 即一个引号字面量)
const value = lit[1].replace(/''/g, "'");
constCols.push({ key: col, value });
continue;
}
plain.push(col);
}
const projected = plain.length > 0 ? projectColumns(row, plain) : {};
// v0.7.3: hasStar 时以原行全部列为基(projectColumns 仅投影 plain 列,不含 * 的其余列)
const projected = hasStar
? { ...row }
: (plain.length > 0 ? projectColumns(row, plain) : {});
for (const { alias, source } of aliasCols) {
if (source === '*') {
Object.assign(projected, row);
} else {
const lit = source.match(/^'(.*)'$/s);
projected[alias] = lit ? lit[1].replace(/\\'/g, "'") : row[source];
// v0.7.3: 同 constCols —— SQL 标准 '' 转义还原
projected[alias] = lit ? lit[1].replace(/''/g, "'") : row[source];
}
}
for (const { key, value } of constCols) {