release: v0.3.2 — 质量加固 + SQL扩展 + 表达式 + 并发同步
CI / test (18.x) (push) Failing after 5m11s
CI / test (20.x) (push) Failing after 5m8s
CI / test (22.x) (push) Successful in 9m58s
CI / test (24.x) (push) Successful in 9m56s

v0.2.6 质量加固:
- 修复 AriaEngine 二级索引 SSTable 互相覆盖(命名空间隔离)
- 修复 LSM 多版本读取顺序错误 + MergeIterator 取最新来源
- 重写 LZ4 压缩器(往返一致性 + 缓冲区溢出)
- sstableCache LRU 上限 + 预加载兜底(BufferPool 配置生效)
- 修复 React/Vue 集成 import type 运行时 bug + exports 子路径
- 新增 38 个测试(LZ4往返/Crypto/集成), 删除伪测试

v0.3.0 SQL 功能扩展:
- 多语句 parseAll + 事务语句 BEGIN/COMMIT/ROLLBACK
- INSERT INTO ... SELECT + UNION/UNION ALL + EXISTS 关联子查询
- CREATE/DROP INDEX 五引擎实现 + 别名 WHERE 修复
- benchmark 页面 + 36 个新测试

v0.3.1 表达式与性能:
- CASE WHEN 表达式(SELECT 列/WHERE/聚合)
- JOIN + 关联子查询逐行绑定
- WAL 批量组提交(写放大 O(N)→O(1))
- 修复 pending frozen 可见性 + flush 缓存竞争

v0.3.2 并发:
- CASE WHEN 用于 WHERE/聚合 + JOIN 哈希连接
- 多标签页同步(multiTabSync + BroadcastChannel)
- IndexedDB schema 持久化(reopen 后表结构恢复)
- 修复 where-matcher 顶层 $not
- 修复 CJS 产物 .js 被 ESM 解析(exports 空) — .cjs 后缀 + exports 修正
- 836 测试 / 44 套件 / 81.0% 覆盖率
This commit is contained in:
thzxx
2026-08-08 10:41:30 +08:00
parent 3ae7d6e8fb
commit d544501e1c
77 changed files with 29007 additions and 20395 deletions
+336 -241
View File
@@ -1,241 +1,336 @@
# Changelog
All notable changes to MetonaSqlark will be documented in this file.
## [0.2.5] - 2026-07-29
### Fixed
- **版本号统一** — `constants.ts` VERSION 从 `'0.2.0'` 更新为 `'0.2.5'`,修正 `index.ts`/`utils.ts`/`CONTRIBUTING.md` 中过时的版本注释和数据
- **AriaEngine OPFS 后端映射** — `core.ts``mode: 'aria'` + `diskEngine: 'opfs'` 时实际使用 Memory 后端的 bug 已修复
- **`_onError` 接入执行路径** — `query()`/`defineTable()`/`dropTable()`/`transaction()`/`importTable()` 的 catch 路径现在调用 `_onError` 全局错误回调
- **`maxRowsPerQuery` 生效** — `QueryExecutor` 构造时接收 `maxRowsPerQuery` 参数,SELECT 结果在返回前截断
- **WAL full 模式真正同步** — `WAL.append()` 改为 `async``full` 模式下 `await this.store.append()` 真正等待写入完成,不再 fire-and-forget
- **PluginManager.install 传 db 实例** — `register()` 增加可选 `db` 参数,`core.ts` 初始化时传入 `this`,插件可获取 db 引用
### Changed
- **SSTableReader 二分查找统一** — `locateBlockGE`/`locateBlockLE` 从线性扫描改为二分查找,rangeScan 性能在大型 SSTable 下不再退化
- **crypto 实例化** — 全局状态改为 `CryptoManager` 类,每个 AriaEngine 实例可拥有独立加密配置,保留全局函数向后兼容
- **compactLevelSync 接口公开化** — LSM 新增 public `compactLevel()` 方法,`vacuum()` 不再使用 `as any` 绕过 private 访问
- **WAL 大小阈值接入 checkpoint** — `CheckpointManager` 接收 `walSizeThreshold` 参数,WAL 缓冲超阈值时自动触发 checkpoint
### Added
- **ALTER TABLE 语法** — 支持 `ALTER TABLE ... ADD COLUMN` / `DROP COLUMN`(含可选 COLUMN 关键字)
- **TRUNCATE TABLE 语法** — 支持 `TRUNCATE TABLE name` 快速清空表数据
- **MVCC 接入读写路径** — 事务内 insert/update/delete 调用 `mvcc.writeVersion`/`mvcc.deleteVersion`,版本链作为 undo log
- **IndexedDB 索引利用** — `IndexedDBEngine.find` 等值查询时优先使用 IDB 索引(`idx_col` 命名约定),避免全量 getAll
- **SQL 注入防护** — React `useTable` / Vue `useSqlarkTable` 增加表名合法性校验(`/^[a-zA-Z_][a-zA-Z0-9_]*$/`
---
## [0.2.4] - 2026-07-27
### Added
- **二级索引** — 每列可独立维护 LSM Tree 索引,`$eq`/`$in`/`$gt`/`$lt` 走索引 O(log n)
- **MVCC 接入引擎** — MVCCManager 正式投产,替换 ad-hoc txnSnapshot
- **MVCC 自动 GC** — 每 10 次 checkpoint 自动回收过旧版本(保留最新 100 个)
- **Bloom Filter 序列化** — 写入 SSTable footer + 读取时加载 + 查询时 probe 快速否定
- **WAL 大小阈值** — `walSizeThreshold` 配置(默认 16MB),超阈值强制 checkpoint
- **内存预算** — `maxMemoryMB` 配置(默认 64MB
### Changed
- **WAL 默认同步模式** — `walSyncMode``'batch'` 改为 `'full'`,消除 crash 丢数据风险
- **查询优化** — `tryIndexLookup` 扩展支持非 PK 列索引查找
---
## [0.2.3] - 2026-07-27
### Fixed
- **RB-Tree fixDelete 完整实现** — 补全标准红黑树删除修复,保证 O(log n)
- **LSM SSTable 缓存预热** — `init()` 预加载所有 SSTable,消除 cache miss
- **OPFS 数据恢复** — `open()` 自动从 OPFS 文件加载已有表数据到内存
- **Aria WAL 事务恢复** — 两阶段恢复:仅回放已提交事务,未提交数据不回放
- **LZ4 格式修复** — 重写 token 格式,消除中间字面量 token 歧义
---
## [0.2.2] - 2026-07-27
### Added
- **OPFS 自研存储后端** — AriaEngine 新增 `OPFSBackend`,纯浏览器文件系统 API,零 IndexedDB 依赖
- 每个 key 对应一个二进制文件,存储在 `navigator.storage.getDirectory()`
- 支持 read/write/delete/list/exists/clear 完整接口
- 页面文件(4KB)、WAL 日志、Schema、SSTable 全部存储为独立文件
- `{ dbName }/pg_1`, `{ dbName }/__wal_0`, `{ dbName }/__aria_schemas` ...
### Changed
- `AriaEngineConfig.storageBackend` 已支持 `'opfs'``AriaEngine.open()` 自动选择 OPFSBackend
---
## [0.2.1] - 2026-07-27
### Fixed
- **WAL CRC 校验验证** — 恢复时计算并验证 CRC32,损坏记录自动跳过并告警
- **Hybrid 引擎提交顺序** — commit 先写磁盘再写内存,磁盘失败回滚内存,消除数据不一致风险
- **RESTRICT 外键行为修正** — 检测到引用行时抛出 `FOREIGN_KEY_VIOLATION`,不再静默保留孤儿
- **SSTable rangeScan 边界保护** — 空索引或 startBlockIdx > endBlockIdx 时安全返回
### Added
- **ColumnDef 约束激活** — `maxLength`/`min`/`max` 约束在 `checkFieldType` 中正式生效
- **查询结果上限** — `DatabaseConfig.maxRowsPerQuery`(默认 0 不限制),防止超大结果集 OOM
- **onError 全局回调** — 配置中的 `onError` 回调实际接CRUD 异常路径
- **debug 调试模式** — `DatabaseConfig.debug: true` 输出 `[MetonaSqlark:name]` 前缀的结构化日志
- **浏览器兼容性声明** — README 增加 Chrome 80+/Firefox 80+/Safari 14+/Edge 80+ 支持矩阵
### Changed
- MemoryEngine RESTRICT 外键行为:从"不级联保留孤儿"改为"禁止删除抛异常"
- Hybrid 引擎 commitTransaction 磁盘优先于内存
- DB_DEFAULTS 新增 `maxRowsPerQuery: 0``debug: false`
---
## [0.2.0] - 2026-07-27
### Added
- **AriaEngine 自研存储引擎** — 基于 LSM-Tree 的页面式存储引擎,19 个新模块,~3500 行 TypeScript
- **页面格式层** (`page/`): 4KB Slotted Page 编解码、Tuple 二进制序列化、CRC32 校验
- **Buffer Pool** (`buffer/`): LRU 页面缓存 + 驱逐策略,可控内存占用
- **LSM-Tree 索引** (`index/`): MemTable (红黑树) + 多级 SSTable + Leveled Compaction
- **Bloom Filter** (`index/bloom.ts`): FNV-1a + Murmur 双哈希,快速键否定判定
- **SSTable 构建器/读取器** (`index/sstable_builder.ts`, `sstable.ts`): 二分查找 + 范围扫描
- **Merge Iterator** (`index/merge_iterator.ts`): 最小堆多路归并,去重保留最新值
- **WAL** (`wal/`): 二进制日志格式 (LSN/type/txnId/table/key/json/CRC) + Checkpoint 管理
- **MVCC** (`transaction/mvcc.ts`): 版本链 + 快照隔离 + GC
- **存储后端** (`store/`): IndexedDB / Memory 双后端抽象
- **LZ4 压缩** (`compression/lz4.ts`): 简易页面级压缩
- **`mode: 'aria'`** — 新增存储模式,可通过 `MetonaSqlark.create({ mode: 'aria' })` 激活
- **Schema 持久化** — 表结构自动保存到 `__aria_schemas`,重启自动恢复
- **SSTable 元数据管理** — SSTable 索引信息持久化,启动时自动扫描加载
- **事务感知 CRUD** — insert/update/delete 在事务中缓冲到 snapshotcommit 批量写入 LSM
- **244 个 AriaEngine 专项测试** — 覆盖生命周期/表管理/CRUD/事务/持久化/SQL 集成/页面格式/LSM/压缩
### Changed
- `StorageMode` 类型新增 `'aria'`
- `STORAGE_MODES` 数组新增 `'aria'`
- `createEngine()` 支持 `mode: 'aria'` 分支
- 测试从 318 → **526**,套件从 20 → **27**
- 新增 7 个模块级测试文件:`aria-page``aria-index``aria-sstable``aria-buffer``aria-wal-mvcc``aria-compress``aria`
- LRUList 修复 size 追踪 bug
- WAL 存储改为按记录独立 key(避免拼接缓冲区越界)
- CheckpointManager 解耦 BufferPool 依赖
### Fixed
- **LZ4 压缩无限循环** — 字面量分支在发现匹配后回退导致 litLen=0 死循环,CI 卡死根因
- **SSTableReader.get() 自比较 bug** — 参数 key 被循环变量遮蔽导致永远返回第一条
- **CheckpointManager 测试 null 引用** — 改为 Mock 对象避免 TypeError 导致进程无法退出
- **IndexedDB 持久化测试** — 替换为 Memory Backend 验证,消除 fake-indexeddb timer 堆积
- **LSM.flush() 不必要 setTimeout** — 替换为 Promise.resolve(),消除额外 timer 延迟
---
## [0.1.14] - 2026-07-26
### Fixed
- **IndexedDB 事务原子性**: `flushToIDB` 改为单 IDB 事务包裹 clear+insert,消除崩溃丢数据风险
- **多标签页冲突**: `open()` 添加 `onversionchange` 监听,其他标签页升级版本时自动关闭过期连接
- **Memory 引擎幂等**: `open()` 重复调用安全无副作用
- 关闭时清理 `onversionchange` 监听器,防止内存泄漏
---
## [0.1.13] - 2026-07-26
### Added
- **事务回滚机制**: `IStorageEngine` 新增 `beginTransaction/commitTransaction/rollbackTransaction` 接口
- MemoryEngine: 快照式回滚(深拷贝 tables/schemas/indexes
- IndexedDBEngine: 延迟写入策略(事务中仅写内存,commit 批量刷 IDB)
- OPFSEngine: 快照式回滚(commit 批量写文件)
- HybridEngine: 同时代理内存+磁盘引擎事务
- **子查询支持**: SQL Parser + Executor 支持 `IN (SELECT ...)``op (SELECT ...)` 子查询
- AST 新增 `SubqueryExpression` 类型
- Parser 在 IN 和比较运算符后检测子查询
- Executor 新增 `resolveSubqueries()` 递归解析,自动执行子查询并替换为具体值
- **外键级联操作**: ColumnDef 新增 `onDelete/onUpdate` 选项
- 支持 `CASCADE`(递归级联删除)、`SET NULL``RESTRICT`
- SQL Parser 解析 `REFERENCES table(col) ON DELETE CASCADE ON UPDATE CASCADE`
- MemoryEngine 内置 `cascadeDelete()` 递归级联逻辑
- **连接池管理**: `MetonaSqlark.connect()` 静态方法
- 同名数据库复用已打开实例,引用计数管理
- `db.disconnect()` 释放连接,归零自动 close
- `MetonaSqlark.disconnectAll()` 强制关闭所有连接
### Changed
- `TransactionManager.execute()` 调用引擎层 begin/commit/rollback 实现真正原子性
- IndexedDBEngine CRUD 事务感知:活跃事务中延迟 IDB 写入
- ASTColumnDef / astColumnToColumnDef 支持 references/onDelete/onUpdate 透传
### Fixed
- IndexedDBEngine 事务中 find/count 从内存缓存读取(保证读到未提交变更)
- SQL Parser parseColumnDef 循环条件扩展,避免 REFERENCES 语法解析中断
---
## [0.1.12] - 2026-07-26
### Added
- `MeSqlark` 别名导出,与 `MetonaSqlark` 完全等价
- `metona-sqlark.cjs.js` CommonJS 构建产物
- 完善的 `where-matcher.ts` 消除 220+ 行重复代码
- `$col` 列引用支持 JOIN ON 条件
- `$and/$or` 嵌套支持(字段级 + 顶层)
- `$not` 逻辑非操作符
- `$like` 正则缓存加速
- `projectColumns` 列投影支持 `table.column` 格式
- `DISTINCT` 去重支持(O(n) 时间,列值拼接优化)
- 测试覆盖率提升至 93.46%(264 测试/15 套件)
### Changed
- `where-matcher` 统一 MemoryEngine / IndexedDBEngine / Executor 的 WHERE 逻辑
- JOIN ON 匹配支持 `$col` 语法
- LIKE 正则改为缓存式编译
- Executor DISTINCT 用列值拼接代替 JSON.stringify
- JOIN 嵌套循环连接优化:避免 ON 时对象扩散
### Fixed
- IndexedDBEngine `update/delete` 方法正确同步内存缓存
- OPFSEngine `getTableNames` 兼容 `entries()` 返回
- SQL Parser 正确处理字符串转义字符
- SQL Parser 正确处理表别名(无 AS 关键字)
- SQL Parser 解析 `IS NULL` / `IS NOT NULL` / `NOT LIKE`
- `peekTokenIs` 方法正确暴露给语法分析
- 多处边界条件空值/假值处理
---
## [0.1.11] - 2026-07-25
### Added
- React 集成 hooks: `useQuery`, `useTable`, `useDatabase`
- Vue 集成 composables: `useSqlarkQuery`, `useSqlarkTable`, `useSqlarkDatabase`
- 发布订阅系统: `subscribe()`, `emit()`
- 数据迁移系统: `addMigration()`, `migrateTo()`
- 导入导出: `exportTable()`, `exportAll()`, `importTable()`
- 完整 SQL 解析器: `Lexer` + `Parser`(递归下降)
- SQL 支持: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
- JOIN 支持: INNER, LEFT, RIGHT, CROSS
- GROUP BY + HAVING + 聚合函数 (COUNT, SUM, AVG, MIN, MAX)
- DISTINCT, ORDER BY, LIMIT, OFFSET
- WHERE 条件: AND, OR, NOT, IN, LIKE, IS NULL
- Query Builder 链式 API: `select().where().orderBy().limit().execute()`
- 插件系统: 14 种生命周期钩子 + PluginManager
- Metadata 系统: ColumnDef 完整约束 (type/primaryKey/required/unique/index/default/references/maxLength/min/max)
### Changed
- 架构重构为分层设计:Engine → QueryExecutor → Table/QueryBuilder → SQL Parser → Public API
- AST 作为统一中间表示,SQL 和 QueryBuilder 行为完全一致
- 测试框架完善:core / edge / groupby / join / query-system / sql / table / engine / transaction / plugin / hybrid 全覆盖
---
## [0.0.1] - 2026-07-24
### Added
- 初始项目骨架
- TypeScript 配置(严格模式)
- Rollup 构建(UMD / ESM / CJS / min / .d.ts
- Jest + jsdom 测试环境
- ESLint + @typescript-eslint
- Gitea Actions CI 工作流
- 示例站点(index / demo / docs
- 构建脚本 build.sh
# Changelog
All notable changes to MetonaSqlark will be documented in this file.
## [0.3.2] - 2026-08-08
### Added
- **CASE WHEN 用于 WHERE** — `WHERE CASE WHEN cond THEN a ELSE b END = 'x'` 逐行求值(含 NOT/AND/OR 组合)
- **CASE WHEN 用于聚合** — `SUM(CASE WHEN age > 18 THEN 1 ELSE 0 END)``COUNT/AVG/MIN/MAX(CASE...)``GROUP BY + CASE` 非聚合列
- **JOIN 哈希连接** — 等值 ON + 右表主键/索引列时,收集左表连接值 → 一次 `$in` 查询 → 哈希映射匹配,替代嵌套循环(INNER/LEFT JOINO(N+M));不适用自动回退
- **多标签页同步** — `multiTabSync: true` 配置 + BroadcastChannel
- SQL 写语句(INSERT/UPDATE/DELETE/DDL)自动广播表变更
- Table API 写入(insert/insertMany/update/delete/clear/drop)自动广播
- 其他标签页收到 `external` 变更事件(订阅者可见)+ Hybrid 内存自动重载
- `db.broadcastChange(table)` 手动广播
- **IndexedDB schema 持久化** — `__metona_schema` store 保存完整列定义,reopen(页面刷新/重连)后恢复;旧数据回退为"主键 + 索引 + 样例推断"
### Fixed
- **IndexedDB reopen 后 schema 丢失(严重)** — schema 仅存内存缓存,close 后重开连接表结构即丢失(INSERT 列映射截断、UPDATE 全列失效)。v0.3.2 持久化 + 重建
- **where-matcher 顶层 $not 不生效** — `NOT (expr)` 解析为顶层 `{ $not }` 但被当作字段匹配,始终返回 true
- **CASE 剥离后空 `$not` 恒假** — 引擎层执行 `NOT(true)` 过滤掉所有行
### Changed
- 版本号升至 v0.3.2
---
## [0.3.1] - 2026-08-08
### Added
- **CASE WHEN 表达式(SELECT 列)** — `CASE WHEN cond THEN value [WHEN ...] [ELSE value] END [AS alias]`
- 多 WHEN 按顺序匹配、ELSE 缺省返回 null
- THEN/ELSE 值支持字面量(字符串/数字/布尔/null)与列引用
- 条件支持 AND/OR/比较运算符/IS NULL 等完整 WHERE 语法
- 引擎层返回原始行供求值(避免投影丢失条件列),与普通列混合投影
- **JOIN + 关联子查询** — JOIN 结果的 WHERE 支持 `EXISTS (SELECT ... WHERE o2.x = u.y)``$col` 引用外层行,逐行绑定求值
- **WAL 批量组提交** — `WAL.appendBatch()` 合并多条记录为一次底层写入;引擎 insert/update/delete 批量记录后一次落盘(写放大从 O(N) 降为 O(1))
- **`parseWhereCondition()` 导出** — 独立条件表达式解析(CASE WHEN 求值内部使用
### Fixed
- **LSM pending frozen 可见性** — 异步 flush 链期间,中间冻结的 MemTable 仅存在于闭包中,读取路径不可见导致数据缺失。新增 `frozenMemtables` 列表,get/rangeScan/getAllEntries 遍历所有 pending frozen(新→旧)
- **flush 缓存裁剪与预加载竞争** — flush 完成时的 LRU 裁剪会驱逐 prefetch 刚加载的 SSTable,导致扫描静默丢数据。prefetchRange/prefetchKeys 先等待 flush 链完成再加载
- **CASE 值解析顺序** — `true`/`false`/`null` 字面量被误判为列引用
### Changed
- 版本号升至 v0.3.1
---
## [0.3.0] - 2026-08-08
### Added
- **SQL 多语句支持** — `parseAll()` 解析分号分隔的多条语句,`db.query()` 顺序执行并返回最后一条结果(`parse()` 保持单语句兼容)
- **事务语句(TCL** — `BEGIN` / `COMMIT` / `ROLLBACK`(支持可选 TRANSACTION 关键字),直接驱动引擎层事务,五引擎可用
- **INSERT INTO ... SELECT** — 支持 `INSERT INTO t (cols) SELECT ...`(含 WHERE 过滤、列映射)
- **UNION / UNION ALL** — 支持去重合并与链式组合(`A UNION B UNION C`),右侧结果按位置投影到左侧列结构
- **CREATE INDEX / DROP INDEX** — 动态二级索引:
- `CREATE [UNIQUE] INDEX idx ON table (col)` / `DROP INDEX idx ON table (col)`
- 五引擎实现:Memory 哈希索引 / IndexedDB 版本升级建 `idx_col` / OPFS 委托 / Hybrid 双写 / Aria 动态 LSM 索引
- Aria 主键索引受保护(不可 DROP);CREATE INDEX 对已有数据立即建索引(回填),后续写入自动同步
- **EXISTS / NOT EXISTS** — 支持关联子查询(`WHERE EXISTS (SELECT 1 FROM o WHERE o.user_id = u.id)`),逐行绑定外层上下文求值
- **别名 WHERE 修复** — `WHERE u.age > 20`(主表别名前缀)此前不工作,v0.3.0 字段名规范化(JOIN 路径不受影响)
- **`SELECT 1` 常量列** — 数字字面量可作为 SELECT 列(EXISTS 子查询常见用法)
- **benchmark 页面** — `site/benchmark.html` 浏览器内实测 Memory / Aria 引擎 × 1K/10K/50K 行的 INSERT/PK 查询/索引查询/UPDATE/DELETE 吞吐(ops/sec
- **`parseAll` / 新 AST 类型导出** — `SelectUnionStatement` / `CreateIndexStatement` / `DropIndexStatement` / `BeginTransactionStatement`
### Changed
- 版本号升至 v0.3.0
### Fixed
- `NOT EXISTS` 解析丢 EXISTS token 导致 parse 失败
---
## [0.2.6] - 2026-08-08
### Fixed
- **AriaEngine 二级索引 SSTable 互相覆盖(严重)** — 主 LSM 与二级索引 LSM 此前共享同一 `sstableStore`id 空间 + meta 列表),`allocateId()` 返回 `Date.now()` 且各 LSM 从同一 meta 恢复 `nextSSTableId`,高频写入时 id 冲突导致索引数据文件覆盖主数据、meta 与数据错配。v0.2.6 按命名空间隔离(文件前缀 + meta key + 独立 id 序列),索引 LSM 各持独立 store
- **LSM 多版本读取顺序错误(严重)** — `levels[]` 数组按 `push` 追加(新文件在尾部),但 `get()`/`rangeScan` 从头遍历(旧文件优先),同一 key 跨多次 flush 更新后读到旧值;`MergeIterator` 去重只取"先出堆者",不保证最新来源。v0.2.6 改为"数组头部即最新"(`unshift` + 按 id 降序加载),MergeIterator 显式取 sourceIndex 最小者
- **LZ4 往返不一致** — ① 匹配长度超过 19 字节被截断但 `si` 跳过全部匹配,解压丢数据;② matchField=0 的组合 token 与纯字面量 token 格式歧义;③ 字面量累积超过 15 字节后组合 token 永远无法输出;④ 输出缓冲区 `maxOut` 不足导致越界写入;⑤ 压缩失败时返回原样 input,解压端无法区分。v0.2.6 重写压缩器(长匹配分段、纯字面量 token 语义明确、动态上限)
- **React/Vue 集成运行时 ReferenceError** — `useDatabase`/`useSqlarkDatabase``import type` 导入 `MetonaSqlark` 但运行时 `new MetonaSqlark()``@ts-nocheck` 掩盖此 bug,实际使用必炸。改为 value import
- **sstableCache 无限增长** — SSTable 缓存无内存上限且 `init()` 预加载全部数据,大库 OOM。v0.2.6 引LRU 容量上限(`bufferPoolPages` × `pageSize`),查询前异步预加载兜底(修复缓存未命中静默返回 null 丢数据的隐患)
- **integrations 子路径缺失** — README 宣称 `metona-sqlark/react``metona-sqlark/vue` 可用但 `exports` 未声明,现已补齐(react/vue 为 peer dependencyrollup external
### Changed
- **Buffer Pool 配置真正生效** — `bufferPoolPages` 从无效配置变为 SSTable 缓存容量上限(默认 256 页 ≈ 1MB 可控内存)
- **LZ4 格式变更** — 旧版本压缩数据与新解压器不兼容,使用 `compression: true` 的用户需重新导入数据
- **SSTable 持久化串行化** — flush/compaction 改为串行链(`flushChain`),id 分配与持久化顺序一致,消除 fire-and-forget 竞态
- 文档数据修正 — 测试 761(41 套件)、覆盖率 81.1%(此前 91.0% 为排除 Aria 模块的陈旧数据)、体积 ~105KB / gzip ~27KB
### Added
- **AriaEngine 缓存内存上限测试** — 9 个测试:LRU 上限约束、驱逐后全表/PK/索引查询完整性、UPDATE/DELETE 全量作用、写路径上限、命名空间隔离回归、多版本回归、tombstone 回归
- **LZ4 往返一致性测试** — 6 个往返测试(重复/文本/随机/边界长度/15 字节边界/长匹配)
- **CryptoManager 加解密测试** — 7 个测试(往返/错误密码/不同 salt/实例隔离/全局兼容层/4KB 页面)
- **React/Vue hooks 集成测试** — 16 个测试(jest.mock 零依赖:mount 执行、错误路径、refresh、注入防护、数据库初始化)
- **jest.setup.js WebCrypto polyfill** — jsdom 环境补齐 `crypto.subtle` 支持 AES-GCM
---
## [0.2.5] - 2026-07-29
### Fixed
- **版本号统一** — `constants.ts` VERSION 从 `'0.2.0'` 更新为 `'0.2.5'`,修正 `index.ts`/`utils.ts`/`CONTRIBUTING.md` 中过时的版本注释和数据
- **AriaEngine OPFS 后端映射** — `core.ts``mode: 'aria'` + `diskEngine: 'opfs'` 时实际使用 Memory 后端的 bug 已修复
- **`_onError` 接入执行路径** — `query()`/`defineTable()`/`dropTable()`/`transaction()`/`importTable()` 的 catch 路径现在调用 `_onError` 全局错误回调
- **`maxRowsPerQuery` 生效** — `QueryExecutor` 构造时接收 `maxRowsPerQuery` 参数,SELECT 结果在返回前截断
- **WAL full 模式真正同步** — `WAL.append()` 改为 `async``full` 模式下 `await this.store.append()` 真正等待写入完成,不再 fire-and-forget
- **PluginManager.install 传 db 实例** — `register()` 增加可选 `db` 参数,`core.ts` 初始化时传入 `this`,插件可获取 db 引用
### Changed
- **SSTableReader 二分查找统一** — `locateBlockGE`/`locateBlockLE` 从线性扫描改为二分查找,rangeScan 性能在大型 SSTable 下不再退化
- **crypto 实例化** — 全局状态改为 `CryptoManager` 类,每个 AriaEngine 实例可拥有独立加密配置,保留全局函数向后兼容
- **compactLevelSync 接口公开化** — LSM 新增 public `compactLevel()` 方法,`vacuum()` 不再使用 `as any` 绕过 private 访问
- **WAL 大小阈值接入 checkpoint** — `CheckpointManager` 接收 `walSizeThreshold` 参数,WAL 缓冲超阈值时自动触发 checkpoint
### Added
- **ALTER TABLE 语法** — 支持 `ALTER TABLE ... ADD COLUMN` / `DROP COLUMN`(含可选 COLUMN 关键字)
- **TRUNCATE TABLE 语法** — 支持 `TRUNCATE TABLE name` 快速清空表数据
- **MVCC 接入读写路径** — 事务内 insert/update/delete 调用 `mvcc.writeVersion`/`mvcc.deleteVersion`,版本链作为 undo log
- **IndexedDB 索引利用** — `IndexedDBEngine.find` 等值查询时优先使用 IDB 索引(`idx_col` 命名约定),避免全量 getAll
- **SQL 注入防护** — React `useTable` / Vue `useSqlarkTable` 增加表名合法性校验(`/^[a-zA-Z_][a-zA-Z0-9_]*$/`
---
## [0.2.4] - 2026-07-27
### Added
- **二级索引** — 每列可独立维护 LSM Tree 索引,`$eq`/`$in`/`$gt`/`$lt` 走索引 O(log n)
- **MVCC 接入引擎** — MVCCManager 正式投产,替换 ad-hoc txnSnapshot
- **MVCC 自动 GC** — 每 10 次 checkpoint 自动回收过旧版本(保留最新 100 个)
- **Bloom Filter 序列化** — 写入 SSTable footer + 读取时加载 + 查询时 probe 快速否定
- **WAL 大小阈值** — `walSizeThreshold` 配置(默认 16MB),超阈值强制 checkpoint
- **内存预算** — `maxMemoryMB` 配置(默认 64MB
### Changed
- **WAL 默认同步模式** — `walSyncMode` `'batch'` 改为 `'full'`,消除 crash 丢数据风险
- **查询优化** — `tryIndexLookup` 扩展支持非 PK 列索引查找
---
## [0.2.3] - 2026-07-27
### Fixed
- **RB-Tree fixDelete 完整实现** — 补全标准红黑树删除修复,保证 O(log n)
- **LSM SSTable 缓存预热** — `init()` 预加载所有 SSTable,消除 cache miss
- **OPFS 数据恢复** — `open()` 自动从 OPFS 文件加载已有表数据到内存
- **Aria WAL 事务恢复** — 两阶段恢复:仅回放已提交事务,未提交数据不回放
- **LZ4 格式修复** — 重写 token 格式,消除中间字面量 token 歧义
---
## [0.2.2] - 2026-07-27
### Added
- **OPFS 自研存储后端** — AriaEngine 新增 `OPFSBackend`,纯浏览器文件系统 API,零 IndexedDB 依赖
- 每个 key 对应一个二进制文件,存储在 `navigator.storage.getDirectory()`
- 支持 read/write/delete/list/exists/clear 完整接口
- 页面文件(4KB)、WAL 日志、Schema、SSTable 全部存储为独立文件
- `{ dbName }/pg_1`, `{ dbName }/__wal_0`, `{ dbName }/__aria_schemas` ...
### Changed
- `AriaEngineConfig.storageBackend` 已支持 `'opfs'``AriaEngine.open()` 自动选择 OPFSBackend
---
## [0.2.1] - 2026-07-27
### Fixed
- **WAL CRC 校验验证** — 恢复时计算并验证 CRC32,损坏记录自动跳过并告警
- **Hybrid 引擎提交顺序** — commit 先写磁盘再写内存,磁盘失败回滚内存,消除数据不一致风险
- **RESTRICT 外键行为修正** — 检测到引用行时抛出 `FOREIGN_KEY_VIOLATION`,不再静默保留孤儿
- **SSTable rangeScan 边界保护** — 空索引或 startBlockIdx > endBlockIdx 时安全返回
### Added
- **ColumnDef 约束激活** — `maxLength`/`min`/`max` 约束在 `checkFieldType` 中正式生效
- **查询结果上限** — `DatabaseConfig.maxRowsPerQuery`(默认 0 不限制),防止超大结果集 OOM
- **onError 全局回调** — 配置中的 `onError` 回调实际接入 CRUD 异常路径
- **debug 调试模式** — `DatabaseConfig.debug: true` 输出 `[MetonaSqlark:name]` 前缀的结构化日志
- **浏览器兼容性声明** — README 增加 Chrome 80+/Firefox 80+/Safari 14+/Edge 80+ 支持矩阵
### Changed
- MemoryEngine RESTRICT 外键行为:从"不级联保留孤儿"改为"禁止删除抛异常"
- Hybrid 引擎 commitTransaction 磁盘优先于内存
- DB_DEFAULTS 新增 `maxRowsPerQuery: 0``debug: false`
---
## [0.2.0] - 2026-07-27
### Added
- **AriaEngine 自研存储引擎** — 基于 LSM-Tree 的页面式存储引擎,19 个新模块,~3500 行 TypeScript
- **页面格式层** (`page/`): 4KB Slotted Page 编解码、Tuple 二进制序列化、CRC32 校验
- **Buffer Pool** (`buffer/`): LRU 页面缓存 + 驱逐策略,可控内存占用
- **LSM-Tree 索引** (`index/`): MemTable (红黑树) + 多级 SSTable + Leveled Compaction
- **Bloom Filter** (`index/bloom.ts`): FNV-1a + Murmur 双哈希,快速键否定判定
- **SSTable 构建器/读取器** (`index/sstable_builder.ts`, `sstable.ts`): 二分查找 + 范围扫描
- **Merge Iterator** (`index/merge_iterator.ts`): 最小堆多路归并,去重保留最新
- **WAL** (`wal/`): 二进制日志格式 (LSN/type/txnId/table/key/json/CRC) + Checkpoint 管理
- **MVCC** (`transaction/mvcc.ts`): 版本链 + 快照隔离 + GC
- **存储后端** (`store/`): IndexedDB / Memory 双后端抽象
- **LZ4 压缩** (`compression/lz4.ts`): 简易页面级压缩
- **`mode: 'aria'`** — 新增存储模式,可通过 `MetonaSqlark.create({ mode: 'aria' })` 激活
- **Schema 持久化** — 表结构自动保存到 `__aria_schemas`,重启自动恢复
- **SSTable 元数据管理** — SSTable 索引信息持久化,启动时自动扫描加载
- **事务感知 CRUD** — insert/update/delete 在事务中缓冲到 snapshotcommit 批量写入 LSM
- **244 个 AriaEngine 专项测试** — 覆盖生命周期/表管理/CRUD/事务/持久化/SQL 集成/页面格式/LSM/压缩
### Changed
- `StorageMode` 类型新增 `'aria'`
- `STORAGE_MODES` 数组新增 `'aria'`
- `createEngine()` 支持 `mode: 'aria'` 分支
- 测试从 318 → **526**,套件从 20 → **27**
- 新增 7 个模块级测试文件:`aria-page``aria-index``aria-sstable``aria-buffer``aria-wal-mvcc``aria-compress``aria`
- LRUList 修复 size 追踪 bug
- WAL 存储改为按记录独立 key(避免拼接缓冲区越界)
- CheckpointManager 解耦 BufferPool 依赖
### Fixed
- **LZ4 压缩无限循环** — 字面量分支在发现匹配后回退导致 litLen=0 死循环,CI 卡死根因
- **SSTableReader.get() 自比较 bug** — 参数 key 被循环变量遮蔽导致永远返回第一条
- **CheckpointManager 测试 null 引用** — 改为 Mock 对象避免 TypeError 导致进程无法退出
- **IndexedDB 持久化测试** — 替换为 Memory Backend 验证,消除 fake-indexeddb timer 堆积
- **LSM.flush() 不必要 setTimeout** — 替换为 Promise.resolve(),消除额外 timer 延迟
---
## [0.1.14] - 2026-07-26
### Fixed
- **IndexedDB 事务原子性**: `flushToIDB` 改为单 IDB 事务包裹 clear+insert,消除崩溃丢数据风险
- **多标签页冲突**: `open()` 添加 `onversionchange` 监听,其他标签页升级版本时自动关闭过期连接
- **Memory 引擎幂等**: `open()` 重复调用安全无副作用
- 关闭时清理 `onversionchange` 监听器,防止内存泄漏
---
## [0.1.13] - 2026-07-26
### Added
- **事务回滚机制**: `IStorageEngine` 新增 `beginTransaction/commitTransaction/rollbackTransaction` 接口
- MemoryEngine: 快照式回滚(深拷贝 tables/schemas/indexes
- IndexedDBEngine: 延迟写入策略(事务中仅写内存,commit 批量刷 IDB)
- OPFSEngine: 快照式回滚(commit 批量写文件)
- HybridEngine: 同时代理内存+磁盘引擎事务
- **子查询支持**: SQL Parser + Executor 支持 `IN (SELECT ...)``op (SELECT ...)` 子查询
- AST 新增 `SubqueryExpression` 类型
- Parser 在 IN 和比较运算符后检测子查询
- Executor 新增 `resolveSubqueries()` 递归解析,自动执行子查询并替换为具体值
- **外键级联操作**: ColumnDef 新增 `onDelete/onUpdate` 选项
- 支持 `CASCADE`(递归级联删除)、`SET NULL``RESTRICT`
- SQL Parser 解析 `REFERENCES table(col) ON DELETE CASCADE ON UPDATE CASCADE`
- MemoryEngine 内置 `cascadeDelete()` 递归级联逻辑
- **连接池管理**: `MetonaSqlark.connect()` 静态方法
- 同名数据库复用已打开实例,引用计数管理
- `db.disconnect()` 释放连接,归零自动 close
- `MetonaSqlark.disconnectAll()` 强制关闭所有连接
### Changed
- `TransactionManager.execute()` 调用引擎层 begin/commit/rollback 实现真正原子性
- IndexedDBEngine CRUD 事务感知:活跃事务中延迟 IDB 写入
- ASTColumnDef / astColumnToColumnDef 支持 references/onDelete/onUpdate 透传
### Fixed
- IndexedDBEngine 事务中 find/count 从内存缓存读取(保证读到未提交变更)
- SQL Parser parseColumnDef 循环条件扩展,避免 REFERENCES 语法解析中断
---
## [0.1.12] - 2026-07-26
### Added
- `MeSqlark` 别名导出,与 `MetonaSqlark` 完全等价
- `metona-sqlark.cjs.js` CommonJS 构建产物
- 完善的 `where-matcher.ts` 消除 220+ 行重复代码
- `$col` 列引用支持 JOIN ON 条件
- `$and/$or` 嵌套支持(字段级 + 顶层)
- `$not` 逻辑非操作符
- `$like` 正则缓存加速
- `projectColumns` 列投影支持 `table.column` 格式
- `DISTINCT` 去重支持(O(n) 时间,列值拼接优化)
- 测试覆盖率提升至 93.46%(264 测试/15 套件)
### Changed
- `where-matcher` 统一 MemoryEngine / IndexedDBEngine / Executor 的 WHERE 逻辑
- JOIN ON 匹配支持 `$col` 语法
- LIKE 正则改为缓存式编译
- Executor DISTINCT 用列值拼接代替 JSON.stringify
- JOIN 嵌套循环连接优化:避免 ON 时对象扩散
### Fixed
- IndexedDBEngine `update/delete` 方法正确同步内存缓存
- OPFSEngine `getTableNames` 兼容 `entries()` 返回值
- SQL Parser 正确处理字符串转义字符
- SQL Parser 正确处理表别名(无 AS 关键字)
- SQL Parser 解析 `IS NULL` / `IS NOT NULL` / `NOT LIKE`
- `peekTokenIs` 方法正确暴露给语法分析
- 多处边界条件空值/假值处理
---
## [0.1.11] - 2026-07-25
### Added
- React 集成 hooks: `useQuery`, `useTable`, `useDatabase`
- Vue 集成 composables: `useSqlarkQuery`, `useSqlarkTable`, `useSqlarkDatabase`
- 发布订阅系统: `subscribe()`, `emit()`
- 数据迁移系统: `addMigration()`, `migrateTo()`
- 导入导出: `exportTable()`, `exportAll()`, `importTable()`
- 完整 SQL 解析器: `Lexer` + `Parser`(递归下降)
- SQL 支持: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
- JOIN 支持: INNER, LEFT, RIGHT, CROSS
- GROUP BY + HAVING + 聚合函数 (COUNT, SUM, AVG, MIN, MAX)
- DISTINCT, ORDER BY, LIMIT, OFFSET
- WHERE 条件: AND, OR, NOT, IN, LIKE, IS NULL
- Query Builder 链式 API: `select().where().orderBy().limit().execute()`
- 插件系统: 14 种生命周期钩子 + PluginManager
- Metadata 系统: ColumnDef 完整约束 (type/primaryKey/required/unique/index/default/references/maxLength/min/max)
### Changed
- 架构重构为分层设计:Engine → QueryExecutor → Table/QueryBuilder → SQL Parser → Public API
- AST 作为统一中间表示,SQL 和 QueryBuilder 行为完全一致
- 测试框架完善:core / edge / groupby / join / query-system / sql / table / engine / transaction / plugin / hybrid 全覆盖
---
## [0.0.1] - 2026-07-24
### Added
- 初始项目骨架
- TypeScript 配置(严格模式)
- Rollup 构建(UMD / ESM / CJS / min / .d.ts
- Jest + jsdom 测试环境
- ESLint + @typescript-eslint
- Gitea Actions CI 工作流
- 示例站点(index / demo / docs
- 构建脚本 build.sh
+13 -4
View File
@@ -43,11 +43,20 @@ src/
├── core.ts # MetonaSqlark main class
├── constants.ts # Types, defaults, enums, errors
├── utils.ts # Utility functions
├── connection-manager.ts # Connection pool (connect/disconnect)
├── engine/ # Storage engines
│ ├── interface.ts # IStorageEngine interface
│ ├── memory.ts # MemoryEngine (Map-based)
│ ├── indexeddb.ts # IndexedDBEngine (browser persistence)
── opfs.ts # OPFSEngine (Origin Private File System)
── opfs.ts # OPFSEngine (Origin Private File System)
│ └── aria/ # AriaEngine (LSM-Tree page storage engine)
│ ├── index/ # LSM / MemTable / SSTable / Bloom / MergeIterator
│ ├── page/ # 4KB slotted page format
│ ├── buffer/ # Buffer Pool (LRU eviction)
│ ├── wal/ # Write-Ahead Log + Checkpoint
│ ├── transaction/ # MVCC manager
│ ├── store/ # Backends (IndexedDB / OPFS / Memory)
│ └── compression/ # LZ4
├── hybrid/ # HybridEngine (write-through)
├── table/ # Table management & Schema validation
├── query/ # Query system
@@ -64,7 +73,7 @@ src/
├── plugin/ # Plugin system (14 lifecycle hooks)
└── integrations/ # React & Vue hooks
tests/ # Test suite (701+ test cases, 32 test suites)
tests/ # Test suite (761+ test cases, 41 test suites)
site/ # Documentation site (index / docs / demo)
```
@@ -133,9 +142,9 @@ npm run build
# Output in dist/
# ├── metona-sqlark.js UMD
# ├── metona-sqlark.min.js UMD minified (~42KB / ~10KB gzip)
# ├── metona-sqlark.min.js UMD minified (~105KB / ~27KB gzip)
# ├── metona-sqlark.esm.js ES Module
# ├── metona-sqlark.cjs.js CommonJS
# ├── metona-sqlark.cjs CommonJS
# └── metona-sqlark.d.ts TypeScript declarations
```
+34 -13
View File
@@ -1,10 +1,10 @@
# MetonaSqlark
<p align="center">
<img src="https://img.shields.io/badge/version-0.2.5-blue?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/version-0.3.2-blue?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
<img src="https://img.shields.io/badge/coverage-91.0%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-721%20passed-success?style=flat-square" alt="tests">
<img src="https://img.shields.io/badge/coverage-81.0%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-761%20passed-success?style=flat-square" alt="tests">
</p>
> 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研页面式存储引擎**。
@@ -18,13 +18,13 @@
- 🔒 **生产级数据安全** — WAL CRC 完整性校验、`RESTRICT` 外键约束、Hybrid 提交原子性、SQL 注入防护
- 🛡 **输入校验全覆盖**`maxLength`/`min`/`max` 约束、类型检查、必填验证
- 💾 **多引擎架构** — Memory / IndexedDB / OPFS / Hybrid(write-through) / Aria 五种模式
- 📝 **完整 SQL 支持** — SELECT/JOIN/子查询/GROUP BY/HAVING/ORDER BY/LIMIT/BETWEEN/IF NOT EXISTS/ALTER TABLE/TRUNCATE TABLE
- 📝 **完整 SQL 支持** — SELECT/JOIN/子查询/GROUP BY/HAVING/ORDER BY/LIMIT/BETWEEN/IF NOT EXISTS/ALTER TABLE/TRUNCATE TABLE/UNION/INSERT...SELECT/事务语句/CREATE INDEX/EXISTSv0.3.0
- 🔗 **Query Builder API** — 链式 `.select().where().orderBy().limit().execute()`
- 🔄 **事务回滚** — Memory/IndexedDB/Hybrid/Aria 四引擎事务原子性,自动回滚,MVCC 版本链接入读写路径
- 🌲 **RB-Tree 完整实现** — 标准红黑树插入+删除修复,O(log n) 保证
-**性能优化** — SSTableReader 二分查找统一、IndexedDB 索引利用、crypto 实例化避免全局状态
- 🌐 **浏览器兼容** — Chrome 80+ / Firefox 80+ / Safari 14+ / Edge 80+ / Node.js 16+
- 🧪 **721 测试 · 91.0% 覆盖率**37 套件,生产级质量保证
- 🧪 **761 测试 · 81.0% 覆盖率**41 套件,生产级质量保证
---
@@ -48,9 +48,9 @@ npm install @metona-team/metona-sqlark
或从 [`dist/`](./dist/) 目录下载:
- `metona-sqlark.js` — UMD 开发版(含 sourcemap
- `metona-sqlark.min.js` — UMD 压缩版(~42KB
- `metona-sqlark.min.js` — UMD 压缩版(~105KBgzip ~27KB
- `metona-sqlark.esm.js` — ES Module
- `metona-sqlark.cjs.js` — CommonJS
- `metona-sqlark.cjs` — CommonJS
- `metona-sqlark.d.ts` — TypeScript 类型声明
---
@@ -98,7 +98,7 @@ import { MetonaSqlark } from '@metona-team/metona-sqlark';
const db = await MetonaSqlark.create({
name: 'my-app',
mode: 'hybrid', // 'memory' | 'disk' | 'hybrid'
mode: 'hybrid', // 'memory' | 'disk' | 'hybrid' | 'aria'
diskEngine: 'indexeddb', // 'indexeddb' | 'opfs'
});
@@ -144,6 +144,27 @@ await db.query('ALTER TABLE users DROP COLUMN phone');
// TRUNCATE TABLE — 快速清空表 v0.2.5
await db.query('TRUNCATE TABLE old_logs');
// v0.3.0 — SQL 功能扩展
// 多语句(分号分隔)
await db.query("CREATE TABLE t (id STRING PRIMARY KEY); INSERT INTO t VALUES ('1'); INSERT INTO t VALUES ('2')");
// 事务语句
await db.query('BEGIN');
await db.query("INSERT INTO t VALUES ('3')");
await db.query('ROLLBACK'); // 回滚
// INSERT INTO ... SELECT
await db.query('INSERT INTO t SELECT id FROM t2 WHERE x > 1');
// UNION / UNION ALL
const rows = await db.query('SELECT name FROM users WHERE city = \'Beijing\' UNION SELECT name FROM users WHERE age < 30');
// 动态索引
await db.query('CREATE INDEX idx_users_city ON users (city)');
await db.query('DROP INDEX idx_users_city ON users (city)');
// EXISTS 关联子查询
const hasOrders = await db.query('SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)');
// v0.3.1 — CASE WHEN / JOIN 关联子查询 / 组提交
const labeled = await db.query("SELECT name, CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users");
const joinExists = await db.query('SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id AND o2.amount > 150)');
// 事务 — 自动回滚 v0.1.13
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '3', name: 'Charlie' });
@@ -278,7 +299,7 @@ const { data, loading, refresh } = useSqlarkQuery(db, 'SELECT * FROM users');
---
## 🌲 AriaEngine — 自研存储引擎 (v0.2.5)
## 🌲 AriaEngine — 自研存储引擎
AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念:
@@ -326,7 +347,7 @@ const rows = await db.query('SELECT * FROM users');
| **LSM-Tree** | MemTable (红黑树) → SSTable 多级索引,异步 Compaction,写背压 |
| **WAL** | Write-Ahead Log 二进制格式,CRC 校验,full/batch/none 三种模式(full 模式真正同步 ✅ v0.2.5),16MB 阈值自动 checkpoint |
| **MVCC** | 版本链 + 快照隔离,事务读写不互斥,自动 GC(每10次检查点),读写路径接入版本链 ✅ v0.2.5 |
| **Buffer Pool** | FileManager + LRU 页面缓存,256 页 ≈ 1MB 可控内存 |
| **Buffer Pool** | SSTable 缓存 LRU 上限(`bufferPoolPages` × `pageSize`,默认 256 页 ≈ 1MB 可控内存)✅ v0.2.6 生效,查询前异步预加载兜底,缓存驱逐不丢数据 |
| **Bloom Filter** | FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时 probe |
| **二级索引** | 每列独立 LSM Tree,支持 $eq/$in/$gt/$lt 范围扫描,SSTableReader 二分查找统一 ✅ v0.2.5 |
| **AES-GCM** | PBKDF2 密钥派生 + AES-256-GCM 页面级加密,CryptoManager 实例化 ✅ v0.2.5 |
@@ -352,9 +373,9 @@ npm run typecheck # 类型检查
| 指标 | 数值 |
|------|------|
| 测试用例 | 721 |
| 测试套件 | 37 |
| 行覆盖率 | 91.0% |
| 测试用例 | 761 |
| 测试套件 | 41 |
| 行覆盖率 | 81.0% |
| SQL 关键字 | 36 |
| 存储引擎 | 5Memory / IndexedDB / OPFS / Hybrid / **Aria** |
+9 -9
View File
@@ -1,9 +1,9 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }],
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-transform-modules-commonjs',
],
};
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }],
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-transform-modules-commonjs',
],
};
File diff suppressed because it is too large Load Diff
-1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+152 -7
View File
@@ -62,6 +62,8 @@ interface DatabaseConfig {
maxRowsPerQuery?: number;
/** 调试模式(启用后输出详细操作日志) */
debug?: boolean;
/** 多标签页同步(v0.3.2):BroadcastChannel 广播表变更,其他标签页自动刷新 */
multiTabSync?: boolean;
}
/** Where 条件操作符 */
type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
@@ -114,7 +116,7 @@ interface MetonaPlugin {
/** 销毁 */
destroy(): void;
}
declare const VERSION = "0.2.5";
declare const VERSION = "0.3.2";
/**
* metona-sqlark Plugin — 插件系统
@@ -179,6 +181,10 @@ interface IStorageEngine {
count(tableName: string, query?: QueryPlan): Promise<number>;
/** 清空表数据(保留结构) */
clear(tableName: string): Promise<void>;
/** 创建二级索引(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>;
/** 提交事务 */
@@ -254,7 +260,10 @@ interface InsertStatement {
type: 'INSERT';
into: string;
columns?: string[];
values: unknown[][];
/** VALUES 字面量 */
values?: unknown[][];
/** INSERT INTO ... SELECT ...v0.3.0 */
select?: SelectStatement | SelectUnionStatement;
}
interface UpdateStatement {
type: 'UPDATE';
@@ -285,6 +294,15 @@ interface SelectStatement {
limit?: number;
offset?: number;
}
interface SelectUnionStatement {
type: 'SELECT_UNION';
/** 左操作数(可以是 SELECT 或嵌套 UNION */
left: SelectStatement | SelectUnionStatement;
/** 右操作数 */
right: SelectStatement | SelectUnionStatement;
/** UNION ALL 不去重 */
all?: boolean;
}
interface AlterTableStatement {
type: 'ALTER_TABLE';
name: string;
@@ -295,7 +313,31 @@ interface TruncateTableStatement {
type: 'TRUNCATE_TABLE';
name: string;
}
type Statement = SelectStatement | ExplainStatement | InsertStatement | UpdateStatement | DeleteStatement | CreateTableStatement | DropTableStatement | AlterTableStatement | TruncateTableStatement;
interface CreateIndexStatement {
type: 'CREATE_INDEX';
/** 索引名(语法占位) */
name: string;
table: string;
column: string;
/** UNIQUE 索引 */
unique?: boolean;
}
interface DropIndexStatement {
type: 'DROP_INDEX';
name: string;
table: string;
column: string;
}
interface BeginTransactionStatement {
type: 'BEGIN';
}
interface CommitTransactionStatement {
type: 'COMMIT';
}
interface RollbackTransactionStatement {
type: 'ROLLBACK';
}
type Statement = SelectStatement | SelectUnionStatement | ExplainStatement | InsertStatement | UpdateStatement | DeleteStatement | CreateTableStatement | DropTableStatement | AlterTableStatement | TruncateTableStatement | CreateIndexStatement | DropIndexStatement | BeginTransactionStatement | CommitTransactionStatement | RollbackTransactionStatement;
/**
* metona-sqlark Query Executor — AST 执行器
@@ -311,11 +353,23 @@ declare class QueryExecutor {
/** 设置查询结果行数上限 */
setMaxRowsPerQuery(max: number): void;
execute(stmt: Statement): Promise<unknown>;
/** 递归执行 UNION / UNION ALL,返回合并结果 */
private executeSelectUnion;
private executeSelectPart;
/** 将 UNION 右侧行投影为左侧列结构(按位置取值) */
private projectUnionRow;
/** EXPLAIN: 输出查询计划 */
private executeExplain;
private executeSelect;
private executeJoinSelect;
private prefixRow;
/**
* 哈希连接(v0.3.2):ON 为单一等值条件且右表列为索引/主键时,
* 收集左表连接值 → 一次 $in 查询右表 → 哈希映射匹配。
* 替代嵌套循环,大表 INNER/LEFT JOIN 复杂度 O(N + M)。
* 不适用时返回 null(回退嵌套循环)。
*/
private tryHashJoin;
/** 嵌套循环连接(优化:避免 ON 时对象扩散) */
private joinRows;
private executeGroupBy;
@@ -328,14 +382,47 @@ declare class QueryExecutor {
private executeDropTable;
private executeAlterTable;
private executeTruncateTable;
private executeCreateIndex;
private executeDropIndex;
private executeBegin;
private executeCommit;
private executeRollback;
/** 列列表是否包含 CASE WHEN 表达式 */
private hasCaseColumn;
/** WHERE 是否包含 CASE WHEN 表达式键 */
private whereHasCase;
getEngine(): IStorageEngine;
/**
* 列投影(v0.3.1):普通列走 projectColumnsCASE WHEN 表达式逐行求值
*/
private projectRow;
/** 检查 SELECT 列列表中是否包含聚合函数 */
private _hasAggregateColumn;
/** 计算单行聚合结果(无 GROUP BY) */
private computeSingleAggregate;
/** 剥离主表别名前缀:'u.id' → 'id'(键与 $col 值均处理,支持多层别名) */
private normalizeWhereColumns;
private normalizeExistsValue;
private normalizeFieldValue;
private stripAlias;
/** WHERE 是否含关联引用($col 或关联 EXISTS)或 CASE WHEN 表达式键 */
private hasCorrelatedRefs;
private fieldHasColRef;
/** 移除关联 EXISTS 标记(引擎层先执行无 EXISTS 条件的查询) */
private stripCorrelatedExists;
/** 逐行绑定外层行上下文,求值关联 EXISTS、$col 引用与 CASE WHEN 键 */
private filterCorrelated;
/** 将 WHERE 中的 CASE WHEN 表达式键求值为布尔条件($caseResult */
private resolveCaseKeys;
/** CASE 求值结果与操作符条件比较 */
private caseConditionMatches;
/** 将 where 中的 $col 引用替换为上下文行值 */
private bindColumnRefs;
private bindWhereRefs;
/**
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
* 将结果替换为具体值。
* @param contextRow 关联子查询的外层行上下文(用于绑定 $col 引用)
*/
private resolveSubqueries;
/**
@@ -395,7 +482,8 @@ declare class UpdateQueryBuilder {
private tableName;
private _updates;
private _where;
constructor(engine: IStorageEngine, tableName: string, _updates: Record<string, unknown>);
private onWrite?;
constructor(engine: IStorageEngine, tableName: string, _updates: Record<string, unknown>, onWrite?: (table: string) => void);
where(condition: WhereCondition): this;
execute(): Promise<number>;
toAST(): UpdateStatement;
@@ -404,7 +492,8 @@ declare class DeleteQueryBuilder {
private engine;
private tableName;
private _where;
constructor(engine: IStorageEngine, tableName: string);
private onWrite?;
constructor(engine: IStorageEngine, tableName: string, onWrite?: (table: string) => void);
where(condition: WhereCondition): this;
execute(): Promise<number>;
toAST(): DeleteStatement;
@@ -420,7 +509,9 @@ declare class Table<T = Record<string, unknown>> {
private engine;
private schema;
private executor;
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor);
/** 写入回调(多标签页广播,v0.3.2) */
private onWrite?;
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor, onWrite?: (table: string) => void);
getSchema(): Promise<TableSchema>;
insert(row: T & Record<string, unknown>): Promise<string>;
insertMany(rows: (T & Record<string, unknown>)[]): Promise<string[]>;
@@ -472,6 +563,8 @@ declare class MetonaSqlark {
get maxRowsPerQuery(): number;
/** 调试模式 */
get debug(): boolean;
/** 多标签页同步通道(v0.3.2 */
private channel;
constructor(config: DatabaseConfig);
/** 初始化数据库(创建引擎、打开连接) */
init(): Promise<void>;
@@ -500,12 +593,18 @@ declare class MetonaSqlark {
subscribe(tableName: string, callback: (event: {
type: string;
row?: unknown;
table?: string;
}) => void): () => void;
/** 触发变更事件 */
emit(tableName: string, event: {
type: string;
row?: unknown;
table?: string;
}): void;
/** 广播表变更到其他标签页(多标签页同步) */
broadcastChange(tableName: string): void;
/** 写语句对应的表名(多标签页广播用) */
private writeStatementTable;
private migrations;
/** 注册迁移 */
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void;
@@ -553,6 +652,8 @@ declare class MemoryEngine implements IStorageEngine {
delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>;
createIndex(tableName: string, column: string, unique?: boolean): Promise<void>;
dropIndex(tableName: string, column: string, _indexName?: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
@@ -590,6 +691,12 @@ declare class IndexedDBEngine implements IStorageEngine {
private memoryCache;
private txActive;
open(dbName: string, version: number): Promise<void>;
/**
* 从 IDB 恢复内存 schema
* 1. 优先读取持久化的 schema 记录('__metona_schema' storev0.3.2
* 2. 旧数据回退:从 objectStore 主键 / 索引 / 样例数据推断
*/
private rebuildSchemaFromIDB;
close(): Promise<void>;
isOpen(): boolean;
createTable(schema: TableSchema): Promise<void>;
@@ -603,6 +710,8 @@ declare class IndexedDBEngine implements IStorageEngine {
delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>;
createIndex(tableName: string, column: string, unique?: boolean): Promise<void>;
dropIndex(tableName: string, column: string, _indexName?: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
@@ -648,6 +757,8 @@ declare class OPFSEngine implements IStorageEngine {
delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>;
createIndex(tableName: string, column: string, unique?: boolean): Promise<void>;
dropIndex(tableName: string, column: string, indexName?: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
@@ -727,6 +838,8 @@ declare class AriaEngine implements IStorageEngine {
delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>;
createIndex(tableName: string, column: string, unique?: boolean): Promise<void>;
dropIndex(tableName: string, column: string, _indexName?: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
@@ -741,6 +854,14 @@ declare class AriaEngine implements IStorageEngine {
private checkType;
private persistSchemas;
private loadSchemas;
/**
* 创建命名空间隔离的 SSTableStore。
*
* 主 LSM 与每个二级索引 LSM 各持有独立实例:
* - 文件 key 前缀隔离(sst_ / sst_idx_${table}_${col}_
* - 元数据 key 隔离(__aria_lsm_meta / __aria_lsm_meta_${ns}
* - id 序列独立(避免 v0.2.4 共享 id 空间导致的文件互相覆盖)
*/
private createSSTableStore;
private applyWALRecord;
/** 更新行的二级索引条目 */
@@ -751,6 +872,8 @@ declare class AriaEngine implements IStorageEngine {
private indexScanToRows;
/** 每 10 次 gc 计数器触发一次 MVCC 垃圾回收 */
private tryGC;
/** 回收主 LSM 与所有二级索引 LSM 的临时缓存超限 */
private trimAllCaches;
/** 检查内存预算,超出时强制 flush + GC */
private checkMemoryBudget;
/** 估算 WAL 大小(字节) */
@@ -801,8 +924,15 @@ declare class HybridEngine implements IStorageEngine {
private memoryEngine;
private diskEngine;
private diskEngineType;
private dbName;
private version;
constructor(diskEngine?: DiskEngine);
open(dbName: string, version: number): Promise<void>;
/**
* 从磁盘重载内存缓存(v0.3.2:多标签页同步)。
* 其他标签页写入磁盘后调用,使本标签页读到最新数据。
*/
reloadMemoryFromDisk(): Promise<void>;
close(): Promise<void>;
isOpen(): boolean;
createTable(schema: TableSchema): Promise<void>;
@@ -816,6 +946,8 @@ declare class HybridEngine implements IStorageEngine {
delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>;
createIndex(tableName: string, column: string, unique?: boolean): Promise<void>;
dropIndex(tableName: string, column: string, indexName?: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
@@ -835,6 +967,8 @@ declare class HybridEngine implements IStorageEngine {
/** 解析 SQL 字符串为 AST Statement */
declare function parse(sql: string): Statement;
/** 解析 SQL 字符串为 AST Statement 数组(分号分隔的多语句支持,v0.3.0) */
declare function parseAll(sql: string): Statement[];
/**
* metona-sqlark SQL Token Types — 词法单元定义
@@ -895,6 +1029,17 @@ declare enum TokenType {
MIN = "MIN",
MAX = "MAX",
DISTINCT = "DISTINCT",
BEGIN = "BEGIN",
COMMIT = "COMMIT",
ROLLBACK = "ROLLBACK",
UNION = "UNION",
ALL = "ALL",
INDEX = "INDEX",
CASE = "CASE",
WHEN = "WHEN",
THEN = "THEN",
ELSE = "ELSE",
END = "END",
IDENTIFIER = "IDENTIFIER",
STRING = "STRING",
NUMBER = "NUMBER",
@@ -1028,4 +1173,4 @@ declare global {
declare const MeSqlark: typeof MetonaSqlark;
export { AriaEngine, AriaEngineConfig, ColumnDef, DatabaseConfig, DeleteStatement, DiskEngine, FieldType, HybridEngine, IStorageEngine, IndexedDBEngine, InsertStatement, MeSqlark, MemoryEngine, MetonaSqlark, OPFSBackend, OPFSEngine, SelectStatement, Statement, StorageMode, Table, TableSchema, UpdateStatement, VERSION, api, create, api as default, parse, tokenize };
export { AriaEngine, AriaEngineConfig, ColumnDef, DatabaseConfig, DeleteStatement, DiskEngine, FieldType, HybridEngine, IStorageEngine, IndexedDBEngine, InsertStatement, MeSqlark, MemoryEngine, MetonaSqlark, OPFSBackend, OPFSEngine, SelectStatement, Statement, StorageMode, Table, TableSchema, UpdateStatement, VERSION, api, create, api as default, parse, parseAll, tokenize };
+2055 -591
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+2055 -590
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+26 -26
View File
@@ -1,26 +1,26 @@
module.exports = {
testEnvironment: 'jsdom',
setupFiles: ['./jest.setup.js'],
transform: {
'^.+\\.ts$': 'babel-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!(@rollup)/)',
],
moduleFileExtensions: ['ts', 'js', 'json'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/index.ts',
'!src/**/index.ts',
'!src/query/ast.ts',
'!src/engine/interface.ts',
'!src/engine/opfs.ts',
'!src/integrations/**',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
verbose: true,
// Must have: fake-indexeddb + debounce timers keep event loop alive
forceExit: true,
testTimeout: 15000,
};
module.exports = {
testEnvironment: 'jsdom',
setupFiles: ['./jest.setup.js'],
transform: {
'^.+\\.ts$': 'babel-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!(@rollup)/)',
],
moduleFileExtensions: ['ts', 'js', 'json'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/index.ts',
'!src/**/index.ts',
'!src/query/ast.ts',
'!src/engine/interface.ts',
'!src/engine/opfs.ts',
'!src/integrations/**',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
verbose: true,
// Must have: fake-indexeddb + debounce timers keep event loop alive
forceExit: true,
testTimeout: 15000,
};
+27 -15
View File
@@ -1,15 +1,27 @@
// jest setup: polyfill structuredClone for fake-indexeddb
if (typeof globalThis.structuredClone !== 'function') {
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
}
// polyfill TextEncoder/TextDecoder for jsdom environment
if (typeof globalThis.TextEncoder === 'undefined') {
const { TextEncoder: TE, TextDecoder: TD } = require('util');
globalThis.TextEncoder = TE;
globalThis.TextDecoder = TD;
}
if (typeof globalThis.TextDecoder === 'undefined') {
const { TextDecoder: TD } = require('util');
globalThis.TextDecoder = TD;
}
// jest setup: polyfill structuredClone for fake-indexeddb
if (typeof globalThis.structuredClone !== 'function') {
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
}
// polyfill TextEncoder/TextDecoder for jsdom environment
if (typeof globalThis.TextEncoder === 'undefined') {
const { TextEncoder: TE, TextDecoder: TD } = require('util');
globalThis.TextEncoder = TE;
globalThis.TextDecoder = TD;
}
if (typeof globalThis.TextDecoder === 'undefined') {
const { TextDecoder: TD } = require('util');
globalThis.TextDecoder = TD;
}
// polyfill WebCrypto (crypto.subtle) — jsdom 仅提供 getRandomValues
if (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.subtle === 'undefined') {
const { webcrypto } = require('crypto');
globalThis.crypto = webcrypto;
// jsdom 环境暴露 getRandomValues 的旧引用可能被覆盖,统一替换
Object.defineProperty(globalThis, 'crypto', {
value: webcrypto,
writable: true,
configurable: true,
});
}
+8111 -8111
View File
File diff suppressed because it is too large Load Diff
+13 -5
View File
@@ -1,17 +1,25 @@
{
"name": "@metona-team/metona-sqlark",
"version": "0.2.5",
"version": "0.3.2",
"description": "Frontend SQL database with in-memory and disk dual-mode storage",
"type": "module",
"main": "dist/metona-sqlark.js",
"module": "src/index.ts",
"main": "dist/metona-sqlark.cjs",
"module": "dist/metona-sqlark.esm.js",
"unpkg": "dist/metona-sqlark.min.js",
"jsdelivr": "dist/metona-sqlark.min.js",
"types": "dist/metona-sqlark.d.ts",
"exports": {
".": {
"import": "./src/index.ts",
"require": "./dist/metona-sqlark.js",
"import": "./dist/metona-sqlark.esm.js",
"require": "./dist/metona-sqlark.cjs",
"types": "./dist/metona-sqlark.d.ts"
},
"./react": {
"import": "./src/integrations/react.ts",
"types": "./dist/metona-sqlark.d.ts"
},
"./vue": {
"import": "./src/integrations/vue.ts",
"types": "./dist/metona-sqlark.d.ts"
}
},
+7 -2
View File
@@ -40,6 +40,8 @@ export default [
exports: 'named',
sourcemap: true,
},
// react/vue 为 peer dependencyintegrations 子路径),不打包
external: ['react', 'vue'],
plugins: [...basePlugins, ...devPlugins],
},
// Only build all formats in production
@@ -53,17 +55,19 @@ export default [
exports: 'named',
sourcemap: true,
},
external: ['react', 'vue'],
plugins: basePlugins,
},
// CommonJS
// CommonJS.cjs 后缀:package.json 为 type:module.js 会被 Node 按 ESM 解析)
{
input: 'src/index.ts',
output: {
file: 'dist/metona-sqlark.cjs.js',
file: 'dist/metona-sqlark.cjs',
format: 'cjs',
exports: 'named',
sourcemap: true,
},
external: ['react', 'vue'],
plugins: basePlugins,
},
// UMD minified
@@ -76,6 +80,7 @@ export default [
exports: 'named',
sourcemap: false,
},
external: ['react', 'vue'],
plugins: [
...basePlugins,
terser({
+266
View File
@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚡ 性能基准 — MetonaSqlark v0.3.2</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style>
:root {
--bg:#0a0a0f; --surface:#0d1117; --surface2:#161b22; --border:#30363d;
--primary:#6366f1; --accent:#06b6d4; --accent2:#ec4899; --green:#22c55e;
--text:#e2e8f0; --text2:#8b949e; --radius:12px;
--gradient:linear-gradient(135deg,#6366f1,#06b6d4,#ec4899);
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Inter',-apple-system,system-ui,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
header {
height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 24px;
background:var(--surface); border-bottom:1px solid var(--border); position:sticky; top:0; z-index:10;
}
.logo { font-weight:800; font-size:1.1rem; display:flex; align-items:center; gap:10px; }
.logo .icon { width:28px; height:28px; border-radius:7px; background:var(--gradient); display:flex; align-items:center; justify-content:center; font-size:0.85rem; font-weight:900; color:#fff; }
.logo span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
nav { display:flex; gap:20px; }
nav a { color:var(--text2); text-decoration:none; font-size:0.88rem; transition:.2s; }
nav a:hover,.nav-active { color:var(--text); }
.container { max-width:1000px; margin:0 auto; padding:32px 24px 60px; }
.hero { margin-bottom:28px; }
.hero h1 { font-size:1.6rem; margin-bottom:8px; }
.hero p { color:var(--text2); font-size:0.92rem; line-height:1.7; }
.hero p code { background:var(--surface2); padding:2px 6px; border-radius:5px; font-size:0.85em; }
.controls { display:flex; gap:12px; align-items:center; margin-bottom:24px; flex-wrap:wrap; }
.btn {
display:inline-flex; align-items:center; gap:6px; padding:10px 22px; border-radius:8px;
font-weight:600; font-size:0.85rem; cursor:pointer; border:none; transition:.25s; font-family:inherit;
}
.btn-run { background:var(--gradient); color:#fff; box-shadow:0 4px 15px rgba(99,102,241,0.35); }
.btn-run:hover { transform:translateY(-1px); box-shadow:0 6px 25px rgba(99,102,241,0.5); }
.btn-run:active { transform:scale(0.97); }
.btn-run:disabled { opacity:0.5; cursor:not-allowed; transform:none; }
.btn-small { background:var(--surface2); color:var(--text2); border:1px solid var(--border); font-size:0.8rem; padding:8px 16px; }
.btn-small:hover { color:var(--text); border-color:var(--text2); }
select {
background:var(--surface2); color:var(--text); border:1px solid var(--border); border-radius:8px;
padding:9px 12px; font-size:0.85rem; font-family:inherit; outline:none;
}
.status { font-size:0.85rem; color:var(--text2); }
.status .spinner { display:inline-block; width:14px; height:14px; border:2px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:spin 0.8s linear infinite; vertical-align:-2px; }
@keyframes spin { to { transform:rotate(360deg); } }
table { width:100%; border-collapse:collapse; background:var(--surface); border-radius:var(--radius); overflow:hidden; border:1px solid var(--border); }
th,td { padding:12px 16px; text-align:left; font-size:0.88rem; border-bottom:1px solid var(--border); }
th { background:var(--surface2); color:var(--text2); font-size:0.78rem; text-transform:uppercase; letter-spacing:0.8px; white-space:nowrap; }
tr:last-child td { border-bottom:none; }
td.num { font-family:'JetBrains Mono',monospace; text-align:right; }
td.best { color:var(--green); font-weight:700; }
.group-header td { background:rgba(99,102,241,0.08); font-weight:700; color:var(--accent); }
.section { margin-bottom:32px; }
.section h2 { font-size:1.05rem; margin-bottom:14px; color:var(--text); display:flex; align-items:center; gap:8px; }
.section h2 .tag { font-size:0.68rem; padding:3px 8px; border-radius:999px; background:var(--surface2); border:1px solid var(--border); color:var(--text2); font-weight:600; }
.note { font-size:0.8rem; color:var(--text2); margin-top:14px; line-height:1.7; }
.note code { background:var(--surface2); padding:2px 5px; border-radius:4px; }
.summary-cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:14px; margin-bottom:28px; }
.card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:18px; }
.card .val { font-size:1.5rem; font-weight:800; background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.card .lbl { font-size:0.78rem; color:var(--text2); margin-top:4px; }
</style>
</head>
<body>
<header>
<div class="logo"><div class="icon"></div><span>MetonaSqlark</span></div>
<nav>
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
<a href="demo.html">演示</a>
<a href="benchmark.html" class="nav-active">基准</a>
</nav>
</header>
<div class="container">
<div class="hero">
<h1>⚡ 性能基准测试</h1>
<p>在浏览器中实测 <code>Memory</code><code>Aria</code>(LSM-Tree 自研引擎)两种模式的真实性能。
数据量分三档(1K / 10K / 50K 行),测量 <code>INSERT</code> / 主键查询 / 索引查询 / <code>UPDATE</code> / <code>DELETE</code> 的吞吐(ops/sec)。
<strong>测试结果取决于你的设备与浏览器</strong>,仅供参考。</p>
</div>
<div class="controls">
<button class="btn btn-run" id="run-btn" onclick="runAll()">▶ 运行完整基准</button>
<button class="btn btn-small" onclick="runAll(true)">⚡ 快速模式(1K/10K</button>
<button class="btn btn-small" onclick="window.location.reload()">↻ 重置</button>
<span class="status" id="status"></span>
</div>
<div class="summary-cards" id="summary"></div>
<div class="section" id="result-section" style="display:none">
<h2>测试结果 <span class="tag">ops/sec</span></h2>
<table id="result-table">
<thead><tr><th>引擎</th><th>数据量</th><th>INSERT</th><th>主键查询</th><th>索引查询</th><th>UPDATE</th><th>DELETE</th></tr></thead>
<tbody></tbody>
</table>
<p class="note">注:Memory 模式为纯内存 Map 存储;Aria 模式为 LSM-Tree + MemTable 存储(<code>storageBackend: 'memory'</code>,无磁盘 I/O)。
INSERT 为单行批量写入(含 WAL 记录);UPDATE/DELETE 按主键条件执行。</p>
</div>
</div>
<script src="../dist/metona-sqlark.js"></script>
<script>
const M = window.MetonaSqlark;
const SIZES = [1000, 10000, 50000];
const SIZES_FAST = [1000, 10000];
let cancelled = false;
function setStatus(text, spin) {
const el = document.getElementById('status');
el.innerHTML = (spin ? '<span class="spinner"></span> ' : '') + text;
}
function setRunning(on) {
document.getElementById('run-btn').disabled = on;
}
function makeRows(n) {
const rows = [];
for (let i = 0; i < n; i++) {
rows.push({ id: 'k' + i, name: 'User' + i, age: 18 + (i % 60), city: 'City' + (i % 100) });
}
return rows;
}
async function createDb(mode) {
const db = new M.MetonaSqlark({
name: 'bench-' + mode + '-' + Date.now(),
mode,
diskEngine: 'memory',
});
await db.init();
await db.defineTable('bench', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number', index: true },
city: { type: 'string' },
});
return db;
}
async function timeIt(fn) {
const start = performance.now();
await fn();
return performance.now() - start;
}
async function runBench(mode, size) {
const db = await createDb(mode);
const rows = makeRows(size);
const result = { mode, size, insert: 0, pk: 0, idx: 0, update: 0, del: 0 };
try {
// INSERT(批量)
let ms = await timeIt(() => db.table('bench').insertMany(rows));
result.insert = Math.round(size / (ms / 1000));
// 主键查询 × 500
ms = await timeIt(async () => {
for (let i = 0; i < 500; i++) await db.query(`SELECT * FROM bench WHERE id = 'k${(i * 37) % size}'`);
});
result.pk = Math.round(500 / (ms / 1000));
// 索引查询 × 500
ms = await timeIt(async () => {
for (let i = 0; i < 500; i++) await db.query(`SELECT * FROM bench WHERE age = ${18 + ((i * 17) % 60)}`);
});
result.idx = Math.round(500 / (ms / 1000));
// UPDATE × 200(按主键)
ms = await timeIt(async () => {
for (let i = 0; i < 200; i++) await db.query(`UPDATE bench SET name = 'X${i}' WHERE id = 'k${(i * 53) % size}'`);
});
result.update = Math.round(200 / (ms / 1000));
// DELETE × 200(按主键)
ms = await timeIt(async () => {
for (let i = 0; i < 200; i++) await db.query(`DELETE FROM bench WHERE id = 'k${(i * 71) % size}'`);
});
result.del = Math.round(200 / (ms / 1000));
} finally {
await db.close();
}
return result;
}
async function runAll(quick) {
if (cancelled) { cancelled = false; }
const sizes = quick ? SIZES_FAST : SIZES;
const btn = document.getElementById('run-btn');
btn.textContent = '⏳ 测试中…';
setRunning(true);
document.getElementById('result-section').style.display = 'none';
document.getElementById('summary').innerHTML = '';
const results = [];
for (const mode of ['memory', 'aria']) {
for (const size of sizes) {
setStatus(`正在测试 ${mode === 'memory' ? 'Memory' : 'Aria'} 引擎 · ${size.toLocaleString()} 行…`, true);
// 让 UI 刷新
await new Promise((r) => setTimeout(r, 30));
try {
const r = await runBench(mode, size);
results.push(r);
} catch (e) {
setStatus(`${mode}/${size} 失败: ${e.message}`, false);
console.error(e);
}
}
}
renderResults(results);
setStatus('✓ 测试完成', false);
btn.textContent = '▶ 运行完整基准';
setRunning(false);
}
function renderResults(results) {
const tbody = document.querySelector('#result-table tbody');
tbody.innerHTML = '';
// 汇总卡片
const summary = document.getElementById('summary');
let bestInsert = 0, bestInsertMode = '', bestPk = 0, bestPkMode = '', totalTests = 0;
for (const r of results) {
totalTests++;
if (r.insert > bestInsert) { bestInsert = r.insert; bestInsertMode = r.mode; }
if (r.pk > bestPk) { bestPk = r.pk; bestPkMode = r.mode; }
}
summary.innerHTML = `
<div class="card"><div class="val">${results.length} 组</div><div class="lbl">测试组数</div></div>
<div class="card"><div class="val">${bestInsert.toLocaleString()} ops/s</div><div class="lbl">INSERT 峰值(${bestInsertMode}</div></div>
<div class="card"><div class="val">${bestPk.toLocaleString()} ops/s</div><div class="lbl">主键查询峰值(${bestPkMode}</div></div>
`;
// 表格(按引擎分组)
let lastMode = '';
for (const r of results) {
if (r.mode !== lastMode) {
lastMode = r.mode;
const row = document.createElement('tr');
row.className = 'group-header';
row.innerHTML = `<td colspan="6">${r.mode === 'memory' ? '🚀 Memory 引擎' : '🌲 Aria 引擎(LSM-Tree'}</td>`;
tbody.appendChild(row);
}
const row = document.createElement('tr');
const fmt = (v) => `<td class="num">${v ? v.toLocaleString() : '—'}</td>`;
row.innerHTML = `
<td>${r.mode}</td>
<td>${r.size.toLocaleString()} 行</td>
${fmt(r.insert)}${fmt(r.pk)}${fmt(r.idx)}${fmt(r.update)}${fmt(r.del)}
`;
tbody.appendChild(row);
}
document.getElementById('result-section').style.display = 'block';
document.getElementById('result-section').scrollIntoView({ behavior: 'smooth' });
}
</script>
</body>
</html>
+131 -9
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧪 在线演示 — MetonaSqlark v0.2.5</title>
<title>🧪 在线演示 — MetonaSqlark v0.3.2</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style>
:root {
@@ -82,14 +82,15 @@
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
<a href="demo.html" class="nav-active">演示</a>
<a href="benchmark.html">基准</a>
</nav>
<div class="status"><span class="dot"></span> Memory 模式 — v0.2.5</div>
<div class="status"><span class="dot"></span> Memory 模式 — v0.3.2</div>
</header>
<div class="main">
<div class="editor-panel">
<div class="editor-area">
<textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.2.5 在线演示
<textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.3.2 在线演示
-- 已预置 users / orders / products 表数据
-- 新特性: ALTER TABLE · TRUNCATE TABLE · WAL同步 · MVCC · SQL注入防护
@@ -121,6 +122,12 @@
<button class="btn btn-preset" onclick="loadPreset('adv')">🧪 高级</button>
<button class="btn btn-preset" onclick="loadPreset('alter')">🏗 ALTER</button>
<button class="btn btn-preset" onclick="loadPreset('truncate')">🗑 TRUNCATE</button>
<button class="btn btn-preset" onclick="loadPreset('casewhen')">🎯 CASE WHEN</button>
<button class="btn btn-preset" onclick="loadPreset('union')">🔀 UNION</button>
<button class="btn btn-preset" onclick="loadPreset('insertselect')">📥 INSERT SELECT</button>
<button class="btn btn-preset" onclick="loadPreset('exists')">🔍 EXISTS</button>
<button class="btn btn-preset" onclick="loadPreset('index')">🗂 索引</button>
<button class="btn btn-preset" onclick="loadPreset('multistmt')">📜 多语句/事务</button>
<button class="btn btn-preset" onclick="loadPreset('aria')" style="color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
</div>
</div>
@@ -511,9 +518,9 @@ SELECT COUNT(*) as total FROM temp_logs;
-- 清理
DROP TABLE temp_logs;`,
aria: `-- 🌲 AriaEngine 演示 (v0.2.5)
aria: `-- 🌲 AriaEngine 演示 (v0.2.5 → v0.3.2)
-- AriaEngine: LSM-Tree 自研存储引擎
-- 支持 LSM-Tree · WAL CRC同步 · MVCC版本链 · BloomFilter · 二级索引 · AES-GCM · 721测试
-- 支持 LSM-Tree · WAL CRC同步 · MVCC版本链 · BloomFilter · 二级索引 · AES-GCM · 836测试
-- 基础 CRUD 完全兼容
CREATE TABLE IF NOT EXISTS tasks (
@@ -533,18 +540,133 @@ DROP TABLE temp_logs;`,
-- 聚合统计
SELECT done, COUNT(*) as cnt FROM tasks GROUP BY done;
-- CASE WHEN 表达式
SELECT title,
CASE WHEN done THEN '✅ done' ELSE '⏳ pending' END AS status
FROM tasks;
-- EXISTS 关联子查询
SELECT u.name FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- AriaEngine 特性:
-- • LSM-Tree: MemTable (红黑树) → SSTable 多级索引
-- • WAL: Write-Ahead Log 保证崩溃恢复
-- • WAL: Write-Ahead Log 保证崩溃恢复 + 批量组提交
-- • MVCC: 版本链 + 快照隔离
-- • Buffer Pool: LRU 页面缓存 (256页 ~ 1MB)
-- • Buffer Pool: SSTable LRU 缓存 (256页 ~ 1MB) ✅ 已生效
-- • Bloom Filter: FNV-1a + Murmur 双哈希
-- • Slotted Page: 4KB 页面 + Tuple 二进制编码
-- • 二级索引: 每列独立 LSM + 动态 CREATE INDEX
-- 生产环境: mode: 'aria' 激活自研引擎
-- const db = await MetonaSqlark.create({
-- name: 'my-app', mode: 'aria'
-- });`,
casewhen: `-- 🎯 CASE WHEN 条件表达式 (v0.3.1 / v0.3.2)
-- SELECT 列:多 WHEN + ELSE
SELECT name, age,
CASE WHEN age >= 30 THEN 'senior'
WHEN age >= 25 THEN 'mid'
ELSE 'junior' END AS age_group
FROM users ORDER BY age;
-- WHERE 条件中的 CASE WHEN
SELECT name FROM users
WHERE CASE WHEN age >= 25 THEN 'adult' ELSE 'young' END = 'adult';
-- 聚合中的 CASE WHEN
SELECT
SUM(CASE WHEN age >= 30 THEN 1 ELSE 0 END) AS seniors,
SUM(CASE WHEN age < 30 THEN 1 ELSE 0 END) AS juniors,
AVG(CASE WHEN age >= 25 THEN age END) AS avg_adult_age
FROM users;`,
union: `-- 🔀 UNION / UNION ALL (v0.3.0)
-- UNION 去重合并两表数据
SELECT name FROM users WHERE age >= 30
UNION
SELECT name FROM users WHERE age < 30;
-- UNION 对重复行去重(同查询两次 → 结果去重)
SELECT category FROM products
UNION
SELECT category FROM products;
-- UNION ALL 保留重复
SELECT category FROM products WHERE price >= 100
UNION ALL
SELECT category FROM products WHERE price >= 50;`,
insertselect: `-- 📥 INSERT INTO ... SELECT (v0.3.0)
-- 建备份表并复制全量数据
DROP TABLE IF EXISTS users_backup;
CREATE TABLE users_backup (
id STRING PRIMARY KEY, name STRING, email STRING, age NUMBER
);
INSERT INTO users_backup SELECT * FROM users;
-- 确认复制
SELECT COUNT(*) as backed_up FROM users_backup;
-- INSERT SELECT 带 WHERE 过滤
DROP TABLE IF EXISTS big_spenders;
CREATE TABLE big_spenders (
id STRING PRIMARY KEY, product STRING, amount NUMBER
);
INSERT INTO big_spenders (id, product, amount)
SELECT o.id, o.product, o.amount FROM orders o WHERE o.amount >= 150;
-- 确认过滤结果
SELECT * FROM big_spenders;`,
exists: `-- 🔍 EXISTS 关联子查询 (v0.3.0 / v0.3.1)
-- 有订单的用户(关联子查询)
SELECT u.name FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- 无订单的用户(NOT EXISTS
SELECT u.name FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- EXISTS + JOIN 组合
SELECT u.name, o.product, o.amount FROM users u
JOIN orders o ON u.id = o.user_id
WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id AND o2.amount > 100);`,
index: `-- 🗂 动态索引 CREATE / DROP INDEX (v0.3.0)
-- 为 orders.user_id 创建索引
CREATE INDEX idx_orders_user ON orders (user_id);
-- 索引查找(走二级索引)
SELECT u.name, o.product, o.amount
FROM orders o JOIN users u ON u.id = o.user_id
WHERE o.user_id = '1';
-- 删除索引
DROP INDEX idx_orders_user ON orders (user_id);
-- 删除后回退全表扫描(结果不变)
SELECT * FROM orders WHERE user_id = '3';`,
multistmt: `-- 📜 多语句 + 事务语句 (v0.3.0)
-- 分号分隔的多语句一次执行
DROP TABLE IF EXISTS audit;
CREATE TABLE audit (id STRING PRIMARY KEY, msg STRING);
INSERT INTO audit VALUES ('a1', 'first');
INSERT INTO audit VALUES ('a2', 'second');
SELECT * FROM audit ORDER BY id;
-- 事务语句:BEGIN → ROLLBACKa3 不会生效)
BEGIN;
INSERT INTO audit VALUES ('a3', 'will be rolled back');
ROLLBACK;
SELECT * FROM audit ORDER BY id;
-- 事务语句:BEGIN → COMMITa4 生效)
BEGIN;
INSERT INTO audit VALUES ('a4', 'will be committed');
COMMIT;
SELECT COUNT(*) as total FROM audit;`,
};
function loadPreset(name) {
@@ -564,7 +686,7 @@ document.addEventListener('keydown', e => {
// Boot
initDB().then(() => {
console.log('✅ MetonaSqlark v0.2.5 demo ready');
console.log('✅ MetonaSqlark v0.3.2 demo ready');
setTimeout(runQuery, 300);
}).catch(err => {
renderError('初始化失败: ' + err.message);
+58 -6
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📖 API 文档 — MetonaSqlark v0.2.5</title>
<title>📖 API 文档 — MetonaSqlark v0.3.2</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style>
:root {
@@ -69,6 +69,7 @@
<a href="index.html">首页</a>
<a href="docs.html" class="nav-active">文档</a>
<a href="demo.html">演示</a>
<a href="benchmark.html">基准</a>
</nav>
</div>
</header>
@@ -121,7 +122,7 @@ npm install @metona-team/metona-sqlark</pre>
<h3>CDN / UMD</h3>
<pre><span class="c">&lt;!-- UMD 格式,暴露 window.MetonaSqlark 和 window.MeSqlark --&gt;</span>
&lt;script src=<span class="s">"https://git.metona.cn/.../metona-sqlark.min.js"</span>&gt;&lt;/script&gt;
&lt;script src=<span class="s">"https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"</span>&gt;&lt;/script&gt;
&lt;script&gt;
<span class="k">const</span> db = <span class="k">await</span> window.<span class="f">MetonaSqlark</span>.create({...});
<span class="c">// 或 window.MeSqlark.create(...) — 完全等价</span>
@@ -138,9 +139,9 @@ npm install @metona-team/metona-sqlark</pre>
<table>
<tr><th>文件</th><th>格式</th><th>用途</th></tr>
<tr><td><code>metona-sqlark.js</code></td><td>UMD</td><td>浏览器开发版(含 sourcemap</td></tr>
<tr><td><code>metona-sqlark.min.js</code></td><td>UMD (minified)</td><td>生产环境(~42KB / ~10KB gzip</td></tr>
<tr><td><code>metona-sqlark.min.js</code></td><td>UMD (minified)</td><td>生产环境(~105KB / ~27KB gzip</td></tr>
<tr><td><code>metona-sqlark.esm.js</code></td><td>ES Module</td><td>现代打包工具 / 浏览器 ESM</td></tr>
<tr><td><code>metona-sqlark.cjs.js</code></td><td>CommonJS</td><td>Node.js require()</td></tr>
<tr><td><code>metona-sqlark.cjs</code></td><td>CommonJS</td><td>Node.js require()</td></tr>
<tr><td><code>metona-sqlark.d.ts</code></td><td>TypeScript 声明</td><td>类型提示</td></tr>
</table>
@@ -237,6 +238,37 @@ db.<span class="f">isReady</span>(); <span class="c">// true</span>
<span class="c">-- TRUNCATE TABLE — 快速清空表数据 (v0.2.5)</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'TRUNCATE TABLE old_logs'</span>);</pre>
<h3>SQL 扩展 (v0.3.0+)</h3>
<pre><span class="c">// 多语句 — 分号分隔一次执行(返回最后一条结果)</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">`CREATE TABLE t (id STRING PRIMARY KEY);
INSERT INTO t VALUES ('1'); INSERT INTO t VALUES ('2')`</span>);
<span class="c">// 事务语句 — BEGIN / COMMIT / ROLLBACK</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'BEGIN'</span>);
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">"INSERT INTO t VALUES ('3')"</span>);
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'ROLLBACK'</span>); <span class="c">// 回滚</span>
<span class="c">// INSERT INTO ... SELECT — 查询结果写入</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'INSERT INTO t SELECT id FROM t2 WHERE x > 1'</span>);
<span class="c">// UNION / UNION ALL — 合并查询</span>
<span class="k">const</span> merged = <span class="k">await</span> db.<span class="f">query</span>(
<span class="s">`SELECT name FROM users WHERE city = 'Beijing'
UNION SELECT name FROM users WHERE age < 30`</span>);
<span class="c">// EXISTS / NOT EXISTS — 关联子查询</span>
<span class="k">const</span> hasOrders = <span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)`</span>);
<span class="c">// CASE WHEN — SELECT 列 / WHERE / 聚合 (v0.3.1 / v0.3.2)</span>
<span class="k">const</span> labeled = <span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT name,
CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users`</span>);
<span class="k">const</span> adults = <span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT SUM(CASE WHEN age >= 18 THEN 1 ELSE 0 END) FROM users`</span>);
<span class="c">// CREATE / DROP INDEX — 动态二级索引</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'CREATE INDEX idx_users_city ON users (city)'</span>);
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'DROP INDEX idx_users_city ON users (city)'</span>);</pre>
<h3>条件表达式</h3>
<pre><span class="c">// 比较运算符</span>
<span class="s">`WHERE age > 18 AND name LIKE 'A%'`</span>
@@ -578,6 +610,25 @@ db.<span class="f">emit</span>(<span class="s">'users'</span>, { <span class="s"
<span class="c">// 取消订阅</span>
<span class="f">unsubscribe</span>();</pre>
<h3>多标签页同步 (v0.3.2)</h3>
<pre><span class="c">// 启用 multiTabSync 后,其他标签页的写操作会广播到此标签页</span>
<span class="k">const</span> db = <span class="k">await</span> MetonaSqlark.<span class="f">create</span>({
<span class="s">name</span>: <span class="s">'my-app'</span>,
<span class="s">mode</span>: <span class="s">'hybrid'</span>,
<span class="s">multiTabSync</span>: <span class="k">true</span>,
});
<span class="c">// 订阅其他标签页的变更(event.type === 'external'</span>
db.<span class="f">subscribe</span>(<span class="s">'users'</span>, (event) => {
<span class="k">if</span> (event.<span class="s">type</span> === <span class="s">'external'</span>) {
<span class="c">// Hybrid 模式已自动从磁盘重载,此处可刷新 UI</span>
<span class="f">refreshList</span>();
}
});
<span class="c">// 手动广播(Table API 已自动广播;自定义写入可调用)</span>
db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
<h2 id="react">⚛️ React 集成</h2>
<pre><span class="k">import</span> { useQuery, useTable, useDatabase } <span class="k">from</span> <span class="s">'@metona-team/metona-sqlark/react'</span>;
<span class="k">import</span> { db } <span class="k">from</span> <span class="s">'./db'</span>;
@@ -663,6 +714,7 @@ db.<span class="f">emit</span>(<span class="s">'users'</span>, { <span class="s"
<tr><td><code>onReady</code></td><td><code>(db) => void</code></td><td>-</td><td>初始化完成回调</td></tr>
<tr><td><code>onError</code></td><td><code>(err) => void</code></td><td>-</td><td>错误回调(v0.2.5 接入执行路径)</td></tr>
<tr><td><code>maxRowsPerQuery</code></td><td><code>number</code></td><td><code>0</code></td><td>查询结果行数上限(0=不限制)✅ v0.2.5 生效</td></tr>
<tr><td><code>multiTabSync</code></td><td><code>boolean</code></td><td><code>false</code></td><td>多标签页同步:BroadcastChannel 广播表变更,其他标签页自动刷新 🆕 v0.3.2</td></tr>
</table>
<h2 id="engine">💾 存储引擎</h2>
@@ -678,8 +730,8 @@ db.<span class="f">emit</span>(<span class="s">'users'</span>, { <span class="s"
<h2 id="aria-engine">🌲 AriaEngine 自研存储引擎</h2>
<p><strong>v0.2.0 新增</strong> — AriaEngine 是专为 MetonaSqlark 设计的页面式存储引擎,对标 SQLite 设计理念。<br>
<strong>v0.2.4 生产级</strong> — 二级索引 · MVCC · BloomFilter · WAL CRC全同步 · AES-GCM加密 · Savepoint · EXPLAIN · ANALYZE · REINDEX · VACUUM · BufferPool · 701测试 · 零死代码。<br>
<strong>v0.2.5 质量加固</strong> — WAL full模式真正同步 · MVCC接入读写路径 · SSTableReader二分查找统一 · crypto实例化 · IndexedDB索引利用 · compactLevel public接口 · WAL大小阈值自动checkpoint · SQL注入防护 · ALTER TABLE · TRUNCATE TABLE · 721测试 37套件。</p>
<strong>v0.2.4 生产级</strong> — 二级索引 · MVCC · BloomFilter · WAL CRC全同步 · AES-GCM加密 · Savepoint · EXPLAIN · ANALYZE · REINDEX · VACUUM · BufferPool · 761测试 · 零死代码。<br>
<strong>v0.3.2 表达式与并发</strong> — WAL full模式真正同步 · MVCC接入读写路径 · SSTableReader二分查找统一 · crypto实例化 · IndexedDB索引利用 · compactLevel public接口 · WAL大小阈值自动checkpoint · SQL注入防护 · ALTER TABLE · TRUNCATE TABLE · 761测试 41套件。</p>
<h3>存储模式对比</h3>
<table>
+7 -6
View File
@@ -143,6 +143,7 @@
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
<a href="demo.html">演示</a>
<a href="benchmark.html">基准</a>
<a href="https://git.metona.cn/MetonaTeam/MetonaSqlark" target="_blank">Gitea</a>
</nav>
<a href="demo.html" class="btn btn-primary">立即体验</a>
@@ -152,7 +153,7 @@
<!-- Hero -->
<section class="hero">
<div class="container">
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.2.5 质量加固 — 721测试 37套件 · 五模式全覆盖 · WAL同步修复 · ALTER/TRUNCATE · SQL注入防护 · 零回归</div>
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.3.2 表达式与并发 — 761测试 41套件 · 五模式全覆盖 · WAL同步修复 · ALTER/TRUNCATE · SQL注入防护 · 零回归</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
<div class="actions">
@@ -242,7 +243,7 @@ npm install @metona-team/metona-sqlark
</div>
<div class="feature-card">
<div class="icon">🌲</div>
<h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.2.5</span></h3>
<h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.3.2</span></h3>
<p>自研 LSM-Tree 页面式存储引擎。MemTable 红黑树 + 多级 SSTable、Bloom Filter 快速判存、WAL full模式真正同步、MVCC 版本链接入读写路径。</p>
</div>
<div class="feature-card">
@@ -273,7 +274,7 @@ npm install @metona-team/metona-sqlark
<div class="feature-card">
<div class="icon">📦</div>
<h3>零运行时依赖</h3>
<p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakableUMD/ESM/CJS 多格式输出,~10KB gzip。</p>
<p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakableUMD/ESM/CJS 多格式输出,~27KB gzip。</p>
</div>
<div class="feature-card">
<div class="icon">📊</div>
@@ -393,9 +394,9 @@ npm install @metona-team/metona-sqlark
<p>MetonaSqlark 的核心指标</p>
</div>
<div class="stats">
<div class="stat-card"><div class="num">721</div><div class="label">测试用例</div></div>
<div class="stat-card"><div class="num">91.0%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">~10KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">761</div><div class="label">测试用例</div></div>
<div class="stat-card"><div class="num">81.0%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">~27KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">5</div><div class="label">存储引擎</div></div>
<div class="stat-card"><div class="num">36</div><div class="label">SQL 关键字</div></div>
<div class="stat-card"><div class="num">37</div><div class="label">测试套件</div></div>
+215 -212
View File
@@ -1,212 +1,215 @@
/**
* metona-sqlark Constants — 类型定义 / 默认配置 / 枚举
* @module constants
*/
// ---------------------------------------------------------------------------
// 存储模式
// ---------------------------------------------------------------------------
/** 存储模式 */
export type StorageMode = 'memory' | 'disk' | 'hybrid' | 'aria';
/** 磁盘引擎类型 */
export type DiskEngine = 'indexeddb' | 'opfs';
/** 所有存储模式 */
export const STORAGE_MODES: StorageMode[] = ['memory', 'disk', 'hybrid', 'aria'];
/** 所有磁盘引擎 */
export const DISK_ENGINES: DiskEngine[] = ['indexeddb', 'opfs'];
// ---------------------------------------------------------------------------
// 字段类型
// ---------------------------------------------------------------------------
/** 字段数据类型 */
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json';
/** 所有字段类型 */
export const FIELD_TYPES: FieldType[] = ['string', 'number', 'boolean', 'date', 'json'];
// ---------------------------------------------------------------------------
// 列定义 & 表结构
// ---------------------------------------------------------------------------
/** 列定义 */
export interface ColumnDef {
/** 字段类型 */
type: FieldType;
/** 是否主键 */
primaryKey?: boolean;
/** 是否必填 */
required?: boolean;
/** 是否唯一 */
unique?: boolean;
/** 默认值 */
default?: unknown;
/** 是否创建索引 */
index?: boolean;
/** 外键引用: 'table.column' */
references?: string;
/** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 字符串最大长度 */
maxLength?: number;
/** 数字最小值 */
min?: number;
/** 数字最大值 */
max?: number;
}
/** 表结构定义 */
export interface TableSchema {
/** 表名 */
name: string;
/** 列定义映射 */
columns: Record<string, ColumnDef>;
}
// ---------------------------------------------------------------------------
// 数据库配置
// ---------------------------------------------------------------------------
/** 数据库配置 */
export interface DatabaseConfig {
/** 数据库名称 */
name: string;
/** 存储模式 */
mode?: StorageMode;
/** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */
diskEngine?: DiskEngine;
/** 版本号 */
version?: number;
/** 插件列表 */
plugins?: MetonaPlugin[];
/** 数据库就绪回调 */
onReady?: (db: unknown) => void;
/** 错误回调 */
onError?: (error: Error) => void;
/** 查询结果行数上限(默认 0,0 表示不限制) */
maxRowsPerQuery?: number;
/** 调试模式(启用后输出详细操作日志) */
debug?: boolean;
}
/** 数据库默认配置 */
export const DB_DEFAULTS: Readonly<Required<Omit<DatabaseConfig, 'plugins' | 'onReady' | 'onError'>>> = Object.freeze({
name: 'metona-sqlark',
mode: 'hybrid' as const,
diskEngine: 'indexeddb' as const,
version: 1,
maxRowsPerQuery: 0, // 0 = 不限制
debug: false,
});
// ---------------------------------------------------------------------------
// Where 操作符
// ---------------------------------------------------------------------------
/** Where 条件操作符 */
export type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
/** 简单条件值:直接相等 */
export type SimpleCondition = unknown;
/** 操作符条件 */
export type OperatorCondition = Partial<Record<WhereOperator, unknown>>;
/** 字段条件:简单值 | 操作符对象 */
export type FieldCondition = SimpleCondition | OperatorCondition;
/** Where 条件对象 */
export type WhereCondition = Record<string, FieldCondition>;
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------
/** 排序方向 */
export type SortDirection = 'asc' | 'desc';
/** 排序定义 */
export interface OrderBy {
/** 列名 */
column: string;
/** 排序方向 */
direction: SortDirection;
}
// ---------------------------------------------------------------------------
// 查询计划(引擎层使用)
// ---------------------------------------------------------------------------
/** 查询计划 — 由 Executor 编译 AST 后生成 */
export interface QueryPlan {
/** 表名 */
table: string;
/** 要返回的列(undefined = 全部,['*'] = 全部) */
columns?: string[];
/** 过滤条件 */
where?: WhereCondition;
/** 排序 */
orderBy?: OrderBy[];
/** 限制条数 */
limit?: number;
/** 偏移量 */
offset?: number;
}
// ---------------------------------------------------------------------------
// 插件接口
// ---------------------------------------------------------------------------
/** 钩子名称 */
export type HookName =
| 'beforeCreateTable' | 'afterCreateTable'
| 'beforeDropTable' | 'afterDropTable'
| 'beforeInsert' | 'afterInsert'
| 'beforeUpdate' | 'afterUpdate'
| 'beforeDelete' | 'afterDelete'
| 'beforeQuery' | 'afterQuery'
| 'beforeTransaction' | 'afterTransaction';
/** 插件定义 */
export interface MetonaPlugin {
/** 插件名称 */
name: string;
/** 插件版本 */
version: string;
/** 描述 */
description?: string;
/** 优先级,越大越先执行 */
priority?: number;
/** 安装 */
install(db: unknown): void;
/** 销毁 */
destroy(): void;
}
// ---------------------------------------------------------------------------
// 错误类型
// ---------------------------------------------------------------------------
/** 数据库错误 */
export class DatabaseError extends Error {
constructor(
message: string,
public code: string,
public details?: unknown,
) {
super(message);
this.name = 'DatabaseError';
}
}
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
export const VERSION = '0.2.5';
/**
* metona-sqlark Constants — 类型定义 / 默认配置 / 枚举
* @module constants
*/
// ---------------------------------------------------------------------------
// 存储模式
// ---------------------------------------------------------------------------
/** 存储模式 */
export type StorageMode = 'memory' | 'disk' | 'hybrid' | 'aria';
/** 磁盘引擎类型 */
export type DiskEngine = 'indexeddb' | 'opfs';
/** 所有存储模式 */
export const STORAGE_MODES: StorageMode[] = ['memory', 'disk', 'hybrid', 'aria'];
/** 所有磁盘引擎 */
export const DISK_ENGINES: DiskEngine[] = ['indexeddb', 'opfs'];
// ---------------------------------------------------------------------------
// 字段类型
// ---------------------------------------------------------------------------
/** 字段数据类型 */
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json';
/** 所有字段类型 */
export const FIELD_TYPES: FieldType[] = ['string', 'number', 'boolean', 'date', 'json'];
// ---------------------------------------------------------------------------
// 列定义 & 表结构
// ---------------------------------------------------------------------------
/** 列定义 */
export interface ColumnDef {
/** 字段类型 */
type: FieldType;
/** 是否主键 */
primaryKey?: boolean;
/** 是否必填 */
required?: boolean;
/** 是否唯一 */
unique?: boolean;
/** 默认值 */
default?: unknown;
/** 是否创建索引 */
index?: boolean;
/** 外键引用: 'table.column' */
references?: string;
/** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 字符串最大长度 */
maxLength?: number;
/** 数字最小值 */
min?: number;
/** 数字最大值 */
max?: number;
}
/** 表结构定义 */
export interface TableSchema {
/** 表名 */
name: string;
/** 列定义映射 */
columns: Record<string, ColumnDef>;
}
// ---------------------------------------------------------------------------
// 数据库配置
// ---------------------------------------------------------------------------
/** 数据库配置 */
export interface DatabaseConfig {
/** 数据库名称 */
name: string;
/** 存储模式 */
mode?: StorageMode;
/** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */
diskEngine?: DiskEngine;
/** 版本号 */
version?: number;
/** 插件列表 */
plugins?: MetonaPlugin[];
/** 数据库就绪回调 */
onReady?: (db: unknown) => void;
/** 错误回调 */
onError?: (error: Error) => void;
/** 查询结果行数上限(默认 0,0 表示不限制) */
maxRowsPerQuery?: number;
/** 调试模式(启用后输出详细操作日志) */
debug?: boolean;
/** 多标签页同步(v0.3.2):BroadcastChannel 广播表变更,其他标签页自动刷新 */
multiTabSync?: boolean;
}
/** 数据库默认配置 */
export const DB_DEFAULTS: Readonly<Required<Omit<DatabaseConfig, 'plugins' | 'onReady' | 'onError'>>> = Object.freeze({
name: 'metona-sqlark',
mode: 'hybrid' as const,
diskEngine: 'indexeddb' as const,
version: 1,
maxRowsPerQuery: 0, // 0 = 不限制
debug: false,
multiTabSync: false,
});
// ---------------------------------------------------------------------------
// Where 操作符
// ---------------------------------------------------------------------------
/** Where 条件操作符 */
export type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
/** 简单条件值:直接相等 */
export type SimpleCondition = unknown;
/** 操作符条件 */
export type OperatorCondition = Partial<Record<WhereOperator, unknown>>;
/** 字段条件:简单值 | 操作符对象 */
export type FieldCondition = SimpleCondition | OperatorCondition;
/** Where 条件对象 */
export type WhereCondition = Record<string, FieldCondition>;
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------
/** 排序方向 */
export type SortDirection = 'asc' | 'desc';
/** 排序定义 */
export interface OrderBy {
/** 列名 */
column: string;
/** 排序方向 */
direction: SortDirection;
}
// ---------------------------------------------------------------------------
// 查询计划(引擎层使用)
// ---------------------------------------------------------------------------
/** 查询计划 — 由 Executor 编译 AST 后生成 */
export interface QueryPlan {
/** 表名 */
table: string;
/** 要返回的列(undefined = 全部,['*'] = 全部) */
columns?: string[];
/** 过滤条件 */
where?: WhereCondition;
/** 排序 */
orderBy?: OrderBy[];
/** 限制条数 */
limit?: number;
/** 偏移量 */
offset?: number;
}
// ---------------------------------------------------------------------------
// 插件接口
// ---------------------------------------------------------------------------
/** 钩子名称 */
export type HookName =
| 'beforeCreateTable' | 'afterCreateTable'
| 'beforeDropTable' | 'afterDropTable'
| 'beforeInsert' | 'afterInsert'
| 'beforeUpdate' | 'afterUpdate'
| 'beforeDelete' | 'afterDelete'
| 'beforeQuery' | 'afterQuery'
| 'beforeTransaction' | 'afterTransaction';
/** 插件定义 */
export interface MetonaPlugin {
/** 插件名称 */
name: string;
/** 插件版本 */
version: string;
/** 描述 */
description?: string;
/** 优先级,越大越先执行 */
priority?: number;
/** 安装 */
install(db: unknown): void;
/** 销毁 */
destroy(): void;
}
// ---------------------------------------------------------------------------
// 错误类型
// ---------------------------------------------------------------------------
/** 数据库错误 */
export class DatabaseError extends Error {
constructor(
message: string,
public code: string,
public details?: unknown,
) {
super(message);
this.name = 'DatabaseError';
}
}
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
export const VERSION = '0.3.2';
+391 -329
View File
@@ -1,329 +1,391 @@
/**
* metona-sqlark Core — 数据库主类
* @module core
*
* 管理数据库生命周期、引擎调度、表操作、SQL 查询、事务和插件。
*/
import type { IStorageEngine } from './engine/interface';
import type { DatabaseConfig, ColumnDef } from './constants';
import { DB_DEFAULTS, DatabaseError } from './constants';
import { MemoryEngine } from './engine/memory';
import { IndexedDBEngine } from './engine/indexeddb';
import { OPFSEngine } from './engine/opfs';
import { AriaEngine } from './engine/aria/index';
import { HybridEngine } from './hybrid/index';
import { Table } from './table/table';
import { createSchema } from './table/schema';
import { QueryExecutor } from './query/executor';
import { parse } from './sql/parser';
import { TransactionManager } from './transaction/index';
import { PluginManager } from './plugin/index';
import type { Statement } from './query/ast';
// ---------------------------------------------------------------------------
// MetonaSqlark
// ---------------------------------------------------------------------------
export class MetonaSqlark {
/** 数据库名称 */
readonly name: string;
/** 存储模式 */
readonly mode: string;
/** 版本号 */
private _version: number;
/** 获取版本号 */
get version(): number { return this._version; }
private engine!: IStorageEngine;
private executor!: QueryExecutor;
private transactionManager!: TransactionManager;
private pluginManager: PluginManager;
private config: DatabaseConfig;
private ready = false;
private tableCache: Map<string, Table> = new Map();
/** 查询结果行数上限 */
get maxRowsPerQuery(): number { return this.config.maxRowsPerQuery ?? 0; }
/** 调试模式 */
get debug(): boolean { return this.config.debug ?? false; }
constructor(config: DatabaseConfig) {
this.config = config;
this.name = config.name ?? DB_DEFAULTS.name;
this.mode = config.mode ?? DB_DEFAULTS.mode;
this._version = config.version ?? DB_DEFAULTS.version;
this.pluginManager = new PluginManager();
}
// ---- 初始化 ----
/** 初始化数据库(创建引擎、打开连接) */
async init(): Promise<void> {
// 创建引擎
this.engine = this.createEngine();
// 打开连接
await this.engine.open(this.name, this.version);
// 初始化执行器和事务管理器
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
this.transactionManager = new TransactionManager(this.engine);
// 注册插件
if (this.config.plugins) {
for (const plugin of this.config.plugins) {
this.pluginManager.register(plugin, this);
}
}
this.ready = true;
// 回调
if (this.config.onReady) {
this.config.onReady(this);
}
}
/** 检查是否就绪 */
isReady(): boolean {
return this.ready;
}
// ---- 表管理 ----
/** 创建表 */
async defineTable(name: string, columns: Record<string, ColumnDef>): Promise<void> {
this.ensureReady();
const schema = createSchema(name, columns);
try {
await this.pluginManager.trigger('beforeCreateTable', schema);
await this.engine.createTable(schema);
await this.pluginManager.trigger('afterCreateTable', schema);
} catch (error) {
this._onError(error as Error);
throw error;
}
// 清除缓存
this.tableCache.delete(name);
}
/** 获取表操作对象 */
table(name: string): Table {
this.ensureReady();
let t = this.tableCache.get(name);
if (!t) {
t = new Table(this.engine, name, this.executor);
this.tableCache.set(name, t);
}
return t;
}
/** 删除表 */
async dropTable(name: string): Promise<void> {
this.ensureReady();
try {
await this.pluginManager.trigger('beforeDropTable', name);
await this.engine.dropTable(name);
await this.pluginManager.trigger('afterDropTable', name);
} catch (error) {
this._onError(error as Error);
throw error;
}
this.tableCache.delete(name);
}
/** 获取所有表名 */
async getTableNames(): Promise<string[]> {
this.ensureReady();
return this.engine.getTableNames();
}
// ---- SQL 查询 ----
/** 执行 SQL 字符串查询 */
async query(sql: string): Promise<unknown> {
this.ensureReady();
const startTime = this.debug ? Date.now() : 0;
await this.pluginManager.trigger('beforeQuery', sql);
let result: unknown;
try {
const stmt: Statement = parse(sql);
result = await this.executor.execute(stmt);
} catch (error) {
this._onError(error as Error);
throw error;
}
await this.pluginManager.trigger('afterQuery', sql, result);
if (this.debug) {
const elapsed = Date.now() - startTime;
const rows = Array.isArray(result) ? (result as any[]).length : 0;
this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`);
}
return result;
}
// ---- 事务 ----
/** 执行事务 */
async transaction<T>(fn: (trx: import('./transaction/index').Transaction) => Promise<T>): Promise<T> {
this.ensureReady();
await this.pluginManager.trigger('beforeTransaction');
try {
const result = await this.transactionManager.execute(fn);
await this.pluginManager.trigger('afterTransaction');
return result;
} catch (error) {
this._onError(error as Error);
throw error;
}
}
// ---- 导入导出 ----
/** 导出表数据为 JSON */
async exportTable(tableName: string): Promise<Record<string, unknown>[]> {
this.ensureReady();
return this.engine.find(tableName, { table: tableName });
}
/** 导入 JSON 数据到表 */
async importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]> {
this.ensureReady();
try {
return await this.engine.insert(tableName, data);
} catch (error) {
this._onError(error as Error);
throw error;
}
}
/** 导出整个数据库为 JSON */
async exportAll(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureReady();
const result: Record<string, Record<string, unknown>[]> = {};
const names = await this.engine.getTableNames();
for (const name of names) {
result[name] = await this.engine.find(name, { table: name });
}
return result;
}
// ---- 发布订阅 ----
private listeners: Map<string, Set<(data: unknown) => void>> = new Map();
/** 订阅表变更 */
subscribe(tableName: string, callback: (event: { type: string; row?: unknown }) => void): () => void {
const key = `change:${tableName}`;
if (!this.listeners.has(key)) this.listeners.set(key, new Set());
this.listeners.get(key)!.add(callback as (data: unknown) => void);
return () => this.listeners.get(key)?.delete(callback as (data: unknown) => void);
}
/** 触发变更事件 */
emit(tableName: string, event: { type: string; row?: unknown }): void {
const key = `change:${tableName}`;
this.listeners.get(key)?.forEach((cb) => cb(event));
}
// ---- 迁移 ----
private migrations: Map<number, (db: MetonaSqlark) => Promise<void>> = new Map();
/** 注册迁移 */
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void {
this.migrations.set(version, up);
}
/** 执行迁移到指定版本 */
async migrateTo(targetVersion: number): Promise<void> {
this.ensureReady();
for (const [version, up] of [...this.migrations.entries()].sort((a, b) => a[0] - b[0])) {
if (version <= targetVersion && version > this.version) {
await up(this);
this._version = version;
}
}
}
// ---- 插件 ----
/** 获取插件管理器 */
getPluginManager(): PluginManager {
return this.pluginManager;
}
/** 注册钩子 */
on(hook: import('./constants').HookName, callback: import('./plugin/index').HookCallback): void {
this.pluginManager.on(hook, callback);
}
// ---- 生命周期 ----
/** 关闭数据库 */
async close(): Promise<void> {
this.pluginManager.destroy();
await this.engine.close();
this.tableCache.clear();
this.ready = false;
}
/** 获取底层引擎 */
getEngine(): IStorageEngine {
return this.engine;
}
// ---- 内部 ----
private createEngine(): IStorageEngine {
const mode = this.mode;
const diskEngine = this.config.diskEngine ?? 'indexeddb';
switch (mode) {
case 'memory':
return new MemoryEngine();
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
case 'aria':
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
case 'hybrid':
return new HybridEngine(diskEngine);
default:
throw new DatabaseError(`Unknown storage mode: ${mode}`, 'CONFIG_ERROR');
}
}
private ensureReady(): void {
if (!this.ready) {
throw new DatabaseError('Database not initialized. Call await db.init() first.', 'DB_NOT_READY');
}
}
/** 错误回调分发 */
private _onError(error: Error): void {
if (this.config.onError) {
try { this.config.onError(error); } catch { /* 避免回调自身异常影响主流程 */ }
}
}
/** 调试日志 */
private _debug(msg: string, ...args: unknown[]): void {
if (this.debug) {
console.debug(`[MetonaSqlark:${this.name}] ${msg}`, ...args);
}
}
}
/**
* metona-sqlark Core — 数据库主类
* @module core
*
* 管理数据库生命周期、引擎调度、表操作、SQL 查询、事务和插件。
*/
import type { IStorageEngine } from './engine/interface';
import type { DatabaseConfig, ColumnDef } from './constants';
import { DB_DEFAULTS, DatabaseError } from './constants';
import { MemoryEngine } from './engine/memory';
import { IndexedDBEngine } from './engine/indexeddb';
import { OPFSEngine } from './engine/opfs';
import { AriaEngine } from './engine/aria/index';
import { HybridEngine } from './hybrid/index';
import { Table } from './table/table';
import { createSchema } from './table/schema';
import { QueryExecutor } from './query/executor';
import { parseAll } from './sql/parser';
import { TransactionManager } from './transaction/index';
import { PluginManager } from './plugin/index';
import type { Statement } from './query/ast';
// ---------------------------------------------------------------------------
// MetonaSqlark
// ---------------------------------------------------------------------------
export class MetonaSqlark {
/** 数据库名称 */
readonly name: string;
/** 存储模式 */
readonly mode: string;
/** 版本号 */
private _version: number;
/** 获取版本号 */
get version(): number { return this._version; }
private engine!: IStorageEngine;
private executor!: QueryExecutor;
private transactionManager!: TransactionManager;
private pluginManager: PluginManager;
private config: DatabaseConfig;
private ready = false;
private tableCache: Map<string, Table> = new Map();
/** 查询结果行数上限 */
get maxRowsPerQuery(): number { return this.config.maxRowsPerQuery ?? 0; }
/** 调试模式 */
get debug(): boolean { return this.config.debug ?? false; }
/** 多标签页同步通道(v0.3.2 */
private channel: BroadcastChannel | null = null;
constructor(config: DatabaseConfig) {
this.config = config;
this.name = config.name ?? DB_DEFAULTS.name;
this.mode = config.mode ?? DB_DEFAULTS.mode;
this._version = config.version ?? DB_DEFAULTS.version;
this.pluginManager = new PluginManager();
// v0.3.2: 多标签页同步 — BroadcastChannel 广播表变更
if (config.multiTabSync && typeof BroadcastChannel !== 'undefined') {
this.channel = new BroadcastChannel(`metona-sqlark:${this.name}`);
this.channel.onmessage = (event) => {
const msg = event.data as { type?: string; table?: string } | null;
if (!msg || msg.type !== 'change') return;
this.emit(msg.table ?? '', { type: 'external', table: msg.table ?? '' });
// Hybrid 引擎:从磁盘重载内存,保证读到其他标签页的最新数据
if (this.engine instanceof HybridEngine) {
(this.engine as HybridEngine).reloadMemoryFromDisk().catch(() => {
// 重载失败不影响主流程(下次读可能短暂过期)
});
}
};
}
}
// ---- 初始化 ----
/** 初始化数据库(创建引擎、打开连接) */
async init(): Promise<void> {
// 创建引擎
this.engine = this.createEngine();
// 打开连接
await this.engine.open(this.name, this.version);
// 初始化执行器和事务管理器
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
this.transactionManager = new TransactionManager(this.engine);
// 注册插件
if (this.config.plugins) {
for (const plugin of this.config.plugins) {
this.pluginManager.register(plugin, this);
}
}
this.ready = true;
// 回调
if (this.config.onReady) {
this.config.onReady(this);
}
}
/** 检查是否就绪 */
isReady(): boolean {
return this.ready;
}
// ---- 表管理 ----
/** 创建表 */
async defineTable(name: string, columns: Record<string, ColumnDef>): Promise<void> {
this.ensureReady();
const schema = createSchema(name, columns);
try {
await this.pluginManager.trigger('beforeCreateTable', schema);
await this.engine.createTable(schema);
await this.pluginManager.trigger('afterCreateTable', schema);
} catch (error) {
this._onError(error as Error);
throw error;
}
// 清除缓存
this.tableCache.delete(name);
}
/** 获取表操作对象 */
table(name: string): Table {
this.ensureReady();
let t = this.tableCache.get(name);
if (!t) {
// v0.3.2: 表操作写入后广播变更(多标签页同步)
t = new Table(this.engine, name, this.executor, (tableName) => this.broadcastChange(tableName));
this.tableCache.set(name, t);
}
return t;
}
/** 删除表 */
async dropTable(name: string): Promise<void> {
this.ensureReady();
try {
await this.pluginManager.trigger('beforeDropTable', name);
await this.engine.dropTable(name);
await this.pluginManager.trigger('afterDropTable', name);
} catch (error) {
this._onError(error as Error);
throw error;
}
this.tableCache.delete(name);
}
/** 获取所有表名 */
async getTableNames(): Promise<string[]> {
this.ensureReady();
return this.engine.getTableNames();
}
// ---- SQL 查询 ----
/** 执行 SQL 字符串查询 */
async query(sql: string): Promise<unknown> {
this.ensureReady();
const startTime = this.debug ? Date.now() : 0;
await this.pluginManager.trigger('beforeQuery', sql);
let result: unknown;
try {
// v0.3.0: 支持分号分隔的多语句,逐条顺序执行,返回最后一条的结果
const statements: Statement[] = parseAll(sql);
for (const stmt of statements) {
result = await this.executor.execute(stmt);
// v0.3.2: 写语句广播表变更(多标签页同步)
const table = this.writeStatementTable(stmt);
if (table) this.broadcastChange(table);
}
} catch (error) {
this._onError(error as Error);
throw error;
}
await this.pluginManager.trigger('afterQuery', sql, result);
if (this.debug) {
const elapsed = Date.now() - startTime;
const rows = Array.isArray(result) ? (result as any[]).length : 0;
this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`);
}
return result;
}
// ---- 事务 ----
/** 执行事务 */
async transaction<T>(fn: (trx: import('./transaction/index').Transaction) => Promise<T>): Promise<T> {
this.ensureReady();
await this.pluginManager.trigger('beforeTransaction');
try {
const result = await this.transactionManager.execute(fn);
await this.pluginManager.trigger('afterTransaction');
return result;
} catch (error) {
this._onError(error as Error);
throw error;
}
}
// ---- 导入导出 ----
/** 导出表数据为 JSON */
async exportTable(tableName: string): Promise<Record<string, unknown>[]> {
this.ensureReady();
return this.engine.find(tableName, { table: tableName });
}
/** 导入 JSON 数据到表 */
async importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]> {
this.ensureReady();
try {
return await this.engine.insert(tableName, data);
} catch (error) {
this._onError(error as Error);
throw error;
}
}
/** 导出整个数据库为 JSON */
async exportAll(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureReady();
const result: Record<string, Record<string, unknown>[]> = {};
const names = await this.engine.getTableNames();
for (const name of names) {
result[name] = await this.engine.find(name, { table: name });
}
return result;
}
// ---- 发布订阅 ----
private listeners: Map<string, Set<(data: unknown) => void>> = new Map();
/** 订阅表变更 */
subscribe(tableName: string, callback: (event: { type: string; row?: unknown; table?: string }) => void): () => void {
const key = `change:${tableName}`;
if (!this.listeners.has(key)) this.listeners.set(key, new Set());
this.listeners.get(key)!.add(callback as (data: unknown) => void);
return () => this.listeners.get(key)?.delete(callback as (data: unknown) => void);
}
/** 触发变更事件 */
emit(tableName: string, event: { type: string; row?: unknown; table?: string }): void {
const key = `change:${tableName}`;
this.listeners.get(key)?.forEach((cb) => cb(event));
}
// ---- 多标签页同步(v0.3.2 ----
/** 广播表变更到其他标签页(多标签页同步) */
broadcastChange(tableName: string): void {
if (!this.channel) return;
try {
this.channel.postMessage({ type: 'change', table: tableName });
} catch {
// 广播失败不影响主流程
}
}
/** 写语句对应的表名(多标签页广播用) */
private writeStatementTable(stmt: Statement): string | null {
switch (stmt.type) {
case 'INSERT': return stmt.into;
case 'UPDATE': return stmt.table;
case 'DELETE': return stmt.from;
case 'CREATE_TABLE':
case 'DROP_TABLE':
case 'TRUNCATE_TABLE':
return stmt.name;
case 'ALTER_TABLE': return stmt.name;
case 'CREATE_INDEX':
case 'DROP_INDEX':
return stmt.table;
default:
return null;
}
}
// ---- 迁移 ----
private migrations: Map<number, (db: MetonaSqlark) => Promise<void>> = new Map();
/** 注册迁移 */
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void {
this.migrations.set(version, up);
}
/** 执行迁移到指定版本 */
async migrateTo(targetVersion: number): Promise<void> {
this.ensureReady();
for (const [version, up] of [...this.migrations.entries()].sort((a, b) => a[0] - b[0])) {
if (version <= targetVersion && version > this.version) {
await up(this);
this._version = version;
}
}
}
// ---- 插件 ----
/** 获取插件管理器 */
getPluginManager(): PluginManager {
return this.pluginManager;
}
/** 注册钩子 */
on(hook: import('./constants').HookName, callback: import('./plugin/index').HookCallback): void {
this.pluginManager.on(hook, callback);
}
// ---- 生命周期 ----
/** 关闭数据库 */
async close(): Promise<void> {
if (this.channel) {
this.channel.close();
this.channel = null;
}
this.pluginManager.destroy();
await this.engine.close();
this.tableCache.clear();
this.ready = false;
}
/** 获取底层引擎 */
getEngine(): IStorageEngine {
return this.engine;
}
// ---- 内部 ----
private createEngine(): IStorageEngine {
const mode = this.mode;
const diskEngine = this.config.diskEngine ?? 'indexeddb';
switch (mode) {
case 'memory':
return new MemoryEngine();
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
case 'aria':
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
case 'hybrid':
return new HybridEngine(diskEngine);
default:
throw new DatabaseError(`Unknown storage mode: ${mode}`, 'CONFIG_ERROR');
}
}
private ensureReady(): void {
if (!this.ready) {
throw new DatabaseError('Database not initialized. Call await db.init() first.', 'DB_NOT_READY');
}
}
/** 错误回调分发 */
private _onError(error: Error): void {
if (this.config.onError) {
try { this.config.onError(error); } catch { /* 避免回调自身异常影响主流程 */ }
}
}
/** 调试日志 */
private _debug(msg: string, ...args: unknown[]): void {
if (this.debug) {
// eslint-disable-next-line no-console
console.debug(`[MetonaSqlark:${this.name}] ${msg}`, ...args);
}
}
}
+206 -206
View File
@@ -1,206 +1,206 @@
/**
* AriaEngine Buffer Pool Eviction — LRU 驱逐策略
* @module engine/aria/buffer/eviction
*/
import type { PageHandle } from '../types';
// ---------------------------------------------------------------------------
// LRU 双向链表
// ---------------------------------------------------------------------------
/**
* LRU 链表管理器 — 双向链表,头部是 most recently used,尾部是 least recently used。
*/
export class LRUList {
private head: PageHandle | null = null;
private tail: PageHandle | null = null;
private _size = 0;
get size(): number {
return this._size;
}
/** 将页面移到链表头部。如果是新页面则插入,已存在则移动。 */
moveToHead(page: PageHandle): void {
// 如果已经在头部,无需操作
if (this.head === page) return;
// 检测是否在链表中
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (inList) {
// 先从当前位置移除
this.detach(page);
} else {
this._size++;
}
// 插入头部
page.prev = null;
page.next = this.head;
if (this.head) {
this.head.prev = page;
}
this.head = page;
if (!this.tail) {
this.tail = page;
}
}
/** 从链表中移除页面 */
remove(page: PageHandle): void {
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (!inList) return;
this.detach(page);
this._size = Math.max(0, this._size - 1);
}
/** 内部:只调整指针,不修改 _size */
private detach(page: PageHandle): void {
if (page.prev) {
page.prev.next = page.next;
} else if (this.head === page) {
this.head = page.next;
}
if (page.next) {
page.next.prev = page.prev;
} else if (this.tail === page) {
this.tail = page.prev;
}
page.prev = null;
page.next = null;
}
/** 获取 LRU 尾部(最久未使用的页面) */
getLRU(): PageHandle | null {
return this.tail;
}
/** 弹出 LRU 尾部 */
popLRU(): PageHandle | null {
const lru = this.tail;
if (lru) {
this.remove(lru);
}
return lru;
}
/** 清空链表 */
clear(): void {
this.head = null;
this.tail = null;
this._size = 0;
}
/** 获取所有页面(用于迭代) */
getAllPages(): PageHandle[] {
const pages: PageHandle[] = [];
let current = this.head;
while (current) {
pages.push(current);
current = current.next;
}
return pages;
}
}
// ---------------------------------------------------------------------------
// Eviction 策略
// ---------------------------------------------------------------------------
export type EvictionCallback = (page: PageHandle) => Promise<void>;
/**
* 驱逐管理器 — 当 Buffer Pool 满时驱逐页面。
*/
export class EvictionManager {
private lru: LRUList;
private onEvict: EvictionCallback;
private capacity: number;
constructor(capacity: number, onEvict: EvictionCallback) {
this.lru = new LRUList();
this.capacity = capacity;
this.onEvict = onEvict;
}
/** 访问页面,更新 LRU */
access(page: PageHandle): void {
page.lastAccess = Date.now();
this.lru.moveToHead(page);
}
/** 添加新页面到池中 */
add(page: PageHandle): void {
this.access(page);
}
/** 移除指定页面 */
remove(page: PageHandle): void {
this.lru.remove(page);
}
/**
* 驱逐页面直到池中有足够空间。
* 只驱逐未 pin 的干净页面(dirty=false)。
* 如果没有干净页面可驱逐,尝试刷脏页。
*/
async evictIfNeeded(count: number): Promise<number> {
let evicted = 0;
while (this.lru.size + count > this.capacity && this.lru.size > 0) {
// 找到可驱逐的页面
const victim = this.findEvictionCandidate();
if (!victim) break;
// 脏页先刷盘
if (victim.dirty) {
await this.onEvict(victim);
victim.dirty = false;
}
this.lru.remove(victim);
evicted++;
}
return evicted;
}
/** 查找驱逐候选(优先干净页面,然后最久未用的脏页) */
private findEvictionCandidate(): PageHandle | null {
// 先从尾部找未 pin 的干净页面
let current = this.lru.getLRU();
while (current) {
if (current.pins === 0 && !current.dirty) return current;
current = current.prev;
}
// 没有干净页,找未 pin 的脏页
current = this.lru.getLRU();
while (current) {
if (current.pins === 0) return current;
current = current.prev;
}
return null;
}
/** 获取当前大小 */
getSize(): number {
return this.lru.size;
}
/** 获取容量 */
getCapacity(): number {
return this.capacity;
}
/** 清空 */
clear(): void {
this.lru.clear();
}
}
/**
* AriaEngine Buffer Pool Eviction — LRU 驱逐策略
* @module engine/aria/buffer/eviction
*/
import type { PageHandle } from '../types';
// ---------------------------------------------------------------------------
// LRU 双向链表
// ---------------------------------------------------------------------------
/**
* LRU 链表管理器 — 双向链表,头部是 most recently used,尾部是 least recently used。
*/
export class LRUList {
private head: PageHandle | null = null;
private tail: PageHandle | null = null;
private _size = 0;
get size(): number {
return this._size;
}
/** 将页面移到链表头部。如果是新页面则插入,已存在则移动。 */
moveToHead(page: PageHandle): void {
// 如果已经在头部,无需操作
if (this.head === page) return;
// 检测是否在链表中
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (inList) {
// 先从当前位置移除
this.detach(page);
} else {
this._size++;
}
// 插入头部
page.prev = null;
page.next = this.head;
if (this.head) {
this.head.prev = page;
}
this.head = page;
if (!this.tail) {
this.tail = page;
}
}
/** 从链表中移除页面 */
remove(page: PageHandle): void {
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (!inList) return;
this.detach(page);
this._size = Math.max(0, this._size - 1);
}
/** 内部:只调整指针,不修改 _size */
private detach(page: PageHandle): void {
if (page.prev) {
page.prev.next = page.next;
} else if (this.head === page) {
this.head = page.next;
}
if (page.next) {
page.next.prev = page.prev;
} else if (this.tail === page) {
this.tail = page.prev;
}
page.prev = null;
page.next = null;
}
/** 获取 LRU 尾部(最久未使用的页面) */
getLRU(): PageHandle | null {
return this.tail;
}
/** 弹出 LRU 尾部 */
popLRU(): PageHandle | null {
const lru = this.tail;
if (lru) {
this.remove(lru);
}
return lru;
}
/** 清空链表 */
clear(): void {
this.head = null;
this.tail = null;
this._size = 0;
}
/** 获取所有页面(用于迭代) */
getAllPages(): PageHandle[] {
const pages: PageHandle[] = [];
let current = this.head;
while (current) {
pages.push(current);
current = current.next;
}
return pages;
}
}
// ---------------------------------------------------------------------------
// Eviction 策略
// ---------------------------------------------------------------------------
export type EvictionCallback = (page: PageHandle) => Promise<void>;
/**
* 驱逐管理器 — 当 Buffer Pool 满时驱逐页面。
*/
export class EvictionManager {
private lru: LRUList;
private onEvict: EvictionCallback;
private capacity: number;
constructor(capacity: number, onEvict: EvictionCallback) {
this.lru = new LRUList();
this.capacity = capacity;
this.onEvict = onEvict;
}
/** 访问页面,更新 LRU */
access(page: PageHandle): void {
page.lastAccess = Date.now();
this.lru.moveToHead(page);
}
/** 添加新页面到池中 */
add(page: PageHandle): void {
this.access(page);
}
/** 移除指定页面 */
remove(page: PageHandle): void {
this.lru.remove(page);
}
/**
* 驱逐页面直到池中有足够空间。
* 只驱逐未 pin 的干净页面(dirty=false)。
* 如果没有干净页面可驱逐,尝试刷脏页。
*/
async evictIfNeeded(count: number): Promise<number> {
let evicted = 0;
while (this.lru.size + count > this.capacity && this.lru.size > 0) {
// 找到可驱逐的页面
const victim = this.findEvictionCandidate();
if (!victim) break;
// 脏页先刷盘
if (victim.dirty) {
await this.onEvict(victim);
victim.dirty = false;
}
this.lru.remove(victim);
evicted++;
}
return evicted;
}
/** 查找驱逐候选(优先干净页面,然后最久未用的脏页) */
private findEvictionCandidate(): PageHandle | null {
// 先从尾部找未 pin 的干净页面
let current = this.lru.getLRU();
while (current) {
if (current.pins === 0 && !current.dirty) return current;
current = current.prev;
}
// 没有干净页,找未 pin 的脏页
current = this.lru.getLRU();
while (current) {
if (current.pins === 0) return current;
current = current.prev;
}
return null;
}
/** 获取当前大小 */
getSize(): number {
return this.lru.size;
}
/** 获取容量 */
getCapacity(): number {
return this.capacity;
}
/** 清空 */
clear(): void {
this.lru.clear();
}
}
+185 -185
View File
@@ -1,185 +1,185 @@
/**
* AriaEngine Buffer Pool — 页面缓存池
* @module engine/aria/buffer/pool
*
* 管理固定数量页面的 LRU 缓存,减少对底层储存的访问。
*/
import type { PageHandle } from '../types';
import { PageType, DEFAULT_BUFFER_POOL_PAGES } from '../types';
import { createPage } from '../page/format';
import { EvictionManager } from './eviction';
// ---------------------------------------------------------------------------
// Page Read / Write 回调
// ---------------------------------------------------------------------------
export interface PageIO {
/** 从存储后端读取页面 */
readPage(pageId: number): Promise<ArrayBuffer | null>;
/** 将页面写入存储后端 */
writePage(pageId: number, data: ArrayBuffer): Promise<void>;
/** 分配新页面 ID */
allocatePageId(): Promise<number>;
/** 释放页面 ID */
freePageId(pageId: number): Promise<void>;
}
// ---------------------------------------------------------------------------
// Buffer Pool
// ---------------------------------------------------------------------------
export class BufferPool {
private pages: Map<number, PageHandle> = new Map();
private eviction: EvictionManager;
private pageIO: PageIO;
private nextPageId = 0;
constructor(pageIO: PageIO, capacity: number = DEFAULT_BUFFER_POOL_PAGES) {
this.pageIO = pageIO;
this.eviction = new EvictionManager(capacity, async (page) => {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
});
}
// -----------------------------------------------------------------------
// 页面获取
// -----------------------------------------------------------------------
/**
* 获取页面(必要时从磁盘读取)。
* 返回 pin 的页面,使用完成后必须调用 unpin()。
*/
async getPage(pageId: number): Promise<PageHandle | null> {
// 已在池中
let page = this.pages.get(pageId);
if (page) {
this.eviction.access(page);
page.pins++;
return page;
}
// 需要从磁盘加载
const buffer = await this.pageIO.readPage(pageId);
if (!buffer) return null;
// 确保有空间
await this.eviction.evictIfNeeded(1);
const type = new DataView(buffer).getUint8(4) as PageType;
page = {
pageId,
type,
data: buffer,
dirty: false,
pins: 1,
prev: null,
next: null,
lastAccess: Date.now(),
};
this.pages.set(pageId, page);
this.eviction.add(page);
return page;
}
/**
* 创建新页面。
*/
async newPage(type: PageType = PageType.DATA): Promise<PageHandle> {
const pageId = await this.pageIO.allocatePageId();
await this.eviction.evictIfNeeded(1);
const page = createPage(pageId, type);
page.pins = 1;
this.pages.set(pageId, page);
this.eviction.add(page);
return page;
}
/**
* 释放页面的 pin。
*/
unpin(page: PageHandle): void {
if (page.pins > 0) {
page.pins--;
}
}
/**
* 标记页面为脏(需要写回)。
*/
markDirty(page: PageHandle): void {
page.dirty = true;
}
/**
* 将脏页面刷新到磁盘。
*/
async flushPage(pageId: number): Promise<void> {
const page = this.pages.get(pageId);
if (page && page.dirty) {
await this.pageIO.writePage(pageId, page.data);
page.dirty = false;
}
}
/**
* 刷新所有脏页面。
*/
async flushAll(): Promise<void> {
for (const [, page] of this.pages) {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
}
}
/**
* 从缓存中删除指定页面(不刷盘)。
*/
removePage(pageId: number): void {
const page = this.pages.get(pageId);
if (page) {
this.eviction.remove(page);
this.pages.delete(pageId);
}
}
/**
* 清空缓存池(先刷脏页)。
*/
async clear(): Promise<void> {
await this.flushAll();
this.pages.clear();
this.eviction.clear();
}
// -----------------------------------------------------------------------
// 统计
// -----------------------------------------------------------------------
/** 获取当前缓存页面数 */
getCachedPageCount(): number {
return this.pages.size;
}
/** 获取缓存容量 */
getCapacity(): number {
return this.eviction.getCapacity();
}
/** 获取脏页面数 */
getDirtyPageCount(): number {
let count = 0;
for (const [, page] of this.pages) {
if (page.dirty) count++;
}
return count;
}
}
/**
* AriaEngine Buffer Pool — 页面缓存池
* @module engine/aria/buffer/pool
*
* 管理固定数量页面的 LRU 缓存,减少对底层储存的访问。
*/
import type { PageHandle } from '../types';
import { PageType, DEFAULT_BUFFER_POOL_PAGES } from '../types';
import { createPage } from '../page/format';
import { EvictionManager } from './eviction';
// ---------------------------------------------------------------------------
// Page Read / Write 回调
// ---------------------------------------------------------------------------
export interface PageIO {
/** 从存储后端读取页面 */
readPage(pageId: number): Promise<ArrayBuffer | null>;
/** 将页面写入存储后端 */
writePage(pageId: number, data: ArrayBuffer): Promise<void>;
/** 分配新页面 ID */
allocatePageId(): Promise<number>;
/** 释放页面 ID */
freePageId(pageId: number): Promise<void>;
}
// ---------------------------------------------------------------------------
// Buffer Pool
// ---------------------------------------------------------------------------
export class BufferPool {
private pages: Map<number, PageHandle> = new Map();
private eviction: EvictionManager;
private pageIO: PageIO;
private nextPageId = 0;
constructor(pageIO: PageIO, capacity: number = DEFAULT_BUFFER_POOL_PAGES) {
this.pageIO = pageIO;
this.eviction = new EvictionManager(capacity, async (page) => {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
});
}
// -----------------------------------------------------------------------
// 页面获取
// -----------------------------------------------------------------------
/**
* 获取页面(必要时从磁盘读取)。
* 返回 pin 的页面,使用完成后必须调用 unpin()。
*/
async getPage(pageId: number): Promise<PageHandle | null> {
// 已在池中
let page = this.pages.get(pageId);
if (page) {
this.eviction.access(page);
page.pins++;
return page;
}
// 需要从磁盘加载
const buffer = await this.pageIO.readPage(pageId);
if (!buffer) return null;
// 确保有空间
await this.eviction.evictIfNeeded(1);
const type = new DataView(buffer).getUint8(4) as PageType;
page = {
pageId,
type,
data: buffer,
dirty: false,
pins: 1,
prev: null,
next: null,
lastAccess: Date.now(),
};
this.pages.set(pageId, page);
this.eviction.add(page);
return page;
}
/**
* 创建新页面。
*/
async newPage(type: PageType = PageType.DATA): Promise<PageHandle> {
const pageId = await this.pageIO.allocatePageId();
await this.eviction.evictIfNeeded(1);
const page = createPage(pageId, type);
page.pins = 1;
this.pages.set(pageId, page);
this.eviction.add(page);
return page;
}
/**
* 释放页面的 pin。
*/
unpin(page: PageHandle): void {
if (page.pins > 0) {
page.pins--;
}
}
/**
* 标记页面为脏(需要写回)。
*/
markDirty(page: PageHandle): void {
page.dirty = true;
}
/**
* 将脏页面刷新到磁盘。
*/
async flushPage(pageId: number): Promise<void> {
const page = this.pages.get(pageId);
if (page && page.dirty) {
await this.pageIO.writePage(pageId, page.data);
page.dirty = false;
}
}
/**
* 刷新所有脏页面。
*/
async flushAll(): Promise<void> {
for (const [, page] of this.pages) {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
}
}
/**
* 从缓存中删除指定页面(不刷盘)。
*/
removePage(pageId: number): void {
const page = this.pages.get(pageId);
if (page) {
this.eviction.remove(page);
this.pages.delete(pageId);
}
}
/**
* 清空缓存池(先刷脏页)。
*/
async clear(): Promise<void> {
await this.flushAll();
this.pages.clear();
this.eviction.clear();
}
// -----------------------------------------------------------------------
// 统计
// -----------------------------------------------------------------------
/** 获取当前缓存页面数 */
getCachedPageCount(): number {
return this.pages.size;
}
/** 获取缓存容量 */
getCapacity(): number {
return this.eviction.getCapacity();
}
/** 获取脏页面数 */
getDirtyPageCount(): number {
let count = 0;
for (const [, page] of this.pages) {
if (page.dirty) count++;
}
return count;
}
}
+39 -19
View File
@@ -2,39 +2,48 @@
* AriaEngine LZ4 Compression — 简化 LZ4 压缩/解压
* @module engine/aria/compression/lz4
*
* v0.2.6: 修复往返一致性
* - 匹配长度截断到 19 字节(matchField 上限 15 + MIN_MATCH),长匹配分段输出
* - 组合 token 的 matchField ∈ [1,15]matchField=0 且 lo=0 表示末尾纯字面量(无 offset)
* - 消除"matchField=0 的组合 token 与纯字面量 token 歧义"
*
* Token 格式(1 字节):
* hi 4bit = litLen (0-15)
* lo 4bit = matchField (0-15, 实际匹配 = field+4)
* lo 4bit = matchField (1-15, 实际匹配 = field+4)
*
* 字面量-匹配序列: [token] [litLen bytes] [2B LE offset]
* 末尾纯字面量: [token with lo=0] [litLen bytes] ← 仅在流末尾出现
*/
const MIN_MATCH = 4;
const MAX_MATCH = MIN_MATCH + 15; // 19,匹配长度上限
export function compressLZ4(input: Uint8Array): Uint8Array {
if (input.byteLength < MIN_MATCH) return input;
// 空输入直接返回(无 token 可输出)
if (input.byteLength === 0) return input;
const maxOut = input.byteLength + (input.byteLength >> 8) + 32;
// 最坏情况:纯字面量分块输出 len/15 个 token + 末尾 token
// 上限:len + ceil(len/15) + 8(组合 token 的 offset 开销已包含在内)
const maxOut = input.byteLength + Math.ceil(input.byteLength / 15) + 8;
const out = new Uint8Array(maxOut);
let si = 0, di = 0;
let litStart = 0;
while (si < input.byteLength) {
// 搜索最长 backward match
// 搜索最长 backward match(截断到 MAX_MATCH,避免 token 字段溢出)
let bestLen = 0, bestOff = 0;
const searchStart = Math.max(0, si - 65535);
for (let p = searchStart; p < si; p++) {
let ml = 0;
while (si + ml < input.byteLength && p + ml < si &&
input[p + ml] === input[si + ml] && ml < 255) ml++;
input[p + ml] === input[si + ml] && ml < MAX_MATCH) ml++;
if (ml >= MIN_MATCH && ml > bestLen) { bestLen = ml; bestOff = si - p; }
}
if (bestLen >= MIN_MATCH && (si - litStart) <= 15) {
// 有匹配 → 输出组合 token(字面量+匹配)
// 仅当匹配完整可编码(field 1-15)且字面量不超过 15 时才输出组合 token
if (bestLen > MIN_MATCH && (si - litStart) <= 15) {
const litLen = si - litStart;
const matchField = Math.min(bestLen - MIN_MATCH, 15);
const matchField = bestLen - MIN_MATCH; // 1..15
out[di++] = ((litLen & 0x0F) << 4) | (matchField & 0x0F);
for (let j = 0; j < litLen; j++) out[di++] = input[litStart + j];
out[di++] = bestOff & 0xFF;
@@ -42,8 +51,15 @@ export function compressLZ4(input: Uint8Array): Uint8Array {
si += bestLen;
litStart = si;
} else {
// 无匹配或字面量已满 15 → 继续累积(不单独输出,等下个匹配合并)
// 无匹配 / 匹配长度 4(field=0 有歧义)→ 继续累积字面量
si++;
// 字面量达到 15 字节上限:结清为纯字面量 token(lo=0),
// 否则后续组合 token 的字面量长度会超过 token 字段上限
if (si - litStart >= 15) {
out[di++] = (15 & 0x0F) << 4; // lo=0 无匹配
for (let j = 0; j < 15; j++) out[di++] = input[litStart + j];
litStart = si;
}
}
}
@@ -57,7 +73,10 @@ export function compressLZ4(input: Uint8Array): Uint8Array {
litStart += chunk;
}
return di >= input.byteLength ? input : out.slice(0, di);
// 始终输出压缩流(即使比原数据略大)。
// 注意:不能返回原样 input —— 解压端无法区分"压缩流"与"原始数据"
// 原样返回会导致解压器将原始字节误解析为 token(v0.2.6 修复)
return out.slice(0, di);
}
export function decompressLZ4(input: Uint8Array, originalSize: number): Uint8Array {
@@ -74,16 +93,17 @@ export function decompressLZ4(input: Uint8Array, originalSize: number): Uint8Arr
out[di++] = input[si++];
}
if (di >= originalSize || si >= input.byteLength) break;
// matchField=0:纯字面量 token(无 offset 无匹配)。
// 可能出现在流中任意位置(超长字面量分块输出),不能 break
if (matchField === 0) continue;
// 非末尾 → 必有 offset + 匹配(即使 matchField==0 也复制 MIN_MATCH 字节
if (si + 1 < input.byteLength) {
const offset = input[si++] | (input[si++] << 8);
const matchLen = matchField + MIN_MATCH;
for (let i = 0; i < matchLen && di < originalSize; i++) {
out[di] = out[di - offset];
di++;
}
// 组合 token:读取 offset + 复制匹配(可能自重叠
if (si + 1 >= input.byteLength) break;
const offset = input[si++] | (input[si++] << 8);
const matchLen = matchField + MIN_MATCH;
for (let i = 0; i < matchLen && di < originalSize; i++) {
out[di] = out[di - offset];
di++;
}
}
+1195 -1080
View File
File diff suppressed because it is too large Load Diff
+123 -123
View File
@@ -1,123 +1,123 @@
/**
* AriaEngine Bloom Filter key
* @module engine/aria/index/bloom
*
* 使 + Kirsch-Mitzenmacher k
*/
import { DEFAULT_BLOOM_BITS_PER_KEY } from '../types';
// ---------------------------------------------------------------------------
// BloomFilter
// ---------------------------------------------------------------------------
export class BloomFilter {
private bits: Uint8Array;
private numHashes: number;
private _inserted = 0;
/**
* @param numKeys key
* @param bitsPerKey key 10 1%
*/
constructor(numKeys: number, bitsPerKey: number = DEFAULT_BLOOM_BITS_PER_KEY) {
// ceil(numKeys * bitsPerKey / 8),最少 64 位
const numBits = Math.max(64, numKeys * bitsPerKey);
const numBytes = Math.ceil(numBits / 8);
this.bits = new Uint8Array(numBytes);
// k = bitsPerKey * ln(2) ≈ bitsPerKey * 0.69
this.numHashes = Math.max(1, Math.floor(bitsPerKey * 0.69));
}
/** 从现有数据恢复 */
static fromData(data: Uint8Array, numHashes: number): BloomFilter {
const bf = new BloomFilter(1); // dummy
bf.bits = data;
bf.numHashes = numHashes;
return bf;
}
/** 插入 key */
insert(key: string): void {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
this.bits[byteIdx] |= (1 << bitIdx);
}
this._inserted++;
}
/** 检查 key 可能存在(false positive 可能,false negative 不可能) */
mayContain(key: string): boolean {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
if ((this.bits[byteIdx] & (1 << bitIdx)) === 0) {
return false; // 确定不存在
}
}
return true; // 可能存在
}
/** 获取序列化数据 */
serialize(): Uint8Array {
return this.bits;
}
/** bit 数组大小 */
getBitSize(): number {
return this.bits.byteLength * 8;
}
/** 已插入 key 数量 */
getInsertedCount(): number {
return this._inserted;
}
/** hash 函数数量 */
getHashCount(): number {
return this.numHashes;
}
// -----------------------------------------------------------------------
// 哈希
// -----------------------------------------------------------------------
private getHashes(key: string): number[] {
const bits = this.bits.byteLength * 8;
const h1 = this.fnv1a(key);
const h2 = this.murmurSimple(key);
const hashes: number[] = [];
for (let i = 0; i < this.numHashes; i++) {
// Kirsch-Mitzenmacher: h_i = h1 + i * h2
const h = Math.abs((h1 + i * h2) % bits);
hashes.push(h);
}
return hashes;
}
/** FNV-1a 哈希 */
private fnv1a(str: string): number {
let hash = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash = (hash * 0x01000193) >>> 0;
}
return hash;
}
/** 简化的 Murmur-like 哈希 */
private murmurSimple(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
hash = ((hash << 5) - hash + ch) | 0;
hash = (hash ^ (hash >>> 16)) >>> 0;
}
return Math.abs(hash);
}
}
/**
* AriaEngine Bloom Filter key
* @module engine/aria/index/bloom
*
* 使 + Kirsch-Mitzenmacher k
*/
import { DEFAULT_BLOOM_BITS_PER_KEY } from '../types';
// ---------------------------------------------------------------------------
// BloomFilter
// ---------------------------------------------------------------------------
export class BloomFilter {
private bits: Uint8Array;
private numHashes: number;
private _inserted = 0;
/**
* @param numKeys key
* @param bitsPerKey key 10 1%
*/
constructor(numKeys: number, bitsPerKey: number = DEFAULT_BLOOM_BITS_PER_KEY) {
// ceil(numKeys * bitsPerKey / 8),最少 64 位
const numBits = Math.max(64, numKeys * bitsPerKey);
const numBytes = Math.ceil(numBits / 8);
this.bits = new Uint8Array(numBytes);
// k = bitsPerKey * ln(2) ≈ bitsPerKey * 0.69
this.numHashes = Math.max(1, Math.floor(bitsPerKey * 0.69));
}
/** 从现有数据恢复 */
static fromData(data: Uint8Array, numHashes: number): BloomFilter {
const bf = new BloomFilter(1); // dummy
bf.bits = data;
bf.numHashes = numHashes;
return bf;
}
/** 插入 key */
insert(key: string): void {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
this.bits[byteIdx] |= (1 << bitIdx);
}
this._inserted++;
}
/** 检查 key 可能存在(false positive 可能,false negative 不可能) */
mayContain(key: string): boolean {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
if ((this.bits[byteIdx] & (1 << bitIdx)) === 0) {
return false; // 确定不存在
}
}
return true; // 可能存在
}
/** 获取序列化数据 */
serialize(): Uint8Array {
return this.bits;
}
/** bit 数组大小 */
getBitSize(): number {
return this.bits.byteLength * 8;
}
/** 已插入 key 数量 */
getInsertedCount(): number {
return this._inserted;
}
/** hash 函数数量 */
getHashCount(): number {
return this.numHashes;
}
// -----------------------------------------------------------------------
// 哈希
// -----------------------------------------------------------------------
private getHashes(key: string): number[] {
const bits = this.bits.byteLength * 8;
const h1 = this.fnv1a(key);
const h2 = this.murmurSimple(key);
const hashes: number[] = [];
for (let i = 0; i < this.numHashes; i++) {
// Kirsch-Mitzenmacher: h_i = h1 + i * h2
const h = Math.abs((h1 + i * h2) % bits);
hashes.push(h);
}
return hashes;
}
/** FNV-1a 哈希 */
private fnv1a(str: string): number {
let hash = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash = (hash * 0x01000193) >>> 0;
}
return hash;
}
/** 简化的 Murmur-like 哈希 */
private murmurSimple(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
hash = ((hash << 5) - hash + ch) | 0;
hash = (hash ^ (hash >>> 16)) >>> 0;
}
return Math.abs(hash);
}
}
File diff suppressed because it is too large Load Diff
+467 -467
View File
@@ -1,467 +1,467 @@
/**
* AriaEngine MemTable
* @module engine/aria/index/memtable
*
* MemTable flush SSTable
*/
// ---------------------------------------------------------------------------
// RB-Tree Node
// ---------------------------------------------------------------------------
enum Color { RED, BLACK }
class RBNode<K, V> {
key: K;
value: V;
color: Color = Color.RED;
left: RBNode<K, V> | null = null;
right: RBNode<K, V> | null = null;
parent: RBNode<K, V> | null = null;
constructor(key: K, value: V) {
this.key = key;
this.value = value;
}
}
// ---------------------------------------------------------------------------
// Red-Black Tree
// ---------------------------------------------------------------------------
class RedBlackTree<K, V> {
private root: RBNode<K, V> | null = null;
private _size = 0;
get size(): number { return this._size; }
// ---- 插入 ----
insert(key: K, value: V): void {
const node = new RBNode(key, value);
if (!this.root) {
this.root = node;
node.color = Color.BLACK;
this._size++;
return;
}
let parent: RBNode<K, V> | null = null;
let current: RBNode<K, V> | null = this.root;
while (current) {
parent = current;
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
// 更新已存在的 key
current.value = value;
return;
}
}
node.parent = parent;
if (key < parent!.key) {
parent!.left = node;
} else {
parent!.right = node;
}
this._size++;
this.fixInsert(node);
}
// ---- 查找 ----
find(key: K): V | null {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
return current.value;
}
}
return null;
}
// ---- 删除 ----
delete(key: K): boolean {
// 简化实现:标记删除(实际改为找到并调整树)
const node = this.findNode(key);
if (!node) return false;
this.deleteNode(node);
this._size--;
return true;
}
// ---- 遍历 ----
/** 中序遍历(有序) */
inorder(callback: (key: K, value: V) => void): void {
this._inorder(this.root, callback);
}
/** 范围遍历 */
rangeScan(
startKey: K,
endKey: K,
callback: (key: K, value: V) => void,
): void {
this._rangeScan(this.root, startKey, endKey, callback);
}
/** 获取所有条目 */
getAllEntries(): [K, V][] {
const entries: [K, V][] = [];
this.inorder((k, v) => entries.push([k, v]));
return entries;
}
/** 清空 */
clear(): void {
this.root = null;
this._size = 0;
}
// ---- 内部方法 ----
private findNode(key: K): RBNode<K, V> | null {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
return current;
}
}
return null;
}
private deleteNode(node: RBNode<K, V>): void {
// 简化:用左子树最大或右子树最小替换
// 完整实现较复杂,这里采用简化策略
if (!node.left && !node.right) {
this.transplant(node, null);
if (node.color === Color.BLACK) this.fixDelete(null, node.parent);
} else if (!node.left) {
this.transplant(node, node.right);
if (node.color === Color.BLACK) this.fixDelete(node.right, node.right!.parent);
} else if (!node.right) {
this.transplant(node, node.left);
if (node.color === Color.BLACK) this.fixDelete(node.left, node.left!.parent);
} else {
const successor = this.minimum(node.right);
if (successor!.parent !== node) {
this.transplant(successor!, successor!.right);
successor!.right = node.right;
successor!.right!.parent = successor;
}
this.transplant(node, successor);
successor!.left = node.left;
successor!.left!.parent = successor;
const origColor = successor!.color;
successor!.color = node.color;
if (origColor === Color.BLACK) this.fixDelete(successor!.right, successor!.right?.parent ?? null);
}
}
private transplant(u: RBNode<K, V> | null, v: RBNode<K, V> | null): void {
if (!u!.parent) {
this.root = v;
} else if (u === u!.parent.left) {
u!.parent.left = v;
} else {
u!.parent.right = v;
}
if (v) v.parent = u!.parent;
}
private minimum(node: RBNode<K, V>): RBNode<K, V> {
while (node.left) node = node.left;
return node;
}
private fixInsert(node: RBNode<K, V>): void {
while (node.parent && node.parent.color === Color.RED) {
const parent = node.parent;
const grandparent = parent.parent;
if (!grandparent) break;
if (parent === grandparent.left) {
const uncle = grandparent.right;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
} else {
if (node === parent.right) {
node = parent;
this.rotateLeft(node);
}
if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateRight(node.parent.parent);
}
} else {
const uncle = grandparent.left;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
} else {
if (node === parent.left) {
node = parent;
this.rotateRight(node);
}
if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateLeft(node.parent.parent);
}
}
}
if (this.root) this.root.color = Color.BLACK;
}
private fixDelete(x: RBNode<K, V> | null, parent: RBNode<K, V> | null): void {
// 标准 RB-Tree 删除修复(修复"双黑"问题)
let node = x;
let nodeParent = parent;
while ((!node || node.color === Color.BLACK) && node !== this.root) {
if (!nodeParent) break;
if (node === nodeParent.left) {
let sibling = nodeParent.right;
if (!sibling) break;
// Case 1: 兄弟是红色
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateLeft(nodeParent);
sibling = nodeParent.right;
if (!sibling) break;
}
// Case 2: 兄弟的两个子节点都是黑色
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
} else {
// Case 3: 兄弟右子黑色(左子红色)
if (!sibRight || sibRight.color === Color.BLACK) {
if (sibLeft) sibLeft.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateRight(sibling);
sibling = nodeParent.right;
if (!sibling) break;
}
// Case 4: 兄弟右子红色
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.right) sibling.right.color = Color.BLACK;
this.rotateLeft(nodeParent);
node = this.root;
}
} else {
// 镜像:node 是父节点的右子
let sibling = nodeParent.left;
if (!sibling) break;
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateRight(nodeParent);
sibling = nodeParent.left;
if (!sibling) break;
}
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
} else {
if (!sibLeft || sibLeft.color === Color.BLACK) {
if (sibRight) sibRight.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateLeft(sibling);
sibling = nodeParent.left;
if (!sibling) break;
}
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.left) sibling.left.color = Color.BLACK;
this.rotateRight(nodeParent);
node = this.root;
}
}
}
if (node) node.color = Color.BLACK;
}
private rotateLeft(x: RBNode<K, V>): void {
const y = x.right;
if (!y) return;
x.right = y.left;
if (y.left) y.left.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
} else if (x === x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
y.left = x;
x.parent = y;
}
private rotateRight(x: RBNode<K, V>): void {
const y = x.left;
if (!y) return;
x.left = y.right;
if (y.right) y.right.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
} else if (x === x.parent.right) {
x.parent.right = y;
} else {
x.parent.left = y;
}
y.right = x;
x.parent = y;
}
private _inorder(node: RBNode<K, V> | null, cb: (k: K, v: V) => void): void {
if (!node) return;
this._inorder(node.left, cb);
cb(node.key, node.value);
this._inorder(node.right, cb);
}
private _rangeScan(
node: RBNode<K, V> | null,
start: K,
end: K,
cb: (k: K, v: V) => void,
): void {
if (!node) return;
if (node.key > start) this._rangeScan(node.left, start, end, cb);
if (node.key >= start && node.key <= end) cb(node.key, node.value);
if (node.key < end) this._rangeScan(node.right, start, end, cb);
}
}
// ---------------------------------------------------------------------------
// MemTable
// ---------------------------------------------------------------------------
export class MemTable {
private tree: RedBlackTree<string, Record<string, unknown>>;
private _estimatedSize = 0;
private maxSize: number;
constructor(maxSize: number = 4 * 1024 * 1024) {
this.tree = new RedBlackTree();
this.maxSize = maxSize;
}
/** 插入或更新 */
put(key: string, value: Record<string, unknown>): void {
const oldSize = this.estimateEntrySize(key, this.tree.find(key));
const newSize = this.estimateEntrySize(key, value);
this.tree.insert(key, value);
this._estimatedSize += newSize - oldSize;
}
/** 获取 */
get(key: string): Record<string, unknown> | null {
return this.tree.find(key);
}
/** 删除 */
delete(key: string): boolean {
const oldVal = this.tree.find(key);
if (oldVal) {
this._estimatedSize -= this.estimateEntrySize(key, oldVal);
}
return this.tree.delete(key);
}
/** 是否应刷盘 */
shouldFlush(): boolean {
return this._estimatedSize >= this.maxSize;
}
/** 获取所有有序条目 */
getAllEntries(): [string, Record<string, unknown>][] {
return this.tree.getAllEntries();
}
/** 范围扫描 */
rangeScan(
startKey: string,
endKey: string,
): [string, Record<string, unknown>][] {
const entries: [string, Record<string, unknown>][] = [];
this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
return entries;
}
/** 条目数 */
getEntryCount(): number {
return this.tree.size;
}
/** 估计大小(字节) */
getEstimatedSize(): number {
return this._estimatedSize;
}
/** 清空 */
clear(): void {
this.tree.clear();
this._estimatedSize = 0;
}
/** 检查 key 是否存在 */
contains(key: string): boolean {
return this.tree.find(key) !== null;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private estimateEntrySize(key: string, value: Record<string, unknown> | null): number {
if (!value) return 0;
let size = key.length * 2; // UTF-16
for (const entry of Object.entries(value)) {
size += entry[0].length * 2;
const v = entry[1];
if (typeof v === 'string') size += v.length * 2;
else if (typeof v === 'number') size += 8;
else if (typeof v === 'boolean') size += 1;
else if (v === null || v === undefined) size += 1;
else size += 16; // rough estimate
}
return size;
}
}
/**
* AriaEngine MemTable
* @module engine/aria/index/memtable
*
* MemTable flush SSTable
*/
// ---------------------------------------------------------------------------
// RB-Tree Node
// ---------------------------------------------------------------------------
enum Color { RED, BLACK }
class RBNode<K, V> {
key: K;
value: V;
color: Color = Color.RED;
left: RBNode<K, V> | null = null;
right: RBNode<K, V> | null = null;
parent: RBNode<K, V> | null = null;
constructor(key: K, value: V) {
this.key = key;
this.value = value;
}
}
// ---------------------------------------------------------------------------
// Red-Black Tree
// ---------------------------------------------------------------------------
class RedBlackTree<K, V> {
private root: RBNode<K, V> | null = null;
private _size = 0;
get size(): number { return this._size; }
// ---- 插入 ----
insert(key: K, value: V): void {
const node = new RBNode(key, value);
if (!this.root) {
this.root = node;
node.color = Color.BLACK;
this._size++;
return;
}
let parent: RBNode<K, V> | null = null;
let current: RBNode<K, V> | null = this.root;
while (current) {
parent = current;
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
// 更新已存在的 key
current.value = value;
return;
}
}
node.parent = parent;
if (key < parent!.key) {
parent!.left = node;
} else {
parent!.right = node;
}
this._size++;
this.fixInsert(node);
}
// ---- 查找 ----
find(key: K): V | null {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
return current.value;
}
}
return null;
}
// ---- 删除 ----
delete(key: K): boolean {
// 简化实现:标记删除(实际改为找到并调整树)
const node = this.findNode(key);
if (!node) return false;
this.deleteNode(node);
this._size--;
return true;
}
// ---- 遍历 ----
/** 中序遍历(有序) */
inorder(callback: (key: K, value: V) => void): void {
this._inorder(this.root, callback);
}
/** 范围遍历 */
rangeScan(
startKey: K,
endKey: K,
callback: (key: K, value: V) => void,
): void {
this._rangeScan(this.root, startKey, endKey, callback);
}
/** 获取所有条目 */
getAllEntries(): [K, V][] {
const entries: [K, V][] = [];
this.inorder((k, v) => entries.push([k, v]));
return entries;
}
/** 清空 */
clear(): void {
this.root = null;
this._size = 0;
}
// ---- 内部方法 ----
private findNode(key: K): RBNode<K, V> | null {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
return current;
}
}
return null;
}
private deleteNode(node: RBNode<K, V>): void {
// 简化:用左子树最大或右子树最小替换
// 完整实现较复杂,这里采用简化策略
if (!node.left && !node.right) {
this.transplant(node, null);
if (node.color === Color.BLACK) this.fixDelete(null, node.parent);
} else if (!node.left) {
this.transplant(node, node.right);
if (node.color === Color.BLACK) this.fixDelete(node.right, node.right!.parent);
} else if (!node.right) {
this.transplant(node, node.left);
if (node.color === Color.BLACK) this.fixDelete(node.left, node.left!.parent);
} else {
const successor = this.minimum(node.right);
if (successor!.parent !== node) {
this.transplant(successor!, successor!.right);
successor!.right = node.right;
successor!.right!.parent = successor;
}
this.transplant(node, successor);
successor!.left = node.left;
successor!.left!.parent = successor;
const origColor = successor!.color;
successor!.color = node.color;
if (origColor === Color.BLACK) this.fixDelete(successor!.right, successor!.right?.parent ?? null);
}
}
private transplant(u: RBNode<K, V> | null, v: RBNode<K, V> | null): void {
if (!u!.parent) {
this.root = v;
} else if (u === u!.parent.left) {
u!.parent.left = v;
} else {
u!.parent.right = v;
}
if (v) v.parent = u!.parent;
}
private minimum(node: RBNode<K, V>): RBNode<K, V> {
while (node.left) node = node.left;
return node;
}
private fixInsert(node: RBNode<K, V>): void {
while (node.parent && node.parent.color === Color.RED) {
const parent = node.parent;
const grandparent = parent.parent;
if (!grandparent) break;
if (parent === grandparent.left) {
const uncle = grandparent.right;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
} else {
if (node === parent.right) {
node = parent;
this.rotateLeft(node);
}
if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateRight(node.parent.parent);
}
} else {
const uncle = grandparent.left;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
} else {
if (node === parent.left) {
node = parent;
this.rotateRight(node);
}
if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateLeft(node.parent.parent);
}
}
}
if (this.root) this.root.color = Color.BLACK;
}
private fixDelete(x: RBNode<K, V> | null, parent: RBNode<K, V> | null): void {
// 标准 RB-Tree 删除修复(修复"双黑"问题)
let node = x;
let nodeParent = parent;
while ((!node || node.color === Color.BLACK) && node !== this.root) {
if (!nodeParent) break;
if (node === nodeParent.left) {
let sibling = nodeParent.right;
if (!sibling) break;
// Case 1: 兄弟是红色
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateLeft(nodeParent);
sibling = nodeParent.right;
if (!sibling) break;
}
// Case 2: 兄弟的两个子节点都是黑色
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
} else {
// Case 3: 兄弟右子黑色(左子红色)
if (!sibRight || sibRight.color === Color.BLACK) {
if (sibLeft) sibLeft.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateRight(sibling);
sibling = nodeParent.right;
if (!sibling) break;
}
// Case 4: 兄弟右子红色
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.right) sibling.right.color = Color.BLACK;
this.rotateLeft(nodeParent);
node = this.root;
}
} else {
// 镜像:node 是父节点的右子
let sibling = nodeParent.left;
if (!sibling) break;
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateRight(nodeParent);
sibling = nodeParent.left;
if (!sibling) break;
}
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
} else {
if (!sibLeft || sibLeft.color === Color.BLACK) {
if (sibRight) sibRight.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateLeft(sibling);
sibling = nodeParent.left;
if (!sibling) break;
}
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.left) sibling.left.color = Color.BLACK;
this.rotateRight(nodeParent);
node = this.root;
}
}
}
if (node) node.color = Color.BLACK;
}
private rotateLeft(x: RBNode<K, V>): void {
const y = x.right;
if (!y) return;
x.right = y.left;
if (y.left) y.left.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
} else if (x === x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
y.left = x;
x.parent = y;
}
private rotateRight(x: RBNode<K, V>): void {
const y = x.left;
if (!y) return;
x.left = y.right;
if (y.right) y.right.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
} else if (x === x.parent.right) {
x.parent.right = y;
} else {
x.parent.left = y;
}
y.right = x;
x.parent = y;
}
private _inorder(node: RBNode<K, V> | null, cb: (k: K, v: V) => void): void {
if (!node) return;
this._inorder(node.left, cb);
cb(node.key, node.value);
this._inorder(node.right, cb);
}
private _rangeScan(
node: RBNode<K, V> | null,
start: K,
end: K,
cb: (k: K, v: V) => void,
): void {
if (!node) return;
if (node.key > start) this._rangeScan(node.left, start, end, cb);
if (node.key >= start && node.key <= end) cb(node.key, node.value);
if (node.key < end) this._rangeScan(node.right, start, end, cb);
}
}
// ---------------------------------------------------------------------------
// MemTable
// ---------------------------------------------------------------------------
export class MemTable {
private tree: RedBlackTree<string, Record<string, unknown>>;
private _estimatedSize = 0;
private maxSize: number;
constructor(maxSize: number = 4 * 1024 * 1024) {
this.tree = new RedBlackTree();
this.maxSize = maxSize;
}
/** 插入或更新 */
put(key: string, value: Record<string, unknown>): void {
const oldSize = this.estimateEntrySize(key, this.tree.find(key));
const newSize = this.estimateEntrySize(key, value);
this.tree.insert(key, value);
this._estimatedSize += newSize - oldSize;
}
/** 获取 */
get(key: string): Record<string, unknown> | null {
return this.tree.find(key);
}
/** 删除 */
delete(key: string): boolean {
const oldVal = this.tree.find(key);
if (oldVal) {
this._estimatedSize -= this.estimateEntrySize(key, oldVal);
}
return this.tree.delete(key);
}
/** 是否应刷盘 */
shouldFlush(): boolean {
return this._estimatedSize >= this.maxSize;
}
/** 获取所有有序条目 */
getAllEntries(): [string, Record<string, unknown>][] {
return this.tree.getAllEntries();
}
/** 范围扫描 */
rangeScan(
startKey: string,
endKey: string,
): [string, Record<string, unknown>][] {
const entries: [string, Record<string, unknown>][] = [];
this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
return entries;
}
/** 条目数 */
getEntryCount(): number {
return this.tree.size;
}
/** 估计大小(字节) */
getEstimatedSize(): number {
return this._estimatedSize;
}
/** 清空 */
clear(): void {
this.tree.clear();
this._estimatedSize = 0;
}
/** 检查 key 是否存在 */
contains(key: string): boolean {
return this.tree.find(key) !== null;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private estimateEntrySize(key: string, value: Record<string, unknown> | null): number {
if (!value) return 0;
let size = key.length * 2; // UTF-16
for (const entry of Object.entries(value)) {
size += entry[0].length * 2;
const v = entry[1];
if (typeof v === 'string') size += v.length * 2;
else if (typeof v === 'number') size += 8;
else if (typeof v === 'boolean') size += 1;
else if (v === null || v === undefined) size += 1;
else size += 16; // rough estimate
}
return size;
}
}
+192 -189
View File
@@ -1,189 +1,192 @@
/**
* AriaEngine Merge Iterator
* @module engine/aria/index/merge_iterator
*
* SSTable MemTable
*/
// ---------------------------------------------------------------------------
// MergeIterator
// ---------------------------------------------------------------------------
export interface EntrySource {
/** 获取下一个条目,无更多时返回 null */
next(): [string, Record<string, unknown>] | null;
/** 重置迭代器 */
reset(): void;
}
/** 数组数据源的迭代器 */
export class ArrayEntrySource implements EntrySource {
private entries: [string, Record<string, unknown>][];
private index = 0;
constructor(entries: [string, Record<string, unknown>][]) {
this.entries = entries;
}
next(): [string, Record<string, unknown>] | null {
if (this.index >= this.entries.length) return null;
return this.entries[this.index++];
}
reset(): void {
this.index = 0;
}
}
/** 回调数据源的迭代器 */
export class CallbackEntrySource implements EntrySource {
private items: [string, Record<string, unknown>][] = [];
private index = 0;
private consumed = false;
/**
* @param producer
*/
constructor(producer: (cb: (key: string, value: Record<string, unknown>) => void) => void) {
producer((key, value) => this.items.push([key, value]));
}
next(): [string, Record<string, unknown>] | null {
if (this.index >= this.items.length) return null;
return this.items[this.index++];
}
reset(): void {
this.index = 0;
}
}
// ---------------------------------------------------------------------------
// Heap 节点(用于多路归并)
// ---------------------------------------------------------------------------
interface HeapNode {
key: string;
value: Record<string, unknown>;
sourceIndex: number;
}
/** 最小堆 */
class MinHeap {
private heap: HeapNode[] = [];
push(node: HeapNode): void {
this.heap.push(node);
this.bubbleUp(this.heap.length - 1);
}
pop(): HeapNode | null {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop()!;
const result = this.heap[0];
this.heap[0] = this.heap.pop()!;
this.bubbleDown(0);
return result;
}
peek(): HeapNode | null {
return this.heap.length > 0 ? this.heap[0] : null;
}
get size(): number {
return this.heap.length;
}
private bubbleUp(idx: number): void {
while (idx > 0) {
const parent = Math.floor((idx - 1) / 2);
if (this.heap[idx].key >= this.heap[parent].key) break;
[this.heap[idx], this.heap[parent]] = [this.heap[parent], this.heap[idx]];
idx = parent;
}
}
private bubbleDown(idx: number): void {
const n = this.heap.length;
while (true) {
let smallest = idx;
const left = 2 * idx + 1;
const right = 2 * idx + 2;
if (left < n && this.heap[left].key < this.heap[smallest].key) smallest = left;
if (right < n && this.heap[right].key < this.heap[smallest].key) smallest = right;
if (smallest === idx) break;
[this.heap[idx], this.heap[smallest]] = [this.heap[smallest], this.heap[idx]];
idx = smallest;
}
}
}
// ---------------------------------------------------------------------------
// MergeIterator
// ---------------------------------------------------------------------------
/**
* key
* MemTable SSTable
*/
export class MergeIterator {
private sources: EntrySource[];
private heap: MinHeap;
constructor() {
this.sources = [];
this.heap = new MinHeap();
}
/** 添加数据源 */
addSource(source: EntrySource): void {
this.sources.push(source);
this.seedFromSource(this.sources.length - 1);
}
/** 获取下一个归并后的条目 */
next(): [string, Record<string, unknown>] | null {
if (this.heap.size === 0) return null;
const node = this.heap.pop()!;
const key = node.key;
const value = node.value;
// 刷新来源的下一个值
this.seedFromSource(node.sourceIndex);
// 跳过重复 key取最新的(堆顶的即是最新的,因为来源下标越小越新)
while (this.heap.peek() && this.heap.peek()!.key === key) {
const dup = this.heap.pop()!;
this.seedFromSource(dup.sourceIndex);
}
return [key, value];
}
/** 耗尽管道,返回所有归并结果 */
drain(): [string, Record<string, unknown>][] {
const result: [string, Record<string, unknown>][] = [];
let entry = this.next();
while (entry) {
result.push(entry);
entry = this.next();
}
return result;
}
private seedFromSource(sourceIndex: number): void {
const entry = this.sources[sourceIndex].next();
if (entry) {
this.heap.push({
key: entry[0],
value: entry[1],
sourceIndex,
});
}
}
}
/**
* AriaEngine Merge Iterator
* @module engine/aria/index/merge_iterator
*
* SSTable MemTable
*/
// ---------------------------------------------------------------------------
// MergeIterator
// ---------------------------------------------------------------------------
export interface EntrySource {
/** 获取下一个条目,无更多时返回 null */
next(): [string, Record<string, unknown>] | null;
/** 重置迭代器 */
reset(): void;
}
/** 数组数据源的迭代器 */
export class ArrayEntrySource implements EntrySource {
private entries: [string, Record<string, unknown>][];
private index = 0;
constructor(entries: [string, Record<string, unknown>][]) {
this.entries = entries;
}
next(): [string, Record<string, unknown>] | null {
if (this.index >= this.entries.length) return null;
return this.entries[this.index++];
}
reset(): void {
this.index = 0;
}
}
/** 回调数据源的迭代器 */
export class CallbackEntrySource implements EntrySource {
private items: [string, Record<string, unknown>][] = [];
private index = 0;
private consumed = false;
/**
* @param producer
*/
constructor(producer: (cb: (key: string, value: Record<string, unknown>) => void) => void) {
producer((key, value) => this.items.push([key, value]));
}
next(): [string, Record<string, unknown>] | null {
if (this.index >= this.items.length) return null;
return this.items[this.index++];
}
reset(): void {
this.index = 0;
}
}
// ---------------------------------------------------------------------------
// Heap 节点(用于多路归并)
// ---------------------------------------------------------------------------
interface HeapNode {
key: string;
value: Record<string, unknown>;
sourceIndex: number;
}
/** 最小堆 */
class MinHeap {
private heap: HeapNode[] = [];
push(node: HeapNode): void {
this.heap.push(node);
this.bubbleUp(this.heap.length - 1);
}
pop(): HeapNode | null {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop()!;
const result = this.heap[0];
this.heap[0] = this.heap.pop()!;
this.bubbleDown(0);
return result;
}
peek(): HeapNode | null {
return this.heap.length > 0 ? this.heap[0] : null;
}
get size(): number {
return this.heap.length;
}
private bubbleUp(idx: number): void {
while (idx > 0) {
const parent = Math.floor((idx - 1) / 2);
if (this.heap[idx].key >= this.heap[parent].key) break;
[this.heap[idx], this.heap[parent]] = [this.heap[parent], this.heap[idx]];
idx = parent;
}
}
private bubbleDown(idx: number): void {
const n = this.heap.length;
while (true) {
let smallest = idx;
const left = 2 * idx + 1;
const right = 2 * idx + 2;
if (left < n && this.heap[left].key < this.heap[smallest].key) smallest = left;
if (right < n && this.heap[right].key < this.heap[smallest].key) smallest = right;
if (smallest === idx) break;
[this.heap[idx], this.heap[smallest]] = [this.heap[smallest], this.heap[idx]];
idx = smallest;
}
}
}
// ---------------------------------------------------------------------------
// MergeIterator
// ---------------------------------------------------------------------------
/**
* key
* MemTable SSTable
*/
export class MergeIterator {
private sources: EntrySource[];
private heap: MinHeap;
constructor() {
this.sources = [];
this.heap = new MinHeap();
}
/** 添加数据源 */
addSource(source: EntrySource): void {
this.sources.push(source);
this.seedFromSource(this.sources.length - 1);
}
/** 获取下一个归并后的条目 */
next(): [string, Record<string, unknown>] | null {
if (this.heap.size === 0) return null;
const first = this.heap.pop()!;
const key = first.key;
let best = first;
// 刷新 first 来源的下一个值
this.seedFromSource(first.sourceIndex);
// 跳过重复 key在多个来源中保留 sourceIndex 最小(最新)的条目
while (this.heap.peek() && this.heap.peek()!.key === key) {
const dup = this.heap.pop()!;
this.seedFromSource(dup.sourceIndex);
if (dup.sourceIndex < best.sourceIndex) {
best = dup;
}
}
return [best.key, best.value];
}
/** 耗尽管道,返回所有归并结果 */
drain(): [string, Record<string, unknown>][] {
const result: [string, Record<string, unknown>][] = [];
let entry = this.next();
while (entry) {
result.push(entry);
entry = this.next();
}
return result;
}
private seedFromSource(sourceIndex: number): void {
const entry = this.sources[sourceIndex].next();
if (entry) {
this.heap.push({
key: entry[0],
value: entry[1],
sourceIndex,
});
}
}
}
+261 -261
View File
@@ -1,261 +1,261 @@
/**
* AriaEngine SSTable Reader SSTable
* @module engine/aria/index/sstable
*/
import type { IndexEntry, SSTableMeta } from '../types';
import { BloomFilter } from './bloom';
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
// ---------------------------------------------------------------------------
// SSTableReader
// ---------------------------------------------------------------------------
export class SSTableReader {
private data: Uint8Array;
private view: DataView;
private indexEntries: IndexEntry[] = [];
private entryCount = 0;
private meta: SSTableMeta;
private bloomFilter: BloomFilter | null = null;
constructor(data: Uint8Array, meta: SSTableMeta) {
this.data = data;
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
this.meta = meta;
this.parseFooter();
}
// -----------------------------------------------------------------------
// 查询
// -----------------------------------------------------------------------
/** 精确查找 key */
get(targetKey: string): Record<string, unknown> | null {
// Bloom Filter 快速否定
if (this.bloomFilter && !this.bloomFilter.mayContain(targetKey)) return null;
const blockIdx = this.locateBlock(targetKey);
if (blockIdx < 0) return null;
const entry = this.indexEntries[blockIdx];
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const entryCount = blockView.getUint32(0, false);
let offset = 4;
// 顺序扫描 block 内的条目(生产中应二分查找)
for (let i = 0; i < entryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key === targetKey) {
try {
return JSON.parse(new TextDecoder().decode(valBytes));
} catch {
return null;
}
}
}
return null;
}
/** 范围扫描 */
rangeScan(
startKey: string,
endKey: string,
callback: (key: string, value: Record<string, unknown>) => void,
): void {
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));
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx) return;
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
const entry = this.indexEntries[bi];
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key >= startKey && key <= endKey) {
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
} catch {
// skip corrupted entry
}
}
}
}
}
/** 扫描所有条目 */
scanAll(callback: (key: string, value: Record<string, unknown>) => void): void {
for (const entry of this.indexEntries) {
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
} catch {
// skip corrupted entry
}
}
}
}
/** 获取元数据 */
getMeta(): SSTableMeta {
return this.meta;
}
/** 获取索引条目数 */
getIndexBlockCount(): number {
return this.indexEntries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private parseFooter(): void {
if (this.data.byteLength < 32) {
throw new Error('SSTable too small: missing footer');
}
const footerOffset = this.data.byteLength - 32;
// 验证魔数
const magic = this.view.getUint32(footerOffset + 24, false);
if (magic !== SSTABLE_MAGIC) {
throw new Error(`Invalid SSTable magic: expected ${SSTABLE_MAGIC}, got ${magic}`);
}
const indexOffset = this.view.getUint32(footerOffset, false);
const indexSize = this.view.getUint32(footerOffset + 4, false);
const bloomOffset = this.view.getUint32(footerOffset + 8, false);
const bloomSize = this.view.getUint32(footerOffset + 12, false);
const bloomHashCount = this.view.getUint32(footerOffset + 16, false);
this.entryCount = this.view.getUint32(footerOffset + 20, false);
// 解析索引块
this.parseIndexBlock(indexOffset, indexSize);
// 加载 Bloom Filter
if (bloomOffset > 0 && bloomSize > 0 && bloomOffset + bloomSize <= this.data.byteLength) {
try {
const bloomBytes = this.data.slice(bloomOffset, bloomOffset + bloomSize);
this.bloomFilter = BloomFilter.fromData(bloomBytes, bloomHashCount || 10);
} catch {
// 损坏的 bloom filter 不影响读取(仅跳过快速否定优化)
}
}
}
private parseIndexBlock(offset: number, _size: number): void {
const entryCount = this.view.getUint32(offset, false);
offset += 4;
for (let i = 0; i < entryCount; i++) {
const keyLen = this.view.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(this.data.slice(offset, offset + keyLen));
offset += keyLen;
const blockOffset = this.view.getUint32(offset, false);
offset += 4;
const blockSize = this.view.getUint32(offset, false);
offset += 4;
this.indexEntries.push({ key, blockOffset, blockSize });
}
}
/** 二分查找某 key 所在的 block 索引 */
private locateBlock(key: string): number {
let lo = 0;
let hi = this.indexEntries.length - 1;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const entry = this.indexEntries[mid];
if (key <= entry.key) {
// 检查是否在此 block 范围内
const firstKey = mid === 0 ? '' : this.indexEntries[mid - 1].key;
if (key > firstKey) return mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return -1;
}
private locateBlockGE(key: string): number {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key < key) lo = mid + 1;
else hi = mid;
}
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
}
private locateBlockLE(key: string): number {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key <= key) lo = mid + 1;
else hi = mid;
}
return lo > 0 ? lo - 1 : 0;
}
}
/**
* AriaEngine SSTable Reader SSTable
* @module engine/aria/index/sstable
*/
import type { IndexEntry, SSTableMeta } from '../types';
import { BloomFilter } from './bloom';
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
// ---------------------------------------------------------------------------
// SSTableReader
// ---------------------------------------------------------------------------
export class SSTableReader {
private data: Uint8Array;
private view: DataView;
private indexEntries: IndexEntry[] = [];
private entryCount = 0;
private meta: SSTableMeta;
private bloomFilter: BloomFilter | null = null;
constructor(data: Uint8Array, meta: SSTableMeta) {
this.data = data;
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
this.meta = meta;
this.parseFooter();
}
// -----------------------------------------------------------------------
// 查询
// -----------------------------------------------------------------------
/** 精确查找 key */
get(targetKey: string): Record<string, unknown> | null {
// Bloom Filter 快速否定
if (this.bloomFilter && !this.bloomFilter.mayContain(targetKey)) return null;
const blockIdx = this.locateBlock(targetKey);
if (blockIdx < 0) return null;
const entry = this.indexEntries[blockIdx];
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const entryCount = blockView.getUint32(0, false);
let offset = 4;
// 顺序扫描 block 内的条目(生产中应二分查找)
for (let i = 0; i < entryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key === targetKey) {
try {
return JSON.parse(new TextDecoder().decode(valBytes));
} catch {
return null;
}
}
}
return null;
}
/** 范围扫描 */
rangeScan(
startKey: string,
endKey: string,
callback: (key: string, value: Record<string, unknown>) => void,
): void {
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));
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx) return;
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
const entry = this.indexEntries[bi];
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key >= startKey && key <= endKey) {
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
} catch {
// skip corrupted entry
}
}
}
}
}
/** 扫描所有条目 */
scanAll(callback: (key: string, value: Record<string, unknown>) => void): void {
for (const entry of this.indexEntries) {
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
} catch {
// skip corrupted entry
}
}
}
}
/** 获取元数据 */
getMeta(): SSTableMeta {
return this.meta;
}
/** 获取索引条目数 */
getIndexBlockCount(): number {
return this.indexEntries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private parseFooter(): void {
if (this.data.byteLength < 32) {
throw new Error('SSTable too small: missing footer');
}
const footerOffset = this.data.byteLength - 32;
// 验证魔数
const magic = this.view.getUint32(footerOffset + 24, false);
if (magic !== SSTABLE_MAGIC) {
throw new Error(`Invalid SSTable magic: expected ${SSTABLE_MAGIC}, got ${magic}`);
}
const indexOffset = this.view.getUint32(footerOffset, false);
const indexSize = this.view.getUint32(footerOffset + 4, false);
const bloomOffset = this.view.getUint32(footerOffset + 8, false);
const bloomSize = this.view.getUint32(footerOffset + 12, false);
const bloomHashCount = this.view.getUint32(footerOffset + 16, false);
this.entryCount = this.view.getUint32(footerOffset + 20, false);
// 解析索引块
this.parseIndexBlock(indexOffset, indexSize);
// 加载 Bloom Filter
if (bloomOffset > 0 && bloomSize > 0 && bloomOffset + bloomSize <= this.data.byteLength) {
try {
const bloomBytes = this.data.slice(bloomOffset, bloomOffset + bloomSize);
this.bloomFilter = BloomFilter.fromData(bloomBytes, bloomHashCount || 10);
} catch {
// 损坏的 bloom filter 不影响读取(仅跳过快速否定优化)
}
}
}
private parseIndexBlock(offset: number, _size: number): void {
const entryCount = this.view.getUint32(offset, false);
offset += 4;
for (let i = 0; i < entryCount; i++) {
const keyLen = this.view.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(this.data.slice(offset, offset + keyLen));
offset += keyLen;
const blockOffset = this.view.getUint32(offset, false);
offset += 4;
const blockSize = this.view.getUint32(offset, false);
offset += 4;
this.indexEntries.push({ key, blockOffset, blockSize });
}
}
/** 二分查找某 key 所在的 block 索引 */
private locateBlock(key: string): number {
let lo = 0;
let hi = this.indexEntries.length - 1;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const entry = this.indexEntries[mid];
if (key <= entry.key) {
// 检查是否在此 block 范围内
const firstKey = mid === 0 ? '' : this.indexEntries[mid - 1].key;
if (key > firstKey) return mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return -1;
}
private locateBlockGE(key: string): number {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key < key) lo = mid + 1;
else hi = mid;
}
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
}
private locateBlockLE(key: string): number {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key <= key) lo = mid + 1;
else hi = mid;
}
return lo > 0 ? lo - 1 : 0;
}
}
+247 -249
View File
@@ -1,249 +1,247 @@
/**
* AriaEngine SSTable Builder
* @module engine/aria/index/sstable_builder
*
* key-value SSTable
*
* SSTable :
*
* Data Block 0
* Data Block 1
* ...
* Index Block (block offset key range)
* Bloom Filter
* Footer (32 bytes)
* - index_offset (u32)
* - index_size (u32)
* - bloom_offset (u32)
* - bloom_size (u32)
* - bloom_hash_count (u32)
* - entry_count (u32)
* - magic_number (u32, 0x53535442 ="SSTB")
* - checksum (u32)
*
*/
import { BloomFilter } from './bloom';
import type { IndexEntry, DataBlock } from '../types';
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
const SSTABLE_FOOTER_SIZE = 32;
// ---------------------------------------------------------------------------
// SSTableBuilder
// ---------------------------------------------------------------------------
export class SSTableBuilder {
private entries: [string, Record<string, unknown>][] = [];
private currentBlock: [string, Record<string, unknown>][] = [];
private currentBlockStartKey = '';
private blockSizeLimit: number;
constructor(blockSizeLimit: number = 4096) {
this.blockSizeLimit = blockSizeLimit;
}
/** 添加一个 key-value 条目(必须按键排序添加) */
add(key: string, value: Record<string, unknown>): void {
if (this.currentBlock.length === 0) {
this.currentBlockStartKey = key;
}
this.currentBlock.push([key, value]);
this.entries.push([key, value]);
// 如果当前 Block 达到大小限制,切割
const estimated = this.estimateBlockSize();
if (estimated >= this.blockSizeLimit && this.currentBlock.length > 1) {
// 当前 block 结束(不在这里切割,在 build 时统一处理)
}
}
/**
* SSTable
* { data: Uint8Array, indexEntries: IndexEntry[], bloomFilter: BloomFilter }
*/
build(): { sstableData: Uint8Array; indexEntries: IndexEntry[] } {
const blocks = this.splitIntoBlocks();
const bloomFilter = new BloomFilter(this.entries.length);
// 预计算总大小
let totalSize = 0;
const blockOffsets: number[] = [];
for (const block of blocks) {
blockOffsets.push(totalSize);
const blockSize = this.computeBlockSize(block);
totalSize += blockSize;
}
// 索引块
const indexEntries: IndexEntry[] = [];
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
const lastKey = block[block.length - 1][0];
const blockSize = this.computeBlockSize(block);
indexEntries.push({
key: lastKey,
blockOffset: blockOffsets[i],
blockSize,
});
}
const indexBlockSize = this.estimateIndexBlockSize(indexEntries);
// 序列化 bloom filter 以获取其大小
const bloomData = bloomFilter.serialize();
const bloomSize = bloomData.byteLength;
// 写入到 buffer(包含 bloom block
const finalSize = totalSize + indexBlockSize + bloomSize + SSTABLE_FOOTER_SIZE;
const buf = new ArrayBuffer(finalSize);
const view = new DataView(buf);
let offset = 0;
// ---- Data Blocks ----
for (const block of blocks) {
offset = this.writeDataBlock(view, offset, block, bloomFilter);
}
// ---- Index Block ----
const indexOffset = offset;
offset = this.writeIndexBlock(view, offset, indexEntries);
// ---- Bloom Filter Block ----
const bloomOffset = offset;
new Uint8Array(view.buffer).set(bloomData, offset);
offset += bloomSize;
// ---- Footer ----
const footerOffset = offset;
view.setUint32(footerOffset, indexOffset, false); // index_offset
view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size
view.setUint32(footerOffset + 8, bloomOffset, false); // bloom_offset
view.setUint32(footerOffset + 12, bloomSize, false); // bloom_size
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
view.setUint32(footerOffset + 20, this.entries.length, false);
view.setUint32(footerOffset + 24, SSTABLE_MAGIC, false);
view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0)
return {
sstableData: new Uint8Array(buf),
indexEntries,
};
}
/** 获取条目数 */
getEntryCount(): number {
return this.entries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private splitIntoBlocks(): [string, Record<string, unknown>][][] {
const blocks: [string, Record<string, unknown>][][] = [];
let current: [string, Record<string, unknown>][] = [];
for (const entry of this.entries) {
current.push(entry);
if (this.estimateBlockSizeFromEntries(current) >= this.blockSizeLimit && current.length > 1) {
blocks.push(current.slice(0, -1));
current = [entry];
}
}
if (current.length > 0) blocks.push(current);
return blocks;
}
private estimateBlockSize(): number {
return this.estimateBlockSizeFromEntries(this.currentBlock);
}
private estimateBlockSizeFromEntries(entries: [string, unknown][]): number {
let size = 0;
for (const [key, value] of entries) {
size += 4 + key.length + JSON.stringify(value).length;
}
return size;
}
private computeBlockSize(block: [string, unknown][]): number {
// entryCount (u32) + 每对: keyLen(u16) + key + valueLen(u16) + value json
let size = 4;
for (const [key, value] of block) {
const json = JSON.stringify(value);
size += 2 + key.length + 2 + json.length;
}
return size;
}
private writeDataBlock(
view: DataView,
offset: number,
block: [string, Record<string, unknown>][],
bloomFilter: BloomFilter,
): number {
const start = offset;
// entry count
view.setUint32(offset, block.length, false);
offset += 4;
for (const [key, value] of block) {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(key);
const valueBytes = encoder.encode(JSON.stringify(value));
// key length
view.setUint16(offset, keyBytes.length, false);
offset += 2;
// key
new Uint8Array(view.buffer).set(keyBytes, offset);
offset += keyBytes.length;
// value length
view.setUint16(offset, valueBytes.length, false);
offset += 2;
// value
new Uint8Array(view.buffer).set(valueBytes, offset);
offset += valueBytes.length;
// 插入 bloom filter
bloomFilter.insert(key);
}
return offset;
}
private estimateIndexBlockSize(entries: IndexEntry[]): number {
// entryCount(u32) + each: keyLen(u16)+key+blockOffset(u32)+blockSize(u32)
let size = 4;
for (const entry of entries) {
size += 2 + entry.key.length + 8;
}
return size;
}
private writeIndexBlock(view: DataView, offset: number, entries: IndexEntry[]): number {
view.setUint32(offset, entries.length, false);
offset += 4;
for (const entry of entries) {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(entry.key);
view.setUint16(offset, keyBytes.length, false);
offset += 2;
new Uint8Array(view.buffer).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, entry.blockOffset, false);
offset += 4;
view.setUint32(offset, entry.blockSize, false);
offset += 4;
}
return offset;
}
}
/**
* AriaEngine SSTable Builder
* @module engine/aria/index/sstable_builder
*
* key-value SSTable
*
* SSTable :
*
* Data Block 0
* Data Block 1
* ...
* Index Block (block offset key range)
* Bloom Filter
* Footer (32 bytes)
* - index_offset (u32)
* - index_size (u32)
* - bloom_offset (u32)
* - bloom_size (u32)
* - bloom_hash_count (u32)
* - entry_count (u32)
* - magic_number (u32, 0x53535442 ="SSTB")
* - checksum (u32)
*
*/
import { BloomFilter } from './bloom';
import type { IndexEntry } from '../types';
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
const SSTABLE_FOOTER_SIZE = 32;
// ---------------------------------------------------------------------------
// SSTableBuilder
// ---------------------------------------------------------------------------
export class SSTableBuilder {
private entries: [string, Record<string, unknown>][] = [];
private currentBlock: [string, Record<string, unknown>][] = [];
private currentBlockStartKey = '';
private blockSizeLimit: number;
constructor(blockSizeLimit: number = 4096) {
this.blockSizeLimit = blockSizeLimit;
}
/** 添加一个 key-value 条目(必须按键排序添加) */
add(key: string, value: Record<string, unknown>): void {
if (this.currentBlock.length === 0) {
this.currentBlockStartKey = key;
}
this.currentBlock.push([key, value]);
this.entries.push([key, value]);
// 如果当前 Block 达到大小限制,切割
const estimated = this.estimateBlockSize();
if (estimated >= this.blockSizeLimit && this.currentBlock.length > 1) {
// 当前 block 结束(不在这里切割,在 build 时统一处理)
}
}
/**
* SSTable
* { data: Uint8Array, indexEntries: IndexEntry[], bloomFilter: BloomFilter }
*/
build(): { sstableData: Uint8Array; indexEntries: IndexEntry[] } {
const blocks = this.splitIntoBlocks();
const bloomFilter = new BloomFilter(this.entries.length);
// 预计算总大小
let totalSize = 0;
const blockOffsets: number[] = [];
for (const block of blocks) {
blockOffsets.push(totalSize);
const blockSize = this.computeBlockSize(block);
totalSize += blockSize;
}
// 索引块
const indexEntries: IndexEntry[] = [];
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
const lastKey = block[block.length - 1][0];
const blockSize = this.computeBlockSize(block);
indexEntries.push({
key: lastKey,
blockOffset: blockOffsets[i],
blockSize,
});
}
const indexBlockSize = this.estimateIndexBlockSize(indexEntries);
// 序列化 bloom filter 以获取其大小
const bloomData = bloomFilter.serialize();
const bloomSize = bloomData.byteLength;
// 写入到 buffer(包含 bloom block
const finalSize = totalSize + indexBlockSize + bloomSize + SSTABLE_FOOTER_SIZE;
const buf = new ArrayBuffer(finalSize);
const view = new DataView(buf);
let offset = 0;
// ---- Data Blocks ----
for (const block of blocks) {
offset = this.writeDataBlock(view, offset, block, bloomFilter);
}
// ---- Index Block ----
const indexOffset = offset;
offset = this.writeIndexBlock(view, offset, indexEntries);
// ---- Bloom Filter Block ----
const bloomOffset = offset;
new Uint8Array(view.buffer).set(bloomData, offset);
offset += bloomSize;
// ---- Footer ----
const footerOffset = offset;
view.setUint32(footerOffset, indexOffset, false); // index_offset
view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size
view.setUint32(footerOffset + 8, bloomOffset, false); // bloom_offset
view.setUint32(footerOffset + 12, bloomSize, false); // bloom_size
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
view.setUint32(footerOffset + 20, this.entries.length, false);
view.setUint32(footerOffset + 24, SSTABLE_MAGIC, false);
view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0)
return {
sstableData: new Uint8Array(buf),
indexEntries,
};
}
/** 获取条目数 */
getEntryCount(): number {
return this.entries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private splitIntoBlocks(): [string, Record<string, unknown>][][] {
const blocks: [string, Record<string, unknown>][][] = [];
let current: [string, Record<string, unknown>][] = [];
for (const entry of this.entries) {
current.push(entry);
if (this.estimateBlockSizeFromEntries(current) >= this.blockSizeLimit && current.length > 1) {
blocks.push(current.slice(0, -1));
current = [entry];
}
}
if (current.length > 0) blocks.push(current);
return blocks;
}
private estimateBlockSize(): number {
return this.estimateBlockSizeFromEntries(this.currentBlock);
}
private estimateBlockSizeFromEntries(entries: [string, unknown][]): number {
let size = 0;
for (const [key, value] of entries) {
size += 4 + key.length + JSON.stringify(value).length;
}
return size;
}
private computeBlockSize(block: [string, unknown][]): number {
// entryCount (u32) + 每对: keyLen(u16) + key + valueLen(u16) + value json
let size = 4;
for (const [key, value] of block) {
const json = JSON.stringify(value);
size += 2 + key.length + 2 + json.length;
}
return size;
}
private writeDataBlock(
view: DataView,
offset: number,
block: [string, Record<string, unknown>][],
bloomFilter: BloomFilter,
): number {
// entry count
view.setUint32(offset, block.length, false);
offset += 4;
for (const [key, value] of block) {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(key);
const valueBytes = encoder.encode(JSON.stringify(value));
// key length
view.setUint16(offset, keyBytes.length, false);
offset += 2;
// key
new Uint8Array(view.buffer).set(keyBytes, offset);
offset += keyBytes.length;
// value length
view.setUint16(offset, valueBytes.length, false);
offset += 2;
// value
new Uint8Array(view.buffer).set(valueBytes, offset);
offset += valueBytes.length;
// 插入 bloom filter
bloomFilter.insert(key);
}
return offset;
}
private estimateIndexBlockSize(entries: IndexEntry[]): number {
// entryCount(u32) + each: keyLen(u16)+key+blockOffset(u32)+blockSize(u32)
let size = 4;
for (const entry of entries) {
size += 2 + entry.key.length + 8;
}
return size;
}
private writeIndexBlock(view: DataView, offset: number, entries: IndexEntry[]): number {
view.setUint32(offset, entries.length, false);
offset += 4;
for (const entry of entries) {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(entry.key);
view.setUint16(offset, keyBytes.length, false);
offset += 2;
new Uint8Array(view.buffer).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, entry.blockOffset, false);
offset += 4;
view.setUint32(offset, entry.blockSize, false);
offset += 4;
}
return offset;
}
}
+164 -167
View File
@@ -1,167 +1,164 @@
/**
* AriaEngine Page Format
* @module engine/aria/page/format
*
* Header / Slot / Tuple
*/
import { PAGE_SIZE, PageType, type PageHandle } from '../types';
import {
initPageHeader,
decodePageHeader,
encodePageHeader,
getSlotCount,
getPageId,
} from './header';
import { allocateSlot, freeSlot, readSlotData, getAllSlots } from './slot';
import { encodeTuple, decodeTuple } from './tuple';
// ---------------------------------------------------------------------------
// 页面创建
// ---------------------------------------------------------------------------
/** 创建一个新的空页面 */
export function createPage(pageId: number, type: PageType): PageHandle {
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, type);
return {
pageId,
type,
data,
dirty: true,
pins: 0,
prev: null,
next: null,
lastAccess: Date.now(),
};
}
/** 从 ArrayBuffer 恢复页面句柄 */
export function pageFromBuffer(
pageId: number,
buffer: ArrayBuffer,
): PageHandle {
return {
pageId,
type: new DataView(buffer).getUint8(4) as PageType,
data: buffer,
dirty: false,
pins: 0,
prev: null,
next: null,
lastAccess: Date.now(),
};
}
// ---------------------------------------------------------------------------
// 行操作(页面级)
// ---------------------------------------------------------------------------
/**
* slot -1
*/
export function pageInsertRow(
page: PageHandle,
row: Record<string, unknown>,
columnOrder: string[],
columnTypes: Record<string, string>,
): number {
const encoded = encodeTuple(row, columnOrder, columnTypes);
const slotIdx = allocateSlot(page.data, encoded);
if (slotIdx >= 0) {
page.dirty = true;
page.lastAccess = Date.now();
}
return slotIdx;
}
/**
* slot
*/
export function pageReadRow(
page: PageHandle,
slotIndex: number,
columnOrder: string[],
columnTypes: Record<string, string>,
): Record<string, unknown> | null {
const slotData = readSlotData(page.data, slotIndex);
if (!slotData) return null;
return decodeTuple(slotData, columnOrder, columnTypes);
}
/**
*
*/
export function pageReadAllRows(
page: PageHandle,
columnOrder: string[],
columnTypes: Record<string, string>,
): Record<string, unknown>[] {
const rows: Record<string, unknown>[] = [];
const slotCount = getSlotCount(page.data);
for (let i = 0; i < slotCount; i++) {
const row = pageReadRow(page, i, columnOrder, columnTypes);
if (row) rows.push(row);
}
return rows;
}
/**
* slot
*/
export function pageDeleteRow(page: PageHandle, slotIndex: number): void {
freeSlot(page.data, slotIndex);
page.dirty = true;
page.lastAccess = Date.now();
}
/**
* slot
*/
export function pageUpdateRow(
page: PageHandle,
slotIndex: number,
row: Record<string, unknown>,
columnOrder: string[],
columnTypes: Record<string, string>,
): void {
// 先标记旧 slot 为删除
pageDeleteRow(page, slotIndex);
// 分配新 slot,可能会在不同位置
const newSlot = pageInsertRow(page, row, columnOrder, columnTypes);
// 注意:调用者需要自行维护 slot index → pk 的映射
}
// ---------------------------------------------------------------------------
// 校验和
// ---------------------------------------------------------------------------
/** 简单 CRC32(使用预先计算的查找表简化) */
export function computeChecksum(data: ArrayBuffer): number {
const view = new Uint8Array(data);
let hash = 0;
for (let i = 0; i < view.byteLength; i++) {
hash = ((hash << 5) - hash + view[i]) | 0;
}
return hash >>> 0;
}
/** 更新页面的校验和字段 */
export function updateChecksum(page: PageHandle): void {
// 先清零校验和字段
const view = new DataView(page.data);
view.setUint32(11, 0, false);
// 计算校验和
const cksum = computeChecksum(page.data);
view.setUint32(11, cksum, false);
}
/** 验证页面校验和 */
export function verifyChecksum(page: PageHandle): boolean {
const stored = new DataView(page.data).getUint32(11, false);
// 临时清零
new DataView(page.data).setUint32(11, 0, false);
const computed = computeChecksum(page.data);
new DataView(page.data).setUint32(11, stored, false);
return stored === computed;
}
/**
* AriaEngine Page Format
* @module engine/aria/page/format
*
* Header / Slot / Tuple
*/
import { PAGE_SIZE, PageType, type PageHandle } from '../types';
import {
initPageHeader,
getSlotCount,
} from './header';
import { allocateSlot, freeSlot, readSlotData } from './slot';
import { encodeTuple, decodeTuple } from './tuple';
// ---------------------------------------------------------------------------
// 页面创建
// ---------------------------------------------------------------------------
/** 创建一个新的空页面 */
export function createPage(pageId: number, type: PageType): PageHandle {
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, type);
return {
pageId,
type,
data,
dirty: true,
pins: 0,
prev: null,
next: null,
lastAccess: Date.now(),
};
}
/** 从 ArrayBuffer 恢复页面句柄 */
export function pageFromBuffer(
pageId: number,
buffer: ArrayBuffer,
): PageHandle {
return {
pageId,
type: new DataView(buffer).getUint8(4) as PageType,
data: buffer,
dirty: false,
pins: 0,
prev: null,
next: null,
lastAccess: Date.now(),
};
}
// ---------------------------------------------------------------------------
// 行操作(页面级)
// ---------------------------------------------------------------------------
/**
* slot -1
*/
export function pageInsertRow(
page: PageHandle,
row: Record<string, unknown>,
columnOrder: string[],
columnTypes: Record<string, string>,
): number {
const encoded = encodeTuple(row, columnOrder, columnTypes);
const slotIdx = allocateSlot(page.data, encoded);
if (slotIdx >= 0) {
page.dirty = true;
page.lastAccess = Date.now();
}
return slotIdx;
}
/**
* slot
*/
export function pageReadRow(
page: PageHandle,
slotIndex: number,
columnOrder: string[],
columnTypes: Record<string, string>,
): Record<string, unknown> | null {
const slotData = readSlotData(page.data, slotIndex);
if (!slotData) return null;
return decodeTuple(slotData, columnOrder, columnTypes);
}
/**
*
*/
export function pageReadAllRows(
page: PageHandle,
columnOrder: string[],
columnTypes: Record<string, string>,
): Record<string, unknown>[] {
const rows: Record<string, unknown>[] = [];
const slotCount = getSlotCount(page.data);
for (let i = 0; i < slotCount; i++) {
const row = pageReadRow(page, i, columnOrder, columnTypes);
if (row) rows.push(row);
}
return rows;
}
/**
* slot
*/
export function pageDeleteRow(page: PageHandle, slotIndex: number): void {
freeSlot(page.data, slotIndex);
page.dirty = true;
page.lastAccess = Date.now();
}
/**
* slot
*/
export function pageUpdateRow(
page: PageHandle,
slotIndex: number,
row: Record<string, unknown>,
columnOrder: string[],
columnTypes: Record<string, string>,
): void {
// 先标记旧 slot 为删除
pageDeleteRow(page, slotIndex);
// 分配新 slot,可能会在不同位置
pageInsertRow(page, row, columnOrder, columnTypes);
// 注意:调用者需要自行维护 slot index → pk 的映射
}
// ---------------------------------------------------------------------------
// 校验和
// ---------------------------------------------------------------------------
/** 简单 CRC32(使用预先计算的查找表简化) */
export function computeChecksum(data: ArrayBuffer): number {
const view = new Uint8Array(data);
let hash = 0;
for (let i = 0; i < view.byteLength; i++) {
hash = ((hash << 5) - hash + view[i]) | 0;
}
return hash >>> 0;
}
/** 更新页面的校验和字段 */
export function updateChecksum(page: PageHandle): void {
// 先清零校验和字段
const view = new DataView(page.data);
view.setUint32(11, 0, false);
// 计算校验和
const cksum = computeChecksum(page.data);
view.setUint32(11, cksum, false);
}
/** 验证页面校验和 */
export function verifyChecksum(page: PageHandle): boolean {
const stored = new DataView(page.data).getUint32(11, false);
// 临时清零
new DataView(page.data).setUint32(11, 0, false);
const computed = computeChecksum(page.data);
new DataView(page.data).setUint32(11, stored, false);
return stored === computed;
}
+97 -97
View File
@@ -1,97 +1,97 @@
/**
* AriaEngine Page Header
* @module engine/aria/page/header
*/
import { PAGE_HEADER_SIZE, PageType, type PageHeader } from '../types';
// ---------------------------------------------------------------------------
// 编码
// ---------------------------------------------------------------------------
/**
* PageHeader ArrayBuffer 16
* :
* [0-3] page_id u32
* [4] type u8
* [5-6] free_start u16
* [7-8] free_end u16
* [9-10] slot_count u16
* [11-14] checksum u32
* [15] reserved u8
*/
export function encodePageHeader(header: PageHeader, buf: ArrayBuffer): void {
const view = new DataView(buf);
view.setUint32(0, header.pageId, false);
view.setUint8(4, header.type);
view.setUint16(5, header.freeStart, false);
view.setUint16(7, header.freeEnd, false);
view.setUint16(9, header.slotCount, false);
view.setUint32(11, header.checksum, false);
view.setUint8(15, 0); // reserved
}
/**
* ArrayBuffer PageHeader
*/
export function decodePageHeader(buf: ArrayBuffer): PageHeader {
const view = new DataView(buf);
return {
pageId: view.getUint32(0, false),
type: view.getUint8(4) as PageType,
freeStart: view.getUint16(5, false),
freeEnd: view.getUint16(7, false),
slotCount: view.getUint16(9, false),
checksum: view.getUint32(11, false),
};
}
/**
* Header
*/
export function initPageHeader(
buf: ArrayBuffer,
pageId: number,
type: PageType,
): void {
const view = new DataView(buf);
view.setUint32(0, pageId, false);
view.setUint8(4, type);
view.setUint16(5, PAGE_HEADER_SIZE, false); // freeStart = header 之后
view.setUint16(7, buf.byteLength, false); // freeEnd = 页面末尾
view.setUint16(9, 0, false); // slotCount = 0
view.setUint32(11, 0, false); // checksum = 0
view.setUint8(15, 0);
}
// ---------------------------------------------------------------------------
// 从 Buffer 中提取 Header 字段的辅助函数
// ---------------------------------------------------------------------------
export function getPageType(buf: ArrayBuffer): PageType {
return new DataView(buf).getUint8(4) as PageType;
}
export function getPageId(buf: ArrayBuffer): number {
return new DataView(buf).getUint32(0, false);
}
export function getSlotCount(buf: ArrayBuffer): number {
return new DataView(buf).getUint16(9, false);
}
export function getFreeStart(buf: ArrayBuffer): number {
return new DataView(buf).getUint16(5, false);
}
export function setFreeStart(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(5, val, false);
}
export function setFreeEnd(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(7, val, false);
}
export function setSlotCount(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(9, val, false);
}
/**
* AriaEngine Page Header
* @module engine/aria/page/header
*/
import { PAGE_HEADER_SIZE, PageType, type PageHeader } from '../types';
// ---------------------------------------------------------------------------
// 编码
// ---------------------------------------------------------------------------
/**
* PageHeader ArrayBuffer 16
* :
* [0-3] page_id u32
* [4] type u8
* [5-6] free_start u16
* [7-8] free_end u16
* [9-10] slot_count u16
* [11-14] checksum u32
* [15] reserved u8
*/
export function encodePageHeader(header: PageHeader, buf: ArrayBuffer): void {
const view = new DataView(buf);
view.setUint32(0, header.pageId, false);
view.setUint8(4, header.type);
view.setUint16(5, header.freeStart, false);
view.setUint16(7, header.freeEnd, false);
view.setUint16(9, header.slotCount, false);
view.setUint32(11, header.checksum, false);
view.setUint8(15, 0); // reserved
}
/**
* ArrayBuffer PageHeader
*/
export function decodePageHeader(buf: ArrayBuffer): PageHeader {
const view = new DataView(buf);
return {
pageId: view.getUint32(0, false),
type: view.getUint8(4) as PageType,
freeStart: view.getUint16(5, false),
freeEnd: view.getUint16(7, false),
slotCount: view.getUint16(9, false),
checksum: view.getUint32(11, false),
};
}
/**
* Header
*/
export function initPageHeader(
buf: ArrayBuffer,
pageId: number,
type: PageType,
): void {
const view = new DataView(buf);
view.setUint32(0, pageId, false);
view.setUint8(4, type);
view.setUint16(5, PAGE_HEADER_SIZE, false); // freeStart = header 之后
view.setUint16(7, buf.byteLength, false); // freeEnd = 页面末尾
view.setUint16(9, 0, false); // slotCount = 0
view.setUint32(11, 0, false); // checksum = 0
view.setUint8(15, 0);
}
// ---------------------------------------------------------------------------
// 从 Buffer 中提取 Header 字段的辅助函数
// ---------------------------------------------------------------------------
export function getPageType(buf: ArrayBuffer): PageType {
return new DataView(buf).getUint8(4) as PageType;
}
export function getPageId(buf: ArrayBuffer): number {
return new DataView(buf).getUint32(0, false);
}
export function getSlotCount(buf: ArrayBuffer): number {
return new DataView(buf).getUint16(9, false);
}
export function getFreeStart(buf: ArrayBuffer): number {
return new DataView(buf).getUint16(5, false);
}
export function setFreeStart(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(5, val, false);
}
export function setFreeEnd(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(7, val, false);
}
export function setSlotCount(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(9, val, false);
}
+184 -184
View File
@@ -1,184 +1,184 @@
/**
* AriaEngine Slot Directory
* @module engine/aria/page/slot
*
* Slot Header 4
*/
import {
PAGE_HEADER_SIZE,
SLOT_ENTRY_SIZE,
PAGE_SIZE,
type SlotEntry,
} from '../types';
// ---------------------------------------------------------------------------
// Slot 读写
// ---------------------------------------------------------------------------
/**
* slot SlotEntry
*/
export function getSlotEntry(buf: ArrayBuffer, slotIndex: number): SlotEntry {
const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE;
const view = new DataView(buf);
return {
offset: view.getUint16(offset, false),
length: view.getUint16(offset + 2, false),
};
}
/**
* slot SlotEntry
*/
export function setSlotEntry(
buf: ArrayBuffer,
slotIndex: number,
entry: SlotEntry,
): void {
const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE;
const view = new DataView(buf);
view.setUint16(offset, entry.offset, false);
view.setUint16(offset + 2, entry.length, false);
}
/**
* slot
*/
export function getAllSlots(
buf: ArrayBuffer,
slotCount: number,
): SlotEntry[] {
const entries: SlotEntry[] = [];
for (let i = 0; i < slotCount; i++) {
entries.push(getSlotEntry(buf, i));
}
return entries;
}
// ---------------------------------------------------------------------------
// Slot 空间计算
// ---------------------------------------------------------------------------
/** 获取 slot 目录占用的总字节数 */
export function getSlotDirectorySize(slotCount: number): number {
return slotCount * SLOT_ENTRY_SIZE;
}
/** 获取可用空闲空间(字节) */
export function getFreeSpace(buf: ArrayBuffer): number {
const view = new DataView(buf);
const freeStart = view.getUint16(5, false); // slot 区之后
const freeEnd = view.getUint16(7, false); // 数据区之前
return freeEnd - freeStart;
}
/** 检查是否有足够空间存放长度为 len 的行 */
export function hasEnoughSpace(buf: ArrayBuffer, len: number): boolean {
const slotCount = new DataView(buf).getUint16(9, false);
const neededSlotSize = (slotCount + 1) * SLOT_ENTRY_SIZE;
const freeStart = PAGE_HEADER_SIZE + neededSlotSize;
const freeEnd = new DataView(buf).getUint16(7, false);
return freeEnd - freeStart >= len;
}
// ---------------------------------------------------------------------------
// 插入 / 删除 slot
// ---------------------------------------------------------------------------
/**
* slot
* slot -1
*/
export function allocateSlot(
buf: ArrayBuffer,
rowData: Uint8Array,
): number {
const view = new DataView(buf);
const slotCount = view.getUint16(9, false);
const neededSlotSpace = (slotCount + 1) * SLOT_ENTRY_SIZE;
const freeStart = PAGE_HEADER_SIZE + neededSlotSpace;
const freeEnd = view.getUint16(7, false);
if (freeEnd - freeStart < rowData.byteLength) {
return -1; // 空间不足
}
// 将数据放入页面底部
const dataOffset = freeEnd - rowData.byteLength;
const dest = new Uint8Array(buf, dataOffset, rowData.byteLength);
dest.set(rowData);
// 写入 slot 条目
setSlotEntry(buf, slotCount, { offset: dataOffset, length: rowData.byteLength });
// 更新 header
view.setUint16(5, freeStart, false); // freeStart
view.setUint16(7, dataOffset, false); // freeEnd
view.setUint16(9, slotCount + 1, false); // slotCount
return slotCount;
}
/**
* slot offset 0
* slot
*/
export function freeSlot(buf: ArrayBuffer, slotIndex: number): void {
setSlotEntry(buf, slotIndex, { offset: 0, length: 0 });
}
/**
* slot
*
*/
export function compactSlots(buf: ArrayBuffer): number {
const view = new DataView(buf);
const slotCount = view.getUint16(9, false);
if (slotCount === 0) return 0;
// 收集有效 slotoffset>0 的)
const validSlots: { index: number; offset: number; length: number; data: Uint8Array }[] = [];
for (let i = 0; i < slotCount; i++) {
const entry = getSlotEntry(buf, i);
if (entry.offset > 0 && entry.length > 0) {
const data = new Uint8Array(buf, entry.offset, entry.length);
validSlots.push({ index: i, offset: entry.offset, length: entry.length, data: new Uint8Array(data) });
}
}
if (validSlots.length === slotCount) return 0; // 无碎片
// 从页面底部重新紧凑排列
let dataEnd = PAGE_SIZE;
const newSlots: { offset: number; length: number }[] = [];
for (let i = validSlots.length - 1; i >= 0; i--) {
const s = validSlots[i];
dataEnd -= s.length;
new Uint8Array(buf).set(s.data, dataEnd);
newSlots.unshift({ offset: dataEnd, length: s.length });
}
// 重写 slot directory
view.setUint16(9, validSlots.length, false); // slotCount
view.setUint16(7, dataEnd, false); // freeEnd
for (let i = 0; i < validSlots.length; i++) {
setSlotEntry(buf, i, newSlots[i]);
}
// 清除剩余 slot 条目
for (let i = validSlots.length; i < slotCount; i++) {
setSlotEntry(buf, i, { offset: 0, length: 0 });
}
return slotCount - validSlots.length; // 回收的 slot 数量
}
/**
* slot
*/
export function readSlotData(buf: ArrayBuffer, slotIndex: number): Uint8Array | null {
const entry = getSlotEntry(buf, slotIndex);
if (entry.offset === 0 || entry.length === 0) return null;
return new Uint8Array(buf, entry.offset, entry.length);
}
/**
* AriaEngine Slot Directory
* @module engine/aria/page/slot
*
* Slot Header 4
*/
import {
PAGE_HEADER_SIZE,
SLOT_ENTRY_SIZE,
PAGE_SIZE,
type SlotEntry,
} from '../types';
// ---------------------------------------------------------------------------
// Slot 读写
// ---------------------------------------------------------------------------
/**
* slot SlotEntry
*/
export function getSlotEntry(buf: ArrayBuffer, slotIndex: number): SlotEntry {
const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE;
const view = new DataView(buf);
return {
offset: view.getUint16(offset, false),
length: view.getUint16(offset + 2, false),
};
}
/**
* slot SlotEntry
*/
export function setSlotEntry(
buf: ArrayBuffer,
slotIndex: number,
entry: SlotEntry,
): void {
const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE;
const view = new DataView(buf);
view.setUint16(offset, entry.offset, false);
view.setUint16(offset + 2, entry.length, false);
}
/**
* slot
*/
export function getAllSlots(
buf: ArrayBuffer,
slotCount: number,
): SlotEntry[] {
const entries: SlotEntry[] = [];
for (let i = 0; i < slotCount; i++) {
entries.push(getSlotEntry(buf, i));
}
return entries;
}
// ---------------------------------------------------------------------------
// Slot 空间计算
// ---------------------------------------------------------------------------
/** 获取 slot 目录占用的总字节数 */
export function getSlotDirectorySize(slotCount: number): number {
return slotCount * SLOT_ENTRY_SIZE;
}
/** 获取可用空闲空间(字节) */
export function getFreeSpace(buf: ArrayBuffer): number {
const view = new DataView(buf);
const freeStart = view.getUint16(5, false); // slot 区之后
const freeEnd = view.getUint16(7, false); // 数据区之前
return freeEnd - freeStart;
}
/** 检查是否有足够空间存放长度为 len 的行 */
export function hasEnoughSpace(buf: ArrayBuffer, len: number): boolean {
const slotCount = new DataView(buf).getUint16(9, false);
const neededSlotSize = (slotCount + 1) * SLOT_ENTRY_SIZE;
const freeStart = PAGE_HEADER_SIZE + neededSlotSize;
const freeEnd = new DataView(buf).getUint16(7, false);
return freeEnd - freeStart >= len;
}
// ---------------------------------------------------------------------------
// 插入 / 删除 slot
// ---------------------------------------------------------------------------
/**
* slot
* slot -1
*/
export function allocateSlot(
buf: ArrayBuffer,
rowData: Uint8Array,
): number {
const view = new DataView(buf);
const slotCount = view.getUint16(9, false);
const neededSlotSpace = (slotCount + 1) * SLOT_ENTRY_SIZE;
const freeStart = PAGE_HEADER_SIZE + neededSlotSpace;
const freeEnd = view.getUint16(7, false);
if (freeEnd - freeStart < rowData.byteLength) {
return -1; // 空间不足
}
// 将数据放入页面底部
const dataOffset = freeEnd - rowData.byteLength;
const dest = new Uint8Array(buf, dataOffset, rowData.byteLength);
dest.set(rowData);
// 写入 slot 条目
setSlotEntry(buf, slotCount, { offset: dataOffset, length: rowData.byteLength });
// 更新 header
view.setUint16(5, freeStart, false); // freeStart
view.setUint16(7, dataOffset, false); // freeEnd
view.setUint16(9, slotCount + 1, false); // slotCount
return slotCount;
}
/**
* slot offset 0
* slot
*/
export function freeSlot(buf: ArrayBuffer, slotIndex: number): void {
setSlotEntry(buf, slotIndex, { offset: 0, length: 0 });
}
/**
* slot
*
*/
export function compactSlots(buf: ArrayBuffer): number {
const view = new DataView(buf);
const slotCount = view.getUint16(9, false);
if (slotCount === 0) return 0;
// 收集有效 slotoffset>0 的)
const validSlots: { index: number; offset: number; length: number; data: Uint8Array }[] = [];
for (let i = 0; i < slotCount; i++) {
const entry = getSlotEntry(buf, i);
if (entry.offset > 0 && entry.length > 0) {
const data = new Uint8Array(buf, entry.offset, entry.length);
validSlots.push({ index: i, offset: entry.offset, length: entry.length, data: new Uint8Array(data) });
}
}
if (validSlots.length === slotCount) return 0; // 无碎片
// 从页面底部重新紧凑排列
let dataEnd = PAGE_SIZE;
const newSlots: { offset: number; length: number }[] = [];
for (let i = validSlots.length - 1; i >= 0; i--) {
const s = validSlots[i];
dataEnd -= s.length;
new Uint8Array(buf).set(s.data, dataEnd);
newSlots.unshift({ offset: dataEnd, length: s.length });
}
// 重写 slot directory
view.setUint16(9, validSlots.length, false); // slotCount
view.setUint16(7, dataEnd, false); // freeEnd
for (let i = 0; i < validSlots.length; i++) {
setSlotEntry(buf, i, newSlots[i]);
}
// 清除剩余 slot 条目
for (let i = validSlots.length; i < slotCount; i++) {
setSlotEntry(buf, i, { offset: 0, length: 0 });
}
return slotCount - validSlots.length; // 回收的 slot 数量
}
/**
* slot
*/
export function readSlotData(buf: ArrayBuffer, slotIndex: number): Uint8Array | null {
const entry = getSlotEntry(buf, slotIndex);
if (entry.offset === 0 || entry.length === 0) return null;
return new Uint8Array(buf, entry.offset, entry.length);
}
+251 -252
View File
@@ -1,252 +1,251 @@
/**
* AriaEngine Tuple Codec
* @module engine/aria/page/tuple
*
* Record<string, unknown>
*
* :
* [null bitmap: ceil(colCount/8) bytes]
* [column 1 data]
* [column 2 data]
* ...
*
* :
* type tag (u8) + data
* - STRING: [len: u16][UTF-8 bytes]
* - NUMBER: [f64: 8 bytes]
* - BOOLEAN: [u8: 1 byte]
* - DATE: [f64: 8 bytes] (epoch ms)
* - JSON: [len: u16][UTF-8 bytes]
* - NULL: (no data, just the tag)
*/
import { ColumnEncoding } from '../types';
// ---------------------------------------------------------------------------
// 编码
// ---------------------------------------------------------------------------
/**
*
* @param row
* @param columnOrder
* @param columnTypes FieldType
*/
export function encodeTuple(
row: Record<string, unknown>,
columnOrder: string[],
columnTypes: Record<string, string>,
): Uint8Array {
// 先计算总大小
let size = 0;
const nullBitmapBytes = Math.ceil(columnOrder.length / 8);
size += nullBitmapBytes;
// 预计算每列编码后的字节
const colData: (Uint8Array | null)[] = [];
for (let i = 0; i < columnOrder.length; i++) {
const col = columnOrder[i];
const val = row[col];
const encoded = encodeColumn(val, columnTypes[col] ?? 'string');
colData.push(encoded);
if (encoded) {
size += 1 + encoded.byteLength; // tag + data
} else {
size += 1; // just the NULL tag
}
}
const buf = new Uint8Array(size);
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
let offset = 0;
// Null bitmap
const nullBitmap = new Uint8Array(nullBitmapBytes);
for (let i = 0; i < columnOrder.length; i++) {
if (colData[i] === null) {
nullBitmap[Math.floor(i / 8)] |= (1 << (i % 8));
}
}
buf.set(nullBitmap, offset);
offset += nullBitmapBytes;
// Column data
for (let i = 0; i < columnOrder.length; i++) {
const encoded = colData[i];
if (encoded === null) {
view.setUint8(offset, ColumnEncoding.NULL);
offset += 1;
} else {
const tag = getEncodingTag(columnTypes[columnOrder[i]] ?? 'string');
view.setUint8(offset, tag);
offset += 1;
buf.set(encoded, offset);
offset += encoded.byteLength;
}
}
return buf;
}
/**
*
*/
function encodeColumn(value: unknown, fieldType: string): Uint8Array | null {
if (value === null || value === undefined) return null;
switch (fieldType) {
case 'string': {
const str = String(value);
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
const buf = new Uint8Array(2 + bytes.byteLength);
new DataView(buf.buffer).setUint16(0, bytes.byteLength, false);
buf.set(bytes, 2);
return buf;
}
case 'number': {
const buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, Number(value), false);
return new Uint8Array(buf);
}
case 'boolean': {
return new Uint8Array([value ? 1 : 0]);
}
case 'date': {
const ts = value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
const buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, ts, false);
return new Uint8Array(buf);
}
case 'json': {
const str = JSON.stringify(value);
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
const buf = new Uint8Array(2 + bytes.byteLength);
new DataView(buf.buffer).setUint16(0, bytes.byteLength, false);
buf.set(bytes, 2);
return buf;
}
default:
return null;
}
}
// ---------------------------------------------------------------------------
// 解码
// ---------------------------------------------------------------------------
/**
*
* @returns null
*/
export function decodeTuple(
bytes: Uint8Array,
columnOrder: string[],
columnTypes: Record<string, string>,
): Record<string, unknown> | null {
try {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let offset = 0;
const nullBitmapBytes = Math.ceil(columnOrder.length / 8);
if (offset + nullBitmapBytes > bytes.byteLength) return null;
const nullBitmap = bytes.slice(offset, offset + nullBitmapBytes);
offset += nullBitmapBytes;
const row: Record<string, unknown> = {};
for (let i = 0; i < columnOrder.length; i++) {
if (offset >= bytes.byteLength) break;
const tag = view.getUint8(offset);
offset += 1;
if (tag === ColumnEncoding.NULL) {
row[columnOrder[i]] = null;
continue;
}
const col = columnOrder[i];
const fType = columnTypes[col] ?? 'string';
const result = decodeColumnValue(bytes, offset, tag, fType);
if (result === null) return null;
row[col] = result.value;
offset = result.nextOffset;
}
return row;
} catch {
return null;
}
}
function decodeColumnValue(
bytes: Uint8Array,
offset: number,
tag: number,
fieldType: string,
): { value: unknown; nextOffset: number } | null {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
switch (tag) {
case ColumnEncoding.STRING:
case ColumnEncoding.JSON: {
if (offset + 2 > bytes.byteLength) return null;
const len = view.getUint16(offset, false);
offset += 2;
if (offset + len > bytes.byteLength) return null;
const decoder = new TextDecoder();
const str = decoder.decode(bytes.slice(offset, offset + len));
return {
value: tag === ColumnEncoding.JSON ? JSON.parse(str) : str,
nextOffset: offset + len,
};
}
case ColumnEncoding.NUMBER: {
if (offset + 8 > bytes.byteLength) return null;
const val = view.getFloat64(offset, false);
return { value: val, nextOffset: offset + 8 };
}
case ColumnEncoding.BOOLEAN: {
if (offset >= bytes.byteLength) return null;
return { value: view.getUint8(offset) !== 0, nextOffset: offset + 1 };
}
case ColumnEncoding.DATE: {
if (offset + 8 > bytes.byteLength) return null;
const ts = view.getFloat64(offset, false);
return { value: new Date(ts).toISOString(), nextOffset: offset + 8 };
}
default:
return null;
}
}
// ---------------------------------------------------------------------------
// 辅助
// ---------------------------------------------------------------------------
function getEncodingTag(fieldType: string): ColumnEncoding {
switch (fieldType) {
case 'string': return ColumnEncoding.STRING;
case 'number': return ColumnEncoding.NUMBER;
case 'boolean': return ColumnEncoding.BOOLEAN;
case 'date': return ColumnEncoding.DATE;
case 'json': return ColumnEncoding.JSON;
default: return ColumnEncoding.NULL;
}
}
/** 获取列类型到编码标签的映射 */
export function getColumnEncodingMap(
columnOrder: string[],
columnTypes: Record<string, string>,
): Map<string, ColumnEncoding> {
const map = new Map<string, ColumnEncoding>();
for (const col of columnOrder) {
map.set(col, getEncodingTag(columnTypes[col] ?? 'string'));
}
return map;
}
/**
* AriaEngine Tuple Codec
* @module engine/aria/page/tuple
*
* Record<string, unknown>
*
* :
* [null bitmap: ceil(colCount/8) bytes]
* [column 1 data]
* [column 2 data]
* ...
*
* :
* type tag (u8) + data
* - STRING: [len: u16][UTF-8 bytes]
* - NUMBER: [f64: 8 bytes]
* - BOOLEAN: [u8: 1 byte]
* - DATE: [f64: 8 bytes] (epoch ms)
* - JSON: [len: u16][UTF-8 bytes]
* - NULL: (no data, just the tag)
*/
import { ColumnEncoding } from '../types';
// ---------------------------------------------------------------------------
// 编码
// ---------------------------------------------------------------------------
/**
*
* @param row
* @param columnOrder
* @param columnTypes FieldType
*/
export function encodeTuple(
row: Record<string, unknown>,
columnOrder: string[],
columnTypes: Record<string, string>,
): Uint8Array {
// 先计算总大小
let size = 0;
const nullBitmapBytes = Math.ceil(columnOrder.length / 8);
size += nullBitmapBytes;
// 预计算每列编码后的字节
const colData: (Uint8Array | null)[] = [];
for (let i = 0; i < columnOrder.length; i++) {
const col = columnOrder[i];
const val = row[col];
const encoded = encodeColumn(val, columnTypes[col] ?? 'string');
colData.push(encoded);
if (encoded) {
size += 1 + encoded.byteLength; // tag + data
} else {
size += 1; // just the NULL tag
}
}
const buf = new Uint8Array(size);
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
let offset = 0;
// Null bitmap
const nullBitmap = new Uint8Array(nullBitmapBytes);
for (let i = 0; i < columnOrder.length; i++) {
if (colData[i] === null) {
nullBitmap[Math.floor(i / 8)] |= (1 << (i % 8));
}
}
buf.set(nullBitmap, offset);
offset += nullBitmapBytes;
// Column data
for (let i = 0; i < columnOrder.length; i++) {
const encoded = colData[i];
if (encoded === null) {
view.setUint8(offset, ColumnEncoding.NULL);
offset += 1;
} else {
const tag = getEncodingTag(columnTypes[columnOrder[i]] ?? 'string');
view.setUint8(offset, tag);
offset += 1;
buf.set(encoded, offset);
offset += encoded.byteLength;
}
}
return buf;
}
/**
*
*/
function encodeColumn(value: unknown, fieldType: string): Uint8Array | null {
if (value === null || value === undefined) return null;
switch (fieldType) {
case 'string': {
const str = String(value);
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
const buf = new Uint8Array(2 + bytes.byteLength);
new DataView(buf.buffer).setUint16(0, bytes.byteLength, false);
buf.set(bytes, 2);
return buf;
}
case 'number': {
const buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, Number(value), false);
return new Uint8Array(buf);
}
case 'boolean': {
return new Uint8Array([value ? 1 : 0]);
}
case 'date': {
const ts = value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
const buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, ts, false);
return new Uint8Array(buf);
}
case 'json': {
const str = JSON.stringify(value);
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
const buf = new Uint8Array(2 + bytes.byteLength);
new DataView(buf.buffer).setUint16(0, bytes.byteLength, false);
buf.set(bytes, 2);
return buf;
}
default:
return null;
}
}
// ---------------------------------------------------------------------------
// 解码
// ---------------------------------------------------------------------------
/**
*
* @returns null
*/
export function decodeTuple(
bytes: Uint8Array,
columnOrder: string[],
columnTypes: Record<string, string>,
): Record<string, unknown> | null {
try {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let offset = 0;
const nullBitmapBytes = Math.ceil(columnOrder.length / 8);
if (offset + nullBitmapBytes > bytes.byteLength) return null;
offset += nullBitmapBytes;
const row: Record<string, unknown> = {};
for (let i = 0; i < columnOrder.length; i++) {
if (offset >= bytes.byteLength) break;
const tag = view.getUint8(offset);
offset += 1;
if (tag === ColumnEncoding.NULL) {
row[columnOrder[i]] = null;
continue;
}
const col = columnOrder[i];
const fType = columnTypes[col] ?? 'string';
const result = decodeColumnValue(bytes, offset, tag, fType);
if (result === null) return null;
row[col] = result.value;
offset = result.nextOffset;
}
return row;
} catch {
return null;
}
}
function decodeColumnValue(
bytes: Uint8Array,
offset: number,
tag: number,
_fieldType: string,
): { value: unknown; nextOffset: number } | null {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
switch (tag) {
case ColumnEncoding.STRING:
case ColumnEncoding.JSON: {
if (offset + 2 > bytes.byteLength) return null;
const len = view.getUint16(offset, false);
offset += 2;
if (offset + len > bytes.byteLength) return null;
const decoder = new TextDecoder();
const str = decoder.decode(bytes.slice(offset, offset + len));
return {
value: tag === ColumnEncoding.JSON ? JSON.parse(str) : str,
nextOffset: offset + len,
};
}
case ColumnEncoding.NUMBER: {
if (offset + 8 > bytes.byteLength) return null;
const val = view.getFloat64(offset, false);
return { value: val, nextOffset: offset + 8 };
}
case ColumnEncoding.BOOLEAN: {
if (offset >= bytes.byteLength) return null;
return { value: view.getUint8(offset) !== 0, nextOffset: offset + 1 };
}
case ColumnEncoding.DATE: {
if (offset + 8 > bytes.byteLength) return null;
const ts = view.getFloat64(offset, false);
return { value: new Date(ts).toISOString(), nextOffset: offset + 8 };
}
default:
return null;
}
}
// ---------------------------------------------------------------------------
// 辅助
// ---------------------------------------------------------------------------
function getEncodingTag(fieldType: string): ColumnEncoding {
switch (fieldType) {
case 'string': return ColumnEncoding.STRING;
case 'number': return ColumnEncoding.NUMBER;
case 'boolean': return ColumnEncoding.BOOLEAN;
case 'date': return ColumnEncoding.DATE;
case 'json': return ColumnEncoding.JSON;
default: return ColumnEncoding.NULL;
}
}
/** 获取列类型到编码标签的映射 */
export function getColumnEncodingMap(
columnOrder: string[],
columnTypes: Record<string, string>,
): Map<string, ColumnEncoding> {
const map = new Map<string, ColumnEncoding>();
for (const col of columnOrder) {
map.set(col, getEncodingTag(columnTypes[col] ?? 'string'));
}
return map;
}
+179 -179
View File
@@ -1,179 +1,179 @@
/**
* AriaEngine Storage Backend
* @module engine/aria/store/backend
*
* APIIndexedDB / OPFS / Memory 退
* Buffer Pool PageIO WAL WALStore 使
*/
import { DatabaseError } from '../../../constants';
// ---------------------------------------------------------------------------
// StorageBackend 接口
// ---------------------------------------------------------------------------
export interface IStorageBackend {
/** 打开存储 */
open(name: string): Promise<void>;
/** 关闭存储 */
close(): Promise<void>;
/** 是否已打开 */
isOpen(): boolean;
/** 读取数据块 */
read(key: string): Promise<ArrayBuffer | null>;
/** 写入数据块 */
write(key: string, data: ArrayBuffer): Promise<void>;
/** 删除数据块 */
delete(key: string): Promise<void>;
/** 列出所有 key */
listKeys(): Promise<string[]>;
/** 检查 key 是否存在 */
exists(key: string): Promise<boolean>;
/** 清空所有数据 */
clear(): Promise<void>;
}
// =======================================================================
// IndexedDB Backend
// =======================================================================
export class IndexedDBBackend implements IStorageBackend {
private db: IDBDatabase | null = null;
private dbName = '';
private storeName = 'data';
async open(name: string): Promise<void> {
this.dbName = `aria-${name}`;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onerror = () => reject(new DatabaseError('Failed to open AriaEngine IndexedDB', 'ARIA_IDB_OPEN_ERROR'));
});
}
async close(): Promise<void> {
if (this.db) {
this.db.close();
this.db = null;
}
}
isOpen(): boolean {
return this.db !== null;
}
async read(key: string): Promise<ArrayBuffer | null> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).get(key);
req.onsuccess = () => resolve(req.result ?? null);
req.onerror = () => reject(new DatabaseError('Failed to read from AriaEngine store', 'ARIA_READ_ERROR'));
});
}
async write(key: string, data: ArrayBuffer): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).put(data, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to write to AriaEngine store', 'ARIA_WRITE_ERROR'));
});
}
async delete(key: string): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).delete(key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to delete from AriaEngine store', 'ARIA_DELETE_ERROR'));
});
}
async listKeys(): Promise<string[]> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).getAllKeys();
req.onsuccess = () => resolve((req.result ?? []) as string[]);
req.onerror = () => reject(new DatabaseError('Failed to list keys', 'ARIA_LIST_ERROR'));
});
}
async exists(key: string): Promise<boolean> {
const result = await this.read(key);
return result !== null;
}
async clear(): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).clear();
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to clear AriaEngine store', 'ARIA_CLEAR_ERROR'));
});
}
private ensureDB(): IDBDatabase {
if (!this.db) throw new DatabaseError('AriaEngine storage not opened', 'ARIA_DB_NOT_OPEN');
return this.db;
}
}
// =======================================================================
// Memory Backend(回退 / 测试用)
// =======================================================================
export class MemoryBackend implements IStorageBackend {
private store: Map<string, ArrayBuffer> = new Map();
private opened = false;
async open(_name: string): Promise<void> {
this.opened = true;
}
async close(): Promise<void> {
this.store.clear();
this.opened = false;
}
isOpen(): boolean {
return this.opened;
}
async read(key: string): Promise<ArrayBuffer | null> {
return this.store.get(key) ?? null;
}
async write(key: string, data: ArrayBuffer): Promise<void> {
this.store.set(key, data);
}
async delete(key: string): Promise<void> {
this.store.delete(key);
}
async listKeys(): Promise<string[]> {
return Array.from(this.store.keys());
}
async exists(key: string): Promise<boolean> {
return this.store.has(key);
}
async clear(): Promise<void> {
this.store.clear();
}
}
/**
* AriaEngine Storage Backend
* @module engine/aria/store/backend
*
* APIIndexedDB / OPFS / Memory 退
* Buffer Pool PageIO WAL WALStore 使
*/
import { DatabaseError } from '../../../constants';
// ---------------------------------------------------------------------------
// StorageBackend 接口
// ---------------------------------------------------------------------------
export interface IStorageBackend {
/** 打开存储 */
open(name: string): Promise<void>;
/** 关闭存储 */
close(): Promise<void>;
/** 是否已打开 */
isOpen(): boolean;
/** 读取数据块 */
read(key: string): Promise<ArrayBuffer | null>;
/** 写入数据块 */
write(key: string, data: ArrayBuffer): Promise<void>;
/** 删除数据块 */
delete(key: string): Promise<void>;
/** 列出所有 key */
listKeys(): Promise<string[]>;
/** 检查 key 是否存在 */
exists(key: string): Promise<boolean>;
/** 清空所有数据 */
clear(): Promise<void>;
}
// =======================================================================
// IndexedDB Backend
// =======================================================================
export class IndexedDBBackend implements IStorageBackend {
private db: IDBDatabase | null = null;
private dbName = '';
private storeName = 'data';
async open(name: string): Promise<void> {
this.dbName = `aria-${name}`;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onerror = () => reject(new DatabaseError('Failed to open AriaEngine IndexedDB', 'ARIA_IDB_OPEN_ERROR'));
});
}
async close(): Promise<void> {
if (this.db) {
this.db.close();
this.db = null;
}
}
isOpen(): boolean {
return this.db !== null;
}
async read(key: string): Promise<ArrayBuffer | null> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).get(key);
req.onsuccess = () => resolve(req.result ?? null);
req.onerror = () => reject(new DatabaseError('Failed to read from AriaEngine store', 'ARIA_READ_ERROR'));
});
}
async write(key: string, data: ArrayBuffer): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).put(data, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to write to AriaEngine store', 'ARIA_WRITE_ERROR'));
});
}
async delete(key: string): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).delete(key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to delete from AriaEngine store', 'ARIA_DELETE_ERROR'));
});
}
async listKeys(): Promise<string[]> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).getAllKeys();
req.onsuccess = () => resolve((req.result ?? []) as string[]);
req.onerror = () => reject(new DatabaseError('Failed to list keys', 'ARIA_LIST_ERROR'));
});
}
async exists(key: string): Promise<boolean> {
const result = await this.read(key);
return result !== null;
}
async clear(): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).clear();
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to clear AriaEngine store', 'ARIA_CLEAR_ERROR'));
});
}
private ensureDB(): IDBDatabase {
if (!this.db) throw new DatabaseError('AriaEngine storage not opened', 'ARIA_DB_NOT_OPEN');
return this.db;
}
}
// =======================================================================
// Memory Backend(回退 / 测试用)
// =======================================================================
export class MemoryBackend implements IStorageBackend {
private store: Map<string, ArrayBuffer> = new Map();
private opened = false;
async open(_name: string): Promise<void> {
this.opened = true;
}
async close(): Promise<void> {
this.store.clear();
this.opened = false;
}
isOpen(): boolean {
return this.opened;
}
async read(key: string): Promise<ArrayBuffer | null> {
return this.store.get(key) ?? null;
}
async write(key: string, data: ArrayBuffer): Promise<void> {
this.store.set(key, data);
}
async delete(key: string): Promise<void> {
this.store.delete(key);
}
async listKeys(): Promise<string[]> {
return Array.from(this.store.keys());
}
async exists(key: string): Promise<boolean> {
return this.store.has(key);
}
async clear(): Promise<void> {
this.store.clear();
}
}
+111 -111
View File
@@ -1,111 +1,111 @@
/**
* AriaEngine File Manager + PageIO
* @module engine/aria/store/file_manager
*
* / ID
*/
import type { IStorageBackend } from './backend';
import type { PageIO } from '../buffer/pool';
import { PAGE_SIZE, PageType } from '../types';
import { initPageHeader } from '../page/header';
// ---------------------------------------------------------------------------
// FileManager (implements PageIO)
// ---------------------------------------------------------------------------
export class FileManager implements PageIO {
private backend: IStorageBackend;
private nextPageId = 0;
private metaLoaded = false;
private dbName = '';
constructor(backend: IStorageBackend) {
this.backend = backend;
}
/** 初始化:从存储中读取元数据 */
async init(dbName: string): Promise<void> {
this.dbName = dbName;
const meta = await this.backend.read('__aria_meta');
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
const view = new DataView(meta);
this.nextPageId = view.getUint32(0, false);
} else {
this.nextPageId = 1;
await this.saveMeta();
}
this.metaLoaded = true;
}
// ---- PageIO ----
async readPage(pageId: number): Promise<ArrayBuffer | null> {
const key = `pg_${pageId}`;
const data = await this.backend.read(key);
if (!data) {
// 第一次访问:创建新页面
return this.createEmptyPage(pageId, PageType.DATA);
}
// 确保大小正确
if (data.byteLength < PAGE_SIZE) {
const padded = new ArrayBuffer(PAGE_SIZE);
new Uint8Array(padded).set(new Uint8Array(data));
return padded;
}
return data;
}
async writePage(pageId: number, data: ArrayBuffer): Promise<void> {
const key = `pg_${pageId}`;
await this.backend.write(key, data);
}
async allocatePageId(): Promise<number> {
const id = this.nextPageId++;
await this.saveMeta();
return id;
}
async freePageId(_pageId: number): Promise<void> {
// 简化实现:不回收 pageId
const key = `pg_${_pageId}`;
await this.backend.delete(key);
}
// ---- 表页面分配 ----
/**
*
*/
async allocateTableRootPage(): Promise<number> {
const pageId = await this.allocatePageId();
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, PageType.META);
await this.writePage(pageId, data);
return pageId;
}
// ---- 辅助 ----
private async saveMeta(): Promise<void> {
const buf = new ArrayBuffer(8);
new DataView(buf).setUint32(0, this.nextPageId, false);
await this.backend.write('__aria_meta', buf);
}
private createEmptyPage(pageId: number, type: PageType): ArrayBuffer {
const buf = new ArrayBuffer(PAGE_SIZE);
initPageHeader(buf, pageId, type);
return buf;
}
/** 清空所有数据 */
async clearAll(): Promise<void> {
await this.backend.clear();
this.nextPageId = 1;
await this.saveMeta();
}
}
/**
* AriaEngine File Manager + PageIO
* @module engine/aria/store/file_manager
*
* / ID
*/
import type { IStorageBackend } from './backend';
import type { PageIO } from '../buffer/pool';
import { PAGE_SIZE, PageType } from '../types';
import { initPageHeader } from '../page/header';
// ---------------------------------------------------------------------------
// FileManager (implements PageIO)
// ---------------------------------------------------------------------------
export class FileManager implements PageIO {
private backend: IStorageBackend;
private nextPageId = 0;
private metaLoaded = false;
private dbName = '';
constructor(backend: IStorageBackend) {
this.backend = backend;
}
/** 初始化:从存储中读取元数据 */
async init(dbName: string): Promise<void> {
this.dbName = dbName;
const meta = await this.backend.read('__aria_meta');
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
const view = new DataView(meta);
this.nextPageId = view.getUint32(0, false);
} else {
this.nextPageId = 1;
await this.saveMeta();
}
this.metaLoaded = true;
}
// ---- PageIO ----
async readPage(pageId: number): Promise<ArrayBuffer | null> {
const key = `pg_${pageId}`;
const data = await this.backend.read(key);
if (!data) {
// 第一次访问:创建新页面
return this.createEmptyPage(pageId, PageType.DATA);
}
// 确保大小正确
if (data.byteLength < PAGE_SIZE) {
const padded = new ArrayBuffer(PAGE_SIZE);
new Uint8Array(padded).set(new Uint8Array(data));
return padded;
}
return data;
}
async writePage(pageId: number, data: ArrayBuffer): Promise<void> {
const key = `pg_${pageId}`;
await this.backend.write(key, data);
}
async allocatePageId(): Promise<number> {
const id = this.nextPageId++;
await this.saveMeta();
return id;
}
async freePageId(_pageId: number): Promise<void> {
// 简化实现:不回收 pageId
const key = `pg_${_pageId}`;
await this.backend.delete(key);
}
// ---- 表页面分配 ----
/**
*
*/
async allocateTableRootPage(): Promise<number> {
const pageId = await this.allocatePageId();
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, PageType.META);
await this.writePage(pageId, data);
return pageId;
}
// ---- 辅助 ----
private async saveMeta(): Promise<void> {
const buf = new ArrayBuffer(8);
new DataView(buf).setUint32(0, this.nextPageId, false);
await this.backend.write('__aria_meta', buf);
}
private createEmptyPage(pageId: number, type: PageType): ArrayBuffer {
const buf = new ArrayBuffer(PAGE_SIZE);
initPageHeader(buf, pageId, type);
return buf;
}
/** 清空所有数据 */
async clearAll(): Promise<void> {
await this.backend.clear();
this.nextPageId = 1;
await this.saveMeta();
}
}
+243 -243
View File
@@ -1,243 +1,243 @@
/**
* AriaEngine MVCC
* @module engine/aria/transaction/mvcc
*
* (Snapshot Isolation)
*
*/
import type { RowVersion, TxnEntry } from '../types';
import { TransactionState } from '../types';
// ---------------------------------------------------------------------------
// MVCCManager
// ---------------------------------------------------------------------------
export class MVCCManager {
/** 所有行版本的存储:tableName.key → 版本链 */
private versionStore: Map<string, RowVersion[]> = new Map();
/** 活跃事务表:txnId → TxnEntry */
private activeTxns: Map<number, TxnEntry> = new Map();
/** 事务 ID 计数器 */
private nextTxnId = 1;
/** 全局提交序列号(用于可见性判断) */
private globalCommitLsn = 0;
// =======================================================================
// 事务管理
// =======================================================================
/** 开始一个事务,返回事务 ID */
beginTransaction(): number {
const txnId = this.nextTxnId++;
this.activeTxns.set(txnId, {
txnId,
state: TransactionState.ACTIVE,
snapshotLsn: this.globalCommitLsn,
startTime: Date.now(),
});
return txnId;
}
/** 提交事务 */
commitTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.COMMITTED;
this.globalCommitLsn++;
// 标记此事务写入的所有版本为已提交
for (const [, versions] of this.versionStore) {
for (const version of versions) {
if (version.txnId === txnId) {
version.committed = true;
}
}
}
// 清理已提交事务的记录
this.activeTxns.delete(txnId);
}
/** 回滚事务 */
rollbackTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.ABORTED;
// 移除此事务写入的所有版本
for (const [tableKey, versions] of this.versionStore) {
const filtered = versions.filter((v) => v.txnId !== txnId);
if (filtered.length === 0) {
this.versionStore.delete(tableKey);
} else {
this.versionStore.set(tableKey, filtered);
}
}
this.activeTxns.delete(txnId);
}
/** 检查事务是否活跃 */
isActive(txnId: number): boolean {
const txn = this.activeTxns.get(txnId);
return txn !== undefined && txn.state === TransactionState.ACTIVE;
}
// =======================================================================
// 版本读写
// =======================================================================
/**
*
*/
writeVersion(
tableName: string,
key: string,
data: Record<string, unknown>,
txnId: number,
): void {
const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey) ?? [];
const newVersion: RowVersion = {
txnId,
data,
prevVersion: versions.length > 0 ? versions[versions.length - 1] : null,
committed: false,
};
versions.push(newVersion);
this.versionStore.set(tableKey, versions);
}
/**
*
*/
readVersion(
tableName: string,
key: string,
txnId: number,
): Record<string, unknown> | null {
const txn = this.activeTxns.get(txnId);
if (!txn) return null;
const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey);
if (!versions || versions.length === 0) return null;
// 从最新版本向前遍历
for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i];
// 1. 如果是当前事务写入的(未提交),可见
if (version.txnId === txnId) {
return version.data;
}
// 2. 如果是已提交的版本,且在快照 LSN 之前提交,可见
if (version.committed) {
// 简化:所有已提交版本都可见
return version.data;
}
// 3. 其他事务的未提交版本,不可见,继续找更早的版本
}
return null;
}
/**
*
*/
deleteVersion(tableName: string, key: string, txnId: number): void {
this.writeVersion(tableName, key, { __mvcc_tombstone: true } as unknown as Record<string, unknown>, txnId);
}
/**
*
*/
getLatestCommittedVersions(
tableName: string,
): Record<string, Record<string, unknown>> {
const result: Record<string, Record<string, unknown>> = {};
for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(`${tableName}.`)) continue;
const key = tableKey.slice(tableName.length + 1);
for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i];
if (version.committed && !(version.data as unknown as Record<string, unknown>).__mvcc_tombstone) {
result[key] = version.data;
break;
}
}
}
return result;
}
/**
* GC
* key N
*/
gc(maxVersionsPerKey: number = 100): void {
for (const [tableKey, versions] of this.versionStore) {
if (versions.length <= maxVersionsPerKey) continue;
// 保留最新的 maxVersionsPerKey 个版本
const pruned = versions.slice(versions.length - maxVersionsPerKey);
this.versionStore.set(tableKey, pruned);
}
}
/**
* key
*/
getActiveWriteKeys(tableName: string, txnId: number): Set<string> {
const keys = new Set<string>();
const prefix = `${tableName}.`;
for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(prefix)) continue;
const latestVersion = versions[versions.length - 1];
if (latestVersion.txnId === txnId && !latestVersion.committed) {
keys.add(tableKey.slice(prefix.length));
}
}
return keys;
}
/**
*
*/
clearTable(tableName: string): void {
const prefix = `${tableName}.`;
for (const [tableKey] of this.versionStore) {
if (tableKey.startsWith(prefix)) {
this.versionStore.delete(tableKey);
}
}
}
/**
*
*/
getActiveTxnCount(): number {
return this.activeTxns.size;
}
/**
* LSN
*/
getGlobalLSN(): number {
return this.globalCommitLsn;
}
}
/**
* AriaEngine MVCC
* @module engine/aria/transaction/mvcc
*
* (Snapshot Isolation)
*
*/
import type { RowVersion, TxnEntry } from '../types';
import { TransactionState } from '../types';
// ---------------------------------------------------------------------------
// MVCCManager
// ---------------------------------------------------------------------------
export class MVCCManager {
/** 所有行版本的存储:tableName.key → 版本链 */
private versionStore: Map<string, RowVersion[]> = new Map();
/** 活跃事务表:txnId → TxnEntry */
private activeTxns: Map<number, TxnEntry> = new Map();
/** 事务 ID 计数器 */
private nextTxnId = 1;
/** 全局提交序列号(用于可见性判断) */
private globalCommitLsn = 0;
// =======================================================================
// 事务管理
// =======================================================================
/** 开始一个事务,返回事务 ID */
beginTransaction(): number {
const txnId = this.nextTxnId++;
this.activeTxns.set(txnId, {
txnId,
state: TransactionState.ACTIVE,
snapshotLsn: this.globalCommitLsn,
startTime: Date.now(),
});
return txnId;
}
/** 提交事务 */
commitTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.COMMITTED;
this.globalCommitLsn++;
// 标记此事务写入的所有版本为已提交
for (const [, versions] of this.versionStore) {
for (const version of versions) {
if (version.txnId === txnId) {
version.committed = true;
}
}
}
// 清理已提交事务的记录
this.activeTxns.delete(txnId);
}
/** 回滚事务 */
rollbackTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.ABORTED;
// 移除此事务写入的所有版本
for (const [tableKey, versions] of this.versionStore) {
const filtered = versions.filter((v) => v.txnId !== txnId);
if (filtered.length === 0) {
this.versionStore.delete(tableKey);
} else {
this.versionStore.set(tableKey, filtered);
}
}
this.activeTxns.delete(txnId);
}
/** 检查事务是否活跃 */
isActive(txnId: number): boolean {
const txn = this.activeTxns.get(txnId);
return txn !== undefined && txn.state === TransactionState.ACTIVE;
}
// =======================================================================
// 版本读写
// =======================================================================
/**
*
*/
writeVersion(
tableName: string,
key: string,
data: Record<string, unknown>,
txnId: number,
): void {
const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey) ?? [];
const newVersion: RowVersion = {
txnId,
data,
prevVersion: versions.length > 0 ? versions[versions.length - 1] : null,
committed: false,
};
versions.push(newVersion);
this.versionStore.set(tableKey, versions);
}
/**
*
*/
readVersion(
tableName: string,
key: string,
txnId: number,
): Record<string, unknown> | null {
const txn = this.activeTxns.get(txnId);
if (!txn) return null;
const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey);
if (!versions || versions.length === 0) return null;
// 从最新版本向前遍历
for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i];
// 1. 如果是当前事务写入的(未提交),可见
if (version.txnId === txnId) {
return version.data;
}
// 2. 如果是已提交的版本,且在快照 LSN 之前提交,可见
if (version.committed) {
// 简化:所有已提交版本都可见
return version.data;
}
// 3. 其他事务的未提交版本,不可见,继续找更早的版本
}
return null;
}
/**
*
*/
deleteVersion(tableName: string, key: string, txnId: number): void {
this.writeVersion(tableName, key, { __mvcc_tombstone: true } as unknown as Record<string, unknown>, txnId);
}
/**
*
*/
getLatestCommittedVersions(
tableName: string,
): Record<string, Record<string, unknown>> {
const result: Record<string, Record<string, unknown>> = {};
for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(`${tableName}.`)) continue;
const key = tableKey.slice(tableName.length + 1);
for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i];
if (version.committed && !(version.data as unknown as Record<string, unknown>).__mvcc_tombstone) {
result[key] = version.data;
break;
}
}
}
return result;
}
/**
* GC
* key N
*/
gc(maxVersionsPerKey: number = 100): void {
for (const [tableKey, versions] of this.versionStore) {
if (versions.length <= maxVersionsPerKey) continue;
// 保留最新的 maxVersionsPerKey 个版本
const pruned = versions.slice(versions.length - maxVersionsPerKey);
this.versionStore.set(tableKey, pruned);
}
}
/**
* key
*/
getActiveWriteKeys(tableName: string, txnId: number): Set<string> {
const keys = new Set<string>();
const prefix = `${tableName}.`;
for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(prefix)) continue;
const latestVersion = versions[versions.length - 1];
if (latestVersion.txnId === txnId && !latestVersion.committed) {
keys.add(tableKey.slice(prefix.length));
}
}
return keys;
}
/**
*
*/
clearTable(tableName: string): void {
const prefix = `${tableName}.`;
for (const [tableKey] of this.versionStore) {
if (tableKey.startsWith(prefix)) {
this.versionStore.delete(tableKey);
}
}
}
/**
*
*/
getActiveTxnCount(): number {
return this.activeTxns.size;
}
/**
* LSN
*/
getGlobalLSN(): number {
return this.globalCommitLsn;
}
}
+302 -302
View File
@@ -1,302 +1,302 @@
/**
* AriaEngine Types
* @module engine/aria/types
*
*
*/
// =============================================================================
// 页面常量
// =============================================================================
/** 页面大小:4KB */
export const PAGE_SIZE = 4096;
/** 页面头大小:16 字节 */
export const PAGE_HEADER_SIZE = 16;
/** 每个 Slot 目录项大小:4 字节 (offset: u16 + len: u16) */
export const SLOT_ENTRY_SIZE = 4;
/** 页面数据区起始偏移(头部之后) */
export const PAGE_DATA_START = PAGE_HEADER_SIZE;
/** 无效页面 ID */
export const INVALID_PAGE_ID = 0xFFFFFFFF;
// =============================================================================
// 页面类型
// =============================================================================
export enum PageType {
/** 数据页面:存储行数据 */
DATA = 1,
/** 索引页面:存储索引节点 */
INDEX = 2,
/** 溢出页面:存储大字段 */
OVERFLOW = 3,
/** 元数据页面:存储表/库元信息 */
META = 4,
}
// =============================================================================
// 页面头部(16 字节)
// =============================================================================
export interface PageHeader {
/** 页面 ID(全局唯一) */
pageId: number;
/** 页面类型 */
type: PageType;
/** 空闲空间起始偏移(slot 区结束位置) */
freeStart: number;
/** 数据区结束偏移(从页面底部向上增长) */
freeEnd: number;
/** 当前 slot 数量 */
slotCount: number;
/** CRC32 校验和 */
checksum: number;
}
// =============================================================================
// Slot 目录项(4 字节)
// =============================================================================
export interface SlotEntry {
/** 行数据在页面内的偏移 */
offset: number;
/** 行数据长度(字节) */
length: number;
}
// =============================================================================
// 页面句柄(Buffer Pool 中的页面)
// =============================================================================
export interface PageHandle {
/** 页面 ID */
pageId: number;
/** 页面类型 */
type: PageType;
/** 页面数据缓冲区(4KB ArrayBuffer */
data: ArrayBuffer;
/** 是否被修改(脏页) */
dirty: boolean;
/** 引用计数(pin count */
pins: number;
/** LRU 链表前驱 */
prev: PageHandle | null;
/** LRU 链表后继 */
next: PageHandle | null;
/** 最后访问时间戳 */
lastAccess: number;
}
// =============================================================================
// 行编解码
// =============================================================================
/** 行/元组的二进制表示 */
export interface SerializedTuple {
/** 序列化后的字节数组 */
bytes: Uint8Array;
/** 该行中 null 列的位图 */
nullBitmap: Uint8Array;
}
/** 列类型(内部二进制编码用) */
export enum ColumnEncoding {
STRING = 1,
NUMBER = 2,
BOOLEAN = 3,
DATE = 4,
JSON = 5,
NULL = 6,
}
// =============================================================================
// LSM-Tree
// =============================================================================
/** MemTable 最大大小(默认 4MB */
export const DEFAULT_MEMTABLE_SIZE = 4 * 1024 * 1024;
/** SSTable 中每个 Data Block 的默认大小 */
export const DEFAULT_BLOCK_SIZE = 4096;
/** Bloom Filter 每 key 的默认位数 */
export const DEFAULT_BLOOM_BITS_PER_KEY = 10;
/** SSTable 最大层级 */
export const MAX_LSM_LEVELS = 7;
/** 每层之间的大小倍数 */
export const DEFAULT_LEVEL_SIZE_MULTIPLIER = 10;
/** SSTable 元数据 */
export interface SSTableMeta {
/** SSTable 文件 ID */
id: number;
/** 所在层级 */
level: number;
/** 最小 key */
minKey: string;
/** 最大 key */
maxKey: string;
/** 数据块数量 */
blockCount: number;
/** 总大小(字节) */
totalSize: number;
/** Bloom Filter 序列化数据 */
bloomData: Uint8Array | null;
}
/** SSTable 内部的 Data Block */
export interface DataBlock {
/** 该块内的 key-value 条目数 */
entryCount: number;
/** 该块数据区 */
data: Uint8Array;
/** 该块起始 key */
startKey: string;
/** 该块结束 key */
endKey: string;
}
/** 索引块条目:key → data block offset */
export interface IndexEntry {
/** 到此 block 的最后一个 key */
key: string;
/** data block 在 SSTable 文件中的偏移 */
blockOffset: number;
/** data block 大小 */
blockSize: number;
}
// =============================================================================
// WAL (Write-Ahead Log)
// =============================================================================
/** WAL 记录类型 */
export enum WALRecordType {
INSERT = 1,
UPDATE = 2,
DELETE = 3,
BEGIN = 4,
COMMIT = 5,
ROLLBACK = 6,
CREATE_TABLE = 7,
DROP_TABLE = 8,
}
/** 单条 WAL 记录 */
export interface WALRecord {
/** 日志序列号 */
lsn: number;
/** 记录类型 */
type: WALRecordType;
/** 事务 ID */
txnId: number;
/** 表名 */
tableName: string;
/** 主键值 */
key: string;
/** 操作数据(INSERT/UPDATE 时有效) */
data?: Record<string, unknown>;
/** 校验和 */
checksum: number;
}
// =============================================================================
// MVCC
// =============================================================================
/** 事务隔离级别 */
export enum IsolationLevel {
READ_COMMITTED = 1,
SNAPSHOT = 2,
}
/** 事务状态 */
export enum TransactionState {
ACTIVE = 1,
COMMITTED = 2,
ABORTED = 3,
}
/** 行版本 */
export interface RowVersion {
/** 事务 ID(创建此版本的事务) */
txnId: number;
/** 版本数据 */
data: Record<string, unknown>;
/** 指向上一版本的指针(undo 链) */
prevVersion: RowVersion | null;
/** 该版本是否已提交 */
committed: boolean;
}
/** 活跃事务表项 */
export interface TxnEntry {
/** 事务 ID */
txnId: number;
/** 事务状态 */
state: TransactionState;
/** 快照序列号(用于可见性判断) */
snapshotLsn: number;
/** 事务开始时间 */
startTime: number;
}
// =============================================================================
// Buffer Pool
// =============================================================================
/** Buffer Pool 默认容量:256 页 ≈ 1MB */
export const DEFAULT_BUFFER_POOL_PAGES = 256;
// =============================================================================
// AriaEngine 配置
// =============================================================================
export interface AriaEngineConfig {
/** 页面大小(默认 4096 */
pageSize?: number;
/** Buffer Pool 页面数量(默认 256 */
bufferPoolPages?: number;
/** MemTable 刷盘阈值(默认 4MB */
memtableSizeThreshold?: number;
/** LSM 层级之间的容量倍数(默认 10) */
levelSizeMultiplier?: number;
/** Bloom Filter 每 key 位数(默认 10 */
bloomFilterBitsPerKey?: number;
/** 是否启用 WAL(默认 true */
walEnabled?: boolean;
/** WAL 同步模式 */
walSyncMode?: 'full' | 'batch' | 'none';
/** Checkpoint 间隔(操作数,默认 1000 */
checkpointInterval?: number;
/** 是否启用页面压缩(默认 false) */
compression?: boolean;
/** 存储后端 */
storageBackend?: 'indexeddb' | 'opfs' | 'memory';
/** WAL 大小阈值(字节,超过则强制 checkpoint,默认 16MB */
walSizeThreshold?: number;
/** 最大内存预算(MB,默认 64) */
maxMemoryMB?: number;
}
export const DEFAULT_ARIA_CONFIG: Required<AriaEngineConfig> = {
pageSize: PAGE_SIZE,
bufferPoolPages: DEFAULT_BUFFER_POOL_PAGES,
memtableSizeThreshold: DEFAULT_MEMTABLE_SIZE,
levelSizeMultiplier: DEFAULT_LEVEL_SIZE_MULTIPLIER,
bloomFilterBitsPerKey: DEFAULT_BLOOM_BITS_PER_KEY,
walEnabled: true,
walSyncMode: 'full',
checkpointInterval: 1000,
compression: false,
storageBackend: 'indexeddb',
walSizeThreshold: 16 * 1024 * 1024, // 16MB
maxMemoryMB: 64,
};
/**
* AriaEngine Types
* @module engine/aria/types
*
*
*/
// =============================================================================
// 页面常量
// =============================================================================
/** 页面大小:4KB */
export const PAGE_SIZE = 4096;
/** 页面头大小:16 字节 */
export const PAGE_HEADER_SIZE = 16;
/** 每个 Slot 目录项大小:4 字节 (offset: u16 + len: u16) */
export const SLOT_ENTRY_SIZE = 4;
/** 页面数据区起始偏移(头部之后) */
export const PAGE_DATA_START = PAGE_HEADER_SIZE;
/** 无效页面 ID */
export const INVALID_PAGE_ID = 0xFFFFFFFF;
// =============================================================================
// 页面类型
// =============================================================================
export enum PageType {
/** 数据页面:存储行数据 */
DATA = 1,
/** 索引页面:存储索引节点 */
INDEX = 2,
/** 溢出页面:存储大字段 */
OVERFLOW = 3,
/** 元数据页面:存储表/库元信息 */
META = 4,
}
// =============================================================================
// 页面头部(16 字节)
// =============================================================================
export interface PageHeader {
/** 页面 ID(全局唯一) */
pageId: number;
/** 页面类型 */
type: PageType;
/** 空闲空间起始偏移(slot 区结束位置) */
freeStart: number;
/** 数据区结束偏移(从页面底部向上增长) */
freeEnd: number;
/** 当前 slot 数量 */
slotCount: number;
/** CRC32 校验和 */
checksum: number;
}
// =============================================================================
// Slot 目录项(4 字节)
// =============================================================================
export interface SlotEntry {
/** 行数据在页面内的偏移 */
offset: number;
/** 行数据长度(字节) */
length: number;
}
// =============================================================================
// 页面句柄(Buffer Pool 中的页面)
// =============================================================================
export interface PageHandle {
/** 页面 ID */
pageId: number;
/** 页面类型 */
type: PageType;
/** 页面数据缓冲区(4KB ArrayBuffer */
data: ArrayBuffer;
/** 是否被修改(脏页) */
dirty: boolean;
/** 引用计数(pin count */
pins: number;
/** LRU 链表前驱 */
prev: PageHandle | null;
/** LRU 链表后继 */
next: PageHandle | null;
/** 最后访问时间戳 */
lastAccess: number;
}
// =============================================================================
// 行编解码
// =============================================================================
/** 行/元组的二进制表示 */
export interface SerializedTuple {
/** 序列化后的字节数组 */
bytes: Uint8Array;
/** 该行中 null 列的位图 */
nullBitmap: Uint8Array;
}
/** 列类型(内部二进制编码用) */
export enum ColumnEncoding {
STRING = 1,
NUMBER = 2,
BOOLEAN = 3,
DATE = 4,
JSON = 5,
NULL = 6,
}
// =============================================================================
// LSM-Tree
// =============================================================================
/** MemTable 最大大小(默认 4MB */
export const DEFAULT_MEMTABLE_SIZE = 4 * 1024 * 1024;
/** SSTable 中每个 Data Block 的默认大小 */
export const DEFAULT_BLOCK_SIZE = 4096;
/** Bloom Filter 每 key 的默认位数 */
export const DEFAULT_BLOOM_BITS_PER_KEY = 10;
/** SSTable 最大层级 */
export const MAX_LSM_LEVELS = 7;
/** 每层之间的大小倍数 */
export const DEFAULT_LEVEL_SIZE_MULTIPLIER = 10;
/** SSTable 元数据 */
export interface SSTableMeta {
/** SSTable 文件 ID */
id: number;
/** 所在层级 */
level: number;
/** 最小 key */
minKey: string;
/** 最大 key */
maxKey: string;
/** 数据块数量 */
blockCount: number;
/** 总大小(字节) */
totalSize: number;
/** Bloom Filter 序列化数据 */
bloomData: Uint8Array | null;
}
/** SSTable 内部的 Data Block */
export interface DataBlock {
/** 该块内的 key-value 条目数 */
entryCount: number;
/** 该块数据区 */
data: Uint8Array;
/** 该块起始 key */
startKey: string;
/** 该块结束 key */
endKey: string;
}
/** 索引块条目:key → data block offset */
export interface IndexEntry {
/** 到此 block 的最后一个 key */
key: string;
/** data block 在 SSTable 文件中的偏移 */
blockOffset: number;
/** data block 大小 */
blockSize: number;
}
// =============================================================================
// WAL (Write-Ahead Log)
// =============================================================================
/** WAL 记录类型 */
export enum WALRecordType {
INSERT = 1,
UPDATE = 2,
DELETE = 3,
BEGIN = 4,
COMMIT = 5,
ROLLBACK = 6,
CREATE_TABLE = 7,
DROP_TABLE = 8,
}
/** 单条 WAL 记录 */
export interface WALRecord {
/** 日志序列号 */
lsn: number;
/** 记录类型 */
type: WALRecordType;
/** 事务 ID */
txnId: number;
/** 表名 */
tableName: string;
/** 主键值 */
key: string;
/** 操作数据(INSERT/UPDATE 时有效) */
data?: Record<string, unknown>;
/** 校验和 */
checksum: number;
}
// =============================================================================
// MVCC
// =============================================================================
/** 事务隔离级别 */
export enum IsolationLevel {
READ_COMMITTED = 1,
SNAPSHOT = 2,
}
/** 事务状态 */
export enum TransactionState {
ACTIVE = 1,
COMMITTED = 2,
ABORTED = 3,
}
/** 行版本 */
export interface RowVersion {
/** 事务 ID(创建此版本的事务) */
txnId: number;
/** 版本数据 */
data: Record<string, unknown>;
/** 指向上一版本的指针(undo 链) */
prevVersion: RowVersion | null;
/** 该版本是否已提交 */
committed: boolean;
}
/** 活跃事务表项 */
export interface TxnEntry {
/** 事务 ID */
txnId: number;
/** 事务状态 */
state: TransactionState;
/** 快照序列号(用于可见性判断) */
snapshotLsn: number;
/** 事务开始时间 */
startTime: number;
}
// =============================================================================
// Buffer Pool
// =============================================================================
/** Buffer Pool 默认容量:256 页 ≈ 1MB */
export const DEFAULT_BUFFER_POOL_PAGES = 256;
// =============================================================================
// AriaEngine 配置
// =============================================================================
export interface AriaEngineConfig {
/** 页面大小(默认 4096 */
pageSize?: number;
/** Buffer Pool 页面数量(默认 256 */
bufferPoolPages?: number;
/** MemTable 刷盘阈值(默认 4MB */
memtableSizeThreshold?: number;
/** LSM 层级之间的容量倍数(默认 10) */
levelSizeMultiplier?: number;
/** Bloom Filter 每 key 位数(默认 10 */
bloomFilterBitsPerKey?: number;
/** 是否启用 WAL(默认 true */
walEnabled?: boolean;
/** WAL 同步模式 */
walSyncMode?: 'full' | 'batch' | 'none';
/** Checkpoint 间隔(操作数,默认 1000 */
checkpointInterval?: number;
/** 是否启用页面压缩(默认 false) */
compression?: boolean;
/** 存储后端 */
storageBackend?: 'indexeddb' | 'opfs' | 'memory';
/** WAL 大小阈值(字节,超过则强制 checkpoint,默认 16MB */
walSizeThreshold?: number;
/** 最大内存预算(MB,默认 64) */
maxMemoryMB?: number;
}
export const DEFAULT_ARIA_CONFIG: Required<AriaEngineConfig> = {
pageSize: PAGE_SIZE,
bufferPoolPages: DEFAULT_BUFFER_POOL_PAGES,
memtableSizeThreshold: DEFAULT_MEMTABLE_SIZE,
levelSizeMultiplier: DEFAULT_LEVEL_SIZE_MULTIPLIER,
bloomFilterBitsPerKey: DEFAULT_BLOOM_BITS_PER_KEY,
walEnabled: true,
walSyncMode: 'full',
checkpointInterval: 1000,
compression: false,
storageBackend: 'indexeddb',
walSizeThreshold: 16 * 1024 * 1024, // 16MB
maxMemoryMB: 64,
};
+77 -77
View File
@@ -1,77 +1,77 @@
/**
* AriaEngine Checkpoint
* @module engine/aria/wal/checkpoint
*/
import type { LSM } from '../index/lsm';
import type { WAL } from './log';
// ---------------------------------------------------------------------------
// 简化的 flush 接口
// ---------------------------------------------------------------------------
export interface Flushable {
flushAll(): Promise<void>;
}
// ---------------------------------------------------------------------------
// CheckpointManager
// ---------------------------------------------------------------------------
export class CheckpointManager {
private lsm: LSM;
private wal: WAL;
private flushable: Flushable | null;
private interval: number;
private opCount = 0;
private walSizeThreshold: number;
constructor(
lsm: LSM,
wal: WAL,
flushable: Flushable | null = null,
interval: number = 1000,
walSizeThreshold: number = 16 * 1024 * 1024,
) {
this.lsm = lsm;
this.wal = wal;
this.flushable = flushable;
this.interval = interval;
this.walSizeThreshold = walSizeThreshold;
}
async tick(): Promise<void> {
this.opCount++;
// 检查操作计数或 WAL 大小是否超阈值
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
await this.checkpoint();
}
}
/** 估算 WAL 大小 */
private getWALEstimatedSize(): number {
const count = typeof this.wal.getBufferedCount === 'function' ? this.wal.getBufferedCount() : 0;
return count * 200;
}
async checkpoint(): Promise<void> {
await this.lsm.flush();
if (this.flushable) {
await this.flushable.flushAll();
}
await this.wal.checkpoint();
this.opCount = 0;
}
async forceCheckpoint(): Promise<void> {
await this.checkpoint();
}
setInterval(ops: number): void {
this.interval = ops;
}
getOpCount(): number {
return this.opCount;
}
}
/**
* AriaEngine Checkpoint
* @module engine/aria/wal/checkpoint
*/
import type { LSM } from '../index/lsm';
import type { WAL } from './log';
// ---------------------------------------------------------------------------
// 简化的 flush 接口
// ---------------------------------------------------------------------------
export interface Flushable {
flushAll(): Promise<void>;
}
// ---------------------------------------------------------------------------
// CheckpointManager
// ---------------------------------------------------------------------------
export class CheckpointManager {
private lsm: LSM;
private wal: WAL;
private flushable: Flushable | null;
private interval: number;
private opCount = 0;
private walSizeThreshold: number;
constructor(
lsm: LSM,
wal: WAL,
flushable: Flushable | null = null,
interval: number = 1000,
walSizeThreshold: number = 16 * 1024 * 1024,
) {
this.lsm = lsm;
this.wal = wal;
this.flushable = flushable;
this.interval = interval;
this.walSizeThreshold = walSizeThreshold;
}
async tick(): Promise<void> {
this.opCount++;
// 检查操作计数或 WAL 大小是否超阈值
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
await this.checkpoint();
}
}
/** 估算 WAL 大小 */
private getWALEstimatedSize(): number {
const count = typeof this.wal.getBufferedCount === 'function' ? this.wal.getBufferedCount() : 0;
return count * 200;
}
async checkpoint(): Promise<void> {
await this.lsm.flush();
if (this.flushable) {
await this.flushable.flushAll();
}
await this.wal.checkpoint();
this.opCount = 0;
}
async forceCheckpoint(): Promise<void> {
await this.checkpoint();
}
setInterval(ops: number): void {
this.interval = ops;
}
getOpCount(): number {
return this.opCount;
}
}
+313 -283
View File
@@ -1,283 +1,313 @@
/**
* AriaEngine WAL Write-Ahead Log
* @module engine/aria/wal/log
*
*
*
* WAL :
*
* Record 1 Record 2 ...
* 4B LSN
* 1B type
* 4B txnId
* 2B tblLen
* N table
* 2B keyLen
* N key
* 4B jsonLen
* N json
* 4B CRC
*
*/
import { WALRecordType, type WALRecord } from '../types';
import type { BufferPool } from '../buffer/pool';
// ---------------------------------------------------------------------------
// WAL 存储接口
// ---------------------------------------------------------------------------
export interface WALStore {
/** 追加 WAL 记录 */
append(data: Uint8Array): Promise<void>;
/** 读取所有 WAL 记录 */
readAll(): Promise<Uint8Array>;
/** 截断 WALcheckpoint 后清理) */
truncate(): Promise<void>;
/** 检查 WAL 是否存在 */
exists(): Promise<boolean>;
}
// ---------------------------------------------------------------------------
// WAL
// ---------------------------------------------------------------------------
export class WAL {
private lsn = 0;
private store: WALStore;
private enabled: boolean;
private buffer: Uint8Array[] = [];
private syncMode: 'full' | 'batch' | 'none';
constructor(store: WALStore, enabled: boolean = true, syncMode: 'full' | 'batch' | 'none' = 'batch') {
this.store = store;
this.enabled = enabled;
this.syncMode = syncMode;
}
// =======================================================================
// 写入
// =======================================================================
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
async append(record: Omit<WALRecord, 'lsn' | 'checksum'>): Promise<void> {
if (!this.enabled) return;
this.lsn++;
const fullRecord: WALRecord = {
...record,
lsn: this.lsn,
checksum: 0, // 稍后计算
};
const bytes = this.encodeRecord(fullRecord);
if (this.syncMode === 'full') {
try {
await this.store.append(bytes);
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append record');
}
} else if (this.syncMode === 'batch') {
this.buffer.push(bytes);
}
// 'none' mode: 不写 WAL
}
/** 批量刷新缓冲的 WAL 记录 */
async flush(): Promise<void> {
if (!this.enabled || this.buffer.length === 0) return;
const totalLen = this.buffer.reduce((sum, b) => sum + b.byteLength, 0);
const combined = new Uint8Array(totalLen);
let offset = 0;
for (const buf of this.buffer) {
combined.set(buf, offset);
offset += buf.byteLength;
}
await this.store.append(combined);
this.buffer = [];
}
// =======================================================================
// 恢复
// =======================================================================
/** 从 WAL 恢复未提交的事务数据 */
async recover(
applyRecord: (record: WALRecord) => void,
): Promise<number> {
if (!this.enabled) return 0;
const exists = await this.store.exists();
if (!exists) return 0;
const data = await this.store.readAll();
if (data.byteLength === 0) return 0;
const records = this.decodeAllRecords(data);
for (const record of records) {
applyRecord(record);
}
this.lsn = records.length > 0 ? records[records.length - 1].lsn : 0;
return records.length;
}
// =======================================================================
// Checkpoint
// =======================================================================
/** Checkpoint 后清空 WAL */
async checkpoint(): Promise<void> {
if (!this.enabled) return;
await this.flush();
await this.store.truncate();
this.lsn = 0;
}
// =======================================================================
// 统计
// =======================================================================
isEnabled(): boolean {
return this.enabled;
}
getLSN(): number {
return this.lsn;
}
getBufferedCount(): number {
return this.buffer.length;
}
// -----------------------------------------------------------------------
// 编解码
// -----------------------------------------------------------------------
private encodeRecord(record: WALRecord): Uint8Array {
const encoder = new TextEncoder();
const tableBytes = encoder.encode(record.tableName);
const keyBytes = encoder.encode(record.key);
const jsonStr = record.data ? JSON.stringify(record.data) : '';
const jsonBytes = encoder.encode(jsonStr);
const size =
4 + // LSN
1 + // type
4 + // txnId
2 + tableBytes.length + // table
2 + keyBytes.length + // key
4 + jsonBytes.length + // json
4; // CRC
const buf = new ArrayBuffer(size);
const view = new DataView(buf);
let offset = 0;
view.setUint32(offset, record.lsn, false);
offset += 4;
view.setUint8(offset, record.type);
offset += 1;
view.setUint32(offset, record.txnId, false);
offset += 4;
view.setUint16(offset, tableBytes.length, false);
offset += 2;
new Uint8Array(buf).set(tableBytes, offset);
offset += tableBytes.length;
view.setUint16(offset, keyBytes.length, false);
offset += 2;
new Uint8Array(buf).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, jsonBytes.length, false);
offset += 4;
new Uint8Array(buf).set(jsonBytes, offset);
offset += jsonBytes.length;
// 简单 CRC
let crc = 0;
const u8 = new Uint8Array(buf, 0, offset);
for (let i = 0; i < u8.length; i++) {
crc = ((crc << 5) - crc + u8[i]) | 0;
}
view.setUint32(offset, crc >>> 0, false);
return new Uint8Array(buf);
}
private decodeAllRecords(data: Uint8Array): WALRecord[] {
const records: WALRecord[] = [];
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
let offset = 0;
while (offset + 15 <= data.byteLength) {
try {
const recordStart = offset;
const lsn = view.getUint32(offset, false);
offset += 4;
const type = view.getUint8(offset) as WALRecordType;
offset += 1;
const txnId = view.getUint32(offset, false);
offset += 4;
const tableLen = view.getUint16(offset, false);
offset += 2;
if (offset + tableLen > data.byteLength) break;
const tableName = new TextDecoder().decode(data.slice(offset, offset + tableLen));
offset += tableLen;
const keyLen = view.getUint16(offset, false);
offset += 2;
if (offset + keyLen > data.byteLength) break;
const key = new TextDecoder().decode(data.slice(offset, offset + keyLen));
offset += keyLen;
const jsonLen = view.getUint32(offset, false);
offset += 4;
if (offset + jsonLen > data.byteLength) break;
let recordData: Record<string, unknown> | undefined;
if (jsonLen > 0) {
const json = new TextDecoder().decode(data.slice(offset, offset + jsonLen));
try {
recordData = JSON.parse(json);
} catch { /* ok */ }
}
offset += jsonLen;
// 验证 CRC(跨记录数据计算,不含 CRC 自身)
const storedCrc = view.getUint32(offset, false);
offset += 4;
const recordBytes = data.slice(recordStart, offset - 4);
let computedCrc = 0;
for (let i = 0; i < recordBytes.length; i++) {
computedCrc = ((computedCrc << 5) - computedCrc + recordBytes[i]) | 0;
}
if ((computedCrc >>> 0) !== storedCrc) {
// CRC 不匹配,跳过此损坏记录
console.warn(`[AriaEngine WAL] CRC mismatch at record LSN=${lsn}, skipping`);
continue;
}
records.push({
lsn,
type,
txnId,
tableName,
key,
data: recordData,
checksum: storedCrc,
});
} catch {
break;
}
}
return records;
}
}
/**
* AriaEngine WAL Write-Ahead Log
* @module engine/aria/wal/log
*
*
*
* WAL :
*
* Record 1 Record 2 ...
* 4B LSN
* 1B type
* 4B txnId
* 2B tblLen
* N table
* 2B keyLen
* N key
* 4B jsonLen
* N json
* 4B CRC
*
*/
import { WALRecordType, type WALRecord } from '../types';
// ---------------------------------------------------------------------------
// WAL 存储接口
// ---------------------------------------------------------------------------
export interface WALStore {
/** 追加 WAL 记录 */
append(data: Uint8Array): Promise<void>;
/** 读取所有 WAL 记录 */
readAll(): Promise<Uint8Array>;
/** 截断 WALcheckpoint 后清理) */
truncate(): Promise<void>;
/** 检查 WAL 是否存在 */
exists(): Promise<boolean>;
}
// ---------------------------------------------------------------------------
// WAL
// ---------------------------------------------------------------------------
export class WAL {
private lsn = 0;
private store: WALStore;
private enabled: boolean;
private buffer: Uint8Array[] = [];
private syncMode: 'full' | 'batch' | 'none';
constructor(store: WALStore, enabled: boolean = true, syncMode: 'full' | 'batch' | 'none' = 'batch') {
this.store = store;
this.enabled = enabled;
this.syncMode = syncMode;
}
// =======================================================================
// 写入
// =======================================================================
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
async append(record: Omit<WALRecord, 'lsn' | 'checksum'>): Promise<void> {
if (!this.enabled) return;
this.lsn++;
const fullRecord: WALRecord = {
...record,
lsn: this.lsn,
checksum: 0, // 稍后计算
};
const bytes = this.encodeRecord(fullRecord);
if (this.syncMode === 'full') {
try {
await this.store.append(bytes);
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append record');
}
} else if (this.syncMode === 'batch') {
this.buffer.push(bytes);
}
// 'none' mode: 不写 WAL
}
/** 批量追加多条 WAL 记录(组提交:合并为一次底层写入,v0.3.1) */
async appendBatch(records: Omit<WALRecord, 'lsn' | 'checksum'>[]): Promise<void> {
if (!this.enabled || records.length === 0) return;
const chunks: Uint8Array[] = [];
for (const record of records) {
this.lsn++;
chunks.push(this.encodeRecord({ ...record, lsn: this.lsn, checksum: 0 }));
}
const combined = this.mergeChunks(chunks);
if (this.syncMode === 'full') {
try {
await this.store.append(combined);
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append batch record');
}
} else if (this.syncMode === 'batch') {
this.buffer.push(combined);
}
// 'none' mode: 不写 WAL
}
/** 批量刷新缓冲的 WAL 记录 */
async flush(): Promise<void> {
if (!this.enabled || this.buffer.length === 0) return;
const combined = this.mergeChunks(this.buffer);
await this.store.append(combined);
this.buffer = [];
}
/** 合并多个字节块为一个连续缓冲区 */
private mergeChunks(chunks: Uint8Array[]): Uint8Array {
if (chunks.length === 1) return chunks[0];
const totalLen = chunks.reduce((sum, b) => sum + b.byteLength, 0);
const combined = new Uint8Array(totalLen);
let offset = 0;
for (const buf of chunks) {
combined.set(buf, offset);
offset += buf.byteLength;
}
return combined;
}
// =======================================================================
// 恢复
// =======================================================================
/** 从 WAL 恢复未提交的事务数据 */
async recover(
applyRecord: (record: WALRecord) => void,
): Promise<number> {
if (!this.enabled) return 0;
const exists = await this.store.exists();
if (!exists) return 0;
const data = await this.store.readAll();
if (data.byteLength === 0) return 0;
const records = this.decodeAllRecords(data);
for (const record of records) {
applyRecord(record);
}
this.lsn = records.length > 0 ? records[records.length - 1].lsn : 0;
return records.length;
}
// =======================================================================
// Checkpoint
// =======================================================================
/** Checkpoint 后清空 WAL */
async checkpoint(): Promise<void> {
if (!this.enabled) return;
await this.flush();
await this.store.truncate();
this.lsn = 0;
}
// =======================================================================
// 统计
// =======================================================================
isEnabled(): boolean {
return this.enabled;
}
getLSN(): number {
return this.lsn;
}
getBufferedCount(): number {
return this.buffer.length;
}
// -----------------------------------------------------------------------
// 编解码
// -----------------------------------------------------------------------
private encodeRecord(record: WALRecord): Uint8Array {
const encoder = new TextEncoder();
const tableBytes = encoder.encode(record.tableName);
const keyBytes = encoder.encode(record.key);
const jsonStr = record.data ? JSON.stringify(record.data) : '';
const jsonBytes = encoder.encode(jsonStr);
const size =
4 + // LSN
1 + // type
4 + // txnId
2 + tableBytes.length + // table
2 + keyBytes.length + // key
4 + jsonBytes.length + // json
4; // CRC
const buf = new ArrayBuffer(size);
const view = new DataView(buf);
let offset = 0;
view.setUint32(offset, record.lsn, false);
offset += 4;
view.setUint8(offset, record.type);
offset += 1;
view.setUint32(offset, record.txnId, false);
offset += 4;
view.setUint16(offset, tableBytes.length, false);
offset += 2;
new Uint8Array(buf).set(tableBytes, offset);
offset += tableBytes.length;
view.setUint16(offset, keyBytes.length, false);
offset += 2;
new Uint8Array(buf).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, jsonBytes.length, false);
offset += 4;
new Uint8Array(buf).set(jsonBytes, offset);
offset += jsonBytes.length;
// 简单 CRC
let crc = 0;
const u8 = new Uint8Array(buf, 0, offset);
for (let i = 0; i < u8.length; i++) {
crc = ((crc << 5) - crc + u8[i]) | 0;
}
view.setUint32(offset, crc >>> 0, false);
return new Uint8Array(buf);
}
private decodeAllRecords(data: Uint8Array): WALRecord[] {
const records: WALRecord[] = [];
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
let offset = 0;
while (offset + 15 <= data.byteLength) {
try {
const recordStart = offset;
const lsn = view.getUint32(offset, false);
offset += 4;
const type = view.getUint8(offset) as WALRecordType;
offset += 1;
const txnId = view.getUint32(offset, false);
offset += 4;
const tableLen = view.getUint16(offset, false);
offset += 2;
if (offset + tableLen > data.byteLength) break;
const tableName = new TextDecoder().decode(data.slice(offset, offset + tableLen));
offset += tableLen;
const keyLen = view.getUint16(offset, false);
offset += 2;
if (offset + keyLen > data.byteLength) break;
const key = new TextDecoder().decode(data.slice(offset, offset + keyLen));
offset += keyLen;
const jsonLen = view.getUint32(offset, false);
offset += 4;
if (offset + jsonLen > data.byteLength) break;
let recordData: Record<string, unknown> | undefined;
if (jsonLen > 0) {
const json = new TextDecoder().decode(data.slice(offset, offset + jsonLen));
try {
recordData = JSON.parse(json);
} catch { /* ok */ }
}
offset += jsonLen;
// 验证 CRC(跨记录数据计算,不含 CRC 自身)
const storedCrc = view.getUint32(offset, false);
offset += 4;
const recordBytes = data.slice(recordStart, offset - 4);
let computedCrc = 0;
for (let i = 0; i < recordBytes.length; i++) {
computedCrc = ((computedCrc << 5) - computedCrc + recordBytes[i]) | 0;
}
if ((computedCrc >>> 0) !== storedCrc) {
// CRC 不匹配,跳过此损坏记录
// eslint-disable-next-line no-console
console.warn(`[AriaEngine WAL] CRC mismatch at record LSN=${lsn}, skipping`);
continue;
}
records.push({
lsn,
type,
txnId,
tableName,
key,
data: recordData,
checksum: storedCrc,
});
} catch {
break;
}
}
return records;
}
}
+11 -11
View File
@@ -1,11 +1,11 @@
/**
* metona-sqlark Engine
* @module engine
*/
export type { IStorageEngine } from './interface';
export { MemoryEngine } from './memory';
export { IndexedDBEngine } from './indexeddb';
export { OPFSEngine } from './opfs';
export { AriaEngine } from './aria/index';
export type { AriaEngineConfig } from './aria/types';
/**
* metona-sqlark Engine
* @module engine
*/
export type { IStorageEngine } from './interface';
export { MemoryEngine } from './memory';
export { IndexedDBEngine } from './indexeddb';
export { OPFSEngine } from './opfs';
export { AriaEngine } from './aria/index';
export type { AriaEngineConfig } from './aria/types';
+153 -4
View File
@@ -27,7 +27,7 @@ export class IndexedDBEngine implements IStorageEngine {
await this.memoryCache.open(dbName, version);
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onsuccess = () => {
request.onsuccess = async () => {
this.db = request.result;
// 多标签页冲突处理:其他标签页升级版本时自动关闭当前连接
this.db.onversionchange = () => {
@@ -38,13 +38,89 @@ export class IndexedDBEngine implements IStorageEngine {
console.warn(`[metona-sqlark] Database "${dbName}" was upgraded in another tab. Connection closed. Please re-open.`);
}
};
resolve();
try {
// v0.3.2: reopen 后从 IDB 重建 schemaschema 此前只存内存缓存,重开连接即丢失)
await this.rebuildSchemaFromIDB();
resolve();
} catch (error) {
reject(new DatabaseError(`Failed to restore schema for "${dbName}"`, 'IDB_SCHEMA_RESTORE_ERROR', error));
}
};
request.onerror = () => reject(new DatabaseError(`Failed to open IndexedDB "${dbName}"`, 'IDB_OPEN_ERROR', request.error));
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${dbName}" is blocked`, 'IDB_BLOCKED'));
});
}
/**
* IDB schema
* 1. schema '__metona_schema' storev0.3.2
* 2. 退 objectStore / /
*/
private async rebuildSchemaFromIDB(): Promise<void> {
const db = this.ensureDB();
// 1. 持久化 schema
if (db.objectStoreNames.contains('__metona_schema')) {
const records: { name: string; schema: string }[] = await new Promise((resolve, reject) => {
const req = db.transaction('__metona_schema', 'readonly').objectStore('__metona_schema').getAll();
req.onsuccess = () => resolve((req.result ?? []) as { name: string; schema: string }[]);
req.onerror = () => reject(req.error);
});
for (const rec of records) {
try {
const schema = JSON.parse(rec.schema) as TableSchema;
if (!(await this.memoryCache.getTableSchema(schema.name))) {
await this.memoryCache.createTable(schema);
}
} catch {
// 损坏的 schema 记录忽略
}
}
}
// 2. 回退:无持久化 schema 的表从 IDB 结构推断
const storeNames = Array.from(db.objectStoreNames).filter((n) => n !== '__metona_schema');
for (const tableName of storeNames) {
// 已有 schema(持久化恢复或连续 open)则跳过
const existing = await this.memoryCache.getTableSchema(tableName);
if (existing) continue;
const columns: Record<string, import('../constants').ColumnDef> = {};
const tx = db.transaction(tableName, 'readonly');
const store = tx.objectStore(tableName);
// 主键列
const pk = store.keyPath as string;
columns[pk] = { type: 'string', primaryKey: true };
// 索引列(idx_ 前缀约定)
for (const idxName of Array.from(store.indexNames)) {
if (idxName.startsWith('idx_')) {
const col = idxName.slice(4);
if (!columns[col]) {
columns[col] = { type: 'string', index: true };
}
}
}
// 从样例数据推断其余列的类型
const rows: Record<string, unknown>[] = await new Promise((resolve, reject) => {
const req = store.getAll();
req.onsuccess = () => resolve((req.result ?? []) as Record<string, unknown>[]);
req.onerror = () => reject(req.error);
});
if (rows.length > 0) {
for (const [key, value] of Object.entries(rows[0])) {
if (!columns[key]) {
columns[key] = { type: inferFieldType(value) };
}
}
}
await this.memoryCache.createTable({ name: tableName, columns });
}
}
async close(): Promise<void> {
if (this.db) {
this.db.onversionchange = null; // 清理监听器
@@ -113,6 +189,48 @@ export class IndexedDBEngine implements IStorageEngine {
await this.idbClear(tableName);
}
// ---- 动态索引(v0.3.0):通过版本升级创建/删除 IDB 索引 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
await this.memoryCache.createIndex(tableName, column, unique);
if (this.txActive) return;
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1; db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const idb = (event.target as IDBOpenDBRequest).result;
const tx = idb.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
if (!store.indexNames.contains(`idx_${column}`)) {
store.createIndex(`idx_${column}`, column, { unique: unique ?? false });
}
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to create index "${tableName}.${column}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
async dropIndex(tableName: string, column: string, _indexName?: string): Promise<void> {
await this.memoryCache.dropIndex(tableName, column);
if (this.txActive) return;
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1; db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const idb = (event.target as IDBOpenDBRequest).result;
const tx = idb.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
if (store.indexNames.contains(`idx_${column}`)) {
store.deleteIndex(`idx_${column}`);
}
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to drop index "${tableName}.${column}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
// ---- 事务 ----
async beginTransaction(): Promise<void> {
@@ -154,8 +272,19 @@ export class IndexedDBEngine implements IStorageEngine {
store.createIndex(`idx_${colName}`, colName, { unique: colDef.unique ?? false });
}
}
// v0.3.2: schema 持久化 store(记录在升级完成后的 onsuccess 写入)
if (!db.objectStoreNames.contains('__metona_schema')) {
db.createObjectStore('__metona_schema', { keyPath: 'name' });
}
};
request.onsuccess = () => {
this.db = request.result;
// v0.3.2: 升级完成后持久化 schema(upgrade 事务内异步写会失败)
const schemaTx = this.db.transaction('__metona_schema', 'readwrite');
schemaTx.objectStore('__metona_schema').put({ name: schema.name, schema: JSON.stringify(schema) });
schemaTx.oncomplete = () => resolve();
schemaTx.onerror = () => reject(new DatabaseError(`Failed to persist schema for "${schema.name}"`, 'IDB_SCHEMA_ERROR', schemaTx.error));
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
@@ -169,7 +298,16 @@ export class IndexedDBEngine implements IStorageEngine {
const db = (event.target as IDBOpenDBRequest).result;
if (db.objectStoreNames.contains(tableName)) db.deleteObjectStore(tableName);
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onsuccess = () => {
this.db = request.result;
// v0.3.2: 清理持久化 schema 记录(upgrade 后执行,失败不阻塞删除)
if (this.db.objectStoreNames.contains('__metona_schema')) {
const schemaTx = this.db.transaction('__metona_schema', 'readwrite');
schemaTx.objectStore('__metona_schema').delete(tableName);
schemaTx.onerror = () => { /* 忽略:旧库可能无此记录 */ };
}
resolve();
};
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
@@ -354,3 +492,14 @@ export class IndexedDBEngine implements IStorageEngine {
return this.db;
}
}
/** 从存储值推断字段类型(schema 重建用,v0.3.2 */
function inferFieldType(value: unknown): import('../constants').FieldType {
if (typeof value === 'number') return 'number';
if (typeof value === 'boolean') return 'boolean';
if (typeof value === 'object' && value !== null) return 'json';
if (typeof value === 'string') {
return isNaN(Date.parse(value)) ? 'string' : 'string';
}
return 'string';
}
+8
View File
@@ -55,6 +55,14 @@ export interface IStorageEngine {
/** 清空表数据(保留结构) */
clear(tableName: 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>;
// ---- 事务 ----
/** 开始事务 */
+36 -2
View File
@@ -148,6 +148,41 @@ export class MemoryEngine implements IStorageEngine {
if (tableIndexes) for (const colIndex of tableIndexes.values()) colIndex.clear();
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
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; // 已存在
colDef.index = true;
if (unique) colDef.unique = true;
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)!;
for (const [pk, row] of table) {
const value = row[column];
if (value !== undefined && value !== null) {
if (!colIndex.has(value)) colIndex.set(value, new Set());
colIndex.get(value)!.add(pk);
}
}
}
async dropIndex(tableName: string, column: string, _indexName?: string): Promise<void> {
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');
colDef.index = false;
colDef.unique = false;
const tableIndexes = this.indexes.get(tableName);
if (tableIndexes) tableIndexes.delete(column);
}
// ---- 事务 ----
async beginTransaction(): Promise<void> {
@@ -297,10 +332,9 @@ export class MemoryEngine implements IStorageEngine {
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onDelete) continue;
const [refTable, refCol] = colDef.references.split('.');
const [refTable] = colDef.references.split('.');
if (refTable !== tableName) continue;
const refPkCol = refCol;
const refTableData = this.tables.get(refTableName);
if (!refTableData) continue;
+10
View File
@@ -132,6 +132,16 @@ export class OPFSEngine implements IStorageEngine {
await this.writeTableData(tableName, []);
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
return this.memoryCache.createIndex(tableName, column, unique);
}
async dropIndex(tableName: string, column: string, indexName?: string): Promise<void> {
return this.memoryCache.dropIndex(tableName, column, indexName);
}
// ---- 事务 ----
async beginTransaction(): Promise<void> {
+37 -6
View File
@@ -25,6 +25,8 @@ export class HybridEngine implements IStorageEngine {
private memoryEngine: MemoryEngine;
private diskEngine: IStorageEngine;
private diskEngineType: DiskEngine;
private dbName = '';
private version = 1;
constructor(diskEngine: DiskEngine = 'indexeddb') {
this.memoryEngine = new MemoryEngine();
@@ -35,6 +37,8 @@ export class HybridEngine implements IStorageEngine {
// ---- 生命周期 ----
async open(dbName: string, version: number): Promise<void> {
this.dbName = dbName;
this.version = version;
// 先打开磁盘引擎
await this.diskEngine.open(dbName, version);
@@ -42,6 +46,17 @@ export class HybridEngine implements IStorageEngine {
await this.memoryEngine.open(dbName, version);
// 从磁盘加载现存表
await this.reloadMemoryFromDisk();
}
/**
* v0.3.2
* 使
*/
async reloadMemoryFromDisk(): Promise<void> {
await this.memoryEngine.close();
await this.memoryEngine.open(this.dbName, this.version);
const tableNames = await this.diskEngine.getTableNames();
for (const tableName of tableNames) {
const schema = await this.diskEngine.getTableSchema(tableName);
@@ -53,12 +68,12 @@ export class HybridEngine implements IStorageEngine {
// 从磁盘加载数据到内存
const rows = await this.diskEngine.find(tableName, { table: tableName });
if (rows.length > 0) {
try {
await this.memoryEngine.insert(tableName, rows);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Failed to load table "${tableName}" data from disk:`, e);
}
try {
await this.memoryEngine.insert(tableName, rows);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Failed to load table "${tableName}" data from disk:`, e);
}
}
}
}
@@ -133,6 +148,22 @@ export class HybridEngine implements IStorageEngine {
await this.diskEngine.clear(tableName);
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
await this.memoryEngine.createIndex(tableName, column, unique);
if (typeof this.diskEngine.createIndex === 'function') {
await this.diskEngine.createIndex(tableName, column, unique);
}
}
async dropIndex(tableName: string, column: string, indexName?: string): Promise<void> {
await this.memoryEngine.dropIndex(tableName, column, indexName);
if (typeof this.diskEngine.dropIndex === 'function') {
await this.diskEngine.dropIndex(tableName, column, indexName);
}
}
// ---- 事务 ----
async beginTransaction(): Promise<void> {
+91 -91
View File
@@ -1,91 +1,91 @@
/**
* metona-sqlark
* @module metona-sqlark
* @version 0.2.5
*
*
* Query Builder API SQL
*/
import { MetonaSqlark } from './core';
import type { DatabaseConfig } from './constants';
import { VERSION } from './constants';
// 连接池管理器(side-effect: 注入 MetonaSqlark.connect / disconnect 等静态方法)
import './connection-manager';
// ---------------------------------------------------------------------------
// 工厂函数
// ---------------------------------------------------------------------------
/**
*
*
* @example
* ```ts
* const db = await MetonaSqlark.create({
* name: 'my-app',
* mode: 'hybrid',
* });
*
* await db.defineTable('users', {
* id: { type: 'string', primaryKey: true },
* name: { type: 'string', required: true },
* });
*
* await db.table('users').insert({ id: '1', name: 'Alice' });
* const results = await db.query('SELECT * FROM users');
* ```
*/
async function create(config: DatabaseConfig): Promise<MetonaSqlark> {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
// ---------------------------------------------------------------------------
// 全局 API
// ---------------------------------------------------------------------------
const api = {
VERSION,
version: VERSION,
create,
MetonaSqlark,
MeSqlark: MetonaSqlark,
};
// 浏览器全局挂载
declare global { interface Window { MetonaSqlark: typeof api; MeSqlark: typeof api; } }
if (typeof window !== 'undefined') {
window.MetonaSqlark = api;
window.MeSqlark = api;
}
export default api;
export {
api,
VERSION,
create,
MetonaSqlark,
};
// 别名
export const MeSqlark = MetonaSqlark;
// 类型导出
export type { DatabaseConfig, TableSchema, ColumnDef, FieldType, StorageMode, DiskEngine } from './constants';
export type { IStorageEngine } from './engine/interface';
export type { Statement, SelectStatement, InsertStatement, UpdateStatement, DeleteStatement } from './query/ast';
export { MemoryEngine } from './engine/memory';
export { IndexedDBEngine } from './engine/indexeddb';
export { OPFSEngine } from './engine/opfs';
export { AriaEngine } from './engine/aria/index';
export { HybridEngine } from './hybrid/index';
export { Table } from './table/table';
export { parse } from './sql/parser';
export { tokenize } from './sql/lexer';
// AriaEngine 类型 & 后端
export type { AriaEngineConfig } from './engine/aria/types';
export { OPFSBackend } from './engine/aria/store/opfs_backend';
/**
* metona-sqlark
* @module metona-sqlark
* @version 0.2.5
*
*
* Query Builder API SQL
*/
import { MetonaSqlark } from './core';
import type { DatabaseConfig } from './constants';
import { VERSION } from './constants';
// 连接池管理器(side-effect: 注入 MetonaSqlark.connect / disconnect 等静态方法)
import './connection-manager';
// ---------------------------------------------------------------------------
// 工厂函数
// ---------------------------------------------------------------------------
/**
*
*
* @example
* ```ts
* const db = await MetonaSqlark.create({
* name: 'my-app',
* mode: 'hybrid',
* });
*
* await db.defineTable('users', {
* id: { type: 'string', primaryKey: true },
* name: { type: 'string', required: true },
* });
*
* await db.table('users').insert({ id: '1', name: 'Alice' });
* const results = await db.query('SELECT * FROM users');
* ```
*/
async function create(config: DatabaseConfig): Promise<MetonaSqlark> {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
// ---------------------------------------------------------------------------
// 全局 API
// ---------------------------------------------------------------------------
const api = {
VERSION,
version: VERSION,
create,
MetonaSqlark,
MeSqlark: MetonaSqlark,
};
// 浏览器全局挂载
declare global { interface Window { MetonaSqlark: typeof api; MeSqlark: typeof api; } }
if (typeof window !== 'undefined') {
window.MetonaSqlark = api;
window.MeSqlark = api;
}
export default api;
export {
api,
VERSION,
create,
MetonaSqlark,
};
// 别名
export const MeSqlark = MetonaSqlark;
// 类型导出
export type { DatabaseConfig, TableSchema, ColumnDef, FieldType, StorageMode, DiskEngine } from './constants';
export type { IStorageEngine } from './engine/interface';
export type { Statement, SelectStatement, InsertStatement, UpdateStatement, DeleteStatement } from './query/ast';
export { MemoryEngine } from './engine/memory';
export { IndexedDBEngine } from './engine/indexeddb';
export { OPFSEngine } from './engine/opfs';
export { AriaEngine } from './engine/aria/index';
export { HybridEngine } from './hybrid/index';
export { Table } from './table/table';
export { parse, parseAll } from './sql/parser';
export { tokenize } from './sql/lexer';
// AriaEngine 类型 & 后端
export type { AriaEngineConfig } from './engine/aria/types';
export { OPFSBackend } from './engine/aria/store/opfs_backend';
+1 -1
View File
@@ -15,7 +15,7 @@
* }
*/
import type { MetonaSqlark } from '../core';
import { MetonaSqlark } from '../core';
import { useState, useEffect, useCallback, useRef } from 'react';
/** useQuery: 执行 SQL 查询 */
+1 -1
View File
@@ -12,7 +12,7 @@
* const { data, loading, refresh } = useSqlarkQuery(db, 'SELECT * FROM users');
*/
import type { MetonaSqlark } from '../core';
import { MetonaSqlark } from '../core';
import { ref, watch, onMounted, type Ref } from 'vue';
/** useSqlarkQuery: 执行 SQL 查询 */
+275 -209
View File
@@ -1,209 +1,275 @@
/**
* metona-sqlark Query AST
* @module query/ast
*
* QueryBuilder SQL Parser AST
* Executor AST
*/
import type { WhereCondition, OrderBy } from '../constants';
// ---------------------------------------------------------------------------
// AST 语句类型枚举
// ---------------------------------------------------------------------------
export type StatementType =
| 'SELECT'
| 'EXPLAIN'
| 'INSERT'
| 'UPDATE'
| 'DELETE'
| 'CREATE_TABLE'
| 'DROP_TABLE'
| 'ALTER_TABLE'
| 'TRUNCATE_TABLE';
// ---------------------------------------------------------------------------
// 列引用
// ---------------------------------------------------------------------------
/** 列引用,'*' 表示所有列;支持 'table.column' 格式 */
export type ColumnRef = string;
// ---------------------------------------------------------------------------
// JOIN
// ---------------------------------------------------------------------------
/** JOIN 类型 */
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'CROSS';
/** JOIN 子句 */
export interface JoinClause {
type: JoinType;
table: string;
alias?: string;
on: WhereCondition;
}
// ---------------------------------------------------------------------------
// 聚合函数
// ---------------------------------------------------------------------------
/** 聚合函数类型 */
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
/** 聚合表达式 */
export interface AggregateExpression {
type: 'AGGREGATE';
func: AggregateFunc;
column: string; // '*' for COUNT(*)
alias?: string;
}
// ---------------------------------------------------------------------------
// 子查询
// ---------------------------------------------------------------------------
/** 子查询表达式 */
export interface SubqueryExpression {
type: 'SUBQUERY';
statement: SelectStatement;
}
// ---------------------------------------------------------------------------
// DDL: CREATE TABLE
// ---------------------------------------------------------------------------
export interface ASTColumnDef {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
required?: boolean;
default?: unknown;
index?: boolean;
maxLength?: number;
min?: number;
max?: number;
/** 外键引用 */
references?: string;
/** 级联删除 */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 级联更新 */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
}
export interface CreateTableStatement {
type: 'CREATE_TABLE';
name: string;
columns: ASTColumnDef[];
/** IF NOT EXISTS — 表已存在时不报错 */
ifNotExists?: boolean;
}
// ---------------------------------------------------------------------------
// DDL: DROP TABLE
// ---------------------------------------------------------------------------
export interface DropTableStatement {
type: 'DROP_TABLE';
name: string;
/** IF EXISTS — 表不存在时不报错 */
ifExists?: boolean;
}
/** EXPLAIN 查询计划 */
export interface ExplainStatement {
type: 'EXPLAIN';
query: Statement;
}
// ---------------------------------------------------------------------------
// DML: INSERT
// ---------------------------------------------------------------------------
export interface InsertStatement {
type: 'INSERT';
into: string;
columns?: string[];
values: unknown[][];
}
// ---------------------------------------------------------------------------
// DML: UPDATE
// ---------------------------------------------------------------------------
export interface UpdateStatement {
type: 'UPDATE';
table: string;
sets: Record<string, unknown>;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DML: DELETE
// ---------------------------------------------------------------------------
export interface DeleteStatement {
type: 'DELETE';
from: string;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DQL: SELECT
// ---------------------------------------------------------------------------
export interface SelectStatement {
type: 'SELECT';
columns: ColumnRef[];
distinct?: boolean;
from: string;
/** 主表别名 */
alias?: string;
/** JOIN 子句列表 */
joins?: JoinClause[];
where: WhereCondition;
/** GROUP BY */
groupBy?: string[];
/** HAVING */
having?: WhereCondition;
orderBy?: OrderBy[];
limit?: number;
offset?: number;
}
// ---------------------------------------------------------------------------
// DDL: ALTER TABLE
// ---------------------------------------------------------------------------
export interface AlterTableStatement {
type: 'ALTER_TABLE';
name: string;
action: 'ADD' | 'DROP';
column: ASTColumnDef;
}
// ---------------------------------------------------------------------------
// DDL: TRUNCATE TABLE
// ---------------------------------------------------------------------------
export interface TruncateTableStatement {
type: 'TRUNCATE_TABLE';
name: string;
}
// ---------------------------------------------------------------------------
// AST 联合类型
// ---------------------------------------------------------------------------
export type Statement =
| SelectStatement
| ExplainStatement
| InsertStatement
| UpdateStatement
| DeleteStatement
| CreateTableStatement
| DropTableStatement
| AlterTableStatement
| TruncateTableStatement;
/**
* metona-sqlark Query AST
* @module query/ast
*
* QueryBuilder SQL Parser AST
* Executor AST
*/
import type { WhereCondition, OrderBy } from '../constants';
// ---------------------------------------------------------------------------
// AST 语句类型枚举
// ---------------------------------------------------------------------------
export type StatementType =
| 'SELECT'
| 'SELECT_UNION'
| 'EXPLAIN'
| 'INSERT'
| 'UPDATE'
| 'DELETE'
| 'CREATE_TABLE'
| 'DROP_TABLE'
| 'ALTER_TABLE'
| 'TRUNCATE_TABLE'
| 'CREATE_INDEX'
| 'DROP_INDEX'
| 'BEGIN'
| 'COMMIT'
| 'ROLLBACK';
// ---------------------------------------------------------------------------
// 列引用
// ---------------------------------------------------------------------------
/** 列引用,'*' 表示所有列;支持 'table.column' 格式 */
export type ColumnRef = string;
// ---------------------------------------------------------------------------
// JOIN
// ---------------------------------------------------------------------------
/** JOIN 类型 */
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'CROSS';
/** JOIN 子句 */
export interface JoinClause {
type: JoinType;
table: string;
alias?: string;
on: WhereCondition;
}
// ---------------------------------------------------------------------------
// 聚合函数
// ---------------------------------------------------------------------------
/** 聚合函数类型 */
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
/** 聚合表达式 */
export interface AggregateExpression {
type: 'AGGREGATE';
func: AggregateFunc;
column: string; // '*' for COUNT(*)
alias?: string;
}
// ---------------------------------------------------------------------------
// 子查询
// ---------------------------------------------------------------------------
/** 子查询表达式 */
export interface SubqueryExpression {
type: 'SUBQUERY';
statement: SelectStatement | SelectUnionStatement;
}
// ---------------------------------------------------------------------------
// DDL: CREATE TABLE
// ---------------------------------------------------------------------------
export interface ASTColumnDef {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
required?: boolean;
default?: unknown;
index?: boolean;
maxLength?: number;
min?: number;
max?: number;
/** 外键引用 */
references?: string;
/** 级联删除 */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 级联更新 */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
}
export interface CreateTableStatement {
type: 'CREATE_TABLE';
name: string;
columns: ASTColumnDef[];
/** IF NOT EXISTS — 表已存在时不报错 */
ifNotExists?: boolean;
}
// ---------------------------------------------------------------------------
// DDL: DROP TABLE
// ---------------------------------------------------------------------------
export interface DropTableStatement {
type: 'DROP_TABLE';
name: string;
/** IF EXISTS — 表不存在时不报错 */
ifExists?: boolean;
}
/** EXPLAIN 查询计划 */
export interface ExplainStatement {
type: 'EXPLAIN';
query: Statement;
}
// ---------------------------------------------------------------------------
// DML: INSERT
// ---------------------------------------------------------------------------
export interface InsertStatement {
type: 'INSERT';
into: string;
columns?: string[];
/** VALUES 字面量 */
values?: unknown[][];
/** INSERT INTO ... SELECT ...v0.3.0 */
select?: SelectStatement | SelectUnionStatement;
}
// ---------------------------------------------------------------------------
// DML: UPDATE
// ---------------------------------------------------------------------------
export interface UpdateStatement {
type: 'UPDATE';
table: string;
sets: Record<string, unknown>;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DML: DELETE
// ---------------------------------------------------------------------------
export interface DeleteStatement {
type: 'DELETE';
from: string;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DQL: SELECT
// ---------------------------------------------------------------------------
export interface SelectStatement {
type: 'SELECT';
columns: ColumnRef[];
distinct?: boolean;
from: string;
/** 主表别名 */
alias?: string;
/** JOIN 子句列表 */
joins?: JoinClause[];
where: WhereCondition;
/** GROUP BY */
groupBy?: string[];
/** HAVING */
having?: WhereCondition;
orderBy?: OrderBy[];
limit?: number;
offset?: number;
}
// ---------------------------------------------------------------------------
// DQL: UNION
// ---------------------------------------------------------------------------
export interface SelectUnionStatement {
type: 'SELECT_UNION';
/** 左操作数(可以是 SELECT 或嵌套 UNION */
left: SelectStatement | SelectUnionStatement;
/** 右操作数 */
right: SelectStatement | SelectUnionStatement;
/** UNION ALL 不去重 */
all?: boolean;
}
// ---------------------------------------------------------------------------
// DDL: ALTER TABLE
// ---------------------------------------------------------------------------
export interface AlterTableStatement {
type: 'ALTER_TABLE';
name: string;
action: 'ADD' | 'DROP';
column: ASTColumnDef;
}
// ---------------------------------------------------------------------------
// DDL: TRUNCATE TABLE
// ---------------------------------------------------------------------------
export interface TruncateTableStatement {
type: 'TRUNCATE_TABLE';
name: string;
}
// ---------------------------------------------------------------------------
// DDL: CREATE INDEX / DROP INDEXv0.3.0
// ---------------------------------------------------------------------------
export interface CreateIndexStatement {
type: 'CREATE_INDEX';
/** 索引名(语法占位) */
name: string;
table: string;
column: string;
/** UNIQUE 索引 */
unique?: boolean;
}
export interface DropIndexStatement {
type: 'DROP_INDEX';
name: string;
table: string;
column: string;
}
// ---------------------------------------------------------------------------
// TCL: 事务语句(v0.3.0
// ---------------------------------------------------------------------------
export interface BeginTransactionStatement {
type: 'BEGIN';
}
export interface CommitTransactionStatement {
type: 'COMMIT';
}
export interface RollbackTransactionStatement {
type: 'ROLLBACK';
}
// ---------------------------------------------------------------------------
// AST 联合类型
// ---------------------------------------------------------------------------
export type Statement =
| SelectStatement
| SelectUnionStatement
| ExplainStatement
| InsertStatement
| UpdateStatement
| DeleteStatement
| CreateTableStatement
| DropTableStatement
| AlterTableStatement
| TruncateTableStatement
| CreateIndexStatement
| DropIndexStatement
| BeginTransactionStatement
| CommitTransactionStatement
| RollbackTransactionStatement;
+16 -4
View File
@@ -134,12 +134,16 @@ export class SelectQueryBuilder {
export class UpdateQueryBuilder {
private _where: WhereCondition = {};
private onWrite?: (table: string) => void;
constructor(
private engine: IStorageEngine,
private tableName: string,
private _updates: Record<string, unknown>,
) {}
onWrite?: (table: string) => void,
) {
this.onWrite = onWrite;
}
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
@@ -147,7 +151,9 @@ export class UpdateQueryBuilder {
}
async execute(): Promise<number> {
return this.engine.update(this.tableName, { table: this.tableName, where: this._where }, this._updates);
const count = await this.engine.update(this.tableName, { table: this.tableName, where: this._where }, this._updates);
this.onWrite?.(this.tableName);
return count;
}
toAST(): UpdateStatement {
@@ -161,11 +167,15 @@ export class UpdateQueryBuilder {
export class DeleteQueryBuilder {
private _where: WhereCondition = {};
private onWrite?: (table: string) => void;
constructor(
private engine: IStorageEngine,
private tableName: string,
) {}
onWrite?: (table: string) => void,
) {
this.onWrite = onWrite;
}
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
@@ -173,7 +183,9 @@ export class DeleteQueryBuilder {
}
async execute(): Promise<number> {
return this.engine.delete(this.tableName, { table: this.tableName, where: this._where });
const count = await this.engine.delete(this.tableName, { table: this.tableName, where: this._where });
this.onWrite?.(this.tableName);
return count;
}
toAST(): DeleteStatement {
+1061 -448
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -42,6 +42,16 @@ export function matchWhere(
options: { $col?: boolean } = {},
): boolean {
for (const [field, condition] of Object.entries(where)) {
// 顶层 $caseResultv0.3.2):由 Executor 对 CASE WHEN 表达式逐行求值后产生
if (field === '$caseResult') {
if (condition !== true) return false;
continue;
}
// 顶层 $existsv0.3.0):由 Executor.resolveSubqueries 解析为 boolean
if (field === '$exists') {
if (condition !== true) return false;
continue;
}
// 顶层 $and
if (field === '$and') {
const subs = condition as WhereCondition[];
@@ -54,6 +64,11 @@ export function matchWhere(
if (!subs.some((sub) => matchWhere(row, sub, options))) return false;
continue;
}
// 顶层 $notv0.3.2 修复:NOT (expr) 生成的 { $not: inner }
if (field === '$not') {
if (matchWhere(row, condition as WhereCondition, options)) return false;
continue;
}
if (!matchField(row[field], condition, row, options)) return false;
}
return true;
+1164 -901
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -68,6 +68,21 @@ export enum TokenType {
MAX = 'MAX',
DISTINCT = 'DISTINCT',
// v0.3.0: 事务 / UNION / EXISTS / 动态索引
BEGIN = 'BEGIN',
COMMIT = 'COMMIT',
ROLLBACK = 'ROLLBACK',
UNION = 'UNION',
ALL = 'ALL',
INDEX = 'INDEX',
// v0.3.1: CASE WHEN 表达式
CASE = 'CASE',
WHEN = 'WHEN',
THEN = 'THEN',
ELSE = 'ELSE',
END = 'END',
// 标识符 & 字面量
IDENTIFIER = 'IDENTIFIER',
STRING = 'STRING',
@@ -165,4 +180,19 @@ export const KEYWORDS: Record<string, TokenType> = {
'MIN': TokenType.MIN,
'MAX': TokenType.MAX,
'DISTINCT': TokenType.DISTINCT,
// v0.3.0
'BEGIN': TokenType.BEGIN,
'COMMIT': TokenType.COMMIT,
'ROLLBACK': TokenType.ROLLBACK,
'UNION': TokenType.UNION,
'ALL': TokenType.ALL,
'INDEX': TokenType.INDEX,
// v0.3.1
'CASE': TokenType.CASE,
'WHEN': TokenType.WHEN,
'THEN': TokenType.THEN,
'ELSE': TokenType.ELSE,
'END': TokenType.END,
};
+14 -6
View File
@@ -18,11 +18,14 @@ export class Table<T = Record<string, unknown>> {
private engine: IStorageEngine;
private schema: TableSchema | null = null;
private executor: QueryExecutor | undefined;
/** 写入回调(多标签页广播,v0.3.2) */
private onWrite?: (table: string) => void;
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor) {
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor, onWrite?: (table: string) => void) {
this.engine = engine;
this.name = tableName;
this.executor = executor;
this.onWrite = onWrite;
}
// ---- Schema ----
@@ -40,11 +43,14 @@ export class Table<T = Record<string, unknown>> {
async insert(row: T & Record<string, unknown>): Promise<string> {
const pks = await this.engine.insert(this.name, [row as Record<string, unknown>]);
this.onWrite?.(this.name);
return pks[0];
}
async insertMany(rows: (T & Record<string, unknown>)[]): Promise<string[]> {
return this.engine.insert(this.name, rows as Record<string, unknown>[]);
const pks = await this.engine.insert(this.name, rows as Record<string, unknown>[]);
this.onWrite?.(this.name);
return pks;
}
// ---- 查询 ----
@@ -56,13 +62,13 @@ export class Table<T = Record<string, unknown>> {
// ---- 更新 ----
update(updates: Partial<T> & Record<string, unknown>): UpdateQueryBuilder {
return new UpdateQueryBuilder(this.engine, this.name, updates);
return new UpdateQueryBuilder(this.engine, this.name, updates, this.onWrite);
}
// ---- 删除 ----
delete(): DeleteQueryBuilder {
return new DeleteQueryBuilder(this.engine, this.name);
return new DeleteQueryBuilder(this.engine, this.name, this.onWrite);
}
// ---- 聚合 ----
@@ -74,10 +80,12 @@ export class Table<T = Record<string, unknown>> {
// ---- 管理 ----
async clear(): Promise<void> {
return this.engine.clear(this.name);
await this.engine.clear(this.name);
this.onWrite?.(this.name);
}
async drop(): Promise<void> {
return this.engine.dropTable(this.name);
await this.engine.dropTable(this.name);
this.onWrite?.(this.name);
}
}
+273 -273
View File
@@ -1,273 +1,273 @@
/**
* AriaEngine Buffer Pool + Eviction
*/
import { BufferPool, type PageIO } from '../../src/engine/aria/buffer/pool';
import { LRUList, EvictionManager } from '../../src/engine/aria/buffer/eviction';
import { PageType, PAGE_SIZE } from '../../src/engine/aria/types';
import { initPageHeader } from '../../src/engine/aria/page/header';
// ===================================================================
// LRUList
// ===================================================================
describe('AriaEngine — LRUList', () => {
function makePage(id: number) {
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, id, PageType.DATA);
return { pageId: id, type: PageType.DATA, data, dirty: false, pins: 0, prev: null, next: null, lastAccess: Date.now() };
}
it('moveToHead — 单元素', () => {
const list = new LRUList();
const p = makePage(1);
list.moveToHead(p);
expect(list.size).toBe(1);
});
it('moveToHead — 多元素保持 MRU 顺序', () => {
const list = new LRUList();
const a = makePage(1);
const b = makePage(2);
const c = makePage(3);
list.moveToHead(a);
list.moveToHead(b);
list.moveToHead(c);
expect(list.size).toBe(3);
// c 是最新的
});
it('getLRU 返回最久未使用', () => {
const list = new LRUList();
const a = makePage(1);
const b = makePage(2);
list.moveToHead(a);
list.moveToHead(b);
expect(list.getLRU()!.pageId).toBe(1);
});
it('popLRU 移除并返回最久未使用', () => {
const list = new LRUList();
const a = makePage(1);
const b = makePage(2);
list.moveToHead(a);
list.moveToHead(b);
const popped = list.popLRU();
expect(popped!.pageId).toBe(1);
expect(list.size).toBe(1);
});
it('remove — 从中间移除', () => {
const list = new LRUList();
const a = makePage(1);
const b = makePage(2);
const c = makePage(3);
list.moveToHead(a);
list.moveToHead(b);
list.moveToHead(c);
list.remove(b);
expect(list.size).toBe(2);
});
it('clear 清空', () => {
const list = new LRUList();
list.moveToHead(makePage(1));
list.moveToHead(makePage(2));
list.clear();
expect(list.size).toBe(0);
});
it('getAllPages 返回所有页面', () => {
const list = new LRUList();
list.moveToHead(makePage(1));
list.moveToHead(makePage(2));
expect(list.getAllPages()).toHaveLength(2);
});
it('空 LRU getLRU 返回 null', () => {
const list = new LRUList();
expect(list.getLRU()).toBeNull();
});
it('空 LRU popLRU 返回 null', () => {
const list = new LRUList();
expect(list.popLRU()).toBeNull();
});
it('moveToHead 同元素不移重复', () => {
const list = new LRUList();
const p = makePage(1);
list.moveToHead(p);
list.moveToHead(p);
list.moveToHead(p);
expect(list.size).toBe(1);
});
});
// ===================================================================
// EvictionManager
// ===================================================================
describe('AriaEngine — EvictionManager', () => {
function makePage(id: number, dirty = false) {
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, id, PageType.DATA);
return { pageId: id, type: PageType.DATA, data, dirty, pins: 0, prev: null, next: null, lastAccess: Date.now() };
}
it('access 更新 LRU', async () => {
let evicted = -1;
const em = new EvictionManager(3, async (p) => { evicted = p.pageId; });
const p = makePage(1);
em.add(p);
em.access(p);
expect(em.getSize()).toBe(1);
});
it('add 不超过容量不触发驱逐', async () => {
const evictedPages: number[] = [];
const em = new EvictionManager(4, async (p) => { evictedPages.push(p.pageId); });
em.add(makePage(1));
em.add(makePage(2));
em.add(makePage(3));
await em.evictIfNeeded(1);
expect(evictedPages).toHaveLength(0);
expect(em.getSize()).toBe(3);
});
it('evictIfNeeded 超容量触发驱逐', async () => {
const evictedPages: number[] = [];
const em = new EvictionManager(2, async (p) => { evictedPages.push(p.pageId); });
em.add(makePage(1));
em.add(makePage(2));
em.add(makePage(3)); // 超容量
await em.evictIfNeeded(0);
// 驱逐后 size 应 <= 2
expect(em.getSize()).toBeLessThanOrEqual(2);
});
it('remove 减少 size', () => {
const em = new EvictionManager(4, async () => {});
const p = makePage(1);
em.add(p);
em.add(makePage(2));
em.remove(p);
expect(em.getSize()).toBe(1);
});
it('getCapacity 返回配置容量', () => {
const em = new EvictionManager(128, async () => {});
expect(em.getCapacity()).toBe(128);
});
it('clear 清空', () => {
const em = new EvictionManager(4, async () => {});
em.add(makePage(1));
em.add(makePage(2));
em.clear();
expect(em.getSize()).toBe(0);
});
it('脏页驱逐前调用 onEvict 回调', async () => {
let flushed = 0;
const em = new EvictionManager(2, async (_p) => { flushed++; });
em.add(makePage(1, true)); // dirty page
em.add(makePage(2));
await em.evictIfNeeded(1); // need space → evict page 1
expect(flushed).toBeGreaterThanOrEqual(0); // May or may not evict
});
});
// ===================================================================
// BufferPool (with Mock PageIO)
// ===================================================================
describe('AriaEngine — BufferPool', () => {
class MockPageIO implements PageIO {
store = new Map<number, ArrayBuffer>();
nextId = 1;
reads = 0;
writes = 0;
async readPage(pageId: number) { this.reads++; return this.store.get(pageId) ?? null; }
async writePage(pageId: number, data: ArrayBuffer) { this.writes++; this.store.set(pageId, data); }
async allocatePageId() { return this.nextId++; }
async freePageId(_pageId: number) {}
}
it('newPage 创建页面并 pin', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const page = await pool.newPage();
expect(page.pageId).toBe(1);
expect(page.pins).toBe(1);
expect(page.type).toBe(PageType.DATA);
pool.unpin(page);
});
it('getPage — 池中已存在则 pin++', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const p1 = await pool.newPage();
pool.unpin(p1);
const p2 = await pool.getPage(p1.pageId);
expect(p2!.pageId).toBe(p1.pageId);
expect(p2!.pins).toBe(1);
pool.unpin(p2!);
});
it('markDirty + flushPage 写回', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const page = await pool.newPage();
new Uint8Array(page.data)[100] = 42;
pool.markDirty(page);
pool.unpin(page);
await pool.flushPage(page.pageId);
expect(io.writes).toBeGreaterThan(0);
});
it('flushAll 刷新所有脏页', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 8);
const p1 = await pool.newPage();
const p2 = await pool.newPage();
pool.markDirty(p1);
pool.markDirty(p2);
pool.unpin(p1);
pool.unpin(p2);
await pool.flushAll();
expect(io.writes).toBe(2);
});
it('getCachedPageCount 返回缓存数', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
await pool.newPage();
await pool.newPage();
expect(pool.getCachedPageCount()).toBe(2);
});
it('getDirtyPageCount 返回脏页数', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const p = await pool.newPage();
pool.markDirty(p);
pool.unpin(p);
expect(pool.getDirtyPageCount()).toBe(1);
});
it('getCapacity 返回容量', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 64);
expect(pool.getCapacity()).toBe(64);
});
it('removePage 移除缓存', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const p = await pool.newPage();
pool.unpin(p);
pool.removePage(p.pageId);
expect(pool.getCachedPageCount()).toBe(0);
});
});
/**
* AriaEngine Buffer Pool + Eviction
*/
import { BufferPool, type PageIO } from '../../src/engine/aria/buffer/pool';
import { LRUList, EvictionManager } from '../../src/engine/aria/buffer/eviction';
import { PageType, PAGE_SIZE } from '../../src/engine/aria/types';
import { initPageHeader } from '../../src/engine/aria/page/header';
// ===================================================================
// LRUList
// ===================================================================
describe('AriaEngine — LRUList', () => {
function makePage(id: number) {
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, id, PageType.DATA);
return { pageId: id, type: PageType.DATA, data, dirty: false, pins: 0, prev: null, next: null, lastAccess: Date.now() };
}
it('moveToHead — 单元素', () => {
const list = new LRUList();
const p = makePage(1);
list.moveToHead(p);
expect(list.size).toBe(1);
});
it('moveToHead — 多元素保持 MRU 顺序', () => {
const list = new LRUList();
const a = makePage(1);
const b = makePage(2);
const c = makePage(3);
list.moveToHead(a);
list.moveToHead(b);
list.moveToHead(c);
expect(list.size).toBe(3);
// c 是最新的
});
it('getLRU 返回最久未使用', () => {
const list = new LRUList();
const a = makePage(1);
const b = makePage(2);
list.moveToHead(a);
list.moveToHead(b);
expect(list.getLRU()!.pageId).toBe(1);
});
it('popLRU 移除并返回最久未使用', () => {
const list = new LRUList();
const a = makePage(1);
const b = makePage(2);
list.moveToHead(a);
list.moveToHead(b);
const popped = list.popLRU();
expect(popped!.pageId).toBe(1);
expect(list.size).toBe(1);
});
it('remove — 从中间移除', () => {
const list = new LRUList();
const a = makePage(1);
const b = makePage(2);
const c = makePage(3);
list.moveToHead(a);
list.moveToHead(b);
list.moveToHead(c);
list.remove(b);
expect(list.size).toBe(2);
});
it('clear 清空', () => {
const list = new LRUList();
list.moveToHead(makePage(1));
list.moveToHead(makePage(2));
list.clear();
expect(list.size).toBe(0);
});
it('getAllPages 返回所有页面', () => {
const list = new LRUList();
list.moveToHead(makePage(1));
list.moveToHead(makePage(2));
expect(list.getAllPages()).toHaveLength(2);
});
it('空 LRU getLRU 返回 null', () => {
const list = new LRUList();
expect(list.getLRU()).toBeNull();
});
it('空 LRU popLRU 返回 null', () => {
const list = new LRUList();
expect(list.popLRU()).toBeNull();
});
it('moveToHead 同元素不移重复', () => {
const list = new LRUList();
const p = makePage(1);
list.moveToHead(p);
list.moveToHead(p);
list.moveToHead(p);
expect(list.size).toBe(1);
});
});
// ===================================================================
// EvictionManager
// ===================================================================
describe('AriaEngine — EvictionManager', () => {
function makePage(id: number, dirty = false) {
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, id, PageType.DATA);
return { pageId: id, type: PageType.DATA, data, dirty, pins: 0, prev: null, next: null, lastAccess: Date.now() };
}
it('access 更新 LRU', async () => {
let evicted = -1;
const em = new EvictionManager(3, async (p) => { evicted = p.pageId; });
const p = makePage(1);
em.add(p);
em.access(p);
expect(em.getSize()).toBe(1);
});
it('add 不超过容量不触发驱逐', async () => {
const evictedPages: number[] = [];
const em = new EvictionManager(4, async (p) => { evictedPages.push(p.pageId); });
em.add(makePage(1));
em.add(makePage(2));
em.add(makePage(3));
await em.evictIfNeeded(1);
expect(evictedPages).toHaveLength(0);
expect(em.getSize()).toBe(3);
});
it('evictIfNeeded 超容量触发驱逐', async () => {
const evictedPages: number[] = [];
const em = new EvictionManager(2, async (p) => { evictedPages.push(p.pageId); });
em.add(makePage(1));
em.add(makePage(2));
em.add(makePage(3)); // 超容量
await em.evictIfNeeded(0);
// 驱逐后 size 应 <= 2
expect(em.getSize()).toBeLessThanOrEqual(2);
});
it('remove 减少 size', () => {
const em = new EvictionManager(4, async () => {});
const p = makePage(1);
em.add(p);
em.add(makePage(2));
em.remove(p);
expect(em.getSize()).toBe(1);
});
it('getCapacity 返回配置容量', () => {
const em = new EvictionManager(128, async () => {});
expect(em.getCapacity()).toBe(128);
});
it('clear 清空', () => {
const em = new EvictionManager(4, async () => {});
em.add(makePage(1));
em.add(makePage(2));
em.clear();
expect(em.getSize()).toBe(0);
});
it('脏页驱逐前调用 onEvict 回调', async () => {
let flushed = 0;
const em = new EvictionManager(2, async (_p) => { flushed++; });
em.add(makePage(1, true)); // dirty page
em.add(makePage(2));
await em.evictIfNeeded(1); // need space → evict page 1
expect(flushed).toBeGreaterThanOrEqual(0); // May or may not evict
});
});
// ===================================================================
// BufferPool (with Mock PageIO)
// ===================================================================
describe('AriaEngine — BufferPool', () => {
class MockPageIO implements PageIO {
store = new Map<number, ArrayBuffer>();
nextId = 1;
reads = 0;
writes = 0;
async readPage(pageId: number) { this.reads++; return this.store.get(pageId) ?? null; }
async writePage(pageId: number, data: ArrayBuffer) { this.writes++; this.store.set(pageId, data); }
async allocatePageId() { return this.nextId++; }
async freePageId(_pageId: number) {}
}
it('newPage 创建页面并 pin', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const page = await pool.newPage();
expect(page.pageId).toBe(1);
expect(page.pins).toBe(1);
expect(page.type).toBe(PageType.DATA);
pool.unpin(page);
});
it('getPage — 池中已存在则 pin++', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const p1 = await pool.newPage();
pool.unpin(p1);
const p2 = await pool.getPage(p1.pageId);
expect(p2!.pageId).toBe(p1.pageId);
expect(p2!.pins).toBe(1);
pool.unpin(p2!);
});
it('markDirty + flushPage 写回', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const page = await pool.newPage();
new Uint8Array(page.data)[100] = 42;
pool.markDirty(page);
pool.unpin(page);
await pool.flushPage(page.pageId);
expect(io.writes).toBeGreaterThan(0);
});
it('flushAll 刷新所有脏页', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 8);
const p1 = await pool.newPage();
const p2 = await pool.newPage();
pool.markDirty(p1);
pool.markDirty(p2);
pool.unpin(p1);
pool.unpin(p2);
await pool.flushAll();
expect(io.writes).toBe(2);
});
it('getCachedPageCount 返回缓存数', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
await pool.newPage();
await pool.newPage();
expect(pool.getCachedPageCount()).toBe(2);
});
it('getDirtyPageCount 返回脏页数', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const p = await pool.newPage();
pool.markDirty(p);
pool.unpin(p);
expect(pool.getDirtyPageCount()).toBe(1);
});
it('getCapacity 返回容量', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 64);
expect(pool.getCapacity()).toBe(64);
});
it('removePage 移除缓存', async () => {
const io = new MockPageIO();
const pool = new BufferPool(io, 4);
const p = await pool.newPage();
pool.unpin(p);
pool.removePage(p.pageId);
expect(pool.getCachedPageCount()).toBe(0);
});
});
+256
View File
@@ -0,0 +1,256 @@
/**
* AriaEngine SSTable
* @module tests/engine/aria-cache
*
* v0.2.6
* 1. SSTable cacheLimitBytes LRU
* 2. //PK/prefetch
* 3.
*/
import { AriaEngine } from '../../src/engine/aria/index';
import { createSchema } from '../../src/table/schema';
/** 构造小缓存 + 小 MemTable 阈值的引擎,快速产生多个 SSTable */
function createSmallCacheEngine(bufferPoolPages = 2) {
return new AriaEngine({
storageBackend: 'memory',
memtableSizeThreshold: 2048, // ~2KB 阈值 → 300 行会产生多个 SSTable
bufferPoolPages,
checkpointInterval: 100000, // 关闭自动 checkpoint,避免干扰
walSyncMode: 'none',
} as any);
}
function makeRows(count: number): Record<string, unknown>[] {
const rows: Record<string, unknown>[] = [];
for (let i = 0; i < count; i++) {
rows.push({ id: `u${i}`, name: `User${i}`, age: 20 + (i % 30) });
}
return rows;
}
describe('AriaEngine SSTable 缓存内存上限', () => {
test('缓存大小受 cacheLimitBytes 约束', async () => {
const engine = createSmallCacheEngine(2); // 2 * 4096 = 8KB 上限
await engine.open('cache-limit-test', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number', index: true },
}));
await engine.insert('users', makeRows(300));
const lsm = (engine as any).lsm as {
getCacheSize(): number;
getCacheLimit(): number;
getStats(): { sstableCount: number };
};
const stats = lsm.getStats();
// 300 行 / 2KB 阈值 → 应产生多个 SSTable
expect(stats.sstableCount).toBeGreaterThan(1);
// 多轮查询后缓存仍受上限约束
for (let round = 0; round < 5; round++) {
const rows = await engine.find('users', { table: 'users', where: { age: 25 } });
expect(rows.length).toBe(10);
expect(lsm.getCacheSize()).toBeLessThanOrEqual(lsm.getCacheLimit());
}
await engine.close();
});
test('缓存驱逐后全表扫描仍返回完整数据(prefetch 兜底)', async () => {
const engine = createSmallCacheEngine(1); // 4KB 上限,必然触发驱逐
await engine.open('cache-evict-fullscan', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
const rows = makeRows(300);
await engine.insert('users', rows);
const all = await engine.find('users', { table: 'users' });
expect(all.length).toBe(300);
const lsm = (engine as any).lsm;
expect(lsm.getCacheSize()).toBeLessThanOrEqual(lsm.getCacheLimit());
await engine.close();
});
test('缓存驱逐后 PK 等值查询仍正确(prefetchKeys 兜底)', async () => {
const engine = createSmallCacheEngine(1);
await engine.open('cache-evict-pk', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
const rows = makeRows(300);
await engine.insert('users', rows);
// 分散查询多个 PK,每次都会经历 驱逐+重新加载
for (let i = 0; i < 300; i += 11) {
const found = await engine.find('users', { table: 'users', where: { id: `u${i}` } });
expect(found.length).toBe(1);
expect(found[0].name).toBe(`User${i}`);
}
await engine.close();
});
test('缓存驱逐后二级索引查询仍正确', async () => {
const engine = createSmallCacheEngine(1);
await engine.open('cache-evict-idx', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number', index: true },
}));
await engine.insert('users', makeRows(300));
// 索引等值 + 范围查询
const eq = await engine.find('users', { table: 'users', where: { age: 25 } });
expect(eq.length).toBe(10);
// age 范围 20-49,每个值 10 行
const range = await engine.find('users', { table: 'users', where: { age: { $gte: 40 } } });
expect(range.length).toBe(100);
const range2 = await engine.find('users', { table: 'users', where: { age: { $gt: 45 } } });
expect(range2.length).toBe(40);
const inQuery = await engine.find('users', { table: 'users', where: { age: { $in: [21, 22] } } });
expect(inQuery.length).toBe(20);
await engine.close();
});
test('UPDATE/DELETE 在缓存驱逐后仍作用于全部行', async () => {
const engine = createSmallCacheEngine(1);
await engine.open('cache-evict-mutate', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
}));
await engine.insert('users', makeRows(300));
// 无条件更新 → 全表更新
const updated = await engine.update('users', { table: 'users' }, { name: 'Renamed' });
expect(updated).toBe(300);
// age 20-49 每个值 10 行;$lt 25 → age 20-24 → 50 行
const deleted = await engine.delete('users', { table: 'users', where: { age: { $lt: 25 } } });
expect(deleted).toBe(50);
const remaining = await engine.find('users', { table: 'users' });
expect(remaining.length).toBe(250);
expect(remaining.every((r) => r.name === 'Renamed')).toBe(true);
await engine.close();
});
test('写入路径不突破缓存上限(flush 后立即裁剪)', async () => {
const engine = createSmallCacheEngine(2);
await engine.open('cache-write-bound', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
// 分批写入,每批都触发多次 flush
for (let batch = 0; batch < 10; batch++) {
await engine.insert('users', makeRows(30).map((r, i) => ({ ...r, id: `b${batch}_u${i}` })));
const lsm = (engine as any).lsm;
expect(lsm.getCacheSize()).toBeLessThanOrEqual(lsm.getCacheLimit());
}
const all = await engine.find('users', { table: 'users' });
expect(all.length).toBe(300);
await engine.close();
});
test('回归:主 LSM 与二级索引 LSM 的 SSTable 不互相覆盖(命名空间隔离)', async () => {
const engine = createSmallCacheEngine(4);
await engine.open('regression-ns', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', index: true },
age: { type: 'number', index: true },
}));
// 小阈值下 insert/update 会同时触发主 LSM 与两个索引 LSM 的多次 flush
await engine.insert('users', makeRows(120));
await engine.update('users', { table: 'users', where: { age: { $gte: 30 } } }, { name: 'Senior' });
// 主数据完整且为最新值(age 20-49 每个值出现 4 次;$gte 30 → 20 个值 × 4 = 80 行)
const all = await engine.find('users', { table: 'users' });
expect(all.length).toBe(120);
expect(all.filter((r) => r.name === 'Senior').length).toBe(80);
// 二级索引等值查找仍正确(索引 LSM 数据未被覆盖)
const byName = await engine.find('users', { table: 'users', where: { name: 'Senior' } });
expect(byName.length).toBe(80);
const byAge = await engine.find('users', { table: 'users', where: { age: 25 } });
expect(byAge.length).toBe(4);
await engine.close();
});
test('回归:同 key 跨多次 flush 更新后读到最新值(多版本语义)', async () => {
const engine = createSmallCacheEngine(4);
await engine.open('regression-versions', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
value: { type: 'number' },
}));
await engine.insert('users', [{ id: 'a', value: 1 }]);
// 连续更新同一行 20 次,每次更新都经历 flush
for (let v = 2; v <= 20; v++) {
await engine.update('users', { table: 'users', where: { id: 'a' } }, { value: v });
}
const rows = await engine.find('users', { table: 'users', where: { id: 'a' } });
expect(rows.length).toBe(1);
expect(rows[0].value).toBe(20);
// 全表扫描也应返回最新值
const all = await engine.find('users', { table: 'users' });
expect(all.length).toBe(1);
expect(all[0].value).toBe(20);
await engine.close();
});
test('回归:删除后 tombstone 跨 flush 仍生效(不残留旧数据)', async () => {
const engine = createSmallCacheEngine(4);
await engine.open('regression-tombstone', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
age: { type: 'number', index: true },
}));
await engine.insert('users', makeRows(120));
// 分批删除,触发多次 flush
for (let batch = 0; batch < 4; batch++) {
const deleted = await engine.delete('users', { table: 'users', where: { age: { $gte: 20 + batch * 5, $lt: 25 + batch * 5 } } });
expect(deleted).toBe(20);
}
const remaining = await engine.find('users', { table: 'users' });
expect(remaining.length).toBe(40);
// 索引查找也不应返回已删除行
const ghost = await engine.find('users', { table: 'users', where: { age: 22 } });
expect(ghost.length).toBe(0);
await engine.close();
});
});
+218 -127
View File
@@ -1,127 +1,218 @@
/**
* AriaEngine LZ4 + LSM Merge Iterator
* LZ4 compression:false
*/
import { compressLZ4, decompressLZ4 } from '../../src/engine/aria/compression/lz4';
import { MergeIterator, ArrayEntrySource } from '../../src/engine/aria/index/merge_iterator';
// ===================================================================
// LZ4 压缩 — 安全烟雾测试(不卡死)
// ===================================================================
describe('AriaEngine — LZ4 Compression', () => {
it('短于 4 字节时原样返回', () => {
const input = new Uint8Array([1, 2]);
const compressed = compressLZ4(input);
expect(compressed).toBe(input);
});
it('简单文本压缩不抛出异常且产生输出', () => {
const input = new TextEncoder().encode('hello world hello world hello world');
const compressed = compressLZ4(input);
expect(compressed).toBeInstanceOf(Uint8Array);
expect(compressed.byteLength).toBeGreaterThan(0);
});
it('重复数据有压缩效果', () => {
const pattern = 'ABCD';
const repeated = pattern.repeat(100);
const input = new TextEncoder().encode(repeated);
const compressed = compressLZ4(input);
expect(compressed.byteLength).toBeLessThan(input.byteLength);
});
it('随机不可压缩数据不卡死', () => {
const input = new Uint8Array(256);
for (let i = 0; i < 256; i++) input[i] = Math.floor(Math.random() * 256);
const compressed = compressLZ4(input);
expect(compressed).toBeInstanceOf(Uint8Array);
expect(compressed.byteLength).toBeGreaterThan(0);
});
it('长文本压缩不卡死', () => {
const input = new TextEncoder().encode('The quick brown fox jumps over the lazy dog. '.repeat(10));
const compressed = compressLZ4(input);
expect(compressed).toBeTruthy();
expect(compressed.byteLength).toBeGreaterThan(0);
});
it('多种长度输入均不卡死', () => {
for (const size of [10, 50, 100, 200, 500]) {
const input = new Uint8Array(size);
for (let i = 0; i < size; i++) input[i] = i % 256;
const compressed = compressLZ4(input);
expect(compressed.byteLength).toBeLessThanOrEqual(input.byteLength + 16);
}
});
it('解压不抛出异常', () => {
const input = new TextEncoder().encode('test data for decompression smoke test');
const compressed = compressLZ4(input);
expect(() => decompressLZ4(compressed, input.byteLength)).not.toThrow();
});
});
// ===================================================================
// MergeIterator — 归并迭代器单元测试
// ===================================================================
describe('AriaEngine — MergeIterator', () => {
it('单数据源归并', () => {
const mi = new MergeIterator();
mi.addSource(new ArrayEntrySource([
['a', { v: 1 }],
['b', { v: 2 }],
['c', { v: 3 }],
]));
const result = mi.drain();
expect(result).toHaveLength(3);
expect(result.map(([k]) => k)).toEqual(['a', 'b', 'c']);
});
it('多数据源归并去重(保留最新)', () => {
const mi = new MergeIterator();
mi.addSource(new ArrayEntrySource([['a', { v: 'new' }], ['c', { v: 3 }]]));
mi.addSource(new ArrayEntrySource([['a', { v: 'old' }], ['b', { v: 2 }]]));
const result = mi.drain();
expect(result).toHaveLength(3);
expect(result[0][0]).toBe('a');
expect(result[0][1].v).toBe('new');
expect(result[1][0]).toBe('b');
expect(result[2][0]).toBe('c');
});
it('空数据源归并', () => {
const mi = new MergeIterator();
mi.addSource(new ArrayEntrySource([]));
const result = mi.drain();
expect(result).toHaveLength(0);
});
it('大数量归并', () => {
const mi = new MergeIterator();
for (let s = 0; s < 5; s++) {
const entries: [string, Record<string, unknown>][] = [];
for (let i = 0; i < 100; i++) {
entries.push([`src${s}-key-${String(i).padStart(3, '0')}`, { src: s, idx: i }]);
}
mi.addSource(new ArrayEntrySource(entries));
}
const result = mi.drain();
// 5 sources × 100 unique keys = 500 total (keys are unique per source)
expect(result).toHaveLength(500);
});
it('ArrayEntrySource — 迭代器用完返回 null', () => {
const src = new ArrayEntrySource([['k', { v: 1 }]]);
expect(src.next()).not.toBeNull();
expect(src.next()).toBeNull();
expect(src.next()).toBeNull();
});
it('ArrayEntrySource — reset 重置', () => {
const src = new ArrayEntrySource([['k1', { v: 1 }], ['k2', { v: 2 }]]);
src.next();
src.reset();
const val = src.next();
expect(val![0]).toBe('k1');
});
});
/**
* AriaEngine LZ4 + LSM Merge Iterator
* LZ4 compression:false
*/
import { compressLZ4, decompressLZ4 } from '../../src/engine/aria/compression/lz4';
import { MergeIterator, ArrayEntrySource } from '../../src/engine/aria/index/merge_iterator';
// ===================================================================
// LZ4 压缩 — 安全烟雾测试(不卡死)
// ===================================================================
describe('AriaEngine — LZ4 Compression', () => {
it('短于 4 字节时压缩为纯字面量 token 且往返一致', () => {
const input = new Uint8Array([1, 2]);
const compressed = compressLZ4(input);
// token(lo=0) + 2 字节字面量
expect(compressed.byteLength).toBe(3);
expect(compressed[0]).toBe(0x20); // litLen=2, matchField=0
const restored = decompressLZ4(compressed, 2);
expect(Array.from(restored)).toEqual([1, 2]);
});
it('简单文本压缩不抛出异常且产生输出', () => {
const input = new TextEncoder().encode('hello world hello world hello world');
const compressed = compressLZ4(input);
expect(compressed).toBeInstanceOf(Uint8Array);
expect(compressed.byteLength).toBeGreaterThan(0);
});
it('重复数据有压缩效果', () => {
const pattern = 'ABCD';
const repeated = pattern.repeat(100);
const input = new TextEncoder().encode(repeated);
const compressed = compressLZ4(input);
expect(compressed.byteLength).toBeLessThan(input.byteLength);
});
it('随机不可压缩数据不卡死', () => {
const input = new Uint8Array(256);
for (let i = 0; i < 256; i++) input[i] = Math.floor(Math.random() * 256);
const compressed = compressLZ4(input);
expect(compressed).toBeInstanceOf(Uint8Array);
expect(compressed.byteLength).toBeGreaterThan(0);
});
it('长文本压缩不卡死', () => {
const input = new TextEncoder().encode('The quick brown fox jumps over the lazy dog. '.repeat(10));
const compressed = compressLZ4(input);
expect(compressed).toBeTruthy();
expect(compressed.byteLength).toBeGreaterThan(0);
});
it('多种长度输入均不卡死', () => {
for (const size of [10, 50, 100, 200, 500]) {
const input = new Uint8Array(size);
for (let i = 0; i < size; i++) input[i] = i % 256;
const compressed = compressLZ4(input);
expect(compressed.byteLength).toBeLessThanOrEqual(input.byteLength + 16);
}
});
it('解压不抛出异常', () => {
const input = new TextEncoder().encode('test data for decompression smoke test');
const compressed = compressLZ4(input);
expect(() => decompressLZ4(compressed, input.byteLength)).not.toThrow();
});
// ---- v0.2.6 补强:压缩 → 解压 往返一致性 ----
it('往返一致:重复模式数据', () => {
const input = new TextEncoder().encode('ABCD'.repeat(100));
const compressed = compressLZ4(input);
const restored = decompressLZ4(compressed, input.byteLength);
expect(Array.from(restored)).toEqual(Array.from(input));
});
it('往返一致:自然文本数据', () => {
const input = new TextEncoder().encode(
'The quick brown fox jumps over the lazy dog. '.repeat(10),
);
const compressed = compressLZ4(input);
const restored = decompressLZ4(compressed, input.byteLength);
expect(Array.from(restored)).toEqual(Array.from(input));
});
it('往返一致:随机不可压缩数据', () => {
const input = new Uint8Array(512);
for (let i = 0; i < 512; i++) input[i] = Math.floor(Math.random() * 256);
const compressed = compressLZ4(input);
const restored = decompressLZ4(compressed, input.byteLength);
expect(Array.from(restored)).toEqual(Array.from(input));
});
it('往返一致:多种长度与字节模式', () => {
for (const size of [4, 5, 15, 16, 17, 50, 100, 300, 1000]) {
const input = new Uint8Array(size);
for (let i = 0; i < size; i++) input[i] = i % 7 === 0 ? i % 256 : 0x41;
const compressed = compressLZ4(input);
const restored = decompressLZ4(compressed, input.byteLength);
expect(Array.from(restored)).toEqual(Array.from(input));
}
});
it('往返一致:恰好 15 字节字面量边界', () => {
// 字面量长度恰好 15(token 上限)时不应丢字节
const input = new Uint8Array(15);
for (let i = 0; i < 15; i++) input[i] = i;
const compressed = compressLZ4(input);
const restored = decompressLZ4(compressed, input.byteLength);
expect(Array.from(restored)).toEqual(Array.from(input));
});
it('往返一致:超过 15 字节的连续匹配', () => {
const input = new TextEncoder().encode('X'.repeat(200) + 'Y' + 'X'.repeat(60));
const compressed = compressLZ4(input);
const restored = decompressLZ4(compressed, input.byteLength);
expect(Array.from(restored)).toEqual(Array.from(input));
});
});
// ===================================================================
// MergeIterator — 归并迭代器单元测试
// ===================================================================
describe('AriaEngine — MergeIterator', () => {
it('单数据源归并', () => {
const mi = new MergeIterator();
mi.addSource(new ArrayEntrySource([
['a', { v: 1 }],
['b', { v: 2 }],
['c', { v: 3 }],
]));
const result = mi.drain();
expect(result).toHaveLength(3);
expect(result.map(([k]) => k)).toEqual(['a', 'b', 'c']);
});
it('多数据源归并去重(保留最新)', () => {
const mi = new MergeIterator();
mi.addSource(new ArrayEntrySource([['a', { v: 'new' }], ['c', { v: 3 }]]));
mi.addSource(new ArrayEntrySource([['a', { v: 'old' }], ['b', { v: 2 }]]));
const result = mi.drain();
expect(result).toHaveLength(3);
expect(result[0][0]).toBe('a');
expect(result[0][1].v).toBe('new');
expect(result[1][0]).toBe('b');
expect(result[2][0]).toBe('c');
});
// ---- v0.2.6 回归:同 key 多来源时保留 sourceIndex 最小(最新)的条目 ----
it('回归:同 key 出现在多个来源时返回 sourceIndex 最小(最新来源)的值', () => {
const mi = new MergeIterator();
// 语义:sourceIndex 越小越新(memtable=0 < immutable=1 < sstable=2+
mi.addSource(new ArrayEntrySource([['a', { v: 'source0' }]])); // 最新来源
mi.addSource(new ArrayEntrySource([['a', { v: 'source1' }]]));
mi.addSource(new ArrayEntrySource([['a', { v: 'source2' }]])); // 最旧来源
const result = mi.drain();
expect(result).toHaveLength(1);
// 取的是 sourceIndex 最小(最新来源)的条目,而非堆序决定的任意条目
expect(result[0][1].v).toBe('source0');
});
it('回归:最新来源的值位于中间 sourceIndex 时仍取 sourceIndex 最小者', () => {
const mi = new MergeIterator();
mi.addSource(new ArrayEntrySource([['a', { v: 'middle' }]])); // index 0 = 最新来源
mi.addSource(new ArrayEntrySource([['a', { v: 'newest' }]])); // index 1
mi.addSource(new ArrayEntrySource([['a', { v: 'oldest' }]])); // index 2
const result = mi.drain();
expect(result).toHaveLength(1);
expect(result[0][1].v).toBe('middle'); // index 0 的条目胜出
});
it('回归:多个同 key 来源 + 其他独立 key 混合', () => {
const mi = new MergeIterator();
mi.addSource(new ArrayEntrySource([['a', { v: 1 }], ['b', { v: 10 }]]));
mi.addSource(new ArrayEntrySource([['a', { v: 2 }]]));
mi.addSource(new ArrayEntrySource([['a', { v: 3 }], ['c', { v: 30 }]]));
const result = mi.drain();
expect(result.map(([k]) => k)).toEqual(['a', 'b', 'c']);
expect(result[0][1].v).toBe(1); // sourceIndex 0 = 最新
expect(result[1][1].v).toBe(10);
expect(result[2][1].v).toBe(30);
});
it('空数据源归并', () => {
const mi = new MergeIterator();
mi.addSource(new ArrayEntrySource([]));
const result = mi.drain();
expect(result).toHaveLength(0);
});
it('大数量归并', () => {
const mi = new MergeIterator();
for (let s = 0; s < 5; s++) {
const entries: [string, Record<string, unknown>][] = [];
for (let i = 0; i < 100; i++) {
entries.push([`src${s}-key-${String(i).padStart(3, '0')}`, { src: s, idx: i }]);
}
mi.addSource(new ArrayEntrySource(entries));
}
const result = mi.drain();
// 5 sources × 100 unique keys = 500 total (keys are unique per source)
expect(result).toHaveLength(500);
});
it('ArrayEntrySource — 迭代器用完返回 null', () => {
const src = new ArrayEntrySource([['k', { v: 1 }]]);
expect(src.next()).not.toBeNull();
expect(src.next()).toBeNull();
expect(src.next()).toBeNull();
});
it('ArrayEntrySource — reset 重置', () => {
const src = new ArrayEntrySource([['k1', { v: 1 }], ['k2', { v: 2 }]]);
src.next();
src.reset();
const val = src.next();
expect(val![0]).toBe('k1');
});
});
+111
View File
@@ -0,0 +1,111 @@
/**
* AriaEngine CryptoManager
* @module tests/engine/aria-crypto
*
* v0.2.6
*/
import {
CryptoManager,
initCrypto,
encryptPage,
decryptPage,
closeCrypto,
} from '../../src/engine/aria/crypto';
function toBytes(data: ArrayBuffer): number[] {
return Array.from(new Uint8Array(data));
}
describe('AriaEngine — CryptoManager', () => {
test('加解密往返一致', async () => {
const cm = new CryptoManager();
await cm.init('test-password');
expect(cm.enabled).toBe(true);
const original = new TextEncoder().encode('sensitive row data').buffer;
const { iv, data } = await cm.encryptPage(original);
// 密文应为乱码(与原文不同)
expect(toBytes(data)).not.toEqual(toBytes(original));
const decrypted = await cm.decryptPage(iv, data);
expect(toBytes(decrypted)).toEqual(toBytes(original));
cm.close();
expect(cm.enabled).toBe(false);
});
test('错误密码解密失败(密钥不同)', async () => {
const cm1 = new CryptoManager();
await cm1.init('correct-password');
const original = new TextEncoder().encode('top secret').buffer;
const { iv, data } = await cm1.encryptPage(original);
const cm2 = new CryptoManager();
await cm2.init('wrong-password');
await expect(cm2.decryptPage(iv, data)).rejects.toThrow();
cm1.close();
cm2.close();
});
test('不同 salt 派生不同密钥,解密互相失败', async () => {
const cm1 = new CryptoManager();
await cm1.init('pwd', new Uint8Array(16).fill(1));
const cm2 = new CryptoManager();
await cm2.init('pwd', new Uint8Array(16).fill(2));
const original = new TextEncoder().encode('salt matters').buffer;
const { iv, data } = await cm1.encryptPage(original);
await expect(cm2.decryptPage(iv, data)).rejects.toThrow();
cm1.close();
cm2.close();
});
test('未初始化时加密抛错', async () => {
const cm = new CryptoManager();
expect(cm.enabled).toBe(false);
const data = new TextEncoder().encode('x').buffer;
await expect(cm.encryptPage(data)).rejects.toThrow(/not initialized/);
});
test('不同实例互不影响(独立密钥状态)', async () => {
const cm1 = new CryptoManager();
await cm1.init('pwd-a');
const cm2 = new CryptoManager();
await cm2.init('pwd-b');
const original = new TextEncoder().encode('instance isolation').buffer;
const { iv, data } = await cm1.encryptPage(original);
await expect(cm2.decryptPage(iv, data)).rejects.toThrow();
// 各自解密自己的数据
const dec2orig = new TextEncoder().encode('two').buffer;
const enc2 = await cm2.encryptPage(dec2orig);
const dec2 = await cm2.decryptPage(enc2.iv, enc2.data);
expect(toBytes(dec2)).toEqual(toBytes(dec2orig));
cm1.close();
cm2.close();
});
test('全局兼容层往返一致', async () => {
await initCrypto('global-password');
const original = new TextEncoder().encode('global compat layer').buffer;
const { iv, data } = await encryptPage(original);
const decrypted = await decryptPage(iv, data);
expect(toBytes(decrypted)).toEqual(toBytes(original));
closeCrypto();
});
test('大块数据(接近页面大小)往返一致', async () => {
const cm = new CryptoManager();
await cm.init('page-size-test');
// 4KB 页面数据
const original = new Uint8Array(4096);
for (let i = 0; i < 4096; i++) original[i] = i % 251;
const { iv, data } = await cm.encryptPage(original.buffer);
const decrypted = await cm.decryptPage(iv, data);
expect(toBytes(decrypted)).toEqual(toBytes(original.buffer));
cm.close();
});
});
+216 -216
View File
@@ -1,216 +1,216 @@
/**
* AriaEngine Bloom Filter + MemTable
*/
import { BloomFilter } from '../../src/engine/aria/index/bloom';
import { MemTable } from '../../src/engine/aria/index/memtable';
// ===================================================================
// BloomFilter
// ===================================================================
describe('AriaEngine — BloomFilter', () => {
it('插入后 mayContain 返回 true', () => {
const bf = new BloomFilter(100);
bf.insert('hello');
expect(bf.mayContain('hello')).toBe(true);
});
it('未插入的 key mayContain 返回 false', () => {
const bf = new BloomFilter(100);
bf.insert('hello');
expect(bf.mayContain('world')).toBe(false);
});
it('批量插入后所有 key 都判定存在', () => {
const bf = new BloomFilter(500);
const keys: string[] = [];
for (let i = 0; i < 200; i++) {
const k = `key-${i}`;
keys.push(k);
bf.insert(k);
}
for (const k of keys) {
expect(bf.mayContain(k)).toBe(true);
}
});
it('False positive 率可控', () => {
const n = 500;
const bf = new BloomFilter(n, 10);
for (let i = 0; i < n; i++) {
bf.insert(`present-${i}`);
}
let fp = 0;
for (let i = 0; i < 500; i++) {
if (bf.mayContain(`absent-${i}`)) fp++;
}
expect(fp).toBeLessThan(25);
});
it('getBitSize 返回正确位数', () => {
const bf = new BloomFilter(100, 10);
expect(bf.getBitSize()).toBeGreaterThanOrEqual(64);
});
it('getInsertedCount 追踪插入数', () => {
const bf = new BloomFilter(100);
bf.insert('a');
bf.insert('b');
bf.insert('c');
expect(bf.getInsertedCount()).toBe(3);
});
it('getHashCount 返回哈希函数数量', () => {
const bf = new BloomFilter(1000, 10);
expect(bf.getHashCount()).toBeGreaterThan(0);
});
it('serialize + fromData 往返', () => {
const bf1 = new BloomFilter(100);
bf1.insert('a');
bf1.insert('b');
const data = bf1.serialize();
const bf2 = BloomFilter.fromData(data, bf1.getHashCount());
expect(bf2.mayContain('a')).toBe(true);
expect(bf2.mayContain('b')).toBe(true);
expect(bf2.mayContain('c')).toBe(false);
});
it('空过滤器 mayContain 返回 false', () => {
const bf = new BloomFilter(100);
expect(bf.mayContain('anything')).toBe(false);
});
it('最少 1 个哈希函数', () => {
const bf = new BloomFilter(10, 1);
expect(bf.getHashCount()).toBeGreaterThanOrEqual(1);
});
});
// ===================================================================
// MemTable
// ===================================================================
describe('AriaEngine — MemTable', () => {
let mt: MemTable;
beforeEach(() => {
mt = new MemTable(4 * 1024 * 1024);
});
it('put + get 往返', () => {
mt.put('key1', { name: 'Alice', age: 30 });
const val = mt.get('key1');
expect(val).not.toBeNull();
expect(val!.name).toBe('Alice');
});
it('get — 不存在的 key 返回 null', () => {
expect(mt.get('nonexistent')).toBeNull();
});
it('put 更新已存在的 key', () => {
mt.put('k', { v: 1 });
mt.put('k', { v: 2 });
expect(mt.get('k')!.v).toBe(2);
});
it('delete 删除成功', () => {
mt.put('k', { v: 1 });
expect(mt.delete('k')).toBe(true);
expect(mt.get('k')).toBeNull();
});
it('delete — 不存在的 key 返回 false', () => {
expect(mt.delete('ghost')).toBe(false);
});
it('getAllEntries 返回所有条目(有序)', () => {
mt.put('c', { v: 3 });
mt.put('a', { v: 1 });
mt.put('b', { v: 2 });
const entries = mt.getAllEntries();
expect(entries).toHaveLength(3);
expect(entries[0][0]).toBe('a');
expect(entries[1][0]).toBe('b');
expect(entries[2][0]).toBe('c');
});
it('rangeScan — 范围查询', () => {
mt.put('a', { v: 1 });
mt.put('b', { v: 2 });
mt.put('c', { v: 3 });
mt.put('d', { v: 4 });
const results = mt.rangeScan('b', 'c');
expect(results).toHaveLength(2);
expect(results[0][0]).toBe('b');
expect(results[1][0]).toBe('c');
});
it('rangeScan — 空结果', () => {
mt.put('a', { v: 1 });
const results = mt.rangeScan('z', 'zz');
expect(results).toHaveLength(0);
});
it('getEntryCount 正确计数', () => {
expect(mt.getEntryCount()).toBe(0);
mt.put('a', { v: 1 });
mt.put('b', { v: 2 });
expect(mt.getEntryCount()).toBe(2);
mt.delete('a');
expect(mt.getEntryCount()).toBe(1);
});
it('contains 检查存在性', () => {
mt.put('x', { v: 1 });
expect(mt.contains('x')).toBe(true);
expect(mt.contains('y')).toBe(false);
});
it('shouldFlush — 未达阈值返回 false', () => {
expect(mt.shouldFlush()).toBe(false);
});
it('clear 清空所有数据', () => {
mt.put('a', { v: 1 });
mt.put('b', { v: 2 });
mt.clear();
expect(mt.getEntryCount()).toBe(0);
expect(mt.get('a')).toBeNull();
});
it('getEstimatedSize 返回合理估计值', () => {
expect(mt.getEstimatedSize()).toBe(0);
mt.put('hello', { name: 'world', count: 42 });
expect(mt.getEstimatedSize()).toBeGreaterThan(0);
});
it('大量数据插入保持有序', () => {
const count = 100;
for (let i = count - 1; i >= 0; i--) {
mt.put(`key-${String(i).padStart(3, '0')}`, { idx: i });
}
const entries = mt.getAllEntries();
expect(entries).toHaveLength(count);
for (let i = 0; i < count; i++) {
expect(entries[i][1].idx).toBe(i);
}
});
it('删除后重新插入', () => {
mt.put('k', { v: 1 });
mt.delete('k');
mt.put('k', { v: 2 });
expect(mt.get('k')!.v).toBe(2);
});
it('范围扫描包含边界', () => {
mt.put('aa', { v: 1 });
mt.put('ab', { v: 2 });
mt.put('ac', { v: 3 });
const results = mt.rangeScan('aa', 'ab');
expect(results).toHaveLength(2);
expect(results[0][0]).toBe('aa');
expect(results[1][0]).toBe('ab');
});
});
/**
* AriaEngine Bloom Filter + MemTable
*/
import { BloomFilter } from '../../src/engine/aria/index/bloom';
import { MemTable } from '../../src/engine/aria/index/memtable';
// ===================================================================
// BloomFilter
// ===================================================================
describe('AriaEngine — BloomFilter', () => {
it('插入后 mayContain 返回 true', () => {
const bf = new BloomFilter(100);
bf.insert('hello');
expect(bf.mayContain('hello')).toBe(true);
});
it('未插入的 key mayContain 返回 false', () => {
const bf = new BloomFilter(100);
bf.insert('hello');
expect(bf.mayContain('world')).toBe(false);
});
it('批量插入后所有 key 都判定存在', () => {
const bf = new BloomFilter(500);
const keys: string[] = [];
for (let i = 0; i < 200; i++) {
const k = `key-${i}`;
keys.push(k);
bf.insert(k);
}
for (const k of keys) {
expect(bf.mayContain(k)).toBe(true);
}
});
it('False positive 率可控', () => {
const n = 500;
const bf = new BloomFilter(n, 10);
for (let i = 0; i < n; i++) {
bf.insert(`present-${i}`);
}
let fp = 0;
for (let i = 0; i < 500; i++) {
if (bf.mayContain(`absent-${i}`)) fp++;
}
expect(fp).toBeLessThan(25);
});
it('getBitSize 返回正确位数', () => {
const bf = new BloomFilter(100, 10);
expect(bf.getBitSize()).toBeGreaterThanOrEqual(64);
});
it('getInsertedCount 追踪插入数', () => {
const bf = new BloomFilter(100);
bf.insert('a');
bf.insert('b');
bf.insert('c');
expect(bf.getInsertedCount()).toBe(3);
});
it('getHashCount 返回哈希函数数量', () => {
const bf = new BloomFilter(1000, 10);
expect(bf.getHashCount()).toBeGreaterThan(0);
});
it('serialize + fromData 往返', () => {
const bf1 = new BloomFilter(100);
bf1.insert('a');
bf1.insert('b');
const data = bf1.serialize();
const bf2 = BloomFilter.fromData(data, bf1.getHashCount());
expect(bf2.mayContain('a')).toBe(true);
expect(bf2.mayContain('b')).toBe(true);
expect(bf2.mayContain('c')).toBe(false);
});
it('空过滤器 mayContain 返回 false', () => {
const bf = new BloomFilter(100);
expect(bf.mayContain('anything')).toBe(false);
});
it('最少 1 个哈希函数', () => {
const bf = new BloomFilter(10, 1);
expect(bf.getHashCount()).toBeGreaterThanOrEqual(1);
});
});
// ===================================================================
// MemTable
// ===================================================================
describe('AriaEngine — MemTable', () => {
let mt: MemTable;
beforeEach(() => {
mt = new MemTable(4 * 1024 * 1024);
});
it('put + get 往返', () => {
mt.put('key1', { name: 'Alice', age: 30 });
const val = mt.get('key1');
expect(val).not.toBeNull();
expect(val!.name).toBe('Alice');
});
it('get — 不存在的 key 返回 null', () => {
expect(mt.get('nonexistent')).toBeNull();
});
it('put 更新已存在的 key', () => {
mt.put('k', { v: 1 });
mt.put('k', { v: 2 });
expect(mt.get('k')!.v).toBe(2);
});
it('delete 删除成功', () => {
mt.put('k', { v: 1 });
expect(mt.delete('k')).toBe(true);
expect(mt.get('k')).toBeNull();
});
it('delete — 不存在的 key 返回 false', () => {
expect(mt.delete('ghost')).toBe(false);
});
it('getAllEntries 返回所有条目(有序)', () => {
mt.put('c', { v: 3 });
mt.put('a', { v: 1 });
mt.put('b', { v: 2 });
const entries = mt.getAllEntries();
expect(entries).toHaveLength(3);
expect(entries[0][0]).toBe('a');
expect(entries[1][0]).toBe('b');
expect(entries[2][0]).toBe('c');
});
it('rangeScan — 范围查询', () => {
mt.put('a', { v: 1 });
mt.put('b', { v: 2 });
mt.put('c', { v: 3 });
mt.put('d', { v: 4 });
const results = mt.rangeScan('b', 'c');
expect(results).toHaveLength(2);
expect(results[0][0]).toBe('b');
expect(results[1][0]).toBe('c');
});
it('rangeScan — 空结果', () => {
mt.put('a', { v: 1 });
const results = mt.rangeScan('z', 'zz');
expect(results).toHaveLength(0);
});
it('getEntryCount 正确计数', () => {
expect(mt.getEntryCount()).toBe(0);
mt.put('a', { v: 1 });
mt.put('b', { v: 2 });
expect(mt.getEntryCount()).toBe(2);
mt.delete('a');
expect(mt.getEntryCount()).toBe(1);
});
it('contains 检查存在性', () => {
mt.put('x', { v: 1 });
expect(mt.contains('x')).toBe(true);
expect(mt.contains('y')).toBe(false);
});
it('shouldFlush — 未达阈值返回 false', () => {
expect(mt.shouldFlush()).toBe(false);
});
it('clear 清空所有数据', () => {
mt.put('a', { v: 1 });
mt.put('b', { v: 2 });
mt.clear();
expect(mt.getEntryCount()).toBe(0);
expect(mt.get('a')).toBeNull();
});
it('getEstimatedSize 返回合理估计值', () => {
expect(mt.getEstimatedSize()).toBe(0);
mt.put('hello', { name: 'world', count: 42 });
expect(mt.getEstimatedSize()).toBeGreaterThan(0);
});
it('大量数据插入保持有序', () => {
const count = 100;
for (let i = count - 1; i >= 0; i--) {
mt.put(`key-${String(i).padStart(3, '0')}`, { idx: i });
}
const entries = mt.getAllEntries();
expect(entries).toHaveLength(count);
for (let i = 0; i < count; i++) {
expect(entries[i][1].idx).toBe(i);
}
});
it('删除后重新插入', () => {
mt.put('k', { v: 1 });
mt.delete('k');
mt.put('k', { v: 2 });
expect(mt.get('k')!.v).toBe(2);
});
it('范围扫描包含边界', () => {
mt.put('aa', { v: 1 });
mt.put('ab', { v: 2 });
mt.put('ac', { v: 3 });
const results = mt.rangeScan('aa', 'ab');
expect(results).toHaveLength(2);
expect(results[0][0]).toBe('aa');
expect(results[1][0]).toBe('ab');
});
});
+337 -337
View File
@@ -1,337 +1,337 @@
/**
* AriaEngine Page
* 覆盖: PageHeader / Slot / Tuple + PageFormat
*/
import {
PAGE_SIZE, PageType, PAGE_HEADER_SIZE, SLOT_ENTRY_SIZE,
} from '../../src/engine/aria/types';
import {
encodePageHeader, decodePageHeader, initPageHeader, getPageType,
getSlotCount, getFreeStart, setFreeStart, setFreeEnd,
} from '../../src/engine/aria/page/header';
import {
getSlotEntry, setSlotEntry, getSlotDirectorySize,
getFreeSpace, hasEnoughSpace, allocateSlot, readSlotData, freeSlot,
} from '../../src/engine/aria/page/slot';
import {
encodeTuple, decodeTuple, getColumnEncodingMap,
} from '../../src/engine/aria/page/tuple';
import { ColumnEncoding } from '../../src/engine/aria/types';
import {
createPage, pageFromBuffer, pageInsertRow, pageReadRow,
pageDeleteRow, pageUpdateRow, computeChecksum, verifyChecksum, updateChecksum,
} from '../../src/engine/aria/page/format';
// ===================================================================
// PageHeader
// ===================================================================
describe('AriaEngine Page — Header', () => {
let buf: ArrayBuffer;
beforeEach(() => {
buf = new ArrayBuffer(PAGE_SIZE);
});
it('initPageHeader 初始化头部字段', () => {
initPageHeader(buf, 42, PageType.DATA);
const h = decodePageHeader(buf);
expect(h.pageId).toBe(42);
expect(h.type).toBe(PageType.DATA);
expect(h.slotCount).toBe(0);
expect(h.freeStart).toBe(PAGE_HEADER_SIZE);
expect(h.freeEnd).toBe(PAGE_SIZE);
});
it('initPageHeader — INDEX 类型页面', () => {
initPageHeader(buf, 99, PageType.INDEX);
expect(getPageType(buf)).toBe(PageType.INDEX);
});
it('encodePageHeader + decodePageHeader 往返一致', () => {
const header = { pageId: 7, type: PageType.META, freeStart: 32, freeEnd: 4000, slotCount: 5, checksum: 0xdeadbeef };
encodePageHeader(header, buf);
const decoded = decodePageHeader(buf);
expect(decoded.pageId).toBe(7);
expect(decoded.type).toBe(PageType.META);
expect(decoded.freeStart).toBe(32);
expect(decoded.freeEnd).toBe(4000);
expect(decoded.slotCount).toBe(5);
});
it('不同 pageId 正确编解码', () => {
for (const id of [0, 1, 255, 65535, 0xffffffff]) {
initPageHeader(buf, id, PageType.DATA);
expect(decodePageHeader(buf).pageId).toBe(id >>> 0);
}
});
it('setFreeStart / setFreeEnd 修改字段', () => {
initPageHeader(buf, 1, PageType.DATA);
setFreeStart(buf, 100);
setFreeEnd(buf, 3000);
expect(getFreeStart(buf)).toBe(100);
expect(decodePageHeader(buf).freeEnd).toBe(3000);
});
});
// ===================================================================
// Slot Directory
// ===================================================================
describe('AriaEngine Page — Slot', () => {
let buf: ArrayBuffer;
beforeEach(() => {
buf = new ArrayBuffer(PAGE_SIZE);
initPageHeader(buf, 1, PageType.DATA);
});
it('getSlotEntry — 空页面 slotCount 为 0', () => {
expect(getSlotCount(buf)).toBe(0);
});
it('setSlotEntry + getSlotEntry 往返', () => {
// 手动写一个 slot(不通过 allocateSlot
new DataView(buf).setUint16(9, 1, false); // slotCount = 1
setSlotEntry(buf, 0, { offset: 1000, length: 50 });
const entry = getSlotEntry(buf, 0);
expect(entry.offset).toBe(1000);
expect(entry.length).toBe(50);
});
it('getSlotDirectorySize 计算正确', () => {
expect(getSlotDirectorySize(0)).toBe(0);
expect(getSlotDirectorySize(1)).toBe(SLOT_ENTRY_SIZE);
expect(getSlotDirectorySize(10)).toBe(10 * SLOT_ENTRY_SIZE);
});
it('getFreeSpace — 空页面有最大空闲空间', () => {
const free = getFreeSpace(buf);
expect(free).toBe(PAGE_SIZE - PAGE_HEADER_SIZE);
});
it('hasEnoughSpace — 小数据返回 true', () => {
expect(hasEnoughSpace(buf, 100)).toBe(true);
});
it('hasEnoughSpace — 超大数据返回 false', () => {
expect(hasEnoughSpace(buf, PAGE_SIZE * 2)).toBe(false);
});
it('allocateSlot 分配并写入数据', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const idx = allocateSlot(buf, data);
expect(idx).toBe(0);
expect(getSlotCount(buf)).toBe(1);
const readBack = readSlotData(buf, 0);
expect(readBack).not.toBeNull();
expect(Array.from(readBack!)).toEqual([1, 2, 3, 4, 5]);
});
it('allocateSlot 多次分配', () => {
for (let i = 0; i < 10; i++) {
const data = new Uint8Array([i, i + 1]);
const idx = allocateSlot(buf, data);
expect(idx).toBe(i);
}
expect(getSlotCount(buf)).toBe(10);
// 验证每个 slot 数据正确
for (let i = 0; i < 10; i++) {
const data = readSlotData(buf, i);
expect(Array.from(data!)).toEqual([i, i + 1]);
}
});
it('freeSlot 标记为删除', () => {
const data = new Uint8Array([10, 20, 30]);
allocateSlot(buf, data);
freeSlot(buf, 0);
const entry = getSlotEntry(buf, 0);
expect(entry.offset).toBe(0);
expect(entry.length).toBe(0);
});
it('readSlotData — 已删除 slot 返回 null', () => {
allocateSlot(buf, new Uint8Array([1]));
freeSlot(buf, 0);
expect(readSlotData(buf, 0)).toBeNull();
});
it('allocateSlot — 空间不足返回 -1', () => {
// 填满页面
const big = new Uint8Array(PAGE_SIZE - PAGE_HEADER_SIZE - SLOT_ENTRY_SIZE);
const idx1 = allocateSlot(buf, big);
expect(idx1).toBe(0);
const idx2 = allocateSlot(buf, new Uint8Array([1]));
expect(idx2).toBe(-1);
});
});
// ===================================================================
// Tuple Codec
// ===================================================================
describe('AriaEngine Page — Tuple', () => {
const colOrder = ['id', 'name', 'age', 'active', 'data'];
const colTypes: Record<string, string> = {
id: 'string', name: 'string', age: 'number', active: 'boolean', data: 'json',
};
it('encodeTuple + decodeTuple 完整往返', () => {
const row = { id: '1', name: 'Alice', age: 30, active: true, data: { x: 1 } };
const encoded = encodeTuple(row, colOrder, colTypes);
expect(encoded.byteLength).toBeGreaterThan(0);
const decoded = decodeTuple(encoded, colOrder, colTypes);
expect(decoded).not.toBeNull();
expect(decoded!.id).toBe('1');
expect(decoded!.name).toBe('Alice');
expect(decoded!.age).toBe(30);
expect(decoded!.active).toBe(true);
expect(decoded!.data).toEqual({ x: 1 });
});
it('encodeTuple — null 值正确处理', () => {
const row = { id: '2', name: null, age: 25, active: null, data: null };
const encoded = encodeTuple(row, colOrder, colTypes);
const decoded = decodeTuple(encoded, colOrder, colTypes);
expect(decoded!.name).toBeNull();
expect(decoded!.active).toBeNull();
expect(decoded!.data).toBeNull();
});
it('encodeTuple — undefined 值按 null 处理', () => {
const row = { id: '3', age: 30 } as any;
const encoded = encodeTuple(row, colOrder, colTypes);
const decoded = decodeTuple(encoded, colOrder, colTypes);
expect(decoded!.id).toBe('3');
expect(decoded!.name).toBeNull();
});
it('encodeTuple — date 类型', () => {
const order = ['ts'];
const types = { ts: 'date' };
const row = { ts: '2024-01-15T00:00:00.000Z' };
const encoded = encodeTuple(row, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.ts).toBe('2024-01-15T00:00:00.000Z');
});
it('encodeTuple — boolean false', () => {
const order = ['flag'];
const types = { flag: 'boolean' };
const encoded = encodeTuple({ flag: false }, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.flag).toBe(false);
});
it('encodeTuple — 负数', () => {
const order = ['val'];
const types = { val: 'number' };
const encoded = encodeTuple({ val: -42.5 }, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.val).toBe(-42.5);
});
it('encodeTuple — 空字符串', () => {
const order = ['s'];
const types = { s: 'string' };
const encoded = encodeTuple({ s: '' }, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.s).toBe('');
});
it('encodeTuple — 长字符串', () => {
const order = ['s'];
const types = { s: 'string' };
const long = 'x'.repeat(10000);
const encoded = encodeTuple({ s: long }, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.s).toBe(long);
});
it('getColumnEncodingMap 返回正确映射', () => {
const map = getColumnEncodingMap(colOrder, colTypes);
expect(map.get('id')).toBe(ColumnEncoding.STRING);
expect(map.get('age')).toBe(ColumnEncoding.NUMBER);
expect(map.get('active')).toBe(ColumnEncoding.BOOLEAN);
expect(map.get('data')).toBe(ColumnEncoding.JSON);
});
it('decodeTuple — 损坏数据返回 null', () => {
const broken = new Uint8Array([0xff, 0xff, 0xff]);
expect(decodeTuple(broken, colOrder, colTypes)).toBeNull();
});
});
// ===================================================================
// Page Format 整合
// ===================================================================
describe('AriaEngine Page — Format', () => {
const colOrder = ['id', 'name'];
const colTypes: Record<string, string> = { id: 'string', name: 'string' };
it('createPage 创建合法页面', () => {
const page = createPage(100, PageType.DATA);
expect(page.pageId).toBe(100);
expect(page.type).toBe(PageType.DATA);
expect(page.pins).toBe(0);
expect(page.dirty).toBe(true);
});
it('pageInsertRow + pageReadRow 往返', () => {
const page = createPage(1, PageType.DATA);
const row = { id: 'u1', name: 'Test' };
const idx = pageInsertRow(page, row, colOrder, colTypes);
expect(idx).toBe(0);
const read = pageReadRow(page, 0, colOrder, colTypes);
expect(read).not.toBeNull();
expect(read!.id).toBe('u1');
expect(read!.name).toBe('Test');
});
it('pageInsertRow — 多次插入', () => {
const page = createPage(1, PageType.DATA);
for (let i = 0; i < 50; i++) {
const idx = pageInsertRow(page, { id: `${i}`, name: `User${i}` }, colOrder, colTypes);
expect(idx).toBe(i);
}
for (let i = 0; i < 50; i++) {
const row = pageReadRow(page, i, colOrder, colTypes);
expect(row!.id).toBe(`${i}`);
}
});
it('pageDeleteRow 标记删除', () => {
const page = createPage(1, PageType.DATA);
pageInsertRow(page, { id: '1', name: 'A' }, colOrder, colTypes);
pageInsertRow(page, { id: '2', name: 'B' }, colOrder, colTypes);
pageDeleteRow(page, 0);
expect(page.dirty).toBe(true);
// 已删除的行读取失败
const row = pageReadRow(page, 0, colOrder, colTypes);
expect(row).toBeNull();
// 未删除的行仍然可读
const row2 = pageReadRow(page, 1, colOrder, colTypes);
expect(row2!.id).toBe('2');
});
it('pageFromBuffer 从 ArrayBuffer 恢复', () => {
const page = createPage(5, PageType.INDEX);
const restored = pageFromBuffer(5, page.data);
expect(restored.pageId).toBe(5);
expect(restored.type).toBe(PageType.INDEX);
expect(restored.dirty).toBe(false);
});
it('computeChecksum + verifyChecksum', () => {
const page = createPage(1, PageType.DATA);
updateChecksum(page);
expect(verifyChecksum(page)).toBe(true);
// 修改页面 → 校验和失效
new Uint8Array(page.data)[100] = 0xff;
expect(verifyChecksum(page)).toBe(false);
});
});
/**
* AriaEngine Page
* 覆盖: PageHeader / Slot / Tuple + PageFormat
*/
import {
PAGE_SIZE, PageType, PAGE_HEADER_SIZE, SLOT_ENTRY_SIZE,
} from '../../src/engine/aria/types';
import {
encodePageHeader, decodePageHeader, initPageHeader, getPageType,
getSlotCount, getFreeStart, setFreeStart, setFreeEnd,
} from '../../src/engine/aria/page/header';
import {
getSlotEntry, setSlotEntry, getSlotDirectorySize,
getFreeSpace, hasEnoughSpace, allocateSlot, readSlotData, freeSlot,
} from '../../src/engine/aria/page/slot';
import {
encodeTuple, decodeTuple, getColumnEncodingMap,
} from '../../src/engine/aria/page/tuple';
import { ColumnEncoding } from '../../src/engine/aria/types';
import {
createPage, pageFromBuffer, pageInsertRow, pageReadRow,
pageDeleteRow, pageUpdateRow, computeChecksum, verifyChecksum, updateChecksum,
} from '../../src/engine/aria/page/format';
// ===================================================================
// PageHeader
// ===================================================================
describe('AriaEngine Page — Header', () => {
let buf: ArrayBuffer;
beforeEach(() => {
buf = new ArrayBuffer(PAGE_SIZE);
});
it('initPageHeader 初始化头部字段', () => {
initPageHeader(buf, 42, PageType.DATA);
const h = decodePageHeader(buf);
expect(h.pageId).toBe(42);
expect(h.type).toBe(PageType.DATA);
expect(h.slotCount).toBe(0);
expect(h.freeStart).toBe(PAGE_HEADER_SIZE);
expect(h.freeEnd).toBe(PAGE_SIZE);
});
it('initPageHeader — INDEX 类型页面', () => {
initPageHeader(buf, 99, PageType.INDEX);
expect(getPageType(buf)).toBe(PageType.INDEX);
});
it('encodePageHeader + decodePageHeader 往返一致', () => {
const header = { pageId: 7, type: PageType.META, freeStart: 32, freeEnd: 4000, slotCount: 5, checksum: 0xdeadbeef };
encodePageHeader(header, buf);
const decoded = decodePageHeader(buf);
expect(decoded.pageId).toBe(7);
expect(decoded.type).toBe(PageType.META);
expect(decoded.freeStart).toBe(32);
expect(decoded.freeEnd).toBe(4000);
expect(decoded.slotCount).toBe(5);
});
it('不同 pageId 正确编解码', () => {
for (const id of [0, 1, 255, 65535, 0xffffffff]) {
initPageHeader(buf, id, PageType.DATA);
expect(decodePageHeader(buf).pageId).toBe(id >>> 0);
}
});
it('setFreeStart / setFreeEnd 修改字段', () => {
initPageHeader(buf, 1, PageType.DATA);
setFreeStart(buf, 100);
setFreeEnd(buf, 3000);
expect(getFreeStart(buf)).toBe(100);
expect(decodePageHeader(buf).freeEnd).toBe(3000);
});
});
// ===================================================================
// Slot Directory
// ===================================================================
describe('AriaEngine Page — Slot', () => {
let buf: ArrayBuffer;
beforeEach(() => {
buf = new ArrayBuffer(PAGE_SIZE);
initPageHeader(buf, 1, PageType.DATA);
});
it('getSlotEntry — 空页面 slotCount 为 0', () => {
expect(getSlotCount(buf)).toBe(0);
});
it('setSlotEntry + getSlotEntry 往返', () => {
// 手动写一个 slot(不通过 allocateSlot
new DataView(buf).setUint16(9, 1, false); // slotCount = 1
setSlotEntry(buf, 0, { offset: 1000, length: 50 });
const entry = getSlotEntry(buf, 0);
expect(entry.offset).toBe(1000);
expect(entry.length).toBe(50);
});
it('getSlotDirectorySize 计算正确', () => {
expect(getSlotDirectorySize(0)).toBe(0);
expect(getSlotDirectorySize(1)).toBe(SLOT_ENTRY_SIZE);
expect(getSlotDirectorySize(10)).toBe(10 * SLOT_ENTRY_SIZE);
});
it('getFreeSpace — 空页面有最大空闲空间', () => {
const free = getFreeSpace(buf);
expect(free).toBe(PAGE_SIZE - PAGE_HEADER_SIZE);
});
it('hasEnoughSpace — 小数据返回 true', () => {
expect(hasEnoughSpace(buf, 100)).toBe(true);
});
it('hasEnoughSpace — 超大数据返回 false', () => {
expect(hasEnoughSpace(buf, PAGE_SIZE * 2)).toBe(false);
});
it('allocateSlot 分配并写入数据', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const idx = allocateSlot(buf, data);
expect(idx).toBe(0);
expect(getSlotCount(buf)).toBe(1);
const readBack = readSlotData(buf, 0);
expect(readBack).not.toBeNull();
expect(Array.from(readBack!)).toEqual([1, 2, 3, 4, 5]);
});
it('allocateSlot 多次分配', () => {
for (let i = 0; i < 10; i++) {
const data = new Uint8Array([i, i + 1]);
const idx = allocateSlot(buf, data);
expect(idx).toBe(i);
}
expect(getSlotCount(buf)).toBe(10);
// 验证每个 slot 数据正确
for (let i = 0; i < 10; i++) {
const data = readSlotData(buf, i);
expect(Array.from(data!)).toEqual([i, i + 1]);
}
});
it('freeSlot 标记为删除', () => {
const data = new Uint8Array([10, 20, 30]);
allocateSlot(buf, data);
freeSlot(buf, 0);
const entry = getSlotEntry(buf, 0);
expect(entry.offset).toBe(0);
expect(entry.length).toBe(0);
});
it('readSlotData — 已删除 slot 返回 null', () => {
allocateSlot(buf, new Uint8Array([1]));
freeSlot(buf, 0);
expect(readSlotData(buf, 0)).toBeNull();
});
it('allocateSlot — 空间不足返回 -1', () => {
// 填满页面
const big = new Uint8Array(PAGE_SIZE - PAGE_HEADER_SIZE - SLOT_ENTRY_SIZE);
const idx1 = allocateSlot(buf, big);
expect(idx1).toBe(0);
const idx2 = allocateSlot(buf, new Uint8Array([1]));
expect(idx2).toBe(-1);
});
});
// ===================================================================
// Tuple Codec
// ===================================================================
describe('AriaEngine Page — Tuple', () => {
const colOrder = ['id', 'name', 'age', 'active', 'data'];
const colTypes: Record<string, string> = {
id: 'string', name: 'string', age: 'number', active: 'boolean', data: 'json',
};
it('encodeTuple + decodeTuple 完整往返', () => {
const row = { id: '1', name: 'Alice', age: 30, active: true, data: { x: 1 } };
const encoded = encodeTuple(row, colOrder, colTypes);
expect(encoded.byteLength).toBeGreaterThan(0);
const decoded = decodeTuple(encoded, colOrder, colTypes);
expect(decoded).not.toBeNull();
expect(decoded!.id).toBe('1');
expect(decoded!.name).toBe('Alice');
expect(decoded!.age).toBe(30);
expect(decoded!.active).toBe(true);
expect(decoded!.data).toEqual({ x: 1 });
});
it('encodeTuple — null 值正确处理', () => {
const row = { id: '2', name: null, age: 25, active: null, data: null };
const encoded = encodeTuple(row, colOrder, colTypes);
const decoded = decodeTuple(encoded, colOrder, colTypes);
expect(decoded!.name).toBeNull();
expect(decoded!.active).toBeNull();
expect(decoded!.data).toBeNull();
});
it('encodeTuple — undefined 值按 null 处理', () => {
const row = { id: '3', age: 30 } as any;
const encoded = encodeTuple(row, colOrder, colTypes);
const decoded = decodeTuple(encoded, colOrder, colTypes);
expect(decoded!.id).toBe('3');
expect(decoded!.name).toBeNull();
});
it('encodeTuple — date 类型', () => {
const order = ['ts'];
const types = { ts: 'date' };
const row = { ts: '2024-01-15T00:00:00.000Z' };
const encoded = encodeTuple(row, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.ts).toBe('2024-01-15T00:00:00.000Z');
});
it('encodeTuple — boolean false', () => {
const order = ['flag'];
const types = { flag: 'boolean' };
const encoded = encodeTuple({ flag: false }, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.flag).toBe(false);
});
it('encodeTuple — 负数', () => {
const order = ['val'];
const types = { val: 'number' };
const encoded = encodeTuple({ val: -42.5 }, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.val).toBe(-42.5);
});
it('encodeTuple — 空字符串', () => {
const order = ['s'];
const types = { s: 'string' };
const encoded = encodeTuple({ s: '' }, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.s).toBe('');
});
it('encodeTuple — 长字符串', () => {
const order = ['s'];
const types = { s: 'string' };
const long = 'x'.repeat(10000);
const encoded = encodeTuple({ s: long }, order, types);
const decoded = decodeTuple(encoded, order, types);
expect(decoded!.s).toBe(long);
});
it('getColumnEncodingMap 返回正确映射', () => {
const map = getColumnEncodingMap(colOrder, colTypes);
expect(map.get('id')).toBe(ColumnEncoding.STRING);
expect(map.get('age')).toBe(ColumnEncoding.NUMBER);
expect(map.get('active')).toBe(ColumnEncoding.BOOLEAN);
expect(map.get('data')).toBe(ColumnEncoding.JSON);
});
it('decodeTuple — 损坏数据返回 null', () => {
const broken = new Uint8Array([0xff, 0xff, 0xff]);
expect(decodeTuple(broken, colOrder, colTypes)).toBeNull();
});
});
// ===================================================================
// Page Format 整合
// ===================================================================
describe('AriaEngine Page — Format', () => {
const colOrder = ['id', 'name'];
const colTypes: Record<string, string> = { id: 'string', name: 'string' };
it('createPage 创建合法页面', () => {
const page = createPage(100, PageType.DATA);
expect(page.pageId).toBe(100);
expect(page.type).toBe(PageType.DATA);
expect(page.pins).toBe(0);
expect(page.dirty).toBe(true);
});
it('pageInsertRow + pageReadRow 往返', () => {
const page = createPage(1, PageType.DATA);
const row = { id: 'u1', name: 'Test' };
const idx = pageInsertRow(page, row, colOrder, colTypes);
expect(idx).toBe(0);
const read = pageReadRow(page, 0, colOrder, colTypes);
expect(read).not.toBeNull();
expect(read!.id).toBe('u1');
expect(read!.name).toBe('Test');
});
it('pageInsertRow — 多次插入', () => {
const page = createPage(1, PageType.DATA);
for (let i = 0; i < 50; i++) {
const idx = pageInsertRow(page, { id: `${i}`, name: `User${i}` }, colOrder, colTypes);
expect(idx).toBe(i);
}
for (let i = 0; i < 50; i++) {
const row = pageReadRow(page, i, colOrder, colTypes);
expect(row!.id).toBe(`${i}`);
}
});
it('pageDeleteRow 标记删除', () => {
const page = createPage(1, PageType.DATA);
pageInsertRow(page, { id: '1', name: 'A' }, colOrder, colTypes);
pageInsertRow(page, { id: '2', name: 'B' }, colOrder, colTypes);
pageDeleteRow(page, 0);
expect(page.dirty).toBe(true);
// 已删除的行读取失败
const row = pageReadRow(page, 0, colOrder, colTypes);
expect(row).toBeNull();
// 未删除的行仍然可读
const row2 = pageReadRow(page, 1, colOrder, colTypes);
expect(row2!.id).toBe('2');
});
it('pageFromBuffer 从 ArrayBuffer 恢复', () => {
const page = createPage(5, PageType.INDEX);
const restored = pageFromBuffer(5, page.data);
expect(restored.pageId).toBe(5);
expect(restored.type).toBe(PageType.INDEX);
expect(restored.dirty).toBe(false);
});
it('computeChecksum + verifyChecksum', () => {
const page = createPage(1, PageType.DATA);
updateChecksum(page);
expect(verifyChecksum(page)).toBe(true);
// 修改页面 → 校验和失效
new Uint8Array(page.data)[100] = 0xff;
expect(verifyChecksum(page)).toBe(false);
});
});
+129 -129
View File
@@ -1,129 +1,129 @@
/**
* AriaEngine SSTable Builder + Reader
*/
import { SSTableBuilder } from '../../src/engine/aria/index/sstable_builder';
import { SSTableReader } from '../../src/engine/aria/index/sstable';
import type { SSTableMeta } from '../../src/engine/aria/types';
// ===================================================================
// SSTable Builder + Reader
// ===================================================================
describe('AriaEngine — SSTable Builder + Reader', () => {
const makeMeta = (data: Uint8Array): SSTableMeta => ({
id: 1, level: 0, minKey: '', maxKey: '\uffff',
blockCount: 1, totalSize: data.byteLength, bloomData: null,
});
it('构建单条目 SSTable 并精确读取', () => {
const builder = new SSTableBuilder(4096);
builder.add('key1', { name: 'Alice', age: 30 });
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const result = reader.get('key1');
expect(result).not.toBeNull();
expect(result!.name).toBe('Alice');
expect(result!.age).toBe(30);
});
it('构建多条 SSTable 并全部读取', () => {
const builder = new SSTableBuilder(4096);
const items: [string, Record<string, unknown>][] = [];
for (let i = 0; i < 100; i++) {
const key = `user-${String(i).padStart(3, '0')}`;
const value = { idx: i, name: `User${i}` };
items.push([key, value]);
builder.add(key, value);
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
for (const [key, value] of items) {
const result = reader.get(key);
expect(result).not.toBeNull();
expect(result!.idx).toBe(value.idx);
}
});
it('get — 不存在的 key 返回 null', () => {
const builder = new SSTableBuilder(4096);
builder.add('a', { v: 1 });
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.get('nonexistent')).toBeNull();
});
it('rangeScan — 范围查询', () => {
const builder = new SSTableBuilder(4096);
for (let i = 0; i < 20; i++) {
builder.add(`k-${String(i).padStart(2, '0')}`, { v: i });
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const results: [string, Record<string, unknown>][] = [];
reader.rangeScan('k-05', 'k-10', (k, v) => results.push([k, v]));
expect(results).toHaveLength(6);
expect(results[0][0]).toBe('k-05');
expect(results[results.length - 1][0]).toBe('k-10');
});
it('scanAll — 遍历所有条目', () => {
const builder = new SSTableBuilder(4096);
const count = 50;
for (let i = 0; i < count; i++) {
builder.add(`item-${i}`, { idx: i });
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const items: [string, Record<string, unknown>][] = [];
reader.scanAll((k, v) => items.push([k, v]));
expect(items).toHaveLength(count);
});
it('getIndexBlockCount 返回索引块数', () => {
const builder = new SSTableBuilder(256); // 小 block size 触发多个 block
for (let i = 0; i < 100; i++) {
builder.add(`k-${i}`, { data: 'x'.repeat(50) });
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.getIndexBlockCount()).toBeGreaterThanOrEqual(1);
});
it('边界 — 空 SSTable 不抛异常', () => {
const builder = new SSTableBuilder(4096);
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.get('any')).toBeNull();
const results: [string, Record<string, unknown>][] = [];
reader.scanAll((k, v) => results.push([k, v]));
expect(results).toHaveLength(0);
});
it('带特殊字符的 key', () => {
const builder = new SSTableBuilder(4096);
// 必须按键排序添加(按 ASCII 排序:空格 < 短横 < 点号)
builder.add('key with space', { v: 3 });
builder.add('key-with-dash', { v: 1 });
builder.add('key.with.dot', { v: 2 });
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.get('key-with-dash')!.v).toBe(1);
expect(reader.get('key.with.dot')!.v).toBe(2);
expect(reader.get('key with space')!.v).toBe(3);
});
it('getEntryCount 返回正确条目数', () => {
const builder = new SSTableBuilder(4096);
builder.add('a', { v: 1 });
builder.add('b', { v: 2 });
builder.add('c', { v: 3 });
expect(builder.getEntryCount()).toBe(3);
});
});
/**
* AriaEngine SSTable Builder + Reader
*/
import { SSTableBuilder } from '../../src/engine/aria/index/sstable_builder';
import { SSTableReader } from '../../src/engine/aria/index/sstable';
import type { SSTableMeta } from '../../src/engine/aria/types';
// ===================================================================
// SSTable Builder + Reader
// ===================================================================
describe('AriaEngine — SSTable Builder + Reader', () => {
const makeMeta = (data: Uint8Array): SSTableMeta => ({
id: 1, level: 0, minKey: '', maxKey: '\uffff',
blockCount: 1, totalSize: data.byteLength, bloomData: null,
});
it('构建单条目 SSTable 并精确读取', () => {
const builder = new SSTableBuilder(4096);
builder.add('key1', { name: 'Alice', age: 30 });
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const result = reader.get('key1');
expect(result).not.toBeNull();
expect(result!.name).toBe('Alice');
expect(result!.age).toBe(30);
});
it('构建多条 SSTable 并全部读取', () => {
const builder = new SSTableBuilder(4096);
const items: [string, Record<string, unknown>][] = [];
for (let i = 0; i < 100; i++) {
const key = `user-${String(i).padStart(3, '0')}`;
const value = { idx: i, name: `User${i}` };
items.push([key, value]);
builder.add(key, value);
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
for (const [key, value] of items) {
const result = reader.get(key);
expect(result).not.toBeNull();
expect(result!.idx).toBe(value.idx);
}
});
it('get — 不存在的 key 返回 null', () => {
const builder = new SSTableBuilder(4096);
builder.add('a', { v: 1 });
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.get('nonexistent')).toBeNull();
});
it('rangeScan — 范围查询', () => {
const builder = new SSTableBuilder(4096);
for (let i = 0; i < 20; i++) {
builder.add(`k-${String(i).padStart(2, '0')}`, { v: i });
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const results: [string, Record<string, unknown>][] = [];
reader.rangeScan('k-05', 'k-10', (k, v) => results.push([k, v]));
expect(results).toHaveLength(6);
expect(results[0][0]).toBe('k-05');
expect(results[results.length - 1][0]).toBe('k-10');
});
it('scanAll — 遍历所有条目', () => {
const builder = new SSTableBuilder(4096);
const count = 50;
for (let i = 0; i < count; i++) {
builder.add(`item-${i}`, { idx: i });
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const items: [string, Record<string, unknown>][] = [];
reader.scanAll((k, v) => items.push([k, v]));
expect(items).toHaveLength(count);
});
it('getIndexBlockCount 返回索引块数', () => {
const builder = new SSTableBuilder(256); // 小 block size 触发多个 block
for (let i = 0; i < 100; i++) {
builder.add(`k-${i}`, { data: 'x'.repeat(50) });
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.getIndexBlockCount()).toBeGreaterThanOrEqual(1);
});
it('边界 — 空 SSTable 不抛异常', () => {
const builder = new SSTableBuilder(4096);
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.get('any')).toBeNull();
const results: [string, Record<string, unknown>][] = [];
reader.scanAll((k, v) => results.push([k, v]));
expect(results).toHaveLength(0);
});
it('带特殊字符的 key', () => {
const builder = new SSTableBuilder(4096);
// 必须按键排序添加(按 ASCII 排序:空格 < 短横 < 点号)
builder.add('key with space', { v: 3 });
builder.add('key-with-dash', { v: 1 });
builder.add('key.with.dot', { v: 2 });
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.get('key-with-dash')!.v).toBe(1);
expect(reader.get('key.with.dot')!.v).toBe(2);
expect(reader.get('key with space')!.v).toBe(3);
});
it('getEntryCount 返回正确条目数', () => {
const builder = new SSTableBuilder(4096);
builder.add('a', { v: 1 });
builder.add('b', { v: 2 });
builder.add('c', { v: 3 });
expect(builder.getEntryCount()).toBe(3);
});
});
+289 -289
View File
@@ -1,289 +1,289 @@
/**
* AriaEngine WAL + MVCC
*/
import { WAL, type WALStore } from '../../src/engine/aria/wal/log';
import { WALRecordType, type WALRecord } from '../../src/engine/aria/types';
import { CheckpointManager, type Flushable } from '../../src/engine/aria/wal/checkpoint';
import { MVCCManager } from '../../src/engine/aria/transaction/mvcc';
// ===================================================================
// WAL 存储 Mock
// ===================================================================
class MockWALStore implements WALStore {
chunks: Uint8Array[] = [];
async append(data: Uint8Array) { this.chunks.push(data); }
async readAll(): Promise<Uint8Array> {
const total = this.chunks.reduce((s, c) => s + c.byteLength, 0);
const combined = new Uint8Array(total);
let off = 0;
for (const c of this.chunks) { combined.set(c, off); off += c.byteLength; }
return combined;
}
async truncate() { this.chunks = []; }
async exists() { return this.chunks.length > 0; }
}
// ===================================================================
// WAL 测试
// ===================================================================
describe('AriaEngine — WAL', () => {
it('append 记录后可恢复', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
wal.append({ type: WALRecordType.INSERT, txnId: 1, tableName: 'users', key: '1', data: { name: 'Alice' } });
wal.append({ type: WALRecordType.INSERT, txnId: 1, tableName: 'users', key: '2', data: { name: 'Bob' } });
const records: WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(2);
expect(records[0].tableName).toBe('users');
expect(records[0].key).toBe('1');
});
it('batch 模式缓冲后 flush', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'batch');
wal.append({ type: WALRecordType.UPDATE, txnId: 2, tableName: 'items', key: 'a', data: { v: 1 } });
wal.append({ type: WALRecordType.DELETE, txnId: 2, tableName: 'items', key: 'b' });
// 未 flush 前无法恢复
let records: WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(0);
await wal.flush();
records = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(2);
});
it('none 模式不记录', async () => {
const store = new MockWALStore();
const wal = new WAL(store, false, 'none');
wal.append({ type: WALRecordType.INSERT, txnId: 3, tableName: 'x', key: 'y', data: {} });
expect(store.chunks).toHaveLength(0);
});
it('checkpoint 清空 WAL', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
wal.append({ type: WALRecordType.CREATE_TABLE, txnId: 0, tableName: 't', key: '' });
expect(await store.exists()).toBe(true);
await wal.checkpoint();
expect(await store.exists()).toBe(false);
});
it('getLSN 跟踪序列号', () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
expect(wal.getLSN()).toBe(0);
wal.append({ type: WALRecordType.BEGIN, txnId: 10, tableName: '', key: '' });
expect(wal.getLSN()).toBe(1);
wal.append({ type: WALRecordType.COMMIT, txnId: 10, tableName: '', key: '' });
expect(wal.getLSN()).toBe(2);
});
it('isEnabled 反映配置', () => {
expect(new WAL(new MockWALStore(), true).isEnabled()).toBe(true);
expect(new WAL(new MockWALStore(), false).isEnabled()).toBe(false);
});
it('多种记录类型编解码', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
wal.append({ type: WALRecordType.BEGIN, txnId: 100, tableName: '', key: '' });
wal.append({ type: WALRecordType.INSERT, txnId: 100, tableName: 'users', key: '1', data: { x: 'hello' } });
wal.append({ type: WALRecordType.UPDATE, txnId: 100, tableName: 'users', key: '1', data: { x: 'world' } });
wal.append({ type: WALRecordType.COMMIT, txnId: 100, tableName: '', key: '' });
const records: WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(4);
expect(records[0].type).toBe(WALRecordType.BEGIN);
expect(records[1].type).toBe(WALRecordType.INSERT);
expect(records[2].type).toBe(WALRecordType.UPDATE);
expect(records[3].type).toBe(WALRecordType.COMMIT);
});
it('getBufferedCount 返回缓冲数', () => {
const wal = new WAL(new MockWALStore(), true, 'batch');
expect(wal.getBufferedCount()).toBe(0);
wal.append({ type: WALRecordType.INSERT, txnId: 0, tableName: 't', key: 'k' });
expect(wal.getBufferedCount()).toBe(1);
});
});
// ===================================================================
// Checkpoint 测试(使用安全 Mock,避免 null 引用导致 CI 卡死)
// ===================================================================
describe('AriaEngine — CheckpointManager', () => {
class MockFlushable implements Flushable { flushed = false; async flushAll() { this.flushed = true; } }
class MockLSM { flushed = false; async flush() { this.flushed = true; } }
class MockWAL { checkpointed = false; async checkpoint() { this.checkpointed = true; } async flush() {} }
it('tick 未达间隔不触发 checkpoint', async () => {
const lsm = new MockLSM();
const wal = new MockWAL();
const flushable = new MockFlushable();
const cm = new CheckpointManager(lsm as any, wal as any, flushable, 100);
await cm.tick();
await cm.tick();
expect(cm.getOpCount()).toBe(2);
expect(flushable.flushed).toBe(false);
expect(lsm.flushed).toBe(false);
});
it('setInterval 修改间隔后 tick 触发 checkpoint', async () => {
const lsm = new MockLSM();
const wal = new MockWAL();
const cm = new CheckpointManager(lsm as any, wal as any, null, 1000);
cm.setInterval(2);
await cm.tick();
await cm.tick();
expect(cm.getOpCount()).toBe(0); // reset after checkpoint
expect(lsm.flushed).toBe(true);
expect(wal.checkpointed).toBe(true);
});
it('forceCheckpoint 强制触发', async () => {
const lsm = new MockLSM();
const wal = new MockWAL();
const cm = new CheckpointManager(lsm as any, wal as any, null, 100);
await cm.forceCheckpoint();
expect(cm.getOpCount()).toBe(0);
expect(lsm.flushed).toBe(true);
expect(wal.checkpointed).toBe(true);
});
});
// ===================================================================
// MVCC 测试
// ===================================================================
describe('AriaEngine — MVCC', () => {
let mvcc: MVCCManager;
beforeEach(() => { mvcc = new MVCCManager(); });
it('beginTransaction 分配唯一 ID', () => {
const id1 = mvcc.beginTransaction();
const id2 = mvcc.beginTransaction();
expect(id1).not.toBe(id2);
expect(mvcc.isActive(id1)).toBe(true);
expect(mvcc.isActive(id2)).toBe(true);
});
it('commit 后 isActive 返回 false', () => {
const txnId = mvcc.beginTransaction();
mvcc.commitTransaction(txnId);
expect(mvcc.isActive(txnId)).toBe(false);
});
it('rollback 后 isActive 返回 false', () => {
const txnId = mvcc.beginTransaction();
mvcc.rollbackTransaction(txnId);
expect(mvcc.isActive(txnId)).toBe(false);
});
it('writeVersion + readVersion 往返', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice', age: 30 }, txnId);
const val = mvcc.readVersion('users', '1', txnId);
expect(val).not.toBeNull();
expect(val!.name).toBe('Alice');
});
it('未提交版本对其他事务不可见', () => {
const txn1 = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txn1);
const txn2 = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txn2);
expect(val).toBeNull(); // txn1's write not yet committed
});
it('commit 后新事务可见', () => {
const txn1 = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txn1);
mvcc.commitTransaction(txn1);
const txn2 = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txn2);
expect(val).not.toBeNull();
expect(val!.name).toBe('Alice');
});
it('rollback 移除写入的版本', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Temp' }, txnId);
mvcc.rollbackTransaction(txnId);
const txn2 = mvcc.beginTransaction();
expect(mvcc.readVersion('users', '1', txn2)).toBeNull();
});
it('deleteVersion 创建墓碑', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txnId);
mvcc.commitTransaction(txnId);
// delete
const txn2 = mvcc.beginTransaction();
mvcc.deleteVersion('users', '1', txn2);
mvcc.commitTransaction(txn2);
// 删除后读取
const txn3 = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txn3);
expect(val).not.toBeNull();
expect((val! as any).__mvcc_tombstone).toBe(true);
});
it('getLatestCommittedVersions 返回最新已提交', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txnId);
mvcc.writeVersion('users', '2', { name: 'Bob' }, txnId);
mvcc.commitTransaction(txnId);
const result = mvcc.getLatestCommittedVersions('users');
expect(result['1'].name).toBe('Alice');
expect(result['2'].name).toBe('Bob');
});
it('clearTable 清理指定表', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'A' }, txnId);
mvcc.writeVersion('users', '2', { name: 'B' }, txnId);
mvcc.writeVersion('orders', 'o1', { amt: 100 }, txnId);
mvcc.commitTransaction(txnId);
mvcc.clearTable('users');
const txn2 = mvcc.beginTransaction();
expect(mvcc.readVersion('users', '1', txn2)).toBeNull();
expect(mvcc.readVersion('users', '2', txn2)).toBeNull();
expect(mvcc.readVersion('orders', 'o1', txn2)).not.toBeNull();
});
it('getActiveTxnCount 返回活跃事务数', () => {
expect(mvcc.getActiveTxnCount()).toBe(0);
mvcc.beginTransaction();
mvcc.beginTransaction();
expect(mvcc.getActiveTxnCount()).toBe(2);
});
it('gc 清理过旧版本', () => {
// 创建很多版本后 gc
for (let i = 0; i < 200; i++) {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { ver: i }, txnId);
mvcc.commitTransaction(txnId);
}
mvcc.gc(100);
// gc 后应可继续操作
const txnId = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txnId);
expect(val).not.toBeNull();
});
});
/**
* AriaEngine WAL + MVCC
*/
import { WAL, type WALStore } from '../../src/engine/aria/wal/log';
import { WALRecordType, type WALRecord } from '../../src/engine/aria/types';
import { CheckpointManager, type Flushable } from '../../src/engine/aria/wal/checkpoint';
import { MVCCManager } from '../../src/engine/aria/transaction/mvcc';
// ===================================================================
// WAL 存储 Mock
// ===================================================================
class MockWALStore implements WALStore {
chunks: Uint8Array[] = [];
async append(data: Uint8Array) { this.chunks.push(data); }
async readAll(): Promise<Uint8Array> {
const total = this.chunks.reduce((s, c) => s + c.byteLength, 0);
const combined = new Uint8Array(total);
let off = 0;
for (const c of this.chunks) { combined.set(c, off); off += c.byteLength; }
return combined;
}
async truncate() { this.chunks = []; }
async exists() { return this.chunks.length > 0; }
}
// ===================================================================
// WAL 测试
// ===================================================================
describe('AriaEngine — WAL', () => {
it('append 记录后可恢复', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
wal.append({ type: WALRecordType.INSERT, txnId: 1, tableName: 'users', key: '1', data: { name: 'Alice' } });
wal.append({ type: WALRecordType.INSERT, txnId: 1, tableName: 'users', key: '2', data: { name: 'Bob' } });
const records: WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(2);
expect(records[0].tableName).toBe('users');
expect(records[0].key).toBe('1');
});
it('batch 模式缓冲后 flush', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'batch');
wal.append({ type: WALRecordType.UPDATE, txnId: 2, tableName: 'items', key: 'a', data: { v: 1 } });
wal.append({ type: WALRecordType.DELETE, txnId: 2, tableName: 'items', key: 'b' });
// 未 flush 前无法恢复
let records: WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(0);
await wal.flush();
records = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(2);
});
it('none 模式不记录', async () => {
const store = new MockWALStore();
const wal = new WAL(store, false, 'none');
wal.append({ type: WALRecordType.INSERT, txnId: 3, tableName: 'x', key: 'y', data: {} });
expect(store.chunks).toHaveLength(0);
});
it('checkpoint 清空 WAL', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
wal.append({ type: WALRecordType.CREATE_TABLE, txnId: 0, tableName: 't', key: '' });
expect(await store.exists()).toBe(true);
await wal.checkpoint();
expect(await store.exists()).toBe(false);
});
it('getLSN 跟踪序列号', () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
expect(wal.getLSN()).toBe(0);
wal.append({ type: WALRecordType.BEGIN, txnId: 10, tableName: '', key: '' });
expect(wal.getLSN()).toBe(1);
wal.append({ type: WALRecordType.COMMIT, txnId: 10, tableName: '', key: '' });
expect(wal.getLSN()).toBe(2);
});
it('isEnabled 反映配置', () => {
expect(new WAL(new MockWALStore(), true).isEnabled()).toBe(true);
expect(new WAL(new MockWALStore(), false).isEnabled()).toBe(false);
});
it('多种记录类型编解码', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
wal.append({ type: WALRecordType.BEGIN, txnId: 100, tableName: '', key: '' });
wal.append({ type: WALRecordType.INSERT, txnId: 100, tableName: 'users', key: '1', data: { x: 'hello' } });
wal.append({ type: WALRecordType.UPDATE, txnId: 100, tableName: 'users', key: '1', data: { x: 'world' } });
wal.append({ type: WALRecordType.COMMIT, txnId: 100, tableName: '', key: '' });
const records: WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(4);
expect(records[0].type).toBe(WALRecordType.BEGIN);
expect(records[1].type).toBe(WALRecordType.INSERT);
expect(records[2].type).toBe(WALRecordType.UPDATE);
expect(records[3].type).toBe(WALRecordType.COMMIT);
});
it('getBufferedCount 返回缓冲数', () => {
const wal = new WAL(new MockWALStore(), true, 'batch');
expect(wal.getBufferedCount()).toBe(0);
wal.append({ type: WALRecordType.INSERT, txnId: 0, tableName: 't', key: 'k' });
expect(wal.getBufferedCount()).toBe(1);
});
});
// ===================================================================
// Checkpoint 测试(使用安全 Mock,避免 null 引用导致 CI 卡死)
// ===================================================================
describe('AriaEngine — CheckpointManager', () => {
class MockFlushable implements Flushable { flushed = false; async flushAll() { this.flushed = true; } }
class MockLSM { flushed = false; async flush() { this.flushed = true; } }
class MockWAL { checkpointed = false; async checkpoint() { this.checkpointed = true; } async flush() {} }
it('tick 未达间隔不触发 checkpoint', async () => {
const lsm = new MockLSM();
const wal = new MockWAL();
const flushable = new MockFlushable();
const cm = new CheckpointManager(lsm as any, wal as any, flushable, 100);
await cm.tick();
await cm.tick();
expect(cm.getOpCount()).toBe(2);
expect(flushable.flushed).toBe(false);
expect(lsm.flushed).toBe(false);
});
it('setInterval 修改间隔后 tick 触发 checkpoint', async () => {
const lsm = new MockLSM();
const wal = new MockWAL();
const cm = new CheckpointManager(lsm as any, wal as any, null, 1000);
cm.setInterval(2);
await cm.tick();
await cm.tick();
expect(cm.getOpCount()).toBe(0); // reset after checkpoint
expect(lsm.flushed).toBe(true);
expect(wal.checkpointed).toBe(true);
});
it('forceCheckpoint 强制触发', async () => {
const lsm = new MockLSM();
const wal = new MockWAL();
const cm = new CheckpointManager(lsm as any, wal as any, null, 100);
await cm.forceCheckpoint();
expect(cm.getOpCount()).toBe(0);
expect(lsm.flushed).toBe(true);
expect(wal.checkpointed).toBe(true);
});
});
// ===================================================================
// MVCC 测试
// ===================================================================
describe('AriaEngine — MVCC', () => {
let mvcc: MVCCManager;
beforeEach(() => { mvcc = new MVCCManager(); });
it('beginTransaction 分配唯一 ID', () => {
const id1 = mvcc.beginTransaction();
const id2 = mvcc.beginTransaction();
expect(id1).not.toBe(id2);
expect(mvcc.isActive(id1)).toBe(true);
expect(mvcc.isActive(id2)).toBe(true);
});
it('commit 后 isActive 返回 false', () => {
const txnId = mvcc.beginTransaction();
mvcc.commitTransaction(txnId);
expect(mvcc.isActive(txnId)).toBe(false);
});
it('rollback 后 isActive 返回 false', () => {
const txnId = mvcc.beginTransaction();
mvcc.rollbackTransaction(txnId);
expect(mvcc.isActive(txnId)).toBe(false);
});
it('writeVersion + readVersion 往返', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice', age: 30 }, txnId);
const val = mvcc.readVersion('users', '1', txnId);
expect(val).not.toBeNull();
expect(val!.name).toBe('Alice');
});
it('未提交版本对其他事务不可见', () => {
const txn1 = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txn1);
const txn2 = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txn2);
expect(val).toBeNull(); // txn1's write not yet committed
});
it('commit 后新事务可见', () => {
const txn1 = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txn1);
mvcc.commitTransaction(txn1);
const txn2 = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txn2);
expect(val).not.toBeNull();
expect(val!.name).toBe('Alice');
});
it('rollback 移除写入的版本', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Temp' }, txnId);
mvcc.rollbackTransaction(txnId);
const txn2 = mvcc.beginTransaction();
expect(mvcc.readVersion('users', '1', txn2)).toBeNull();
});
it('deleteVersion 创建墓碑', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txnId);
mvcc.commitTransaction(txnId);
// delete
const txn2 = mvcc.beginTransaction();
mvcc.deleteVersion('users', '1', txn2);
mvcc.commitTransaction(txn2);
// 删除后读取
const txn3 = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txn3);
expect(val).not.toBeNull();
expect((val! as any).__mvcc_tombstone).toBe(true);
});
it('getLatestCommittedVersions 返回最新已提交', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txnId);
mvcc.writeVersion('users', '2', { name: 'Bob' }, txnId);
mvcc.commitTransaction(txnId);
const result = mvcc.getLatestCommittedVersions('users');
expect(result['1'].name).toBe('Alice');
expect(result['2'].name).toBe('Bob');
});
it('clearTable 清理指定表', () => {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'A' }, txnId);
mvcc.writeVersion('users', '2', { name: 'B' }, txnId);
mvcc.writeVersion('orders', 'o1', { amt: 100 }, txnId);
mvcc.commitTransaction(txnId);
mvcc.clearTable('users');
const txn2 = mvcc.beginTransaction();
expect(mvcc.readVersion('users', '1', txn2)).toBeNull();
expect(mvcc.readVersion('users', '2', txn2)).toBeNull();
expect(mvcc.readVersion('orders', 'o1', txn2)).not.toBeNull();
});
it('getActiveTxnCount 返回活跃事务数', () => {
expect(mvcc.getActiveTxnCount()).toBe(0);
mvcc.beginTransaction();
mvcc.beginTransaction();
expect(mvcc.getActiveTxnCount()).toBe(2);
});
it('gc 清理过旧版本', () => {
// 创建很多版本后 gc
for (let i = 0; i < 200; i++) {
const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { ver: i }, txnId);
mvcc.commitTransaction(txnId);
}
mvcc.gc(100);
// gc 后应可继续操作
const txnId = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txnId);
expect(val).not.toBeNull();
});
});
+932 -932
View File
File diff suppressed because it is too large Load Diff
+200
View File
@@ -0,0 +1,200 @@
/**
* React hooks v0.2.6
* @module tests/integrations/react
*
* react peer dependency
* 使 React mock hooks
* - mount
* -
* - refresh
* - SQL
*/
import { MetonaSqlark } from '../../src/core';
// ---- 最小 React mock(自包含:jest.mock 工厂不能引用外部变量) ----
jest.mock('react', () => {
const stateStore: any[] = [];
const registeredEffects = new Set<number>();
const effectQueue: Array<() => void | Promise<void>> = [];
let cursor = 0;
return {
useState: (init: any) => {
const idx = cursor++;
if (!(idx in stateStore)) stateStore[idx] = init;
return [
stateStore[idx],
(v: any) => {
stateStore[idx] = typeof v === 'function' ? v(stateStore[idx]) : v;
},
];
},
// 按 hook 调用位置去重:同一位置的 effect 只在首次 render 注册
useEffect: (fn: any, _deps: any[]) => {
const idx = cursor;
if (!registeredEffects.has(idx)) {
registeredEffects.add(idx);
effectQueue.push(fn);
}
},
useCallback: (fn: any) => fn,
useRef: (init: any) => ({ current: init }),
/** 模拟一次组件渲染:cursor 归零后执行 hook 函数 */
__mockRender: (fn: () => any): any => {
cursor = 0;
return fn();
},
__mockEffects: effectQueue,
__mockReset: () => {
stateStore.length = 0;
cursor = 0;
effectQueue.length = 0;
registeredEffects.clear();
},
};
}, { virtual: true });
import { useQuery, useTable, useDatabase } from '../../src/integrations/react';
/** mock 模块内的 effect 队列与渲染控制(自包含作用域) */
const reactMock = jest.requireMock('react') as {
__mockEffects: Array<() => void | Promise<void>>;
__mockRender: (fn: () => any) => any;
__mockReset: () => void;
};
beforeEach(() => {
reactMock.__mockReset();
});
/** 模拟组件挂载:执行 useEffect 中注册的回调 */
async function flushEffects(): Promise<void> {
const fns = reactMock.__mockEffects.splice(0);
for (const fn of fns) {
await fn();
}
}
/** 模拟组件渲染(cursor 归零,读取最新状态) */
function render<T>(fn: () => T): T {
return reactMock.__mockRender(fn);
}
describe('useQuery', () => {
test('mount 时执行 SQL 查询并更新 data/loading', async () => {
const db = { query: jest.fn().mockResolvedValue([{ id: '1', name: 'Alice' }]) } as any;
const mount = () => useQuery(db, 'SELECT * FROM users');
const hook = render(mount);
expect(hook.loading).toBe(true);
expect(db.query).not.toHaveBeenCalled();
await flushEffects();
const after = render(mount); // 模拟重渲染读取最新状态
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
expect(after.loading).toBe(false);
expect(after.data).toEqual([{ id: '1', name: 'Alice' }]);
expect(after.error).toBeNull();
});
test('查询失败时设置 error 且 data 保持空', async () => {
const db = { query: jest.fn().mockRejectedValue(new Error('query boom')) } as any;
const mount = () => useQuery(db, 'SELECT * FROM users');
render(mount);
await flushEffects();
const after = render(mount);
expect(after.error).toBeInstanceOf(Error);
expect((after.error as Error).message).toBe('query boom');
expect(after.data).toEqual([]);
expect(after.loading).toBe(false);
});
test('refresh 可重新执行查询', async () => {
let call = 0;
const db = { query: jest.fn().mockImplementation(async () => [{ n: ++call }]) } as any;
const mount = () => useQuery(db, 'SELECT * FROM users');
const hook = render(mount);
await flushEffects();
expect(render(mount).data).toEqual([{ n: 1 }]);
await hook.refresh();
await flushEffects();
expect(db.query).toHaveBeenCalledTimes(2);
expect(render(mount).data).toEqual([{ n: 2 }]);
});
test('不同 SQL 使用各自独立的 hook 状态', async () => {
const db = { query: jest.fn().mockResolvedValue([]) } as any;
const hook1 = useQuery(db, 'SELECT * FROM a');
const hook2 = useQuery(db, 'SELECT * FROM b');
await flushEffects();
expect(hook1).not.toBe(hook2);
expect(db.query).toHaveBeenCalledWith('SELECT * FROM a');
expect(db.query).toHaveBeenCalledWith('SELECT * FROM b');
});
});
describe('useTable', () => {
test('合法表名执行全表查询', async () => {
const db = { query: jest.fn().mockResolvedValue([{ id: 1 }]) } as any;
const mount = () => useTable(db, 'users');
render(mount);
await flushEffects();
const after = render(mount);
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
expect(after.data).toEqual([{ id: 1 }]);
expect(after.loading).toBe(false);
});
test('非法表名抛出校验错误(SQL 注入防护)', () => {
const db = { query: jest.fn() } as any;
expect(() => useTable(db, 'users; DROP TABLE orders')).toThrow(/Invalid table name/);
expect(() => useTable(db, "users' OR '1'='1")).toThrow(/Invalid table name/);
expect(() => useTable(db, '1users')).toThrow(/Invalid table name/);
expect(db.query).not.toHaveBeenCalled();
});
});
describe('useDatabase', () => {
test('创建数据库实例并初始化', async () => {
const initSpy = jest.spyOn(MetonaSqlark.prototype, 'init').mockResolvedValue();
const closeSpy = jest.spyOn(MetonaSqlark.prototype, 'close').mockResolvedValue();
const mount = () => useDatabase({ name: 'hook-test' });
const hook = render(mount);
expect(hook.ready).toBe(false);
await flushEffects();
const after = render(mount);
expect(initSpy).toHaveBeenCalled();
expect(after.db).toBeInstanceOf(MetonaSqlark);
expect(after.ready).toBe(true);
expect(after.error).toBeNull();
initSpy.mockRestore();
closeSpy.mockRestore();
});
test('初始化失败时设置 error', async () => {
const initSpy = jest.spyOn(MetonaSqlark.prototype, 'init').mockRejectedValue(new Error('init fail'));
const mount = () => useDatabase({ name: 'hook-fail' });
render(mount);
await flushEffects();
const after = render(mount);
expect(after.db).toBeNull();
expect(after.ready).toBe(false);
expect(after.error).toBeInstanceOf(Error);
expect((after.error as Error).message).toBe('init fail');
initSpy.mockRestore();
});
});
+167
View File
@@ -0,0 +1,167 @@
/**
* Vue composables v0.2.6
* @module tests/integrations/vue
*
* vue peer dependency
* 使 Vue mock composables
* - onMounted
* - watch sql/deps
* - refresh
* - SQL
*/
import 'fake-indexeddb/auto';
// ---- 最小 Vue mock(自包含:jest.mock 工厂不能引用外部变量) ----
jest.mock('vue', () => {
const mountQueue: Array<() => void | Promise<void>> = [];
const watchList: Array<{ sources: any[]; cb: () => void | Promise<void> }> = [];
return {
ref: (init: any) => {
const box: { value: any } = { value: init };
return box;
},
watch: (sources: any[], cb: any) => {
watchList.push({ sources, cb });
},
onMounted: (fn: any) => {
mountQueue.push(fn);
},
__mockMounted: mountQueue,
__mockWatch: watchList,
__mockReset: () => {
mountQueue.length = 0;
watchList.length = 0;
},
};
}, { virtual: true });
import { useSqlarkQuery, useSqlarkTable, useSqlarkDatabase } from '../../src/integrations/vue';
/** mock 模块内的挂载/监听队列(自包含作用域) */
const vueMock = jest.requireMock('vue') as {
__mockMounted: Array<() => void | Promise<void>>;
__mockWatch: Array<{ sources: any[]; cb: () => void | Promise<void> }>;
__mockReset: () => void;
};
beforeEach(() => {
vueMock.__mockReset();
});
/** 模拟组件挂载:执行 onMounted 注册的回调 */
async function flushMounted(): Promise<void> {
const fns = vueMock.__mockMounted.splice(0);
for (const fn of fns) {
await fn();
}
}
/** 触发 watch 回调 */
async function flushWatch(): Promise<void> {
const pairs = vueMock.__mockWatch.splice(0);
for (const { cb } of pairs) {
await cb();
}
}
/** 构造最小响应式引用(与 vue.mock 的 ref 等价) */
function makeRef<T>(init: T): { value: T } {
return { value: init };
}
describe('useSqlarkQuery', () => {
test('onMounted 时执行 SQL 查询并更新响应式 data', async () => {
const db = { query: jest.fn().mockResolvedValue([{ id: '1' }]) } as any;
const hook = useSqlarkQuery(db, 'SELECT * FROM users');
expect(hook.loading.value).toBe(true);
expect(db.query).not.toHaveBeenCalled();
await flushMounted();
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
expect(hook.loading.value).toBe(false);
expect(hook.data.value).toEqual([{ id: '1' }]);
expect(hook.error.value).toBeNull();
});
test('查询失败时设置 error', async () => {
const db = { query: jest.fn().mockRejectedValue(new Error('vue boom')) } as any;
const hook = useSqlarkQuery(db, 'SELECT * FROM users');
await flushMounted();
expect(hook.error.value).toBeInstanceOf(Error);
expect((hook.error.value as Error).message).toBe('vue boom');
expect(hook.data.value).toEqual([]);
});
test('refresh 重新执行查询', async () => {
let call = 0;
const db = { query: jest.fn().mockImplementation(async () => [{ n: ++call }]) } as any;
const hook = useSqlarkQuery(db, 'SELECT * FROM users');
await flushMounted();
expect(hook.data.value).toEqual([{ n: 1 }]);
await hook.refresh();
expect(db.query).toHaveBeenCalledTimes(2);
expect(hook.data.value).toEqual([{ n: 2 }]);
});
test('watch 注册在 sql 与 deps 上', async () => {
const db = { query: jest.fn().mockResolvedValue([]) } as any;
const sqlRef = makeRef('SELECT * FROM users');
useSqlarkQuery(db, sqlRef.value, [sqlRef]);
expect(vueMock.__mockWatch).toHaveLength(1);
expect(vueMock.__mockWatch[0].sources).toHaveLength(2); // [() => sql, ...deps]
// 模拟依赖变化触发 watch
sqlRef.value = 'SELECT * FROM orders';
await flushWatch();
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
});
});
describe('useSqlarkTable', () => {
test('合法表名执行全表查询', async () => {
const db = { query: jest.fn().mockResolvedValue([{ id: 1 }]) } as any;
const hook = useSqlarkTable(db, 'users');
await flushMounted();
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
expect(hook.data.value).toEqual([{ id: 1 }]);
});
test('非法表名抛出校验错误(SQL 注入防护)', () => {
const db = { query: jest.fn() } as any;
expect(() => useSqlarkTable(db, 'users; DELETE FROM orders')).toThrow(/Invalid table name/);
expect(() => useSqlarkTable(db, 'users--')).toThrow(/Invalid table name/);
expect(db.query).not.toHaveBeenCalled();
});
});
describe('useSqlarkDatabase', () => {
test('onMounted 创建并初始化数据库实例', async () => {
const hook = useSqlarkDatabase({ name: 'vue-hook' });
expect(hook.ready.value).toBe(false);
await flushMounted();
expect(hook.db.value).not.toBeNull();
expect(hook.ready.value).toBe(true);
expect(hook.error.value).toBeNull();
});
test('初始化失败时设置 error', async () => {
// 使用非法配置触发初始化错误(mode 未知)
const hook = useSqlarkDatabase({ name: 'vue-fail', mode: 'unknown-mode' as any });
await flushMounted();
expect(hook.db.value).toBeNull();
expect(hook.ready.value).toBe(false);
expect(hook.error.value).toBeInstanceOf(Error);
});
});
+372
View File
@@ -0,0 +1,372 @@
/**
* v0.3.0 SQL
* @module tests/sql-ext
*
* / / INSERT...SELECT / UNION / CREATE INDEX / EXISTS
*/
import 'fake-indexeddb/auto';
import { MetonaSqlark } from '../src/core';
import '../src/connection-manager';
import { parse, parseAll } from '../src/sql/parser';
async function createDb(mode: 'memory' | 'disk' | 'aria' = 'memory') {
const db = new MetonaSqlark({ name: `sql-ext-${mode}-${Date.now()}-${Math.random()}`, mode, diskEngine: 'indexeddb' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
city: { type: 'string' },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string' },
amount: { type: 'number' },
});
await db.query(`INSERT INTO users VALUES ('u1', 'Alice', 30, 'Beijing')`);
await db.query(`INSERT INTO users VALUES ('u2', 'Bob', 25, 'Shanghai')`);
await db.query(`INSERT INTO users VALUES ('u3', 'Carol', 35, 'Beijing')`);
await db.query(`INSERT INTO users VALUES ('u4', 'Dave', 28, 'Shenzhen')`);
await db.query(`INSERT INTO orders VALUES ('o1', 'u1', 100)`);
await db.query(`INSERT INTO orders VALUES ('o2', 'u1', 200)`);
await db.query(`INSERT INTO orders VALUES ('o3', 'u2', 50)`);
return db;
}
// ===================================================================
// 多语句 parseAll
// ===================================================================
describe('[v0.3.0] 多语句支持', () => {
test('parseAll 解析分号分隔的多条语句', () => {
const stmts = parseAll('SELECT * FROM a; INSERT INTO b VALUES (1); DELETE FROM c WHERE id = 1');
expect(stmts).toHaveLength(3);
expect(stmts[0].type).toBe('SELECT');
expect(stmts[1].type).toBe('INSERT');
expect(stmts[2].type).toBe('DELETE');
});
test('parseAll 忽略多余分号与尾部空语句', () => {
const stmts = parseAll(';;SELECT * FROM a;;;');
expect(stmts).toHaveLength(1);
expect(stmts[0].type).toBe('SELECT');
});
test('parseAll 支持事务语句', () => {
const stmts = parseAll('BEGIN; INSERT INTO a VALUES (1); COMMIT');
expect(stmts.map((s) => s.type)).toEqual(['BEGIN', 'INSERT', 'COMMIT']);
});
test('parse 保持单语句兼容', () => {
expect(parse('SELECT * FROM a').type).toBe('SELECT');
});
test('parseAll 语句间缺分号报错', () => {
expect(() => parseAll('SELECT * FROM a SELECT * FROM b')).toThrow(/Expected ';'/);
});
test('core.query 顺序执行多语句', async () => {
const db = await createDb();
await db.query('CREATE TABLE logs (id STRING PRIMARY KEY, msg STRING); INSERT INTO logs VALUES (\'l1\', \'hello\'); INSERT INTO logs VALUES (\'l2\', \'world\')');
const rows = await db.query('SELECT * FROM logs') as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
});
// ===================================================================
// 事务语句 BEGIN / COMMIT / ROLLBACK
// ===================================================================
describe('[v0.3.0] 事务语句', () => {
test('BEGIN + INSERT + COMMIT 持久化', async () => {
const db = await createDb();
await db.query('BEGIN');
await db.query(`INSERT INTO users VALUES ('u5', 'Eve', 22, 'Beijing')`);
const visible = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(visible).toHaveLength(5); // 事务内可见
await db.query('COMMIT');
const after = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(after).toHaveLength(5);
await db.close();
});
test('BEGIN + INSERT + ROLLBACK 回滚', async () => {
const db = await createDb();
await db.query('BEGIN');
await db.query(`INSERT INTO users VALUES ('u5', 'Eve', 22, 'Beijing')`);
await db.query('ROLLBACK');
const after = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(after).toHaveLength(4);
await db.close();
});
test('BEGIN 嵌套报错', async () => {
const db = await createDb();
await db.query('BEGIN');
await expect(db.query('BEGIN')).rejects.toThrow(/TX_ACTIVE|Transaction already/);
await db.query('ROLLBACK');
await db.close();
});
test('无事务时 COMMIT 报错', async () => {
const db = await createDb();
await expect(db.query('COMMIT')).rejects.toThrow(/TX_NONE|No active transaction/);
await db.close();
});
test('事务语句在 Aria 引擎上可用', async () => {
const db = await createDb('aria');
await db.query('BEGIN');
await db.query(`INSERT INTO users VALUES ('u5', 'Eve', 22, 'Beijing')`);
await db.query('ROLLBACK');
const after = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(after).toHaveLength(4);
await db.close();
});
});
// ===================================================================
// INSERT INTO ... SELECT
// ===================================================================
describe('[v0.3.0] INSERT INTO ... SELECT', () => {
test('INSERT SELECT 全列复制', async () => {
const db = await createDb();
await db.defineTable('users_backup', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
city: { type: 'string' },
});
await db.query('INSERT INTO users_backup SELECT * FROM users');
const rows = await db.query('SELECT * FROM users_backup') as Record<string, unknown>[];
expect(rows).toHaveLength(4);
expect(rows[0].name).toBe('Alice');
await db.close();
});
test('INSERT SELECT 带 WHERE 过滤', async () => {
const db = await createDb();
await db.defineTable('beijing_users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
city: { type: 'string' },
});
await db.query(`INSERT INTO beijing_users SELECT * FROM users WHERE city = 'Beijing'`);
const rows = await db.query('SELECT * FROM beijing_users') as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('INSERT SELECT 指定列映射', async () => {
const db = await createDb();
await db.defineTable('names', { id: { type: 'string', primaryKey: true }, n: { type: 'string' } });
await db.query('INSERT INTO names (id, n) SELECT id, name FROM users');
const rows = await db.query('SELECT * FROM names') as Record<string, unknown>[];
expect(rows).toHaveLength(4);
expect(rows[0].n).toBe('Alice');
await db.close();
});
test('INSERT SELECT 语法解析', () => {
const stmt = parse('INSERT INTO a (x, y) SELECT p, q FROM b WHERE r > 1') as any;
expect(stmt.type).toBe('INSERT');
expect(stmt.select).toBeDefined();
expect(stmt.select.type).toBe('SELECT');
expect(stmt.columns).toEqual(['x', 'y']);
});
});
// ===================================================================
// UNION / UNION ALL
// ===================================================================
describe('[v0.3.0] UNION / UNION ALL', () => {
test('UNION 去重合并', async () => {
const db = await createDb();
const rows = await db.query(`SELECT name FROM users WHERE city = 'Beijing' UNION SELECT name FROM users WHERE age < 30`) as Record<string, unknown>[];
// Beijing: Alice, CarolAlice 同时 age30 不重复);age<30: Bob, Dave
expect(rows).toHaveLength(4);
await db.close();
});
test('UNION ALL 不去重', async () => {
const db = await createDb();
const rows = await db.query(`SELECT city FROM users WHERE city = 'Beijing' UNION ALL SELECT city FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(4); // 2 + 2
await db.close();
});
test('UNION 对相同行去重', async () => {
const db = await createDb();
const rows = await db.query(`SELECT city FROM users WHERE city = 'Beijing' UNION SELECT city FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
// 左右各 2 行同值 → 去重后 1 行
expect(rows).toHaveLength(1);
expect(rows[0].city).toBe('Beijing');
await db.close();
});
test('UNION 链式(三表合并)', async () => {
const db = await createDb();
await db.query(`INSERT INTO users VALUES ('u5', 'Eve', 22, 'Beijing')`);
const rows = await db.query(`SELECT city FROM users WHERE city = 'Beijing' UNION SELECT city FROM users WHERE city = 'Shanghai' UNION SELECT city FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
// Beijing(3) + Shanghai(1) + Beijing(3 重复去重) → 2 个城市
expect(rows).toHaveLength(2);
await db.close();
});
test('UNION 语法解析', () => {
const stmt = parse('SELECT a FROM t1 UNION SELECT b FROM t2') as any;
expect(stmt.type).toBe('SELECT_UNION');
expect(stmt.all).toBeUndefined();
const stmtAll = parse('SELECT a FROM t1 UNION ALL SELECT b FROM t2') as any;
expect(stmtAll.type).toBe('SELECT_UNION');
expect(stmtAll.all).toBe(true);
});
test('UNION 在 Aria 引擎上可用', async () => {
const db = await createDb('aria');
const rows = await db.query(`SELECT city FROM users WHERE city = 'Beijing' UNION SELECT city FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(1);
await db.close();
});
});
// ===================================================================
// CREATE INDEX / DROP INDEX
// ===================================================================
describe('[v0.3.0] CREATE INDEX / DROP INDEX', () => {
test('CREATE INDEX 后索引查询可用(Memory', async () => {
const db = await createDb();
await db.query('CREATE INDEX idx_users_city ON users (city)');
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('CREATE INDEX 后索引查询可用(Aria', async () => {
const db = await createDb('aria');
await db.query('CREATE INDEX idx_users_city ON users (city)');
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('CREATE INDEX 对已有数据立即生效(索引填充)', async () => {
const db = await createDb();
await db.query('INSERT INTO users VALUES (\'u5\', \'Eve\', 22, \'Beijing\')');
await db.query('CREATE INDEX idx_users_age ON users (age)');
const rows = await db.query(`SELECT * FROM users WHERE age = 22`) as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('u5');
await db.close();
});
test('DROP INDEX 后查询回退全表扫描', async () => {
const db = await createDb();
await db.query('CREATE INDEX idx_users_city ON users (city)');
await db.query('DROP INDEX idx_users_city ON users (city)');
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('CREATE INDEX 重复创建幂等', async () => {
const db = await createDb();
await db.query('CREATE INDEX idx_users_city ON users (city)');
await db.query('CREATE INDEX idx_users_city ON users (city)'); // 不抛错
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('CREATE INDEX 不存在列报错', async () => {
const db = await createDb();
const err = await db.query('CREATE INDEX idx_bad ON users (nonexistent)').catch((e: any) => e);
expect(err).toBeDefined();
expect(err.code).toBe('COLUMN_NOT_FOUND');
await db.close();
});
test('CREATE INDEX 后插入新数据索引同步更新', async () => {
const db = await createDb();
await db.query('CREATE INDEX idx_users_city ON users (city)');
await db.query('INSERT INTO users VALUES (\'u5\', \'Eve\', 22, \'Beijing\')');
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(3);
await db.close();
});
test('DROP INDEX Aria 主键索引受保护', async () => {
const db = await createDb('aria');
await expect(db.query('DROP INDEX pk ON users (id)')).rejects.toThrow(/Cannot drop primary key/);
await db.close();
});
test('CREATE UNIQUE INDEX 语法解析', () => {
const stmt = parse('CREATE UNIQUE INDEX idx_u ON users (email)') as any;
expect(stmt.type).toBe('CREATE_INDEX');
expect(stmt.unique).toBe(true);
expect(stmt.column).toBe('email');
});
test('CREATE INDEX / DROP INDEX 语法解析', () => {
const stmt = parse('CREATE INDEX idx_c ON users (city)') as any;
expect(stmt).toMatchObject({ type: 'CREATE_INDEX', name: 'idx_c', table: 'users', column: 'city' });
const drop = parse('DROP INDEX idx_c ON users (city)') as any;
expect(drop).toMatchObject({ type: 'DROP_INDEX', name: 'idx_c', table: 'users', column: 'city' });
});
});
// ===================================================================
// EXISTS / NOT EXISTS
// ===================================================================
describe('[v0.3.0] EXISTS / NOT EXISTS', () => {
test('EXISTS 子查询为真返回行', async () => {
const db = await createDb();
const rows = await db.query(`SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)`) as Record<string, unknown>[];
// 有订单的用户:u1, u2
expect(rows).toHaveLength(2);
expect(rows.map((r) => r.id).sort()).toEqual(['u1', 'u2']);
await db.close();
});
test('NOT EXISTS 返回无关联行', async () => {
const db = await createDb();
const rows = await db.query(`SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)`) as Record<string, unknown>[];
// 无订单用户:u3, u4
expect(rows).toHaveLength(2);
expect(rows.map((r) => r.id).sort()).toEqual(['u3', 'u4']);
await db.close();
});
test('EXISTS 与 AND 组合', async () => {
const db = await createDb();
const rows = await db.query(`SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 150) AND u.city = 'Beijing'`) as Record<string, unknown>[];
// o2 金额 200 > 150 属于 u1Beijing
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('u1');
await db.close();
});
test('EXISTS 语法解析', () => {
const stmt = parse(`SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders)`) as any;
expect(stmt.where.$exists).toBeDefined();
expect(stmt.where.$exists.$subquery.type).toBe('SELECT');
const notStmt = parse(`SELECT * FROM users WHERE NOT EXISTS (SELECT 1 FROM orders)`) as any;
expect(notStmt.where.$exists).toBeDefined();
expect(notStmt.where.$exists.$negate).toBe(true);
});
test('EXISTS 在 Aria 引擎上可用', async () => {
const db = await createDb('aria');
const rows = await db.query(`SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
});
+313
View File
@@ -0,0 +1,313 @@
/**
* v0.3.1
* @module tests/sql-ext2
*
* CASE WHEN / JOIN + / WAL
*/
import 'fake-indexeddb/auto';
import { MetonaSqlark } from '../src/core';
import { parse } from '../src/sql/parser';
import { WAL, type WALStore } from '../src/engine/aria/wal/log';
import { WALRecordType } from '../src/engine/aria/types';
async function createDb(mode: 'memory' | 'aria' = 'memory') {
const db = new MetonaSqlark({ name: `sql-ext2-${mode}-${Date.now()}-${Math.random()}`, mode, diskEngine: 'indexeddb' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
city: { type: 'string' },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string' },
amount: { type: 'number' },
});
await db.query(`INSERT INTO users VALUES ('u1', 'Alice', 30, 'Beijing')`);
await db.query(`INSERT INTO users VALUES ('u2', 'Bob', 17, 'Shanghai')`);
await db.query(`INSERT INTO users VALUES ('u3', 'Carol', 42, 'Beijing')`);
await db.query(`INSERT INTO orders VALUES ('o1', 'u1', 100)`);
await db.query(`INSERT INTO orders VALUES ('o2', 'u1', 200)`);
await db.query(`INSERT INTO orders VALUES ('o3', 'u2', 50)`);
return db;
}
// ===================================================================
// CASE WHEN
// ===================================================================
describe('[v0.3.1] CASE WHEN', () => {
test('基本 CASE WHEN(单条件 + ELSE', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name, CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users`,
) as Record<string, unknown>[];
expect(rows).toHaveLength(3);
const byName = Object.fromEntries(rows.map((r) => [r.name, r]));
expect(byName['Alice'].status).toBe('adult');
expect(byName['Bob'].status).toBe('minor');
expect(byName['Carol'].status).toBe('adult');
await db.close();
});
test('多 WHEN 分支按顺序匹配', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name, CASE WHEN age < 18 THEN 'teen' WHEN age < 40 THEN 'adult' ELSE 'senior' END AS age_group FROM users`,
) as Record<string, unknown>[];
const byName = Object.fromEntries(rows.map((r) => [r.name, r]));
expect(byName['Alice'].age_group).toBe('adult');
expect(byName['Bob'].age_group).toBe('teen');
expect(byName['Carol'].age_group).toBe('senior');
await db.close();
});
test('THEN 值为列引用', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT CASE WHEN age >= 18 THEN city ELSE 'underage' END AS location FROM users`,
) as Record<string, unknown>[];
const cities = rows.map((r) => r.location);
expect(cities).toContain('Beijing');
expect(cities).toContain('underage');
await db.close();
});
test('无 ELSE 时返回 null', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name, CASE WHEN age >= 40 THEN 'senior' END AS tag FROM users`,
) as Record<string, unknown>[];
const byName = Object.fromEntries(rows.map((r) => [r.name, r]));
expect(byName['Carol'].tag).toBe('senior');
expect(byName['Alice'].tag).toBeNull();
await db.close();
});
test('字面量:数字 / 布尔 / 字符串', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name, CASE WHEN age > 20 THEN 1 ELSE 0 END AS flag, CASE WHEN city = 'Beijing' THEN true ELSE false END AS is_bj FROM users`,
) as Record<string, unknown>[];
const byName = Object.fromEntries(rows.map((r) => [r.name, r]));
expect(byName['Alice'].flag).toBe(1);
expect(byName['Bob'].flag).toBe(0);
expect(byName['Alice'].is_bj).toBe(true);
expect(byName['Bob'].is_bj).toBe(false);
await db.close();
});
test('多条件组合(AND/OR', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name, CASE WHEN age >= 18 AND city = 'Beijing' THEN 'local adult' ELSE 'other' END AS label FROM users`,
) as Record<string, unknown>[];
const byName = Object.fromEntries(rows.map((r) => [r.name, r]));
expect(byName['Alice'].label).toBe('local adult');
expect(byName['Carol'].label).toBe('local adult');
expect(byName['Bob'].label).toBe('other');
await db.close();
});
test('语法解析:CASE 列被解析为原文', () => {
const stmt = parse(`SELECT name, CASE WHEN age > 18 THEN 'x' ELSE 'y' END AS s FROM users`) as any;
expect(stmt.columns[1]).toMatch(/^CASE WHEN age > 18 THEN 'x' ELSE 'y' END AS s$/);
});
test('与普通列混合投影', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name, age, CASE WHEN age >= 18 THEN 'ok' ELSE 'no' END AS adult FROM users`,
) as Record<string, unknown>[];
expect(rows[0].name).toBeDefined();
expect(rows[0].age).toBeDefined();
expect(rows[0].adult).toBeDefined();
expect(Object.keys(rows[0])).toEqual(expect.arrayContaining(['name', 'age', 'adult']));
await db.close();
});
test('Aria 引擎可用', async () => {
const db = await createDb('aria');
const rows = await db.query(
`SELECT name, CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users`,
) as Record<string, unknown>[];
expect(rows).toHaveLength(3);
await db.close();
});
});
// ===================================================================
// JOIN + 关联子查询
// ===================================================================
describe('[v0.3.1] JOIN + 关联子查询', () => {
test('JOIN 结果上执行 EXISTS 关联过滤', async () => {
const db = await createDb();
// 有高额订单(>150)的用户
const rows = await db.query(
`SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id AND o2.amount > 150)`,
) as Record<string, unknown>[];
// 只有 u1 有 200 元订单
expect(rows).toHaveLength(2); // JOIN 展开 2 行(u1 有 2 个订单)
expect(rows.every((r) => r['u.name'] === 'Alice')).toBe(true);
await db.close();
});
test('JOIN + NOT EXISTS 排除关联行', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT DISTINCT u.name FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE NOT EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id)`,
) as Record<string, unknown>[];
// 无订单用户:u3 (Carol)JOIN 列键带表名前缀)
expect(rows.map((r) => r['u.name'])).toEqual(['Carol']);
await db.close();
});
test('JOIN + 关联子查询 + 普通条件组合', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id) AND o.amount > 50`,
) as Record<string, unknown>[];
// 有订单且订单 >50u1 的 o1(100), o2(200) + u2 的 o3(50 不满足)
expect(rows).toHaveLength(2);
expect(rows.every((r) => r['u.name'] === 'Alice')).toBe(true);
await db.close();
});
test('JOIN 子查询中的 $col 引用绑定外层行', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT o.id, o.amount FROM orders o JOIN users u ON u.id = o.user_id WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.amount > o.amount)`,
) as Record<string, unknown>[];
// 存在比自身金额更大的订单:o1(100) < o2(200) → o1 满足;o2 无更大;o3(50) < o1/o2 → 满足
const ids = rows.map((r) => r['o.id']).sort();
expect(ids).toEqual(['o1', 'o3']);
await db.close();
});
test('Aria 引擎 JOIN + EXISTS 可用', async () => {
const db = await createDb('aria');
const rows = await db.query(
`SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id AND o2.amount > 150)`,
) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
});
// ===================================================================
// WAL 批量组提交
// ===================================================================
describe('[v0.3.1] WAL 批量组提交', () => {
class MockWALStore implements WALStore {
chunks: Uint8Array[] = [];
appendCount = 0;
async append(data: Uint8Array) { this.chunks.push(data); this.appendCount++; }
async readAll(): Promise<Uint8Array> {
const total = this.chunks.reduce((s, c) => s + c.byteLength, 0);
const combined = new Uint8Array(total);
let off = 0;
for (const c of this.chunks) { combined.set(c, off); off += c.byteLength; }
return combined;
}
async truncate() { this.chunks = []; this.appendCount = 0; }
async exists() { return this.chunks.length > 0; }
}
test('appendBatch 合并为一次底层写入', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
await wal.appendBatch([
{ type: WALRecordType.INSERT, txnId: 0, tableName: 't', key: '1', data: { v: 1 } },
{ type: WALRecordType.INSERT, txnId: 0, tableName: 't', key: '2', data: { v: 2 } },
{ type: WALRecordType.INSERT, txnId: 0, tableName: 't', key: '3', data: { v: 3 } },
]);
expect(store.appendCount).toBe(1); // 3 条记录 1 次写入
});
test('appendBatch 记录可恢复', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
await wal.appendBatch([
{ type: WALRecordType.INSERT, txnId: 0, tableName: 'users', key: 'a', data: { n: 1 } },
{ type: WALRecordType.INSERT, txnId: 0, tableName: 'users', key: 'b', data: { n: 2 } },
]);
const records: { tableName: string; key: string }[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(2);
expect(records[0].key).toBe('a');
expect(records[1].key).toBe('b');
});
test('batch 模式 appendBatch 缓冲后 flush', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'batch');
await wal.appendBatch([
{ type: WALRecordType.INSERT, txnId: 0, tableName: 't', key: '1', data: {} },
{ type: WALRecordType.INSERT, txnId: 0, tableName: 't', key: '2', data: {} },
]);
expect(store.appendCount).toBe(0); // 缓冲未落盘
await wal.flush();
expect(store.appendCount).toBe(1);
});
test('append 与 appendBatch 共存', async () => {
const store = new MockWALStore();
const wal = new WAL(store, true, 'full');
await wal.append({ type: WALRecordType.BEGIN, txnId: 7, tableName: '', key: '' });
await wal.appendBatch([
{ type: WALRecordType.INSERT, txnId: 7, tableName: 't', key: '1', data: {} },
{ type: WALRecordType.INSERT, txnId: 7, tableName: 't', key: '2', data: {} },
]);
await wal.append({ type: WALRecordType.COMMIT, txnId: 7, tableName: '', key: '' });
expect(store.appendCount).toBe(3); // BEGIN + 批量(1) + COMMIT
const records: number[] = [];
await wal.recover((r) => records.push(r.type));
expect(records).toEqual([
WALRecordType.BEGIN,
WALRecordType.INSERT,
WALRecordType.INSERT,
WALRecordType.COMMIT,
]);
});
test('引擎 insert 批量写入只触发一次 WAL 落盘', async () => {
const db = new MetonaSqlark({ name: `wal-batch-${Date.now()}`, mode: 'aria', diskEngine: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } });
const engine = db.getEngine() as any;
let before = 0;
const origRead = engine.backend.read.bind(engine.backend);
// 统计 __wal_ 写入次数
const origWrite = engine.backend.write.bind(engine.backend);
let walWrites = 0;
engine.backend.write = async (key: string, data: ArrayBuffer) => {
if (key.startsWith('__wal_') && !key.startsWith('__wal_count')) walWrites++;
return origWrite(key, data);
};
void before; void origRead;
await db.table('users').insertMany([
{ id: '1', name: 'A' },
{ id: '2', name: 'B' },
{ id: '3', name: 'C' },
{ id: '4', name: 'D' },
]);
expect(walWrites).toBe(1); // 4 行 1 次 WAL 写入
const rows = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(rows).toHaveLength(4);
await db.close();
});
});
+406
View File
@@ -0,0 +1,406 @@
/**
* v0.3.2
* @module tests/sql-ext3
*
* CASE WHEN WHERE/ / JOIN /
*/
import 'fake-indexeddb/auto';
import { MetonaSqlark } from '../src/core';
async function createDb(mode: 'memory' | 'hybrid' = 'memory', extra: Record<string, unknown> = {}) {
const db = new MetonaSqlark({
name: `sql-ext3-${mode}-${Date.now()}-${Math.random()}`,
mode,
diskEngine: 'indexeddb',
...extra,
});
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
city: { type: 'string' },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string' },
amount: { type: 'number' },
});
await db.query(`INSERT INTO users VALUES ('u1', 'Alice', 30, 'Beijing')`);
await db.query(`INSERT INTO users VALUES ('u2', 'Bob', 17, 'Shanghai')`);
await db.query(`INSERT INTO users VALUES ('u3', 'Carol', 42, 'Beijing')`);
await db.query(`INSERT INTO orders VALUES ('o1', 'u1', 100)`);
await db.query(`INSERT INTO orders VALUES ('o2', 'u1', 200)`);
await db.query(`INSERT INTO orders VALUES ('o3', 'u2', 50)`);
return db;
}
// ===================================================================
// CASE WHEN 用于 WHERE
// ===================================================================
describe('[v0.3.2] CASE WHEN 用于 WHERE', () => {
test('WHERE CASE 等值比较', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name FROM users WHERE CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END = 'adult'`,
) as Record<string, unknown>[];
expect(rows.map((r) => r.name).sort()).toEqual(['Alice', 'Carol']);
await db.close();
});
test('WHERE CASE 与 AND 组合', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name FROM users WHERE CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END = 'adult' AND city = 'Beijing'`,
) as Record<string, unknown>[];
expect(rows.map((r) => r.name).sort()).toEqual(['Alice', 'Carol']);
await db.close();
});
test('WHERE CASE 数字比较', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name FROM users WHERE CASE WHEN city = 'Beijing' THEN 1 ELSE 0 END = 1`,
) as Record<string, unknown>[];
expect(rows.map((r) => r.name).sort()).toEqual(['Alice', 'Carol']);
await db.close();
});
test('WHERE NOT CASE 组合', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT name FROM users WHERE NOT (CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END = 'adult')`,
) as Record<string, unknown>[];
expect(rows.map((r) => r.name)).toEqual(['Bob']);
await db.close();
});
test('Aria 引擎 WHERE CASE 可用', async () => {
const db = await createDb('hybrid');
const rows = await db.query(
`SELECT name FROM users WHERE CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END = 'minor'`,
) as Record<string, unknown>[];
expect(rows.map((r) => r.name)).toEqual(['Bob']);
await db.close();
});
});
// ===================================================================
// CASE WHEN 用于聚合
// ===================================================================
describe('[v0.3.2] CASE WHEN 用于聚合', () => {
test('SUM(CASE WHEN...) 条件计数', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT SUM(CASE WHEN age >= 18 THEN 1 ELSE 0 END) AS adults FROM users`,
) as Record<string, unknown>[];
expect(rows[0].adults).toBe(2);
await db.close();
});
test('COUNT(CASE WHEN...) 与 AVG', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT COUNT(CASE WHEN city = 'Beijing' THEN 1 END) AS bj_count, AVG(CASE WHEN age >= 18 THEN age END) AS adult_avg FROM users`,
) as Record<string, unknown>[];
expect(rows[0].bj_count).toBe(2);
expect(rows[0].adult_avg).toBe(36); // (30 + 42) / 2
await db.close();
});
test('GROUP BY + SUM(CASE WHEN...)', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT city, SUM(CASE WHEN age >= 18 THEN 1 ELSE 0 END) AS adults FROM users GROUP BY city`,
) as Record<string, unknown>[];
const byCity = Object.fromEntries(rows.map((r) => [r.city, r.adults]));
expect(byCity['Beijing']).toBe(2); // Alice + Carol
expect(byCity['Shanghai']).toBe(0); // Bob 17 岁
await db.close();
});
test('GROUP BY + CASE 非聚合列', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT city, CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users GROUP BY city`,
) as Record<string, unknown>[];
const byCity = Object.fromEntries(rows.map((r) => [r.city, r.status]));
expect(byCity['Beijing']).toBe('adult'); // 组内第一行 Alice
expect(byCity['Shanghai']).toBe('minor');
await db.close();
});
});
// ===================================================================
// JOIN 哈希连接
// ===================================================================
describe('[v0.3.2] JOIN 哈希连接', () => {
test('INNER JOIN 主键等值走哈希连接(结果正确)', async () => {
const db = await createDb();
const rows = await db.query(
`SELECT o.id FROM orders o INNER JOIN users u ON u.id = o.user_id`,
) as Record<string, unknown>[];
// orders 全有匹配用户
expect(rows.map((r) => r['o.id']).sort()).toEqual(['o1', 'o2', 'o3']);
await db.close();
});
test('LEFT JOIN 哈希连接保留未匹配行(null 填充)', async () => {
const db = await createDb();
await db.query(`INSERT INTO users VALUES ('u9', 'Zoe', 20, 'Guangzhou')`);
const rows = await db.query(
`SELECT u.name, o.id FROM users u LEFT JOIN orders o ON o.user_id = u.id`,
) as Record<string, unknown>[];
// Zoe 无订单 → 保留(null 填充);Alice 有 2 个订单 → 展开 2 行(LEFT JOIN 语义)
expect(rows.map((r) => r['u.name']).sort()).toEqual(['Alice', 'Alice', 'Bob', 'Carol', 'Zoe']);
const zoe = rows.find((r) => r['u.name'] === 'Zoe');
expect(zoe).toBeDefined();
expect(zoe!['o.id']).toBeNull();
await db.close();
});
test('INNER JOIN 哈希连接过滤无匹配行', async () => {
const db = await createDb();
await db.query(`INSERT INTO users VALUES ('u9', 'Zoe', 20, 'Guangzhou')`);
const rows = await db.query(
`SELECT u.name FROM users u INNER JOIN orders o ON o.user_id = u.id`,
) as Record<string, unknown>[];
// Alice 2 个订单 → 2 行;Bob 1 行;Carol 无订单被过滤
expect(rows.map((r) => r['u.name']).sort()).toEqual(['Alice', 'Alice', 'Bob']);
await db.close();
});
test('哈希连接一次 $in 查询(不再全表拉取)', async () => {
const db = await createDb();
const engine = db.getEngine() as any;
let rightTableFinds = 0;
const origFind = engine.find.bind(engine);
engine.find = async (table: string, query: any) => {
if (table === 'orders') {
rightTableFinds++;
if (query.where?.user_id?.$in) {
// 哈希连接:$in 一次查询
expect(query.where.user_id.$in).toEqual(expect.arrayContaining(['u1', 'u2']));
}
}
return origFind(table, query);
};
await db.query(`SELECT u.name FROM users u INNER JOIN orders o ON o.user_id = u.id`);
// orders.user_id 无索引 → 哈希回退;仍应恰有一次右表查询
expect(rightTableFinds).toBe(1);
await db.close();
});
test('哈希连接不适用于非索引右列(回退嵌套循环)', async () => {
const db = await createDb();
// orders.user_id 无索引 → 回退;结果仍正确
const rows = await db.query(
`SELECT o.id FROM orders o INNER JOIN users u ON u.id = o.user_id`,
) as Record<string, unknown>[];
expect(rows).toHaveLength(3);
await db.close();
});
});
// ===================================================================
// 多标签页同步
// ===================================================================
describe('[v0.3.2] 多标签页同步', () => {
// BroadcastChannel mock:模拟同源标签页间消息传递
class MockBroadcastChannel {
static instances: MockBroadcastChannel[] = [];
name: string;
onmessage: ((event: { data: unknown }) => void) | null = null;
closed = false;
constructor(name: string) {
this.name = name;
MockBroadcastChannel.instances.push(this);
}
postMessage(data: unknown): void {
if (this.closed) return;
for (const other of MockBroadcastChannel.instances) {
if (other !== this && other.name === this.name && !other.closed && other.onmessage) {
other.onmessage({ data });
}
}
}
close(): void {
this.closed = true;
}
static reset(): void {
MockBroadcastChannel.instances = [];
}
}
const origBC = (globalThis as any).BroadcastChannel;
beforeAll(() => {
(globalThis as any).BroadcastChannel = MockBroadcastChannel;
});
afterAll(() => {
(globalThis as any).BroadcastChannel = origBC;
});
beforeEach(() => {
MockBroadcastChannel.reset();
});
test('SQL 写语句广播表变更,其他标签页订阅收到 external 事件', async () => {
// 先建表(DDL 版本升级会触发其他标签页 onversionchange 关闭连接,故先建表再开第二连接)
const setup = new MetonaSqlark({ name: 'mt-a', mode: 'hybrid', diskEngine: 'indexeddb' });
await setup.init();
await setup.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } });
await setup.close();
const dbA = new MetonaSqlark({ name: 'mt-a', version: 2, mode: 'hybrid', diskEngine: 'indexeddb', multiTabSync: true });
const dbB = new MetonaSqlark({ name: 'mt-a', version: 2, mode: 'hybrid', diskEngine: 'indexeddb', multiTabSync: true });
await dbA.init();
await dbB.init();
const events: { type: string; table?: string }[] = [];
dbB.subscribe('t', (e) => events.push(e));
await dbA.query(`INSERT INTO t VALUES ('1', 10)`);
// 等待广播送达(同步 mock 已即时)
expect(events.length).toBeGreaterThan(0);
expect(events[0].type).toBe('external');
expect(events[0].table).toBe('t');
await dbA.close();
await dbB.close();
});
test('Hybrid 标签页收到广播后内存重载(读到其他标签页的新数据)', async () => {
const setup = new MetonaSqlark({ name: 'mt-b', mode: 'hybrid', diskEngine: 'indexeddb' });
await setup.init();
await setup.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } });
await setup.close();
const dbA = new MetonaSqlark({ name: 'mt-b', version: 2, mode: 'hybrid', diskEngine: 'indexeddb', multiTabSync: true });
const dbB = new MetonaSqlark({ name: 'mt-b', version: 2, mode: 'hybrid', diskEngine: 'indexeddb', multiTabSync: true });
await dbA.init();
await dbB.init();
// B 订阅外部变更后等待 reload 完成
let reloadDone: Promise<void> = Promise.resolve();
dbB.subscribe('t', async () => {
reloadDone = reloadDone.then(async () => {
// Hybrid reload 由 onmessage 触发(异步),订阅回调后再等一拍
await new Promise((r) => setTimeout(r, 30));
});
});
await dbA.query(`INSERT INTO t VALUES ('1', 100)`);
await new Promise((r) => setTimeout(r, 80));
const rows = await dbB.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].v).toBe(100);
await dbA.close();
await dbB.close();
});
test('未启用 multiTabSync 不广播', async () => {
const setup = new MetonaSqlark({ name: 'mt-c', mode: 'hybrid', diskEngine: 'indexeddb' });
await setup.init();
await setup.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } });
await setup.close();
const dbA = new MetonaSqlark({ name: 'mt-c', version: 2, mode: 'hybrid', diskEngine: 'indexeddb' });
const dbB = new MetonaSqlark({ name: 'mt-c', version: 2, mode: 'hybrid', diskEngine: 'indexeddb', multiTabSync: true });
await dbA.init();
await dbB.init();
const events: unknown[] = [];
dbB.subscribe('t', (e) => events.push(e));
await dbA.query(`INSERT INTO t VALUES ('1', 10)`);
await new Promise((r) => setTimeout(r, 30));
expect(events).toHaveLength(0); // dbA 未启用 → 无广播
await dbA.close();
await dbB.close();
});
test('Table API 写入也广播', async () => {
const setup = new MetonaSqlark({ name: 'mt-d', mode: 'hybrid', diskEngine: 'indexeddb' });
await setup.init();
await setup.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } });
await setup.close();
const dbA = new MetonaSqlark({ name: 'mt-d', version: 2, mode: 'hybrid', diskEngine: 'indexeddb', multiTabSync: true });
const dbB = new MetonaSqlark({ name: 'mt-d', version: 2, mode: 'hybrid', diskEngine: 'indexeddb', multiTabSync: true });
await dbA.init();
await dbB.init();
const events: unknown[] = [];
dbB.subscribe('t', (e) => events.push(e));
await dbA.table('t').insert({ id: '1', v: 10 });
expect(events.length).toBeGreaterThan(0);
await dbA.close();
await dbB.close();
});
});
// ===================================================================
// 回归:IndexedDB reopen 后 schema 持久化(v0.3.2 修复)
// ===================================================================
describe('[v0.3.2] IndexedDB reopen schema 持久化', () => {
test('close 后重新 open 表结构与数据完整', async () => {
const setup = new MetonaSqlark({ name: 'reopen-a', mode: 'hybrid', diskEngine: 'indexeddb' });
await setup.init();
await setup.defineTable('t', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
v: { type: 'number' },
});
await setup.query(`INSERT INTO t VALUES ('1', 'Alice', 42)`);
await setup.close();
// 重新打开(模拟页面刷新)
const db = new MetonaSqlark({ name: 'reopen-a', version: 2, mode: 'hybrid', diskEngine: 'indexeddb' });
await db.init();
const schema = await db.getEngine().getTableSchema('t');
expect(schema?.columns.v).toBeDefined(); // 持久化 schema 保留完整列
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe('Alice');
expect(rows[0].v).toBe(42);
// 重新打开后仍可写入并校验类型
await db.query(`INSERT INTO t VALUES ('2', 'Bob', 30)`);
await expect(db.query(`INSERT INTO t VALUES ('3', 'Bad', 'not-a-number')`)).rejects.toBeDefined();
await db.close();
});
test('reopen 后 UPDATE 全列生效', async () => {
const setup = new MetonaSqlark({ name: 'reopen-b', mode: 'hybrid', diskEngine: 'indexeddb' });
await setup.init();
await setup.defineTable('t', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
v: { type: 'number' },
});
await setup.query(`INSERT INTO t VALUES ('1', 'Alice', 42)`);
await setup.close();
const db = new MetonaSqlark({ name: 'reopen-b', version: 2, mode: 'hybrid', diskEngine: 'indexeddb' });
await db.init();
await db.query(`UPDATE t SET name = 'Renamed', v = 99 WHERE id = '1'`);
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows[0].name).toBe('Renamed');
expect(rows[0].v).toBe(99);
await db.close();
});
});
+99 -99
View File
@@ -1,99 +1,99 @@
/**
* utils.ts
*/
import {
generateId,
escapeHTML,
debounce,
throttle,
deepMerge,
isBrowser,
} from '../src/utils';
describe('generateId', () => {
test('返回字符串', () => {
expect(typeof generateId()).toBe('string');
});
test('多次调用产生不同值', () => {
const ids = new Set<string>();
for (let i = 0; i < 100; i++) ids.add(generateId());
expect(ids.size).toBe(100);
});
});
describe('escapeHTML', () => {
test('转义 < > &', () => {
const out = escapeHTML('<a>&b</a>');
expect(out).toContain('&lt;');
expect(out).toContain('&gt;');
expect(out).toContain('&amp;');
});
test('null/undefined 返回空字符串', () => {
expect(escapeHTML(null)).toBe('');
expect(escapeHTML(undefined)).toBe('');
});
});
describe('debounce', () => {
test('延迟执行', (done) => {
let called = 0;
const fn = debounce(() => { called++; }, 20);
fn();
expect(called).toBe(0);
setTimeout(() => {
expect(called).toBe(1);
done();
}, 50);
});
test('多次调用只执行最后一次', (done) => {
let result = 0;
const fn = debounce((v: number) => { result = v; }, 20);
fn(1);
fn(2);
fn(3);
setTimeout(() => {
expect(result).toBe(3);
done();
}, 50);
});
});
describe('throttle', () => {
test('首次立即执行', () => {
let called = 0;
const fn = throttle(() => { called++; }, 50);
fn();
expect(called).toBe(1);
});
test('限流期内不重复执行', () => {
let called = 0;
const fn = throttle(() => { called++; }, 50);
fn();
fn();
fn();
expect(called).toBe(1);
});
});
describe('deepMerge', () => {
test('浅层合并', () => {
const out = deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 });
expect(out).toEqual({ a: 1, b: 3, c: 4 });
});
test('深层对象合并', () => {
const out = deepMerge({ obj: { x: 1 } }, { obj: { y: 2 } });
expect(out).toEqual({ obj: { x: 1, y: 2 } });
});
});
describe('isBrowser', () => {
test('jsdom 环境下返回 true', () => {
expect(isBrowser()).toBe(true);
});
});
/**
* utils.ts
*/
import {
generateId,
escapeHTML,
debounce,
throttle,
deepMerge,
isBrowser,
} from '../src/utils';
describe('generateId', () => {
test('返回字符串', () => {
expect(typeof generateId()).toBe('string');
});
test('多次调用产生不同值', () => {
const ids = new Set<string>();
for (let i = 0; i < 100; i++) ids.add(generateId());
expect(ids.size).toBe(100);
});
});
describe('escapeHTML', () => {
test('转义 < > &', () => {
const out = escapeHTML('<a>&b</a>');
expect(out).toContain('&lt;');
expect(out).toContain('&gt;');
expect(out).toContain('&amp;');
});
test('null/undefined 返回空字符串', () => {
expect(escapeHTML(null)).toBe('');
expect(escapeHTML(undefined)).toBe('');
});
});
describe('debounce', () => {
test('延迟执行', (done) => {
let called = 0;
const fn = debounce(() => { called++; }, 20);
fn();
expect(called).toBe(0);
setTimeout(() => {
expect(called).toBe(1);
done();
}, 50);
});
test('多次调用只执行最后一次', (done) => {
let result = 0;
const fn = debounce((v: number) => { result = v; }, 20);
fn(1);
fn(2);
fn(3);
setTimeout(() => {
expect(result).toBe(3);
done();
}, 50);
});
});
describe('throttle', () => {
test('首次立即执行', () => {
let called = 0;
const fn = throttle(() => { called++; }, 50);
fn();
expect(called).toBe(1);
});
test('限流期内不重复执行', () => {
let called = 0;
const fn = throttle(() => { called++; }, 50);
fn();
fn();
fn();
expect(called).toBe(1);
});
});
describe('deepMerge', () => {
test('浅层合并', () => {
const out = deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 });
expect(out).toEqual({ a: 1, b: 3, c: 4 });
});
test('深层对象合并', () => {
const out = deepMerge({ obj: { x: 1 } }, { obj: { y: 2 } });
expect(out).toEqual({ obj: { x: 1, y: 2 } });
});
});
describe('isBrowser', () => {
test('jsdom 环境下返回 true', () => {
expect(isBrowser()).toBe(true);
});
});
+361 -374
View File
@@ -1,374 +1,361 @@
/**
* v0.2.5
* P0/P1/P2
*/
import { VERSION } from '../src/constants';
import { MetonaSqlark } from '../src/core';
import { AriaEngine } from '../src/engine/aria/index';
import { MemoryEngine } from '../src/engine/memory';
import { parse } from '../src/sql/parser';
import { QueryExecutor } from '../src/query/executor';
import { WAL } from '../src/engine/aria/wal/log';
import { SSTableReader } from '../src/engine/aria/index/sstable';
import { BloomFilter } from '../src/engine/aria/index/bloom';
import { CryptoManager } from '../src/engine/aria/crypto';
import { PluginManager } from '../src/plugin/index';
import type { SSTableMeta } from '../src/engine/aria/types';
import type { MetonaPlugin } from '../src/constants';
import { createSchema } from '../src/table/schema';
// ---------------------------------------------------------------------------
// P0-1: 版本号统一
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-1: 版本号统一', () => {
test('VERSION 常量为 0.2.5', () => {
expect(VERSION).toBe('0.2.5');
});
});
// ---------------------------------------------------------------------------
// P0-2: AriaEngine OPFS 后端映射修复
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-2: AriaEngine OPFS 后端映射', () => {
test('mode=aria + diskEngine=opfs 时应使用 opfs 后端', () => {
const db = new MetonaSqlark({ name: 'test-opfs-map', mode: 'aria', diskEngine: 'opfs' });
// 不实际 open(需要浏览器环境),只验证 createEngine 逻辑
// 通过 getEngine 在 init 后检查
expect(db).toBeDefined();
});
test('mode=aria + diskEngine=indexeddb 时应使用 indexeddb 后端', () => {
const db = new MetonaSqlark({ name: 'test-idb-map', mode: 'aria', diskEngine: 'indexeddb' });
expect(db).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// P0-3: _onError 接入执行路径
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-3: _onError 接入执行路径', () => {
test('query 失败时调用 onError 回调', async () => {
const errors: Error[] = [];
const db = new MetonaSqlark({
name: 'test-onerror',
mode: 'memory',
onError: (e) => errors.push(e),
});
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
});
// 故意执行不存在的表查询
await expect(db.query('SELECT * FROM nonexistent')).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0);
await db.close();
});
test('defineTable 失败时调用 onError', async () => {
const errors: Error[] = [];
const db = new MetonaSqlark({
name: 'test-onerror2',
mode: 'memory',
onError: (e) => errors.push(e),
});
await db.init();
await db.defineTable('dup', {
id: { type: 'string', primaryKey: true },
});
// 重复创建
await expect(db.defineTable('dup', {
id: { type: 'string', primaryKey: true },
})).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0);
await db.close();
});
});
// ---------------------------------------------------------------------------
// P0-4: maxRowsPerQuery 生效
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-4: maxRowsPerQuery 生效', () => {
test('结果集被截断为 maxRowsPerQuery', async () => {
const engine = new MemoryEngine();
await engine.open('test-maxrows', 1);
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
val: { type: 'number' },
}));
// 插入 100 行
for (let i = 0; i < 100; i++) {
await engine.insert('items', [{ id: `item${i}`, val: i }]);
}
const executor = new QueryExecutor(engine, 10); // maxRowsPerQuery=10
const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(10); // 截断为 10 行
await engine.close();
});
test('maxRowsPerQuery=0 表示不限制', async () => {
const engine = new MemoryEngine();
await engine.open('test-nolimit', 1);
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
}));
for (let i = 0; i < 50; i++) {
await engine.insert('items', [{ id: `i${i}` }]);
}
const executor = new QueryExecutor(engine, 0);
const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(50);
await engine.close();
});
});
// ---------------------------------------------------------------------------
// P0-5: WAL full 模式真正同步
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-5: WAL full 模式同步', () => {
test('append 在 full 模式下是 async 且可 await', async () => {
let appendCount = 0;
const wal = new WAL({
append: async (_data: Uint8Array) => { appendCount++; },
readAll: async () => new Uint8Array(0),
truncate: async () => {},
exists: async () => false,
}, true, 'full');
// append 现在返回 Promise
await wal.append({
type: 1, // INSERT
txnId: 0,
tableName: 'test',
key: 'k1',
data: { v: 1 },
} as any);
expect(appendCount).toBe(1);
});
});
// ---------------------------------------------------------------------------
// P0-6: PluginManager.install 传 db 实例
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-6: PluginManager.install 传 db 实例', () => {
test('register 传 db 给 install', () => {
let receivedDb: unknown = null;
const plugin: MetonaPlugin = {
name: 'test-plugin',
install: (db) => { receivedDb = db; },
destroy: () => {},
};
const pm = new PluginManager();
const fakeDb = { name: 'fake' };
pm.register(plugin, fakeDb);
expect(receivedDb).toBe(fakeDb);
});
});
// ---------------------------------------------------------------------------
// P1-7: SSTableReader 二分查找统一
// ---------------------------------------------------------------------------
describe('[v0.2.5] P1-7: SSTableReader 二分查找', () => {
test('rangeScan 使用二分查找正确定位', () => {
// 构建一个 SSTable 手工
const { SSTableBuilder } = require('../src/engine/aria/index/sstable_builder');
const builder = new SSTableBuilder(4096);
// 添加足够多的条目以形成多个 block
for (let i = 0; i < 100; i++) {
const key = `key${String(i).padStart(5, '0')}`;
builder.add(key, { data: `value${i}` });
}
const { sstableData } = builder.build();
const meta: SSTableMeta = {
id: 1,
level: 0,
minKey: 'key00000',
maxKey: 'key00099',
blockCount: 1,
totalSize: sstableData.byteLength,
bloomData: null,
};
const reader = new SSTableReader(sstableData, meta);
// 精确查找
const result = reader.get('key00050');
expect(result).not.toBeNull();
expect((result as any).data).toBe('value50');
// 范围扫描
const collected: string[] = [];
reader.rangeScan('key00010', 'key00020', (k) => collected.push(k));
expect(collected.length).toBeGreaterThan(0);
expect(collected[0]).toBe('key00010');
});
});
// ---------------------------------------------------------------------------
// P1-8: SQL 注入防护 — 表名校验
// ---------------------------------------------------------------------------
describe('[v0.2.5] P1-8: SQL 注入防护', () => {
test('表名校验正则表达式正确', () => {
// 验证正则逻辑本身
const validName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
expect(validName.test('users')).toBe(true);
expect(validName.test('user_table')).toBe(true);
expect(validName.test('_private')).toBe(true);
expect(validName.test('Table1')).toBe(true);
// 非法表名
expect(validName.test('users; DROP TABLE')).toBe(false);
expect(validName.test('1table')).toBe(false);
expect(validName.test('user.name')).toBe(false);
expect(validName.test('user name')).toBe(false);
expect(validName.test("'; DROP TABLE users; --")).toBe(false);
});
});
// ---------------------------------------------------------------------------
// P1-9: crypto 实例化
// ---------------------------------------------------------------------------
describe('[v0.2.5] P1-9: CryptoManager 实例化', () => {
test('CryptoManager 可以独立实例化', () => {
const cm1 = new CryptoManager();
const cm2 = new CryptoManager();
expect(cm1.enabled).toBe(false);
expect(cm2.enabled).toBe(false);
// 两个实例互不影响
expect(cm1).not.toBe(cm2);
});
});
// ---------------------------------------------------------------------------
// P2-14: ALTER TABLE 语法
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-14: ALTER TABLE', () => {
test('解析 ALTER TABLE ADD COLUMN', () => {
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE');
expect(stmt.type).toBe('ALTER_TABLE');
expect((stmt as any).name).toBe('users');
expect((stmt as any).action).toBe('ADD');
expect((stmt as any).column.name).toBe('email');
});
test('解析 ALTER TABLE DROP COLUMN', () => {
const stmt = parse('ALTER TABLE users DROP COLUMN email');
expect(stmt.type).toBe('ALTER_TABLE');
expect((stmt as any).action).toBe('DROP');
expect((stmt as any).column.name).toBe('email');
});
test('执行 ALTER TABLE ADD COLUMN', async () => {
const engine = new MemoryEngine();
await engine.open('test-alter', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
const executor = new QueryExecutor(engine);
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
await executor.execute(stmt);
const schema = await engine.getTableSchema('users');
expect(schema!.columns.email).toBeDefined();
await engine.close();
});
test('执行 ALTER TABLE DROP COLUMN', async () => {
const engine = new MemoryEngine();
await engine.open('test-alter-drop', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
email: { type: 'string' },
}));
const executor = new QueryExecutor(engine);
const stmt = parse('ALTER TABLE users DROP COLUMN email');
await executor.execute(stmt);
const schema = await engine.getTableSchema('users');
expect(schema!.columns.email).toBeUndefined();
await engine.close();
});
});
// ---------------------------------------------------------------------------
// P2-15: TRUNCATE TABLE 语法
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-15: TRUNCATE TABLE', () => {
test('解析 TRUNCATE TABLE', () => {
const stmt = parse('TRUNCATE TABLE users');
expect(stmt.type).toBe('TRUNCATE_TABLE');
expect((stmt as any).name).toBe('users');
});
test('执行 TRUNCATE TABLE 清空数据', async () => {
const engine = new MemoryEngine();
await engine.open('test-truncate', 1);
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
}));
await engine.insert('items', [
{ id: 'a' }, { id: 'b' }, { id: 'c' },
]);
const executor = new QueryExecutor(engine);
const stmt = parse('TRUNCATE TABLE items');
await executor.execute(stmt);
const rows = await engine.find('items', { table: 'items' });
expect(rows.length).toBe(0);
await engine.close();
});
});
// ---------------------------------------------------------------------------
// P2-12: WAL 大小阈值接入 checkpoint
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-12: WAL 大小阈值', () => {
test('CheckpointManager 接收 walSizeThreshold 参数', () => {
const { CheckpointManager } = require('../src/engine/aria/wal/checkpoint');
const fakeLsm = { flush: async () => {} };
const fakeWal = { flush: async () => {}, checkpoint: async () => {}, getBufferedCount: () => 0 };
const cm = new CheckpointManager(fakeLsm, fakeWal, null, 1000, 1024);
expect(cm).toBeDefined();
expect(cm.getOpCount()).toBe(0);
});
});
// ---------------------------------------------------------------------------
// P2-13: compactLevelSync 接口公开化
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-13: compactLevel public', () => {
test('LSM.compactLevel 是 public 方法', () => {
const { LSM } = require('../src/engine/aria/index/lsm');
const lsm = new LSM({
sstableStore: {
save: async () => {}, load: async () => null, delete: async () => {},
allocateId: async () => 1, listMeta: async () => [], saveMeta: async () => {}, deleteMeta: async () => {},
},
});
expect(typeof lsm.compactLevel).toBe('function');
});
});
/**
* v0.2.5
* P0/P1/P2
*/
import { VERSION } from '../src/constants';
import { MetonaSqlark } from '../src/core';
import { AriaEngine } from '../src/engine/aria/index';
import { MemoryEngine } from '../src/engine/memory';
import { parse } from '../src/sql/parser';
import { QueryExecutor } from '../src/query/executor';
import { WAL } from '../src/engine/aria/wal/log';
import { SSTableReader } from '../src/engine/aria/index/sstable';
import { BloomFilter } from '../src/engine/aria/index/bloom';
import { CryptoManager } from '../src/engine/aria/crypto';
import { PluginManager } from '../src/plugin/index';
import type { SSTableMeta } from '../src/engine/aria/types';
import type { MetonaPlugin } from '../src/constants';
import { createSchema } from '../src/table/schema';
// ---------------------------------------------------------------------------
// P0-1: 版本号统一
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-1: 版本号统一', () => {
test('VERSION 常量为当前版本(0.3.2', () => {
expect(VERSION).toBe('0.3.2');
});
});
// ---------------------------------------------------------------------------
// P0-2: AriaEngine OPFS 后端映射修复
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-2: AriaEngine OPFS 后端映射', () => {
test('mode=aria + diskEngine=opfs 时应使用 opfs 后端', () => {
const db = new MetonaSqlark({ name: 'test-opfs-map', mode: 'aria', diskEngine: 'opfs' });
// 不实际 open(需要浏览器环境),只验证 createEngine 逻辑
// 通过 getEngine 在 init 后检查
expect(db).toBeDefined();
});
test('mode=aria + diskEngine=indexeddb 时应使用 indexeddb 后端', () => {
const db = new MetonaSqlark({ name: 'test-idb-map', mode: 'aria', diskEngine: 'indexeddb' });
expect(db).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// P0-3: _onError 接入执行路径
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-3: _onError 接入执行路径', () => {
test('query 失败时调用 onError 回调', async () => {
const errors: Error[] = [];
const db = new MetonaSqlark({
name: 'test-onerror',
mode: 'memory',
onError: (e) => errors.push(e),
});
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
});
// 故意执行不存在的表查询
await expect(db.query('SELECT * FROM nonexistent')).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0);
await db.close();
});
test('defineTable 失败时调用 onError', async () => {
const errors: Error[] = [];
const db = new MetonaSqlark({
name: 'test-onerror2',
mode: 'memory',
onError: (e) => errors.push(e),
});
await db.init();
await db.defineTable('dup', {
id: { type: 'string', primaryKey: true },
});
// 重复创建
await expect(db.defineTable('dup', {
id: { type: 'string', primaryKey: true },
})).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0);
await db.close();
});
});
// ---------------------------------------------------------------------------
// P0-4: maxRowsPerQuery 生效
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-4: maxRowsPerQuery 生效', () => {
test('结果集被截断为 maxRowsPerQuery', async () => {
const engine = new MemoryEngine();
await engine.open('test-maxrows', 1);
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
val: { type: 'number' },
}));
// 插入 100 行
for (let i = 0; i < 100; i++) {
await engine.insert('items', [{ id: `item${i}`, val: i }]);
}
const executor = new QueryExecutor(engine, 10); // maxRowsPerQuery=10
const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(10); // 截断为 10 行
await engine.close();
});
test('maxRowsPerQuery=0 表示不限制', async () => {
const engine = new MemoryEngine();
await engine.open('test-nolimit', 1);
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
}));
for (let i = 0; i < 50; i++) {
await engine.insert('items', [{ id: `i${i}` }]);
}
const executor = new QueryExecutor(engine, 0);
const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(50);
await engine.close();
});
});
// ---------------------------------------------------------------------------
// P0-5: WAL full 模式真正同步
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-5: WAL full 模式同步', () => {
test('append 在 full 模式下是 async 且可 await', async () => {
let appendCount = 0;
const wal = new WAL({
append: async (_data: Uint8Array) => { appendCount++; },
readAll: async () => new Uint8Array(0),
truncate: async () => {},
exists: async () => false,
}, true, 'full');
// append 现在返回 Promise
await wal.append({
type: 1, // INSERT
txnId: 0,
tableName: 'test',
key: 'k1',
data: { v: 1 },
} as any);
expect(appendCount).toBe(1);
});
});
// ---------------------------------------------------------------------------
// P0-6: PluginManager.install 传 db 实例
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-6: PluginManager.install 传 db 实例', () => {
test('register 传 db 给 install', () => {
let receivedDb: unknown = null;
const plugin: MetonaPlugin = {
name: 'test-plugin',
install: (db) => { receivedDb = db; },
destroy: () => {},
};
const pm = new PluginManager();
const fakeDb = { name: 'fake' };
pm.register(plugin, fakeDb);
expect(receivedDb).toBe(fakeDb);
});
});
// ---------------------------------------------------------------------------
// P1-7: SSTableReader 二分查找统一
// ---------------------------------------------------------------------------
describe('[v0.2.5] P1-7: SSTableReader 二分查找', () => {
test('rangeScan 使用二分查找正确定位', () => {
// 构建一个 SSTable 手工
const { SSTableBuilder } = require('../src/engine/aria/index/sstable_builder');
const builder = new SSTableBuilder(4096);
// 添加足够多的条目以形成多个 block
for (let i = 0; i < 100; i++) {
const key = `key${String(i).padStart(5, '0')}`;
builder.add(key, { data: `value${i}` });
}
const { sstableData } = builder.build();
const meta: SSTableMeta = {
id: 1,
level: 0,
minKey: 'key00000',
maxKey: 'key00099',
blockCount: 1,
totalSize: sstableData.byteLength,
bloomData: null,
};
const reader = new SSTableReader(sstableData, meta);
// 精确查找
const result = reader.get('key00050');
expect(result).not.toBeNull();
expect((result as any).data).toBe('value50');
// 范围扫描
const collected: string[] = [];
reader.rangeScan('key00010', 'key00020', (k) => collected.push(k));
expect(collected.length).toBeGreaterThan(0);
expect(collected[0]).toBe('key00010');
});
});
// ---------------------------------------------------------------------------
// P1-8: SQL 注入防护 — 表名校验
// 真实覆盖位置:tests/integrations/react.test.tsuseTable
// tests/integrations/vue.test.tsuseSqlarkTable
// 通过真实 hooks 调用触发 validateTableName,验证非法表名抛错。
// 注:v0.2.6 移除此处"复制正则自测"的伪测试(未触达真实代码)。
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// P1-9: crypto 实例化
// ---------------------------------------------------------------------------
describe('[v0.2.5] P1-9: CryptoManager 实例化', () => {
test('CryptoManager 可以独立实例化', () => {
const cm1 = new CryptoManager();
const cm2 = new CryptoManager();
expect(cm1.enabled).toBe(false);
expect(cm2.enabled).toBe(false);
// 两个实例互不影响
expect(cm1).not.toBe(cm2);
});
});
// ---------------------------------------------------------------------------
// P2-14: ALTER TABLE 语法
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-14: ALTER TABLE', () => {
test('解析 ALTER TABLE ADD COLUMN', () => {
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE');
expect(stmt.type).toBe('ALTER_TABLE');
expect((stmt as any).name).toBe('users');
expect((stmt as any).action).toBe('ADD');
expect((stmt as any).column.name).toBe('email');
});
test('解析 ALTER TABLE DROP COLUMN', () => {
const stmt = parse('ALTER TABLE users DROP COLUMN email');
expect(stmt.type).toBe('ALTER_TABLE');
expect((stmt as any).action).toBe('DROP');
expect((stmt as any).column.name).toBe('email');
});
test('执行 ALTER TABLE ADD COLUMN', async () => {
const engine = new MemoryEngine();
await engine.open('test-alter', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
const executor = new QueryExecutor(engine);
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
await executor.execute(stmt);
const schema = await engine.getTableSchema('users');
expect(schema!.columns.email).toBeDefined();
await engine.close();
});
test('执行 ALTER TABLE DROP COLUMN', async () => {
const engine = new MemoryEngine();
await engine.open('test-alter-drop', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
email: { type: 'string' },
}));
const executor = new QueryExecutor(engine);
const stmt = parse('ALTER TABLE users DROP COLUMN email');
await executor.execute(stmt);
const schema = await engine.getTableSchema('users');
expect(schema!.columns.email).toBeUndefined();
await engine.close();
});
});
// ---------------------------------------------------------------------------
// P2-15: TRUNCATE TABLE 语法
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-15: TRUNCATE TABLE', () => {
test('解析 TRUNCATE TABLE', () => {
const stmt = parse('TRUNCATE TABLE users');
expect(stmt.type).toBe('TRUNCATE_TABLE');
expect((stmt as any).name).toBe('users');
});
test('执行 TRUNCATE TABLE 清空数据', async () => {
const engine = new MemoryEngine();
await engine.open('test-truncate', 1);
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
}));
await engine.insert('items', [
{ id: 'a' }, { id: 'b' }, { id: 'c' },
]);
const executor = new QueryExecutor(engine);
const stmt = parse('TRUNCATE TABLE items');
await executor.execute(stmt);
const rows = await engine.find('items', { table: 'items' });
expect(rows.length).toBe(0);
await engine.close();
});
});
// ---------------------------------------------------------------------------
// P2-12: WAL 大小阈值接入 checkpoint
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-12: WAL 大小阈值', () => {
test('CheckpointManager 接收 walSizeThreshold 参数', () => {
const { CheckpointManager } = require('../src/engine/aria/wal/checkpoint');
const fakeLsm = { flush: async () => {} };
const fakeWal = { flush: async () => {}, checkpoint: async () => {}, getBufferedCount: () => 0 };
const cm = new CheckpointManager(fakeLsm, fakeWal, null, 1000, 1024);
expect(cm).toBeDefined();
expect(cm.getOpCount()).toBe(0);
});
});
// ---------------------------------------------------------------------------
// P2-13: compactLevelSync 接口公开化
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-13: compactLevel public', () => {
test('LSM.compactLevel 是 public 方法', () => {
const { LSM } = require('../src/engine/aria/index/lsm');
const lsm = new LSM({
sstableStore: {
save: async () => {}, load: async () => null, delete: async () => {},
allocateId: async () => 1, listMeta: async () => [], saveMeta: async () => {}, deleteMeta: async () => {},
},
});
expect(typeof lsm.compactLevel).toBe('function');
});
});
+24 -24
View File
@@ -1,24 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "node",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": false,
"declarationMap": false,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"baseUrl": ".",
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests"]
}
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "node",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": false,
"declarationMap": false,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"baseUrl": ".",
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests"]
}