666 lines
24 KiB
TypeScript
666 lines
24 KiB
TypeScript
/**
|
||
* KVStoreEngine — 基于自研 KVStore 的磁盘存储引擎(替代 IndexedDBEngine / OPFSEngine)
|
||
* @module engine/kvstore_engine
|
||
*
|
||
* v0.6.0: 完全移除 IndexedDB 后的 disk 模式引擎。
|
||
*
|
||
* 架构:MemoryEngine(内存热路径 + 事务快照)+ KVStore(持久化 + 原子写)
|
||
* - 读:始终走内存(写路径同步落盘,重启从 KVStore 恢复)
|
||
* - 写:内存先行 + KVStore 增量持久化(insert 增量 putMany;update/delete 受影响行重写;
|
||
* 主键变更/级联场景整表 diff;全部原子)
|
||
* - 事务:内存快照 + commit 时受影响表原子 flush(writeBatch 单记录 = 真原子,
|
||
* 此前 IndexedDBEngine 依赖 IDB 事务,现在完全自研)
|
||
*
|
||
* 数据布局(KVStore keys):
|
||
* `__schema` — JSON { tableName: TableSchema }
|
||
* `__meta:{key}` — 库内元数据(迁移版本等)
|
||
* `t:{table}:{pk}` — 行数据(JSON)
|
||
*/
|
||
|
||
import type { IStorageEngine } from './interface';
|
||
import type { QueryPlan, TableSchema } from '../constants';
|
||
import { DatabaseError } from '../constants';
|
||
import { MemoryEngine } from './memory';
|
||
import { KVStore } from './kvstore/index';
|
||
import type { IStorageBackend } from './aria/store/backend';
|
||
import { stripUndefinedUpdates } from '../table/schema';
|
||
|
||
const SCHEMA_KEY = '__schema';
|
||
const ROW_PREFIX = 't:';
|
||
|
||
const enc = (s: string) => new TextEncoder().encode(s).buffer as ArrayBuffer;
|
||
const dec = (b: ArrayBuffer) => new TextDecoder().decode(b);
|
||
|
||
export class KVStoreEngine implements IStorageEngine {
|
||
readonly name = 'kv';
|
||
|
||
private kv!: KVStore;
|
||
private memory: MemoryEngine = new MemoryEngine();
|
||
private dbName = '';
|
||
private version = 1;
|
||
private opened = false;
|
||
|
||
/** 活跃事务标记 */
|
||
private txActive = false;
|
||
/** 事务中写过的表(commit 时只 flush 这些表) */
|
||
private txDirtyTables: Set<string> = new Set();
|
||
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
|
||
private txSchemaChanged = false;
|
||
/**
|
||
* v0.7.0: 事务行级变更记录(table → pk → put/delete)。
|
||
* commit 时按行增量 flush(此前整表 diff:大表事务改 1 行也重写全表)。
|
||
*/
|
||
private txChanges: Map<string, Map<string, 'put' | 'delete'>> = new Map();
|
||
/** v0.7.0: 无法行级追踪的表(主键变更/级联影响表)→ commit 时整表 diff */
|
||
private txFullTables: Set<string> = new Set();
|
||
/** v0.7.0: 事务内 clear 的表 → commit 时清空 KV 行 */
|
||
private txClearedTables: Set<string> = new Set();
|
||
|
||
constructor(medium?: IStorageBackend, checkpointThreshold?: number) {
|
||
this.kv = new KVStore(medium, checkpointThreshold);
|
||
}
|
||
|
||
// ---- 行 key 编解码 ----
|
||
|
||
private rowKey(table: string, pk: string): string {
|
||
return `${ROW_PREFIX}${table}:${pk}`;
|
||
}
|
||
|
||
private rowPrefix(table: string): string {
|
||
return `${ROW_PREFIX}${table}:`;
|
||
}
|
||
|
||
// ---- 生命周期 ----
|
||
|
||
async open(dbName: string, version: number): Promise<void> {
|
||
if (this.opened) return;
|
||
this.dbName = dbName;
|
||
this.version = version;
|
||
await this.kv.open(dbName);
|
||
await this.memory.open(dbName, version);
|
||
|
||
// 恢复 schema
|
||
const schemaRaw = await this.kv.get(SCHEMA_KEY);
|
||
if (schemaRaw) {
|
||
try {
|
||
const schemas = JSON.parse(dec(schemaRaw)) as Record<string, TableSchema>;
|
||
for (const schema of Object.values(schemas)) {
|
||
await this.memory.createTable(schema);
|
||
}
|
||
} catch {
|
||
throw new DatabaseError('Corrupted schema in KVStore', 'KV_SCHEMA_ERROR');
|
||
}
|
||
}
|
||
|
||
// 恢复行数据 + 重建索引
|
||
const all = await this.kv.getAll();
|
||
for (const [key, value] of all) {
|
||
if (!key.startsWith(ROW_PREFIX)) continue;
|
||
const sep = key.indexOf(':', ROW_PREFIX.length);
|
||
if (sep < 0) continue;
|
||
const table = key.slice(ROW_PREFIX.length, sep);
|
||
if (!(await this.memory.hasTable(table))) continue;
|
||
try {
|
||
const row = JSON.parse(dec(value));
|
||
await this.memory.insert(table, [row]);
|
||
} catch {
|
||
// 单行损坏跳过(repair 可清理)
|
||
}
|
||
}
|
||
// 重建二级索引(schema 标记的索引列)
|
||
const tables = await this.memory.getTableNames();
|
||
for (const table of tables) {
|
||
const schema = await this.memory.getTableSchema(table);
|
||
if (!schema) continue;
|
||
for (const [col, colDef] of Object.entries(schema.columns)) {
|
||
if (colDef.index || colDef.unique) {
|
||
await this.memory.createIndex(table, col, colDef.unique);
|
||
}
|
||
}
|
||
}
|
||
|
||
this.opened = true;
|
||
}
|
||
|
||
async close(): Promise<void> {
|
||
if (!this.opened) return;
|
||
// 活跃事务先回滚
|
||
if (this.txActive) {
|
||
try { await this.rollbackTransaction(); } catch { /* ignore */ }
|
||
}
|
||
// v0.7.0: 关闭前 checkpoint(截断日志,重开更快)。
|
||
// 失败不阻塞关闭(日志已持久,重开可全量重放)。
|
||
try { await this.kv.checkpoint(); } catch { /* 数据在日志中,重开放心重放 */ }
|
||
await this.kv.close();
|
||
await this.memory.close();
|
||
this.opened = false;
|
||
}
|
||
|
||
isOpen(): boolean {
|
||
return this.opened;
|
||
}
|
||
|
||
/**
|
||
* v0.6.0: 从 KVStore 重新加载全部数据到内存(多标签页同步重载用)。
|
||
* Hybrid 引擎的 reloadMemoryFromDisk 依赖磁盘引擎"读穿透",
|
||
* KVStoreEngine 读内存 → 提供 reload 重新加载磁盘最新数据。
|
||
*/
|
||
async reload(): Promise<void> {
|
||
if (!this.opened) return;
|
||
// 1. KVStore 重新从介质加载(外部写入可见)
|
||
await this.kv.reload();
|
||
// 2. 内存缓存重载
|
||
await this.memory.close();
|
||
await this.memory.open(this.dbName, this.version);
|
||
this.opened = false;
|
||
await this.open(this.dbName, this.version);
|
||
}
|
||
|
||
/** v0.4.2-fix: 自愈 — 校验 KVStore 日志/快照完整性并重建内存 */
|
||
async repair(): Promise<void> {
|
||
this.ensureOpen();
|
||
await this.kv.repair();
|
||
await this.memory.close();
|
||
await this.memory.open(this.dbName, this.version);
|
||
// 重新恢复(复用 open 的恢复逻辑)
|
||
this.opened = false;
|
||
await this.open(this.dbName, this.version);
|
||
}
|
||
|
||
async clearAll(): Promise<void> {
|
||
this.ensureOpen();
|
||
await this.kv.clear();
|
||
await this.memory.clearAll();
|
||
}
|
||
|
||
async getMeta(key: string): Promise<string | null> {
|
||
const raw = await this.kv.get(`__meta:${key}`);
|
||
return raw ? dec(raw) : null;
|
||
}
|
||
|
||
async setMeta(key: string, value: string): Promise<void> {
|
||
await this.kv.put(`__meta:${key}`, enc(value));
|
||
}
|
||
|
||
// ---- 表管理 ----
|
||
|
||
async createTable(schema: TableSchema): Promise<void> {
|
||
this.ensureOpen();
|
||
await this.memory.createTable(schema);
|
||
if (this.txActive) {
|
||
this.txDirtyTables.add(schema.name);
|
||
this.txSchemaChanged = true;
|
||
return;
|
||
}
|
||
await this.persistSchema();
|
||
}
|
||
|
||
async dropTable(tableName: string): Promise<void> {
|
||
this.ensureOpen();
|
||
await this.memory.dropTable(tableName);
|
||
if (this.txActive) {
|
||
this.txDirtyTables.add(tableName);
|
||
this.txSchemaChanged = true;
|
||
return;
|
||
}
|
||
await this.persistSchema();
|
||
// 删除该表全部行(KV 中残留清理)
|
||
const diff = await this.collectTableDiff(tableName);
|
||
await this.kv.writeBatch(diff.puts, diff.deletes);
|
||
}
|
||
|
||
async hasTable(tableName: string): Promise<boolean> {
|
||
this.ensureOpen();
|
||
return this.memory.hasTable(tableName);
|
||
}
|
||
|
||
async getTableNames(): Promise<string[]> {
|
||
this.ensureOpen();
|
||
return this.memory.getTableNames();
|
||
}
|
||
|
||
async getTableSchema(tableName: string): Promise<TableSchema | null> {
|
||
this.ensureOpen();
|
||
return this.memory.getTableSchema(tableName);
|
||
}
|
||
|
||
async alterTable(
|
||
tableName: string,
|
||
action: 'ADD' | 'DROP',
|
||
column: import('../constants').ColumnDef & { name: string },
|
||
): Promise<void> {
|
||
this.ensureOpen();
|
||
// v0.7.2: 事务内 ALTER 显式拒绝(与 AriaEngine/MemoryEngine 对齐)——
|
||
// memory.alterTable 直接修改共享 columns 对象,事务快照无法回滚
|
||
// (此前 ROLLBACK 后新增列残留)
|
||
if (this.txActive) {
|
||
throw new DatabaseError(
|
||
`ALTER TABLE is not supported inside a transaction (KVStoreEngine DDL is not transactional)`,
|
||
'NOT_SUPPORTED',
|
||
);
|
||
}
|
||
await this.memory.alterTable(tableName, action, column);
|
||
if (this.txActive) {
|
||
this.txDirtyTables.add(tableName);
|
||
this.txSchemaChanged = true;
|
||
return;
|
||
}
|
||
await this.persistSchema();
|
||
if (action === 'DROP') {
|
||
// 重写存储行(移除该列)
|
||
const diff = await this.collectTableDiff(tableName);
|
||
await this.kv.writeBatch(diff.puts, diff.deletes);
|
||
}
|
||
}
|
||
|
||
// ---- CRUD ----
|
||
|
||
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
|
||
this.ensureOpen();
|
||
const pks = await this.memory.insert(tableName, rows);
|
||
if (this.txActive) {
|
||
this.txDirtyTables.add(tableName);
|
||
// v0.7.0: 事务内 clear 后又写入 → 清空语义被覆盖,整表 diff 兜底
|
||
if (this.txClearedTables.has(tableName)) {
|
||
this.txClearedTables.delete(tableName);
|
||
this.txFullTables.add(tableName);
|
||
return pks;
|
||
}
|
||
// v0.7.0: 行级变更记录(增量 flush)
|
||
let changes = this.txChanges.get(tableName);
|
||
if (!changes) {
|
||
changes = new Map();
|
||
this.txChanges.set(tableName, changes);
|
||
}
|
||
for (const pk of pks) changes.set(pk, 'put');
|
||
return pks;
|
||
}
|
||
// 增量持久化(原子 putMany)
|
||
const schema = await this.memory.getTableSchema(tableName);
|
||
if (!schema) throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
|
||
// v0.7.3: 持久化内存中的 validated 行(含 default 值/类型归一/列投影)——
|
||
// 此前写原始入参 row:default 不落盘、schema 外列被持久化,重启后行不一致
|
||
const puts: Record<string, ArrayBuffer> = {};
|
||
for (const pk of pks) {
|
||
const row = this.memory.getRow(tableName, pk);
|
||
if (row) puts[this.rowKey(tableName, pk)] = enc(JSON.stringify(row));
|
||
}
|
||
await this.kv.putMany(puts);
|
||
return pks;
|
||
}
|
||
|
||
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
|
||
this.ensureOpen();
|
||
return this.memory.find(tableName, query);
|
||
}
|
||
|
||
async findStream(tableName: string, query: QueryPlan, onRow: (row: Record<string, unknown>) => void): Promise<number> {
|
||
this.ensureOpen();
|
||
return this.memory.findStream(tableName, query, onRow);
|
||
}
|
||
|
||
async update(
|
||
tableName: string,
|
||
query: QueryPlan,
|
||
updates: Record<string, unknown>,
|
||
): Promise<number> {
|
||
this.ensureOpen();
|
||
const schema = await this.memory.getTableSchema(tableName);
|
||
if (!schema) throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
|
||
const pkCol = this.getPK(schema);
|
||
// v0.7.2: undefined 值视为"不更新该列"(与 memory.update 语义对齐)
|
||
const cleanUpdates = stripUndefinedUpdates(updates);
|
||
const pkChanged = pkCol in cleanUpdates;
|
||
|
||
// 收集受影响旧主键(内存匹配)
|
||
const affected = pkChanged ? [] : await this.collectMatchingPks(tableName, query);
|
||
const count = await this.memory.update(tableName, query, cleanUpdates);
|
||
if (this.txActive) {
|
||
this.txDirtyTables.add(tableName);
|
||
// v0.7.0: 行级变更记录 —— 主键变更无法行级追踪(旧键删除+新键落盘+级联),
|
||
// 相关表整表 diff 兜底;普通更新记录受影响行
|
||
if (pkChanged) {
|
||
for (const t of await this.affectedTables(tableName)) {
|
||
this.txFullTables.add(t);
|
||
this.txDirtyTables.add(t);
|
||
}
|
||
} else {
|
||
let changes = this.txChanges.get(tableName);
|
||
if (!changes) {
|
||
changes = new Map();
|
||
this.txChanges.set(tableName, changes);
|
||
}
|
||
for (const pk of affected) changes.set(pk, 'put');
|
||
// 级联影响表(理论上非主键更新不级联,防御性兜底)
|
||
for (const t of await this.affectedTables(tableName)) {
|
||
if (t !== tableName) {
|
||
this.txFullTables.add(t);
|
||
this.txDirtyTables.add(t);
|
||
}
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
const puts: Record<string, ArrayBuffer> = {};
|
||
const deletes: string[] = [];
|
||
if (pkChanged) {
|
||
// 主键变更:相关表整表 diff(罕见操作,可靠性优先)
|
||
for (const t of await this.affectedTables(tableName)) {
|
||
const diff = await this.collectTableDiff(t);
|
||
Object.assign(puts, diff.puts);
|
||
deletes.push(...diff.deletes);
|
||
}
|
||
} else {
|
||
// v0.6.2-fix(P0): 受影响主键经 String() 化后按 `where { [pkCol]: pk }` 回查内存行,
|
||
// 数值型主键(123 !== "123")不命中 → 行被误判删除 → 重启丢数据。
|
||
// 改为单次全表扫描 + 受影响集合过滤(同时消除此前 O(N×M) 逐主键回查开销)。
|
||
const affectedSet = new Set(affected);
|
||
const allRows = await this.memory.find(tableName, { table: tableName });
|
||
for (const row of allRows) {
|
||
const pkStr = String(row[pkCol]);
|
||
if (affectedSet.has(pkStr)) {
|
||
puts[this.rowKey(tableName, pkStr)] = enc(JSON.stringify(row));
|
||
affectedSet.delete(pkStr);
|
||
}
|
||
}
|
||
// 剩余主键(内存中已不存在,如被级联移除)→ 删除对应 KV 行
|
||
for (const pk of affectedSet) {
|
||
deletes.push(this.rowKey(tableName, pk));
|
||
}
|
||
// 级联影响表(SET NULL/CASCADE 外键)整表 diff
|
||
for (const t of await this.affectedTables(tableName)) {
|
||
if (t === tableName) continue;
|
||
const diff = await this.collectTableDiff(t);
|
||
Object.assign(puts, diff.puts);
|
||
deletes.push(...diff.deletes);
|
||
}
|
||
}
|
||
// 单次原子写(一条日志记录 = 真原子,v0.6.1)
|
||
await this.kv.writeBatch(puts, deletes);
|
||
return count;
|
||
}
|
||
|
||
async delete(tableName: string, query: QueryPlan): Promise<number> {
|
||
this.ensureOpen();
|
||
// 收集受影响主键(内存匹配)
|
||
const pks = await this.collectMatchingPks(tableName, query);
|
||
const count = await this.memory.delete(tableName, query);
|
||
if (this.txActive) {
|
||
this.txDirtyTables.add(tableName);
|
||
// v0.7.0: 行级变更记录 —— 删除行记录 delete;级联影响表整表 diff 兜底
|
||
let changes = this.txChanges.get(tableName);
|
||
if (!changes) {
|
||
changes = new Map();
|
||
this.txChanges.set(tableName, changes);
|
||
}
|
||
for (const pk of pks) changes.set(pk, 'delete');
|
||
for (const t of await this.affectedTables(tableName)) {
|
||
if (t !== tableName) {
|
||
this.txFullTables.add(t);
|
||
this.txDirtyTables.add(t);
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
const puts: Record<string, ArrayBuffer> = {};
|
||
const deletes = pks.map((pk) => this.rowKey(tableName, pk));
|
||
// 级联影响表整表 diff(合并到单次原子写,v0.6.1)
|
||
for (const t of await this.affectedTables(tableName)) {
|
||
if (t === tableName) continue;
|
||
const diff = await this.collectTableDiff(t);
|
||
Object.assign(puts, diff.puts);
|
||
deletes.push(...diff.deletes);
|
||
}
|
||
await this.kv.writeBatch(puts, deletes);
|
||
return count;
|
||
}
|
||
|
||
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
||
this.ensureOpen();
|
||
return this.memory.count(tableName, query);
|
||
}
|
||
|
||
async clear(tableName: string): Promise<void> {
|
||
this.ensureOpen();
|
||
await this.memory.clear(tableName);
|
||
if (this.txActive) {
|
||
this.txDirtyTables.add(tableName);
|
||
// v0.7.0: 事务内清空 → commit 时删除全部 KV 行(比整表 diff 更高效)
|
||
this.txClearedTables.add(tableName);
|
||
this.txChanges.delete(tableName);
|
||
return;
|
||
}
|
||
const diff = await this.collectTableDiff(tableName);
|
||
await this.kv.writeBatch(diff.puts, diff.deletes);
|
||
}
|
||
|
||
// ---- 动态索引 ----
|
||
|
||
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
|
||
this.ensureOpen();
|
||
if (this.txActive) {
|
||
throw new DatabaseError(
|
||
`CREATE INDEX is not supported inside a transaction (KVStoreEngine DDL is not transactional)`,
|
||
'NOT_SUPPORTED',
|
||
);
|
||
}
|
||
await this.memory.createIndex(tableName, column, unique);
|
||
if (this.txActive) {
|
||
this.txDirtyTables.add(tableName);
|
||
this.txSchemaChanged = true;
|
||
return;
|
||
}
|
||
await this.persistSchema();
|
||
}
|
||
|
||
async dropIndex(tableName: string, column: string, indexName?: string): Promise<void> {
|
||
this.ensureOpen();
|
||
if (this.txActive) {
|
||
throw new DatabaseError(
|
||
`DROP INDEX is not supported inside a transaction (KVStoreEngine DDL is not transactional)`,
|
||
'NOT_SUPPORTED',
|
||
);
|
||
}
|
||
await this.memory.dropIndex(tableName, column, indexName);
|
||
if (this.txActive) {
|
||
this.txDirtyTables.add(tableName);
|
||
this.txSchemaChanged = true;
|
||
return;
|
||
}
|
||
await this.persistSchema();
|
||
}
|
||
|
||
// ---- 事务(原子 flush) ----
|
||
|
||
async beginTransaction(): Promise<void> {
|
||
this.ensureOpen();
|
||
await this.memory.beginTransaction();
|
||
this.txActive = true;
|
||
this.txDirtyTables = new Set();
|
||
this.txSchemaChanged = false;
|
||
this.txChanges = new Map();
|
||
this.txFullTables = new Set();
|
||
this.txClearedTables = new Set();
|
||
}
|
||
|
||
async commitTransaction(): Promise<void> {
|
||
this.ensureOpen();
|
||
if (!this.txActive) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||
// v0.6.1: 全部 dirty 表合并为单次原子 flush(一条日志记录 = 真原子,
|
||
// 多表事务中途崩溃/失败不会出现"部分表已提交")。
|
||
// v0.7.0: 行级增量 flush —— 普通 insert/update/delete 仅写事务内改动的行
|
||
// (此前 collectTableDiff 整表重写:大表事务改 1 行也 O(表大小));
|
||
// 主键变更/级联影响表整表 diff 兜底;clear/drop 表只删 KV 行。
|
||
const puts: Record<string, ArrayBuffer> = {};
|
||
const deletes: string[] = [];
|
||
for (const table of this.txDirtyTables) {
|
||
if (!(await this.memory.hasTable(table))) {
|
||
// 事务内 drop 的表:清理 KV 残留行
|
||
const all = await this.kv.getAll();
|
||
const prefix = this.rowPrefix(table);
|
||
for (const [key] of all) {
|
||
if (key.startsWith(prefix)) deletes.push(key);
|
||
}
|
||
continue;
|
||
}
|
||
if (this.txClearedTables.has(table)) {
|
||
// 事务内 clear 的表:删除全部 KV 行
|
||
const all = await this.kv.getAll();
|
||
const prefix = this.rowPrefix(table);
|
||
for (const [key] of all) {
|
||
if (key.startsWith(prefix)) deletes.push(key);
|
||
}
|
||
continue;
|
||
}
|
||
if (this.txFullTables.has(table)) {
|
||
const diff = await this.collectTableDiff(table);
|
||
Object.assign(puts, diff.puts);
|
||
deletes.push(...diff.deletes);
|
||
continue;
|
||
}
|
||
const changes = this.txChanges.get(table);
|
||
if (changes && changes.size > 0) {
|
||
// v0.7.0: 增量 flush —— 单次全表扫描 + 变更集合过滤
|
||
const schema = await this.memory.getTableSchema(table);
|
||
if (!schema) continue;
|
||
const pkCol = this.getPK(schema);
|
||
const pending = new Map(changes);
|
||
const rows = await this.memory.find(table, { table: table });
|
||
for (const row of rows) {
|
||
const pkStr = String(row[pkCol]);
|
||
const kind = pending.get(pkStr);
|
||
if (kind !== undefined) {
|
||
if (kind === 'put') puts[this.rowKey(table, pkStr)] = enc(JSON.stringify(row));
|
||
else deletes.push(this.rowKey(table, pkStr));
|
||
pending.delete(pkStr);
|
||
}
|
||
}
|
||
// 内存中已不存在的行(后续操作删除)→ KV 行删除
|
||
for (const [pk, kind] of pending) {
|
||
if (kind === 'delete') deletes.push(this.rowKey(table, pk));
|
||
}
|
||
}
|
||
}
|
||
await this.kv.writeBatch(puts, deletes);
|
||
// 事务内 DDL 的 schema 一并持久化
|
||
if (this.txSchemaChanged) {
|
||
await this.persistSchema();
|
||
}
|
||
// v0.7.0-perf: 移除每次 commit 的强制全量 checkpoint —— KVStore 按日志阈值
|
||
// 自动 checkpoint(日志重放保证崩溃恢复正确),大库高频事务不再 O(库大小)。
|
||
// close() 时统一 checkpoint(截断日志,重开更快)。
|
||
await this.memory.commitTransaction();
|
||
this.txActive = false;
|
||
this.txDirtyTables = new Set();
|
||
this.txChanges = new Map();
|
||
this.txFullTables = new Set();
|
||
this.txClearedTables = new Set();
|
||
}
|
||
|
||
async rollbackTransaction(): Promise<void> {
|
||
this.ensureOpen();
|
||
if (!this.txActive) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||
await this.memory.rollbackTransaction();
|
||
this.txActive = false;
|
||
this.txDirtyTables = new Set();
|
||
this.txSchemaChanged = false;
|
||
this.txChanges = new Map();
|
||
this.txFullTables = new Set();
|
||
this.txClearedTables = new Set();
|
||
}
|
||
|
||
// ---- 内部 ----
|
||
|
||
private ensureOpen(): void {
|
||
if (!this.opened) throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
||
}
|
||
|
||
private getPK(schema: TableSchema): string {
|
||
for (const [name, col] of Object.entries(schema.columns)) {
|
||
if (col.primaryKey) return name;
|
||
}
|
||
return Object.keys(schema.columns)[0];
|
||
}
|
||
|
||
/** 收集匹配查询的内存行主键(持久化差异计算用) */
|
||
private async collectMatchingPks(tableName: string, query: QueryPlan): Promise<string[]> {
|
||
const schema = await this.memory.getTableSchema(tableName);
|
||
if (!schema) throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
|
||
const pkCol = this.getPK(schema);
|
||
const rows = await this.memory.find(tableName, query);
|
||
return rows.map((r) => String(r[pkCol]));
|
||
}
|
||
|
||
/**
|
||
* 计算外键级联影响的表集合(传递闭包:A 被 B 引用,B 被 C 引用 → {A, B, C})。
|
||
* 级联操作(delete/update 主键)需要把这些表一并重写持久化。
|
||
*/
|
||
private async affectedTables(tableName: string): Promise<Set<string>> {
|
||
const set = new Set<string>([tableName]);
|
||
let changed = true;
|
||
while (changed) {
|
||
changed = false;
|
||
for (const table of await this.memory.getTableNames()) {
|
||
if (set.has(table)) continue;
|
||
const schema = await this.memory.getTableSchema(table);
|
||
if (!schema) continue;
|
||
for (const col of Object.values(schema.columns)) {
|
||
if (col.references) {
|
||
const ref = col.references.split('.')[0];
|
||
if (set.has(ref)) {
|
||
set.add(table);
|
||
changed = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return set;
|
||
}
|
||
|
||
/** 持久化 schema(全部表) */
|
||
private async persistSchema(): Promise<void> {
|
||
const schemas: Record<string, TableSchema> = {};
|
||
for (const table of await this.memory.getTableNames()) {
|
||
const schema = await this.memory.getTableSchema(table);
|
||
if (schema) schemas[table] = schema;
|
||
}
|
||
await this.kv.put(SCHEMA_KEY, enc(JSON.stringify(schemas)));
|
||
}
|
||
|
||
/**
|
||
* v0.6.1: 整表 diff 收集(不落盘):内存行全部 put + KV 残留行删除。
|
||
* 调用方合并到单次原子 writeBatch(put+delete 同一条日志记录,多表操作真原子)。
|
||
*/
|
||
private async collectTableDiff(tableName: string): Promise<{ puts: Record<string, ArrayBuffer>; deletes: string[] }> {
|
||
const prefix = this.rowPrefix(tableName);
|
||
const puts: Record<string, ArrayBuffer> = {};
|
||
const deletes: string[] = [];
|
||
// 表已删除:仅收集 KV 残留行删除
|
||
const schema = await this.memory.getTableSchema(tableName);
|
||
if (schema) {
|
||
const pkCol = this.getPK(schema);
|
||
const rows = await this.memory.find(tableName, { table: tableName });
|
||
const current = new Set<string>();
|
||
for (const row of rows) {
|
||
const key = this.rowKey(tableName, String(row[pkCol]));
|
||
current.add(key);
|
||
puts[key] = enc(JSON.stringify(row));
|
||
}
|
||
// KV 残留行(内存中已不存在)
|
||
const all = await this.kv.getAll();
|
||
for (const [key] of all) {
|
||
if (key.startsWith(prefix) && !current.has(key)) deletes.push(key);
|
||
}
|
||
} else {
|
||
const all = await this.kv.getAll();
|
||
for (const [key] of all) {
|
||
if (key.startsWith(prefix)) deletes.push(key);
|
||
}
|
||
}
|
||
return { puts, deletes };
|
||
}
|
||
}
|