Files
MetonaSqlark/src/engine/interface.ts
T
thzxx 074afd3f1e fix(A8 + LSM 读自洽): 行所有权根治 + 读取路径不再依赖 prefetch
A8 行引用泄漏(调用方改查询结果即改写存储)
  实测:rows[0].tag = 'HACKED' 后,tag='HACKED' 与 tag='x' 两条索引查询都返回 0 行 ——
  行与索引失配、该行永久查不出来;嵌套 json 值同样按引用共享。
  Aria 因走反序列化路径反而幸免,又形成跨引擎差异。

  根治:在 IStorageEngine 契约层写入**行所有权约定**(engine/interface.ts)
  —— 读出的行是副本、写入接收的行也是副本;新增 cloneRow/cloneRows
  (优先 structuredClone,退化路径处理 Date/嵌套对象/二进制)。
  Memory/KVStore/Hybrid:find、findStream、getRow 全部返回副本。
  Aria:getAllRows 此前只做 `{ ...value }` 浅拷贝(嵌套 json 仍共享引用),
  改为深拷贝;find/findStream 返回副本。
  实测四种引擎:修改返回值后重读不变、索引两条查询均正确。

LSM 读取自洽(审计 P1-1:缓存未命中 = 静默丢数据)
  此前 loadSSTableReader 缓存未命中返回 null,而所有调用方都是
    const reader = this.loadSSTableReader(meta); if (!reader) continue;
  于是**未命中就静默跳过整个 SSTable**。实测:缓存上限 4KB 而 SSTable 更大时,
  300 行只能查回 59 行,且不报错。
  同时"读路径必须先 prefetch"这个隐式约定,是每次读都要 drainChain + prefetch
  的原因(性能悬崖的另一半)。

  根治:LSM.get / rangeScan / rangeScanLazy 改为 async,未命中即
  `await sstableStore.load()` 回源 + CRC 校验(损坏则自愈清理 meta),
  只有数据确实不存在才返回 null。checkUniqueSync 相应改名 checkUnique 并 async
  (原命名正是因为依赖 prefetch 约定)。引擎侧 12 处调用点补 await。

附带修正的缓存语义:
  - tryCacheSSTable:单个 SSTable 超过缓存上限时标记为常驻(pinned),
    不参与驱逐 —— 驱逐它等价于静默丢数据;内存上限因此是
    cacheLimit + 单个最大 SSTable,已在代码与测试中明确。
  - trimCache 跳过 pinned 条目(此前会把全部缓存一次性清空)。
  - 新增 getCacheSize/getCacheLimit/getOversizedCount/setCacheLimit 访问器
    (测试此前直接读私有字段 cacheSize/cacheLimitBytes —— 那是 TS 错误,
    只因测试不做类型检查才没暴露)。

queryStream async 回调
  此前用 constructor.name === 'AsyncFunction' 判定,对"普通函数返回 Promise"
  完全失效(Promise 被静默丢弃)。现改为双条件识别并走物化路径逐行 await,
  async 回调被真正等待。

测试契约修正:
  - aria-cache:'缓存大小受上限约束' 在极小缓存下是不可成立的契约,改为断言
    真正重要的不变量(数据完整;可装入时受上限约束),并新增"超大 SSTable 常驻"
    用例;'缓存驱逐后全表扫描仍返回完整数据' 保留 300 行断言(此前会失败)。
  - hybrid:磁盘引擎标签断言从 indexeddb(v0.6.0 已移除)改为 opfs。
2026-09-14 21:43:21 +08:00

166 lines
6.0 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 Engine Interface — 存储引擎抽象接口
* @module engine/interface
*/
import type { QueryPlan, TableSchema } from '../constants';
// ---------------------------------------------------------------------------
// v0.8.0: 行所有权(row ownership)约定
// ---------------------------------------------------------------------------
/**
* 深拷贝一行,使调用方**无法通过修改返回值改写存储**。
*
* 为什么必须做(审计实测):Memory/KVStore/Hybrid 三个引擎此前把内部行对象
* **直接**交给调用方:
* const rows = await db.query('SELECT * FROM t');
* rows[0].tag = 'HACKED'; // 存储被改写
* 再查 WHERE tag='HACKED' → 0 行;WHERE tag='x' → 0 行
* 即调用方一次无意的原地修改就能让索引与行失配、该行永久查不出来(Aria 因为是
* 反序列化路径反而幸免,于是又成了跨引擎行为差异)。
*
* 约定(写入 interface 文档,所有引擎必须遵守):
* **读出的行是副本,写入接收的行也是副本** —— 引擎不得把内部行对象暴露给外部,
* 也不得持有调用方传入的行对象引用。
*
* 实现说明:结构化克隆可用时优先使用(正确处理 Date/嵌套对象/循环引用);
* 存储层写入的行已经过 validateRow 的 JSON 安全性检查,因此退化路径也是安全的。
*/
export function cloneRow<T extends Record<string, unknown>>(row: T): T {
if (row === null || typeof row !== 'object') return row;
if (typeof structuredClone === 'function') {
try {
return structuredClone(row);
} catch {
// 含不可克隆值(函数/Proxy)时退化为逐层复制
}
}
return cloneRowFallback(row) as T;
}
/** 退化实现:递归复制普通对象与数组(保留 Date) */
function cloneRowFallback(value: unknown): unknown {
if (value === null || typeof value !== 'object') return value;
if (value instanceof Date) return new Date(value.getTime());
if (Array.isArray(value)) return value.map((v) => cloneRowFallback(v));
if (value instanceof Uint8Array) return new Uint8Array(value);
if (value instanceof ArrayBuffer) return value.slice(0);
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
out[k] = cloneRowFallback(v);
}
return out;
}
/** 批量深拷贝 */
export function cloneRows<T extends Record<string, unknown>>(rows: T[]): T[] {
return rows.map((r) => cloneRow(r));
}
// ---------------------------------------------------------------------------
// IStorageEngine — 所有存储引擎必须实现的接口
// ---------------------------------------------------------------------------
export interface IStorageEngine {
/** 引擎名称 */
readonly name: string;
/** 打开数据库 */
open(dbName: string, version: number): Promise<void>;
/** 关闭数据库 */
close(): Promise<void>;
/** 检查数据库是否已打开 */
isOpen(): boolean;
/** 创建表 */
createTable(schema: TableSchema): Promise<void>;
/** 删除表 */
dropTable(tableName: string): Promise<void>;
/** 检查表是否存在 */
hasTable(tableName: string): Promise<boolean>;
/** 获取所有表名 */
getTableNames(): Promise<string[]>;
/** 获取表结构 */
getTableSchema(tableName: string): Promise<TableSchema | null>;
/** 插入行,返回主键值列表 */
insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]>;
/** 查询行 */
find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]>;
/** v0.4.0: 流式查询 — 逐行回调扫描(有 where/limit/projection,无 orderBy 语义;有 orderBy 时实现可回退物化) */
findStream?(tableName: string, query: QueryPlan, onRow: (row: Record<string, unknown>) => void): Promise<number>;
/** 更新行,返回影响行数 */
update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number>;
/** 删除行,返回影响行数 */
delete(tableName: string, query: QueryPlan): Promise<number>;
/** 计数 */
count(tableName: string, query?: QueryPlan): Promise<number>;
/** 清空表数据(保留结构) */
clear(tableName: string): Promise<void>;
/** v0.4.1: ALTER TABLE(可选)— 引擎级结构变更(Aria 需重写存储行,其余引擎走 Executor 通用路径) */
alterTable?(tableName: string, action: 'ADD' | 'DROP', column: import('../constants').ColumnDef & { name: string }): Promise<void>;
// ---- 动态索引(可选,v0.3.0 ----
/** 创建二级索引(CREATE INDEX */
createIndex?(tableName: string, column: string, unique?: boolean): Promise<void>;
/** 删除二级索引(DROP INDEX */
dropIndex?(tableName: string, column: string, indexName?: string): Promise<void>;
// ---- 事务 ----
/** 开始事务 */
beginTransaction(): Promise<void>;
/** 提交事务 */
commitTransaction(): Promise<void>;
/** 回滚事务 */
rollbackTransaction(): Promise<void>;
// ---- Savepoint (可选) ----
/** 创建 Savepoint */
savepoint?(name: string): Promise<void>;
/** 回滚到 Savepoint */
rollbackToSavepoint?(name: string): Promise<void>;
/** 释放 Savepoint */
releaseSavepoint?(name: string): Promise<void>;
// ---- 备份 (可选) ----
/** 在线备份:导出全库一致性快照 */
backup?(): Promise<Record<string, Record<string, unknown>[]>>;
// ---- 自愈/重置 (可选,v0.4.2-fix) ----
/** 崩溃恢复自愈:校验并清理损坏数据、恢复一致性(检测到异常后调用,无需删库重建) */
repair?(): Promise<void>;
/** 清空全部数据与表结构(保留库本身,供演示页刷新/重建用) */
clearAll?(): Promise<void>;
/** 读取库内元数据(迁移版本持久化用) */
getMeta?(key: string): Promise<string | null>;
/** 写入库内元数据(迁移版本持久化用) */
setMeta?(key: string, value: string): Promise<void>;
}