Files
MetonaSqlark/src/engine/kvstore_engine.ts
T

666 lines
24 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.
/**
* KVStoreEngine — 基于自研 KVStore 的磁盘存储引擎(替代 IndexedDBEngine / OPFSEngine
* @module engine/kvstore_engine
*
* v0.6.0: 完全移除 IndexedDB 后的 disk 模式引擎。
*
* 架构:MemoryEngine(内存热路径 + 事务快照)+ KVStore(持久化 + 原子写)
* - 读:始终走内存(写路径同步落盘,重启从 KVStore 恢复)
* - 写:内存先行 + KVStore 增量持久化(insert 增量 putManyupdate/delete 受影响行重写;
* 主键变更/级联场景整表 diff;全部原子)
* - 事务:内存快照 + commit 时受影响表原子 flushwriteBatch 单记录 = 真原子,
* 此前 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 值/类型归一/列投影)——
// 此前写原始入参 rowdefault 不落盘、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 残留行删除。
* 调用方合并到单次原子 writeBatchput+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 };
}
}