Files
MetonaSqlark/src/engine/memory.ts
T

880 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* metona-sqlark Memory Engine — 基于 Map 的内存存储引擎
* @module engine/memory
*/
import type { IStorageEngine } from './interface';
import type { QueryPlan, TableSchema, WhereCondition } from '../constants';
import { DatabaseError } from '../constants';
import { matchWhere, applyOrderBy, projectColumns, containsUnresolvedSubqueries } from '../query/where-matcher';
import { stripUndefinedUpdates } from '../table/schema';
export class MemoryEngine implements IStorageEngine {
readonly name = 'memory';
private tables: Map<string, Map<string, Record<string, unknown>>> = new Map();
private schemas: Map<string, TableSchema> = new Map();
private indexes: Map<string, Map<string, Map<unknown, Set<string>>>> = new Map();
private opened = false;
/** v0.4.2-fix: 库内元数据(迁移版本持久化用) */
private metaStore: Map<string, string> = new Map();
/**
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且不可恢复)。
*/
private uniqueIndexCols: Set<string> = new Set();
// ---- 事务快照 ----
private snapshot: {
tables: Map<string, Map<string, Record<string, unknown>>>;
schemas: Map<string, TableSchema>;
indexes: Map<string, Map<string, Map<unknown, Set<string>>>>;
} | null = null;
// ---- 生命周期 ----
async open(_dbName: string, _version: number): Promise<void> {
if (this.opened) {
// 幂等:已打开则忽略
return;
}
this.opened = true;
}
async close(): Promise<void> {
this.tables.clear(); this.schemas.clear(); this.indexes.clear(); this.metaStore.clear(); this.opened = false;
}
isOpen(): boolean { return this.opened; }
// ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ----
/** 内存引擎无需修复(无持久化损坏概念) */
async repair(): Promise<void> { return; }
/** 清空全部数据与表结构 */
async clearAll(): Promise<void> {
const names = Array.from(this.schemas.keys());
for (const name of names) {
await this.dropTable(name);
}
this.metaStore.clear();
}
async getMeta(key: string): Promise<string | null> {
return this.metaStore.get(key) ?? null;
}
async setMeta(key: string, value: string): Promise<void> {
this.metaStore.set(key, value);
}
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
if (this.schemas.has(schema.name)) throw new DatabaseError(`Table "${schema.name}" already exists`, 'TABLE_EXISTS');
// v0.4.2-fix: 存储 schema 深拷贝 — 此前 Hybrid.reloadMemoryFromDisk 直接存入
// disk 引擎的 schema 引用,内存/磁盘引擎共享同一对象,任一引擎 ALTER 都会污染对方
const copy: TableSchema = { name: schema.name, columns: {} };
for (const [colName, colDef] of Object.entries(schema.columns)) {
copy.columns[colName] = { ...colDef };
}
this.schemas.set(schema.name, copy);
this.tables.set(schema.name, new Map());
const tableIndexes = new Map<string, Map<unknown, Set<string>>>();
for (const [colName, colDef] of Object.entries(copy.columns)) {
if (colDef.index || colDef.unique) tableIndexes.set(colName, new Map());
}
this.indexes.set(schema.name, tableIndexes);
}
async dropTable(tableName: string): Promise<void> {
this.ensureTable(tableName);
this.schemas.delete(tableName); this.tables.delete(tableName); this.indexes.delete(tableName);
// v0.7.4: 清理该表的 unique 索引来源标记(重建同名表不残留)
const prefix = `${tableName}:`;
for (const key of this.uniqueIndexCols) {
if (key.startsWith(prefix)) this.uniqueIndexCols.delete(key);
}
}
async hasTable(tableName: string): Promise<boolean> { return this.schemas.has(tableName); }
async getTableNames(): Promise<string[]> { return Array.from(this.schemas.keys()); }
async getTableSchema(tableName: string): Promise<TableSchema | null> { return this.schemas.get(tableName) ?? null; }
/**
* v0.4.2-fix: 引擎级 ALTER TABLE — 直接修改内存 schema 引用并清理行数据。
* (此前走 executor 通用路径,行为相同;统一到引擎层保证 Hybrid/IndexedDB 委托一致性)
*/
async alterTable(
tableName: string,
action: 'ADD' | 'DROP',
column: import('../constants').ColumnDef & { name: string },
): Promise<void> {
// v0.7.2: 事务内 DDL 显式拒绝(与 AriaEngine 对齐)。此前事务快照对 schema
// 是浅拷贝,alterTable 直接修改共享 columns 对象 → ROLLBACK 后结构变更残留
// (三引擎行为不一致:Aria 拒绝 / Memory、KVStore 静默残留)
if (this.snapshot) {
throw new DatabaseError(
`ALTER TABLE is not supported inside a transaction (MemoryEngine DDL is not transactional)`,
'NOT_SUPPORTED',
);
}
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;
return;
}
if (!schema.columns[column.name]) {
throw new DatabaseError(`Column "${column.name}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
}
// v0.7.3: 被删列是索引列 → 同步清理索引 Map —— 此前残留旧索引:
// 查询已删列仍走旧索引(不含新行)→ 结果不完整(对齐 AriaEngine cleanupTableIndexes
if (schema.columns[column.name].index || schema.columns[column.name].unique) {
this.indexes.get(tableName)?.delete(column.name);
}
delete schema.columns[column.name];
// 清理已有行中该列的值(find 返回行引用,直接删除生效)
const table = this.tables.get(tableName)!;
for (const row of table.values()) {
if (column.name in row) delete row[column.name];
}
}
// ---- CRUD ----
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const table = this.tables.get(tableName)!;
const pkColumn = this.getPrimaryKey(schema);
const pks: string[] = [];
// v0.7.3: 语句级原子性 —— 两阶段(先全量预检,后执行)。
// 此前逐行"校验+写入":第 N 行主键重复/唯一冲突抛错时,前 N-1 行已提交
// (无事务下语句级部分提交,与 v0.7.2 修复的 UPDATE 同类问题)。
const validated: Record<string, unknown>[] = [];
const pkSet = new Set<string>();
const batchUnique: Map<string, Set<unknown>> = new Map();
// 阶段 1:全量预检(任何一行失败 → 整条语句不执行)
for (const row of rows) {
const validatedRow = this.validateRow(schema, row);
const pkValue = String(validatedRow[pkColumn]);
// 批内主键互查(内存表尚未反映本批写入)
if (table.has(pkValue) || pkSet.has(pkValue)) {
throw new DatabaseError(`Duplicate primary key "${pkValue}" in table "${tableName}"`, 'DUPLICATE_KEY');
}
pkSet.add(pkValue);
// v0.7.3: 批内唯一互查 + 索引查(此前两行同批写入同一唯一值时,
// 第一行已写入索引 → 第二行 checkUniqueness 抛错 → 第一行残留)
this.checkInsertUniqueness(schema, tableName, validatedRow, batchUnique);
validated.push(validatedRow);
}
// 阶段 2:执行(预检已通过,此阶段不再抛校验类错误)
for (const validatedRow of validated) {
const pkValue = String(validatedRow[pkColumn]);
table.set(pkValue, validatedRow);
this.updateIndexes(tableName, validatedRow, pkValue);
pks.push(pkValue);
}
return pks;
}
/** v0.7.3: 按主键取已验证行(KVStoreEngine 持久化 validated 行用,含 default/类型归一) */
getRow(tableName: string, pkValue: string): Record<string, unknown> | null {
const table = this.tables.get(tableName);
if (!table) return null;
return table.get(pkValue) ?? null;
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
this.ensureTable(tableName);
const table = this.tables.get(tableName)!;
let results = this.tryIndexLookup(tableName, table, query);
if (query.where && Object.keys(query.where).length > 0) {
results = results.filter((row) => matchWhere(row, query.where!));
}
if (query.orderBy && query.orderBy.length > 0) {
results = applyOrderBy(results, query.orderBy);
}
const offset = query.offset ?? 0;
const limit = query.limit ?? results.length;
results = results.slice(offset, offset + limit);
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
results = results.map((row) => projectColumns(row, query.columns!));
}
return results;
}
/** v0.4.0: 流式查询 — 逐行回调(单次迭代,不物化结果数组) */
async findStream(tableName: string, query: QueryPlan, onRow: (row: Record<string, unknown>) => void): Promise<number> {
this.ensureTable(tableName);
const table = this.tables.get(tableName)!;
const hasWhere = !!(query.where && Object.keys(query.where).length > 0);
const limit = query.limit ?? Infinity;
const offset = query.offset ?? 0;
const project = query.columns && query.columns.length > 0 && query.columns[0] !== '*'
? (row: Record<string, unknown>) => projectColumns(row, query.columns!)
: null;
let count = 0;
let skipped = 0;
for (const row of table.values()) {
if (hasWhere && !matchWhere(row, query.where!)) continue;
if (skipped < offset) { skipped++; continue; }
onRow(project ? project(row) : row);
count++;
if (count >= limit) break;
}
return count;
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const table = this.tables.get(tableName)!;
const pkCol = this.getPrimaryKey(schema);
// v0.7.2: undefined 值视为"不更新该列"(保留旧值),null 显式置空
const cleanUpdates = stripUndefinedUpdates(updates);
// 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',
);
}
// v0.7.4: 未知列显式报错 —— 此前 SET nonexistent = ... 被静默写入存储行
// validateRow 只遍历 schema 列,脏列残留在行内并随持久化落盘)
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.7.2: 语句级原子性 — 两阶段(先全量预检,后执行)。
// 此前逐行"校验+写入":第 N 行唯一冲突/校验失败抛错时,前 N-1 行已写入
// → 无事务下语句级部分提交(数据半更新且调用方已收到错误)。
const planned: { pk: string; row: Record<string, unknown>; updated: Record<string, unknown>; newPk: string }[] = [];
const batchUnique: Map<string, Set<unknown>> = new Map();
// 阶段 1:全量预检(任何一行失败 → 整条语句不执行)
for (const [pk, row] of table) {
if (query.where && Object.keys(query.where).length > 0 && !matchWhere(row, query.where)) continue;
const updated = { ...row, ...cleanUpdates };
this.validateRow(schema, updated);
this.checkUpdateUniqueness(schema, tableName, pk, updated, batchUnique);
const newPk = String(updated[pkCol]);
// v0.6.2-fix(P0): 主键变更撞已有主键 → 抛 DUPLICATE_KEY(此前静默覆盖丢数据)
if (newPk !== pk && table.has(newPk)) {
throw new DatabaseError(
`Duplicate primary key "${newPk}" in table "${tableName}" (cannot update key to existing value)`,
'DUPLICATE_KEY',
);
}
planned.push({ pk, row, updated, newPk });
}
// 阶段 1b:主键变更 RESTRICT 预检(引用表依赖行检查,任何修改前)
for (const p of planned) {
if (p.newPk !== p.pk) this.checkUpdateRestrict(tableName, p.pk);
}
// 阶段 2:执行(预检已通过,此阶段不再抛校验类错误)
let count = 0;
for (const { pk, row, updated, newPk } of planned) {
// v0.3.3: 先移除旧值索引条目(修复 update 后唯一约束被绕过、按新值查索引丢行)
this.removeIndexEntries(tableName, row, pk);
// v0.4.2-fix: 主键变更 — 删除旧键 + 级联更新引用表 + 新键落表
if (newPk !== pk) {
await this.applyUpdateCascade(tableName, pk, newPk);
}
table.delete(pk);
table.set(newPk, updated);
this.updateIndexes(tableName, updated, newPk);
count++;
}
return count;
}
/**
* v0.7.3: 插入唯一性预检 —— 批内互查(本批前几行写入同一唯一值)
* + 索引查(表中已有行)。与 update 的 checkUpdateUniqueness 对称,
* 两阶段 insert 预检阶段调用(索引尚未反映本批写入)。
*/
private checkInsertUniqueness(
schema: TableSchema,
tableName: string,
row: Record<string, unknown>,
batchUnique: Map<string, Set<unknown>>,
): void {
const tableIndexes = this.indexes.get(tableName);
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (!colDef.unique) continue;
const value = row[colName];
if (value === undefined || value === null) continue;
let seen = batchUnique.get(colName);
if (!seen) {
seen = new Set<unknown>();
batchUnique.set(colName, seen);
}
if (seen.has(value)) {
throw new DatabaseError(
`Unique constraint violation on column "${colName}" in table "${schema.name}"`,
'UNIQUE_VIOLATION',
);
}
seen.add(value);
if (!tableIndexes) continue;
const colIndex = tableIndexes.get(colName);
if (colIndex && colIndex.has(value)) {
throw new DatabaseError(
`Unique constraint violation on column "${colName}" in table "${schema.name}"`,
'UNIQUE_VIOLATION',
);
}
}
}
/**
* v0.7.3: 更新唯一性预检 — 批内互查(多条行更新到同一唯一值)+ 索引查
* (排除自身旧条目)。阶段 1 中索引尚未更新,批内互查避免"两行同时改到
* 同一新值"绕过唯一约束。
*/
private checkUpdateUniqueness(
schema: TableSchema,
tableName: string,
pk: string,
updated: Record<string, unknown>,
batchUnique: Map<string, Set<unknown>>,
): void {
const tableIndexes = this.indexes.get(tableName);
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (!colDef.unique) continue;
const value = updated[colName];
if (value === undefined || value === null) continue;
let seen = batchUnique.get(colName);
if (!seen) {
seen = new Set<unknown>();
batchUnique.set(colName, seen);
}
if (seen.has(value)) {
throw new DatabaseError(
`Unique constraint violation on column "${colName}" in table "${schema.name}"`,
'UNIQUE_VIOLATION',
);
}
seen.add(value);
if (!tableIndexes) continue;
const colIndex = tableIndexes.get(colName);
if (colIndex && colIndex.has(value)) {
const pks = colIndex.get(value)!;
// 值未变(新值 = 旧值)且索引中只有自身 → 允许
if (!(pks.size === 1 && pks.has(pk))) {
throw new DatabaseError(
`Unique constraint violation on column "${colName}" in table "${schema.name}"`,
'UNIQUE_VIOLATION',
);
}
}
}
}
/**
* v0.7.2: ON UPDATE RESTRICT 预检 — 从 applyUpdateCascade 提取,
* 两阶段 update 在任何修改前调用(整体拒绝语义)。
*/
private checkUpdateRestrict(tableName: string, oldPk: string): void {
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName) continue;
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onUpdate) continue;
const [refTable] = colDef.references.split('.');
if (refTable !== tableName) continue;
const refTableData = this.tables.get(refTableName);
if (!refTableData) continue;
let hasDependents = false;
for (const [, refRow] of refTableData) {
if (String(refRow[colName]) !== oldPk) continue;
hasDependents = true;
if (colDef.onUpdate === 'RESTRICT') {
throw new DatabaseError(
`Cannot update "${tableName}" key "${oldPk}": foreign key "${colName}" in "${refTableName}" has dependent rows`,
'FOREIGN_KEY_VIOLATION',
);
}
}
// v0.7.2: SET NULL 到 required 列违反约束 —— 与 RESTRICT 同样整体拒绝
// (此前级联直写 null 绕过 validateRowrequired 列被静默置空)
if (hasDependents && colDef.onUpdate === 'SET NULL' && colDef.required) {
throw new DatabaseError(
`Cannot update "${tableName}" key "${oldPk}": foreign key "${colName}" in "${refTableName}" is required (SET NULL violates constraint)`,
'FOREIGN_KEY_VIOLATION',
);
}
}
}
}
/**
* v0.4.2-fix: ON UPDATE 外键级联 — 被引用表主键变更时处理引用表:
* RESTRICT 抛错 / CASCADE 更新 FK 值 / SET NULL 置空。
* v0.7.3-perf: 删除冗余的阶段 1 RESTRICT 扫描 —— checkUpdateRestrict 已在
* 两阶段 update 预检(阶段 1b)覆盖 RESTRICT 与 SET NULL+required
* 此处任何修改前重复全表扫描纯属浪费。直接执行 CASCADE / SET NULL。
*/
private async applyUpdateCascade(tableName: string, oldPk: string, newPk: string): Promise<void> {
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName) continue;
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onUpdate) continue;
const [refTable] = colDef.references.split('.');
if (refTable !== tableName) continue;
const refTableData = this.tables.get(refTableName);
if (!refTableData) continue;
if (colDef.onUpdate !== 'CASCADE' && colDef.onUpdate !== 'SET NULL') continue;
for (const [refPk, refRow] of refTableData) {
if (String(refRow[colName]) !== oldPk) continue;
this.removeIndexEntries(refTableName, refRow, refPk);
refRow[colName] = colDef.onUpdate === 'CASCADE' ? newPk : null;
this.updateIndexes(refTableName, refRow, refPk);
}
}
}
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
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 table = this.tables.get(tableName)!;
const toDelete: { pk: string; row: Record<string, unknown> }[] = [];
for (const [pk, row] of table) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
toDelete.push({ pk, row });
}
}
// v0.6.3-fix: 级联两阶段 —— 先对全部待删行做 RESTRICT 预检(沿 CASCADE 链递归),
// 任何一行违规则整体拒绝。此前逐行执行:第 N 行 RESTRICT 抛错时,前 N-1 行的
// 级联子行已被删除、父行未删 → 无事务下部分级联(数据不一致)
//
// v0.7.3-fix: 索引清理移到预检之后 —— 此前 removeIndexEntries 在收集阶段执行,
// RESTRICT 预检抛错时行未删但索引条目已删 → 唯一约束失效、索引查询丢行
const restrictVisited = new Set<string>();
for (const { pk } of toDelete) {
this.checkCascadeRestrict(tableName, pk, restrictVisited);
}
// 预检通过:清理索引 + 级联删除(此阶段不再抛校验类错误)
let cascadeCount = 0;
for (const { pk, row } of toDelete) {
// v0.3.3: 删除行前清理其索引条目(修复删除后索引残留)
this.removeIndexEntries(tableName, row, pk);
cascadeCount += await this.cascadeDelete(tableName, pk, row);
}
for (const { pk } of toDelete) table.delete(pk);
return toDelete.length + cascadeCount;
}
/**
* v0.6.3: RESTRICT 预检(delete 级联两阶段之一)。
* 递归沿 CASCADE 链检查引用表:RESTRICT 引用存在依赖行则抛 FOREIGN_KEY_VIOLATION。
*/
private checkCascadeRestrict(tableName: string, pkValue: string, visited: Set<string>): void {
const visitKey = `${tableName}:${pkValue}`;
if (visited.has(visitKey)) return;
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 refTableData = this.tables.get(refTableName);
if (!refTableData) continue;
const refPks: string[] = [];
for (const [refPk, refRow] of refTableData) {
if (String(refRow[colName]) === pkValue) refPks.push(refPk);
}
if (colDef.onDelete === 'RESTRICT' && refPks.length > 0) {
throw new DatabaseError(
`Cannot delete from "${tableName}": foreign key "${colName}" in "${refTableName}" has dependent rows`,
'FOREIGN_KEY_VIOLATION',
);
}
// v0.7.2: SET NULL 到 required 列违反约束 —— 预检阶段整体拒绝
if (colDef.onDelete === 'SET NULL' && colDef.required && refPks.length > 0) {
throw new DatabaseError(
`Cannot delete from "${tableName}": foreign key "${colName}" in "${refTableName}" is required (SET NULL violates constraint)`,
'FOREIGN_KEY_VIOLATION',
);
}
if (colDef.onDelete === 'CASCADE') {
for (const refPk of refPks) {
this.checkCascadeRestrict(refTableName, refPk, visited);
}
}
}
}
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
this.ensureTable(tableName);
const table = this.tables.get(tableName)!;
if (!query?.where || Object.keys(query.where).length === 0) return table.size;
let result = 0;
for (const row of table.values()) { if (matchWhere(row, query.where)) result++; }
return result;
}
async clear(tableName: string): Promise<void> {
this.ensureTable(tableName);
this.tables.get(tableName)!.clear();
const tableIndexes = this.indexes.get(tableName);
if (tableIndexes) for (const colIndex of tableIndexes.values()) colIndex.clear();
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
// v0.7.2: 事务内修改列级标志(colDef.index/unique)会写入共享列对象,
// 事务快照无法回滚 → 与 alterTable 同样显式拒绝
if (this.snapshot) {
throw new DatabaseError(
`CREATE INDEX is not supported inside a transaction (MemoryEngine DDL is not transactional)`,
'NOT_SUPPORTED',
);
}
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const colDef = schema.columns[column];
if (!colDef) throw new DatabaseError(`Column "${column}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
if (colDef.index || colDef.unique) return; // 已存在
const tableIndexes = this.indexes.get(tableName)!;
if (!tableIndexes.has(column)) tableIndexes.set(column, new Map());
const colIndex = tableIndexes.get(column)!;
const table = this.tables.get(tableName)!;
try {
for (const [pk, row] of table) {
const value = row[column];
if (value !== undefined && value !== null) {
// v0.7.3: UNIQUE 索引回填校验存量唯一性 —— 此前重复数据静默建索引
// (SQLite 语义应报错),且此后该列唯一约束永远无法满足
if (unique && colIndex.has(value)) {
throw new DatabaseError(
`Unique index on column "${column}" in table "${tableName}" cannot be created: duplicate value "${String(value)}"`,
'UNIQUE_VIOLATION',
);
}
if (!colIndex.has(value)) colIndex.set(value, new Set());
colIndex.get(value)!.add(pk);
}
}
} catch (error) {
// 回填失败(唯一冲突):清理半初始化索引,标志未落,保持原子语义
tableIndexes.delete(column);
throw error;
}
colDef.index = true;
if (unique) {
colDef.unique = true;
// v0.7.4: 记录唯一约束来源(DROP INDEX 时可解除;建表约束不可)
this.uniqueIndexCols.add(`${tableName}:${column}`);
}
}
async dropIndex(tableName: string, column: string, _indexName?: string): Promise<void> {
// v0.7.2: 同 createIndex —— 列级标志修改无法通过事务快照回滚,显式拒绝
if (this.snapshot) {
throw new DatabaseError(
`DROP INDEX is not supported inside a transaction (MemoryEngine DDL is not transactional)`,
'NOT_SUPPORTED',
);
}
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const colDef = schema.columns[column];
if (!colDef) throw new DatabaseError(`Column "${column}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
// v0.4.1: DROP 不存在的索引应报错(此前静默成功)
if (!colDef.index && !colDef.unique) {
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
// 静默解除约束(后续唯一性检查失效、重复数据入库)。对齐 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 tableIndexes = this.indexes.get(tableName);
if (tableIndexes) tableIndexes.delete(column);
}
// ---- 事务 ----
async beginTransaction(): Promise<void> {
if (this.snapshot) throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
this.snapshot = {
tables: this.deepCloneMapMap(this.tables),
schemas: new Map(this.schemas),
indexes: this.deepCloneIndexes(this.indexes),
};
}
async commitTransaction(): Promise<void> {
if (!this.snapshot) throw new DatabaseError('No active transaction', 'TX_NONE');
this.snapshot = null;
}
async rollbackTransaction(): Promise<void> {
if (!this.snapshot) throw new DatabaseError('No active transaction', 'TX_NONE');
this.tables = this.snapshot.tables;
this.schemas = this.snapshot.schemas;
this.indexes = this.snapshot.indexes;
this.snapshot = null;
}
// ---- 事务快照辅助 ----
private deepCloneMapMap(source: Map<string, Map<string, Record<string, unknown>>>): Map<string, Map<string, Record<string, unknown>>> {
const clone = new Map<string, Map<string, Record<string, unknown>>>();
for (const [k, v] of source) {
const innerClone = new Map<string, Record<string, unknown>>();
for (const [ik, iv] of v) innerClone.set(ik, { ...iv });
clone.set(k, innerClone);
}
return clone;
}
private deepCloneIndexes(source: Map<string, Map<string, Map<unknown, Set<string>>>>): Map<string, Map<string, Map<unknown, Set<string>>>> {
const clone = new Map<string, Map<string, Map<unknown, Set<string>>>>();
for (const [tableName, tableIndexes] of source) {
const tableClone = new Map<string, Map<unknown, Set<string>>>();
for (const [col, colIndex] of tableIndexes) {
const colClone = new Map<unknown, Set<string>>();
for (const [val, pkSet] of colIndex) colClone.set(val, new Set(pkSet));
tableClone.set(col, colClone);
}
clone.set(tableName, tableClone);
}
return clone;
}
// ---- 内部辅助 ----
private ensureTable(tableName: string): void {
if (!this.tables.has(tableName)) throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
}
private getPrimaryKey(schema: TableSchema): string {
for (const [name, col] of Object.entries(schema.columns)) { if (col.primaryKey) return name; }
return Object.keys(schema.columns)[0];
}
private validateRow(schema: TableSchema, row: Record<string, unknown>): Record<string, unknown> {
const validated: Record<string, unknown> = {};
for (const [colName, colDef] of Object.entries(schema.columns)) {
let value = row[colName];
if (value === undefined && colDef.default !== undefined) value = colDef.default;
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);
if (value !== undefined) validated[colName] = value;
}
return validated;
}
private checkType(colName: string, type: string, value: unknown): void {
const jsType = typeof value;
switch (type) {
case 'string': if (jsType !== 'string') throw new DatabaseError(`Column "${colName}" expects string, got ${jsType}`, 'TYPE_ERROR'); break;
case 'number': if (jsType !== 'number') throw new DatabaseError(`Column "${colName}" expects number, got ${jsType}`, 'TYPE_ERROR'); break;
case 'boolean': if (jsType !== 'boolean') throw new DatabaseError(`Column "${colName}" expects boolean, got ${jsType}`, 'TYPE_ERROR'); break;
case 'date': if (jsType !== 'string' || isNaN(Date.parse(value as string))) throw new DatabaseError(`Column "${colName}" expects valid date`, 'TYPE_ERROR'); break;
case 'json': if (jsType !== 'object') throw new DatabaseError(`Column "${colName}" expects object/array, got ${jsType}`, 'TYPE_ERROR'); break;
}
}
/** 索引查找 */
private tryIndexLookup(
tableName: string, table: Map<string, Record<string, unknown>>, query: QueryPlan,
): Record<string, unknown>[] {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes || !query.where) return Array.from(table.values());
// v0.7.3: 递归展开 $and 中的等值条件 —— 此前仅顶层键,
// `WHERE a AND b`(解析为顶层 $and)永远全表扫描,索引形同虚设。
// $or/$not 语义不适用单索引下推,保守跳过。命中索引后 find 仍以
// 全条件 matchWhere 过滤(子集语义安全)。
const flat: [string, unknown][] = [];
const collect = (w: WhereCondition): void => {
for (const [k, v] of Object.entries(w)) {
if (k === '$and') {
for (const sub of (v as WhereCondition[])) collect(sub);
continue;
}
if (k === '$or' || k === '$not') continue;
flat.push([k, v]);
}
};
collect(query.where);
for (const [col, condition] of flat) {
// v0.4.1: 支持 { $eq: value } 形式(SQL 解析器生成的等值条件)走索引
let targetValue: unknown;
if (typeof condition !== 'object' || condition === null) {
targetValue = condition;
} else if ('$eq' in (condition as Record<string, unknown>) && Object.keys(condition as Record<string, unknown>).length === 1) {
targetValue = (condition as Record<string, unknown>).$eq;
} else {
continue;
}
// v0.7.3: null/undefined 条件不走索引 —— 索引不含 null 条目,
// colIndex.get(null) 恒 undefined → return [] 短路全表扫描 → 索引列
// IS NULL 恒空(对齐 AriaEngine v0.6.2 修复)
if (targetValue === null || targetValue === undefined) continue;
const colIndex = tableIndexes.get(col);
if (colIndex) {
const pks = colIndex.get(targetValue);
if (pks) {
const result: Record<string, unknown>[] = [];
for (const pk of pks) { const r = table.get(pk); if (r) result.push(r); }
return result;
}
return [];
}
}
return Array.from(table.values());
}
/** 更新索引 */
private updateIndexes(tableName: string, row: Record<string, unknown>, pk: string): void {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes) return;
for (const [colName, colIndex] of tableIndexes) {
const value = row[colName];
if (value !== undefined && value !== null) {
if (!colIndex.has(value)) colIndex.set(value, new Set());
colIndex.get(value)!.add(pk);
}
}
}
/** v0.3.3: 从所有索引中移除一行的条目(update/delete 前调用,修复索引过期/残留) */
private removeIndexEntries(tableName: string, row: Record<string, unknown>, pk: string): void {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes) return;
for (const [colName, colIndex] of tableIndexes) {
const value = row[colName];
if (value !== undefined && value !== null) {
const pks = colIndex.get(value);
if (pks) {
pks.delete(pk);
if (pks.size === 0) colIndex.delete(value);
}
}
}
}
// ---- 外键级联 ----
/**
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
* v0.6.1-fix: 环路保护(A→B→A 级联环不再无限递归栈溢出,AriaEngine 同语义)。
* @returns 级联删除的行数
*/
private async cascadeDelete(
tableName: string,
pkValue: string,
_row: Record<string, unknown>,
visited: Set<string> = new Set(),
): Promise<number> {
const visitKey = `${tableName}:${pkValue}`;
if (visited.has(visitKey)) return 0;
visited.add(visitKey);
let totalCascade = 0;
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 refTableData = this.tables.get(refTableName);
if (!refTableData) continue;
// 查找所有引用此主键的行
const toDelete: string[] = [];
for (const [refPk, refRow] of refTableData) {
if (String(refRow[colName]) === pkValue) {
toDelete.push(refPk);
}
}
// RESTRICT: 存在引用行时禁止删除
if (colDef.onDelete === 'RESTRICT' && toDelete.length > 0) {
throw new DatabaseError(
`Cannot delete from "${tableName}": foreign key "${colName}" in "${refTableName}" has dependent rows`,
'FOREIGN_KEY_VIOLATION',
);
}
if (colDef.onDelete === 'CASCADE') {
// 递归级联
for (const refPk of toDelete) {
const refRow = refTableData.get(refPk);
if (refRow) {
// v0.3.3: 级联删除前清理索引条目
this.removeIndexEntries(refTableName, refRow, refPk);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow, visited);
}
refTableData.delete(refPk);
totalCascade++;
}
} else if (colDef.onDelete === 'SET NULL') {
for (const refPk of toDelete) {
const refRow = refTableData.get(refPk);
if (refRow) {
// v0.6.3-fix: 复用 removeIndexEntries 清理旧值索引 —— 此前手动
// `pks.get(v)?.delete(pk)` 后遗留空 Set → checkUniqueness 对旧值
// 永久误报 UNIQUE_VIOLATION(外键列带 unique 约束时)
this.removeIndexEntries(refTableName, refRow, refPk);
refRow[colName] = null;
this.updateIndexes(refTableName, refRow, refPk);
}
}
}
}
}
return totalCascade;
}
}