release: v0.4.1 — Aria 级联/ALTER/clearAll + 流式查询/派生表 + 正确性加固
新增: - 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:
+384
-28
@@ -2,7 +2,7 @@
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
*
|
||||
* v0.2.5: WAL 同步修复 + MVCC 接入 + 版本统一 + 生产加固
|
||||
* v0.4.1: 外键级联 + ALTER TABLE 重写 + clearAll 重置 + 崩溃恢复加固
|
||||
*/
|
||||
|
||||
import type { IStorageEngine } from '../interface';
|
||||
@@ -161,10 +161,22 @@ export class AriaEngine implements IStorageEngine {
|
||||
// 第二遍:仅应用 txnId==0(非事务)或已提交事务的数据
|
||||
for (const r of allRecords) {
|
||||
if (r.txnId === 0 || committedTxns.has(r.txnId)) {
|
||||
this.applyWALRecord(r);
|
||||
if (r.type === WALRecordType.DROP_TABLE) {
|
||||
// v0.3.3: DROP_TABLE 回放(异步:需预加载 SSTable 后清除残留数据)
|
||||
await this.applyDropTableRecovery(r.tableName);
|
||||
} else {
|
||||
this.applyWALRecord(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v0.3.3: 恢复完成后将回放数据落盘并截断 WAL,
|
||||
// 避免每次重启重复回放 + WAL 无限膨胀
|
||||
if (allRecords.length > 0) {
|
||||
await this.lsm.flush();
|
||||
await this.wal.checkpoint();
|
||||
}
|
||||
|
||||
// 8. Checkpoint Manager(接入 WAL 大小阈值)
|
||||
this.checkpointManager = new CheckpointManager(
|
||||
this.lsm,
|
||||
@@ -187,6 +199,29 @@ export class AriaEngine implements IStorageEngine {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.4.1: 重置数据库 — 清空全部数据与表结构(演示页刷新/重新初始化用)。
|
||||
* 清空存储后端、LSM、WAL、MVCC 与二级索引,后续可继续使用本实例。
|
||||
*/
|
||||
async clearAll(): Promise<void> {
|
||||
this.ensureOpen();
|
||||
// 清空存储后端(页面文件 / WAL 记录 / schema 记录 / 元数据)
|
||||
await this.backend.clear();
|
||||
this.schemas.clear();
|
||||
this.tablePKs.clear();
|
||||
this.secondaryIndexes.clear();
|
||||
this.lsm.clear();
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
this.txnSnapshot = null;
|
||||
this.savepoints.clear();
|
||||
this.opCounter = 0;
|
||||
// 持久化空 schema(防止旧 schema 记录残留)
|
||||
await this.persistSchemas();
|
||||
// 重置 WAL 状态(backend.clear 已清记录,同步内存计数)
|
||||
await this.wal.checkpoint();
|
||||
}
|
||||
|
||||
isOpen(): boolean { return this.opened; }
|
||||
|
||||
// =======================================================================
|
||||
@@ -203,8 +238,9 @@ export class AriaEngine implements IStorageEngine {
|
||||
this.tablePKs.set(schema.name, this.getPK(schema));
|
||||
|
||||
// 为索引列创建二级索引 LSM(每个索引使用独立命名空间的 SSTableStore,避免 id/meta 冲突)
|
||||
// v0.3.3: 主键列不建冗余二级索引(主 LSM 本身就是 PK 索引,范围查询走前缀扫描)
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
if (colDef.index || colDef.unique || colDef.primaryKey) {
|
||||
if (colDef.index || colDef.unique) {
|
||||
const idxKey = `${schema.name}:idx:${colName}`;
|
||||
if (!this.secondaryIndexes.has(idxKey)) {
|
||||
const idxLsm = new LSM({
|
||||
@@ -344,24 +380,8 @@ export class AriaEngine implements IStorageEngine {
|
||||
rows = await this.getAllRows(tableName);
|
||||
}
|
||||
|
||||
// Merge transaction snapshot writes (uncommitted data visible within txn)
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
const pkCol = this.tablePKs.get(tableName)!;
|
||||
const prefix = `${tableName}:`;
|
||||
for (const [key, value] of this.txnSnapshot) {
|
||||
if (!key.startsWith(prefix)) continue;
|
||||
const pk = key.slice(prefix.length);
|
||||
const del = (value as unknown as Record<string, unknown>).__txn_deleted;
|
||||
const idx = rows.findIndex((r) => r[pkCol] === pk);
|
||||
if (del) {
|
||||
if (idx >= 0) rows.splice(idx, 1);
|
||||
} else {
|
||||
const row = { ...value, [pkCol]: pk };
|
||||
if (idx >= 0) rows[idx] = row;
|
||||
else rows.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
// v0.3.3: 事务内合并未提交快照(统一在 mergeTxnSnapshot 处理)
|
||||
rows = this.mergeTxnSnapshot(tableName, rows);
|
||||
|
||||
// WHERE filter
|
||||
if (query.where && Object.keys(query.where).length > 0) {
|
||||
@@ -447,12 +467,16 @@ export class AriaEngine implements IStorageEngine {
|
||||
let count = 0;
|
||||
// v0.3.1: 批量 WAL 写入(组提交)
|
||||
const walRecords: Omit<import('./types').WALRecord, 'lsn' | 'checksum'>[] = [];
|
||||
// v0.4.1: 外键级联(环路保护)
|
||||
const visited = new Set<string>();
|
||||
|
||||
for (const row of rows) {
|
||||
const pkCol = this.tablePKs.get(tableName)!;
|
||||
const key = `${tableName}:${row[pkCol]}`;
|
||||
|
||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||
// v0.4.1: 外键规则(RESTRICT 抛错 / CASCADE 递归删 / SET NULL 置空)
|
||||
count += await this.applyForeignKeyRules(tableName, String(row[pkCol]), walRecords, visited);
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
// Buffer delete in snapshot + MVCC tombstone
|
||||
this.txnSnapshot.set(key, { __txn_deleted: true } as unknown as Record<string, unknown>);
|
||||
@@ -482,6 +506,154 @@ export class AriaEngine implements IStorageEngine {
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.4.1: 外键级联规则 — 对齐 MemoryEngine.cascadeDelete 行为。
|
||||
* 删除 tableName 主键为 pkValue 的行前,检查引用它的所有表:
|
||||
* - RESTRICT: 存在引用行 → 抛 FOREIGN_KEY_VIOLATION
|
||||
* - CASCADE: 递归删除引用行(含索引/WAL)
|
||||
* - SET NULL: 引用行外键列置 null(含索引/WAL)
|
||||
* @returns 级联影响的行数(CASCADE 删除行数 + SET NULL 更新行数)
|
||||
*/
|
||||
private async applyForeignKeyRules(
|
||||
tableName: string,
|
||||
pkValue: string,
|
||||
walRecords: Omit<import('./types').WALRecord, 'lsn' | 'checksum'>[],
|
||||
visited: Set<string>,
|
||||
): Promise<number> {
|
||||
let total = 0;
|
||||
const visitKey = `${tableName}:${pkValue}`;
|
||||
if (visited.has(visitKey)) return 0;
|
||||
visited.add(visitKey);
|
||||
|
||||
for (const [refTableName, refSchema] of this.schemas) {
|
||||
if (refTableName === tableName) continue;
|
||||
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
|
||||
if (!colDef.references || !colDef.onDelete) continue;
|
||||
const [refTable] = colDef.references.split('.');
|
||||
if (refTable !== tableName) continue;
|
||||
|
||||
const refRows = await this.getAllRows(refTableName);
|
||||
const matched = refRows.filter((r) => String(r[colName]) === pkValue);
|
||||
|
||||
if (colDef.onDelete === 'RESTRICT' && matched.length > 0) {
|
||||
throw new DatabaseError(
|
||||
`Cannot delete from "${tableName}": foreign key "${colName}" in "${refTableName}" has dependent rows`,
|
||||
'FOREIGN_KEY_VIOLATION',
|
||||
);
|
||||
}
|
||||
|
||||
if (colDef.onDelete === 'CASCADE') {
|
||||
const refPkCol = this.tablePKs.get(refTableName)!;
|
||||
for (const refRow of matched) {
|
||||
const refPk = String(refRow[refPkCol]);
|
||||
// 递归级联(先处理更深层引用)
|
||||
total += await this.applyForeignKeyRules(refTableName, refPk, walRecords, visited);
|
||||
// 删除引用行
|
||||
const refKey = `${refTableName}:${refPk}`;
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
this.txnSnapshot.set(refKey, { __txn_deleted: true } as unknown as Record<string, unknown>);
|
||||
this.mvcc.deleteVersion(refTableName, refPk, this.currentTxnId);
|
||||
} else {
|
||||
this.lsm.delete(refKey);
|
||||
}
|
||||
this.updateSecondaryIndexes(refTableName, refPk, null, refRow);
|
||||
walRecords.push({
|
||||
type: WALRecordType.DELETE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName: refTableName,
|
||||
key: refPk,
|
||||
});
|
||||
total++;
|
||||
}
|
||||
} else if (colDef.onDelete === 'SET NULL') {
|
||||
const refPkCol = this.tablePKs.get(refTableName)!;
|
||||
for (const refRow of matched) {
|
||||
const refPk = String(refRow[refPkCol]);
|
||||
const updated = { ...refRow, [colName]: null };
|
||||
const refKey = `${refTableName}:${refPk}`;
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
this.txnSnapshot.set(refKey, updated);
|
||||
this.mvcc.writeVersion(refTableName, refPk, updated, this.currentTxnId);
|
||||
} else {
|
||||
this.lsm.put(refKey, updated);
|
||||
}
|
||||
this.updateSecondaryIndexes(refTableName, refPk, updated, refRow);
|
||||
walRecords.push({
|
||||
type: WALRecordType.UPDATE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName: refTableName,
|
||||
key: refPk,
|
||||
data: updated,
|
||||
});
|
||||
// 对齐 Memory 语义:SET NULL 不影响返回的删除行数
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.4.0: 流式查询 — 逐行回调,不物化结果数组。
|
||||
* 全表路径走 LSM rangeScanLazy 惰性扫描;索引等值/范围路径复用 tryIndexLookup。
|
||||
* 事务中回退物化(快照合并需要全量行集)。
|
||||
*/
|
||||
async findStream(
|
||||
tableName: string,
|
||||
query: QueryPlan,
|
||||
onRow: (row: Record<string, unknown>) => void,
|
||||
): Promise<number> {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
|
||||
const hasWhere = !!(query.where && Object.keys(query.where).length > 0);
|
||||
const project = query.columns && query.columns.length > 0 && query.columns[0] !== '*'
|
||||
? (row: Record<string, unknown>) => projectColumns(row, query.columns!)
|
||||
: null;
|
||||
const limit = query.limit ?? Infinity;
|
||||
const offset = query.offset ?? 0;
|
||||
const pkCol = this.tablePKs.get(tableName)!;
|
||||
const prefix = `${tableName}:`;
|
||||
let count = 0;
|
||||
let skipped = 0;
|
||||
|
||||
const emit = (row: Record<string, unknown>): boolean => {
|
||||
if (hasWhere && !matchWhere(row, query.where!)) return true;
|
||||
if (skipped < offset) { skipped++; return true; }
|
||||
onRow(project ? project(row) : row);
|
||||
count++;
|
||||
return count < limit;
|
||||
};
|
||||
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
// 事务中:物化后逐行回调(快照合并需要全量行集)
|
||||
const rows = await this.find(tableName, { ...query, orderBy: undefined, limit: undefined, offset: undefined });
|
||||
for (const row of rows) {
|
||||
onRow(project ? project(row) : row);
|
||||
}
|
||||
return rows.length;
|
||||
}
|
||||
|
||||
// 索引路径:等值/范围查找(结果行已过滤,直接回调)
|
||||
const fastPath = await this.tryIndexLookup(tableName, query);
|
||||
if (fastPath !== null) {
|
||||
for (const row of fastPath) {
|
||||
if (!emit(row)) break;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 全表惰性扫描(含 WHERE 过滤,不物化)
|
||||
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
|
||||
this.lsm.rangeScanLazy(prefix, `${prefix}\uffff`, (key, value) => {
|
||||
if (count >= limit) return;
|
||||
const row = { ...value };
|
||||
row[pkCol] = key.slice(prefix.length);
|
||||
emit(row);
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
@@ -495,13 +667,93 @@ export class AriaEngine implements IStorageEngine {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
const rows = await this.getAllRows(tableName);
|
||||
// v0.3.3: 事务内清空走快照(删除标记),提交时生效;并写入 WAL
|
||||
const walRecords: Omit<import('./types').WALRecord, 'lsn' | 'checksum'>[] = [];
|
||||
for (const row of rows) {
|
||||
const pkCol = this.tablePKs.get(tableName)!;
|
||||
this.lsm.delete(`${tableName}:${row[pkCol]}`);
|
||||
const key = `${tableName}:${row[pkCol]}`;
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
this.txnSnapshot.set(key, { __txn_deleted: true } as unknown as Record<string, unknown>);
|
||||
this.mvcc.deleteVersion(tableName, String(row[pkCol]), this.currentTxnId);
|
||||
} else {
|
||||
this.lsm.delete(key);
|
||||
}
|
||||
walRecords.push({
|
||||
type: WALRecordType.DELETE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
key: String(row[pkCol]),
|
||||
});
|
||||
// 移除二级索引
|
||||
this.updateSecondaryIndexes(tableName, String(row[pkCol]), null, row);
|
||||
}
|
||||
await this.wal.appendBatch(walRecords);
|
||||
this.opCounter += rows.length;
|
||||
await this.checkpointManager.tick();
|
||||
this.tryGC();
|
||||
}
|
||||
|
||||
// ---- 动态索引(v0.3.0) ----
|
||||
// ---- ALTER TABLE(v0.4.1) ----
|
||||
|
||||
/**
|
||||
* v0.4.1: ALTER TABLE — 结构变更真正生效于存储:
|
||||
* - ADD: 持久化 schema(persistSchemas),行无需修改
|
||||
* - DROP: 持久化 schema + 遍历主 LSM 重写所有行(移除该列键)+ WAL UPDATE 记录
|
||||
* (通用路径 getTableSchema 返回副本,Executor 的引用修改对 Aria 无效)
|
||||
*/
|
||||
async alterTable(
|
||||
tableName: string,
|
||||
action: 'ADD' | 'DROP',
|
||||
column: import('../../constants').ColumnDef & { name: string },
|
||||
): Promise<void> {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
const schema = this.schemas.get(tableName)!;
|
||||
|
||||
if (action === 'ADD') {
|
||||
if (schema.columns[column.name]) {
|
||||
throw new DatabaseError(`Column "${column.name}" already exists in table "${tableName}"`, 'COLUMN_EXISTS');
|
||||
}
|
||||
schema.columns[column.name] = column;
|
||||
await this.persistSchemas();
|
||||
return;
|
||||
}
|
||||
|
||||
// DROP
|
||||
if (!schema.columns[column.name]) {
|
||||
throw new DatabaseError(`Column "${column.name}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
delete schema.columns[column.name];
|
||||
await this.persistSchemas();
|
||||
|
||||
// 重写主 LSM:移除所有行的该列键(find 副本无法就地删除,必须重写存储)
|
||||
const pkCol = this.tablePKs.get(tableName)!;
|
||||
const prefix = `${tableName}:`;
|
||||
const endKey = `${prefix}\uffff`;
|
||||
await this.lsm.prefetchRange(prefix, endKey);
|
||||
const entries = this.lsm.rangeScan(prefix, endKey);
|
||||
const walRecords: Omit<import('./types').WALRecord, 'lsn' | 'checksum'>[] = [];
|
||||
for (const [key, value] of entries) {
|
||||
if (!(column.name in value)) continue;
|
||||
const updated = { ...value };
|
||||
delete updated[column.name];
|
||||
this.lsm.put(key, updated);
|
||||
// 二级索引列被删时同步清理索引
|
||||
const pk = key.slice(prefix.length);
|
||||
this.updateSecondaryIndexes(tableName, pk, updated, value);
|
||||
walRecords.push({
|
||||
type: WALRecordType.UPDATE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
key: pk,
|
||||
data: updated,
|
||||
});
|
||||
}
|
||||
await this.wal.appendBatch(walRecords);
|
||||
this.opCounter += walRecords.length;
|
||||
await this.checkpointManager.tick();
|
||||
this.trimAllCaches();
|
||||
}
|
||||
|
||||
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
|
||||
this.ensureOpen();
|
||||
@@ -551,6 +803,10 @@ export class AriaEngine implements IStorageEngine {
|
||||
if (colDef.primaryKey) {
|
||||
throw new DatabaseError(`Cannot drop primary key index on column "${column}"`, 'NOT_SUPPORTED');
|
||||
}
|
||||
// v0.4.1: DROP 不存在的索引应报错(此前静默成功)
|
||||
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');
|
||||
}
|
||||
colDef.index = false;
|
||||
colDef.unique = false;
|
||||
|
||||
@@ -610,6 +866,15 @@ export class AriaEngine implements IStorageEngine {
|
||||
async rollbackTransaction(): Promise<void> {
|
||||
if (!this.currentTxnId) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||
|
||||
// v0.3.3: 记录事务涉及的表(用于回滚后重建索引,消除索引残留)
|
||||
const affectedTables = new Set<string>();
|
||||
if (this.txnSnapshot) {
|
||||
for (const key of this.txnSnapshot.keys()) {
|
||||
const idx = key.indexOf(':');
|
||||
if (idx > 0) affectedTables.add(key.slice(0, idx));
|
||||
}
|
||||
}
|
||||
|
||||
this.mvcc.rollbackTransaction(this.currentTxnId);
|
||||
this.txnSnapshot = null;
|
||||
|
||||
@@ -621,6 +886,13 @@ export class AriaEngine implements IStorageEngine {
|
||||
});
|
||||
|
||||
this.currentTxnId = null;
|
||||
|
||||
// v0.3.3: 事务内直接写入了二级索引 LSM,回滚后全量重建受影响表的索引
|
||||
for (const tableName of affectedTables) {
|
||||
if (this.schemas.has(tableName)) {
|
||||
await this.reindexTable(tableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Savepoint 嵌套事务 ----
|
||||
@@ -642,6 +914,9 @@ export class AriaEngine implements IStorageEngine {
|
||||
if (!sp) throw new DatabaseError(`Savepoint "${name}" not found`, 'SAVEPOINT_NOT_FOUND');
|
||||
// 恢复到 savepoint 时的快照
|
||||
this.txnSnapshot = sp.snapshot ? new Map(sp.snapshot) : null;
|
||||
// v0.3.3: 清理该事务在 MVCC 版本链中的全部记录(快照已含正确数据,
|
||||
// 版本链仅作 undo 记录,清空后 commit 时 LSM 写入与快照保持一致)
|
||||
this.mvcc.discardVersions(this.currentTxnId!);
|
||||
// 清除此 savepoint 之后的所有 savepoint
|
||||
let found = false;
|
||||
for (const [k] of this.savepoints) {
|
||||
@@ -676,11 +951,37 @@ export class AriaEngine implements IStorageEngine {
|
||||
// 预加载范围内涉及的 SSTable,避免 rangeScan 时缓存未命中静默丢数据
|
||||
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
|
||||
const entries = this.lsm.rangeScan(prefix, `${prefix}\uffff`);
|
||||
return entries.map(([key, value]) => {
|
||||
const rows = entries.map(([key, value]) => {
|
||||
const row = { ...value };
|
||||
row[pkCol] = key.slice(prefix.length);
|
||||
return row;
|
||||
});
|
||||
// v0.3.3: 事务内合并未提交快照(update/delete/count/clear 也能看到本事务的写入)
|
||||
return this.mergeTxnSnapshot(tableName, rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.3.3: 将事务未提交快照的变更合并到行列表(新增/更新/删除标记)。
|
||||
* 幂等操作:行已是最新时不重复修改。
|
||||
*/
|
||||
private mergeTxnSnapshot(tableName: string, rows: Record<string, unknown>[]): Record<string, unknown>[] {
|
||||
if (!this.currentTxnId || !this.txnSnapshot) return rows;
|
||||
const pkCol = this.tablePKs.get(tableName)!;
|
||||
const prefix = `${tableName}:`;
|
||||
for (const [key, value] of this.txnSnapshot) {
|
||||
if (!key.startsWith(prefix)) continue;
|
||||
const pk = key.slice(prefix.length);
|
||||
const del = (value as unknown as Record<string, unknown>).__txn_deleted;
|
||||
const idx = rows.findIndex((r) => r[pkCol] === pk);
|
||||
if (del) {
|
||||
if (idx >= 0) rows.splice(idx, 1);
|
||||
} else {
|
||||
const row = { ...value, [pkCol]: pk };
|
||||
if (idx >= 0) rows[idx] = row;
|
||||
else rows.push(row);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private getPK(schema: TableSchema): string {
|
||||
@@ -872,11 +1173,31 @@ export class AriaEngine implements IStorageEngine {
|
||||
case WALRecordType.COMMIT:
|
||||
case WALRecordType.ROLLBACK:
|
||||
case WALRecordType.BEGIN:
|
||||
case WALRecordType.DROP_TABLE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.3.3: DROP_TABLE 恢复 — 删除 schema 并清除主 LSM 中该表的所有残留数据。
|
||||
*
|
||||
* 此前 DROP_TABLE 在恢复时被忽略,而 CREATE_TABLE 回放会重建 schema,
|
||||
* 导致崩溃后"已删除的表和数据复活"(实证 P0 bug)。
|
||||
*/
|
||||
private async applyDropTableRecovery(tableName: string): Promise<void> {
|
||||
if (!tableName) return;
|
||||
this.schemas.delete(tableName);
|
||||
this.tablePKs.delete(tableName);
|
||||
|
||||
// 清除主 LSM 中该表前缀的所有数据(含 SSTable 中的旧数据)
|
||||
const prefix = `${tableName}:`;
|
||||
const endKey = `${prefix}\uffff`;
|
||||
await this.lsm.prefetchRange(prefix, endKey);
|
||||
const entries = this.lsm.rangeScan(prefix, endKey);
|
||||
for (const [key] of entries) {
|
||||
this.lsm.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// 二级索引
|
||||
// =======================================================================
|
||||
@@ -891,7 +1212,8 @@ export class AriaEngine implements IStorageEngine {
|
||||
if (!schema) return;
|
||||
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
if (!colDef.index && !colDef.unique && !colDef.primaryKey) continue;
|
||||
// v0.3.3: 主键列不建冗余二级索引(主 LSM 即 PK 索引)
|
||||
if (!colDef.index && !colDef.unique) continue;
|
||||
const idxKey = `${tableName}:idx:${colName}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
if (!idxLsm) continue;
|
||||
@@ -947,6 +1269,32 @@ export class AriaEngine implements IStorageEngine {
|
||||
const value = this.lsm.get(key);
|
||||
return value ? [{ ...value, [pkCol]: cond.$eq }] : [];
|
||||
}
|
||||
// v0.3.3: PK $in → 主 LSM 多次精确查找(替代冗余 PK 二级索引)
|
||||
if ('$in' in cond && Array.isArray(cond.$in)) {
|
||||
const keys = cond.$in.map((v) => `${tableName}:${v}`);
|
||||
await this.lsm.prefetchKeys(keys);
|
||||
const rows: Record<string, unknown>[] = [];
|
||||
const seen = new Set<string>(); // v0.4.1: IN 子查询可能含重复值,按 pk 去重
|
||||
for (const v of cond.$in) {
|
||||
const pk = String(v);
|
||||
if (seen.has(pk)) continue;
|
||||
const value = this.lsm.get(`${tableName}:${pk}`);
|
||||
if (value) { seen.add(pk); rows.push({ ...value, [pkCol]: pk }); }
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
// v0.3.3: PK 范围查询 → 主 LSM 前缀扫描 + 条件过滤(修复字符串算术 bug)
|
||||
if ('$gt' in cond || '$gte' in cond || '$lt' in cond || '$lte' in cond) {
|
||||
const prefix = `${tableName}:`;
|
||||
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
|
||||
const entries = this.lsm.rangeScan(prefix, `${prefix}\uffff`);
|
||||
const rows: Record<string, unknown>[] = [];
|
||||
for (const [key, value] of entries) {
|
||||
const candidate = { ...value, [pkCol]: key.slice(prefix.length) };
|
||||
if (matchWhere(candidate, { [pkCol]: condition })) rows.push(candidate);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
// 二级索引查找
|
||||
@@ -966,9 +1314,16 @@ export class AriaEngine implements IStorageEngine {
|
||||
// $in → 多次精确查找
|
||||
if ('$in' in c && Array.isArray(c.$in)) {
|
||||
const results: Record<string, unknown>[] = [];
|
||||
const seenPks = new Set<string>(); // v0.4.1: IN 值可能重复,按 pk 去重
|
||||
for (const val of c.$in) {
|
||||
const rows = await this.indexScanToRows(tableName, pkCol, idxLsm, String(val), String(val));
|
||||
results.push(...rows);
|
||||
for (const row of rows) {
|
||||
const pk = String(row[pkCol]);
|
||||
if (!seenPks.has(pk)) {
|
||||
seenPks.add(pk);
|
||||
results.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
@@ -1091,7 +1446,8 @@ export class AriaEngine implements IStorageEngine {
|
||||
let rebuiltCount = 0;
|
||||
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
if (!colDef.index && !colDef.unique && !colDef.primaryKey) continue;
|
||||
// v0.3.3: 主键列不建冗余二级索引(主 LSM 即 PK 索引)
|
||||
if (!colDef.index && !colDef.unique) continue;
|
||||
const idxKey = `${tableName}:idx:${colName}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
if (!idxLsm) continue;
|
||||
|
||||
Reference in New Issue
Block a user