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:
+81
-10
@@ -8,7 +8,7 @@
|
||||
import type { IStorageEngine } from '../interface';
|
||||
import type { QueryPlan, TableSchema, ColumnDef, WhereCondition } from '../../constants';
|
||||
import { DatabaseError } from '../../constants';
|
||||
import { matchWhere, applyOrderBy, projectColumns } from '../../query/where-matcher';
|
||||
import { matchWhere, applyOrderBy, projectColumns, containsUnresolvedSubqueries } from '../../query/where-matcher';
|
||||
import { checkFieldType, stripUndefinedUpdates } from '../../table/schema';
|
||||
|
||||
import type { AriaEngineConfig, SSTableMeta } from './types';
|
||||
@@ -63,6 +63,14 @@ export class AriaEngine implements IStorageEngine {
|
||||
// 二级索引:table.colKey → LSM
|
||||
private secondaryIndexes: Map<string, LSM> = new Map();
|
||||
|
||||
/**
|
||||
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
|
||||
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
|
||||
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且重启后永久消失)。
|
||||
* 注:重启后无法区分历史来源,schema 中的 unique 一律按建表约束保护(保守)。
|
||||
*/
|
||||
private uniqueIndexCols: Set<string> = new Set();
|
||||
|
||||
// MVCC 事务
|
||||
private mvcc: MVCCManager = new MVCCManager();
|
||||
private currentTxnId: number | null = null;
|
||||
@@ -297,6 +305,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
this.schemas.clear();
|
||||
this.tablePKs.clear();
|
||||
this.secondaryIndexes.clear();
|
||||
this.uniqueIndexCols.clear();
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
this.txnSnapshot = null;
|
||||
@@ -392,6 +401,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
this.schemas.clear();
|
||||
this.tablePKs.clear();
|
||||
this.secondaryIndexes.clear();
|
||||
this.uniqueIndexCols.clear();
|
||||
this.lsm.clear();
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
@@ -479,6 +489,11 @@ export class AriaEngine implements IStorageEngine {
|
||||
// v0.4.2-fix: 清理该表的全部二级索引 LSM 与持久化文件 —
|
||||
// 此前残留孤儿索引,重建同名表后旧索引数据污染新表(索引查询返回错误行)
|
||||
await this.cleanupTableIndexes(tableName);
|
||||
// v0.7.4: 清理该表的 unique 索引来源标记
|
||||
const uPrefix = `${tableName}:`;
|
||||
for (const k of this.uniqueIndexCols) {
|
||||
if (k.startsWith(uPrefix)) this.uniqueIndexCols.delete(k);
|
||||
}
|
||||
|
||||
this.schemas.delete(tableName);
|
||||
this.tablePKs.delete(tableName);
|
||||
@@ -701,6 +716,14 @@ export class AriaEngine implements IStorageEngine {
|
||||
): Promise<number> {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError(
|
||||
'Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)',
|
||||
'NOT_SUPPORTED',
|
||||
);
|
||||
}
|
||||
const schema = this.schemas.get(tableName)!;
|
||||
const rows = await this.getAllRows(tableName);
|
||||
let count = 0;
|
||||
@@ -711,6 +734,14 @@ export class AriaEngine implements IStorageEngine {
|
||||
// v0.7.2: undefined 值视为"不更新该列"(保留旧值),null 显式置空
|
||||
const cleanUpdates = stripUndefinedUpdates(updates);
|
||||
|
||||
// v0.7.4: 未知列显式报错 —— 此前 SET nonexistent = ... 被静默写入存储行
|
||||
// (validateRow 只遍历 schema 列,脏列残留在行内并随 SSTable 持久化)
|
||||
for (const col of Object.keys(cleanUpdates)) {
|
||||
if (!schema.columns[col]) {
|
||||
throw new DatabaseError(`Column "${col}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
}
|
||||
|
||||
// v0.6.2: 唯一约束 — 批量预加载本批更新涉及的唯一列索引范围(一次 drainChain)
|
||||
const uniqueCols = this.uniqueColumns(tableName, schema);
|
||||
for (const colName of uniqueCols) {
|
||||
@@ -941,6 +972,14 @@ export class AriaEngine implements IStorageEngine {
|
||||
async delete(tableName: string, query: QueryPlan): Promise<number> {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError(
|
||||
'Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)',
|
||||
'NOT_SUPPORTED',
|
||||
);
|
||||
}
|
||||
|
||||
const rows = await this.getAllRows(tableName);
|
||||
let count = 0;
|
||||
@@ -1175,13 +1214,14 @@ export class AriaEngine implements IStorageEngine {
|
||||
return count;
|
||||
}
|
||||
|
||||
// 全表惰性扫描(含 WHERE 过滤,不物化)
|
||||
// 全表惰性扫描(含 WHERE 过滤,不物化;v0.7.4: callback 返回 false 提前终止,
|
||||
// 未消费的 SSTable 块 / 子树不再解析 —— 真流式,大表 limit 内存 O(1))
|
||||
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
|
||||
this.lsm.rangeScanLazy(prefix, `${prefix}\uffff`, (key, value) => {
|
||||
if (count >= limit) return;
|
||||
if (count >= limit) return false;
|
||||
const row = { ...value };
|
||||
row[pkCol] = key.slice(prefix.length);
|
||||
emit(row);
|
||||
return emit(row);
|
||||
});
|
||||
return count;
|
||||
}
|
||||
@@ -1350,7 +1390,11 @@ export class AriaEngine implements IStorageEngine {
|
||||
throw error;
|
||||
}
|
||||
colDef.index = true;
|
||||
if (unique) colDef.unique = true;
|
||||
if (unique) {
|
||||
colDef.unique = true;
|
||||
// v0.7.4: 记录唯一约束来源(DROP INDEX 时可解除;建表约束不可)
|
||||
this.uniqueIndexCols.add(`${tableName}:${column}`);
|
||||
}
|
||||
await this.persistSchemas();
|
||||
}
|
||||
|
||||
@@ -1369,8 +1413,20 @@ export class AriaEngine implements IStorageEngine {
|
||||
if (!colDef.index && !colDef.unique && !this.secondaryIndexes.has(`${tableName}:idx:${column}`)) {
|
||||
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
|
||||
}
|
||||
// v0.7.4: 建表 UNIQUE 约束不可通过 DROP INDEX 解除 —— 此前 colDef.unique = false
|
||||
// 静默解除约束(重启后 persistSchemas 使约束永久消失)。对齐 SQLite 语义:
|
||||
// 约束随建表存在,解除需重建表;仅 CREATE UNIQUE INDEX 添加的约束可随索引删除。
|
||||
const uniqueKey = `${tableName}:${column}`;
|
||||
if (colDef.unique && !this.uniqueIndexCols.has(uniqueKey)) {
|
||||
throw new DatabaseError(
|
||||
`Cannot drop index on column "${column}" in table "${tableName}": ` +
|
||||
'UNIQUE constraint defined at table creation must be removed by recreating the table',
|
||||
'NOT_SUPPORTED',
|
||||
);
|
||||
}
|
||||
colDef.index = false;
|
||||
colDef.unique = false;
|
||||
this.uniqueIndexCols.delete(uniqueKey);
|
||||
|
||||
const idxKey = `${tableName}:idx:${column}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
@@ -1596,6 +1652,14 @@ export class AriaEngine implements IStorageEngine {
|
||||
if (colDef.required && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
// v0.7.4: 主键列强制非空(SQL 语义 PK 隐含 NOT NULL)——
|
||||
// 此前 null/undefined 主键被 String() 化为 "null"/"undefined" 静默入库
|
||||
if (colDef.primaryKey && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(
|
||||
`Primary key column "${colName}" in table "${schema.name}" cannot be null or undefined`,
|
||||
'VALIDATION_ERROR',
|
||||
);
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
this.checkType(colName, colDef.type, value, colDef);
|
||||
}
|
||||
@@ -2147,9 +2211,17 @@ export class AriaEngine implements IStorageEngine {
|
||||
if (!schema) return 0;
|
||||
let rebuiltCount = 0;
|
||||
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
// v0.3.3: 主键列不建冗余二级索引(主 LSM 即 PK 索引)
|
||||
if (!colDef.index && !colDef.unique) continue;
|
||||
// v0.7.4-perf: 单次全表扫描重建全部索引列 —— 此前每个索引列各做一次
|
||||
// getAllRows(N 列 × M 行全表扫描 + 每次 prefetchRange drainChain),
|
||||
// 多索引大表 REINDEX/崩溃恢复按索引列数线性放大
|
||||
const pkCol = this.tablePKs.get(tableName)!;
|
||||
const idxCols = Object.entries(schema.columns).filter(
|
||||
([, colDef]) => colDef.index || colDef.unique,
|
||||
);
|
||||
if (idxCols.length === 0) return 0;
|
||||
|
||||
const rows = await this.getAllRows(tableName);
|
||||
for (const [colName] of idxCols) {
|
||||
const idxKey = `${tableName}:idx:${colName}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
if (!idxLsm) continue;
|
||||
@@ -2159,11 +2231,10 @@ export class AriaEngine implements IStorageEngine {
|
||||
rebuiltCount++;
|
||||
|
||||
// 从主 LSM 重建索引
|
||||
const rows = await this.getAllRows(tableName);
|
||||
for (const row of rows) {
|
||||
const val = row[colName];
|
||||
if (val !== undefined && val !== null) {
|
||||
idxLsm.put(`${String(val)}:${row[this.tablePKs.get(tableName)!]}`, { pk: row[this.tablePKs.get(tableName)!] });
|
||||
idxLsm.put(`${String(val)}:${row[pkCol]}`, { pk: row[pkCol] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user