docs: 重写 README 与 site 站点 — 剥离历史版本注释、统一信息架构(核心特性/快速开始/API速览/引擎对比/AriaEngine 专章/项目状态);更新 docs.html 存储模式对比表(移除 IndexedDB 遗留列)+ v0.6.1 修复记录;同步测试数 1093/70 套件/89.7%;修正 VERSION 常量 0.6.0→0.6.1 并重建 dist
This commit is contained in:
Vendored
+101
-40
@@ -34,7 +34,7 @@ class DatabaseError extends Error {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 版本
|
||||
// ---------------------------------------------------------------------------
|
||||
const VERSION = '0.6.0';
|
||||
const VERSION = '0.6.1';
|
||||
|
||||
/**
|
||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||
@@ -978,12 +978,15 @@ class OPFSBackend {
|
||||
*
|
||||
* 仅用于测试与 Node 环境;浏览器使用 OPFS 介质(KVStore 默认自动选择)。
|
||||
*/
|
||||
/** 全局注册表:dbName → key → ArrayBuffer(跨实例共享,模拟持久化) */
|
||||
/** 全局注册表:dbName → key → chunk 列表(跨实例共享,模拟持久化) */
|
||||
const registry = new Map();
|
||||
class SharedMemoryBackend {
|
||||
constructor() {
|
||||
this.dbName = '';
|
||||
this.store = null;
|
||||
/** 存储:key → chunk 列表(append O(1),read 时一次性拼接缓存) */
|
||||
this.chunks = new Map();
|
||||
/** 惰性拼接缓存(read 后缓存,write/append 失效) */
|
||||
this.materialized = new Map();
|
||||
}
|
||||
/** 清空全局注册表(测试隔离用) */
|
||||
static clearRegistry() {
|
||||
@@ -994,61 +997,80 @@ class SharedMemoryBackend {
|
||||
if (!registry.has(name)) {
|
||||
registry.set(name, new Map());
|
||||
}
|
||||
this.store = registry.get(name);
|
||||
// 从注册表恢复 chunks(持久化语义)
|
||||
this.chunks = registry.get(name);
|
||||
this.materialized = new Map();
|
||||
}
|
||||
/** close 不清除数据(持久化语义:重开同名库数据仍在) */
|
||||
async close() {
|
||||
this.store = null;
|
||||
this.chunks = new Map();
|
||||
this.materialized = new Map();
|
||||
}
|
||||
isOpen() {
|
||||
return this.store !== null;
|
||||
return this.dbName !== '';
|
||||
}
|
||||
async read(key) {
|
||||
return this.store?.get(key) ?? null;
|
||||
if (this.materialized.has(key))
|
||||
return this.materialized.get(key);
|
||||
const list = this.chunks.get(key);
|
||||
if (!list || list.length === 0)
|
||||
return null;
|
||||
if (list.length === 1) {
|
||||
this.materialized.set(key, list[0]);
|
||||
return list[0];
|
||||
}
|
||||
const total = list.reduce((s2, c) => s2 + c.byteLength, 0);
|
||||
const combined = new Uint8Array(total);
|
||||
let off = 0;
|
||||
for (const c of list) {
|
||||
combined.set(new Uint8Array(c), off);
|
||||
off += c.byteLength;
|
||||
}
|
||||
const buf = combined.buffer;
|
||||
this.materialized.set(key, buf);
|
||||
return buf;
|
||||
}
|
||||
async write(key, data) {
|
||||
this.store?.set(key, data);
|
||||
this.chunks.set(key, [data]);
|
||||
this.materialized.set(key, data);
|
||||
}
|
||||
async append(key, data) {
|
||||
if (!this.store)
|
||||
return;
|
||||
const existing = this.store.get(key);
|
||||
if (existing) {
|
||||
const combined = new Uint8Array(existing.byteLength + data.byteLength);
|
||||
combined.set(new Uint8Array(existing), 0);
|
||||
combined.set(new Uint8Array(data), existing.byteLength);
|
||||
this.store.set(key, combined.buffer);
|
||||
// O(1) 追加:只记录 chunk,read 时惰性拼接
|
||||
const list = this.chunks.get(key);
|
||||
if (list) {
|
||||
list.push(data);
|
||||
}
|
||||
else {
|
||||
this.store.set(key, data);
|
||||
this.chunks.set(key, [data]);
|
||||
}
|
||||
this.materialized.delete(key);
|
||||
}
|
||||
async writeMany(entries) {
|
||||
if (!this.store)
|
||||
return;
|
||||
// 同步批量写入 = 原子(JS 单线程,无中间 await 点)
|
||||
for (const [key, data] of Object.entries(entries)) {
|
||||
this.store.set(key, data);
|
||||
this.chunks.set(key, [data]);
|
||||
this.materialized.set(key, data);
|
||||
}
|
||||
}
|
||||
async delete(key) {
|
||||
this.store?.delete(key);
|
||||
this.chunks.delete(key);
|
||||
this.materialized.delete(key);
|
||||
}
|
||||
async deleteMany(keys) {
|
||||
if (!this.store)
|
||||
return;
|
||||
for (const key of keys) {
|
||||
this.store.delete(key);
|
||||
this.chunks.delete(key);
|
||||
this.materialized.delete(key);
|
||||
}
|
||||
}
|
||||
async listKeys() {
|
||||
return this.store ? Array.from(this.store.keys()) : [];
|
||||
return Array.from(this.chunks.keys());
|
||||
}
|
||||
async exists(key) {
|
||||
return this.store?.has(key) ?? false;
|
||||
return this.chunks.has(key);
|
||||
}
|
||||
async clear() {
|
||||
this.store?.clear();
|
||||
this.chunks.clear();
|
||||
this.materialized.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3282,7 +3304,11 @@ class SSTableReader {
|
||||
if (this.indexEntries.length === 0)
|
||||
return;
|
||||
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
||||
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
|
||||
// v0.6.1-fix(P0): 索引键是"块内最后一个 key"(builder 约定),
|
||||
// locateBlockLE 返回最后一个 tail <= endKey 的块,但下一个块(tail > endKey)
|
||||
// 可能仍包含 < endKey 的条目(如末块 t5:k49425..k49995 尾 key 是 t6 前缀)→
|
||||
// 被排除导致范围扫描漏读尾部数据。多扫一个块,条目级过滤保证不丢。
|
||||
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey) + 1);
|
||||
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
|
||||
return;
|
||||
const lenSize = this.lenFieldSize();
|
||||
@@ -4084,16 +4110,21 @@ class LSM {
|
||||
}
|
||||
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction)
|
||||
await this.drainChain();
|
||||
// 若仍有 frozen 数据未刷盘,在链尾追加
|
||||
// v0.6.1-fix(P0): 剩余数据 flush 必须挂链串行 —— 此前直接 await 执行,
|
||||
// 与链上 compaction 并发写 SSTable meta:compaction 产物 saveMeta 后,
|
||||
// memtable flush 的 saveMeta 读到中间态列表(含 compaction 产物)→ 覆盖产物
|
||||
// 引用 → compaction 产物变孤儿 → 索引/主表数据静默丢失(优雅关闭后重开丢 75%)。
|
||||
// 挂链后按序执行(memtable flush 在 compaction 之后),meta 无竞态。
|
||||
if (this.immutableMemtable) {
|
||||
const frozen = this.immutableMemtable;
|
||||
await this.flushImmutableAsync(frozen);
|
||||
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
|
||||
}
|
||||
if (this.memtable.getEntryCount() > 0) {
|
||||
this.freezeMemtable();
|
||||
const frozen = this.immutableMemtable;
|
||||
if (frozen)
|
||||
await this.flushImmutableAsync(frozen);
|
||||
if (frozen) {
|
||||
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
|
||||
}
|
||||
}
|
||||
this.frozenMemtables = [];
|
||||
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
|
||||
@@ -5290,12 +5321,25 @@ class FileManager {
|
||||
async init(dbName) {
|
||||
this.dbName = dbName;
|
||||
const meta = await this.backend.read('__aria_meta');
|
||||
let nextPageId = 1;
|
||||
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
|
||||
const view = new DataView(meta);
|
||||
this.nextPageId = view.getUint32(0, false);
|
||||
nextPageId = new DataView(meta).getUint32(0, false);
|
||||
}
|
||||
else {
|
||||
this.nextPageId = 1;
|
||||
// v0.6.1-fix(P0): 崩溃一致性 —— allocatePageIds 的 saveMeta 可能在分配
|
||||
// 页面后被崩溃中断(nextPageId 回退)。恢复时若按回退值继续分配,
|
||||
// 页面 id 复用会覆盖旧页面;随后 compaction 按 meta.pageIds 删除"旧"SSTable
|
||||
// 时误删被复用的新数据 → 崩溃恢复后静默丢数据(kv 后端 2 万行丢 75%)。
|
||||
// 修复:以"现存最大页面 id + 1"为准(单调不回退,绝不复用已存在页面)。
|
||||
const keys = await this.backend.listKeys();
|
||||
for (const k of keys) {
|
||||
if (k.startsWith('pg_')) {
|
||||
const id = Number.parseInt(k.slice(3), 10);
|
||||
if (!Number.isNaN(id) && id + 1 > nextPageId)
|
||||
nextPageId = id + 1;
|
||||
}
|
||||
}
|
||||
this.nextPageId = nextPageId;
|
||||
if (!meta || !(meta instanceof ArrayBuffer) || meta.byteLength < 4 || nextPageId !== new DataView(meta).getUint32(0, false)) {
|
||||
await this.saveMeta();
|
||||
}
|
||||
this.metaLoaded = true;
|
||||
@@ -6181,7 +6225,17 @@ class AriaEngine {
|
||||
},
|
||||
getBufferedBytes: () => this.wal.getBufferedBytes(),
|
||||
getBufferedCount: () => this.wal.getBufferedCount(),
|
||||
}, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval, this.config.walSizeThreshold);
|
||||
}, {
|
||||
flushAll: async () => {
|
||||
// v0.6.1-fix: checkpoint 必须同时落盘二级索引 LSM —
|
||||
// 此前只 flush 主 LSM,checkpoint 截断 WAL 后崩溃时索引 memtable 未落盘、
|
||||
// WAL 为空跳过重建 → 二级索引静默丢失最后一批条目(生产数据一致性问题)
|
||||
await this.lsm.flush();
|
||||
for (const idxLsm of this.secondaryIndexes.values()) {
|
||||
await idxLsm.flush();
|
||||
}
|
||||
},
|
||||
}, this.config.checkpointInterval, this.config.walSizeThreshold);
|
||||
this.opened = true;
|
||||
}
|
||||
async close() {
|
||||
@@ -6432,12 +6486,17 @@ class AriaEngine {
|
||||
const pks = [];
|
||||
// v0.3.1: 批量 WAL 写入(组提交),一次 insert 合并为一次落盘
|
||||
const walRecords = [];
|
||||
// v0.6.1-perf: 批量预加载本批 PK 涉及的 SSTable(一次 drainChain)。
|
||||
// 此前循环内逐行 prefetchKeys —— 每行 await drainChain 排空后台链,
|
||||
// 后台 compaction 在链上数秒时每行阻塞数秒 → 大数据量插入性能悬崖
|
||||
// (10 万行 kv 后端从 5ms/批暴跌到 8~11s/批)。批内新数据在 memtable
|
||||
// 或 flush 产物(自动入缓存),循环内 lsm.get 始终完整。
|
||||
await this.lsm.prefetchKeys(rows.map((r) => `${tableName}:${String(r[pkCol])}`));
|
||||
for (const row of rows) {
|
||||
const validated = this.validateRow(schema, row);
|
||||
const pkValue = String(validated[pkCol]);
|
||||
const key = `${tableName}:${pkValue}`;
|
||||
// Check duplicate in LSM + transaction snapshot
|
||||
await this.lsm.prefetchKeys([key]);
|
||||
const existing = this.currentTxnId
|
||||
? (this.txnSnapshot?.get(key) ?? this.lsm.get(key))
|
||||
: this.lsm.get(key);
|
||||
@@ -7306,13 +7365,15 @@ class AriaEngine {
|
||||
},
|
||||
};
|
||||
}
|
||||
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS 后端启用,显式配置可覆盖) */
|
||||
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS / KVStore 后端启用,显式配置可覆盖) */
|
||||
isPageStorage() {
|
||||
if (this.config.pageStorage === true)
|
||||
return true;
|
||||
if (this.config.pageStorage === false)
|
||||
return false;
|
||||
return this.config.storageBackend === 'opfs';
|
||||
// v0.6.1: kv 后端同样页面化 — 整 value SSTable(4MB)超过 1MB 页面缓存时
|
||||
// 每次读取全量重载;拆 4KB 页面后缓存按页命中,大数据量读放大消除
|
||||
return this.config.storageBackend === 'opfs' || this.config.storageBackend === 'kv';
|
||||
}
|
||||
// =======================================================================
|
||||
// WAL 恢复
|
||||
|
||||
Reference in New Issue
Block a user