/** * 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>> = new Map(); private schemas: Map = new Map(); private indexes: Map>>> = new Map(); private opened = false; /** v0.4.2-fix: 库内元数据(迁移版本持久化用) */ private metaStore: Map = new Map(); /** * v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。 * 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique, * 建表约束需重建表(对齐 SQLite 语义,此前静默解除且不可恢复)。 */ private uniqueIndexCols: Set = new Set(); // ---- 事务快照 ---- private snapshot: { tables: Map>>; schemas: Map; indexes: Map>>>; } | null = null; // ---- 生命周期 ---- async open(_dbName: string, _version: number): Promise { if (this.opened) { // 幂等:已打开则忽略 return; } this.opened = true; } async close(): Promise { 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 { return; } /** 清空全部数据与表结构 */ async clearAll(): Promise { const names = Array.from(this.schemas.keys()); for (const name of names) { await this.dropTable(name); } this.metaStore.clear(); } async getMeta(key: string): Promise { return this.metaStore.get(key) ?? null; } async setMeta(key: string, value: string): Promise { this.metaStore.set(key, value); } // ---- 表管理 ---- async createTable(schema: TableSchema): Promise { 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>>(); 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 { 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 { return this.schemas.has(tableName); } async getTableNames(): Promise { return Array.from(this.schemas.keys()); } async getTableSchema(tableName: string): Promise { 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 { // 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[]): Promise { 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[] = []; const pkSet = new Set(); const batchUnique: Map> = 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 | null { const table = this.tables.get(tableName); if (!table) return null; return table.get(pkValue) ?? null; } async find(tableName: string, query: QueryPlan): Promise[]> { 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) => void): Promise { 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) => 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): Promise { 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; updated: Record; newPk: string }[] = []; const batchUnique: Map> = 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, batchUnique: Map>, ): 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(); 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, batchUnique: Map>, ): 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(); 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 绕过 validateRow,required 列被静默置空) 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 { 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 { 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 }[] = []; 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(); 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): 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 { 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 { 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 { // 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 { // 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 { 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 { if (!this.snapshot) throw new DatabaseError('No active transaction', 'TX_NONE'); this.snapshot = null; } async rollbackTransaction(): Promise { 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>>): Map>> { const clone = new Map>>(); for (const [k, v] of source) { const innerClone = new Map>(); for (const [ik, iv] of v) innerClone.set(ik, { ...iv }); clone.set(k, innerClone); } return clone; } private deepCloneIndexes(source: Map>>>): Map>>> { const clone = new Map>>>(); for (const [tableName, tableIndexes] of source) { const tableClone = new Map>>(); for (const [col, colIndex] of tableIndexes) { const colClone = new Map>(); 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): Record { const validated: Record = {}; 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>, query: QueryPlan, ): Record[] { 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) && Object.keys(condition as Record).length === 1) { targetValue = (condition as Record).$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[] = []; 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, 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, 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, visited: Set = new Set(), ): Promise { 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; } }